@datalayer/agent-runtimes 1.3.54 → 1.3.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/lib/chat/base/ChatBase.js +8 -3
  2. package/lib/chat/index.d.ts +1 -0
  3. package/lib/chat/index.js +3 -0
  4. package/lib/chat/marks/SpecMark.d.ts +46 -0
  5. package/lib/chat/marks/SpecMark.js +90 -0
  6. package/lib/chat/marks/iconRef.d.ts +29 -0
  7. package/lib/chat/marks/iconRef.js +40 -0
  8. package/lib/chat/marks/index.d.ts +8 -0
  9. package/lib/chat/marks/index.js +12 -0
  10. package/lib/chat/marks/toolMarks.d.ts +43 -0
  11. package/lib/chat/marks/toolMarks.js +110 -0
  12. package/lib/chat/messages/ChatMessageList.d.ts +8 -1
  13. package/lib/chat/messages/ChatMessageList.js +11 -18
  14. package/lib/chat/parts/ToolPart.js +6 -1
  15. package/lib/chat/prompt/InputPrompt.d.ts +4 -1
  16. package/lib/chat/prompt/InputPrompt.js +2 -2
  17. package/lib/chat/prompt/menus/SkillsMenu.js +2 -1
  18. package/lib/chat/prompt/menus/ToolsMenu.d.ts +3 -1
  19. package/lib/chat/prompt/menus/ToolsMenu.js +25 -9
  20. package/lib/chat/tools/ToolCallDisplay.d.ts +8 -1
  21. package/lib/chat/tools/ToolCallDisplay.js +3 -2
  22. package/lib/examples/AgentStrategyExample.d.ts +12 -0
  23. package/lib/examples/{AgentLoopExample.js → AgentStrategyExample.js} +62 -62
  24. package/lib/examples/example-selector.js +1 -1
  25. package/lib/examples/index.d.ts +1 -1
  26. package/lib/examples/index.js +1 -1
  27. package/lib/examples/main.js +3 -3
  28. package/lib/loop/plugins/chat/ChatView.js +7 -5
  29. package/lib/specs/actions.js +56 -0
  30. package/lib/specs/frontendTools.js +6 -6
  31. package/lib/specs/index.d.ts +1 -1
  32. package/lib/specs/index.js +1 -1
  33. package/lib/specs/mcpServers.d.ts +1 -0
  34. package/lib/specs/mcpServers.js +39 -16
  35. package/lib/specs/skills.js +7 -7
  36. package/lib/specs/strategies.d.ts +34 -0
  37. package/lib/specs/{loops.js → strategies.js} +30 -30
  38. package/lib/specs/tools.js +21 -21
  39. package/lib/types/chat.d.ts +4 -0
  40. package/lib/types/index.d.ts +1 -1
  41. package/lib/types/index.js +1 -1
  42. package/lib/types/mcp.d.ts +2 -2
  43. package/lib/types/skills.d.ts +2 -2
  44. package/lib/types/{loops.d.ts → strategies.d.ts} +17 -17
  45. package/lib/types/tools.d.ts +4 -4
  46. package/package.json +1 -1
  47. package/scripts/codegen/agentspecs_clone.py +5 -3
  48. package/scripts/codegen/generate_agents.py +1 -1
  49. package/scripts/codegen/generate_frontend_tools.py +2 -0
  50. package/scripts/codegen/generate_mcp_servers.py +2 -0
  51. package/scripts/codegen/generate_skills.py +2 -0
  52. package/scripts/codegen/{generate_loops.py → generate_strategies.py} +81 -81
  53. package/scripts/codegen/generate_tools.py +2 -0
  54. package/scripts/codegen/marks.py +33 -0
  55. package/lib/examples/AgentLoopExample.d.ts +0 -12
  56. package/lib/specs/loops.d.ts +0 -34
  57. /package/lib/types/{loops.js → strategies.js} +0 -0
@@ -1342,7 +1342,12 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
1342
1342
  return true;
1343
1343
  })
1344
1344
  .map(tool => ({ id: tool.name, name: tool.name }));
1345
- return [...fromConfig, ...fromPage];
1345
+ /*
1346
+ * Kept apart: the runtime's tools, and the frontend tools — the ones this
1347
+ * page runs. The menu names them under two headings, so a person can tell
1348
+ * what the agent does on the server from what it does here.
1349
+ */
1350
+ return { runtime: fromConfig, frontend: fromPage };
1346
1351
  }, [configQuery.data?.builtinTools, frontendTools, protocol?.options]);
