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
@@ -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
});
}
}
+17
View File
@@ -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',
+5 -1
View File
@@ -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 {
+9 -1
View File
@@ -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';
+8 -4
View File
@@ -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 {