feat(tooling): preserve native tool titles (#1133)

This commit is contained in:
SSU-WEI HUANG
2026-07-23 08:41:33 +08:00
committed by GitHub
parent e69ca0782f
commit 6bedd0d924
18 changed files with 135 additions and 18 deletions
+23
View File
@@ -2,6 +2,29 @@ import { describe, expect, it } from 'vitest'
import { normalizeAgentRecord } from '@/chat/normalizeAgent'
describe('normalizeAgentRecord — agentTimestamp exposure', () => {
it('preserves normalized native tool presentation metadata', () => {
const normalized = normalizeAgentRecord('msg-native', null, 1, {
type: 'codex',
data: {
type: 'tool-call',
callId: 'call-native',
name: 'Bash',
input: { command: 'bun test' },
nativeTitle: 'Run project tests',
nativeKind: 'execute'
}
})
expect(normalized).toMatchObject({
role: 'agent',
content: [{
type: 'tool-call',
nativeTitle: 'Run project tests',
nativeKind: 'execute'
}]
})
})
it('parses data.timestamp into agentTimestamp for an assistant tool_use record', () => {
const normalized = normalizeAgentRecord('msg-1', null, 1_783_953_478_235, {
type: 'output',
+3 -1
View File
@@ -738,7 +738,9 @@ export function normalizeAgentRecord(
id: data.callId,
name: asString(data.name) ?? 'unknown',
input: data.input,
description: null,
description: asString(data.description),
nativeTitle: asString(data.nativeTitle ?? data.title),
nativeKind: asString(data.nativeKind ?? data.kind),
uuid,
parentUUID: null
}],
+2
View File
@@ -880,6 +880,8 @@ export function reduceTimeline(
name: c.name,
input: c.input,
description: c.description,
nativeTitle: c.nativeTitle,
nativeKind: c.nativeKind,
permission,
agentTimestamp: msg.agentTimestamp
})
+10
View File
@@ -65,6 +65,8 @@ export function ensureToolBlock(
name: string
input: unknown
description: string | null
nativeTitle?: string | null
nativeKind?: string | null
permission?: ToolPermission
/** Claude entry execution-machine timestamp for the tool_use, if known (see `ChatToolCall.execStartedAt`). */
agentTimestamp?: number | null
@@ -99,6 +101,12 @@ export function ensureToolBlock(
if (seed.description !== null) {
existing.tool = { ...existing.tool, description: seed.description }
}
if (seed.nativeTitle != null) {
existing.tool = { ...existing.tool, nativeTitle: seed.nativeTitle }
}
if (seed.nativeKind != null) {
existing.tool = { ...existing.tool, nativeKind: seed.nativeKind }
}
// The first call (tool_use) records when the tool was invoked. The
// second call (tool_result) carries the result message's invokedAt,
// which is when the result was processed — not when the tool was
@@ -139,6 +147,8 @@ export function ensureToolBlock(
execStartedAt: initialState === 'running' ? (seed.agentTimestamp ?? null) : null,
execCompletedAt: null,
description: seed.description,
nativeTitle: seed.nativeTitle ?? null,
nativeKind: seed.nativeKind ?? null,
permission: seed.permission
}
+4
View File
@@ -45,6 +45,8 @@ export type ToolUse = {
name: string
input: unknown
description: string | null
nativeTitle?: string | null
nativeKind?: string | null
uuid: string
parentUUID: string | null
}
@@ -175,6 +177,8 @@ export type ChatToolCall = {
execStartedAt: number | null
execCompletedAt: number | null
description: string | null
nativeTitle?: string | null
nativeKind?: string | null
result?: unknown
permission?: ToolPermission
}
+2 -1
View File
@@ -319,13 +319,14 @@ function ToolCardInner(props: ToolCardProps) {
input: props.block.tool.input,
result: props.block.tool.result,
childrenCount: props.block.children.length,
description: props.block.tool.description,
description: props.block.tool.nativeTitle ?? props.block.tool.description,
metadata: props.metadata
}, t), [
props.block.tool.name,
props.block.tool.input,
props.block.tool.result,
props.block.children.length,
props.block.tool.nativeTitle,
props.block.tool.description,
props.metadata,
t
@@ -74,7 +74,7 @@ function RowLabel(props: { block: ToolCallBlock; metadata: SessionMetadataSummar
input: props.block.tool.input,
result: props.block.tool.result,
childrenCount: props.block.children.length,
description: props.block.tool.description,
description: props.block.tool.nativeTitle ?? props.block.tool.description,
metadata: props.metadata
}, t), [props.block, props.metadata, t])
@@ -213,7 +213,7 @@ export function ToolGroupCard(props: {
input: selectedTool.tool.input,
result: selectedTool.tool.result,
childrenCount: selectedTool.children.length,
description: selectedTool.tool.description,
description: selectedTool.tool.nativeTitle ?? selectedTool.tool.description,
metadata: props.metadata
}, t)
}, [selectedTool, props.metadata, t])
+1 -1
View File
@@ -18,7 +18,7 @@ export function formatTaskChildLabel(
input: child.tool.input,
result: child.tool.result,
childrenCount: child.children.length,
description: child.tool.description,
description: child.tool.nativeTitle ?? child.tool.description,
metadata,
}, t)
@@ -216,6 +216,22 @@ describe('getToolPresentation — Codex agent tools', () => {
})
})
describe('getToolPresentation — native titles', () => {
it('uses a preserved native title for unknown lowercase tools', () => {
const presentation = getToolPresentation({
toolName: 'bash',
input: { command: 'bun test' },
result: null,
childrenCount: 0,
description: 'Run project tests',
metadata: null,
})
expect(presentation.title).toBe('Run project tests')
expect(presentation.subtitle).toBe('bun test')
})
})
describe('getToolPresentation — request_user_input', () => {
it('uses the question header instead of exposing its protocol id', () => {
const presentation = getToolPresentation({
+1 -1
View File
@@ -574,7 +574,7 @@ export function getToolPresentation(
// become the subtitle, so the card reads like a sentence instead of
// showing the same string twice. Labels are translated when a Translator
// is supplied; tests and call sites without i18n fall back to English.
let title = opts.toolName
let title = opts.description ?? opts.toolName
if (subtitle && subtitle === title) {
if (filePath) title = t ? t('tool.semanticTitle.readFile') : 'Read file'
else if (command) title = t ? t('tool.semanticTitle.runShell') : 'Run shell'