1347
1352
  /*
1348
1353
  * Who is answering, as one row when that is all there is.
@@ -3489,7 +3494,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3489
3494
  maxWidth: 920,
3490
3495
  mx: 'auto',
3491
3496
  bg: 'canvas.default',
3492
- }, children: _jsx(ChatMessageList, { displayItems: displayItems, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding, renderToolResult: effectiveRenderToolResult, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, agentUsage: agentUsage, onRemoveItems: handleRemoveItems, emptyContent: launching ? null : (_jsx(ChatEmptyState, { emptyState: emptyState, brandIcon: brandIcon, description: resolvedDescription, suggestions: resolvedSuggestions, submitOnSuggestionClick: submitOnSuggestionClick, onSuggestionSubmit: handleSuggestionSubmit, onSuggestionFill: handleSuggestionFill })) }) }));
3497
+ }, children: _jsx(ChatMessageList, { displayItems: displayItems, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding, renderToolResult: effectiveRenderToolResult, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, agentUsage: agentUsage, onRemoveItems: handleRemoveItems, mcpServers: filteredMcpServers, emptyContent: launching ? null : (_jsx(ChatEmptyState, { emptyState: emptyState, brandIcon: brandIcon, description: resolvedDescription, suggestions: resolvedSuggestions, submitOnSuggestionClick: submitOnSuggestionClick, onSuggestionSubmit: handleSuggestionSubmit, onSuggestionFill: handleSuggestionFill })) }) }));
3493
3498
  const inputToolbar = showInput ? (_jsx(InputPrompt, { input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
3494
3499
  /*
3495
3500
  The same openers the empty state offers, typed into the box they would
@@ -3513,7 +3518,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3513
3518
  models: configQuery.data.decisionModels,
3514
3519
  note: configQuery.data.decisionsNote,
3515
3520
  }
3516
- : undefined, selectedModel: selectedModel, onModelSelect: setSelectedModel, availableTools: builtinTools, mcpServers: filteredMcpServers, enabledMcpTools: enabledMcpTools, enabledMcpToolCount: getEnabledMcpToolNames().length, onToggleMcpTool: toggleMcpTool, onToggleAllMcpServerTools: toggleAllMcpServerTools, approvedMcpTools: approvedMcpTools, onToggleMcpToolApproval: toggleMcpToolApproval, skills: skillsQuery.data?.skills || [], skillsLoading: !!skillsQuery.isLoading, enabledSkills: enabledSkills, onToggleSkill: toggleSkill, onToggleAllSkills: toggleAllSkills, approvedSkills: approvedSkills, onToggleSkillApproval: toggleSkillApproval, apiBase: indicatorApiBase, authToken: protocol?.authToken, mcpStatusData: effectiveMcpStatusData })) : null;
3521
+ : undefined, selectedModel: selectedModel, onModelSelect: setSelectedModel, availableTools: builtinTools.runtime, availableFrontendTools: builtinTools.frontend, mcpServers: filteredMcpServers, enabledMcpTools: enabledMcpTools, enabledMcpToolCount: getEnabledMcpToolNames().length, onToggleMcpTool: toggleMcpTool, onToggleAllMcpServerTools: toggleAllMcpServerTools, approvedMcpTools: approvedMcpTools, onToggleMcpToolApproval: toggleMcpToolApproval, skills: skillsQuery.data?.skills || [], skillsLoading: !!skillsQuery.isLoading, enabledSkills: enabledSkills, onToggleSkill: toggleSkill, onToggleAllSkills: toggleAllSkills, approvedSkills: approvedSkills, onToggleSkillApproval: toggleSkillApproval, apiBase: indicatorApiBase, authToken: protocol?.authToken, mcpStatusData: effectiveMcpStatusData })) : null;
3517
3522
  // Shared header element. It is rendered either at the top of the chat (when
3518
3523
  // no ephemeral notebook is shown) or INSIDE the chat body column (when the
3519
3524
  // notebook is visible) so the header always follows the chat body across all
@@ -62,6 +62,7 @@ export { ChatHeader, type ChatHeaderProps } from './header/ChatHeader';
62
62
  export { ChatViewModeToggle, VIEW_MODE_ICONS, type ChatViewModeToggleProps, } from './header/ChatViewModeToggle';
63
63
  export { MessagePart, type MessagePartProps, TextPart, type TextPartProps, ReasoningPart, type ReasoningPartProps, ToolPart, type ToolPartProps, DynamicToolPart, type DynamicToolPartProps, } from './parts';
64
64
  export { ToolCallDisplay, type ToolCallDisplayProps } from './tools';
65
+ export { ICON_PACKAGES, SpecMark, hasMark, marksOfFrontendTool, marksOfMcpServer, marksOfRuntimeTool, marksOfSkill, marksOfToolCall, parseIconRef, useMarkIcon, type IconPackage, type IconRef, type MarkIcon, type MarkedMcpServer, type Marks, type SpecMarkProps, type ToolKind, type ToolMarks, } from './marks';
65
66
  export { ToolApprovalDialog, useToolApprovalDialog, type ToolApprovalDialogProps, } from './tools/ToolApprovalDialog';
66
67
  export { ToolApprovalBanner, type ToolApprovalBannerProps, type PendingApproval, } from './tools/ToolApprovalBanner';
67
68
  export { Chat, type ChatProps } from './Chat';
package/lib/chat/index.js CHANGED
@@ -73,6 +73,9 @@ export { ChatViewModeToggle, VIEW_MODE_ICONS, } from './header/ChatViewModeToggl
73
73
  export { MessagePart, TextPart, ReasoningPart, ToolPart, DynamicToolPart, } from './parts';
74
74
  // Components - Tool UI
75
75
  export { ToolCallDisplay } from './tools';
76
+ // Marks: the icon (by package), else the emoji, of an MCP server, a skill, a
77
+ // frontend tool set or a runtime tool — and whose a tool call is.
78
+ export { ICON_PACKAGES, SpecMark, hasMark, marksOfFrontendTool, marksOfMcpServer, marksOfRuntimeTool, marksOfSkill, marksOfToolCall, parseIconRef, useMarkIcon, } from './marks';
76
79
  export { ToolApprovalDialog, useToolApprovalDialog, } from './tools/ToolApprovalDialog';
77
80
  export { ToolApprovalBanner, } from './tools/ToolApprovalBanner';
78
81
  // Components - Transport-agnostic chat
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The mark of a catalogue entry: its icon, else its emoji, else nothing.
3
+ *
4
+ * An MCP server, a skill, a tool and a frontend tool set each name an icon by
5
+ * its package (`@datalayer/icons-react:odoo`). Each package is imported when
6
+ * the first icon from it is drawn, once, and never before: the Datalayer icons
7
+ * are five hundred components, and a chat that names three of them should not
8
+ * have to carry the rest up front.
9
+ *
10
+ * @module chat/marks/SpecMark
11
+ */
12
+ import React from 'react';
13
+ import { type IconPackage, type IconRef } from './iconRef';
14
+ /** What either package exports an icon as. */
15
+ export type MarkIcon = React.ComponentType<{
16
+ size?: number;
17
+ 'aria-hidden'?: boolean | 'true' | 'false';
18
+ }>;
19
+ type IconModule = Record<string, unknown>;
20
+ /** Import a package of icons, once. */
21
+ export declare function loadIconPackage(pkg: IconPackage): Promise<IconModule>;
22
+ /** The icon a reference names, from its loaded package; refused when it has none. */
23
+ export declare function iconOf(ref: IconRef, module: IconModule): MarkIcon;
24
+ /** An icon reference resolved: the component, `undefined` while its package loads. */
25
+ export declare function useMarkIcon(icon: string | undefined): MarkIcon | undefined;
26
+ export interface SpecMarkProps {
27
+ /** `<package>:<name>`, as the catalogue says it. */
28
+ icon?: string;
29
+ /** Drawn where there is no icon. */
30
+ emoji?: string;
31
+ /** In pixels. */
32
+ size?: number;
33
+ }
34
+ /**
35
+ * The icon, else the emoji, else nothing.
36
+ *
37
+ * While the icon's package loads, a blank of the icon's size holds its place,
38
+ * so the line it sits in does not move when it arrives.
39
+ */
40
+ export declare function SpecMark({ icon, emoji, size }: SpecMarkProps): React.JSX.Element | null;
41
+ /** Whether an entry has a mark to draw. */
42
+ export declare function hasMark(marks: {
43
+ icon?: string;
44
+ emoji?: string;
45
+ } | null | undefined): boolean;
46
+ export default SpecMark;
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ /**
7
+ * The mark of a catalogue entry: its icon, else its emoji, else nothing.
8
+ *
9
+ * An MCP server, a skill, a tool and a frontend tool set each name an icon by
10
+ * its package (`@datalayer/icons-react:odoo`). Each package is imported when
11
+ * the first icon from it is drawn, once, and never before: the Datalayer icons
12
+ * are five hundred components, and a chat that names three of them should not
13
+ * have to carry the rest up front.
14
+ *
15
+ * @module chat/marks/SpecMark
16
+ */
17
+ import { useEffect, useState } from 'react';
18
+ import { parseIconRef } from './iconRef';
19
+ const LOADERS = {
20
+ '@datalayer/icons-react': () => import('@datalayer/icons-react'),
21
+ '@primer/octicons-react': () => import('@primer/octicons-react'),
22
+ };
23
+ const loaded = new Map();
24
+ const loading = new Map();
25
+ /** Import a package of icons, once. */
26
+ export function loadIconPackage(pkg) {
27
+ const pending = loading.get(pkg) ??
28
+ LOADERS[pkg]().then(module => {
29
+ loaded.set(pkg, module);
30
+ return module;
31
+ });
32
+ loading.set(pkg, pending);
33
+ return pending;
34
+ }
35
+ /** The icon a reference names, from its loaded package; refused when it has none. */
36
+ export function iconOf(ref, module) {
37
+ const icon = module[ref.exportName];
38
+ if (!icon) {
39
+ throw new Error(`${ref.pkg} has no icon ${ref.name} (${ref.exportName}): name one it has`);
40
+ }
41
+ return icon;
42
+ }
43
+ /** An icon reference resolved: the component, `undefined` while its package loads. */
44
+ export function useMarkIcon(icon) {
45
+ const ref = icon ? parseIconRef(icon) : undefined;
46
+ const pkg = ref?.pkg;
47
+ const ready = pkg ? loaded.get(pkg) : undefined;
48
+ const [, setLoaded] = useState(0);
49
+ const [failure, setFailure] = useState(null);
50
+ useEffect(() => {
51
+ if (!pkg || ready) {
52
+ return;
53
+ }
54
+ let live = true;
55
+ loadIconPackage(pkg).then(() => live && setLoaded(n => n + 1), error => live && setFailure(error));
56
+ return () => {
57
+ live = false;
58
+ };
59
+ }, [pkg, ready]);
60
+ if (failure) {
61
+ throw failure;
62
+ }
63
+ return ref && ready ? iconOf(ref, ready) : undefined;
64
+ }
65
+ /**
66
+ * The icon, else the emoji, else nothing.
67
+ *
68
+ * While the icon's package loads, a blank of the icon's size holds its place,
69
+ * so the line it sits in does not move when it arrives.
70
+ */
71
+ export function SpecMark({ icon, emoji, size = 16 }) {
72
+ const Icon = useMarkIcon(icon);
73
+ if (icon) {
74
+ return Icon ? (_jsx("span", { "data-mark": icon, style: { display: 'inline-flex', flexShrink: 0, lineHeight: 0 }, children: _jsx(Icon, { size: size, "aria-hidden": "true" }) })) : (_jsx("span", { "data-mark": icon, "aria-hidden": "true", style: { display: 'inline-block', width: size, height: size } }));
75
+ }
76
+ if (emoji) {
77
+ return (_jsx("span", { "data-mark": "emoji", "aria-hidden": "true", style: {
78
+ display: 'inline-flex',
79
+ flexShrink: 0,
80
+ fontSize: Math.round(size * 0.9),
81
+ lineHeight: 1,
82
+ }, children: emoji }));
83
+ }
84
+ return null;
85
+ }
86
+ /** Whether an entry has a mark to draw. */
87
+ export function hasMark(marks) {
88
+ return Boolean(marks?.icon || marks?.emoji);
89
+ }
90
+ export default SpecMark;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * An icon reference, as the agentspecs catalogues write it.
3
+ *
4
+ * `<package>:<name>`: the package the icon is in and the icon's own name, in
5
+ * kebab case, as that package names it — `@datalayer/icons-react:odoo` (the
6
+ * Datalayer icons' `odoo.svg`, exported as `OdooIcon`),
7
+ * `@primer/octicons-react:mark-github` (`MarkGithubIcon`). The rule is
8
+ * `agentspecs.marks`; this is the same rule, read on the page.
9
+ *
10
+ * @module chat/marks/iconRef
11
+ */
12
+ /** The packages an icon may come from. */
13
+ export declare const ICON_PACKAGES: readonly ["@datalayer/icons-react", "@primer/octicons-react"];
14
+ export type IconPackage = (typeof ICON_PACKAGES)[number];
15
+ export interface IconRef {
16
+ /** The package the icon is in. */
17
+ pkg: IconPackage;
18
+ /** The icon's name, in kebab case: `odoo`, `mark-github`. */
19
+ name: string;
20
+ /** What the package exports it as: `OdooIcon`, `MarkGithubIcon`. */
21
+ exportName: string;
22
+ }
23
+ /**
24
+ * The component an icon's name is exported as, by the rule both packages
25
+ * follow: each word capitalised, joined, and `Icon` after it.
26
+ */
27
+ export declare function exportNameOf(name: string): string;
28
+ /** Read an icon reference; anything else is refused. */
29
+ export declare function parseIconRef(ref: string): IconRef;
@@ -0,0 +1,40 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * An icon reference, as the agentspecs catalogues write it.
7
+ *
8
+ * `<package>:<name>`: the package the icon is in and the icon's own name, in
9
+ * kebab case, as that package names it — `@datalayer/icons-react:odoo` (the
10
+ * Datalayer icons' `odoo.svg`, exported as `OdooIcon`),
11
+ * `@primer/octicons-react:mark-github` (`MarkGithubIcon`). The rule is
12
+ * `agentspecs.marks`; this is the same rule, read on the page.
13
+ *
14
+ * @module chat/marks/iconRef
15
+ */
16
+ /** The packages an icon may come from. */
17
+ export const ICON_PACKAGES = [
18
+ '@datalayer/icons-react',
19
+ '@primer/octicons-react',
20
+ ];
21
+ const ICON_REF = /^(@datalayer\/icons-react|@primer\/octicons-react):([a-z0-9]+(?:-[a-z0-9]+)*)$/;
22
+ /**
23
+ * The component an icon's name is exported as, by the rule both packages
24
+ * follow: each word capitalised, joined, and `Icon` after it.
25
+ */
26
+ export function exportNameOf(name) {
27
+ return (name
28
+ .split('-')
29
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
30
+ .join('') + 'Icon');
31
+ }
32
+ /** Read an icon reference; anything else is refused. */
33
+ export function parseIconRef(ref) {
34
+ const match = ICON_REF.exec(ref);
35
+ if (!match) {
36
+ throw new Error(`The icon ${JSON.stringify(ref)} is not <package>:<name>, the package one of ${ICON_PACKAGES.join(', ')} and the name in kebab case`);
37
+ }
38
+ const [, pkg, name] = match;
39
+ return { pkg: pkg, name, exportName: exportNameOf(name) };
40
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The marks of the catalogues: icons by package, emoji, and whose a tool call is.
3
+ *
4
+ * @module chat/marks
5
+ */
6
+ export { ICON_PACKAGES, exportNameOf, parseIconRef, type IconPackage, type IconRef, } from './iconRef';
7
+ export { SpecMark, hasMark, iconOf, loadIconPackage, useMarkIcon, type MarkIcon, type SpecMarkProps, } from './SpecMark';
8
+ export { SKILL_TOOLS, marksOfFrontendTool, marksOfMcpServer, marksOfRuntimeTool, marksOfSkill, marksOfToolCall, skillIdOfCall, type MarkedMcpServer, type Marks, type ToolKind, type ToolMarks, } from './toolMarks';
@@ -0,0 +1,12 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The marks of the catalogues: icons by package, emoji, and whose a tool call is.
7
+ *
8
+ * @module chat/marks
9
+ */
10
+ export { ICON_PACKAGES, exportNameOf, parseIconRef, } from './iconRef';
11
+ export { SpecMark, hasMark, iconOf, loadIconPackage, useMarkIcon, } from './SpecMark';
12
+ export { SKILL_TOOLS, marksOfFrontendTool, marksOfMcpServer, marksOfRuntimeTool, marksOfSkill, marksOfToolCall, skillIdOfCall, } from './toolMarks';
@@ -0,0 +1,43 @@
1
+ /** The two marks of a catalogue entry. */
2
+ export interface Marks {
3
+ /** `<package>:<name>` */
4
+ icon?: string;
5
+ emoji?: string;
6
+ }
7
+ /** What a tool call is, by who it belongs to. */
8
+ export type ToolKind = 'mcp' | 'skill' | 'frontend' | 'runtime';
9
+ export interface ToolMarks extends Marks {
10
+ kind: ToolKind;
11
+ /** The catalogue entry (or the MCP server) it belongs to. */
12
+ ownerId: string;
13
+ }
14
+ /** An MCP server as the chat knows it: what it serves, and its marks if it says them. */
15
+ export interface MarkedMcpServer {
16
+ id: string;
17
+ tools: {
18
+ name: string;
19
+ }[];
20
+ icon?: string;
21
+ emoji?: string;
22
+ }
23
+ /** The tools the skills area runs a skill through. */
24
+ export declare const SKILL_TOOLS: Set<string>;
25
+ /** The skill a skill tool call names, without its version. */
26
+ export declare function skillIdOfCall(toolName: string, args: Record<string, unknown> | undefined): string | null;
27
+ /** An MCP server's marks: its own when it says them, the catalogue's by its id otherwise. */
28
+ export declare function marksOfMcpServer(server: {
29
+ id: string;
30
+ icon?: string;
31
+ emoji?: string;
32
+ }): Marks;
33
+ /** A skill's marks, from the catalogue, by its id with or without a version. */
34
+ export declare function marksOfSkill(skillId: string): Marks;
35
+ /** A frontend tool's marks, by its name. */
36
+ export declare function marksOfFrontendTool(toolName: string): Marks;
37
+ /** A runtime tool's marks, by the name it is registered under. */
38
+ export declare function marksOfRuntimeTool(toolName: string): Marks;
39
+ /**
40
+ * Who a call belongs to, and its marks; `null` when nobody the catalogue
41
+ * marks claims it.
42
+ */
43
+ export declare function marksOfToolCall(toolName: string, args: Record<string, unknown> | undefined, mcpServers?: readonly MarkedMcpServer[]): ToolMarks | null;
@@ -0,0 +1,110 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * Whose mark a tool call carries.
7
+ *
8
+ * A call names a tool, and the tool belongs to something the catalogue marks:
9
+ * an MCP server (by the tools the server says it serves), a skill (the skill
10
+ * tools carry the skill's name in their arguments), a frontend tool set (by
11
+ * the names in its `toolset`) or a runtime tool (by its `runtime.method`, the
12
+ * name the runtime registers it under). A tool none of them claims has no mark.
13
+ *
14
+ * @module chat/marks/toolMarks
15
+ */
16
+ import { MCP_SERVER_LIBRARY } from '../../specs/mcpServers';
17
+ import { SKILLS_CATALOG } from '../../specs/skills';
18
+ import { FRONTEND_TOOL_CATALOG } from '../../specs/frontendTools';
19
+ import { TOOL_CATALOG } from '../../specs/tools';
20
+ /** The tools the skills area runs a skill through. */
21
+ export const SKILL_TOOLS = new Set([
22
+ 'run_skill_script',
23
+ 'load_skill',
24
+ 'read_skill_resource',
25
+ ]);
26
+ function marksOf(entry) {
27
+ return { icon: entry?.icon, emoji: entry?.emoji };
28
+ }
29
+ /** The skill a skill tool call names, without its version. */
30
+ export function skillIdOfCall(toolName, args) {
31
+ if (!SKILL_TOOLS.has(toolName)) {
32
+ return null;
33
+ }
34
+ const raw = args?.skill_name ?? args?.skill ?? args?.name;
35
+ if (typeof raw !== 'string' || raw.length === 0) {
36
+ return null;
37
+ }
38
+ return raw.split(':', 1)[0] || raw;
39
+ }
40
+ let frontendIndex = null;
41
+ let runtimeIndex = null;
42
+ /** Each frontend tool's name, to the first set of the catalogue that has it. */
43
+ function frontendTools() {
44
+ if (!frontendIndex) {
45
+ frontendIndex = new Map();
46
+ for (const spec of Object.values(FRONTEND_TOOL_CATALOG)) {
47
+ for (const name of spec.toolset ?? []) {
48
+ if (!frontendIndex.has(name)) {
49
+ frontendIndex.set(name, { id: spec.id, ...marksOf(spec) });
50
+ }
51
+ }
52
+ }
53
+ }
54
+ return frontendIndex;
55
+ }
56
+ /** Each runtime tool's registered name (`runtime.method`), to its spec. */
57
+ function runtimeTools() {
58
+ if (!runtimeIndex) {
59
+ runtimeIndex = new Map();
60
+ for (const spec of Object.values(TOOL_CATALOG)) {
61
+ const method = spec.runtime?.method;
62
+ if (method && !runtimeIndex.has(method)) {
63
+ runtimeIndex.set(method, { id: spec.id, ...marksOf(spec) });
64
+ }
65
+ }
66
+ }
67
+ return runtimeIndex;
68
+ }
69
+ /** An MCP server's marks: its own when it says them, the catalogue's by its id otherwise. */
70
+ export function marksOfMcpServer(server) {
71
+ if (server.icon || server.emoji) {
72
+ return marksOf(server);
73
+ }
74
+ return marksOf(MCP_SERVER_LIBRARY[server.id]);
75
+ }
76
+ /** A skill's marks, from the catalogue, by its id with or without a version. */
77
+ export function marksOfSkill(skillId) {
78
+ return marksOf(SKILLS_CATALOG[skillId.split(':', 1)[0] || skillId]);
79
+ }
80
+ /** A frontend tool's marks, by its name. */
81
+ export function marksOfFrontendTool(toolName) {
82
+ return marksOf(frontendTools().get(toolName));
83
+ }
84
+ /** A runtime tool's marks, by the name it is registered under. */
85
+ export function marksOfRuntimeTool(toolName) {
86
+ return marksOf(runtimeTools().get(toolName));
87
+ }
88
+ /**
89
+ * Who a call belongs to, and its marks; `null` when nobody the catalogue
90
+ * marks claims it.
91
+ */
92
+ export function marksOfToolCall(toolName, args, mcpServers = []) {
93
+ const skillId = skillIdOfCall(toolName, args);
94
+ if (skillId) {
95
+ return { kind: 'skill', ownerId: skillId, ...marksOfSkill(skillId) };
96
+ }
97
+ const server = mcpServers.find(candidate => candidate.tools.some(tool => tool.name === toolName));
98
+ if (server) {
99
+ return { kind: 'mcp', ownerId: server.id, ...marksOfMcpServer(server) };
100
+ }
101
+ const frontend = frontendTools().get(toolName);
102
+ if (frontend) {
103
+ return { kind: 'frontend', ownerId: frontend.id, ...marksOf(frontend) };
104
+ }
105
+ const runtime = runtimeTools().get(toolName);
106
+ if (runtime) {
107
+ return { kind: 'runtime', ownerId: runtime.id, ...marksOf(runtime) };
108
+ }
109
+ return null;
110
+ }
@@ -5,6 +5,7 @@
5
5
  * @module chat/messages/MessageList
6
6
  */
7
7
  import { type ReactNode, type RefObject } from 'react';
8
+ import { type MarkedMcpServer } from '../marks';
8
9
  import type { DisplayItem, AvatarConfig, RenderToolResult } from '../../types/chat';
9
10
  import type { ChatMessage } from '../../types/messages';
10
11
  import type { ContextSnapshotData } from '../../types/context';
@@ -66,6 +67,12 @@ export interface ChatMessageListProps {
66
67
  * `note`, which has no footer of its own.
67
68
  */
68
69
  renderMessageFooter?: (message: ChatMessage) => ReactNode;
70
+ /**
71
+ * The MCP servers the agent has, with the tools each serves: a tool call is
72
+ * drawn with the marks of the server it belongs to. Skills, frontend tools
73
+ * and runtime tools are found in the catalogues.
74
+ */
75
+ mcpServers?: readonly MarkedMcpServer[];
69
76
  }
70
77
  type SubagentAvatarConfig = Required<Pick<AvatarConfig, 'userAvatar' | 'assistantAvatar' | 'showAvatars' | 'avatarSize' | 'userAvatarBg' | 'assistantAvatarBg'>>;
71
78
  export interface SubagentChatPanelProps {
@@ -85,5 +92,5 @@ export interface SubagentChatPanelProps {
85
92
  * approval decisions are sent over the shared monitoring socket.
86
93
  */
87
94
  export declare function SubagentChatPanel({ toolCallId, subagentName: subagentNameHint, height, avatarConfig, }: SubagentChatPanelProps): React.ReactElement | null;
88
- export declare function ChatMessageList({ displayItems, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, renderToolResult, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters, agentUsage, onRemoveItems, renderMessageFooter, }: ChatMessageListProps): import("react").JSX.Element;
95
+ export declare function ChatMessageList({ displayItems, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, renderToolResult, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters, agentUsage, onRemoveItems, renderMessageFooter, mcpServers, }: ChatMessageListProps): import("react").JSX.Element;
89
96
  export {};
@@ -16,6 +16,7 @@ import { Box } from '@datalayer/primer-addons';
16
16
  import { Streamdown } from 'streamdown';
17
17
  import { streamdownMarkdownStyles, streamdownCodeBlockStyles, } from '../styles/streamdownStyles';
18
18
  import { ToolCallDisplay } from '../tools/ToolCallDisplay';
19
+ import { marksOfToolCall, skillIdOfCall } from '../marks';
19
20
  import { TurnFooter } from './TurnFooter';
20
21
  import { normalizeAssistantMarkdown } from './assistantMarkdown';
21
22
  import { parseFormSubmission } from './formSubmission';
@@ -275,7 +276,7 @@ export function SubagentChatPanel({ toolCallId, subagentName: subagentNameHint,
275
276
  // DefaultToolCallRenderer — reads approvals from the Zustand store,
276
277
  // sends decisions over the shared WebSocket (no REST polling).
277
278
  // ---------------------------------------------------------------------------
278
- function DefaultToolCallRenderer({ item, onRespond, }) {
279
+ function DefaultToolCallRenderer({ item, onRespond, mcpServers, }) {
279
280
  const resultObject = item.result && typeof item.result === 'object'
280
281
  ? item.result
281
282
  : undefined;
@@ -300,21 +301,13 @@ function DefaultToolCallRenderer({ item, onRespond, }) {
300
301
  // Capture the skill id carried in the tool-call arguments so we can match
301
302
  // the inline approval button against that synthetic approval entry.
302
303
  const skillApprovalKey = useMemo(() => {
303
- const SKILL_TOOLS = new Set([
304
- 'run_skill_script',
305
- 'load_skill',
306
- 'read_skill_resource',
307
- ]);
308
- if (!SKILL_TOOLS.has(item.toolName))
309
- return null;
310
- const a = (item.args ?? {});
311
- const raw = a.skill_name ?? a.skill ?? a.name;
312
- if (typeof raw !== 'string' || raw.length === 0)
313
- return null;
314
- // Strip optional ``<name>:<version>`` suffix to the base skill id.
315
- const base = raw.split(':', 1)[0] || raw;
316
- return `skill:${base}`.toLowerCase();
304
+ // The base skill id, without an optional ``<name>:<version>`` suffix.
305
+ const skillId = skillIdOfCall(item.toolName, item.args);
306
+ return skillId ? `skill:${skillId}`.toLowerCase() : null;
317
307
  }, [item.toolName, item.args]);
308
+ // Whose tool it is — its MCP server, skill, frontend tool set or runtime
309
+ // tool — for the mark drawn before its name.
310
+ const marks = useMemo(() => marksOfToolCall(item.toolName, item.args, mcpServers), [item.toolName, item.args, mcpServers]);
318
311
  // Read pending approvals from the Zustand store (fed by the agent-runtime WS).
319
312
  const approvals = useAgentRuntimeStore(s => s.approvals);
320
313
  // Prefer an exact match by approval id (when present in SSE tool result).
@@ -399,12 +392,12 @@ function DefaultToolCallRenderer({ item, onRespond, }) {
399
392
  ? () => makeDecision('approve')
400
393
  : undefined, onDeny: effectiveApprovalId && !decision && !externalDecision
401
394
  ? () => makeDecision('reject')
402
- : undefined, approvalLoading: false }), isSubagentDelegation && (_jsx(SubagentChatPanel, { toolCallId: item.toolCallId, subagentName: delegatedSubagentName }))] }));
395
+ : undefined, approvalLoading: false, marks: marks }), isSubagentDelegation && (_jsx(SubagentChatPanel, { toolCallId: item.toolCallId, subagentName: delegatedSubagentName }))] }));
403
396
  }
