diff --git a/ui-tui-opentui-v2/src/boundary/gateway/client.ts b/ui-tui-opentui-v2/src/boundary/gateway/client.ts index b6831ce2376..ac358852bac 100644 --- a/ui-tui-opentui-v2/src/boundary/gateway/client.ts +++ b/ui-tui-opentui-v2/src/boundary/gateway/client.ts @@ -104,7 +104,10 @@ export class RawGatewayClient { // A recovery-respawn re-enters start(), so this re-arms per respawn — desired. this.startupTimer = setTimeout(() => { 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) } diff --git a/ui-tui-opentui-v2/src/entry/main.tsx b/ui-tui-opentui-v2/src/entry/main.tsx index be1dfb9b275..c1a30d53d7c 100644 --- a/ui-tui-opentui-v2/src/entry/main.tsx +++ b/ui-tui-opentui-v2/src/entry/main.tsx @@ -244,7 +244,9 @@ export const run = Effect.fn('Tui.run')(function* (input: TuiInput) { gateway .request('session.interrupt', { session_id: sid }) .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 { 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) } catch { diff --git a/ui-tui-opentui-v2/src/logic/store.ts b/ui-tui-opentui-v2/src/logic/store.ts index cb928ba3c0b..66db44b3192 100644 --- a/ui-tui-opentui-v2/src/logic/store.ts +++ b/ui-tui-opentui-v2/src/logic/store.ts @@ -782,8 +782,18 @@ export function createSessionStore() { const mcp = obj(root.mcp) setState('catalog', { mcp: { servers: strs(mcp.servers) }, - skills: { 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) } + skills: { + 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) + } }) } diff --git a/ui-tui-opentui-v2/src/logic/toolOutput.ts b/ui-tui-opentui-v2/src/logic/toolOutput.ts index 937ad1e8fc5..94e97871e73 100644 --- a/ui-tui-opentui-v2/src/logic/toolOutput.ts +++ b/ui-tui-opentui-v2/src/logic/toolOutput.ts @@ -40,7 +40,11 @@ export function truncate(s: string, width: number): string { export function normalizeOutput(text: string): string { const real = (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 } diff --git a/ui-tui-opentui-v2/src/test/lib/render.ts b/ui-tui-opentui-v2/src/test/lib/render.ts index 6231625dec5..26e960803f3 100644 --- a/ui-tui-opentui-v2/src/test/lib/render.ts +++ b/ui-tui-opentui-v2/src/test/lib/render.ts @@ -28,7 +28,12 @@ function withKeymap(node: () => JSX.Element): () => JSX.Element { return () => { const renderer = useRenderer() const keymap = createMemo(() => createDefaultOpenTuiKeymap(renderer)) - return KeymapProvider({ keymap: keymap(), get children() { return node() } }) + return KeymapProvider({ + keymap: keymap(), + get children() { + return node() + } + }) } } diff --git a/ui-tui-opentui-v2/src/test/slash.test.ts b/ui-tui-opentui-v2/src/test/slash.test.ts index 85526d5d435..fea857ad289 100644 --- a/ui-tui-opentui-v2/src/test/slash.test.ts +++ b/ui-tui-opentui-v2/src/test/slash.test.ts @@ -46,7 +46,11 @@ describe('planCompletion (items 5 + 13)', () => { method: 'complete.path', 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({ from: 'open '.length, method: 'complete.path', diff --git a/ui-tui-opentui-v2/src/test/toolOutput.test.ts b/ui-tui-opentui-v2/src/test/toolOutput.test.ts index 3ec5ef8df2b..3e88147c193 100644 --- a/ui-tui-opentui-v2/src/test/toolOutput.test.ts +++ b/ui-tui-opentui-v2/src/test/toolOutput.test.ts @@ -9,7 +9,9 @@ import { collapseToolOutput, stripAnsi, stripOmittedNote, stripToolEnvelope, tru describe('stripAnsi (item 8 - gateway slash/notice text is ANSI-colored for Ink)', () => { const ESC = String.fromCharCode(27) 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', () => { 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)', () => { 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(body).toBe('line one\nline two') }) diff --git a/ui-tui-opentui-v2/src/view/App.tsx b/ui-tui-opentui-v2/src/view/App.tsx index 3238b9c2269..dbd6bf112e8 100644 --- a/ui-tui-opentui-v2/src/view/App.tsx +++ b/ui-tui-opentui-v2/src/view/App.tsx @@ -68,75 +68,75 @@ export function App(props: AppProps) { return ( - - {/* 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). */} - -
- - {/* content zone: a full-screen overlay (pager / agents dashboard) OR the transcript + input zone */} - - - {/* transient busy face floats at the bottom of the transcript area */} - - {/* input region — a top-edge rule separates the status bar + textbox from the + +
+ + {/* content zone: a full-screen overlay (pager / agents dashboard) OR the transcript + input zone */} + + + {/* transient busy face floats at the bottom of the transcript area */} + + {/* 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). */} - - - props.store.state.completions ?? []} - completionFrom={() => props.store.state.completionFrom} - onDismiss={() => props.store.clearCompletions()} - history={props.history} - onImagePaste={props.onImagePaste} - pasteStore={props.pasteStore} - /> - } + - - - - - {sessions => } - - - {p => ( - { - p().onPick(value) - closePicker() - }} - onClose={closePicker} + + props.store.state.completions ?? []} + completionFrom={() => props.store.state.completionFrom} + onDismiss={() => props.store.clearCompletions()} + history={props.history} + onImagePaste={props.onImagePaste} + pasteStore={props.pasteStore} /> - )} - - - - - } - > - {p => } - - - - - + } + > + + + + + {sessions => } + + + {p => ( + { + p().onPick(value) + closePicker() + }} + onClose={closePicker} + /> + )} + + + + + } + > + {p => } + + + + + ) } diff --git a/ui-tui-opentui-v2/src/view/homeHint.tsx b/ui-tui-opentui-v2/src/view/homeHint.tsx index 76fdfae9582..cfc95da5d03 100644 --- a/ui-tui-opentui-v2/src/view/homeHint.tsx +++ b/ui-tui-opentui-v2/src/view/homeHint.tsx @@ -98,81 +98,96 @@ export function HomeHint(props: { store: SessionStore }) { border borderColor={theme().color.border} > - {/* session info block: model · Nous Research / dir / Session id */} - - - {model => ( + {/* session info block: model · Nous Research / dir / Session id */} + + + {model => ( + + {shortModel(model())} + · Nous Research + + )} + + + {cwd => ( + + {shortCwd(cwd())} + + {` (${info().branch})`} + + + )} + + - {shortModel(model())} - · Nous Research + Session: + {props.store.state.sessionId} - )} - - - {cwd => ( - - {shortCwd(cwd())} - - {` (${info().branch})`} - - - )} - - - - Session: - {props.store.state.sessionId} - - - + + - {/* SEPARATE collapsible sections (Ink parity) + summary */} - - {c => ( - -
- - {ts => ( + {/* SEPARATE collapsible sections (Ink parity) + summary */} + + {c => ( + +
+ + {ts => ( + + {`${ts.name}: `} + + {truncate( + ts.tools.join(', ') || `${ts.count} tools`, + Math.max(20, dims().width - ts.name.length - 8) + )} + + + )} + + TOOLSETS_MAX}> - {`${ts.name}: `} - - {truncate(ts.tools.join(', ') || `${ts.count} tools`, Math.max(20, dims().width - ts.name.length - 8))} - + {`(and ${enabledToolsets().length - TOOLSETS_MAX} more toolsets…)`} - )} - - TOOLSETS_MAX}> + +
+ +
- {`(and ${enabledToolsets().length - TOOLSETS_MAX} more toolsets…)`} + + {c() + .skills.categories.map(s => `${s.name} (${s.count})`) + .join(' ')} + - -
+
-
- - - {c().skills.categories.map(s => `${s.name} (${s.count})`).join(' ')} - - -
+
+ + {c().mcp.servers.join(' ') || 'none configured'} + +
-
- - {c().mcp.servers.join(' ') || 'none configured'} - -
- - - - {`${c().tools.total} tools`} - {` · ${c().skills.total} skills · ${c().mcp.servers.length} MCP · `} - /help - for commands - + + + {`${c().tools.total} tools`} + {` · ${c().skills.total} skills · ${c().mcp.servers.length} MCP · `} + /help + for commands + + -
- )} -
+ )} + {/* end framed session panel */} diff --git a/ui-tui-opentui-v2/src/view/keymap.tsx b/ui-tui-opentui-v2/src/view/keymap.tsx index bcd71c1c90b..a1ebef2030d 100644 --- a/ui-tui-opentui-v2/src/view/keymap.tsx +++ b/ui-tui-opentui-v2/src/view/keymap.tsx @@ -18,7 +18,14 @@ import { useBindings } from '@opentui/keymap/solid' export function useCloseLayer(target: () => BoxRenderable | undefined, onClose: () => void): void { useBindings(() => ({ target, - commands: [{ name: 'close', run() { onClose() } }], + commands: [ + { + name: 'close', + run() { + onClose() + } + } + ], bindings: [ { key: 'escape', cmd: 'close' }, { key: { name: 'c', ctrl: true }, cmd: 'close' } diff --git a/ui-tui-opentui-v2/src/view/overlays/agentsDashboard.tsx b/ui-tui-opentui-v2/src/view/overlays/agentsDashboard.tsx index d72ae293833..36aed97ecb4 100644 --- a/ui-tui-opentui-v2/src/view/overlays/agentsDashboard.tsx +++ b/ui-tui-opentui-v2/src/view/overlays/agentsDashboard.tsx @@ -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 // handler below. Focus the root box on mount so the focus-within close layer is active. onMount(() => rootRef?.focus()) - useCloseLayer(() => rootRef, () => props.onClose()) + useCloseLayer( + () => rootRef, + () => props.onClose() + ) useKeyboard(key => { // `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: () - (traceBox = el)} style={{ flexGrow: 1, minHeight: 0 }} stickyScroll stickyStart="bottom"> + (traceBox = el)} + style={{ flexGrow: 1, minHeight: 0 }} + stickyScroll + stickyStart="bottom" + > 0} fallback={(no activity yet)} diff --git a/ui-tui-opentui-v2/src/view/overlays/pager.tsx b/ui-tui-opentui-v2/src/view/overlays/pager.tsx index efb2491d0e4..8969aa691b5 100644 --- a/ui-tui-opentui-v2/src/view/overlays/pager.tsx +++ b/ui-tui-opentui-v2/src/view/overlays/pager.tsx @@ -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 // active (the scrollbox isn't focused — scroll is global, not focus-gated). onMount(() => rootRef?.focus()) - useCloseLayer(() => rootRef, () => props.onClose()) + useCloseLayer( + () => rootRef, + () => props.onClose() + ) useKeyboard(key => { // `q` closes (the footer advertises "Esc/q close"); Esc/Ctrl+C close via the diff --git a/ui-tui-opentui-v2/src/view/overlays/picker.tsx b/ui-tui-opentui-v2/src/view/overlays/picker.tsx index 9abd5f7d3b6..eb3fa150ff8 100644 --- a/ui-tui-opentui-v2/src/view/overlays/picker.tsx +++ b/ui-tui-opentui-v2/src/view/overlays/picker.tsx @@ -20,7 +20,10 @@ export function Picker(props: { const theme = useTheme() let rootRef: BoxRenderable | undefined // Native select handles ↑↓/j/k/Enter; the keymap owns Esc/Ctrl+C close. - useCloseLayer(() => rootRef, () => props.onClose()) + useCloseLayer( + () => rootRef, + () => props.onClose() + ) const options = createMemo(() => props.items.map(it => ({ description: it.description ?? '', name: it.label, value: it.value })) diff --git a/ui-tui-opentui-v2/src/view/overlays/sessionSwitcher.tsx b/ui-tui-opentui-v2/src/view/overlays/sessionSwitcher.tsx index 6d4162ed0ae..1315209dc1f 100644 --- a/ui-tui-opentui-v2/src/view/overlays/sessionSwitcher.tsx +++ b/ui-tui-opentui-v2/src/view/overlays/sessionSwitcher.tsx @@ -19,7 +19,10 @@ export function SessionSwitcher(props: { const theme = useTheme() let rootRef: BoxRenderable | undefined // Native select handles ↑↓/Enter; the keymap owns Esc/Ctrl+C close. - useCloseLayer(() => rootRef, () => props.onClose()) + useCloseLayer( + () => rootRef, + () => props.onClose() + ) const options = createMemo(() => props.sessions.map(s => ({ diff --git a/ui-tui-opentui-v2/src/view/prompts/approvalPrompt.tsx b/ui-tui-opentui-v2/src/view/prompts/approvalPrompt.tsx index eca9fb58287..37f58210743 100644 --- a/ui-tui-opentui-v2/src/view/prompts/approvalPrompt.tsx +++ b/ui-tui-opentui-v2/src/view/prompts/approvalPrompt.tsx @@ -26,7 +26,10 @@ export function ApprovalPrompt(props: { let rootRef: BoxRenderable | undefined // Native select handles ↑↓/j/k/Enter over the options; the keymap owns the // Esc/Ctrl+C → deny cancel path the select doesn't cover. - useCloseLayer(() => rootRef, () => props.onCancel()) + useCloseLayer( + () => rootRef, + () => props.onCancel() + ) return ( void; onNo: useBindings(() => ({ target: () => rootRef, commands: [ - { name: 'confirm', run() { props.onYes() } }, - { name: 'cancel', run() { props.onNo() } } + { + name: 'confirm', + run() { + props.onYes() + } + }, + { + name: 'cancel', + run() { + props.onNo() + } + } ], bindings: [ { key: 'y', cmd: 'confirm' }, diff --git a/ui-tui-opentui-v2/src/view/reasoningPart.tsx b/ui-tui-opentui-v2/src/view/reasoningPart.tsx index 72f72f0e662..22b6bfa8d4b 100644 --- a/ui-tui-opentui-v2/src/view/reasoningPart.tsx +++ b/ui-tui-opentui-v2/src/view/reasoningPart.tsx @@ -41,10 +41,7 @@ export function ReasoningPart(props: { text: string; streaming?: boolean }) { return ( - + {expanded() ? '▼' : '▶'} diff --git a/ui-tui-opentui-v2/src/view/scrollAnchor.tsx b/ui-tui-opentui-v2/src/view/scrollAnchor.tsx index c98f3bad548..bd3e31d853c 100644 --- a/ui-tui-opentui-v2/src/view/scrollAnchor.tsx +++ b/ui-tui-opentui-v2/src/view/scrollAnchor.tsx @@ -20,7 +20,10 @@ type AnchorFn = (toggle: () => void) => void const Ctx = createContext() -export function ScrollAnchorProvider(props: { scroll: Accessor; children: JSX.Element }) { +export function ScrollAnchorProvider(props: { + scroll: Accessor + children: JSX.Element +}) { const around: AnchorFn = toggle => { const sb = props.scroll() if (!sb) { diff --git a/ui-tui-opentui-v2/src/view/toolPart.tsx b/ui-tui-opentui-v2/src/view/toolPart.tsx index bbdac8949a5..b5b79505ac8 100644 --- a/ui-tui-opentui-v2/src/view/toolPart.tsx +++ b/ui-tui-opentui-v2/src/view/toolPart.tsx @@ -81,9 +81,7 @@ export function ToolPart(props: { part: ToolPartState }) { const collapsible = () => !running() && (lines().length > 1 || showArgs()) // Header subtitle: the primary-arg preview (item 2), else explicit summary, else first line. const subtitle = () => - props.part.error - ? `✗ ${props.part.error}` - : props.part.argsPreview || props.part.summary || lines()[0] || '' + props.part.error ? `✗ ${props.part.error}` : props.part.argsPreview || props.part.summary || lines()[0] || '' const body = createMemo(() => collapseToolOutput(result(), EXPANDED_MAX, bodyWidth() - 2)) const headGlyph = () => (collapsible() ? (expanded() ? '▼' : '▶') : '⚡')