Skip to content
Open
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
20 changes: 19 additions & 1 deletion tests/test_tui_gateway_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,24 @@ def fail_redaction(*_args, **_kwargs):
assert "result_text" not in events[1][2]


def test_tui_tool_display_fields_separate_raw_context_from_final_label():
from agent.display import get_friendly_tool_labels, set_friendly_tool_labels

previous = get_friendly_tool_labels()
try:
set_friendly_tool_labels(True)
assert server._tool_display_fields("read_file", {"path": "package.json", "offset": 1, "limit": 300}) == {
"context": "package.json L1-300",
"label": "Reading package.json L1-300",
}
assert server._tool_display_fields("custom_tool", {"query": "alpha"}) == {"context": "alpha"}

set_friendly_tool_labels(False)
assert server._tool_display_fields("read_file", {"path": "package.json"}) == {"context": "package.json"}
finally:
set_friendly_tool_labels(previous)


def test_tui_tool_output_risk_event_exposes_metadata_without_raw_output(monkeypatch):
events: list[tuple[str, str, dict]] = []
monkeypatch.setattr(
Expand Down Expand Up @@ -935,7 +953,7 @@ def test_history_to_messages_preserves_tool_calls_for_resume_display():

assert server._history_to_messages(history) == [
{"role": "user", "text": "first prompt"},
{"context": "Searching files for resume", "name": "search_files", "role": "tool"},
{"context": "resume", "label": "Searching files for resume", "name": "search_files", "role": "tool"},
{"role": "assistant", "text": "first answer"},
{"role": "user", "text": "second prompt"},
]
Expand Down
21 changes: 14 additions & 7 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3435,13 +3435,19 @@ def _session_info(agent, session: dict | None = None) -> dict:
return info


def _tool_ctx(name: str, args: dict) -> str:
def _tool_display_fields(name: str, args: dict) -> dict[str, str]:
try:
from agent.display import build_tool_label
from agent.display import build_tool_label, build_tool_preview

return build_tool_label(name, args, max_len=80) or ""
context = build_tool_preview(name, args, max_len=80) or ""
label = build_tool_label(name, args, max_len=80) or ""
# Keep context raw for clients that add the tool name themselves; label is final display text.
fields = {"context": context}
if label and label != context:
fields["label"] = label
return fields
except Exception:
return ""
return {"context": ""}


def _emit_session_info_for_session(sid: str, session: dict) -> None:
Expand Down Expand Up @@ -3593,7 +3599,7 @@ def _on_tool_start(sid: str, tool_call_id: str, name: str, args: dict):
payload = {
"tool_id": tool_call_id,
"name": name,
"context": _tool_ctx(name, args),
**_tool_display_fields(name, args),
}
if _session_verbose(sid):
args_text = _tool_args_text(args)
Expand Down Expand Up @@ -4289,7 +4295,8 @@ def progress(message: str, level: str = "info") -> None:

def tool_start(tool_call_id: str, name: str, args: dict) -> None:
started_at[tool_call_id] = time.time()
ctx = _tool_ctx(name, args)
fields = _tool_display_fields(name, args)
ctx = fields.get("label") or fields["context"]
progress(f"Running {name}{f': {ctx}' if ctx else ''}")

def tool_complete(tool_call_id: str, name: str, _args: dict, result: str) -> None:
Expand Down Expand Up @@ -4940,7 +4947,7 @@ def _history_to_messages(history: list[dict]) -> list[dict]:
name = (tc_info[0] if tc_info else None) or m.get("tool_name") or "tool"
args = (tc_info[1] if tc_info else None) or {}
messages.append(
{"role": "tool", "name": name, "context": _tool_ctx(name, args)}
{"role": "tool", "name": name, **_tool_display_fields(name, args)}
)
continue
# An assistant turn may carry only reasoning/thinking content with no
Expand Down
24 changes: 24 additions & 0 deletions ui-tui/src/__tests__/createGatewayEventHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,30 @@ describe('createGatewayEventHandler', () => {
expect(toolTrails[0]?.tools?.[1]).toContain('Read File')
})

it('keeps final labels separate from raw context through live and completed tool state', () => {
const onEvent = createGatewayEventHandler(buildCtx([]))

onEvent({
payload: {
context: 'package.json L1-300',
label: 'Reading package.json L1-300',
name: 'read_file',
tool_id: 'tool-1'
},
type: 'tool.start'
} as any)

expect(getTurnState().tools[0]).toMatchObject({
context: 'package.json L1-300',
label: 'Reading package.json L1-300',
name: 'read_file'
})

onEvent({ payload: { duration_s: 0.5, name: 'read_file', tool_id: 'tool-1' }, type: 'tool.complete' } as any)

expect(getTurnState().streamPendingTools[0]).toBe('Reading package.json L1-300 (0.5s) ✓')
})

it('keeps tool tokens across handler recreation mid-turn', () => {
const appended: Msg[] = []

Expand Down
95 changes: 70 additions & 25 deletions ui-tui/src/__tests__/messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,50 @@ import React from 'react'
import { describe, expect, it } from 'vitest'

import { MessageLine } from '../components/messageLine.js'
import { ToolTrail } from '../components/thinking.js'
import { toTranscriptMessages } from '../domain/messages.js'
import { upsert } from '../lib/messages.js'
import { stripAnsi } from '../lib/text.js'
import { DEFAULT_THEME } from '../theme.js'
import type { SubagentProgress } from '../types.js'

const renderText = (element: React.ReactElement, columns = 80) => {
const stdout = new PassThrough()
const stdin = new PassThrough()
const stderr = new PassThrough()
let output = ''

Object.assign(stdout, { columns, isTTY: false, rows: 24 })
Object.assign(stdin, { isTTY: false })
Object.assign(stderr, { isTTY: false })
stdout.on('data', chunk => {
output += chunk.toString()
})

const instance = renderSync(element, {
patchConsole: false,
stderr: stderr as NodeJS.WriteStream,
stdin: stdin as NodeJS.ReadStream,
stdout: stdout as NodeJS.WriteStream
})

instance.unmount()
instance.cleanup()

return stripAnsi(output)
}

describe('toTranscriptMessages', () => {
it('preserves assistant tool-call rows so resume does not drop prior turns', () => {
const rows = [
{ role: 'user', text: 'first prompt' },
{ role: 'tool', context: 'repo', name: 'search_files', text: 'ignored raw result' },
{
role: 'tool',
context: 'repo',
label: 'Searching files for repo',
name: 'search_files',
text: 'ignored raw result'
},
{ role: 'assistant', text: 'first answer' },
{ role: 'user', text: 'second prompt' }
]
Expand All @@ -24,46 +58,25 @@ describe('toTranscriptMessages', () => {
['assistant', 'first answer'],
['user', 'second prompt']
])
expect(toTranscriptMessages(rows)[1]?.tools?.[0]).toContain('Search Files')
expect(toTranscriptMessages(rows)[1]?.tools?.[0]).toBe('Searching files for repo ✓')
})
})

describe('MessageLine', () => {
it('preserves a separator after compound user prompt glyphs in transcript rows', () => {
const stdout = new PassThrough()
const stdin = new PassThrough()
const stderr = new PassThrough()
let output = ''

Object.assign(stdout, { columns: 80, isTTY: false, rows: 24 })
Object.assign(stdin, { isTTY: false })
Object.assign(stderr, { isTTY: false })
stdout.on('data', chunk => {
output += chunk.toString()
})

const t = {
...DEFAULT_THEME,
brand: { ...DEFAULT_THEME.brand, prompt: 'Ψ >' }
}

const instance = renderSync(
const output = renderText(
React.createElement(MessageLine, {
cols: 80,
msg: { role: 'user', text: 'Okay' },
t
}),
{
patchConsole: false,
stderr: stderr as NodeJS.WriteStream,
stdin: stdin as NodeJS.ReadStream,
stdout: stdout as NodeJS.WriteStream
}
})
)

instance.unmount()
instance.cleanup()

const renderedLine = stripAnsi(output)
.split('\n')
.find(line => line.includes('Okay'))
Expand All @@ -72,6 +85,38 @@ describe('MessageLine', () => {
})
})

describe('ToolTrail', () => {
it('keeps friendly delegate labels attached to their inline subagents', () => {
const subagent: SubagentProgress = {
depth: 0,
goal: 'Inspect regression',
id: 'agent-1',
index: 0,
notes: [],
parentId: null,
status: 'running',
taskCount: 1,
thinking: [],
toolCount: 0,
tools: []
}

const output = renderText(
React.createElement(ToolTrail, {
detailsMode: 'expanded',
subagents: [subagent],
t: DEFAULT_THEME,
tools: [{ id: 'delegate-1', label: 'Delegating inspect regression', name: 'delegate_task' }]
})
)

expect(output).toContain('Delegating inspect regression')
expect(output).toContain('/agents to monitor')
expect(output).toContain('Inspect regression')
expect(output).not.toContain('Spawn tree')
})
})

describe('upsert', () => {
it('appends when last role differs', () => {
expect(upsert([{ role: 'user', text: 'hi' }], 'assistant', 'hello')).toHaveLength(2)
Expand Down
11 changes: 11 additions & 0 deletions ui-tui/src/__tests__/text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
estimateRows,
estimateTokensRough,
fmtK,
formatToolCall,
hasAnsi,
isToolTrailResultLine,
lastCotTrailIndex,
Expand Down Expand Up @@ -36,6 +37,16 @@ describe('buildToolTrailLine', () => {
expect(parseToolTrailResultLine(line)).toEqual({ call: 'Read File("x") (0.9s)', detail: '', mark: '✓' })
expect(splitToolDuration('Read File("x") (0.9s)')).toEqual({ label: 'Read File("x")', duration: ' (0.9s)' })
})

it('renders a final friendly label without formatting it as context', () => {
expect(formatToolCall('read_file', 'package.json L1-300', 'Reading package.json L1-300')).toBe(
'Reading package.json L1-300'
)
expect(buildToolTrailLine('read_file', 'package.json L1-300', false, '', 0.5, 'Reading package.json L1-300')).toBe(
'Reading package.json L1-300 (0.5s) ✓'
)
expect(formatToolCall('read_file', 'package.json L1-300')).toBe('Read File("package.json L1-300")')
})
})

describe('buildVerboseToolTrailLine', () => {
Expand Down
1 change: 1 addition & 0 deletions ui-tui/src/app/createGatewayEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
ev.payload.tool_id,
ev.payload.name ?? 'tool',
ev.payload.context ?? '',
ev.payload.label,
ev.payload.args_text ? stripAnsi(String(ev.payload.args_text)) : undefined
)

Expand Down
10 changes: 6 additions & 4 deletions ui-tui/src/app/turnController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,14 +803,16 @@ class TurnController {
Boolean(error),
duration ?? fallbackDuration,
done?.verboseArgs,
error || resultText || summary || ''
error || resultText || summary || '',
done?.label
)
: buildToolTrailLine(
name,
done?.context || '',
Boolean(error),
error || summary || '',
duration ?? fallbackDuration
duration ?? fallbackDuration,
done?.label
)

this.activeTools = this.activeTools.filter(tool => tool.id !== toolId)
Expand Down Expand Up @@ -857,7 +859,7 @@ class TurnController {
}, STREAM_BATCH_MS)
}

recordToolStart(toolId: string, name: string, context: string, verboseArgs?: string) {
recordToolStart(toolId: string, name: string, context: string, label?: string, verboseArgs?: string) {
if (this.interrupted) {
return
}
Expand All @@ -870,7 +872,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(), verboseArgs }]
this.activeTools = [...this.activeTools, { context, id: toolId, label, name, startedAt: Date.now(), verboseArgs }]

patchTurnState({ toolTokens: this.toolTokenAcc, tools: this.activeTools })
}
Expand Down
9 changes: 6 additions & 3 deletions ui-tui/src/components/thinking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ const fmtElapsed = (ms: number) => {
return sec < 10 ? `${sec.toFixed(1)}s` : `${Math.round(sec)}s`
}

const isDelegateToolLabel = (label: string) =>
label.startsWith('Delegate Task') || label === 'Delegating' || label.startsWith('Delegating ')

type TreeBranch = 'mid' | 'last'
type TreeRails = readonly boolean[]

Expand Down Expand Up @@ -837,7 +840,7 @@ export const ToolTrail = memo(function ToolTrail({
}

for (const tool of tools) {
const label = formatToolCall(tool.name, tool.context || '')
const label = formatToolCall(tool.name, tool.context || '', tool.label)
Comment thread
CNSeniorious000 marked this conversation as resolved.

groups.push({
color: t.color.text,
Expand Down Expand Up @@ -886,7 +889,7 @@ export const ToolTrail = memo(function ToolTrail({
const toolTokensLabel = toolTokens !== undefined && toolTokens > 0 ? `~${fmtK(toolTokens)} tokens` : undefined

const totalTokensLabel = tokenCount > 0 && toolTokenCount > 0 ? `~${fmtK(totalTokenCount)} total` : null
const delegateGroups = groups.filter(g => g.label.startsWith('Delegate Task'))
const delegateGroups = groups.filter(g => isDelegateToolLabel(g.label))
const inlineDelegateKey = hasSubagents && delegateGroups.length === 1 ? delegateGroups[0]!.key : null

const toolLabel = (group: Group) => {
Expand Down Expand Up @@ -1063,7 +1066,7 @@ export const ToolTrail = memo(function ToolTrail({
// Surface the /agents hint the moment a delegate group appears —
// while it's still in-flight and before any subagent has
// registered — so users can open the live monitor immediately.
const isDelegateGroup = group.label.startsWith('Delegate Task')
const isDelegateGroup = isDelegateToolLabel(group.label)

return (
<Box flexDirection="column" key={group.key}>
Expand Down
5 changes: 3 additions & 2 deletions ui-tui/src/domain/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ export const toTranscriptMessages = (rows: unknown): Msg[] => {
continue
}

const { context, name, role, text } = row as TranscriptRow
const { context, label, name, role, text } = row as TranscriptRow

if (role === 'tool') {
pending.push(buildToolTrailLine(name ?? 'tool', context ?? ''))
pending.push(buildToolTrailLine(name ?? 'tool', context ?? '', false, undefined, undefined, label ?? ''))

continue
}
Expand Down Expand Up @@ -85,6 +85,7 @@ interface ImageMeta {

interface TranscriptRow {
context?: string
label?: string
name?: string
role?: string
text?: string
Expand Down
Loading
Loading