Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/images/pr/capability-loading-after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/pr/capability-loading-before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,14 @@ test('unknown Client Capability loads, invokes, and rebinds after UDS reconnect'
const capabilityToolNames = [tool.name, rejectedTool.name].sort((left, right) =>
left.localeCompare(right),
);
assert.deepEqual(loaded, { loaded: capabilityToolNames });
assert.deepEqual(loaded, {
loaded: capabilityToolNames,
group: {
id: group.id,
label: 'Unknown fixture',
description: 'A capability the Host source does not enumerate.',
},
});
assert.deepEqual(
availability.projectActiveTools?.({
completedSteps: [
Expand Down
32 changes: 32 additions & 0 deletions packages/runtime/src/__tests__/tool-availability.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,38 @@ describe('ToolAvailabilityRuntime — economy mode', () => {
assert.ok(connector);
await assert.rejects(async () => connector!.impl({ group: 'nope' }, ctx), /Unknown tool group/);
});

test('connector returns presentation metadata with the loaded tool ids', async () => {
const connector = runtime(true)
.prepare([])
.providerTools.find((t) => t.name === LOAD_TOOLS_NAME);
assert.ok(connector);
assert.deepEqual(await connector.impl({ group: 'rive' }, ctx), {
loaded: ['rive_run'],
group: {
id: 'rive',
label: 'Rive',
},
});
assert.deepEqual(await connector.impl({ group: 'docs' }, ctx), {
loaded: ['docs_edit', 'docs_read'],
group: {
id: 'docs',
description: 'Document tools',
},
});
assert.deepEqual(
await connector.toModelOutput?.({
toolCallId: 'tc',
input: { group: 'rive' },
output: await connector.impl({ group: 'rive' }, ctx),
}),
{
type: 'json',
value: { loaded: ['rive_run'] },
},
);
});
});

describe('ToolAvailabilityRuntime — durable ledger seed', () => {
Expand Down
30 changes: 28 additions & 2 deletions packages/runtime/src/tool-availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ export interface ToolAvailabilityConfig {
groups?: readonly ToolGroup[];
}

export interface LoadedToolGroup {
readonly id: string;
readonly label?: string;
readonly description?: string;
}

export interface LoadToolsResult {
readonly loaded: string[];
readonly group: LoadedToolGroup;
}

export function toolAvailabilityHash(config: ToolAvailabilityConfig): `sha256:${string}` {
return stableHash({
economy: config.economy,
Expand Down Expand Up @@ -272,7 +283,7 @@ export class ToolAvailabilityRuntime {
return out;
}

private buildConnector(): MakaTool<{ group: string }, { loaded: string[] }> {
private buildConnector(): MakaTool<{ group: string }, LoadToolsResult> {
// Only reached when economy is on, which requires at least one gated group,
// so `ids` is always non-empty — a plain enum, no empty fallback.
const ids = this.groups.map((group) => group.id);
Expand All @@ -289,8 +300,23 @@ export class ToolAvailabilityRuntime {
if (!found) {
throw new Error(`Unknown tool group "${group}". Available: ${ids.join(', ')}.`);
}
return { loaded: [...found.toolNames] };
return {
loaded: [...found.toolNames],
group: {
id: found.id,
...(found.label ? { label: found.label } : {}),
...(found.description ? { description: found.description } : {}),
},
};
},
// Keep presentation metadata in the durable result without spending
// provider context on copy the model already received in the catalog.
toModelOutput: ({ output }) => ({
type: 'json',
value: {
loaded: [...(output as LoadToolsResult).loaded],
},
}),
};
}

Expand Down
137 changes: 137 additions & 0 deletions packages/ui/src/__tests__/tool-activity-presentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,143 @@ describe('tool activity presentation', () => {
);
});

it('renders a client capability activation as a localized capability summary', () => {
const item: ToolActivityItem = {
toolUseId: 'load-computer-use',
toolName: 'load_tools',
activityKind: 'tool',
status: 'completed',
args: { group: 'client_a5b9af66b60c5f5c_desktop_computer_use' },
result: {
kind: 'json',
value: {
loaded: ['mcp__desktop_computer_use__maka_computer'],
group: {
id: 'client_a5b9af66b60c5f5c_desktop_computer_use',
label: 'Computer Use',
description: 'Observe and operate the desktop through this Desktop client.',
},
},
},
};

const row = renderToStaticMarkup(createElement(ToolTrow, { items: [item] }));
assert.match(row, /启用桌面操作/);
const detail = renderToStaticMarkup(createElement(ToolCallDetail, { item }));
assert.match(detail, /桌面操作已启用/);
assert.match(detail, /可以查看和操作已授权的本地应用/);
assert.match(detail, /Computer Use/);
assert.match(detail, /1 项能力可用/);
assert.match(detail, /技术详情/);
assert.match(detail, /client_a5b9af66b60c5f5c_desktop_computer_use/);
});

it('keeps legacy Computer Use activations friendly without result metadata', () => {
const item: ToolActivityItem = {
toolUseId: 'legacy-load-computer-use',
toolName: 'load_tool',
status: 'completed',
args: { namespace: 'client_legacy_desktop_computer_use' },
result: {
kind: 'json',
value: { loaded: ['mcp__desktop_computer_use__maka_computer'] },
},
};

const markup = renderToStaticMarkup(createElement(ToolCallDetail, { item }));
assert.match(markup, /桌面操作已启用/);
assert.doesNotMatch(markup, /已加载 client_legacy_desktop_computer_use 工具组/);
});

it('uses supplied labels for third-party capability groups', () => {
const item: ToolActivityItem = {
toolUseId: 'load-third-party',
toolName: 'load_tools',
status: 'completed',
args: { group: 'client_external_notionsuite' },
result: {
kind: 'json',
value: {
loaded: ['mcp__notion__search', 'mcp__notion__create_page'],
group: {
id: 'client_external_notionsuite',
label: 'Notion',
description: 'Search and update the connected workspace.',
},
},
},
};

const row = renderToStaticMarkup(createElement(ToolTrow, { items: [item] }));
assert.match(row, /启用 Notion/);
const detail = renderToStaticMarkup(createElement(ToolCallDetail, { item }));
assert.match(detail, /Notion 已启用/);
assert.match(detail, /Search and update the connected workspace/);
assert.match(detail, /2 项能力可用/);
});

it('uses one localized presentation model for every first-party capability group', () => {
const cases = [
{
id: 'browser',
label: 'Browser',
tool: 'browser_navigate',
row: '启用浏览器操作',
title: '浏览器操作已启用',
},
{
id: 'client_desktop_mcp',
label: 'MCP',
tool: 'mcp__desktop_mcp__list',
row: '连接 MCP',
title: 'MCP 工具已连接',
},
{
id: 'rive',
label: 'Rive',
tool: 'RiveWorkflow',
row: '启用 Rive 工作流',
title: 'Rive 工作流已启用',
},
{
id: 'agent',
label: 'Agent',
tool: 'agent_spawn',
row: '启用子智能体',
title: '子智能体协作已启用',
},
{
id: 'client_desktop_settings',
label: 'Client settings',
tool: 'mcp__desktop_settings__MakaSettingsGet',
row: '启用设置工具',
title: '设置工具已启用',
},
] as const;

for (const capability of cases) {
const item: ToolActivityItem = {
toolUseId: `load-${capability.id}`,
toolName: 'load_tools',
status: 'completed',
args: { group: capability.id },
result: {
kind: 'json',
value: {
loaded: [capability.tool],
group: { id: capability.id, label: capability.label },
},
},
};

const row = renderToStaticMarkup(createElement(ToolTrow, { items: [item] }));
assert.match(row, new RegExp(capability.row));
const detail = renderToStaticMarkup(createElement(ToolCallDetail, { item }));
assert.match(detail, new RegExp(capability.title));
assert.doesNotMatch(detail, new RegExp(`>${capability.id}</p>`));
}
});

it('contains a malformed persisted terminal result instead of crashing the renderer', () => {
const malformed = {
kind: 'terminal',
Expand Down
4 changes: 0 additions & 4 deletions packages/ui/src/primitives/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,6 @@ const PREVIEW_PART_CLASSES = {
"load-tool-title": "maka-load-tool-title",
// `.maka-load-tool-count`
"load-tool-count": "maka-load-tool-count",
// `.maka-load-tool-tools`
"load-tool-tools": "maka-load-tool-tools",
// `.maka-load-tool-footer`
"load-tool-footer": "maka-load-tool-footer",
} as const;

type PreviewPart = keyof typeof PREVIEW_PART_CLASSES;
Expand Down
91 changes: 82 additions & 9 deletions packages/ui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1006,20 +1006,93 @@
.maka-web-search-error-repair { color: var(--foreground-secondary); }

.maka-load-tool-preview {
font: var(--maka-text-supporting);
display: grid; gap: var(--space-0-5); margin: var(--space-1) 0 0; padding: var(--space-1) var(--space-2); border-radius: var(--radius-control); background: var(--background); box-shadow: var(--ring-soft); }
display: grid;
grid-template-columns: var(--space-8) minmax(0, 1fr);
gap: var(--space-2-5);
align-items: start;
margin: var(--space-1) 0 0;
padding: var(--space-2) var(--space-2) var(--space-2-5);
}
.maka-load-tool-preview p { margin: 0; }
.maka-load-tool-icon {
display: grid;
width: var(--space-8);
height: var(--space-8);
place-items: center;
border-radius: var(--radius-element);
color: var(--foreground-secondary);
background: var(--foreground-5);
}
.maka-load-tool-summary {
min-width: 0;
padding-top: var(--space-0-5);
}
.maka-load-tool-title {
font: var(--maka-text-heading-5);
}
.maka-load-tool-count,
.maka-load-tool-footer { color: var(--muted-foreground); }
.maka-load-tool-tools {
}
.maka-load-tool-description {
font: var(--maka-text-supporting);
word-break: break-word; }
.maka-load-tool-footer {
margin-top: var(--space-0-5) !important;
color: var(--foreground-secondary);
overflow-wrap: anywhere;
}
.maka-load-tool-count {
font: var(--maka-text-supporting);
}
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-1);
margin-top: var(--space-1) !important;
color: var(--muted-foreground);
}
.maka-load-tool-separator {
width: 3px;
height: 3px;
border-radius: 50%;
background: currentColor;
opacity: 0.55;
}
.maka-load-tool-technical {
grid-column: 1 / -1;
margin-left: calc(var(--space-8) + var(--space-2-5));
color: var(--muted-foreground);
}
.maka-load-tool-technical > summary {
width: fit-content;
padding: var(--space-0-5) 0;
font: var(--maka-text-supporting);
cursor: pointer;
}
.maka-load-tool-technical > summary:focus-visible {
outline: none;
box-shadow: 0 0 0 var(--focus-ring-width) var(--focus-ring);
border-radius: var(--radius-control);
}
.maka-load-tool-technical dl {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: var(--space-1) var(--space-2);
margin: var(--space-1) 0 0;
padding-top: var(--space-2);
border-top: 1px solid var(--border-soft);
font: var(--maka-text-supporting);
}
.maka-load-tool-technical:not([open]) > dl {
display: none;
}
.maka-load-tool-technical dt {
color: var(--muted-foreground);
}
.maka-load-tool-technical dd {
min-width: 0;
margin: 0;
color: var(--foreground-secondary);
}
.maka-load-tool-technical code {
font: var(--maka-text-code);
white-space: pre-wrap;
overflow-wrap: anywhere;
}

/* Turn-footer action rings go inset (T5 geometry probe): the Astryx chat
layout's scrollport clips an outward ring on the first visible row's
Expand Down
Loading