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

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

View File

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

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