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 bc79644f16
commit 4cb9aa6664
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. // 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)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

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)', () => { 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')
}) })

View File

@ -136,27 +136,40 @@ export function HomeHint(props: { store: SessionStore }) {
<text selectable={false}> <text selectable={false}>
<span style={{ fg: theme().color.label }}>{`${ts.name}: `}</span> <span style={{ fg: theme().color.label }}>{`${ts.name}: `}</span>
<span style={{ fg: theme().color.muted }}> <span style={{ fg: theme().color.muted }}>
{truncate(ts.tools.join(', ') || `${ts.count} tools`, Math.max(20, dims().width - ts.name.length - 8))} {truncate(
ts.tools.join(', ') || `${ts.count} tools`,
Math.max(20, dims().width - ts.name.length - 8)
)}
</span> </span>
</text> </text>
)} )}
</For> </For>
<Show when={enabledToolsets().length > TOOLSETS_MAX}> <Show when={enabledToolsets().length > TOOLSETS_MAX}>
<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 }}
>{`(and ${enabledToolsets().length - TOOLSETS_MAX} more toolsets…)`}</span>
</text> </text>
</Show> </Show>
</Section> </Section>
<Section title={`Available Skills (${c().skills.total})`} suffix={`in ${c().skills.categories.length} categories`}> <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 }}> <span style={{ fg: theme().color.muted }}>
{c().skills.categories.map(s => `${s.name} (${s.count})`).join(' ')} {c()
.skills.categories.map(s => `${s.name} (${s.count})`)
.join(' ')}
</span> </span>
</text> </text>
</Section> </Section>
<Section title={`MCP Servers (${c().mcp.servers.length})`} suffix={c().mcp.servers.length ? 'connected' : ''}> <Section
title={`MCP Servers (${c().mcp.servers.length})`}
suffix={c().mcp.servers.length ? 'connected' : ''}
>
<text selectable={false}> <text selectable={false}>
<span style={{ fg: theme().color.muted }}>{c().mcp.servers.join(' ') || 'none configured'}</span> <span style={{ fg: theme().color.muted }}>{c().mcp.servers.join(' ') || 'none configured'}</span>
</text> </text>
@ -165,7 +178,9 @@ export function HomeHint(props: { store: SessionStore }) {
<box style={{ marginTop: 1 }}> <box style={{ marginTop: 1 }}>
<text selectable={false}> <text selectable={false}>
<span style={{ fg: theme().color.text }}>{`${c().tools.total} tools`}</span> <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.muted }}
>{` · ${c().skills.total} skills · ${c().mcp.servers.length} MCP · `}</span>
<span style={{ fg: theme().color.accent }}>/help</span> <span style={{ fg: theme().color.accent }}>/help</span>
<span style={{ fg: theme().color.muted }}> for commands</span> <span style={{ fg: theme().color.muted }}> for commands</span>
</text> </text>

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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