fix(desktop): compact tool row titles

Make completed desktop tool rows read like useful activity labels instead of raw plumbing: terminal rows use a dispatch-style shell summarizer for agent wrappers, and read_file rows keep the action plus filename and requested line range.

The shell cleanup follows condensed-milk-pi's shape: split command compounds on real separators, strip pipe tails inside each segment, clean redirects/env prefixes, then classify setup/banner/status segments. Multi-command probes render as `first command + N commands`; the full command remains available in copy/detail.

Read rows now render as `Read package.json` or `Read main.ts L25-34`, using requested positive offset/limit and returned line numbers only as fallback for negative/unknown offsets.
This commit is contained in:
Brooklyn Nicholson 2026-06-25 00:00:58 -05:00
parent 7a65800fed
commit 41f302fa73
8 changed files with 480 additions and 12 deletions

View file

@ -140,10 +140,10 @@ describe('buildToolView title actions', () => {
expect(read.titleAction).toEqual({ prefix: '', text: 'Reading', suffix: ' file' })
expect(web.title).toBe('Reading example.com/docs')
expect(web.titleAction).toEqual({ prefix: '', text: 'Reading', suffix: ' example.com/docs' })
expect(terminal.title).toBe('Running · npm test -- --runInBand')
expect(terminal.titleAction).toEqual({ prefix: '', text: 'Running', suffix: ' · npm test -- --runInBand' })
expect(code.title).toBe('Scripting · print("hello")')
expect(code.titleAction).toEqual({ prefix: '', text: 'Scripting', suffix: ' · print("hello")' })
expect(terminal.title).toBe('Running npm test -- --runInBand')
expect(terminal.titleAction).toEqual({ prefix: '', text: 'Running', suffix: ' npm test -- --runInBand' })
expect(code.title).toBe('Scripting print("hello")')
expect(code.titleAction).toEqual({ prefix: '', text: 'Scripting', suffix: ' print("hello")' })
})
it('does not mark completed tool titles as pending actions', () => {
@ -153,6 +153,92 @@ describe('buildToolView title actions', () => {
expect(view.titleAction).toBeUndefined()
})
it('uses the filename for completed read_file rows', () => {
const view = buildToolView(
part({ args: { path: './package.json' }, result: { content: '1|{"name":"demo"}' }, toolName: 'read_file' }),
''
)
expect(view.title).toBe('Read package.json')
expect(view.subtitle).toBe('')
expect(view.titleAction).toBeUndefined()
})
it('adds a compact line range to line-scoped read_file rows', () => {
const view = buildToolView(
part({
args: { limit: 10, offset: 25, path: './src/main.ts' },
result: { content: '25|function toggleDock() {\n26| dock.classList.toggle("hidden");\n34|}' },
toolName: 'read_file'
}),
''
)
expect(view.title).toBe('Read main.ts L25-34')
expect(view.subtitle).toBe('')
})
it('uses the requested positive offset/limit for read_file row line ranges', () => {
const view = buildToolView(
part({
args: { limit: 5, offset: 1, path: './package.json' },
result: { content: '1|{\n2| "name": "bb-rainbows",\n3| "private": true,\n4| "version": "0.0.1",\n5| "type": "module",\n6| "description": "extra"' },
toolName: 'read_file'
}),
''
)
expect(view.title).toBe('Read package.json L1-5')
})
it('uses returned line numbers for negative-offset read_file rows', () => {
const view = buildToolView(
part({
args: { limit: 2, offset: -2, path: './src/main.ts' },
result: { content: '99|lastLine();\n100|done();' },
toolName: 'read_file'
}),
''
)
expect(view.title).toBe('Read main.ts L99-100')
})
it('renders compact terminal titles for session 20260624_231846_bdbd1e commands', () => {
const rows = [
[
'cd /Users/brooklyn/www/bb-rainbows && pnpm run lint 2>&1 | tail -20; echo "lint_exit=${PIPESTATUS[0]}"',
'Ran pnpm run lint'
],
[
'cd /Users/brooklyn/www/bb-rainbows && pnpm run build 2>&1 | tail -20; echo "build_exit=${PIPESTATUS[0]}"',
'Ran pnpm run build'
],
[
'which node pnpm corepack; node -v; echo "---"; corepack --version 2>&1; echo "---pnpm via corepack---"; pnpm --version 2>&1 | tail -5',
'Ran which node pnpm corepack + 3 commands'
],
[
'echo "--- proto pnpm direct ---"; ~/.proto/tools/node/24.11.0/bin/pnpm --version 2>&1 | tail -3; echo "--- proto node ---"; ls ~/.proto/tools/node/ 2>&1; echo "--- corepack cache ---"; ls ~/.cache/node/corepack/v1/pnpm/ 2>&1',
'Ran ~/.proto/tools/node/24.11.0/bin/pnpm --version + 2 commands'
],
[
'cd /Users/brooklyn/www/bb-rainbows && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack pnpm@10.20.0 --version 2>&1 | tail -3',
'Ran COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack pnpm@10.20.0 --version'
],
[
'cd /Users/brooklyn/www/bb-rainbows && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack use pnpm@10.20.0 2>&1 | tail -10; echo "exit=$?"',
'Ran COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack use pnpm@10.20.0'
]
] as const
for (const [command, expectedTitle] of rows) {
const view = buildToolView(part({ args: { command }, result: { output: 'ok', exit_code: 0 }, toolName: 'terminal' }), '')
expect(view.title).toBe(expectedTitle)
}
})
it('uses the runtime locale for title text and action placement', () => {
setRuntimeI18nLocale('ja')

View file

@ -1,5 +1,6 @@
import { type ToolTitleKey, translateNow } from '@/i18n'
import { normalizeExternalUrl } from '@/lib/external-link'
import { summarizeShellCommand } from '@/lib/summarize-command'
import { extractToolErrorMessage, formatToolResultSummary } from '@/lib/tool-result-summary'
export type ToolTone = 'agent' | 'browser' | 'default' | 'file' | 'image' | 'terminal' | 'web'
@ -125,6 +126,45 @@ function fileEditBasename(path: string): string {
return normalized.split('/').filter(Boolean).pop() || normalized
}
function numericField(record: Record<string, unknown>, key: string): number | undefined {
const value = record[key]
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
}
function readFileLineLabel(args: Record<string, unknown>, result: Record<string, unknown>): string {
if (numericField(args, 'offset') === undefined && numericField(args, 'limit') === undefined) {
return ''
}
const content = firstStringField(result, ['content'])
const offset = numericField(args, 'offset')
const limit = numericField(args, 'limit')
if (offset !== undefined && offset > 0) {
if (limit === undefined || limit <= 1) {
return `L${offset}`
}
return `L${offset}-${offset + limit - 1}`
}
const lines = content
.split('\n')
.map(line => /^(\d+)\|/.exec(line)?.[1])
.filter((line): line is string => !!line)
.map(Number)
if (lines.length === 0) {
return ''
}
const start = lines[0]!
const end = lines[lines.length - 1]!
return start === end ? `L${start}` : `L${start}-${end}`
}
const TOOL_META: Record<ToolTitleKey, ToolMetaSpec> = {
browser_click: {
icon: 'globe',
@ -1182,7 +1222,7 @@ function toolSubtitle(
const command = firstStringField(argsRecord, ['command', 'code']) || contextValue(argsRecord)
return command ? compactPreview(command, 120) : 'Executed command'
return command ? compactPreview(summarizeShellCommand(command), 120) : 'Executed command'
}
if (toolName === 'read_file' || isFileEditTool(toolName)) {
@ -1290,7 +1330,7 @@ function toolDetailText(
}
}
if (part.toolName === 'read_file') {
if (part.toolName === 'read_file' && part.result !== undefined) {
const content = firstStringField(resultRecord, ['content', 'text', 'data', 'body'])
if (content) {
@ -1401,7 +1441,7 @@ export function toolCopyPayload(part: ToolPart, view: ToolView): { label: string
}
}
if (part.toolName === 'read_file') {
if (part.toolName === 'read_file' && part.result !== undefined) {
if (hasSubstantialOutput) {
return { label: copy.file, text: detail }
}
@ -1514,11 +1554,27 @@ function dynamicTitle(
return titledAction(
action,
translateNow('assistant.tool.titleTemplates.actionCommand', action, compactPreview(command, 160))
translateNow(
'assistant.tool.titleTemplates.actionCommand',
action,
compactPreview(summarizeShellCommand(command), 160)
)
)
}
}
if (part.toolName === 'read_file' && part.result !== undefined) {
const path = firstStringField(args, ['path', 'file', 'filepath'])
if (path) {
const lineLabel = readFileLineLabel(args, result)
const target = [fileEditBasename(path), lineLabel].filter(Boolean).join(' ')
const action = verb(translateNow('assistant.tool.actions.reading'), translateNow('assistant.tool.actions.read'))
return { title: translateNow('assistant.tool.titleTemplates.actionTarget', action, target) }
}
}
if (isFileEditTool(part.toolName)) {
const path = fileEditPath(args, result)