fix(tui): collapse inline tool errors

This commit is contained in:
Kit Langton 2026-05-21 15:56:53 -04:00
parent e03ffb9ed4
commit dd4aa2ab5a
3 changed files with 52 additions and 15 deletions

View file

@ -1742,6 +1742,7 @@ function InlineTool(props: {
const sync = useSync()
const renderer = useRenderer()
const [hover, setHover] = createSignal(false)
const [errorExpanded, setErrorExpanded] = createSignal(false)
const permission = createMemo(() => {
const callID = sync.data.permission[ctx.sessionID]?.at(0)?.tool?.callID
@ -1749,13 +1750,6 @@ function InlineTool(props: {
return callID === props.part.callID
})
const fg = createMemo(() => {
if (permission()) return theme.warning
if (hover() && props.onClick) return theme.text
if (props.complete) return theme.textMuted
return theme.text
})
const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error : undefined))
const denied = createMemo(
@ -1766,14 +1760,28 @@ function InlineTool(props: {
error()?.includes("user dismissed"),
)
const failed = createMemo(() => Boolean(error() && !denied()))
const clickable = createMemo(() => Boolean(props.onClick || failed()))
const fg = createMemo(() => {
if (permission()) return theme.warning
if (failed()) return theme.error
if (hover() && props.onClick) return theme.text
if (props.complete) return theme.textMuted
return theme.text
})
return (
<box
marginTop={margin()}
paddingLeft={3}
onMouseOver={() => props.onClick && setHover(true)}
onMouseOver={() => clickable() && setHover(true)}
onMouseOut={() => setHover(false)}
onMouseUp={() => {
if (renderer.getSelection()?.getSelectedText()) return
if (failed()) {
setErrorExpanded((value) => !value)
return
}
props.onClick?.()
}}
renderBefore={function () {
@ -1804,19 +1812,23 @@ function InlineTool(props: {
<box flexDirection="row">
<text
width={INLINE_TOOL_ICON_WIDTH}
fg={props.iconColor ?? fg()}
fg={failed() ? theme.error : (props.iconColor ?? fg())}
attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}
>
{props.icon}
</text>
<text flexGrow={1} fg={fg()} attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}>
<text
flexGrow={1}
fg={failed() ? theme.error : fg()}
attributes={denied() ? TextAttributes.STRIKETHROUGH : undefined}
>
{props.children}
</text>
</box>
</Show>
</Match>
</Switch>
<Show when={error() && !denied()}>
<Show when={failed() && errorExpanded()}>
<box paddingLeft={INLINE_TOOL_ICON_WIDTH}>
<text fg={theme.error}>{error()}</text>
</box>

View file

@ -1,6 +1,16 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`TUI inline tool wrapping snapshots consecutive grep, glob, and read rows at a narrow width 1`] = `
" ✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
✱ Glob "**/*db*" in packages/opencode (6 matches)
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
Path\\.data|data =" in packages/opencode/src (115 matches)"
`;
exports[`TUI inline tool wrapping snapshots expanded tool errors under the tool text 1`] = `
" ✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
✱ Glob "**/*db*" in packages/opencode (6 matches)

View file

@ -39,7 +39,7 @@ const tools: readonly ToolFixture[] = [
},
] as const
function InlineToolRow(props: { item: ToolFixture }) {
function InlineToolRow(props: { item: ToolFixture; errorExpanded?: boolean }) {
const [margin, setMargin] = createSignal(0)
return (
@ -56,7 +56,7 @@ function InlineToolRow(props: { item: ToolFixture }) {
<text width={INLINE_TOOL_ICON_WIDTH}>{props.item.icon}</text>
<text flexGrow={1}>{props.item.label}</text>
</box>
{props.item.error && (
{props.item.error && props.errorExpanded && (
<box paddingLeft={INLINE_TOOL_ICON_WIDTH}>
<text>{props.item.error}</text>
</box>
@ -65,11 +65,11 @@ function InlineToolRow(props: { item: ToolFixture }) {
)
}
function Fixture() {
function Fixture(props: { errorExpanded?: boolean }) {
return (
<box flexDirection="column" width={72}>
<box flexDirection="column">
<For each={tools}>{(item) => <InlineToolRow item={item} />}</For>
<For each={tools}>{(item) => <InlineToolRow item={item} errorExpanded={props.errorExpanded} />}</For>
</box>
</box>
)
@ -90,4 +90,19 @@ describe("TUI inline tool wrapping", () => {
.trimEnd(),
).toMatchSnapshot()
})
test("snapshots expanded tool errors under the tool text", async () => {
testSetup = await testRender(() => <Fixture errorExpanded />, { width: 72, height: 12 })
await testSetup.renderOnce()
await testSetup.renderOnce()
expect(
testSetup
.captureCharFrame()
.split("\n")
.map((line) => line.trimEnd())
.join("\n")
.trimEnd(),
).toMatchSnapshot()
})
})