Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 50 additions & 7 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,10 @@ def _session_tool_progress_mode(sid: str) -> str:
return str(_sessions.get(sid, {}).get("tool_progress_mode", "all") or "all")


def _session_verbose(sid: str) -> bool:
return _session_tool_progress_mode(sid) == "verbose"


def _tool_progress_enabled(sid: str) -> bool:
return _session_tool_progress_mode(sid) != "off"

Expand Down Expand Up @@ -1461,6 +1465,33 @@ def _tool_ctx(name: str, args: dict) -> str:
return ""


def _redact_tui_verbose_text(text: str) -> str:
try:
from agent.redact import redact_sensitive_text

return redact_sensitive_text(str(text), force=True)
except Exception:
return str(text)


def _tool_args_text(args: dict) -> str:
try:
raw = json.dumps(args or {}, indent=2, ensure_ascii=False, default=str)
except Exception:
raw = str(args or {})
return _redact_tui_verbose_text(raw)


def _tool_result_text(result: object) -> str:
try:
from agent.tool_dispatch_helpers import _multimodal_text_summary

raw = _multimodal_text_summary(result)
except Exception:
raw = str(result)
return _redact_tui_verbose_text(raw)


def _fmt_tool_duration(seconds: float | None) -> str:
if seconds is None:
return ""
Expand Down Expand Up @@ -1522,13 +1553,16 @@ def _on_tool_start(sid: str, tool_call_id: str, name: str, args: dict):
pass
session.setdefault("tool_started_at", {})[tool_call_id] = time.time()
if _tool_progress_enabled(sid):
payload = {
"tool_id": tool_call_id,
"name": name,
"context": _tool_ctx(name, args),
}
if _session_verbose(sid):
payload["args_text"] = _tool_args_text(args)
# tool.complete is the source of truth for todos (full list from the
# tool result). args.todos here may be a partial merge update.
_emit(
"tool.start",
sid,
{"tool_id": tool_call_id, "name": name, "context": _tool_ctx(name, args)},
)
_emit("tool.start", sid, payload)


def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result: str):
Expand All @@ -1545,6 +1579,8 @@ def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result
summary = _tool_summary(name, result, duration_s)
if summary:
payload["summary"] = summary
if _session_verbose(sid):
payload["result_text"] = _tool_result_text(result)
if name == "todo":
try:
data = json.loads(result)
Expand Down Expand Up @@ -1584,7 +1620,10 @@ def _on_tool_progress(
_emit("tool.progress", sid, {"name": name, "preview": preview or ""})
return
if event_type == "reasoning.available" and preview:
_emit("reasoning.available", sid, {"text": str(preview)})
payload = {"text": str(preview)}
if _session_verbose(sid):
payload["verbose"] = True
_emit("reasoning.available", sid, payload)
return
if event_type.startswith("subagent."):
payload = {
Expand Down Expand Up @@ -1660,7 +1699,11 @@ def _agent_cbs(sid: str) -> dict:
"tool_gen_callback": lambda name: _tool_progress_enabled(sid)
and _emit("tool.generating", sid, {"name": name}),
"thinking_callback": lambda text: _emit("thinking.delta", sid, {"text": text}),
"reasoning_callback": lambda text: _emit("reasoning.delta", sid, {"text": text}),
"reasoning_callback": lambda text: _emit(
"reasoning.delta",
sid,
{"text": text, **({"verbose": True} if _session_verbose(sid) else {})},
),
"status_callback": lambda kind, text=None: _status_update(
sid, str(kind), None if text is None else str(text)
),
Expand Down
24 changes: 23 additions & 1 deletion ui-tui/src/__tests__/text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import {
boundedLiveRenderText,
buildToolTrailLine,
buildVerboseToolTrailLine,
edgePreview,
estimateRows,
estimateTokensRough,
Expand All @@ -12,8 +13,8 @@ import {
lastCotTrailIndex,
parseToolTrailResultLine,
pasteTokenLabel,
sanitizeAnsiForRender,
sameToolTrailGroup,
sanitizeAnsiForRender,
splitToolDuration,
stripAnsi,
thinkingPreview
Expand All @@ -37,6 +38,27 @@ 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: 'βœ“'
})
})
})

