opentui(ts): normalize formatting with prettier
Run `prettier --write src` over ui-tui-opentui-v2 to normalize formatting to the repo .prettierrc (no semicolons, single quotes, width 120, arrowParens avoid, trailingComma none). This worktree had pre-existing prettier-version divergences across 19 files; normalizing is correct. No behavior changes — formatting only. The gate (type-check → lint → bun test) stays green.
This commit is contained in:
parent
bc79644f16
commit
4cb9aa6664
@ -104,7 +104,10 @@ export class RawGatewayClient {
|
|||||||
// A recovery-respawn re-enters start(), so this re-arms per respawn — desired.
|
// A recovery-respawn re-enters start(), so this re-arms per respawn — desired.
|
||||||
this.startupTimer = setTimeout(() => {
|
this.startupTimer = setTimeout(() => {
|
||||||
this.startupTimer = undefined
|
this.startupTimer = undefined
|
||||||
this.onEvent({ type: 'gateway.start_timeout', payload: { message: `no gateway.ready within ${STARTUP_TIMEOUT_MS}ms` } })
|
this.onEvent({
|
||||||
|
type: 'gateway.start_timeout',
|
||||||
|
payload: { message: `no gateway.ready within ${STARTUP_TIMEOUT_MS}ms` }
|
||||||
|
})
|
||||||
}, STARTUP_TIMEOUT_MS)
|
}, STARTUP_TIMEOUT_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -244,7 +244,9 @@ export const run = Effect.fn('Tui.run')(function* (input: TuiInput) {
|
|||||||
gateway
|
gateway
|
||||||
.request('session.interrupt', { session_id: sid })
|
.request('session.interrupt', { session_id: sid })
|
||||||
.pipe(
|
.pipe(
|
||||||
Effect.catchCause(cause => Effect.sync(() => getLog().warn('interrupt', 'failed', { cause: String(cause) })))
|
Effect.catchCause(cause =>
|
||||||
|
Effect.sync(() => getLog().warn('interrupt', 'failed', { cause: String(cause) }))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -292,7 +294,11 @@ export const run = Effect.fn('Tui.run')(function* (input: TuiInput) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await Effect.runPromise(
|
await Effect.runPromise(
|
||||||
gateway.request('image.attach_bytes', { content_base64: img.data, filename: 'pasted.png', session_id: sid })
|
gateway.request('image.attach_bytes', {
|
||||||
|
content_base64: img.data,
|
||||||
|
filename: 'pasted.png',
|
||||||
|
session_id: sid
|
||||||
|
})
|
||||||
)
|
)
|
||||||
flashHint('🖼 image attached — type a message and send', 3000)
|
flashHint('🖼 image attached — type a message and send', 3000)
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -782,8 +782,18 @@ export function createSessionStore() {
|
|||||||
const mcp = obj(root.mcp)
|
const mcp = obj(root.mcp)
|
||||||
setState('catalog', {
|
setState('catalog', {
|
||||||
mcp: { servers: strs(mcp.servers) },
|
mcp: { servers: strs(mcp.servers) },
|
||||||
skills: { categories: list(skills.categories).map(pair).filter(p => p.name), total: num(skills.total) },
|
skills: {
|
||||||
tools: { toolsets: list(tools.toolsets).map(toolset).filter(p => p.name), total: num(tools.total) }
|
categories: list(skills.categories)
|
||||||
|
.map(pair)
|
||||||
|
.filter(p => p.name),
|
||||||
|
total: num(skills.total)
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
toolsets: list(tools.toolsets)
|
||||||
|
.map(toolset)
|
||||||
|
.filter(p => p.name),
|
||||||
|
total: num(tools.total)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,11 @@ export function truncate(s: string, width: number): string {
|
|||||||
export function normalizeOutput(text: string): string {
|
export function normalizeOutput(text: string): string {
|
||||||
const real = (text.match(/\n/g) ?? []).length
|
const real = (text.match(/\n/g) ?? []).length
|
||||||
const literal = (text.match(/\\n/g) ?? []).length
|
const literal = (text.match(/\\n/g) ?? []).length
|
||||||
if (literal > real) return text.replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n').replace(/\\t/g, ' ')
|
if (literal > real)
|
||||||
|
return text
|
||||||
|
.replace(/\\r\\n/g, '\n')
|
||||||
|
.replace(/\\n/g, '\n')
|
||||||
|
.replace(/\\t/g, ' ')
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,12 @@ function withKeymap(node: () => JSX.Element): () => JSX.Element {
|
|||||||
return () => {
|
return () => {
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
const keymap = createMemo(() => createDefaultOpenTuiKeymap(renderer))
|
const keymap = createMemo(() => createDefaultOpenTuiKeymap(renderer))
|
||||||
return KeymapProvider({ keymap: keymap(), get children() { return node() } })
|
return KeymapProvider({
|
||||||
|
keymap: keymap(),
|
||||||
|
get children() {
|
||||||
|
return node()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,11 @@ describe('planCompletion (items 5 + 13)', () => {
|
|||||||
method: 'complete.path',
|
method: 'complete.path',
|
||||||
params: { word: '@src/fo' }
|
params: { word: '@src/fo' }
|
||||||
})
|
})
|
||||||
expect(planCompletion('cat ./rea')).toEqual({ from: 'cat '.length, method: 'complete.path', params: { word: './rea' } })
|
expect(planCompletion('cat ./rea')).toEqual({
|
||||||
|
from: 'cat '.length,
|
||||||
|
method: 'complete.path',
|
||||||
|
params: { word: './rea' }
|
||||||
|
})
|
||||||
expect(planCompletion('open ~/proj')).toEqual({
|
expect(planCompletion('open ~/proj')).toEqual({
|
||||||
from: 'open '.length,
|
from: 'open '.length,
|
||||||
method: 'complete.path',
|
method: 'complete.path',
|
||||||
|
|||||||
@ -9,7 +9,9 @@ import { collapseToolOutput, stripAnsi, stripOmittedNote, stripToolEnvelope, tru
|
|||||||
describe('stripAnsi (item 8 - gateway slash/notice text is ANSI-colored for Ink)', () => {
|
describe('stripAnsi (item 8 - gateway slash/notice text is ANSI-colored for Ink)', () => {
|
||||||
const ESC = String.fromCharCode(27)
|
const ESC = String.fromCharCode(27)
|
||||||
test('removes SGR color codes, keeps the text', () => {
|
test('removes SGR color codes, keeps the text', () => {
|
||||||
expect(stripAnsi(`${ESC}[1;38;2;255;215;0m\u2713 Reasoning display: ON${ESC}[0m`)).toBe('\u2713 Reasoning display: ON')
|
expect(stripAnsi(`${ESC}[1;38;2;255;215;0m\u2713 Reasoning display: ON${ESC}[0m`)).toBe(
|
||||||
|
'\u2713 Reasoning display: ON'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
test('removes italic + mouse sequences', () => {
|
test('removes italic + mouse sequences', () => {
|
||||||
expect(stripAnsi(`${ESC}[2;3m Model thinking shown.${ESC}[0m`)).toBe(' Model thinking shown.')
|
expect(stripAnsi(`${ESC}[2;3m Model thinking shown.${ESC}[0m`)).toBe(' Model thinking shown.')
|
||||||
@ -22,7 +24,9 @@ describe('stripAnsi (item 8 - gateway slash/notice text is ANSI-colored for Ink)
|
|||||||
|
|
||||||
describe('stripOmittedNote (item 2 — peel the gateway verbose-tail label)', () => {
|
describe('stripOmittedNote (item 2 — peel the gateway verbose-tail label)', () => {
|
||||||
test('extracts the lines/chars note and returns the clean body', () => {
|
test('extracts the lines/chars note and returns the clean body', () => {
|
||||||
const { body, omittedNote } = stripOmittedNote('[showing verbose tail; omitted 5 lines / 234 chars]\nline one\nline two')
|
const { body, omittedNote } = stripOmittedNote(
|
||||||
|
'[showing verbose tail; omitted 5 lines / 234 chars]\nline one\nline two'
|
||||||
|
)
|
||||||
expect(omittedNote).toBe('5 lines / 234 chars')
|
expect(omittedNote).toBe('5 lines / 234 chars')
|
||||||
expect(body).toBe('line one\nline two')
|
expect(body).toBe('line one\nline two')
|
||||||
})
|
})
|
||||||
|
|||||||
@ -68,75 +68,75 @@ export function App(props: AppProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DimensionsProvider>
|
<DimensionsProvider>
|
||||||
<box style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1, paddingLeft: 1, paddingRight: 1 }}>
|
<box style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1, paddingLeft: 1, paddingRight: 1 }}>
|
||||||
{/* a bottom rule under the header bookends the transcript with the status
|
{/* a bottom rule under the header bookends the transcript with the status
|
||||||
bar's top rule — frames the chrome as intentional (item 8). */}
|
bar's top rule — frames the chrome as intentional (item 8). */}
|
||||||
<box border={['bottom']} borderColor={theme().color.border} style={{ flexShrink: 0 }}>
|
<box border={['bottom']} borderColor={theme().color.border} style={{ flexShrink: 0 }}>
|
||||||
<Header store={props.store} />
|
<Header store={props.store} />
|
||||||
</box>
|
</box>
|
||||||
{/* content zone: a full-screen overlay (pager / agents dashboard) OR the transcript + input zone */}
|
{/* content zone: a full-screen overlay (pager / agents dashboard) OR the transcript + input zone */}
|
||||||
<Switch
|
<Switch
|
||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<Transcript store={props.store} />
|
<Transcript store={props.store} />
|
||||||
{/* transient busy face floats at the bottom of the transcript area */}
|
{/* transient busy face floats at the bottom of the transcript area */}
|
||||||
<StatusLine store={props.store} />
|
<StatusLine store={props.store} />
|
||||||
{/* input region — a top-edge rule separates the status bar + textbox from the
|
{/* input region — a top-edge rule separates the status bar + textbox from the
|
||||||
transcript above; the status bar sits directly ABOVE the composer (item 14). */}
|
transcript above; the status bar sits directly ABOVE the composer (item 14). */}
|
||||||
<box
|
<box
|
||||||
border={['top']}
|
border={['top']}
|
||||||
borderColor={theme().color.border}
|
borderColor={theme().color.border}
|
||||||
style={{ flexShrink: 0, flexDirection: 'column' }}
|
style={{ flexShrink: 0, flexDirection: 'column' }}
|
||||||
>
|
|
||||||
<StatusBar store={props.store} />
|
|
||||||
<Switch
|
|
||||||
fallback={
|
|
||||||
<Composer
|
|
||||||
onSubmit={props.onSubmit ?? NOOP}
|
|
||||||
onType={props.onType}
|
|
||||||
completions={() => props.store.state.completions ?? []}
|
|
||||||
completionFrom={() => props.store.state.completionFrom}
|
|
||||||
onDismiss={() => props.store.clearCompletions()}
|
|
||||||
history={props.history}
|
|
||||||
onImagePaste={props.onImagePaste}
|
|
||||||
pasteStore={props.pasteStore}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Match when={blocked()}>
|
<StatusBar store={props.store} />
|
||||||
<PromptOverlay
|
<Switch
|
||||||
store={props.store}
|
fallback={
|
||||||
onRespond={props.onRespond ?? NOOP_RESPOND}
|
<Composer
|
||||||
sessionId={props.sessionId ?? NO_SESSION}
|
onSubmit={props.onSubmit ?? NOOP}
|
||||||
/>
|
onType={props.onType}
|
||||||
</Match>
|
completions={() => props.store.state.completions ?? []}
|
||||||
<Match when={switcher()}>
|
completionFrom={() => props.store.state.completionFrom}
|
||||||
{sessions => <SessionSwitcher sessions={sessions()} onPick={resume} onClose={closeSwitcher} />}
|
onDismiss={() => props.store.clearCompletions()}
|
||||||
</Match>
|
history={props.history}
|
||||||
<Match when={picker()}>
|
onImagePaste={props.onImagePaste}
|
||||||
{p => (
|
pasteStore={props.pasteStore}
|
||||||
<Picker
|
|
||||||
title={p().title}
|
|
||||||
items={p().items}
|
|
||||||
onPick={value => {
|
|
||||||
p().onPick(value)
|
|
||||||
closePicker()
|
|
||||||
}}
|
|
||||||
onClose={closePicker}
|
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
</Match>
|
>
|
||||||
</Switch>
|
<Match when={blocked()}>
|
||||||
</box>
|
<PromptOverlay
|
||||||
</>
|
store={props.store}
|
||||||
}
|
onRespond={props.onRespond ?? NOOP_RESPOND}
|
||||||
>
|
sessionId={props.sessionId ?? NO_SESSION}
|
||||||
<Match when={pager()}>{p => <Pager title={p().title} text={p().text} onClose={closePager} />}</Match>
|
/>
|
||||||
<Match when={dashboard()}>
|
</Match>
|
||||||
<AgentsDashboard subagents={props.store.state.subagents} onClose={closeDashboard} />
|
<Match when={switcher()}>
|
||||||
</Match>
|
{sessions => <SessionSwitcher sessions={sessions()} onPick={resume} onClose={closeSwitcher} />}
|
||||||
</Switch>
|
</Match>
|
||||||
</box>
|
<Match when={picker()}>
|
||||||
|
{p => (
|
||||||
|
<Picker
|
||||||
|
title={p().title}
|
||||||
|
items={p().items}
|
||||||
|
onPick={value => {
|
||||||
|
p().onPick(value)
|
||||||
|
closePicker()
|
||||||
|
}}
|
||||||
|
onClose={closePicker}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</box>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Match when={pager()}>{p => <Pager title={p().title} text={p().text} onClose={closePager} />}</Match>
|
||||||
|
<Match when={dashboard()}>
|
||||||
|
<AgentsDashboard subagents={props.store.state.subagents} onClose={closeDashboard} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</box>
|
||||||
</DimensionsProvider>
|
</DimensionsProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,81 +98,96 @@ export function HomeHint(props: { store: SessionStore }) {
|
|||||||
border
|
border
|
||||||
borderColor={theme().color.border}
|
borderColor={theme().color.border}
|
||||||
>
|
>
|
||||||
{/* session info block: model · Nous Research / dir / Session id */}
|
{/* session info block: model · Nous Research / dir / Session id */}
|
||||||
<box style={{ flexDirection: 'column' }}>
|
<box style={{ flexDirection: 'column' }}>
|
||||||
<Show when={info().model}>
|
<Show when={info().model}>
|
||||||
{model => (
|
{model => (
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.accent }}>{shortModel(model())}</span>
|
||||||
|
<span style={{ fg: theme().color.muted }}> · Nous Research</span>
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<Show when={info().cwd}>
|
||||||
|
{cwd => (
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>{shortCwd(cwd())}</span>
|
||||||
|
<Show when={info().branch}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>{` (${info().branch})`}</span>
|
||||||
|
</Show>
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<Show when={props.store.state.sessionId}>
|
||||||
<text selectable={false}>
|
<text selectable={false}>
|
||||||
<span style={{ fg: theme().color.accent }}>{shortModel(model())}</span>
|
<span style={{ fg: theme().color.muted }}>Session: </span>
|
||||||
<span style={{ fg: theme().color.muted }}> · Nous Research</span>
|
<span style={{ fg: theme().color.border }}>{props.store.state.sessionId}</span>
|
||||||
</text>
|
</text>
|
||||||
)}
|
</Show>
|
||||||
</Show>
|
</box>
|
||||||
<Show when={info().cwd}>
|
|
||||||
{cwd => (
|
|
||||||
<text selectable={false}>
|
|
||||||
<span style={{ fg: theme().color.muted }}>{shortCwd(cwd())}</span>
|
|
||||||
<Show when={info().branch}>
|
|
||||||
<span style={{ fg: theme().color.muted }}>{` (${info().branch})`}</span>
|
|
||||||
</Show>
|
|
||||||
</text>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<Show when={props.store.state.sessionId}>
|
|
||||||
<text selectable={false}>
|
|
||||||
<span style={{ fg: theme().color.muted }}>Session: </span>
|
|
||||||
<span style={{ fg: theme().color.border }}>{props.store.state.sessionId}</span>
|
|
||||||
</text>
|
|
||||||
</Show>
|
|
||||||
</box>
|
|
||||||
|
|
||||||
{/* SEPARATE collapsible sections (Ink parity) + summary */}
|
{/* SEPARATE collapsible sections (Ink parity) + summary */}
|
||||||
<Show when={cat()}>
|
<Show when={cat()}>
|
||||||
{c => (
|
{c => (
|
||||||
<box style={{ flexDirection: 'column' }}>
|
<box style={{ flexDirection: 'column' }}>
|
||||||
<Section title="Available Tools" open>
|
<Section title="Available Tools" open>
|
||||||
<For each={enabledToolsets().slice(0, TOOLSETS_MAX)}>
|
<For each={enabledToolsets().slice(0, TOOLSETS_MAX)}>
|
||||||
{ts => (
|
{ts => (
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.label }}>{`${ts.name}: `}</span>
|
||||||
|
<span style={{ fg: theme().color.muted }}>
|
||||||
|
{truncate(
|
||||||
|
ts.tools.join(', ') || `${ts.count} tools`,
|
||||||
|
Math.max(20, dims().width - ts.name.length - 8)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<Show when={enabledToolsets().length > TOOLSETS_MAX}>
|
||||||
<text selectable={false}>
|
<text selectable={false}>
|
||||||
<span style={{ fg: theme().color.label }}>{`${ts.name}: `}</span>
|
<span
|
||||||
<span style={{ fg: theme().color.muted }}>
|
style={{ fg: theme().color.muted }}
|
||||||
{truncate(ts.tools.join(', ') || `${ts.count} tools`, Math.max(20, dims().width - ts.name.length - 8))}
|
>{`(and ${enabledToolsets().length - TOOLSETS_MAX} more toolsets…)`}</span>
|
||||||
</span>
|
|
||||||
</text>
|
</text>
|
||||||
)}
|
</Show>
|
||||||
</For>
|
</Section>
|
||||||
<Show when={enabledToolsets().length > TOOLSETS_MAX}>
|
|
||||||
|
<Section
|
||||||
|
title={`Available Skills (${c().skills.total})`}
|
||||||
|
suffix={`in ${c().skills.categories.length} categories`}
|
||||||
|
>
|
||||||
<text selectable={false}>
|
<text selectable={false}>
|
||||||
<span style={{ fg: theme().color.muted }}>{`(and ${enabledToolsets().length - TOOLSETS_MAX} more toolsets…)`}</span>
|
<span style={{ fg: theme().color.muted }}>
|
||||||
|
{c()
|
||||||
|
.skills.categories.map(s => `${s.name} (${s.count})`)
|
||||||
|
.join(' ')}
|
||||||
|
</span>
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Section>
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section title={`Available Skills (${c().skills.total})`} suffix={`in ${c().skills.categories.length} categories`}>
|
<Section
|
||||||
<text selectable={false}>
|
title={`MCP Servers (${c().mcp.servers.length})`}
|
||||||
<span style={{ fg: theme().color.muted }}>
|
suffix={c().mcp.servers.length ? 'connected' : ''}
|
||||||
{c().skills.categories.map(s => `${s.name} (${s.count})`).join(' ')}
|
>
|
||||||
</span>
|
<text selectable={false}>
|
||||||
</text>
|
<span style={{ fg: theme().color.muted }}>{c().mcp.servers.join(' ') || 'none configured'}</span>
|
||||||
</Section>
|
</text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section title={`MCP Servers (${c().mcp.servers.length})`} suffix={c().mcp.servers.length ? 'connected' : ''}>
|
<box style={{ marginTop: 1 }}>
|
||||||
<text selectable={false}>
|
<text selectable={false}>
|
||||||
<span style={{ fg: theme().color.muted }}>{c().mcp.servers.join(' ') || 'none configured'}</span>
|
<span style={{ fg: theme().color.text }}>{`${c().tools.total} tools`}</span>
|
||||||
</text>
|
<span
|
||||||
</Section>
|
style={{ fg: theme().color.muted }}
|
||||||
|
>{` · ${c().skills.total} skills · ${c().mcp.servers.length} MCP · `}</span>
|
||||||
<box style={{ marginTop: 1 }}>
|
<span style={{ fg: theme().color.accent }}>/help</span>
|
||||||
<text selectable={false}>
|
<span style={{ fg: theme().color.muted }}> for commands</span>
|
||||||
<span style={{ fg: theme().color.text }}>{`${c().tools.total} tools`}</span>
|
</text>
|
||||||
<span style={{ fg: theme().color.muted }}>{` · ${c().skills.total} skills · ${c().mcp.servers.length} MCP · `}</span>
|
</box>
|
||||||
<span style={{ fg: theme().color.accent }}>/help</span>
|
|
||||||
<span style={{ fg: theme().color.muted }}> for commands</span>
|
|
||||||
</text>
|
|
||||||
</box>
|
</box>
|
||||||
</box>
|
)}
|
||||||
)}
|
</Show>
|
||||||
</Show>
|
|
||||||
</box>
|
</box>
|
||||||
{/* end framed session panel */}
|
{/* end framed session panel */}
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,14 @@ import { useBindings } from '@opentui/keymap/solid'
|
|||||||
export function useCloseLayer(target: () => BoxRenderable | undefined, onClose: () => void): void {
|
export function useCloseLayer(target: () => BoxRenderable | undefined, onClose: () => void): void {
|
||||||
useBindings<BoxRenderable>(() => ({
|
useBindings<BoxRenderable>(() => ({
|
||||||
target,
|
target,
|
||||||
commands: [{ name: 'close', run() { onClose() } }],
|
commands: [
|
||||||
|
{
|
||||||
|
name: 'close',
|
||||||
|
run() {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
bindings: [
|
bindings: [
|
||||||
{ key: 'escape', cmd: 'close' },
|
{ key: 'escape', cmd: 'close' },
|
||||||
{ key: { name: 'c', ctrl: true }, cmd: 'close' }
|
{ key: { name: 'c', ctrl: true }, cmd: 'close' }
|
||||||
|
|||||||
@ -39,7 +39,10 @@ export function AgentsDashboard(props: { subagents: SubagentInfo[]; onClose: ()
|
|||||||
// Close (Esc/Ctrl+C) is the native keymap; select + scroll stay in the raw global
|
// Close (Esc/Ctrl+C) is the native keymap; select + scroll stay in the raw global
|
||||||
// handler below. Focus the root box on mount so the focus-within close layer is active.
|
// handler below. Focus the root box on mount so the focus-within close layer is active.
|
||||||
onMount(() => rootRef?.focus())
|
onMount(() => rootRef?.focus())
|
||||||
useCloseLayer(() => rootRef, () => props.onClose())
|
useCloseLayer(
|
||||||
|
() => rootRef,
|
||||||
|
() => props.onClose()
|
||||||
|
)
|
||||||
|
|
||||||
useKeyboard(key => {
|
useKeyboard(key => {
|
||||||
// `q` closes (footer advertises "Esc/q close"); Esc/Ctrl+C close via the keymap.
|
// `q` closes (footer advertises "Esc/q close"); Esc/Ctrl+C close via the keymap.
|
||||||
@ -107,7 +110,12 @@ export function AgentsDashboard(props: { subagents: SubagentInfo[]; onClose: ()
|
|||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
<box style={{ flexGrow: 1, minHeight: 0, paddingLeft: 1 }}>
|
<box style={{ flexGrow: 1, minHeight: 0, paddingLeft: 1 }}>
|
||||||
<scrollbox ref={el => (traceBox = el)} style={{ flexGrow: 1, minHeight: 0 }} stickyScroll stickyStart="bottom">
|
<scrollbox
|
||||||
|
ref={el => (traceBox = el)}
|
||||||
|
style={{ flexGrow: 1, minHeight: 0 }}
|
||||||
|
stickyScroll
|
||||||
|
stickyStart="bottom"
|
||||||
|
>
|
||||||
<Show
|
<Show
|
||||||
when={(sa().trace?.length ?? 0) > 0}
|
when={(sa().trace?.length ?? 0) > 0}
|
||||||
fallback={<text fg={theme().color.muted}>(no activity yet)</text>}
|
fallback={<text fg={theme().color.muted}>(no activity yet)</text>}
|
||||||
|
|||||||
@ -27,7 +27,10 @@ export function Pager(props: { title: string; text: string; onClose: () => void
|
|||||||
// handler below. Focus the root box on mount so the focus-within close layer is
|
// handler below. Focus the root box on mount so the focus-within close layer is
|
||||||
// active (the scrollbox isn't focused — scroll is global, not focus-gated).
|
// active (the scrollbox isn't focused — scroll is global, not focus-gated).
|
||||||
onMount(() => rootRef?.focus())
|
onMount(() => rootRef?.focus())
|
||||||
useCloseLayer(() => rootRef, () => props.onClose())
|
useCloseLayer(
|
||||||
|
() => rootRef,
|
||||||
|
() => props.onClose()
|
||||||
|
)
|
||||||
|
|
||||||
useKeyboard(key => {
|
useKeyboard(key => {
|
||||||
// `q` closes (the footer advertises "Esc/q close"); Esc/Ctrl+C close via the
|
// `q` closes (the footer advertises "Esc/q close"); Esc/Ctrl+C close via the
|
||||||
|
|||||||
@ -20,7 +20,10 @@ export function Picker(props: {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
let rootRef: BoxRenderable | undefined
|
let rootRef: BoxRenderable | undefined
|
||||||
// Native select handles ↑↓/j/k/Enter; the keymap owns Esc/Ctrl+C close.
|
// Native select handles ↑↓/j/k/Enter; the keymap owns Esc/Ctrl+C close.
|
||||||
useCloseLayer(() => rootRef, () => props.onClose())
|
useCloseLayer(
|
||||||
|
() => rootRef,
|
||||||
|
() => props.onClose()
|
||||||
|
)
|
||||||
|
|
||||||
const options = createMemo(() =>
|
const options = createMemo(() =>
|
||||||
props.items.map(it => ({ description: it.description ?? '', name: it.label, value: it.value }))
|
props.items.map(it => ({ description: it.description ?? '', name: it.label, value: it.value }))
|
||||||
|
|||||||
@ -19,7 +19,10 @@ export function SessionSwitcher(props: {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
let rootRef: BoxRenderable | undefined
|
let rootRef: BoxRenderable | undefined
|
||||||
// Native select handles ↑↓/Enter; the keymap owns Esc/Ctrl+C close.
|
// Native select handles ↑↓/Enter; the keymap owns Esc/Ctrl+C close.
|
||||||
useCloseLayer(() => rootRef, () => props.onClose())
|
useCloseLayer(
|
||||||
|
() => rootRef,
|
||||||
|
() => props.onClose()
|
||||||
|
)
|
||||||
|
|
||||||
const options = createMemo(() =>
|
const options = createMemo(() =>
|
||||||
props.sessions.map(s => ({
|
props.sessions.map(s => ({
|
||||||
|
|||||||
@ -26,7 +26,10 @@ export function ApprovalPrompt(props: {
|
|||||||
let rootRef: BoxRenderable | undefined
|
let rootRef: BoxRenderable | undefined
|
||||||
// Native select handles ↑↓/j/k/Enter over the options; the keymap owns the
|
// Native select handles ↑↓/j/k/Enter over the options; the keymap owns the
|
||||||
// Esc/Ctrl+C → deny cancel path the select doesn't cover.
|
// Esc/Ctrl+C → deny cancel path the select doesn't cover.
|
||||||
useCloseLayer(() => rootRef, () => props.onCancel())
|
useCloseLayer(
|
||||||
|
() => rootRef,
|
||||||
|
() => props.onCancel()
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
|
|||||||
@ -20,8 +20,18 @@ export function ConfirmPrompt(props: { message: string; onYes: () => void; onNo:
|
|||||||
useBindings<BoxRenderable>(() => ({
|
useBindings<BoxRenderable>(() => ({
|
||||||
target: () => rootRef,
|
target: () => rootRef,
|
||||||
commands: [
|
commands: [
|
||||||
{ name: 'confirm', run() { props.onYes() } },
|
{
|
||||||
{ name: 'cancel', run() { props.onNo() } }
|
name: 'confirm',
|
||||||
|
run() {
|
||||||
|
props.onYes()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cancel',
|
||||||
|
run() {
|
||||||
|
props.onNo()
|
||||||
|
}
|
||||||
|
}
|
||||||
],
|
],
|
||||||
bindings: [
|
bindings: [
|
||||||
{ key: 'y', cmd: 'confirm' },
|
{ key: 'y', cmd: 'confirm' },
|
||||||
|
|||||||
@ -41,10 +41,7 @@ export function ReasoningPart(props: { text: string; streaming?: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<Show when={summary().body || summary().title}>
|
<Show when={summary().body || summary().title}>
|
||||||
<box style={{ flexDirection: 'column', flexShrink: 0 }}>
|
<box style={{ flexDirection: 'column', flexShrink: 0 }}>
|
||||||
<box
|
<box style={{ flexDirection: 'row', flexShrink: 0 }} onMouseDown={toggle}>
|
||||||
style={{ flexDirection: 'row', flexShrink: 0 }}
|
|
||||||
onMouseDown={toggle}
|
|
||||||
>
|
|
||||||
<box style={{ flexShrink: 0, width: GUTTER }}>
|
<box style={{ flexShrink: 0, width: GUTTER }}>
|
||||||
<text selectable={false}>
|
<text selectable={false}>
|
||||||
<span style={{ fg: theme().color.accent }}>{expanded() ? '▼' : '▶'}</span>
|
<span style={{ fg: theme().color.accent }}>{expanded() ? '▼' : '▶'}</span>
|
||||||
|
|||||||
@ -20,7 +20,10 @@ type AnchorFn = (toggle: () => void) => void
|
|||||||
|
|
||||||
const Ctx = createContext<AnchorFn>()
|
const Ctx = createContext<AnchorFn>()
|
||||||
|
|
||||||
export function ScrollAnchorProvider(props: { scroll: Accessor<ScrollBoxRenderable | undefined>; children: JSX.Element }) {
|
export function ScrollAnchorProvider(props: {
|
||||||
|
scroll: Accessor<ScrollBoxRenderable | undefined>
|
||||||
|
children: JSX.Element
|
||||||
|
}) {
|
||||||
const around: AnchorFn = toggle => {
|
const around: AnchorFn = toggle => {
|
||||||
const sb = props.scroll()
|
const sb = props.scroll()
|
||||||
if (!sb) {
|
if (!sb) {
|
||||||
|
|||||||
@ -81,9 +81,7 @@ export function ToolPart(props: { part: ToolPartState }) {
|
|||||||
const collapsible = () => !running() && (lines().length > 1 || showArgs())
|
const collapsible = () => !running() && (lines().length > 1 || showArgs())
|
||||||
// Header subtitle: the primary-arg preview (item 2), else explicit summary, else first line.
|
// Header subtitle: the primary-arg preview (item 2), else explicit summary, else first line.
|
||||||
const subtitle = () =>
|
const subtitle = () =>
|
||||||
props.part.error
|
props.part.error ? `✗ ${props.part.error}` : props.part.argsPreview || props.part.summary || lines()[0] || ''
|
||||||
? `✗ ${props.part.error}`
|
|
||||||
: props.part.argsPreview || props.part.summary || lines()[0] || ''
|
|
||||||
const body = createMemo(() => collapseToolOutput(result(), EXPANDED_MAX, bodyWidth() - 2))
|
const body = createMemo(() => collapseToolOutput(result(), EXPANDED_MAX, bodyWidth() - 2))
|
||||||
|
|
||||||
const headGlyph = () => (collapsible() ? (expanded() ? '▼' : '▶') : '⚡')
|
const headGlyph = () => (collapsible() ? (expanded() ? '▼' : '▶') : '⚡')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user