mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-11 13:41:53 +00:00
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
fdc0e5fea5
commit
2b1564199c
19 changed files with 243 additions and 157 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }))
|
||||
|
|
|
|||
|
|
@ -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 => ({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() ? '▼' : '▶') : '⚡')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue