Item 7 — tools were non-collapsible and "ugly-interlaced": - ToolPart now renders COLLAPSED by default as one line: `▶ name summary (N lines)` (summary = explicit summary / first output line / error). A ▶/▼ glyph marks expandable tools; clicking the header toggles a left-bar block of the full (capped) output. Running tools show `name …`; single-line/erroring tools render inline. Compact by default → far less interlacing clutter. - toolOutput.normalizeOutput: un-double-escapes literal \n/\t when they dominate over real newlines (some gateway tool tails are repr'd, so newlines arrived as backslash-n and rendered as one ugly line). Conservative — genuine multi-line output and legit `\n`-in-code are left alone. Applied in stripToolEnvelope. Item 3 — the input "blue tint": dropped the textarea's blue focusedBackgroundColor and added a `❯` prompt glyph. The composer is now distinguished by structure (the glyph + the status-bar rule above it), not a background tint. Tests: normalizeOutput (dominant-literal vs genuine-multiline). 70 pass. Live-smoked: `ls -la` tool → collapsed `▶ terminal total 3460 (N lines)`; SGR-click → `▼` + clean per-line output; composer shows `❯` with no blue tint.
98 lines
4.3 KiB
TypeScript
98 lines
4.3 KiB
TypeScript
/**
|
|
* ToolPart — one tool call, rendered COLLAPSED by default with a clear expand
|
|
* affordance (spec §7; item 7 — "tools aren't collapsible and ugly-interlaced").
|
|
*
|
|
* ▶ name summary/first-line (N lines) ← collapsed (default), one line
|
|
* ▼ name ← expanded header
|
|
* │ full output… ← left-bar block (capped)
|
|
*
|
|
* A `▶`/`▼` glyph marks expandable tools; clicking the header toggles it (mouse).
|
|
* Running tools show `name …`; single-line/erroring tools render inline (no
|
|
* expand). `resultText` is already `{output,exit_code}`-envelope-stripped by the
|
|
* store. Fully themed (no hardcoded styles).
|
|
*/
|
|
import { type ToolPartState } from '../logic/store.ts'
|
|
import { useTerminalDimensions } from '@opentui/solid'
|
|
import { createMemo, createSignal, For, Show } from 'solid-js'
|
|
|
|
import { collapseToolOutput, truncate } from '../logic/toolOutput.ts'
|
|
import { useTheme } from './theme.tsx'
|
|
|
|
const GUTTER = 2
|
|
/** Max output lines shown when expanded (a sane cap to avoid huge renders). */
|
|
const EXPANDED_MAX = 200
|
|
|
|
export function ToolPart(props: { part: ToolPartState }) {
|
|
const theme = useTheme()
|
|
const dims = useTerminalDimensions()
|
|
const [expanded, setExpanded] = createSignal(false)
|
|
|
|
const bodyWidth = () => Math.max(20, dims().width - GUTTER - 4)
|
|
const result = () => (props.part.resultText ?? '').replace(/\s+$/, '')
|
|
const lines = () => (result() ? result().split('\n') : [])
|
|
const running = () => props.part.state === 'running'
|
|
const multiline = () => lines().length > 1
|
|
const collapsible = () => !running() && multiline()
|
|
// Collapsed gist: the explicit summary, else the first output line, else nothing.
|
|
const summary = () => (props.part.error ? `✗ ${props.part.error}` : (props.part.summary || lines()[0] || ''))
|
|
const body = createMemo(() => collapseToolOutput(result(), EXPANDED_MAX, bodyWidth() - 2))
|
|
|
|
const headGlyph = () => (collapsible() ? (expanded() ? '▼' : '▶') : '⚡')
|
|
const headColor = () => (props.part.error ? theme().color.error : theme().color.muted)
|
|
|
|
return (
|
|
<box style={{ flexDirection: 'column', flexShrink: 0, marginTop: 1 }}>
|
|
{/* header — clickable to toggle when there's expandable output */}
|
|
<box style={{ flexDirection: 'row', flexShrink: 0 }} onMouseDown={() => collapsible() && setExpanded(e => !e)}>
|
|
<box style={{ flexShrink: 0, width: GUTTER }}>
|
|
<text>
|
|
<span style={{ fg: headColor() }}>{headGlyph()}</span>
|
|
</text>
|
|
</box>
|
|
<box style={{ flexDirection: 'row', flexGrow: 1, minWidth: 0 }}>
|
|
<text>
|
|
<span style={{ fg: theme().color.label }}>{props.part.name}</span>
|
|
<Show when={running()}>
|
|
<span style={{ fg: theme().color.muted }}> …</span>
|
|
</Show>
|
|
<Show when={!running() && summary()}>
|
|
<span style={{ fg: props.part.error ? theme().color.error : theme().color.muted }}>
|
|
{` ${truncate(summary(), Math.max(1, bodyWidth() - props.part.name.length - 2))}`}
|
|
</span>
|
|
</Show>
|
|
<Show when={collapsible() && !expanded()}>
|
|
<span style={{ fg: theme().color.muted }}>{` (${lines().length} lines)`}</span>
|
|
</Show>
|
|
</text>
|
|
</box>
|
|
</box>
|
|
{/* expanded body — left-bar block of the (capped) output */}
|
|
<Show when={collapsible() && expanded()}>
|
|
<box style={{ flexDirection: 'row', flexGrow: 1, minWidth: 0, marginLeft: GUTTER }}>
|
|
<box
|
|
style={{
|
|
backgroundColor: props.part.error ? theme().color.error : theme().color.border,
|
|
flexShrink: 0,
|
|
width: 1
|
|
}}
|
|
/>
|
|
<box style={{ flexDirection: 'column', flexGrow: 1, minWidth: 0, paddingLeft: 1 }}>
|
|
<For each={body().lines}>
|
|
{line => (
|
|
<text>
|
|
<span style={{ fg: theme().color.muted }}>{line}</span>
|
|
</text>
|
|
)}
|
|
</For>
|
|
<Show when={body().hiddenLines > 0}>
|
|
<text>
|
|
<span style={{ fg: theme().color.accent }}>{`… +${body().hiddenLines} more lines`}</span>
|
|
</text>
|
|
</Show>
|
|
</box>
|
|
</box>
|
|
</Show>
|
|
</box>
|
|
)
|
|
}
|