404
397
  // ---------------------------------------------------------------------------
405
398
  // Component
406
399
  // ---------------------------------------------------------------------------
407
- export function ChatMessageList({ displayItems, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, renderToolResult, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters = false, agentUsage, onRemoveItems, renderMessageFooter, }) {
400
+ export function ChatMessageList({ displayItems, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, renderToolResult, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters = false, agentUsage, onRemoveItems, renderMessageFooter, mcpServers, }) {
408
401
  if (displayItems.length === 0) {
409
402
  return _jsx(_Fragment, { children: emptyContent });
410
403
  }
@@ -506,7 +499,7 @@ export function ChatMessageList({ displayItems, isLoading, isStreaming, showLoad
506
499
  not care about hid those tools entirely. Offering the default as a
507
500
  node lets it compose instead of choosing.
508
501
  */
509
- const defaultUI = (_jsx(DefaultToolCallRenderer, { item: item, approvalConfig: approvalConfig, onRespond: createRespondCallback(item.toolCallId) }));
502
+ const defaultUI = (_jsx(DefaultToolCallRenderer, { item: item, approvalConfig: approvalConfig, onRespond: createRespondCallback(item.toolCallId), mcpServers: mcpServers }));
510
503
  const toolUI = renderToolResult
511
504
  ? renderToolResult({
512
505
  toolCallId: item.toolCallId,
@@ -13,6 +13,7 @@ import React from 'react';
13
13
  import { Text, Button } from '@primer/react';
14
14
  import { Box } from '@datalayer/primer-addons';
15
15
  import { ChevronDownIcon, AlertIcon } from '@primer/octicons-react';
16
+ import { SpecMark, hasMark, marksOfToolCall } from '../marks';
16
17
  /**
17
18
  * Check if output contains execution result with errors
18
19
  */
@@ -89,6 +90,10 @@ export function ToolPart({ part }) {
89
90
  const executionResult = extractExecutionResult(part.output);
90
91
  const statusInfo = getStatusInfo(part.state, executionResult);
91
92
  const toolName = part.type.split('-').slice(1).join('-') || part.type;
93
+ // Whose tool it is, from the catalogues: its icon, else its emoji, else nothing.
94
+ const marks = marksOfToolCall(toolName, part.input && typeof part.input === 'object'
95
+ ? part.input
96
+ : undefined);
92
97
  const effectiveExitOutput = executionResult?.output ??
93
98
  (typeof part.output === 'string'
94
99
  ? part.output
@@ -119,7 +124,7 @@ export function ToolPart({ part }) {
119
124
  '&:hover': {
120
125
  backgroundColor: 'neutral.muted',
121
126
  },
122
- }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Text, { sx: { fontSize: 1, color: 'fg.muted' }, children: "\uD83D\uDD27" }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'semibold' }, children: toolName }), _jsxs(Box, { sx: {
127
+ }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [hasMark(marks) && (_jsx(Box, { sx: { display: 'flex', color: 'fg.muted' }, children: _jsx(SpecMark, { icon: marks?.icon, emoji: marks?.emoji, size: 14 }) })), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'semibold' }, children: toolName }), _jsxs(Box, { sx: {
123
128
  display: 'inline-flex',
124
129
  alignItems: 'center',
125
130
  gap: 1,
@@ -195,7 +195,10 @@ export interface InputPromptProps {
195
195
  decisions?: Decisions;
196
196
  selectedModel?: string;
197
197
  onModelSelect?: (modelId: string) => void;
198
+ /** The runtime's tools. */
198
199
  availableTools?: BuiltinTool[];
200
+ /** The frontend tools: the ones this page runs for the agent. */
201
+ availableFrontendTools?: BuiltinTool[];
199
202
  /** MCP servers to render (already filtered by selection) */
200
203
  mcpServers?: MCPServerConfig[];
201
204
  enabledMcpTools?: Map<string, Set<string>>;
@@ -220,4 +223,4 @@ export interface InputPromptProps {
220
223
  /** Pre-fetched MCP status from WebSocket — bypasses REST polling */
221
224
  mcpStatusData?: McpToolsetsStatusResponse | null;
222
225
  }
223
- export declare function InputPrompt({ input, setInput, isLoading, kernelStatus, connectionConfirmed, placeholder, typingSuggestions, autoFocus, focusTrigger, padding, placement, floatingAnchor, draggable, container, footerExtras, leadingPanel, trailingPanel, onSend, onStop, disableInputPrompt, promptVariant, mentionableAgents, promptHistory, headerContent, showAgentsMenu, showInlineAgentsMenu, agents, selectedAgentId, onSelectAgent, showTokenUsage, showContextRing, agentUsage, showModelSelector, showToolsMenu, showSkillsMenu, showSuggestionsMenu, suggestions, codemodeEnabled, onToggleCodemode, isA2AProtocol, hasConfigData, hasSkillsData, configLoading, models, decisions, selectedModel, onModelSelect, availableTools, mcpServers, enabledMcpTools, enabledMcpToolCount, onToggleMcpTool, onToggleAllMcpServerTools, approvedMcpTools, onToggleMcpToolApproval, skills, skillsLoading, enabledSkills, onToggleSkill, onToggleAllSkills, approvedSkills, onToggleSkillApproval, apiBase, authToken, mcpStatusData, }: InputPromptProps): import("react").JSX.Element;
226
+ export declare function InputPrompt({ input, setInput, isLoading, kernelStatus, connectionConfirmed, placeholder, typingSuggestions, autoFocus, focusTrigger, padding, placement, floatingAnchor, draggable, container, footerExtras, leadingPanel, trailingPanel, onSend, onStop, disableInputPrompt, promptVariant, mentionableAgents, promptHistory, headerContent, showAgentsMenu, showInlineAgentsMenu, agents, selectedAgentId, onSelectAgent, showTokenUsage, showContextRing, agentUsage, showModelSelector, showToolsMenu, showSkillsMenu, showSuggestionsMenu, suggestions, codemodeEnabled, onToggleCodemode, isA2AProtocol, hasConfigData, hasSkillsData, configLoading, models, decisions, selectedModel, onModelSelect, availableTools, availableFrontendTools, mcpServers, enabledMcpTools, enabledMcpToolCount, onToggleMcpTool, onToggleAllMcpServerTools, approvedMcpTools, onToggleMcpToolApproval, skills, skillsLoading, enabledSkills, onToggleSkill, onToggleAllSkills, approvedSkills, onToggleSkillApproval, apiBase, authToken, mcpStatusData, }: InputPromptProps): import("react").JSX.Element;
@@ -29,7 +29,7 @@ const EMPTY_ID_SET = new Set();
29
29
  // ---------------------------------------------------------------------------
30
30
  // Component
31
31
  // ---------------------------------------------------------------------------
32
- export function InputPrompt({ input, setInput, isLoading, kernelStatus, connectionConfirmed, placeholder, typingSuggestions, autoFocus, focusTrigger, padding, placement, floatingAnchor = 'bottom', draggable = false, container, footerExtras, leadingPanel, trailingPanel, onSend, onStop, disableInputPrompt = false, promptVariant, mentionableAgents, promptHistory, headerContent, showAgentsMenu = false, showInlineAgentsMenu = true, agents = [], selectedAgentId, onSelectAgent, showTokenUsage, showContextRing = false, agentUsage, showModelSelector, showToolsMenu, showSkillsMenu, showSuggestionsMenu = true, suggestions, codemodeEnabled, onToggleCodemode, isA2AProtocol, hasConfigData, hasSkillsData, configLoading = false, models = [], decisions, selectedModel = '', onModelSelect = () => { }, availableTools = [], mcpServers = [], enabledMcpTools = EMPTY_TOOL_MAP, enabledMcpToolCount = 0, onToggleMcpTool = () => { }, onToggleAllMcpServerTools = () => { }, approvedMcpTools = EMPTY_TOOL_MAP, onToggleMcpToolApproval = () => { }, skills = [], skillsLoading = false, enabledSkills = EMPTY_ID_SET, onToggleSkill = () => { }, onToggleAllSkills = () => { }, approvedSkills = EMPTY_ID_SET, onToggleSkillApproval = () => { }, apiBase, authToken, mcpStatusData, }) {
32
+ export function InputPrompt({ input, setInput, isLoading, kernelStatus, connectionConfirmed, placeholder, typingSuggestions, autoFocus, focusTrigger, padding, placement, floatingAnchor = 'bottom', draggable = false, container, footerExtras, leadingPanel, trailingPanel, onSend, onStop, disableInputPrompt = false, promptVariant, mentionableAgents, promptHistory, headerContent, showAgentsMenu = false, showInlineAgentsMenu = true, agents = [], selectedAgentId, onSelectAgent, showTokenUsage, showContextRing = false, agentUsage, showModelSelector, showToolsMenu, showSkillsMenu, showSuggestionsMenu = true, suggestions, codemodeEnabled, onToggleCodemode, isA2AProtocol, hasConfigData, hasSkillsData, configLoading = false, models = [], decisions, selectedModel = '', onModelSelect = () => { }, availableTools = [], availableFrontendTools = [], mcpServers = [], enabledMcpTools = EMPTY_TOOL_MAP, enabledMcpToolCount = 0, onToggleMcpTool = () => { }, onToggleAllMcpServerTools = () => { }, approvedMcpTools = EMPTY_TOOL_MAP, onToggleMcpToolApproval = () => { }, skills = [], skillsLoading = false, enabledSkills = EMPTY_ID_SET, onToggleSkill = () => { }, onToggleAllSkills = () => { }, approvedSkills = EMPTY_ID_SET, onToggleSkillApproval = () => { }, apiBase, authToken, mcpStatusData, }) {
33
33
  const isKernelBusy = kernelStatus === 'busy';
34
34
  /*
35
35
  * Each menu is offered when it is switched on *and* has something behind it.
@@ -143,7 +143,7 @@ export function InputPrompt({ input, setInput, isLoading, kernelStatus, connecti
143
143
  minHeight: 36,
144
144
  bordered: true,
145
145
  subtle: true,
146
- content: (_jsx(_Fragment, { children: anyOffered || extrasOffered || suggestionsOffered ? (_jsxs(_Fragment, { children: [agentsOffered && (_jsx(AgentsMenu, { agents: agents, selectedAgentId: selectedAgentId, onSelectAgent: onSelectAgent })), toolsOffered && (_jsx(ToolsMenu, { codemodeEnabled: codemodeEnabled, onToggleCodemode: onToggleCodemode, mcpServers: mcpServers, enabledMcpTools: enabledMcpTools, enabledMcpToolCount: enabledMcpToolCount, onToggleMcpTool: onToggleMcpTool, onToggleAllMcpServerTools: onToggleAllMcpServerTools, approvedMcpTools: approvedMcpTools, onToggleMcpToolApproval: onToggleMcpToolApproval, availableTools: availableTools })), skillsOffered && (_jsx(SkillsMenu, { skills: skills, skillsLoading: skillsLoading, enabledSkills: enabledSkills, onToggleSkill: onToggleSkill, onToggleAllSkills: onToggleAllSkills, approvedSkills: approvedSkills, onToggleSkillApproval: onToggleSkillApproval })), modelsOffered && (_jsx(ModelSelector, { models: models, decisions: decisions, selectedModel: selectedModel, onModelSelect: onModelSelect, isA2AProtocol: isA2AProtocol })), suggestionsOffered && (_jsx(SuggestionsMenu, { suggestions: suggestionItems, onPick: text => {
146
+ content: (_jsx(_Fragment, { children: anyOffered || extrasOffered || suggestionsOffered ? (_jsxs(_Fragment, { children: [agentsOffered && (_jsx(AgentsMenu, { agents: agents, selectedAgentId: selectedAgentId, onSelectAgent: onSelectAgent })), toolsOffered && (_jsx(ToolsMenu, { codemodeEnabled: codemodeEnabled, onToggleCodemode: onToggleCodemode, mcpServers: mcpServers, enabledMcpTools: enabledMcpTools, enabledMcpToolCount: enabledMcpToolCount, onToggleMcpTool: onToggleMcpTool, onToggleAllMcpServerTools: onToggleAllMcpServerTools, approvedMcpTools: approvedMcpTools, onToggleMcpToolApproval: onToggleMcpToolApproval, availableTools: availableTools, availableFrontendTools: availableFrontendTools })), skillsOffered && (_jsx(SkillsMenu, { skills: skills, skillsLoading: skillsLoading, enabledSkills: enabledSkills, onToggleSkill: onToggleSkill, onToggleAllSkills: onToggleAllSkills, approvedSkills: approvedSkills, onToggleSkillApproval: onToggleSkillApproval })), modelsOffered && (_jsx(ModelSelector, { models: models, decisions: decisions, selectedModel: selectedModel, onModelSelect: onModelSelect, isA2AProtocol: isA2AProtocol })), suggestionsOffered && (_jsx(SuggestionsMenu, { suggestions: suggestionItems, onPick: text => {
147
147
  setInput(text);
148
148
  setPickedFocus(n => n + 1);
149
149
  } })), extrasOffered && (_jsx(Box, { sx: {
@@ -11,6 +11,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
11
11
  import { Text, Button, ActionMenu, ActionList, Tooltip, ToggleSwitch, } from '@primer/react';
12
12
  import { Box } from '@datalayer/primer-addons';
13
13
  import { BriefcaseIcon } from '@primer/octicons-react';
14
+ import { SpecMark, hasMark, marksOfSkill } from '../../marks';
14
15
  export function SkillsMenu({ skills, skillsLoading, enabledSkills, onToggleSkill, onToggleAllSkills, approvedSkills, onToggleSkillApproval, }) {
15
16
  // A count, not a word — see `ToolsMenu`. The names go in the tooltip, which
16
17
  // is where a person asking "which skills?" is already looking.
@@ -40,7 +41,7 @@ export function SkillsMenu({ skills, skillsLoading, enabledSkills, onToggleSkill
40
41
  '&:hover': {
41
42
  backgroundColor: 'canvas.subtle',
42
43
  },
43
- }, children: [_jsxs(Box, { sx: { flex: 1, minWidth: 0 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [_jsx(Text, { id: `toggle-skill-${skill.id}`, sx: { fontWeight: 'semibold' }, children: skill.name }), skill.status && (_jsx(Text, { sx: {
44
+ }, children: [_jsxs(Box, { sx: { flex: 1, minWidth: 0 }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [hasMark(marksOfSkill(skill.id)) && (_jsx(SpecMark, { ...marksOfSkill(skill.id), size: 16 })), _jsx(Text, { id: `toggle-skill-${skill.id}`, sx: { fontWeight: 'semibold' }, children: skill.name }), skill.status && (_jsx(Text, { sx: {
44
45
  fontSize: '10px',
45
46
  px: 1,
46
47
  borderRadius: 2,