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:
alt-glitch 2026-06-09 08:03:08 +00:00
parent fdc0e5fea5
commit 2b1564199c
19 changed files with 243 additions and 157 deletions

View file

@ -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)
}

View file

@ -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 {

View file

@ -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)
}
})
}

View file

@ -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
}

View file

@ -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()
}
})
}
}

View file

@ -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',

View file

@ -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')
})

View file

@ -68,75 +68,75 @@ export function App(props: AppProps) {
return (
<DimensionsProvider>
<box style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1, paddingLeft: 1, paddingRight: 1 }}>
{/* a bottom rule under the header bookends the transcript with the status
<box style={{ flexDirection: 'column', flexGrow: 1, paddingTop: 1, paddingLeft: 1, paddingRight: 1 }}>
{/* a bottom rule under the header bookends the transcript with the status
bar's top rule frames the chrome as intentional (item 8). */}
<box border={['bottom']} borderColor={theme().color.border} style={{ flexShrink: 0 }}>
<Header store={props.store} />
</box>
{/* content zone: a full-screen overlay (pager / agents dashboard) OR the transcript + input zone */}
<Switch
fallback={
<>
<Transcript store={props.store} />
{/* transient busy face floats at the bottom of the transcript area */}
<StatusLine store={props.store} />
{/* input region a top-edge rule separates the status bar + textbox from the
<box border={['bottom']} borderColor={theme().color.border} style={{ flexShrink: 0 }}>
<Header store={props.store} />
</box>
{/* content zone: a full-screen overlay (pager / agents dashboard) OR the transcript + input zone */}
<Switch
fallback={
<>
<Transcript store={props.store} />
{/* transient busy face floats at the bottom of the transcript area */}
<StatusLine store={props.store} />
{/* 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). */}
<box
border={['top']}
borderColor={theme().color.border}
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}
/>
}
<box
border={['top']}
borderColor={theme().color.border}
style={{ flexShrink: 0, flexDirection: 'column' }}
>
<Match when={blocked()}>
<PromptOverlay
store={props.store}
onRespond={props.onRespond ?? NOOP_RESPOND}
sessionId={props.sessionId ?? NO_SESSION}
/>
</Match>
<Match when={switcher()}>
{sessions => <SessionSwitcher sessions={sessions()} onPick={resume} onClose={closeSwitcher} />}
</Match>
<Match when={picker()}>
{p => (
<Picker
title={p().title}
items={p().items}
onPick={value => {
p().onPick(value)
closePicker()
}}
onClose={closePicker}
<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>
</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>
}
>
<Match when={blocked()}>
<PromptOverlay
store={props.store}
onRespond={props.onRespond ?? NOOP_RESPOND}
sessionId={props.sessionId ?? NO_SESSION}
/>
</Match>
<Match when={switcher()}>
{sessions => <SessionSwitcher sessions={sessions()} onPick={resume} onClose={closeSwitcher} />}
</Match>
<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>
)
}

View file

