Skip to content

Commit 5452ebe

Browse files
committed
fix(i18n): localize built-in tool presentation
Resolve Maka-owned tool labels from the active UI locale while preserving provider and compatibility labels for non-localized consumers. Move thread search metadata presentation out of Core so historical results follow the current locale. Refs #3962 Generated-by: OpenAI Codex
1 parent fd31c92 commit 5452ebe

20 files changed

Lines changed: 495 additions & 50 deletions

apps/desktop/e2e/fixture-thread-search.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ test('fixture-seeded transcripts return content hits with turn ids', async ({
4343
hit.target?.kind === 'thread' &&
4444
hit.target.turnId === 'turn-prompt-rail-3',
4545
);
46-
expect(content?.summary).toBe('用户消息');
46+
expect(content?.summary).toBeUndefined();
4747
if (!content || content.target?.kind !== 'thread') {
4848
throw new Error(`expected a thread search hit, got ${JSON.stringify(content)}`);
4949
}
@@ -53,5 +53,6 @@ test('fixture-seeded transcripts return content hits with turn ids', async ({
5353
sessionId: desktopSessionKey({ hostId, sessionId: PROMPT_RAIL_SESSION_ID }),
5454
turnId: 'turn-prompt-rail-3',
5555
sequence: 4,
56+
matchKind: 'user_message',
5657
});
5758
});

apps/desktop/src/main/__tests__/runtime-host-native-capabilities.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
type ClientCapabilityServiceCallFrame,
3030
} from '@maka/runtime-host/protocol';
3131
import { z } from 'zod';
32+
import { buildBrowserTools } from '../browser/browser-tools.js';
3233
import { buildClientSettingsTools } from '../client-settings-tools.js';
3334
import { buildRiveWorkflowTool } from '../rive-workflow-tool.js';
3435
import { createDesktopNativeCapabilityProvider } from '../runtime-host-native-capabilities.js';
@@ -81,6 +82,21 @@ test('publishes self-described session-affine Browser and Computer Use offers',
8182
);
8283
});
8384

85+
test('preserves built-in Browser labels for non-localized consumers', () => {
86+
const provider = createDesktopNativeCapabilityProvider({
87+
browserTools: buildBrowserTools(),
88+
releaseBrowserSession() {},
89+
computerUseTools: computerTools(),
90+
releaseComputerUseSession() {},
91+
});
92+
93+
assert.deepEqual(
94+
provider.offers().find((offer) => offer.offerId === 'desktop_browser')
95+
?.tools.map((tool) => tool.annotations?.title),
96+
['浏览器导航', '浏览器快照', '浏览器点击', '浏览器输入', '浏览器等待', '浏览器提取'],
97+
);
98+
});
99+
84100
test('remote providers do not request Host paths and use a Client-owned cwd', async () => {
85101
let invokedCwd: string | undefined;
86102
const provider = createDesktopNativeCapabilityProvider(

apps/desktop/src/main/__tests__/runtime-host-search-ipc-main.test.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,25 @@ test('Runtime Host transcripts produce title and content hits with turn ids', as
4949
ts: 1,
5050
text: '第 3 个问题:这一段的调用链路是怎样的?',
5151
},
52+
{
53+
type: 'tool_call',
54+
id: 'browser-call',
55+
turnId: 'turn-host-3',
56+
ts: 2,
57+
toolName: 'mcp__desktop_browser__browser_navigate',
58+
displayName: 'browser_navigate',
59+
intent: '打开检查页面',
60+
args: {},
61+
},
62+
{
63+
type: 'tool_result',
64+
id: 'browser-result',
65+
turnId: 'turn-host-3',
66+
ts: 3,
67+
toolUseId: 'browser-call',
68+
isError: true,
69+
content: { kind: 'text', text: '检查页面失败' },
70+
},
5271
],
5372
close: async () => {
5473
closed += 1;
@@ -66,10 +85,11 @@ test('Runtime Host transcripts produce title and content hits with turn ids', as
6685
limit: 10,
6786
}),
6887
);
69-
assert.equal(titleHits[0]?.summary, '任务标题');
88+
assert.equal(titleHits[0]?.summary, undefined);
7089
assert.deepEqual(titleHits[0]?.target, {
7190
kind: 'thread',
7291
sessionId: 'searchable-session',
92+
matchKind: 'session_title',
7393
});
7494

7595
const contentHits = expectResults(
@@ -80,14 +100,27 @@ test('Runtime Host transcripts produce title and content hits with turn ids', as
80100
}),
81101
);
82102
assert.equal(contentHits.length, 1);
83-
assert.equal(contentHits[0]?.summary, '用户消息');
103+
assert.equal(contentHits[0]?.summary, undefined);
84104
assert.deepEqual(contentHits[0]?.target, {
85105
kind: 'thread',
86106
sessionId: 'searchable-session',
87107
turnId: 'turn-host-3',
88108
sequence: 0,
109+
matchKind: 'user_message',
110+
});
111+
112+
const toolHit = expectResults(
113+
await handler({} as never, { source: 'thread', query: '打开检查', limit: 10 }),
114+
)[0];
115+
assert.deepEqual(toolHit?.target?.tool, {
116+
name: 'mcp__desktop_browser__browser_navigate',
117+
displayName: 'browser_navigate',
89118
});
90-
assert.equal(closed, 2);
119+
const resultHit = expectResults(
120+
await handler({} as never, { source: 'thread', query: '检查页面失败', limit: 10 }),
121+
)[0];
122+
assert.equal(resultHit?.target?.toolResultIsError, true);
123+
assert.equal(closed, 4);
91124
});
92125