describe('lastCotTrailIndex', () => {
it('finds last non-result line', () => {
expect(lastCotTrailIndex(['a βœ“', 'thinking…'])).toBe(1)
Expand Down
14 changes: 10 additions & 4 deletions ui-tui/src/app/createGatewayEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -517,7 +517,12 @@ 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': {
Expand All @@ -539,7 +544,8 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
ev.payload.error,
ev.payload.summary,
ev.payload.duration_s,
ev.payload.todos
ev.payload.todos,
ev.payload.result_text ? stripAnsi(String(ev.payload.result_text)) : undefined
)
}

Expand Down
45 changes: 29 additions & 16 deletions ui-tui/src/app/turnController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { hasReasoningTag, splitReasoning } from '../lib/reasoning.js'
import {
boundedLiveRenderText,
buildToolTrailLine,
buildVerboseToolTrailLine,
estimateTokensRough,
isTransientTrailLine,
sameToolTrailGroup,
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}

Expand All @@ -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()
Expand All @@ -617,19 +619,30 @@ class TurnController {
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)

Expand Down Expand Up @@ -675,7 +688,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
}
Expand All @@ -688,7 +701,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 })
}
Expand Down
11 changes: 10 additions & 1 deletion ui-tui/src/components/thinking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,16 @@ export const ToolTrail = memo(function ToolTrail({
color: t.color.text,
key: tool.id,
label,
details: [],
details: tool.verboseArgs
? [
{
color: t.color.muted,
content: `Args:\n${boundedLiveRenderText(tool.verboseArgs)}`,
dimColor: true,
key: `${tool.id}-args`
}
]
: [],
content: (
<>
<Spinner color={t.color.accent} variant="tool" /> {label}
Expand Down
5 changes: 3 additions & 2 deletions ui-tui/src/gatewayTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,11 +477,11 @@ export type GatewayEvent =
type: 'gateway.start_timeout'
}
| { payload?: { preview?: string }; session_id?: string; type: 'gateway.protocol_error' }
| { payload?: { text?: string }; session_id?: string; type: 'reasoning.delta' | 'reasoning.available' }
| { payload?: { text?: string; verbose?: boolean }; session_id?: string; type: 'reasoning.delta' | 'reasoning.available' }
| { payload: { name?: string; preview?: string }; session_id?: string; type: 'tool.progress' }
| { payload: { name?: string }; session_id?: string; type: 'tool.generating' }
| {
payload: { context?: string; name?: string; tool_id: string; todos?: unknown[] }
payload: { args_text?: string; context?: string; name?: string; tool_id: string; todos?: unknown[] }
session_id?: string
type: 'tool.start'
}
Expand All @@ -491,6 +491,7 @@ export type GatewayEvent =
error?: string
inline_diff?: string
name?: string
result_text?: string
summary?: string
tool_id: string
todos?: unknown[]
Expand Down
26 changes: 23 additions & 3 deletions ui-tui/src/lib/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,26 @@ export const buildToolTrailLine = (
return `${formatToolCall(name, context)}${took}${detail ? ` :: ${detail}` : ''} ${error ? 'βœ—' : 'βœ“'}`
}

const verboseToolBlock = (label: string, text?: string) => {
const body = (text ?? '').trim()

return body ? `${label}:\n${boundedLiveRenderText(body)}` : ''
}

export const buildVerboseToolTrailLine = (
name: string,
context: string,
error?: boolean,
duration?: number,
argsText?: string,
resultText?: string
) => {
const detail = [verboseToolBlock('Args', argsText), verboseToolBlock('Result', resultText)].filter(Boolean).join('\n')
const took = duration !== undefined ? ` (${duration.toFixed(1)}s)` : ''

return `${formatToolCall(name, context)}${took}${detail ? ` :: ${detail}` : ''} ${error ? 'βœ—' : 'βœ“'}`
}

export const isToolTrailResultLine = (line: string) => line.endsWith(' βœ“') || line.endsWith(' βœ—')

export const parseToolTrailResultLine = (line: string) => {
Expand All @@ -221,10 +241,10 @@ export const parseToolTrailResultLine = (line: string) => {

const mark = line.endsWith(' βœ—') ? 'βœ—' : 'βœ“'
const body = line.slice(0, -2)
const [call, detail] = body.split(' :: ', 2)
const sep = body.indexOf(' :: ')

if (detail != null) {
return { call, detail, mark }
if (sep >= 0) {
return { call: body.slice(0, sep), detail: body.slice(sep + 4), mark }
}

const legacy = body.indexOf(': ')
Expand Down
1 change: 1 addition & 0 deletions ui-tui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export interface ActiveTool {
context?: string
id: string
name: string
verboseArgs?: string
startedAt?: number
}

Expand Down
Loading