diff --git a/apps/desktop/e2e/fixture-thread-search.spec.ts b/apps/desktop/e2e/fixture-thread-search.spec.ts index e4b50b1bc1..34123bb7a4 100644 --- a/apps/desktop/e2e/fixture-thread-search.spec.ts +++ b/apps/desktop/e2e/fixture-thread-search.spec.ts @@ -43,7 +43,7 @@ test('fixture-seeded transcripts return content hits with turn ids', async ({ hit.target?.kind === 'thread' && hit.target.turnId === 'turn-prompt-rail-3', ); - expect(content?.summary).toBe('用户消息'); + expect(content?.summary).toBeUndefined(); if (!content || content.target?.kind !== 'thread') { throw new Error(`expected a thread search hit, got ${JSON.stringify(content)}`); } @@ -53,5 +53,6 @@ test('fixture-seeded transcripts return content hits with turn ids', async ({ sessionId: desktopSessionKey({ hostId, sessionId: PROMPT_RAIL_SESSION_ID }), turnId: 'turn-prompt-rail-3', sequence: 4, + matchKind: 'user_message', }); }); diff --git a/apps/desktop/src/main/__tests__/runtime-host-native-capabilities.test.ts b/apps/desktop/src/main/__tests__/runtime-host-native-capabilities.test.ts index 07fb18e7c1..a0de8ad7c7 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-native-capabilities.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-native-capabilities.test.ts @@ -29,6 +29,7 @@ import { type ClientCapabilityServiceCallFrame, } from '@maka/runtime-host/protocol'; import { z } from 'zod'; +import { buildBrowserTools } from '../browser/browser-tools.js'; import { buildClientSettingsTools } from '../client-settings-tools.js'; import { buildRiveWorkflowTool } from '../rive-workflow-tool.js'; import { createDesktopNativeCapabilityProvider } from '../runtime-host-native-capabilities.js'; @@ -81,6 +82,21 @@ test('publishes self-described session-affine Browser and Computer Use offers', ); }); +test('preserves built-in Browser labels for non-localized consumers', () => { + const provider = createDesktopNativeCapabilityProvider({ + browserTools: buildBrowserTools(), + releaseBrowserSession() {}, + computerUseTools: computerTools(), + releaseComputerUseSession() {}, + }); + + assert.deepEqual( + provider.offers().find((offer) => offer.offerId === 'desktop_browser') + ?.tools.map((tool) => tool.annotations?.title), + ['浏览器导航', '浏览器快照', '浏览器点击', '浏览器输入', '浏览器等待', '浏览器提取'], + ); +}); + test('remote providers do not request Host paths and use a Client-owned cwd', async () => { let invokedCwd: string | undefined; const provider = createDesktopNativeCapabilityProvider( diff --git a/apps/desktop/src/main/__tests__/runtime-host-search-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-search-ipc-main.test.ts index 3f127d52a9..4df78a6194 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-search-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-search-ipc-main.test.ts @@ -49,6 +49,25 @@ test('Runtime Host transcripts produce title and content hits with turn ids', as ts: 1, text: '第 3 个问题:这一段的调用链路是怎样的?', }, + { + type: 'tool_call', + id: 'browser-call', + turnId: 'turn-host-3', + ts: 2, + toolName: 'mcp__desktop_browser__browser_navigate', + displayName: 'browser_navigate', + intent: '打开检查页面', + args: {}, + }, + { + type: 'tool_result', + id: 'browser-result', + turnId: 'turn-host-3', + ts: 3, + toolUseId: 'browser-call', + isError: true, + content: { kind: 'text', text: '检查页面失败' }, + }, ], close: async () => { closed += 1; @@ -66,10 +85,11 @@ test('Runtime Host transcripts produce title and content hits with turn ids', as limit: 10, }), ); - assert.equal(titleHits[0]?.summary, '任务标题'); + assert.equal(titleHits[0]?.summary, undefined); assert.deepEqual(titleHits[0]?.target, { kind: 'thread', sessionId: 'searchable-session', + matchKind: 'session_title', }); const contentHits = expectResults( @@ -80,14 +100,27 @@ test('Runtime Host transcripts produce title and content hits with turn ids', as }), ); assert.equal(contentHits.length, 1); - assert.equal(contentHits[0]?.summary, '用户消息'); + assert.equal(contentHits[0]?.summary, undefined); assert.deepEqual(contentHits[0]?.target, { kind: 'thread', sessionId: 'searchable-session', turnId: 'turn-host-3', sequence: 0, + matchKind: 'user_message', + }); + + const toolHit = expectResults( + await handler({} as never, { source: 'thread', query: '打开检查', limit: 10 }), + )[0]; + assert.deepEqual(toolHit?.target?.tool, { + name: 'mcp__desktop_browser__browser_navigate', + displayName: 'browser_navigate', }); - assert.equal(closed, 2); + const resultHit = expectResults( + await handler({} as never, { source: 'thread', query: '检查页面失败', limit: 10 }), + )[0]; + assert.equal(resultHit?.target?.toolResultIsError, true); + assert.equal(closed, 4); }); test('a Runtime Host transcript failure yields no content hit', async () => { @@ -128,6 +161,9 @@ function expectResults(outcome: unknown): Array<{ sessionId: string; turnId?: string; sequence?: number; + matchKind?: string; + tool?: { name: string; displayName?: string }; + toolResultIsError?: boolean; }; }> { if (!Array.isArray(outcome)) { diff --git a/apps/desktop/src/main/__tests__/streaming-handoff.test.ts b/apps/desktop/src/main/__tests__/streaming-handoff.test.ts index 53fb370497..e818f47a1c 100644 --- a/apps/desktop/src/main/__tests__/streaming-handoff.test.ts +++ b/apps/desktop/src/main/__tests__/streaming-handoff.test.ts @@ -185,7 +185,7 @@ describe('single live-turn handoff', () => { assert.equal((markup.match(/data-transient-message-id=/g) ?? []).length, 2); }); - it('renders one ordered timeline: thinking before its tool and answer', () => { + it('renders thinking, answer, and tool in one ordered timeline', () => { const markup = renderLiveTurn({ turnId: 'turn-1', phase: 'streamed', @@ -195,7 +195,8 @@ describe('single live-turn handoff', () => { text: { text: '最终答案', truncated: false, complete: true }, tools: [{ toolUseId: 'tool-1', - toolName: 'Bash', + toolName: 'mcp__fixture__ordered_tool', + displayName: 'Timeline tool marker', stepId: 'assistant-1', status: 'running', args: {}, @@ -206,9 +207,8 @@ describe('single live-turn handoff', () => { // Thinking and tools own their disclosures; do not wrap them in another. assert.equal((markup.match(/maka-processing-block/g) ?? []).length, 0); - assert.ok(markup.indexOf('深度思考') >= 0); - assert.ok(markup.indexOf('深度思考') < markup.indexOf('最终答案')); - assert.ok(markup.indexOf('最终答案') < markup.indexOf('Bash')); + assert.ok(markup.indexOf('先检查') < markup.indexOf('最终答案')); + assert.ok(markup.indexOf('最终答案') < markup.indexOf('Timeline tool marker')); assert.equal((markup.match(/data-turn-id=/g) ?? []).length, 1); }); diff --git a/apps/desktop/src/main/__tests__/thread-search.test.ts b/apps/desktop/src/main/__tests__/thread-search.test.ts index 612eb53676..8af68917bc 100644 --- a/apps/desktop/src/main/__tests__/thread-search.test.ts +++ b/apps/desktop/src/main/__tests__/thread-search.test.ts @@ -428,7 +428,7 @@ describe('runThreadSearch', () => { sessionId: 's1', matchKind: 'session_title', }); - assert.equal(titleHit.summary, '任务标题'); + assert.equal(titleHit.summary, undefined); assert.equal(titleHit.url, undefined); assert.match(titleHit.snippet ?? '', /\[redacted\]/); assert.equal(titleHit.snippet?.includes('sk-ant-test-secret-token-12345'), false); @@ -445,7 +445,7 @@ describe('runThreadSearch', () => { matchKind: 'user_message', messageTimestamp: 1_700_000_000_000, }); - assert.equal(messageHit.summary, '用户消息'); + assert.equal(messageHit.summary, undefined); assert.equal(messageHit.url, undefined); }); @@ -536,6 +536,7 @@ describe('thread search text projection', () => { ); assert.equal(hits[0]?.target?.matchKind, 'tool_result'); assert.equal(hits[0]?.target?.messageId, 'tr1'); + assert.equal(hits[0]?.target?.toolResultIsError, false); }); it('indexes tool intent but not tool names or display names', async () => { @@ -562,6 +563,25 @@ describe('thread search text projection', () => { assert.equal(hits.length, 1); assert.equal(hits[0]?.target?.matchKind, 'tool_intent'); assert.equal(hits[0]?.target?.messageId, 'tc1'); + assert.deepEqual(hits[0]?.target?.tool, { + name: 'Bash', + displayName: 'Shell command', + }); + }); + + it('redacts tool labels in result metadata', async () => { + const message = { + ...toolCall('find the metadata needle'), + displayName: 'token=secret-search-label', + }; + const hits = expectResults( + await runThreadSearch( + { source: 'thread', query: 'metadata needle', limit: 5 }, + makeDeps({ s1: { session: session({ id: 's1' }), messages: [message] } }), + ), + ); + + assert.equal(hits[0]?.target?.tool?.displayName, 'token=[redacted]'); }); it('indexes assistant answers without exposing thinking', async () => { diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index 5476d7e02f..b853091d54 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -34,6 +34,7 @@ import { type ConnectionEvent } from '@maka/core/connections'; import { type SessionChangedEvent, type SessionChangedReason } from '@maka/core/session'; import { isBotDeliveryProvider } from '@maka/core/bot-chat-settings'; import { resolveSystemUiLocale } from '@maka/core/ui-locale'; +import { DESKTOP_BUILTIN_TOOL_OFFER_IDS } from '@maka/core/tool-catalog'; import { PROVIDER_DEFAULTS, providerAuthRequiresSecret, @@ -772,14 +773,14 @@ runtimeHostManager = await startRuntimeHostDesktopManager( const mcpTools = buildMcpTools(mcpManager); return [ { - offerId: "desktop_settings", + offerId: DESKTOP_BUILTIN_TOOL_OFFER_IDS.settings, label: "Client settings", description: "Read or update UI and operating-system settings owned by this Desktop client.", tools: clientSettingsTools, }, { - offerId: "desktop_rive", + offerId: DESKTOP_BUILTIN_TOOL_OFFER_IDS.rive, label: "Rive", description: "Use durable Rive workflows through this Desktop client.", diff --git a/apps/desktop/src/main/runtime-host-native-capabilities.ts b/apps/desktop/src/main/runtime-host-native-capabilities.ts index e812492c62..3a4e1e8234 100644 --- a/apps/desktop/src/main/runtime-host-native-capabilities.ts +++ b/apps/desktop/src/main/runtime-host-native-capabilities.ts @@ -18,6 +18,7 @@ */ import { Buffer } from "node:buffer"; +import { DESKTOP_BUILTIN_TOOL_OFFER_IDS } from '@maka/core/tool-catalog'; import type { ComputerUseToolSet } from '@maka/runtime/computer-use-tools'; import type { MakaTool } from '@maka/runtime/tool-runtime'; import { @@ -38,8 +39,8 @@ import { toJSONSchema, z } from "zod"; import type { DesktopTargetScope } from '../shared/runtime-host-identity.js'; const CAPABILITY_VERSION = "0"; -const BROWSER_OFFER_ID = "desktop_browser"; -const COMPUTER_USE_OFFER_ID = "desktop_computer_use"; +const BROWSER_OFFER_ID = DESKTOP_BUILTIN_TOOL_OFFER_IDS.browser; +const COMPUTER_USE_OFFER_ID = DESKTOP_BUILTIN_TOOL_OFFER_IDS.computerUse; export interface DesktopCapabilityGroup { readonly offerId: string; diff --git a/apps/desktop/src/main/runtime-host-search-ipc-main.ts b/apps/desktop/src/main/runtime-host-search-ipc-main.ts index 6fe5e4b07f..76be6625a6 100644 --- a/apps/desktop/src/main/runtime-host-search-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-search-ipc-main.ts @@ -69,6 +69,11 @@ function projectDesktopSearchResult(result: SearchResult): SearchResult { sessionId: result.target.sessionId, ...(result.target.turnId !== undefined ? { turnId: result.target.turnId } : {}), ...(result.target.sequence !== undefined ? { sequence: result.target.sequence } : {}), + ...(result.target.matchKind !== undefined ? { matchKind: result.target.matchKind } : {}), + ...(result.target.tool !== undefined ? { tool: result.target.tool } : {}), + ...(result.target.toolResultIsError !== undefined + ? { toolResultIsError: result.target.toolResultIsError } + : {}), }, }; } diff --git a/packages/core/src/search.ts b/packages/core/src/search.ts index b3a0a58bc4..27d7379018 100644 --- a/packages/core/src/search.ts +++ b/packages/core/src/search.ts @@ -116,6 +116,10 @@ export type SearchResultTarget = { messageId?: string; /** Stable machine-readable classification of the matched transcript surface. */ matchKind?: ThreadSearchMatchKind; + /** Tool identity for UI-owned presentation of tool-intent matches. */ + tool?: { name: string; displayName?: string }; + /** Outcome for UI-owned presentation of tool-result matches. */ + toolResultIsError?: boolean; /** Timestamp of the matched stored message; absent for session-title matches. */ messageTimestamp?: number; }; diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 51ea84dfb5..d79e34020b 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -829,6 +829,7 @@ export interface ToolCallMessage { toolName: string; /** Stable semantic category for presentation; absent on legacy rows. */ activityKind?: ToolActivityKind; + /** Provider/compatibility label; built-in UI copy must resolve from toolName and the active locale. */ displayName?: string; intent?: string; args: unknown; diff --git a/packages/core/src/thread-search.ts b/packages/core/src/thread-search.ts index 7e6086e8d5..1f0b7b4387 100644 --- a/packages/core/src/thread-search.ts +++ b/packages/core/src/thread-search.ts @@ -279,7 +279,6 @@ export async function runThreadSearch( results.push({ source: THREAD_SOURCE, title: searchableTitle, - summary: '任务标题', snippet, target: { kind: 'thread', @@ -339,7 +338,6 @@ export async function runThreadSearch( results.push({ source: THREAD_SOURCE, title: redactSecrets(session.name), - summary: formatSearchResultSummary(message), snippet, // PR-SEARCH-1.5: navigation target via discriminated union; no // `url` field for thread results (maka://session is deferred). @@ -350,6 +348,17 @@ export async function runThreadSearch( sequence: messageIndex, messageId: message.id, matchKind: threadSearchMatchKind(message), + ...(message.type === 'tool_call' + ? { + tool: { + name: message.toolName, + ...(message.displayName + ? { displayName: redactSecrets(message.displayName) } + : {}), + }, + } + : {}), + ...(message.type === 'tool_result' ? { toolResultIsError: message.isError } : {}), messageTimestamp: message.ts, }, }); @@ -471,31 +480,6 @@ export function threadSearchMatchKind(message: StoredMessage): ThreadSearchMatch } } -export function formatSearchResultSummary(message: StoredMessage): string { - switch (message.type) { - case 'user': - return '用户消息'; - case 'assistant': - return '助手回复'; - case 'tool_call': - return message.displayName - ? `工具调用:${message.displayName}` - : `工具调用:${message.toolName}`; - case 'tool_result': - return message.isError ? '工具结果:失败' : '工具结果:成功'; - case 'permission_decision': - return '权限记录'; - case 'token_usage': - return '用量记录'; - case 'turn_state': - return '回合状态'; - case 'workhub_coordination': - return 'WorkHub 协调记录'; - case 'system_note': - return '系统记录'; - } -} - /** * Extract user-visible answer text from a stored message. Returns `undefined` * for excluded message kinds (system notes, token usage, turn state, diff --git a/packages/core/src/tool-catalog.ts b/packages/core/src/tool-catalog.ts index c95e2170b4..a8106a1179 100644 --- a/packages/core/src/tool-catalog.ts +++ b/packages/core/src/tool-catalog.ts @@ -32,6 +32,13 @@ export const TOOL_HOST_IDS = ['desktop', 'cli', 'runtime-host'] as const; export type ToolHostId = (typeof TOOL_HOST_IDS)[number]; +export const DESKTOP_BUILTIN_TOOL_OFFER_IDS = { + browser: 'desktop_browser', + computerUse: 'desktop_computer_use', + settings: 'desktop_settings', + rive: 'desktop_rive', +} as const; + /** Whether a host product surface may bind the pack. Not a runtime enable flag. */ export type ToolHostSupport = 'supported' | 'unsupported'; @@ -92,65 +99,69 @@ function freezeSurface(surface: CatalogSurfaceDef): CatalogSurfaceDef { } /** Always-on product tools (no surface) plus every surface member. */ +const MAKA_CATALOG_TOOL_DEFS = [ + // Core file / shell + { name: 'Bash' }, + { name: 'Read' }, + { name: 'ArchiveRead' }, + { name: 'Write' }, + { name: 'Edit' }, + { name: 'apply_patch' }, + { name: 'FormatJson' }, + { name: 'Glob' }, + { name: 'Grep' }, + { name: 'StopBackgroundTask' }, + { name: 'WriteStdin' }, + // Host product always-on + { name: 'AskUserQuestion' }, + { name: 'request_sandbox_boundary' }, + { name: 'Skill' }, + { name: 'SkillSearch' }, + { name: 'WebFetch', effects: ['network'] as const }, + { name: 'WebSearch' }, + { name: 'SearchHistory', effects: ['read'] as const }, + { name: 'ReadHistory', effects: ['read'] as const }, + { name: 'MakaSettingsGet', effects: ['read'] as const }, + { name: 'MakaSettingsUpdate', effects: ['write'] as const }, + { name: 'ExploreAgent' }, + { name: 'ScheduledTask' }, + { name: 'GoalSet' }, + { name: 'GoalClear' }, + { name: 'GoalStatus' }, + { name: 'GoalPause' }, + { name: 'GoalResume' }, + { name: 'task_create' }, + { name: 'task_update' }, + { name: 'task_list' }, + { name: 'task_get' }, + { name: 'memory_remember' }, + { name: 'memory_extract' }, + // browser surface + { name: 'browser_navigate' }, + { name: 'browser_snapshot' }, + { name: 'browser_click' }, + { name: 'browser_type' }, + { name: 'browser_wait' }, + { name: 'browser_extract' }, + // computer_use surface + { name: 'maka_computer' }, + // rive surface + { name: 'RiveWorkflow' }, + // agent surface (id matches AGENT_TOOL_GROUP_ID) + { name: 'agent_spawn' }, + { name: 'agent_list' }, + { name: 'agent_output' }, + { name: 'agent_swarm_status' }, + // Host-managed agent graph supervisor surface + { name: 'view_agent_graph' }, + { name: 'update_agent_graph' }, + { name: 'yield_agent_graph' }, +] as const satisfies readonly CatalogToolDef[]; + +export type CatalogToolName = (typeof MAKA_CATALOG_TOOL_DEFS)[number]['name']; + export const MAKA_CATALOG_TOOLS: readonly CatalogToolDef[] = Object.freeze( - [ - // Core file / shell - { name: 'Bash' }, - { name: 'Read' }, - { name: 'ArchiveRead' }, - { name: 'Write' }, - { name: 'Edit' }, - { name: 'apply_patch' }, - { name: 'FormatJson' }, - { name: 'Glob' }, - { name: 'Grep' }, - { name: 'StopBackgroundTask' }, - { name: 'WriteStdin' }, - // Host product always-on - { name: 'AskUserQuestion' }, - { name: 'request_sandbox_boundary' }, - { name: 'Skill' }, - { name: 'SkillSearch' }, - { name: 'WebFetch', effects: ['network'] as const }, - { name: 'WebSearch' }, - { name: 'SearchHistory', effects: ['read'] as const }, - { name: 'ReadHistory', effects: ['read'] as const }, - { name: 'MakaSettingsGet', effects: ['read'] as const }, - { name: 'MakaSettingsUpdate', effects: ['write'] as const }, - { name: 'ExploreAgent' }, - { name: 'ScheduledTask' }, - { name: 'GoalSet' }, - { name: 'GoalClear' }, - { name: 'GoalStatus' }, - { name: 'GoalPause' }, - { name: 'GoalResume' }, - { name: 'task_create' }, - { name: 'task_update' }, - { name: 'task_list' }, - { name: 'task_get' }, - { name: 'memory_remember' }, - { name: 'memory_extract' }, - // browser surface - { name: 'browser_navigate' }, - { name: 'browser_snapshot' }, - { name: 'browser_click' }, - { name: 'browser_type' }, - { name: 'browser_wait' }, - { name: 'browser_extract' }, - // computer_use surface - { name: 'maka_computer' }, - // rive surface - { name: 'RiveWorkflow' }, - // agent surface (id matches AGENT_TOOL_GROUP_ID) - { name: 'agent_spawn' }, - { name: 'agent_list' }, - { name: 'agent_output' }, - { name: 'agent_swarm_status' }, - // Host-managed agent graph supervisor surface - { name: 'view_agent_graph' }, - { name: 'update_agent_graph' }, - { name: 'yield_agent_graph' }, - ].map(freezeTool), + MAKA_CATALOG_TOOL_DEFS.map(freezeTool), ); /** diff --git a/packages/runtime/src/__tests__/history-tools.test.ts b/packages/runtime/src/__tests__/history-tools.test.ts index 5fbfbdd384..404edac2a0 100644 --- a/packages/runtime/src/__tests__/history-tools.test.ts +++ b/packages/runtime/src/__tests__/history-tools.test.ts @@ -117,8 +117,12 @@ test('SearchHistory returns typed message hits from current and other sessions', assert.ok(messageRows.every((row) => typeof row.message_id === 'string')); assert.ok(messageRows.every((row) => typeof row.message_timestamp === 'number')); assert.deepEqual( - new Set(result.rows.map((row) => row.match_kind)), - new Set(['session_title', 'user_message', 'assistant_message']), + new Map(result.rows.map((row) => [row.match_kind, row.summary])), + new Map([ + ['session_title', 'Session title'], + ['user_message', 'User message'], + ['assistant_message', 'Assistant response'], + ]), ); assert.equal(result.rows.find((row) => row.session_id === 'current')?.is_current_session, true); 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 assert.equal(second.next_cursor, undefined); }); +test('SearchHistory summaries echo invoked wire tool names and cover tool outcomes', async () => { + const needle = 'history summary needle'; + const messages = new Map([ + [ + 'past', + [ + { + type: 'tool_call', + id: 'legacy-browser', + turnId: 'turn-1', + ts: 1, + toolName: 'mcp__desktop_browser__browser_navigate', + displayName: '浏览器导航', + intent: needle, + args: {}, + }, + { + type: 'tool_call', + id: 'secret-tool', + turnId: 'turn-1', + ts: 2, + toolName: 'sk-ant-test-secret-token-12345', + displayName: 'Acme Lookup', + intent: needle, + args: {}, + }, + { + type: 'tool_result', + id: 'success-result', + turnId: 'turn-1', + ts: 3, + toolUseId: 'legacy-browser', + isError: false, + content: { note: needle } as never, + }, + { + type: 'tool_result', + id: 'failure-result', + turnId: 'turn-1', + ts: 4, + toolUseId: 'secret-tool', + isError: true, + content: { note: needle } as never, + }, + ], + ], + ]); + const result = (await buildSearchHistoryTool( + historyDeps([session('past', 'Past', 1)], messages), + ).impl({ query: needle, limit: 10 }, context())) as { + rows: Array<{ summary: string }>; + }; + + assert.ok( + result.rows.some((row) => row.summary === 'Tool call: mcp__desktop_browser__browser_navigate'), + ); + assert.ok(result.rows.some((row) => row.summary === 'Tool result: Succeeded')); + assert.ok(result.rows.some((row) => row.summary === 'Tool result: Failed')); + assert.match(JSON.stringify(result.rows), /\[redacted\]/u); + assert.doesNotMatch(JSON.stringify(result.rows), /sk-ant-test-secret-token-12345/u); +}); + test('ReadHistory returns a bounded visible excerpt without reasoning or raw tool data', async () => { const huge = `finished ${'x'.repeat(HISTORY_READ_MAX_BYTES * 2)}`; const messages = new Map([ diff --git a/packages/runtime/src/__tests__/tool-catalog-derive.test.ts b/packages/runtime/src/__tests__/tool-catalog-derive.test.ts index 04c10b7c95..37b3667ea7 100644 --- a/packages/runtime/src/__tests__/tool-catalog-derive.test.ts +++ b/packages/runtime/src/__tests__/tool-catalog-derive.test.ts @@ -103,6 +103,13 @@ describe('buildSearchableToolGroupsFromCatalog', () => { }); describe('assertProductBindingCatalogClean', () => { + it('rejects UI-only built-in names outside the product binding catalog', () => { + assert.throws( + () => assertProductBindingCatalogClean('cli', ['deep_research_start']), + /deep_research_start/, + ); + }); + it('throws when a product name is missing from the catalog', () => { assert.throws( () => assertProductBindingCatalogClean('cli', ['Read', 'NotARealTool']), diff --git a/packages/runtime/src/history-tools.ts b/packages/runtime/src/history-tools.ts index 67357be10e..913d64fe88 100644 --- a/packages/runtime/src/history-tools.ts +++ b/packages/runtime/src/history-tools.ts @@ -24,6 +24,7 @@ import { normalizeSearchLimit, normalizeSearchQuery, type SearchError, + type SearchResult, type ThreadSearchMatchKind, } from '@maka/core/search'; import { collapseSessionRevisions } from '@maka/core/session-revisions'; @@ -58,6 +59,25 @@ type HistoryReadErrorReason = | 'empty_transcript' | 'aborted'; +function historySearchSummary(row: SearchResult): string { + switch (row.target?.matchKind) { + case 'session_title': + return 'Session title'; + case 'user_message': + return 'User message'; + case 'assistant_message': + return 'Assistant response'; + case 'tool_intent': { + const tool = row.target.tool; + return `Tool call: ${redactSecrets(tool?.name ?? 'unknown')}`; + } + case 'tool_result': + return row.target.toolResultIsError ? 'Tool result: Failed' : 'Tool result: Succeeded'; + default: + return redactSecrets(row.summary ?? ''); + } +} + interface HistoryTurnMessage { readonly messageId: string; readonly matchKind: Exclude; @@ -178,7 +198,7 @@ export function buildSearchHistoryTool(deps: HistoryToolDeps): MakaTool { : {}), is_current_session: row.target.sessionId === context.sessionId, title: row.title, - summary: redactSecrets(row.summary ?? ''), + summary: historySearchSummary(row), snippet: row.snippet ?? '', ...(session?.lastMessageAt !== undefined ? { last_message_at: session.lastMessageAt } diff --git a/packages/runtime/src/tool-runtime.ts b/packages/runtime/src/tool-runtime.ts index b46e2917a2..ee0f74a61d 100644 --- a/packages/runtime/src/tool-runtime.ts +++ b/packages/runtime/src/tool-runtime.ts @@ -146,7 +146,7 @@ export interface MakaTool

{ description: string; /** Zod schema describing the tool's argument shape. */ parameters: unknown; - /** Optional UI display name. */ + /** Optional provider or compatibility label. Localized built-in UI copy resolves from the canonical name. */ displayName?: string; /** Stable semantic category used by UI presentation; never carries styling. */ activityKind?: ToolActivityKind; diff --git a/packages/ui/src/__tests__/search-modal-source.test.ts b/packages/ui/src/__tests__/search-modal-source.test.ts index 513ff50c81..7dc3d298c5 100644 --- a/packages/ui/src/__tests__/search-modal-source.test.ts +++ b/packages/ui/src/__tests__/search-modal-source.test.ts @@ -20,7 +20,10 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import type { SearchResult } from '@maka/core/search'; -import { createThreadSearchSource } from '../search-modal.js'; +import { + createThreadSearchSource, + formatThreadSearchResultSummary, +} from '../search-modal.js'; function deferred() { let resolve!: (value: T) => void; @@ -87,6 +90,43 @@ function createHarness() { } describe('thread search source', () => { + it('formats result metadata in the active locale', () => { + const summary = ( + matchKind: NonNullable['matchKind']>, + locale: 'zh' | 'en', + target: Partial> = {}, + ) => formatThreadSearchResultSummary({ + source: 'thread', + title: 'Task', + target: { kind: 'thread', sessionId: 's1', matchKind, ...target }, + }, locale); + + assert.equal(summary('session_title', 'en'), 'Task title'); + assert.equal(summary('user_message', 'zh'), '用户消息'); + assert.equal(summary('assistant_message', 'en'), 'Assistant response'); + assert.equal(summary('tool_result', 'zh'), '工具结果:成功'); + assert.equal(summary('tool_result', 'en', { toolResultIsError: true }), 'Tool result: Failed'); + assert.equal( + summary('tool_intent', 'en', { + tool: { name: 'Bash', displayName: '执行命令' }, + }), + 'Tool call: Run command', + ); + assert.equal( + summary('tool_intent', 'zh', { + tool: { name: 'mcp__acme__lookup', displayName: 'Acme Lookup' }, + }), + '工具调用:Acme Lookup', + ); + assert.equal( + summary('tool_intent', 'zh', { + tool: { name: 'tool_search', displayName: 'Enable capability' }, + }), + '工具调用:启用能力', + ); + assert.equal(summary('tool_intent', 'en'), 'Tool call'); + }); + it('keeps the selectable mapping while a filtered follow-up is pending', async () => { const harness = createHarness(); const initial = harness.source.search('current'); diff --git a/packages/ui/src/__tests__/tool-activity-presentation.test.ts b/packages/ui/src/__tests__/tool-activity-presentation.test.ts index ecb7ef2aa9..b45dcba6a3 100644 --- a/packages/ui/src/__tests__/tool-activity-presentation.test.ts +++ b/packages/ui/src/__tests__/tool-activity-presentation.test.ts @@ -31,6 +31,7 @@ import { computerRunningLabel, isComputerTool, } from '../tool-activity/computer-action-label.js'; +import { BUILTIN_TOOL_LABELS } from '../tool-activity/copy.js'; function renderToStaticMarkup(node: ReactNode, locale: 'zh' | 'en' = 'zh'): string { return renderReactToStaticMarkup(createElement(LocaleProvider, { @@ -95,6 +96,21 @@ describe('tool activity presentation', () => { assert.doesNotMatch(markup, /控制本机应用|切换并重试/); }); + it('localizes Desktop Browser proxy rows', () => { + const markup = renderToStaticMarkup(createElement(ToolTrow, { + items: [{ + toolUseId: 'browser-navigation', + toolName: 'mcp__desktop_browser__browser_navigate', + displayName: 'browser_navigate', + status: 'errored', + args: { url: 'https://example.com' }, + } satisfies ToolActivityItem], + }), 'en'); + + assert.match(markup, /Browser navigation/); + assert.doesNotMatch(markup, /browser_navigate/); + }); + it('localizes file-write result summaries', () => { const result = { kind: 'file_write' as const, @@ -435,7 +451,7 @@ describe('tool activity presentation', () => { assert.doesNotMatch(render(firstId), new RegExp(firstId)); assert.doesNotMatch(render(secondId), new RegExp(secondId)); - assert.match(render(firstId), /Bash/); + assert.match(render(firstId), new RegExp(BUILTIN_TOOL_LABELS.Bash.zh)); }); it('disambiguates code copy actions by their tool call', () => { diff --git a/packages/ui/src/__tests__/tool-display-name.test.ts b/packages/ui/src/__tests__/tool-display-name.test.ts new file mode 100644 index 0000000000..5880c1cbc4 --- /dev/null +++ b/packages/ui/src/__tests__/tool-display-name.test.ts @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { UI_LOCALES } from '@maka/core/ui-locale'; +import type { ToolActivityItem } from '../materialize.js'; +import { resolveToolDisplayName } from '../tool-activity/display-name.js'; +import { BUILTIN_TOOL_LABELS } from '../tool-activity/copy.js'; + +function item(toolName: string, displayName?: string): ToolActivityItem { + return { + toolUseId: toolName, + toolName, + ...(displayName !== undefined ? { displayName } : {}), + status: 'completed', + args: {}, + }; +} + +describe('tool display names', () => { + it('renders persisted built-in tools in the active locale', () => { + const browser = item('browser_navigate', '浏览器导航'); + const search = item('WebSearch', 'Web search'); + + assert.equal(resolveToolDisplayName(browser, 'en'), 'Browser navigation'); + assert.equal(resolveToolDisplayName(browser, 'zh'), '浏览器导航'); + assert.equal(resolveToolDisplayName(search, 'en'), 'Web search'); + assert.equal(resolveToolDisplayName(search, 'zh'), '联网搜索'); + }); + + it('localizes every Desktop-owned proxy group', () => { + const cases = [ + ['mcp__desktop_browser__browser_navigate', 'Browser navigation'], + ['mcp__desktop_computer_use__maka_computer', 'Maka Computer'], + ['mcp__desktop_settings__MakaClientSettingsGet', 'Read client settings'], + ['mcp__desktop_rive__RiveWorkflow', 'Rive workflow'], + ] as const; + + for (const [toolName, label] of cases) { + assert.equal(resolveToolDisplayName(item(toolName, 'wrong persisted label'), 'en'), label); + } + }); + + it('resolves every built-in tool through every locale cell', () => { + for (const locale of UI_LOCALES) { + for (const [toolName, labels] of Object.entries(BUILTIN_TOOL_LABELS)) { + assert.equal( + resolveToolDisplayName(item(toolName, 'wrong persisted label'), locale), + labels[locale], + `${toolName} in ${locale}`, + ); + } + } + }); + + it('localizes Maka-owned tools outside the product binding catalog', () => { + assert.equal(resolveToolDisplayName(item('MakaSettingsGet', 'Read Maka settings'), 'zh'), '读取 Maka 设置'); + assert.equal(resolveToolDisplayName(item('SkillSearch', 'SkillSearch'), 'zh'), '搜索技能'); + assert.equal(resolveToolDisplayName(item('SubmitPlan'), 'zh'), '提交计划'); + assert.equal(resolveToolDisplayName(item('tool_search'), 'zh'), '启用能力'); + }); + + it('keeps provider labels for external tools and falls back to their identifiers', () => { + assert.equal(resolveToolDisplayName(item('mcp__acme__lookup', 'Acme Lookup'), 'zh'), 'Acme Lookup'); + assert.equal(resolveToolDisplayName(item('custom_lookup'), 'en'), 'custom_lookup'); + assert.equal(resolveToolDisplayName(item('empty_label', ''), 'en'), 'empty_label'); + }); +}); diff --git a/packages/ui/src/search-modal.tsx b/packages/ui/src/search-modal.tsx index ac13374e1a..8a779256ce 100644 --- a/packages/ui/src/search-modal.tsx +++ b/packages/ui/src/search-modal.tsx @@ -31,6 +31,7 @@ import { import { AstryxLocaleProvider } from './astryx-i18n.js'; import { getShellControlsCopy } from './shell-controls-copy.js'; import { useUiLocale } from './locale-context.js'; +import { resolveToolName } from './tool-activity/display-name.js'; interface SearchModalDeps { searchThread( @@ -50,6 +51,32 @@ interface SearchItemAuxiliaryData { type SearchItem = SearchableItem; +export function formatThreadSearchResultSummary( + result: SearchResult, + locale: UiLocale, +): string | undefined { + if (result.target?.kind !== 'thread' || !result.target.matchKind) return result.summary; + const copy = getShellControlsCopy(locale).search.resultSummary; + switch (result.target.matchKind) { + case 'session_title': + return copy.sessionTitle; + case 'user_message': + return copy.userMessage; + case 'assistant_message': + return copy.assistantMessage; + case 'tool_intent': { + const tool = result.target.tool; + return copy.toolCall( + tool ? resolveToolName(tool.name, tool.displayName, locale) : undefined, + ); + } + case 'tool_result': + return result.target.toolResultIsError + ? copy.toolResultFailure + : copy.toolResultSuccess; + } +} + interface ThreadSearchSourceInput { searchThread?: SearchModalDeps['searchThread']; canNavigate: boolean; @@ -253,14 +280,15 @@ export function SearchModal(props: { renderItem={(item) => { const result = item.auxiliaryData?.result; if (!result) return item.label; + const summary = formatThreadSearchResultSummary(result, locale); return (

{result.title}
- {result.summary && ( + {summary && (
- {result.summary} + {summary}
)} {result.snippet && ( diff --git a/packages/ui/src/shell-controls-copy.ts b/packages/ui/src/shell-controls-copy.ts index c1d184544a..ce7591c2fd 100644 --- a/packages/ui/src/shell-controls-copy.ts +++ b/packages/ui/src/shell-controls-copy.ts @@ -50,6 +50,14 @@ type ShellControlsCopy = { results(count: number): string; truncatedResults(count: number): string; resultsLabel: string; + resultSummary: { + sessionTitle: string; + userMessage: string; + assistantMessage: string; + toolCall(name?: string): string; + toolResultSuccess: string; + toolResultFailure: string; + }; }; }; @@ -83,6 +91,14 @@ const SHELL_CONTROLS_COPY_BY_LOCALE = { results: (count: number) => `找到 ${count} 条匹配`, truncatedResults: (count: number) => `结果较多,已显示前 ${count} 条`, resultsLabel: '搜索结果', + resultSummary: { + sessionTitle: '任务标题', + userMessage: '用户消息', + assistantMessage: '助手回复', + toolCall: (name?: string) => name ? `工具调用:${name}` : '工具调用', + toolResultSuccess: '工具结果:成功', + toolResultFailure: '工具结果:失败', + }, }, }, en: { @@ -115,6 +131,14 @@ const SHELL_CONTROLS_COPY_BY_LOCALE = { results: (count: number) => `${count} ${count === 1 ? 'match' : 'matches'}`, truncatedResults: (count: number) => `Many results; showing the first ${count}`, resultsLabel: 'Search results', + resultSummary: { + sessionTitle: 'Task title', + userMessage: 'User message', + assistantMessage: 'Assistant response', + toolCall: (name?: string) => name ? `Tool call: ${name}` : 'Tool call', + toolResultSuccess: 'Tool result: Succeeded', + toolResultFailure: 'Tool result: Failed', + }, }, }, } satisfies UiCatalog; diff --git a/packages/ui/src/tool-activity/copy.ts b/packages/ui/src/tool-activity/copy.ts index 8dc204d075..40378b85be 100644 --- a/packages/ui/src/tool-activity/copy.ts +++ b/packages/ui/src/tool-activity/copy.ts @@ -18,11 +18,88 @@ */ import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; +import type { CatalogToolName } from '@maka/core/tool-catalog'; type BackgroundTerminalStatus = 'running' | 'completed' | 'failed' | 'timed_out' | 'cancelled' | 'orphaned'; type WebCredentialCopyKey = 'env' | 'settings' | 'missing' | 'unknown'; type WebGuidanceKey = 'env' | 'settings' | 'rate_limited' | 'not_configured' | 'timed_out' | 'privacy_mode' | 'unknown'; +type LocalizedLabel = Record; + +/** Maka-owned tool labels, grouped by tool so translations stay aligned. */ +export const BUILTIN_TOOL_LABELS = { + apply_patch: { zh: '应用补丁', en: 'Apply patch' }, + Read: { zh: '读取文件', en: 'Read file' }, + Write: { zh: '写入文件', en: 'Write file' }, + Edit: { zh: '编辑文件', en: 'Edit file' }, + FormatJson: { zh: '格式化 JSON', en: 'Format JSON' }, + Glob: { zh: '查找文件', en: 'Find files' }, + Grep: { zh: '搜索文本', en: 'Search text' }, + Bash: { zh: '运行命令', en: 'Run command' }, + StopBackgroundTask: { zh: '停止后台任务', en: 'Stop background task' }, + WriteStdin: { zh: '写入终端', en: 'Write to terminal' }, + ArchiveRead: { zh: '读取归档结果', en: 'Read archived result' }, + WebFetch: { zh: '网页读取', en: 'Web fetch' }, + WebSearch: { zh: '联网搜索', en: 'Web search' }, + web_search: { zh: '联网搜索', en: 'Web search' }, + ScheduledTask: { zh: '定时任务', en: 'Scheduled task' }, + AskUserQuestion: { zh: '询问用户', en: 'Ask user' }, + request_sandbox_boundary: { zh: '请求沙箱边界', en: 'Request sandbox boundary' }, + Skill: { zh: '加载技能', en: 'Load skill' }, + SkillSearch: { zh: '搜索技能', en: 'Search skills' }, + MakaSettingsGet: { zh: '读取 Maka 设置', en: 'Read Maka settings' }, + MakaSettingsUpdate: { zh: '更新 Maka 设置', en: 'Update Maka settings' }, + memory_remember: { zh: '记住信息', en: 'Remember information' }, + memory_extract: { zh: '提取记忆', en: 'Extract memory' }, + GoalSet: { zh: '设置目标', en: 'Set goal' }, + GoalClear: { zh: '清除目标', en: 'Clear goal' }, + GoalPause: { zh: '暂停目标', en: 'Pause goal' }, + GoalResume: { zh: '恢复目标', en: 'Resume goal' }, + GoalStatus: { zh: '目标状态', en: 'Goal status' }, + task_create: { zh: '创建任务', en: 'Create task' }, + task_update: { zh: '更新任务', en: 'Update task' }, + task_list: { zh: '任务列表', en: 'List tasks' }, + task_get: { zh: '获取任务', en: 'Get task' }, + SearchHistory: { zh: '搜索对话历史', en: 'Search conversation history' }, + ReadHistory: { zh: '读取对话历史', en: 'Read conversation history' }, + agent_spawn: { zh: '启动智能体', en: 'Agent' }, + agent_list: { zh: '智能体列表', en: 'Agent list' }, + agent_output: { zh: '智能体输出', en: 'Agent output' }, + agent_swarm_status: { zh: '智能体集群状态', en: 'Agent swarm status' }, + view_agent_graph: { zh: '查看智能体图', en: 'View agent graph' }, + update_agent_graph: { zh: '更新智能体图', en: 'Update agent graph' }, + yield_agent_graph: { zh: '让出智能体图', en: 'Yield agent graph' }, + deep_research_start: { zh: '初始化研究工作区', en: 'Initialize research workspace' }, + deep_research_read_artifact: { zh: '读取研究产物', en: 'Read research artifact' }, + deep_research_save_artifact: { zh: '保存研究产物', en: 'Save research artifact' }, + deep_research_update_checklist: { zh: '更新研究清单', en: 'Update research checklist' }, + deep_research_record_step: { zh: '记录研究步骤', en: 'Record research step' }, + deep_research_checkpoint: { zh: '保存研究检查点', en: 'Checkpoint research' }, + deep_research_status: { zh: '读取研究工作区', en: 'Read research workspace' }, + deep_research_complete: { zh: '完成研究', en: 'Complete research' }, + ExploreAgent: { zh: '只读探索', en: 'Read-only exploration' }, + maka_computer: { zh: 'Maka Computer', en: 'Maka Computer' }, + SubmitPlan: { zh: '提交计划', en: 'Submit plan' }, + update_plan: { zh: '更新计划', en: 'Update plan' }, + cancel_plan: { zh: '取消计划', en: 'Cancel plan' }, + browser_navigate: { zh: '浏览器导航', en: 'Browser navigation' }, + browser_snapshot: { zh: '浏览器快照', en: 'Browser snapshot' }, + browser_click: { zh: '浏览器点击', en: 'Browser click' }, + browser_type: { zh: '浏览器输入', en: 'Browser input' }, + browser_wait: { zh: '浏览器等待', en: 'Browser wait' }, + browser_extract: { zh: '浏览器提取', en: 'Browser extract' }, + MakaClientSettingsGet: { zh: '读取客户端设置', en: 'Read client settings' }, + MakaClientSettingsUpdate: { zh: '更新客户端设置', en: 'Update client settings' }, + RiveWorkflow: { zh: 'Rive 工作流', en: 'Rive workflow' }, +} satisfies Record & Record; + +export type BuiltinToolName = keyof typeof BUILTIN_TOOL_LABELS; + +export function getBuiltinToolLabel(toolName: string, locale: UiLocale): string | undefined { + if (!Object.hasOwn(BUILTIN_TOOL_LABELS, toolName)) return undefined; + return BUILTIN_TOOL_LABELS[toolName as BuiltinToolName][locale]; +} + export interface ToolActivityCopy { errorLabel: string; /** The two outcomes a tool row spells out next to its name. */ diff --git a/packages/ui/src/tool-activity/display-name.ts b/packages/ui/src/tool-activity/display-name.ts index c75cfc503a..3d8ff1c625 100644 --- a/packages/ui/src/tool-activity/display-name.ts +++ b/packages/ui/src/tool-activity/display-name.ts @@ -18,15 +18,16 @@ */ /** - * The name a tool row shows. A tool carries its own `displayName` when the - * backend named it; the search/legacy activation connector gets a localized label - * (its raw name reads as an implementation detail); everything else falls back - * to the canonical tool name. + * The name a tool row shows. Maka-owned tools use locale copy, connectors use + * their activation summary, and external tools keep their provider label before + * falling back to the canonical tool name. */ import type { UiLocale } from '@maka/core/ui-locale'; +import { DESKTOP_BUILTIN_TOOL_OFFER_IDS } from '@maka/core/tool-catalog'; import type { ToolActivityItem } from '../materialize.js'; import { describeLoadToolResult, loadToolDisplayName } from '../tool-format.js'; +import { getBuiltinToolLabel } from './copy.js'; const CONNECTOR_TOOL_NAMES: ReadonlySet = new Set([ 'tool_search', @@ -34,16 +35,32 @@ const CONNECTOR_TOOL_NAMES: ReadonlySet = new Set([ 'load_tool', ]); +const BUILTIN_PROXY_PREFIXES = Object.values(DESKTOP_BUILTIN_TOOL_OFFER_IDS) + .map((offerId) => `mcp__${offerId}__`); + export function isConnectorTool(name: string): boolean { return CONNECTOR_TOOL_NAMES.has(name); } +export function resolveToolName( + toolName: string, + displayName: string | undefined, + locale: UiLocale, +): string { + if (isConnectorTool(toolName)) return loadToolDisplayName(locale); + const proxyPrefix = BUILTIN_PROXY_PREFIXES.find((prefix) => toolName.startsWith(prefix)); + const builtinLabel = getBuiltinToolLabel( + proxyPrefix ? toolName.slice(proxyPrefix.length) : toolName, + locale, + ); + return (builtinLabel ?? displayName) || toolName; +} + export function resolveToolDisplayName(item: ToolActivityItem, locale: UiLocale): string { - if (item.displayName) return item.displayName; if (isConnectorTool(item.toolName)) { const value = item.result?.kind === 'json' ? item.result.value : undefined; return describeLoadToolResult(item.args, value, locale)?.actionLabel ?? loadToolDisplayName(locale); } - return item.toolName; + return resolveToolName(item.toolName, item.displayName, locale); } diff --git a/packages/ui/src/tool-format.ts b/packages/ui/src/tool-format.ts index 30c8d44709..7b590c78f2 100644 --- a/packages/ui/src/tool-format.ts +++ b/packages/ui/src/tool-format.ts @@ -17,6 +17,7 @@ * under the License. */ +import { DESKTOP_BUILTIN_TOOL_OFFER_IDS } from '@maka/core/tool-catalog'; import type { UiLocale } from './locale-helpers.js'; import { redactSecrets } from './redact.js'; import { getToolActivityCopy } from './tool-activity/copy.js'; @@ -131,19 +132,19 @@ function loadToolGroupKind( if ( id === 'computer_use' - || id.endsWith('_desktop_computer_use') + || id.endsWith(`_${DESKTOP_BUILTIN_TOOL_OFFER_IDS.computerUse}`) || normalizedLabel === 'computer use' || hasTool('maka_computer') ) return 'computer_use'; if ( id === 'browser' - || id.endsWith('_desktop_browser') + || id.endsWith(`_${DESKTOP_BUILTIN_TOOL_OFFER_IDS.browser}`) || normalizedLabel === 'browser' || hasTool('browser_navigate') ) return 'browser'; if ( id === 'rive' - || id.endsWith('_desktop_rive') + || id.endsWith(`_${DESKTOP_BUILTIN_TOOL_OFFER_IDS.rive}`) || normalizedLabel === 'rive' || hasTool('riveworkflow') ) return 'rive'; @@ -153,7 +154,7 @@ function loadToolGroupKind( || hasTool('agent_spawn') ) return 'agent'; if ( - id.endsWith('_desktop_settings') + id.endsWith(`_${DESKTOP_BUILTIN_TOOL_OFFER_IDS.settings}`) || normalizedLabel === 'client settings' || hasTool('makasettingsget') ) return 'settings';