93126
test('a Runtime Host transcript failure yields no content hit', async () => {
@@ -128,6 +161,9 @@ function expectResults(outcome: unknown): Array<{
128161
sessionId: string;
129162
turnId?: string;
130163
sequence?: number;
164+
matchKind?: string;
165+
tool?: { name: string; displayName?: string };
166+
toolResultIsError?: boolean;
131167
};
132168
}> {
133169
if (!Array.isArray(outcome)) {

apps/desktop/src/main/__tests__/streaming-handoff.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ describe('single live-turn handoff', () => {
185185
assert.equal((markup.match(/data-transient-message-id=/g) ?? []).length, 2);
186186
});
187187

188-
it('renders one ordered timeline: thinking before its tool and answer', () => {
188+
it('renders thinking, answer, and tool in one ordered timeline', () => {
189189
const markup = renderLiveTurn({
190190
turnId: 'turn-1',
191191
phase: 'streamed',
@@ -195,7 +195,8 @@ describe('single live-turn handoff', () => {
195195
text: { text: '最终答案', truncated: false, complete: true },
196196
tools: [{
197197
toolUseId: 'tool-1',
198-
toolName: 'Bash',
198+
toolName: 'mcp__fixture__ordered_tool',
199+
displayName: 'Timeline tool marker',
199200
stepId: 'assistant-1',
200201
status: 'running',
201202
args: {},
@@ -206,9 +207,8 @@ describe('single live-turn handoff', () => {
206207

207208
// Thinking and tools own their disclosures; do not wrap them in another.
208209
assert.equal((markup.match(/maka-processing-block/g) ?? []).length, 0);
209-
assert.ok(markup.indexOf('深度思考') >= 0);
210-
assert.ok(markup.indexOf('深度思考') < markup.indexOf('最终答案'));
211-
assert.ok(markup.indexOf('最终答案') < markup.indexOf('Bash'));
210+
assert.ok(markup.indexOf('先检查') < markup.indexOf('最终答案'));
211+
assert.ok(markup.indexOf('最终答案') < markup.indexOf('Timeline tool marker'));
212212
assert.equal((markup.match(/data-turn-id=/g) ?? []).length, 1);
213213
});
214214

apps/desktop/src/main/__tests__/thread-search.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ describe('runThreadSearch', () => {
428428
sessionId: 's1',
429429
matchKind: 'session_title',
430430
});
431-
assert.equal(titleHit.summary, '任务标题');
431+
assert.equal(titleHit.summary, undefined);
432432
assert.equal(titleHit.url, undefined);
433433
assert.match(titleHit.snippet ?? '', /\[redacted\]/);
434434
assert.equal(titleHit.snippet?.includes('sk-ant-test-secret-token-12345'), false);
@@ -445,7 +445,7 @@ describe('runThreadSearch', () => {
445445
matchKind: 'user_message',
446446
messageTimestamp: 1_700_000_000_000,
447447
});
448-
assert.equal(messageHit.summary, '用户消息');
448+
assert.equal(messageHit.summary, undefined);
449449
assert.equal(messageHit.url, undefined);
450450
});
451451

@@ -536,6 +536,7 @@ describe('thread search text projection', () => {
536536
);
537537
assert.equal(hits[0]?.target?.matchKind, 'tool_result');
538538
assert.equal(hits[0]?.target?.messageId, 'tr1');
539+
assert.equal(hits[0]?.target?.toolResultIsError, false);
539540
});
540541

541542
it('indexes tool intent but not tool names or display names', async () => {
@@ -562,6 +563,25 @@ describe('thread search text projection', () => {
562563
assert.equal(hits.length, 1);
563564
assert.equal(hits[0]?.target?.matchKind, 'tool_intent');
564565
assert.equal(hits[0]?.target?.messageId, 'tc1');
566+
assert.deepEqual(hits[0]?.target?.tool, {
567+
name: 'Bash',
568+
displayName: 'Shell command',
569+
});
570+
});
571+
572+
it('redacts tool labels in result metadata', async () => {
573+
const message = {
574+
...toolCall('find the metadata needle'),
575+
displayName: 'token=secret-search-label',
576+
};
577+
const hits = expectResults(
578+
await runThreadSearch(
579+
{ source: 'thread', query: 'metadata needle', limit: 5 },
580+
makeDeps({ s1: { session: session({ id: 's1' }), messages: [message] } }),
581+
),
582+
);
583+
584+
assert.equal(hits[0]?.target?.tool?.displayName, 'token=[redacted]');
565585
});
566586

567587
it('indexes assistant answers without exposing thinking', async () => {

apps/desktop/src/main/runtime-host-search-ipc-main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ function projectDesktopSearchResult(result: SearchResult): SearchResult {
6969
sessionId: result.target.sessionId,
7070
...(result.target.turnId !== undefined ? { turnId: result.target.turnId } : {}),
7171
...(result.target.sequence !== undefined ? { sequence: result.target.sequence } : {}),
72+
...(result.target.matchKind !== undefined ? { matchKind: result.target.matchKind } : {}),
73+
...(result.target.tool !== undefined ? { tool: result.target.tool } : {}),
74+
...(result.target.toolResultIsError !== undefined
75+
? { toolResultIsError: result.target.toolResultIsError }
76+
: {}),
7277
},
7378
};
7479
}

packages/core/src/search.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ export type SearchResultTarget = {
116116
messageId?: string;
117117
/** Stable machine-readable classification of the matched transcript surface. */
118118
matchKind?: ThreadSearchMatchKind;
119+
/** Tool identity for UI-owned presentation of tool-intent matches. */
120+
tool?: { name: string; displayName?: string };
121+
/** Outcome for UI-owned presentation of tool-result matches. */
122+
toolResultIsError?: boolean;
119123
/** Timestamp of the matched stored message; absent for session-title matches. */
120124
messageTimestamp?: number;
121125
};

packages/core/src/session.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,7 @@ export interface ToolCallMessage {
824824
toolName: string;
825825
/** Stable semantic category for presentation; absent on legacy rows. */
826826
activityKind?: ToolActivityKind;
827+
/** Provider/compatibility label; built-in UI copy must resolve from toolName and the active locale. */
827828
displayName?: string;
828829
intent?: string;
829830
args: unknown;

packages/core/src/thread-search.ts

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,6 @@ export async function runThreadSearch(
279279
results.push({
280280
source: THREAD_SOURCE,
281281
title: searchableTitle,
282-
summary: '任务标题',
283282
snippet,
284283
target: {
285284
kind: 'thread',
@@ -339,7 +338,6 @@ export async function runThreadSearch(
339338
results.push({
340339
source: THREAD_SOURCE,
341340
title: redactSecrets(session.name),
342-
summary: formatSearchResultSummary(message),
343341
snippet,
344342
// PR-SEARCH-1.5: navigation target via discriminated union; no
345343
// `url` field for thread results (maka://session is deferred).
@@ -350,6 +348,17 @@ export async function runThreadSearch(
350348
sequence: messageIndex,
351349
messageId: message.id,
352350
matchKind: threadSearchMatchKind(message),
351+
...(message.type === 'tool_call'
352+
? {
353+
tool: {
354+
name: message.toolName,
355+
...(message.displayName
356+
? { displayName: redactSecrets(message.displayName) }
357+
: {}),
358+
},
359+
}
360+
: {}),
361+
...(message.type === 'tool_result' ? { toolResultIsError: message.isError } : {}),
353362
messageTimestamp: message.ts,
354363
},
355364
});
@@ -470,29 +479,6 @@ export function threadSearchMatchKind(message: StoredMessage): ThreadSearchMatch
470479
}
471480
}
472481

473-
export function formatSearchResultSummary(message: StoredMessage): string {
474-
switch (message.type) {
475-
case 'user':
476-
return '用户消息';
477-
case 'assistant':
478-
return '助手回复';
479-
case 'tool_call':
480-
return message.displayName
481-
? `工具调用:${message.displayName}`
482-
: `工具调用:${message.toolName}`;
483-
case 'tool_result':
484-
return message.isError ? '工具结果:失败' : '工具结果:成功';
485-
case 'permission_decision':
486-
return '权限记录';
487-
case 'token_usage':
488-
return '用量记录';
489-
case 'turn_state':
490-
return '回合状态';
491-
case 'system_note':
492-
return '系统记录';
493-
}
494-
}
495-
496482
/**
497483
* Extract user-visible answer text from a stored message. Returns `undefined`
498484
* for excluded message kinds (system notes, token usage, turn state,

packages/runtime/src/__tests__/history-tools.test.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,12 @@ test('SearchHistory returns typed message hits from current and other sessions',
117117
assert.ok(messageRows.every((row) => typeof row.message_id === 'string'));
118118
assert.ok(messageRows.every((row) => typeof row.message_timestamp === 'number'));
119119
assert.deepEqual(
120-
new Set(result.rows.map((row) => row.match_kind)),
121-
new Set(['session_title', 'user_message', 'assistant_message']),
120+
new Map(result.rows.map((row) => [row.match_kind, row.summary])),
121+
new Map([
122+
['session_title', 'Session title'],
123+
['user_message', 'User message'],
124+
['assistant_message', 'Assistant response'],
125+
]),
122126
);
123127
assert.equal(result.rows.find((row) => row.session_id === 'current')?.is_current_session, true);
124128
assert.ok(result.rows.every((row) => row.turn_id !== 'current-turn'));
@@ -158,6 +162,68 @@ test('SearchHistory exposes and consumes an opaque session continuation', async
158162
assert.equal(second.next_cursor, undefined);
159163
});
160164

165+
test('SearchHistory summaries echo invoked wire tool names and cover tool outcomes', async () => {
166+
const needle = 'history summary needle';
167+
const messages = new Map<string, StoredMessage[]>([
168+
[
169+
'past',
170+
[
171+
{
172+
type: 'tool_call',
173+
id: 'legacy-browser',
174+
turnId: 'turn-1',
175+
ts: 1,
176+
toolName: 'mcp__desktop_browser__browser_navigate',
177+
displayName: '浏览器导航',
178+
intent: needle,
179+
args: {},
180+
},
181+
{
182+
type: 'tool_call',
183+
id: 'secret-tool',
184+
turnId: 'turn-1',
185+
ts: 2,
186+
toolName: 'sk-ant-test-secret-token-12345',
187+
displayName: 'Acme Lookup',
188+
intent: needle,
189+
args: {},
190+
},
191+
{
192+
type: 'tool_result',
193+
id: 'success-result',
194+
turnId: 'turn-1',
195+
ts: 3,
196+
toolUseId: 'legacy-browser',
197+
isError: false,
198+
content: { note: needle } as never,
199+
},
200+
{
201+
type: 'tool_result',
202+
id: 'failure-result',
203+
turnId: 'turn-1',
204+
ts: 4,
205+
toolUseId: 'secret-tool',
206+
isError: true,
207+
content: { note: needle } as never,
208+
},
209+
],
210+
],
211+
]);
212+
const result = (await buildSearchHistoryTool(
213+
historyDeps([session('past', 'Past', 1)], messages),
214+
).impl({ query: needle, limit: 10 }, context())) as {
215+
rows: Array<{ summary: string }>;
216+
};
217+
218+
assert.ok(
219+
result.rows.some((row) => row.summary === 'Tool call: mcp__desktop_browser__browser_navigate'),
220+
);
221+
assert.ok(result.rows.some((row) => row.summary === 'Tool result: Succeeded'));
222+
assert.ok(result.rows.some((row) => row.summary === 'Tool result: Failed'));
223+
assert.match(JSON.stringify(result.rows), /\[redacted\]/u);
224+
assert.doesNotMatch(JSON.stringify(result.rows), /sk-ant-test-secret-token-12345/u);
225+
});
226+
161227
test('ReadHistory returns a bounded visible excerpt without reasoning or raw tool data', async () => {
162228
const huge = `finished ${'x'.repeat(HISTORY_READ_MAX_BYTES * 2)}`;
163229
const messages = new Map<string, StoredMessage[]>([

0 commit comments

Comments
 (0)