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
parent 4e2c66a098
commit 1264fab156
11 changed files with 306 additions and 38 deletions

View File

@@ -491,13 +491,13 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
case 'reasoning.delta':
if (ev.payload?.text) {
turnController.recordReasoningDelta(ev.payload.text)
turnController.recordReasoningDelta(ev.payload.text, Boolean(ev.payload.verbose))
}
return
case 'reasoning.available':
turnController.recordReasoningAvailable(String(ev.payload?.text ?? ''))
turnController.recordReasoningAvailable(String(ev.payload?.text ?? ''), Boolean(ev.payload?.verbose))
return
@@ -517,12 +517,18 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
case 'tool.start':
turnController.recordTodos(ev.payload.todos)
turnController.recordToolStart(ev.payload.tool_id, ev.payload.name ?? 'tool', ev.payload.context ?? '')
turnController.recordToolStart(
ev.payload.tool_id,
ev.payload.name ?? 'tool',
ev.payload.context ?? '',
ev.payload.args_text ? stripAnsi(String(ev.payload.args_text)) : undefined
)
return
case 'tool.complete': {
const inlineDiffText =
ev.payload.inline_diff && getUiState().inlineDiffs ? stripAnsi(String(ev.payload.inline_diff)).trim() : ''
const resultText = ev.payload.result_text ? stripAnsi(String(ev.payload.result_text)) : undefined
if (inlineDiffText) {
turnController.recordInlineDiffToolComplete(
@@ -530,7 +536,8 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
ev.payload.tool_id,
ev.payload.name,
ev.payload.error,
ev.payload.duration_s
ev.payload.duration_s,
resultText
)
} else {
turnController.recordToolComplete(
@@ -539,7 +546,8 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
ev.payload.error,
ev.payload.summary,
ev.payload.duration_s,
ev.payload.todos
ev.payload.todos,
resultText
)
}

View File

@@ -11,6 +11,7 @@ import { hasReasoningTag, splitReasoning } from '../lib/reasoning.js'
import {
boundedLiveRenderText,
buildToolTrailLine,
buildVerboseToolTrailLine,
estimateTokensRough,
isTransientTrailLine,
sameToolTrailGroup,
@@ -542,8 +543,8 @@ class TurnController {
}
}
recordReasoningAvailable(text: string) {
if (this.interrupted || !getUiState().showReasoning) {
recordReasoningAvailable(text: string, force = false) {
if (this.interrupted || (!force && !getUiState().showReasoning)) {
return
}
@@ -560,8 +561,8 @@ class TurnController {
this.pulseReasoningStreaming()
}
recordReasoningDelta(text: string) {
if (this.interrupted || !getUiState().showReasoning) {
recordReasoningDelta(text: string, force = false) {
if (this.interrupted || (!force && !getUiState().showReasoning)) {
return
}
@@ -587,14 +588,15 @@ class TurnController {
error?: string,
summary?: string,
duration?: number,
todos?: unknown
todos?: unknown,
resultText?: string
) {
if (this.interrupted) {
return
}
this.recordTodos(todos)
const line = this.completeTool(toolId, fallbackName, error, summary, duration)
const line = this.completeTool(toolId, fallbackName, error, summary, duration, resultText)
this.pendingSegmentTools = [...this.pendingSegmentTools, line]
this.flushPendingToolsIntoLastSegment()
@@ -606,30 +608,42 @@ class TurnController {
toolId: string,
fallbackName?: string,
error?: string,
duration?: number
duration?: number,
resultText?: string
) {
if (this.interrupted) {
return
}
this.flushStreamingSegment()
this.pushInlineDiffSegment(diffText, [this.completeTool(toolId, fallbackName, error, '', duration)])
this.pushInlineDiffSegment(diffText, [this.completeTool(toolId, fallbackName, error, '', duration, resultText)])
this.publishToolState()
}
private completeTool(toolId: string, fallbackName?: string, error?: string, summary?: string, duration?: number) {
private completeTool(
toolId: string,
fallbackName?: string,
error?: string,
summary?: string,
duration?: number,
resultText?: string
) {
const done = this.activeTools.find(tool => tool.id === toolId)
const name = done?.name ?? fallbackName ?? 'tool'
const label = toolTrailLabel(name)
const fallbackDuration = done?.startedAt ? (Date.now() - done.startedAt) / 1000 : undefined
const line = buildToolTrailLine(
name,
done?.context || '',
Boolean(error),
error || summary || '',
duration ?? fallbackDuration
)
const line =
done?.verboseArgs || resultText
? buildVerboseToolTrailLine(
name,
done?.context || '',
Boolean(error),
duration ?? fallbackDuration,
done?.verboseArgs,
error || resultText || summary || ''
)
: buildToolTrailLine(name, done?.context || '', Boolean(error), error || summary || '', duration ?? fallbackDuration)
this.activeTools = this.activeTools.filter(tool => tool.id !== toolId)
@@ -675,7 +689,7 @@ class TurnController {
}, STREAM_BATCH_MS)
}
recordToolStart(toolId: string, name: string, context: string) {
recordToolStart(toolId: string, name: string, context: string, verboseArgs?: string) {
if (this.interrupted) {
return
}
@@ -688,7 +702,7 @@ class TurnController {
const sample = `${name} ${context}`.trim()
this.toolTokenAcc += sample ? estimateTokensRough(sample) : 0
this.activeTools = [...this.activeTools, { context, id: toolId, name, startedAt: Date.now() }]
this.activeTools = [...this.activeTools, { context, id: toolId, name, startedAt: Date.now(), verboseArgs }]
patchTurnState({ toolTokens: this.toolTokenAcc, tools: this.activeTools })
}