@ -98,81 +98,96 @@ export function HomeHint(props: { store: SessionStore }) {
border
borderColor={theme().color.border}
>
{/* session info block: model · Nous Research / dir / Session id */}
<box style={{ flexDirection: 'column' }}>
<Show when={info().model}>
{model => (
{/* session info block: model · Nous Research / dir / Session id */}
<box style={{ flexDirection: 'column' }}>
<Show when={info().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}>
<span style={{ fg: theme().color.accent }}>{shortModel(model())}</span>
<span style={{ fg: theme().color.muted }}> · Nous Research</span>
<span style={{ fg: theme().color.muted }}>Session: </span>
<span style={{ fg: theme().color.border }}>{props.store.state.sessionId}</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}>
<span style={{ fg: theme().color.muted }}>Session: </span>
<span style={{ fg: theme().color.border }}>{props.store.state.sessionId}</span>
</text>
</Show>
</box>
</Show>
</box>
{/* SEPARATE collapsible sections (Ink parity) + summary */}
<Show when={cat()}>
{c => (
<box style={{ flexDirection: 'column' }}>
<Section title="Available Tools" open>
<For each={enabledToolsets().slice(0, TOOLSETS_MAX)}>
{ts => (
{/* SEPARATE collapsible sections (Ink parity) + summary */}
<Show when={cat()}>
{c => (
<box style={{ flexDirection: 'column' }}>
<Section title="Available Tools" open>
<For each={enabledToolsets().slice(0, TOOLSETS_MAX)}>
{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}>
<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>
<span
style={{ fg: theme().color.muted }}
>{`(and ${enabledToolsets().length - TOOLSETS_MAX} more toolsets…)`}</span>
</text>
)}
</For>
<Show when={enabledToolsets().length > TOOLSETS_MAX}>
</Show>
</Section>
<Section
title={`Available Skills (${c().skills.total})`}
suffix={`in ${c().skills.categories.length} categories`}
>
<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>
</Show>
</Section>
</Section>
<Section title={`Available Skills (${c().skills.total})`} suffix={`in ${c().skills.categories.length} categories`}>
<text selectable={false}>
<span style={{ fg: theme().color.muted }}>
{c().skills.categories.map(s => `${s.name} (${s.count})`).join(' ')}
</span>
</text>
</Section>
<Section
title={`MCP Servers (${c().mcp.servers.length})`}
suffix={c().mcp.servers.length ? 'connected' : ''}
>
<text selectable={false}>
<span style={{ fg: theme().color.muted }}>{c().mcp.servers.join(' ') || 'none configured'}</span>
</text>
</Section>
<Section title={`MCP Servers (${c().mcp.servers.length})`} suffix={c().mcp.servers.length ? 'connected' : ''}>
<text selectable={false}>
<span style={{ fg: theme().color.muted }}>{c().mcp.servers.join(' ') || 'none configured'}</span>
</text>
</Section>
<box style={{ marginTop: 1 }}>
<text selectable={false}>
<span style={{ fg: theme().color.text }}>{`${c().tools.total} tools`}</span>
<span style={{ fg: theme().color.muted }}>{` · ${c().skills.total} skills · ${c().mcp.servers.length} MCP · `}</span>
<span style={{ fg: theme().color.accent }}>/help</span>
<span style={{ fg: theme().color.muted }}> for commands</span>
</text>
<box style={{ marginTop: 1 }}>
<text selectable={false}>
<span style={{ fg: theme().color.text }}>{`${c().tools.total} tools`}</span>
<span
style={{ fg: theme().color.muted }}
>{` · ${c().skills.total} skills · ${c().mcp.servers.length} MCP · `}</span>
<span style={{ fg: theme().color.accent }}>/help</span>
<span style={{ fg: theme().color.muted }}> for commands</span>
</text>
</box>
</box>
</box>
)}
</Show>
)}
</Show>
</box>
{/* end framed session panel */}

View file

@ -18,7 +18,14 @@ import { useBindings } from '@opentui/keymap/solid'
export function useCloseLayer(target: () => BoxRenderable | undefined, onClose: () => void): void {
useBindings<BoxRenderable>(() => ({
target,
commands: [{ name: 'close', run() { onClose() } }],
commands: [
{
name: 'close',
run() {
onClose()
}
}
],
bindings: [
{ key: 'escape', cmd: 'close' },
{ key: { name: 'c', ctrl: true }, cmd: 'close' }

View file

@ -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: ()
</box>
</Show>
<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
when={(sa().trace?.length ?? 0) > 0}
fallback={<text fg={theme().color.muted}>(no activity yet)</text>}

View file

@ -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

View file

@ -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 }))

View file

@ -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 => ({

View file

@ -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 (
<box

View file

@ -20,8 +20,18 @@ export function ConfirmPrompt(props: { message: string; onYes: () => void; onNo:
useBindings<BoxRenderable>(() => ({
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' },

View file

@ -41,10 +41,7 @@ export function ReasoningPart(props: { text: string; streaming?: boolean }) {
return (
<Show when={summary().body || summary().title}>
<box style={{ flexDirection: 'column', flexShrink: 0 }}>
<box
style={{ flexDirection: 'row', flexShrink: 0 }}
onMouseDown={toggle}
>
<box style={{ flexDirection: 'row', flexShrink: 0 }} onMouseDown={toggle}>
<box style={{ flexShrink: 0, width: GUTTER }}>
<text selectable={false}>
<span style={{ fg: theme().color.accent }}>{expanded() ? '▼' : '▶'}</span>

View file

@ -20,7 +20,10 @@ type AnchorFn = (toggle: () => void) => void
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 sb = props.scroll()
if (!sb) {

View file

@ -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() ? '▼' : '▶') : '⚡')