mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(tooling): preserve native tool titles (#1133)
This commit is contained in:
@@ -444,6 +444,7 @@ describe('AcpMessageHandler', () => {
|
||||
);
|
||||
expect(toolCall).toBeDefined();
|
||||
expect(toolCall!.input).toEqual({ command: 'df -hT' });
|
||||
expect(toolCall).toMatchObject({ title: 'df -hT', kind: 'execute' });
|
||||
});
|
||||
|
||||
it('strips "Shell: " prefix from title when deriving execute input (Kimi)', () => {
|
||||
|
||||
@@ -704,7 +704,9 @@ export class AcpMessageHandler {
|
||||
id: toolCallId,
|
||||
name,
|
||||
input,
|
||||
status
|
||||
status,
|
||||
...(asString(update.title) ? { title: asString(update.title)! } : {}),
|
||||
...(asString(update.kind) ? { kind: asString(update.kind)! } : {})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -714,6 +716,10 @@ export class AcpMessageHandler {
|
||||
|
||||
const status = normalizeStatus(update.status);
|
||||
const existing = this.toolCalls.get(toolCallId);
|
||||
const presentation = {
|
||||
...(asString(update.title) ? { title: asString(update.title)! } : {}),
|
||||
...(asString(update.kind) ? { kind: asString(update.kind)! } : {})
|
||||
};
|
||||
|
||||
if (isUsableRawInput(update.rawInput)) {
|
||||
const derivedName = deriveToolNameFromUpdate(update);
|
||||
@@ -725,7 +731,8 @@ export class AcpMessageHandler {
|
||||
id: toolCallId,
|
||||
name,
|
||||
input,
|
||||
status
|
||||
status,
|
||||
...presentation
|
||||
});
|
||||
} else if (existing) {
|
||||
// Enrich existing.input from update's kind+title when initial tool_call
|
||||
@@ -758,7 +765,8 @@ export class AcpMessageHandler {
|
||||
id: toolCallId,
|
||||
name,
|
||||
input,
|
||||
status
|
||||
status,
|
||||
...presentation
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -784,7 +792,8 @@ export class AcpMessageHandler {
|
||||
id: toolCallId,
|
||||
name: hoisted.name,
|
||||
input: hoisted.input,
|
||||
status
|
||||
status,
|
||||
...presentation
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,23 @@ describe('convertAgentMessage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves ACP native presentation metadata', () => {
|
||||
const converted = convertAgentMessage({
|
||||
type: 'tool_call',
|
||||
id: 'call-native',
|
||||
name: 'Bash',
|
||||
input: { command: 'free -h' },
|
||||
status: 'in_progress',
|
||||
title: 'Shell: free -h',
|
||||
kind: 'execute'
|
||||
});
|
||||
|
||||
expect(converted).toMatchObject({
|
||||
nativeTitle: 'Shell: free -h',
|
||||
nativeKind: 'execute'
|
||||
});
|
||||
});
|
||||
|
||||
it('marks failed tool results as error', () => {
|
||||
const converted = convertAgentMessage({
|
||||
type: 'tool_result',
|
||||
|
||||
@@ -24,6 +24,8 @@ export type CodexMessage =
|
||||
callId: string;
|
||||
input: unknown;
|
||||
status?: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
nativeTitle?: string;
|
||||
nativeKind?: string;
|
||||
}
|
||||
| {
|
||||
type: 'tool-call-result';
|
||||
@@ -64,7 +66,9 @@ export function convertAgentMessage(message: AgentMessage): CodexMessage | null
|
||||
name: message.name,
|
||||
callId: message.id,
|
||||
input: message.input,
|
||||
status: message.status
|
||||
status: message.status,
|
||||
...(message.title ? { nativeTitle: message.title } : {}),
|
||||
...(message.kind ? { nativeKind: message.kind } : {})
|
||||
};
|
||||
case 'tool_result':
|
||||
return {
|
||||
|
||||
@@ -31,7 +31,15 @@ export type PlanItem = {
|
||||
export type AgentMessage =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'reasoning'; text: string; id?: string; live?: boolean }
|
||||
| { type: 'tool_call'; id: string; name: string; input: unknown; status: 'pending' | 'in_progress' | 'completed' | 'failed' }
|
||||
| {
|
||||
type: 'tool_call';
|
||||
id: string;
|
||||
name: string;
|
||||
input: unknown;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
title?: string;
|
||||
kind?: string;
|
||||
}
|
||||
| { type: 'tool_result'; id: string; output: unknown; status: 'completed' | 'failed' }
|
||||
| {
|
||||
type: 'usage';
|
||||
|
||||
@@ -335,7 +335,8 @@ export async function opencodeLocalLauncher(
|
||||
type: 'tool-call',
|
||||
name: toolCall.name,
|
||||
callId: toolCall.callId,
|
||||
input: toolCall.input
|
||||
input: toolCall.input,
|
||||
...(toolCall.title ? { nativeTitle: toolCall.title } : {})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -349,7 +350,8 @@ export async function opencodeLocalLauncher(
|
||||
type: 'tool-call',
|
||||
name: toolCall.name,
|
||||
callId: toolCall.callId,
|
||||
input: toolCall.input
|
||||
input: toolCall.input,
|
||||
...(toolCall.title ? { nativeTitle: toolCall.title } : {})
|
||||
});
|
||||
}
|
||||
sentToolResults.add(toolResult.callId);
|
||||
@@ -418,7 +420,8 @@ export async function opencodeLocalLauncher(
|
||||
type: 'tool-call',
|
||||
name,
|
||||
callId,
|
||||
input: toolInput
|
||||
input: toolInput,
|
||||
...(getString(tool.title ?? record.title) ? { nativeTitle: getString(tool.title ?? record.title) } : {})
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -431,7 +434,8 @@ export async function opencodeLocalLauncher(
|
||||
type: 'tool-call',
|
||||
name,
|
||||
callId,
|
||||
input: toolInput
|
||||
input: toolInput,
|
||||
...(getString(tool.title ?? record.title) ? { nativeTitle: getString(tool.title ?? record.title) } : {})
|
||||
});
|
||||
}
|
||||
sentToolResults.add(callId);
|
||||
|
||||
@@ -28,6 +28,19 @@ function collectMessages(parts: unknown[]): Array<{ type: string; name?: string;
|
||||
}
|
||||
|
||||
describe('OpenCode local tool part parsing', () => {
|
||||
it('preserves the native state title', () => {
|
||||
expect(parseToolCall({
|
||||
type: 'tool',
|
||||
tool: 'bash',
|
||||
callID: 'call-title',
|
||||
state: {
|
||||
status: 'running',
|
||||
input: { command: 'bun test' },
|
||||
title: 'Run project tests'
|
||||
}
|
||||
})).toMatchObject({ title: 'Run project tests' });
|
||||
});
|
||||
|
||||
it('does not emit tool-call on pending with empty input; emits on running with real args', () => {
|
||||
const callId = 'call-6049b4cf-0272-4651-be9a-402c3a40c933-0';
|
||||
const messages = collectMessages([
|
||||
|
||||
@@ -4,6 +4,7 @@ export type ParsedToolCall = {
|
||||
callId: string;
|
||||
name: string;
|
||||
input: unknown;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export type ParsedToolResult = {
|
||||
@@ -77,10 +78,12 @@ export function parseToolCall(part: unknown): ParsedToolCall | null {
|
||||
return null;
|
||||
}
|
||||
const input = parseMaybeJson(state.input ?? state.raw ?? record.input ?? record.args ?? record.arguments);
|
||||
return { callId, name, input };
|
||||
const title = getString(state.title);
|
||||
return { callId, name, input, ...(title ? { title } : {}) };
|
||||
}
|
||||
const input = parseMaybeJson(record.input ?? record.args ?? record.arguments ?? record.raw);
|
||||
return { callId, name, input };
|
||||
const title = getString(record.title);
|
||||
return { callId, name, input, ...(title ? { title } : {}) };
|
||||
}
|
||||
|
||||
export function parseToolResult(part: unknown): ParsedToolResult | null {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}],
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user