fix(tui): show terminal tool failure labels (#31934)

This commit is contained in:
Aiden Cline
2026-06-11 12:19:31 -05:00
committed by GitHub
parent 2e71292f2f
commit a150424c16
2 changed files with 53 additions and 4 deletions
+13 -4
View File
@@ -1831,6 +1831,7 @@ function InlineTool(props: {
color?: RGBA color?: RGBA
complete: unknown complete: unknown
pending: string pending: string
failure?: string
spinner?: boolean spinner?: boolean
subagent?: boolean subagent?: boolean
children: JSX.Element children: JSX.Element
@@ -1884,6 +1885,7 @@ function InlineTool(props: {
errorExpanded={errorExpanded()} errorExpanded={errorExpanded()}
complete={props.complete} complete={props.complete}
pending={props.pending} pending={props.pending}
failure={props.failure}
spinner={props.spinner} spinner={props.spinner}
subagent={props.subagent} subagent={props.subagent}
separateAfter={(id) => id !== undefined && ctx.userMessageIDs().has(id)} separateAfter={(id) => id !== undefined && ctx.userMessageIDs().has(id)}
@@ -1915,6 +1917,7 @@ export function InlineToolRow(props: {
errorExpanded?: boolean errorExpanded?: boolean
complete: unknown complete: unknown
pending: string pending: string
failure?: string
spinner?: boolean spinner?: boolean
subagent?: boolean subagent?: boolean
children: JSX.Element children: JSX.Element
@@ -1958,7 +1961,7 @@ export function InlineToolRow(props: {
~ {props.pending} ~ {props.pending}
</text> </text>
} }
when={props.complete} when={props.complete || props.failed}
> >
<box flexDirection="row"> <box flexDirection="row">
<text <text
@@ -1973,7 +1976,7 @@ export function InlineToolRow(props: {
fg={props.failed ? props.errorColor : props.color} fg={props.failed ? props.errorColor : props.color}
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined} attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
> >
{props.children} {props.failed && !props.complete ? (props.failure ?? props.children) : props.children}
</text> </text>
</box> </box>
</Show> </Show>
@@ -2445,7 +2448,7 @@ function ApplyPatch(props: ToolProps) {
</For> </For>
</Match> </Match>
<Match when={true}> <Match when={true}>
<InlineTool icon="%" pending="Preparing patch..." complete={false} part={props.part}> <InlineTool icon="%" pending="Preparing patch..." failure="Patch failed" complete={false} part={props.part}>
Patch Patch
</InlineTool> </InlineTool>
</Match> </Match>
@@ -2465,7 +2468,13 @@ function TodoWrite(props: ToolProps) {
</BlockTool> </BlockTool>
</Match> </Match>
<Match when={true}> <Match when={true}>
<InlineTool icon="⚙" pending="Updating todos..." complete={false} part={props.part}> <InlineTool
icon="⚙"
pending="Updating todos..."
failure="Todo update failed"
complete={false}
part={props.part}
>
Updating todos... Updating todos...
</InlineTool> </InlineTool>
</Match> </Match>
@@ -155,6 +155,34 @@ function StickyScrollFixture(props: { separated: boolean; scroll: (scroll: Scrol
) )
} }
function FailedPendingToolFixture() {
return (
<InlineToolRow
icon="%"
complete={false}
pending="Preparing patch..."
failed={true}
failure="Patch failed"
>
Patch
</InlineToolRow>
)
}
function FailedCompleteToolFixture() {
return (
<InlineToolRow
icon="→"
complete={true}
pending="Reading file..."
failed={true}
failure="Read failed"
>
Read src/index.ts
</InlineToolRow>
)
}
async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) { async function renderFrame(component: () => JSX.Element, options: { width: number; height: number }) {
testSetup = await testRender(component, options) testSetup = await testRender(component, options)
await testSetup.renderOnce() await testSetup.renderOnce()
@@ -173,6 +201,18 @@ describe("TUI inline tool wrapping", () => {
expect(toolDisplay("plugin_tool")).toBe("generic") expect(toolDisplay("plugin_tool")).toBe("generic")
}) })
test("replaces pending copy when a tool fails before completion", async () => {
const frame = await renderFrame(() => <FailedPendingToolFixture />, { width: 72, height: 3 })
expect(frame).toContain("Patch failed")
expect(frame).not.toContain("Preparing patch")
})
test("preserves useful completed copy when a tool fails", async () => {
const frame = await renderFrame(() => <FailedCompleteToolFixture />, { width: 72, height: 3 })
expect(frame).toContain("Read src/index.ts")
expect(frame).not.toContain("Read failed")
})
test("filters malformed nested tool wire data", () => { test("filters malformed nested tool wire data", () => {
expect( expect(
parseApplyPatchFiles([ parseApplyPatchFiles([