@datalayer/agent-runtimes 1.3.30 → 1.3.33

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 (79) hide show
  1. package/lib/chat/ChatFloating.js +40 -8
  2. package/lib/chat/assistant/AssistantStage.d.ts +26 -7
  3. package/lib/chat/assistant/AssistantStage.js +115 -8
  4. package/lib/chat/assistant/SpeechBalloon.js +7 -3
  5. package/lib/chat/assistant/characters.d.ts +32 -1
  6. package/lib/chat/assistant/characters.js +180 -15
  7. package/lib/chat/assistant/state.d.ts +28 -0
  8. package/lib/chat/assistant/state.js +31 -0
  9. package/lib/chat/base/ChatBase.js +34 -19
  10. package/lib/chat/base/modelChoice.d.ts +27 -1
  11. package/lib/chat/base/modelChoice.js +29 -15
  12. package/lib/chat/prompt/InputPrompt.d.ts +7 -1
  13. package/lib/chat/prompt/InputPrompt.js +2 -2
  14. package/lib/chat/prompt/InputPromptBase.js +1 -1
  15. package/lib/chat/prompt/menus/ModelSelector.d.ts +13 -1
  16. package/lib/chat/prompt/menus/ModelSelector.js +17 -5
  17. package/lib/chat/useViewportDrag.d.ts +9 -1
  18. package/lib/chat/useViewportDrag.js +4 -4
  19. package/lib/components/a2ui/index.d.ts +2 -0
  20. package/lib/components/a2ui/index.js +2 -0
  21. package/lib/components/a2ui/visibility.d.ts +15 -0
  22. package/lib/components/a2ui/visibility.js +24 -0
  23. package/lib/components/a2ui/visibleWhen.d.ts +17 -0
  24. package/lib/components/a2ui/visibleWhen.js +54 -0
  25. package/lib/examples/ChatAssistantExample.d.ts +4 -0
  26. package/lib/examples/ChatAssistantExample.js +26 -5
  27. package/lib/examples/utils/owlCharacterPlugin.d.ts +7 -0
  28. package/lib/examples/utils/owlCharacterPlugin.js +44 -0
  29. package/lib/loop/apps/AppFeedback.d.ts +27 -0
  30. package/lib/loop/apps/AppFeedback.js +78 -0
  31. package/lib/loop/apps/AppRenderer.js +17 -2
  32. package/lib/loop/apps/appspec.js +6 -3
  33. package/lib/loop/apps/checks.d.ts +10 -2
  34. package/lib/loop/apps/checks.js +21 -10
  35. package/lib/loop/apps/feedback.d.ts +51 -0
  36. package/lib/loop/apps/feedback.js +61 -0
  37. package/lib/loop/apps/index.d.ts +2 -0
  38. package/lib/loop/apps/index.js +2 -0
  39. package/lib/loop/apps/rules.d.ts +14 -0
  40. package/lib/loop/apps/rules.js +24 -0
  41. package/lib/loop/core/canvasBlocks.d.ts +16 -0
  42. package/lib/loop/core/canvasBlocks.js +13 -0
  43. package/lib/loop/core/index.d.ts +7 -0
  44. package/lib/loop/core/index.js +3 -0
  45. package/lib/loop/index.d.ts +1 -1
  46. package/lib/loop/plugins/a2ui/A2uiView.js +2 -3
  47. package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +4 -2
  48. package/lib/loop/plugins/a2ui-surface/InlineSurface.js +3 -4
  49. package/lib/loop/plugins/app-page/AppPage.js +1 -1
  50. package/lib/loop/plugins/app-page/{appPage.d.ts → appPageModel.d.ts} +1 -1
  51. package/lib/loop/plugins/app-page/index.d.ts +1 -1
  52. package/lib/loop/plugins/app-page/index.js +2 -2
  53. package/lib/loop/plugins/canvas-blocks/index.d.ts +63 -0
  54. package/lib/loop/plugins/canvas-blocks/index.js +118 -0
  55. package/lib/loop/plugins/chat/ChatView.js +7 -3
  56. package/lib/loop/plugins/chat/turnState.d.ts +5 -2
  57. package/lib/loop/plugins/chat/turnState.js +2 -1
  58. package/lib/specs/actions.js +1 -0
  59. package/lib/specs/agents/agents.js +16 -4
  60. package/lib/specs/apps.js +21 -10
  61. package/lib/specs/appspecSchema.js +13 -7
  62. package/lib/specs/models.d.ts +1 -1
  63. package/lib/specs/models.js +8 -8
  64. package/lib/specs/tools.d.ts +1 -0
  65. package/lib/specs/tools.js +19 -0
  66. package/lib/stories/loop/LoopReference.stories.d.ts +21 -0
  67. package/lib/stories/loop/LoopReference.stories.js +31 -0
  68. package/lib/stories/loop/ReferenceScreens.d.ts +53 -0
  69. package/lib/stories/loop/ReferenceScreens.js +256 -0
  70. package/lib/stories/loop/pictures-main.d.ts +2 -0
  71. package/lib/stories/loop/pictures-main.js +75 -0
  72. package/lib/types/agentspecs.d.ts +6 -1
  73. package/lib/types/chat.d.ts +5 -0
  74. package/lib/types/config.d.ts +8 -0
  75. package/package.json +9 -6
  76. package/scripts/check-cloudflare-models.py +2 -2
  77. package/scripts/codegen/generate_apps.py +2 -1
  78. package/scripts/codegen/generate_models.py +4 -4
  79. /package/lib/loop/plugins/app-page/{appPage.js → appPageModel.js} +0 -0
