fix(tui): surface verbose tool details (#30225)

* fix(tui): surface verbose tool details

Emit redacted structured verbose args/results to the TUI so /verbose verbose can show full tool detail without reopening stdout, and fail closed if redaction is unavailable.

Salvages #29011.

Co-authored-by: helix4u <4317663+helix4u@users.noreply.github.com>

* fix(tui): address verbose detail review

Label verbose tool failures as errors, cover forced verbose reasoning, and avoid new diff type warnings from the redaction regression tests.

* fix(tui): bound verbose tool payloads

Cap verbose tool detail text before emitting JSON-RPC events and preserve verbose results on inline diff completions.

* fix(tui): align termux argv test with gc flag

Update the stale TUI launch expectation so the Termux freshness path matches the current direct Node argv.

---------

Co-authored-by: helix4u <4317663+helix4u@users.noreply.github.com>
This commit is contained in:
brooklyn!
2026-05-22 00:16:52 -05:00
committed by GitHub
co-authored by helix4u
parent 4e2c66a098
commit 1264fab156
11 changed files with 306 additions and 38 deletions
@@ -342,6 +342,25 @@ describe('createGatewayEventHandler', () => {
expect(appended[appended.length - 1]).toMatchObject({ role: 'assistant', text: 'final answer' })
})
it('shows verbose reasoning even when normal reasoning display is off', () => {
vi.useFakeTimers()
patchUiState({ showReasoning: false })
const appended: Msg[] = []
const streamed = 'verbose-only reasoning'
try {
const onEvent = createGatewayEventHandler(buildCtx(appended))
onEvent({ payload: { text: streamed, verbose: true }, type: 'reasoning.delta' } as any)
vi.runOnlyPendingTimers()
expect(turnController.reasoningText).toBe(streamed)
expect(getTurnState().reasoning).toBe(streamed)
} finally {
vi.useRealTimers()
}
})
it('ignores fallback reasoning.available when streamed reasoning already exists', () => {
const appended: Msg[] = []
const streamed = 'short streamed reasoning'
@@ -485,6 +504,25 @@ describe('createGatewayEventHandler', () => {
expect(appended[3]?.text).not.toContain('```diff')
})
it('keeps verbose result text on inline_diff tool completions', () => {
const appended: Msg[] = []
const onEvent = createGatewayEventHandler(buildCtx(appended))
const diff = '--- a/foo.ts\n+++ b/foo.ts\n@@\n-old\n+new'
onEvent({
payload: { args_text: '{ "path": "foo.ts" }', context: 'foo.ts', name: 'patch', tool_id: 'tool-1' },
type: 'tool.start'
} as any)
onEvent({
payload: { inline_diff: diff, result_text: 'patched result', tool_id: 'tool-1' },
type: 'tool.complete'
} as any)
expect(turnController.segmentMessages[0]).toMatchObject({ kind: 'diff' })
expect(turnController.segmentMessages[0]?.tools?.[0]).toContain('Args:\n{ "path": "foo.ts" }')
expect(turnController.segmentMessages[0]?.tools?.[0]).toContain('Result:\npatched result')
})
it('keeps full final responses from duplicating flushed pre-diff narration', () => {
const appended: Msg[] = []
const onEvent = createGatewayEventHandler(buildCtx(appended))
+35 -1
View File
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import {
boundedLiveRenderText,
buildToolTrailLine,
buildVerboseToolTrailLine,
edgePreview,
estimateRows,
estimateTokensRough,
@@ -12,8 +13,8 @@ import {
lastCotTrailIndex,
parseToolTrailResultLine,
pasteTokenLabel,
sanitizeAnsiForRender,
sameToolTrailGroup,
sanitizeAnsiForRender,
splitToolDuration,
stripAnsi,
thinkingPreview
@@ -37,6 +38,39 @@ describe('buildToolTrailLine', () => {
})
})
describe('buildVerboseToolTrailLine', () => {
it('preserves multiline args and result details', () => {
const line = buildVerboseToolTrailLine(
'terminal',
'npm test',
false,
1.25,
'{\n "cmd": "npm test"\n}',
'first line\nsecond :: line'
)
expect(line).toContain('Args:\n{')
expect(line).toContain('Result:\nfirst line\nsecond :: line')
expect(parseToolTrailResultLine(line)).toEqual({
call: 'Terminal("npm test") (1.3s)',
detail: 'Args:\n{\n "cmd": "npm test"\n}\nResult:\nfirst line\nsecond :: line',
mark: '✓'
})
})
it('labels verbose failures as errors', () => {
const line = buildVerboseToolTrailLine('terminal', 'npm test', true, 0.5, undefined, 'command failed')
expect(line).toContain('Error:\ncommand failed')
expect(line).not.toContain('Result:\ncommand failed')
expect(parseToolTrailResultLine(line)).toEqual({
call: 'Terminal("npm test") (0.5s)',
detail: 'Error:\ncommand failed',
mark: '✗'
})
})
})
describe('lastCotTrailIndex', () => {
it('finds last non-result line', () => {
expect(lastCotTrailIndex(['a ✓', 'thinking…'])).toBe(1)