@@ -60,3 +60,31 @@ export declare const ASSISTANT_AWAY_KEY = "datalayer-assistant-away";
60
60
  export declare function keptAway(): AssistantAway;
61
61
  /** Keep the choice where it lasts as long as it says; `none` forgets it. */
62
62
  export declare function keepAway(away: AssistantAway): void;
63
+ /** A box on the screen, as `getBoundingClientRect` gives it. */
64
+ export interface ScreenBox {
65
+ left: number;
66
+ top: number;
67
+ right: number;
68
+ bottom: number;
69
+ }
70
+ /**
71
+ * What the assistant never sits over (T-27): an open dialog or overlay —
72
+ * Primer's `Dialog`, an `ActionMenu` or `SelectPanel` overlay, a native
73
+ * `<dialog>` — and the composer of a chat.
74
+ */
75
+ export declare const ASSISTANT_OBSTACLES: string;
76
+ /** How near the pointer works, in pixels, before the assistant steps aside. */
77
+ export declare const POINTER_ROOM = 24;
78
+ /** How long the pointer stays away before the assistant comes back, in ms. */
79
+ export declare const POINTER_CALM_MS = 1500;
80
+ /** Whether two boxes share any area, the first grown by `margin`. */
81
+ export declare function boxesMeet(a: ScreenBox, b: ScreenBox, margin?: number): boolean;
82
+ /**
83
+ * Whether the pointer at `point` works where the assistant stands: over it,
84
+ * or within `POINTER_ROOM` of it — a press beside it is aimed at what it
85
+ * hides or crowds.
86
+ */
87
+ export declare function pointerNear(box: ScreenBox, point: {
88
+ x: number;
89
+ y: number;
90
+ }, room?: number): boolean;
@@ -114,3 +114,34 @@ export function keepAway(away) {
114
114
  // Without storage the choice holds for the page.
115
115
  }
116
116
  }
117
+ /**
118
+ * What the assistant never sits over (T-27): an open dialog or overlay —
119
+ * Primer's `Dialog`, an `ActionMenu` or `SelectPanel` overlay, a native
120
+ * `<dialog>` — and the composer of a chat.
121
+ */
122
+ export const ASSISTANT_OBSTACLES = [
123
+ '[role="dialog"]',
124
+ '[role="alertdialog"]',
125
+ 'dialog[open]',
126
+ '[class*="prc-Overlay-Overlay"]',
127
+ '[data-chat-composer]',
128
+ ].join(', ');
129
+ /** How near the pointer works, in pixels, before the assistant steps aside. */
130
+ export const POINTER_ROOM = 24;
131
+ /** How long the pointer stays away before the assistant comes back, in ms. */
132
+ export const POINTER_CALM_MS = 1500;
133
+ /** Whether two boxes share any area, the first grown by `margin`. */
134
+ export function boxesMeet(a, b, margin = 0) {
135
+ return (a.left - margin < b.right &&
136
+ b.left < a.right + margin &&
137
+ a.top - margin < b.bottom &&
138
+ b.top < a.bottom + margin);
139
+ }
140
+ /**
141
+ * Whether the pointer at `point` works where the assistant stands: over it,
142
+ * or within `POINTER_ROOM` of it — a press beside it is aimed at what it
143
+ * hides or crowds.
144
+ */
145
+ export function pointerNear(box, point, room = POINTER_ROOM) {
146
+ return boxesMeet(box, { left: point.x, top: point.y, right: point.x, bottom: point.y }, room);
147
+ }
@@ -1400,7 +1400,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
1400
1400
  * four are current.
1401
1401
  */
1402
1402
  const catalogued = Object.values(AI_MODEL_CATALOGUE)
1403
- // A typed-judgment model answers typed questions, not a chat.
1403
+ // A typed-decision model answers typed questions, not a chat.
1404
1404
  .filter(model => model.available && isChatModel(model))
1405
1405
  .map(model => ({
1406
1406
  id: model.id,
@@ -2124,9 +2124,13 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2124
2124
  const store = useConversationStore.getState();
2125
2125
  const currentlyFetching = store.isFetching(historyScopeId);
2126
2126
  const storedMessages = store.getMessages(historyScopeId);
2127
- // 1) Fast local hydration for view switches in the same browser session.
2127
+ // 1) Fast local hydration for view switches in the same browser session:
2128
+ // only into an empty conversation. The saved copy holds messages, not
2129
+ // tool calls, and this effect runs again on every change of the socket's
2130
+ // state — put over a live conversation, it erased its tool cards, a
2131
+ // pending approval among them, at the socket's first retry.
2128
2132
  if (storedMessages.length > 0) {
2129
- setDisplayItems(storedMessages);
2133
+ setDisplayItems(prev => (prev.length > 0 ? prev : storedMessages));
2130
2134
  setHistoryLoaded(true);
2131
2135
  }
2132
2136
  if (currentlyFetching) {
@@ -2384,16 +2388,24 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2384
2388
  const incomingId = event.message.id;
2385
2389
  const currentId = currentAssistantMessageRef.current?.id;
2386
2390
  const isNewMessage = !currentId || (incomingId && incomingId !== currentId);
2387
- if (currentAssistantMessageRef.current && !isNewMessage) {
2391
+ if (currentId && !isNewMessage) {
2392
+ /*
2393
+ * The message to write to is the one current now, as the event
2394
+ * arrives — not whichever the ref names when React gets round
2395
+ * to running the updater. A whole answer in one network chunk
2396
+ * queues every delta's updater in the same task, and the turn's
2397
+ * end clears the ref before any of them runs: read lazily, all
2398
+ * but the first found nothing to update, and the conversation
2399
+ * and the balloon showed only the answer's first word.
2400
+ */
2401
+ const rawContent = event.message.content;
2402
+ const sanitizedContent = typeof rawContent === 'string'
2403
+ ? sanitizeAssistantContent(rawContent)
2404
+ : (rawContent ?? '');
2388
2405
  setDisplayItems(prev => {
2389
2406
  const newItems = [...prev];
2390
- const idx = newItems.findIndex(item => !isToolCallMessage(item) &&
2391
- item.id === currentAssistantMessageRef.current?.id);
2407
+ const idx = newItems.findIndex(item => !isToolCallMessage(item) && item.id === currentId);
2392
2408
  if (idx >= 0 && !isToolCallMessage(newItems[idx])) {
2393
- const rawContent = event.message?.content;
2394
- const sanitizedContent = typeof rawContent === 'string'
2395
- ? sanitizeAssistantContent(rawContent)
2396
- : (rawContent ?? '');
2397
2409
  newItems[idx] = {
2398
2410
  ...newItems[idx],
2399
2411
  content: sanitizedContent,
@@ -2401,14 +2413,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2401
2413
  }
2402
2414
  return newItems;
2403
2415
  });
2404
- if (useStoreMode && currentAssistantMessageRef.current) {
2405
- const rawContent = event.message?.content;
2406
- const sanitizedContent = typeof rawContent === 'string'
2407
- ? sanitizeAssistantContent(rawContent)
2408
- : (rawContent ?? '');
2409
- useChatStore
2410
- .getState()
2411
- .updateMessage(currentAssistantMessageRef.current.id, {
2416
+ if (useStoreMode) {
2417
+ useChatStore.getState().updateMessage(currentId, {
2412
2418
  content: sanitizedContent,
2413
2419
  });
2414
2420
  }
@@ -3096,6 +3102,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3096
3102
  // The same reset the header's + performs, for a host whose controls
3097
3103
  // live outside this component — the LOOP prompt's + reaches it here.
3098
3104
  newChat: handleNewChat,
3105
+ thread: () => threadIdRef.current,
3099
3106
  });
3100
3107
  return () => {
3101
3108
  onSendReady(null);
@@ -3490,7 +3497,15 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3490
3497
  promptHistory: configQuery.data?.promptHistory, showTokenUsage: showTokenUsage, showContextRing: showContextRing, agentUsage: agentUsage, showModelSelector: showModelSelector, showToolsMenu: showToolsMenu, showSkillsMenu: showSkillsMenu, codemodeEnabled: codemodeEnabled, onToggleCodemode: onToggleCodemode, isA2AProtocol: isA2AProtocol, showAgentsMenu: showAgentsMenu, agents: footerAgents, selectedAgentId: selectedAgentId ?? footerAgents[0]?.id, onSelectAgent: onSelectAgent, hasConfigData: !!configQuery.data, hasSkillsData: !!skillsQuery.data,
3491
3498
  // So the bar can tell "not here yet" from "not coming": a chat whose
3492
3499
  // agent has no config endpoint waits for ever otherwise.
3493
- configLoading: configQuery.isLoading, models: offeredModels, 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;
3500
+ configLoading: configQuery.isLoading, models: offeredModels,
3501
+ // Listed apart and read-only: a typed-decision model is never the
3502
+ // agent's model.
3503
+ decisions: configQuery.data
3504
+ ? {
3505
+ models: configQuery.data.decisionModels,
3506
+ note: configQuery.data.decisionsNote,
3507
+ }
3508
+ : 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;
3494
3509
  // Shared header element. It is rendered either at the top of the chat (when
3495
3510
  // no ephemeral notebook is shown) or INSIDE the chat body column (when the
3496
3511
  // notebook is visible) so the header always follows the chat body across all
@@ -57,9 +57,28 @@ export type ServerCatalogueModel = {
57
57
  name?: string;
58
58
  available?: boolean;
59
59
  missing_env_vars?: string[];
60
- reason?: string;
60
+ reason?: string | null;
61
61
  warning?: string | null;
62
62
  };
63
+ /**
64
+ * The typed-decision models a runtime lists apart (Jev).
65
+ *
66
+ * They answer a decision's typed questions, not a conversation: the menu
67
+ * shows them read-only, under their own heading, with `note` saying so, and
68
+ * never offers one as the agent's model.
69
+ */
70
+ export type Decisions = {
71
+ models: ModelConfig[];
72
+ note: string;
73
+ };
74
+ /** The runtime's `/configure/models` answer, as far as the chat reads it. */
75
+ export type ServerCatalogue = {
76
+ models?: ServerCatalogueModel[];
77
+ source?: string;
78
+ note?: string;
79
+ decision_models: ServerCatalogueModel[];
80
+ decisions_note: string;
81
+ };
63
82
  /**
64
83
  * The server's catalogue in the chat's shape.
65
84
  *
@@ -73,3 +92,10 @@ export declare function readServerCatalogue(payload: {
73
92
  source?: string;
74
93
  note?: string;
75
94
  } | null | undefined): ModelConfig[];
95
+ /**
96
+ * The typed-decision models of the runtime's answer, in the chat's shape.
97
+ *
98
+ * Read apart from `readServerCatalogue`, so no list a chat picks its model
99
+ * from ever holds one.
100
+ */
101
+ export declare function readServerDecisions(payload: ServerCatalogue): Decisions;
@@ -51,19 +51,33 @@ export function initialModelId(offered, preferred) {
51
51
  * its spec was known went to the first row — Alibaba, and its missing key.
52
52
  */
53
53
  export function readServerCatalogue(payload) {
54
- return (payload?.models ?? []).map(model => {
55
- const missing = model.missing_env_vars ?? [];
56
- const reason = model.reason ??
57
- (missing.length > 0 ? `Set ${missing.join(', ')}` : undefined) ??
58
- model.warning ??
59
- undefined;
60
- return {
61
- id: model.id,
62
- name: model.name ?? model.id,
63
- isAvailable: model.available !== false,
64
- ...(reason && model.available === false
65
- ? { unavailableReason: reason }
66
- : {}),
67
- };
68
- });
54
+ return (payload?.models ?? []).map(readServerModel);
55
+ }
56
+ /**
57
+ * The typed-decision models of the runtime's answer, in the chat's shape.
58
+ *
59
+ * Read apart from `readServerCatalogue`, so no list a chat picks its model
60
+ * from ever holds one.
61
+ */
62
+ export function readServerDecisions(payload) {
63
+ return {
64
+ models: payload.decision_models.map(readServerModel),
65
+ note: payload.decisions_note,
66
+ };
67
+ }
68
+ /** One row of the runtime's answer, in the chat's shape. */
69
+ function readServerModel(model) {
70
+ const missing = model.missing_env_vars ?? [];
71
+ const reason = model.reason ??
72
+ (missing.length > 0 ? `Set ${missing.join(', ')}` : undefined) ??
73
+ model.warning ??
74
+ undefined;
75
+ return {
76
+ id: model.id,
77
+ name: model.name ?? model.id,
78
+ isAvailable: model.available !== false,
79
+ ...(reason && model.available === false
80
+ ? { unavailableReason: reason }
81
+ : {}),
82
+ };
69
83
  }
@@ -16,6 +16,7 @@ import type { PromptSuggestion } from './menus/SuggestionsMenu';
16
16
  import type { MentionableAgent } from './plugins/AgentMentionPlugin';
17
17
  /** One agent the footer may offer. */
18
18
  import type { FooterAgent } from '../../types/chat';
19
+ import type { Decisions } from '../base/modelChoice';
19
20
  export type { FooterAgent };
20
21
  /** Where the prompt stands: in the flow, or in a draggable card over it. */
21
22
  export type InputPromptPlacement = 'docked' | 'floating';
@@ -187,6 +188,11 @@ export interface InputPromptProps {
187
188
  */
188
189
  configLoading?: boolean;
189
190
  models?: ModelConfig[];
191
+ /**
192
+ * The typed-decision models the runtime lists apart (Jev): shown read-only
193
+ * under the models, never one to pick. See `modelChoice`'s `Decisions`.
194
+ */
195
+ decisions?: Decisions;
190
196
  selectedModel?: string;
191
197
  onModelSelect?: (modelId: string) => void;
192
198
  availableTools?: BuiltinTool[];
@@ -214,4 +220,4 @@ export interface InputPromptProps {
214
220
  /** Pre-fetched MCP status from WebSocket — bypasses REST polling */
215
221
  mcpStatusData?: McpToolsetsStatusResponse | null;
216
222
  }
217
- 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, 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;
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;
@@ -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 = [], 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 = [], 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, 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 })), 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: {
@@ -192,7 +192,7 @@ export function InputPromptBase({ variant = 'lexical', placeholder = 'Ask anythi
192
192
  , {
193
193
  // Named so the turn-end refocus can ask whether the caret is already
194
194
  // somewhere inside this prompt before it reaches for it.
195
- ref: containerRef, onFocusCapture: () => {
195
+ ref: containerRef, "data-chat-composer": "", onFocusCapture: () => {
196
196
  if (selfFocused.current) {
197
197
  selfFocused.current = false;
198
198
  return;
@@ -1,6 +1,18 @@
1
1
  import type { ModelConfig } from '../../../types';
2
- export declare function ModelSelector({ models, selectedModel, onModelSelect, isA2AProtocol, }: {
2
+ import type { Decisions } from '../../base/modelChoice';
3
+ /**
4
+ * The typed-decision models, read-only under the models.
5
+ *
6
+ * A decision asks them through `/decisions`; an agent cannot run on one, so
7
+ * no row here selects anything. Each says so, in the runtime's sentence, and
8
+ * why it cannot be used when it cannot — the same rule as the models above.
9
+ */
10
+ export declare function DecisionsGroup({ decisions }: {
11
+ decisions: Decisions;
12
+ }): import("react").JSX.Element;
13
+ export declare function ModelSelector({ models, decisions, selectedModel, onModelSelect, isA2AProtocol, }: {
3
14
  models: ModelConfig[];
15
+ decisions?: Decisions;
4
16
  selectedModel: string;
5
17
  onModelSelect: (modelId: string) => void;
6
18
  isA2AProtocol: boolean;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /*
3
3
  * Copyright (c) 2025-2026 Datalayer, Inc.
4
4
  * Distributed under the terms of the Modified BSD License.
@@ -11,7 +11,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  import { Text, Button, ActionMenu, ActionList, Tooltip } from '@primer/react';
12
12
  import { Box } from '@datalayer/primer-addons';
13
13
  import { AiModelIcon } from '@primer/octicons-react';
14
- export function ModelSelector({ models, selectedModel, onModelSelect, isA2AProtocol, }) {
14
+ /**
15
+ * The typed-decision models, read-only under the models.
16
+ *
17
+ * A decision asks them through `/decisions`; an agent cannot run on one, so
18
+ * no row here selects anything. Each says so, in the runtime's sentence, and
19
+ * why it cannot be used when it cannot — the same rule as the models above.
20
+ */
21
+ export function DecisionsGroup({ decisions }) {
22
+ return (_jsxs(ActionList.Group, { selectionVariant: false, children: [_jsx(ActionList.GroupHeading, { children: "Decisions" }), decisions.models.map(model => (_jsxs(ActionList.Item, { disabled: true, sx: { color: 'fg.muted' }, children: [model.name, _jsx(ActionList.Description, { variant: "block", children: model.isAvailable === false
23
+ ? `${model.unavailableReason ?? 'Not usable here'} · ${decisions.note}`
24
+ : decisions.note })] }, model.id)))] }));
25
+ }
26
+ export function ModelSelector({ models, decisions, selectedModel, onModelSelect, isA2AProtocol, }) {
15
27
  const active = models.find(model => model.id === selectedModel);
16
28
  /*
17
29
  * A count, not a name — the same trigger as tools, skills, agents and
@@ -28,7 +40,7 @@ export function ModelSelector({ models, selectedModel, onModelSelect, isA2AProto
28
40
  alignItems: 'flex-end',
29
41
  }, children: [_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Anchor, { children: _jsx(Tooltip, { text: `Model — ${summary}`, direction: "n", children: _jsx(Button, { type: "button", variant: "invisible", size: "small", "aria-label": `Model — ${summary}`, leadingVisual: AiModelIcon, disabled: isA2AProtocol, sx: isA2AProtocol
30
42
  ? { opacity: 0.5, cursor: 'not-allowed' }
31
- : undefined, children: _jsx(Text, { sx: { fontSize: 0 }, children: models.length }) }) }) }), _jsx(ActionMenu.Overlay, { side: "outside-top", align: "end", children: _jsx(ActionList, { selectionVariant: "single", children: models.map(modelItem => (_jsxs(ActionList.Item, { selected: selectedModel === modelItem.id, onSelect: () => onModelSelect(modelItem.id), disabled: modelItem.isAvailable === false || isA2AProtocol, sx: modelItem.isAvailable === false
32
- ? { color: 'fg.muted' }
33
- : undefined, children: [modelItem.name, modelItem.isAvailable === false && (_jsx(ActionList.Description, { variant: "block", children: modelItem.unavailableReason ?? 'Missing API key' }))] }, modelItem.id))) }) })] }), isA2AProtocol && (_jsx(Text, { sx: { fontSize: 0, color: 'attention.fg', mt: 1 }, children: "A2A: Model set by agent config" }))] }));
43
+ : undefined, children: _jsx(Text, { sx: { fontSize: 0 }, children: models.length }) }) }) }), _jsx(ActionMenu.Overlay, { side: "outside-top", align: "end", children: _jsxs(ActionList, { selectionVariant: "single", children: [models.map(modelItem => (_jsxs(ActionList.Item, { selected: selectedModel === modelItem.id, onSelect: () => onModelSelect(modelItem.id), disabled: modelItem.isAvailable === false || isA2AProtocol, sx: modelItem.isAvailable === false
44
+ ? { color: 'fg.muted' }
45
+ : undefined, children: [modelItem.name, modelItem.isAvailable === false && (_jsx(ActionList.Description, { variant: "block", children: modelItem.unavailableReason ?? 'Missing API key' }))] }, modelItem.id))), decisions && decisions.models.length > 0 && (_jsxs(_Fragment, { children: [_jsx(ActionList.Divider, {}), _jsx(DecisionsGroup, { decisions: decisions })] }))] }) })] }), isA2AProtocol && (_jsx(Text, { sx: { fontSize: 0, color: 'attention.fg', mt: 1 }, children: "A2A: Model set by agent config" }))] }));
34
46
  }
@@ -3,7 +3,15 @@ export type DragPosition = {
3
3
  left: number;
4
4
  top: number;
5
5
  } | null;
6
- export declare function useViewportDrag(boxRef: React.RefObject<HTMLElement | null>): {
6
+ export interface ViewportDragOptions {
7
+ /**
8
+ * Keep the whole box inside the viewport, not only a margin of it: the
9
+ * floating assistant's character, whose balloon and conversation open
10
+ * from where it stands (LOOP T-23).
11
+ */
12
+ whole?: boolean;
13
+ }
14
+ export declare function useViewportDrag(boxRef: React.RefObject<HTMLElement | null>, { whole }?: ViewportDragOptions): {
7
15
  position: DragPosition;
8
16
  onHandlePointerDown: (event: React.PointerEvent<HTMLElement>) => void;
9
17
  reset: () => void;
@@ -17,7 +17,7 @@
17
17
  import { useCallback, useRef, useState } from 'react';
18
18
  /** How much of the box must stay inside the viewport. */
19
19
  const MARGIN = 24;
20
- export function useViewportDrag(boxRef) {
20
+ export function useViewportDrag(boxRef, { whole = false } = {}) {
21
21
  const [position, setPosition] = useState(null);
22
22
  /* Where the pointer took hold, so the box moves by the pointer's delta
23
23
  rather than snapping its corner to the cursor. */
@@ -39,8 +39,8 @@ export function useViewportDrag(boxRef) {
39
39
  if (!grip.current) {
40
40
  return;
41
41
  }
42
- const left = Math.min(Math.max(moved.clientX - grip.current.dx, MARGIN - rect.width), window.innerWidth - MARGIN);
43
- const top = Math.min(Math.max(moved.clientY - grip.current.dy, 0), window.innerHeight - MARGIN);
42
+ const left = Math.min(Math.max(moved.clientX - grip.current.dx, whole ? 0 : MARGIN - rect.width), window.innerWidth - (whole ? rect.width : MARGIN));
43
+ const top = Math.min(Math.max(moved.clientY - grip.current.dy, 0), window.innerHeight - (whole ? rect.height : MARGIN));
44
44
  setPosition({ left, top });
45
45
  };
46
46
  const up = () => {
@@ -53,7 +53,7 @@ export function useViewportDrag(boxRef) {
53
53
  handle.addEventListener('pointerup', up);
54
54
  handle.addEventListener('pointercancel', up);
55
55
  event.preventDefault();
56
- }, [boxRef]);
56
+ }, [boxRef, whole]);
57
57
  /** Back to wherever the box starts: called when the mode changes. */
58
58
  const reset = useCallback(() => setPosition(null), []);
59
59
  return { position, onHandlePointerDown, reset };
@@ -1,3 +1,5 @@
1
1
  export * from './A2uiMarkdownProvider';
2
2
  export * from './A2uiSurfaceComposed';
3
3
  export * from './styles';
4
+ export * from './visibleWhen';
5
+ export * from './visibility';
@@ -5,3 +5,5 @@
5
5
  export * from './A2uiMarkdownProvider';
6
6
  export * from './A2uiSurfaceComposed';
7
7
  export * from './styles';
8
+ export * from './visibleWhen';
9
+ export * from './visibility';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * What `visible_when` is, without the renderer: the key and the rule, for
3
+ * whatever writes or checks a surface (the Studio's Canvas) as well as for
4
+ * what draws it (`visibleWhen`).
5
+ *
6
+ * @module components/a2ui/visibility
7
+ */
8
+ /** The key any block may carry to be shown only under a condition. */
9
+ export declare const VISIBLE_WHEN = "visible_when";
10
+ /**
11
+ * Whether a block whose condition resolved to `value` is shown: `true`, or a
12
+ * value that is not empty. `false`, `''`, `0`, `null`, an empty list, or
13
+ * nothing published at the path yet hide it.
14
+ */
15
+ export declare function isShown(value: unknown): boolean;
@@ -0,0 +1,24 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * What `visible_when` is, without the renderer: the key and the rule, for
7
+ * whatever writes or checks a surface (the Studio's Canvas) as well as for
8
+ * what draws it (`visibleWhen`).
9
+ *
10
+ * @module components/a2ui/visibility
11
+ */
12
+ /** The key any block may carry to be shown only under a condition. */
13
+ export const VISIBLE_WHEN = 'visible_when';
14
+ /**
15
+ * Whether a block whose condition resolved to `value` is shown: `true`, or a
16
+ * value that is not empty. `false`, `''`, `0`, `null`, an empty list, or
17
+ * nothing published at the path yet hide it.
18
+ */
19
+ export function isShown(value) {
20
+ if (Array.isArray(value)) {
21
+ return value.length > 0;
22
+ }
23
+ return Boolean(value);
24
+ }
@@ -0,0 +1,17 @@
1
+ import { type ReactComponentImplementation } from '@a2ui/react/v0_9';
2
+ import { Catalog } from '@a2ui/web_core/v0_9';
3
+ /**
4
+ * Whether a node's resolved properties let it be drawn: no condition, or one
5
+ * that holds. A2UI resolves a dynamic property to a binding whether the block
6
+ * gave it or not; a path's is writable, and its value may be nothing yet,
7
+ * which hides the block. A literal or a function call that resolved to
8
+ * nothing is a block without a condition.
9
+ */
10
+ export declare function nodeIsShown(props: Record<string, unknown>): boolean;
11
+ /** A component, its schema declaring `visible_when`, drawn behind the guard. */
12
+ export declare function withVisibleWhen(component: ReactComponentImplementation): ReactComponentImplementation;
13
+ /**
14
+ * The catalog Datalayer's renderer draws from: A2UI's basic catalog, under
15
+ * its own id — the payloads name it — with `visible_when` on every component.
16
+ */
17
+ export declare const datalayerCatalog: Catalog<ReactComponentImplementation, import("@a2ui/web_core/v0_9").FunctionImplementation>;
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { basicCatalog, useSignalValue, } from '@a2ui/react/v0_9';
3
+ import { Catalog, DynamicBooleanSchema, ResolvedBinding, isWritable, } from '@a2ui/web_core/v0_9';
4
+ import { VISIBLE_WHEN, isShown } from './visibility';
5
+ /**
6
+ * Whether a node's resolved properties let it be drawn: no condition, or one
7
+ * that holds. A2UI resolves a dynamic property to a binding whether the block
8
+ * gave it or not; a path's is writable, and its value may be nothing yet,
9
+ * which hides the block. A literal or a function call that resolved to
10
+ * nothing is a block without a condition.
11
+ */
12
+ export function nodeIsShown(props) {
13
+ const condition = props[VISIBLE_WHEN];
14
+ if (!(condition instanceof ResolvedBinding)) {
15
+ throw new Error(`${VISIBLE_WHEN} resolved to ${String(condition)}, not a binding: the catalog's schema does not declare it.`);
16
+ }
17
+ if (isWritable(condition)) {
18
+ return isShown(condition.value);
19
+ }
20
+ return condition.value === undefined || isShown(condition.value);
21
+ }
22
+ const VISIBLE_WHEN_SCHEMA = DynamicBooleanSchema.describe('REF:#/$defs/DynamicBoolean|Datalayer: the block is shown only while this is true or not empty — a path the application publishes.').optional();
23
+ /** A component, its schema declaring `visible_when`, drawn behind the guard. */
24
+ export function withVisibleWhen(component) {
25
+ const View = component.view;
26
+ if (!View) {
27
+ throw new Error(`${component.name} has no node view: visible_when is read from the node, and A2uiSurface draws through it.`);
28
+ }
29
+ // A zod object: what every basic component's schema is.
30
+ const schema = component.schema;
31
+ if (!schema.shape || typeof schema.extend !== 'function') {
32
+ throw new Error(`${component.name}'s schema is not an object: visible_when cannot be declared on it.`);
33
+ }
34
+ if (VISIBLE_WHEN in schema.shape) {
35
+ throw new Error(`${component.name} already declares ${VISIBLE_WHEN}.`);
36
+ }
37
+ const Guarded = ({ node, buildChild }) => {
38
+ const props = useSignalValue(node.props);
39
+ return nodeIsShown(props) ? (_jsx(View, { node: node, buildChild: buildChild })) : null;
40
+ };
41
+ Guarded.displayName = `${component.name}.visibleWhen`;
42
+ return {
43
+ ...component,
44
+ schema: schema.extend({ [VISIBLE_WHEN]: VISIBLE_WHEN_SCHEMA }),
45
+ // A2uiSurface draws through the view; `render` is only its fallback for
46
+ // a component without one, which this one is not.
47
+ view: Guarded,
48
+ };
49
+ }
50
+ /**
51
+ * The catalog Datalayer's renderer draws from: A2UI's basic catalog, under
52
+ * its own id — the payloads name it — with `visible_when` on every component.
53
+ */
54
+ export const datalayerCatalog = new Catalog(basicCatalog.id, basicCatalog.protocolVersion, [...basicCatalog.components.values()].map(withVisibleWhen), [...basicCatalog.functions.values()], basicCatalog.themeSchema, basicCatalog.instructions);
@@ -2,6 +2,10 @@
2
2
  * The floating assistant (LOOP T-21 to T-27): the chat as a character on the
3
3
  * page, after the Office Assistant — Datalayer's own characters, chosen
4
4
  * here, acting out what the agent does and speaking in a balloon.
5
+ *
6
+ * The characters offered are what the enabled plugins contribute to
7
+ * `loop.assistant.character` (T-24): Datalayer's four, and an owl from a
8
+ * small example plugin (`utils/owlCharacterPlugin`).
5
9
  */
6
10
  import React from 'react';
7
11
  declare const ChatAssistantExample: React.FC;