@datalayer/agent-runtimes 1.5.43 → 1.5.45

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 (91) hide show
  1. package/dist-embed/chunks/1f30a-DxwAt2Ba.js +1 -0
  2. package/dist-embed/chunks/1f310-AqUM9a8q.js +1 -0
  3. package/dist-embed/chunks/1f324-8MHLOhTj.js +1 -0
  4. package/dist-embed/chunks/1f326-YSBr3aXI.js +1 -0
  5. package/dist-embed/chunks/1f327-Ie-nQBNO.js +1 -0
  6. package/dist-embed/chunks/1f33e-Cfl2WM63.js +1 -0
  7. package/dist-embed/chunks/1f372-3XLDlXkE.js +1 -0
  8. package/dist-embed/chunks/1f373-BPUJt2bp.js +1 -0
  9. package/dist-embed/chunks/1f39b-CSNtbAXr.js +1 -0
  10. package/dist-embed/chunks/1f3ab-BUCNQHz1.js +1 -0
  11. package/dist-embed/chunks/1f41b-Dre1z0gP.js +1 -0
  12. package/dist-embed/chunks/1f441-D8yah4C4.js +1 -0
  13. package/dist-embed/chunks/1f465-CPle6Hjq.js +1 -0
  14. package/dist-embed/chunks/1f4b9-aacFyckt.js +1 -0
  15. package/dist-embed/chunks/1f4be-DPAcAU4R.js +1 -0
  16. package/dist-embed/chunks/1f4c1-Ch6aJ0cj.js +1 -0
  17. package/dist-embed/chunks/1f4c2-Dx7BKmLO.js +1 -0
  18. package/dist-embed/chunks/1f4c4-CNvjvC5k.js +1 -0
  19. package/dist-embed/chunks/1f4d3-GTxRgjv7.js +1 -0
  20. package/dist-embed/chunks/1f4d6-COOZY-Q_.js +1 -0
  21. package/dist-embed/chunks/1f4dc-BhoW4cjD.js +1 -0
  22. package/dist-embed/chunks/1f4e1-DezU8ow-.js +1 -0
  23. package/dist-embed/chunks/1f4e2-DsStZsI-.js +1 -0
  24. package/dist-embed/chunks/1f4e7-BBi7c_zx.js +1 -0
  25. package/dist-embed/chunks/1f500-D9xRo-IT.js +1 -0
  26. package/dist-embed/chunks/1f501-CzKeDPcw.js +1 -0
  27. package/dist-embed/chunks/1f504-BYIHMoDP.js +1 -0
  28. package/dist-embed/chunks/1f52d-2QxCLKhU.js +1 -0
  29. package/dist-embed/chunks/1f552-4d0Y0kn7.js +1 -0
  30. package/dist-embed/chunks/1f58b-Cp8F_HBg.js +1 -0
  31. package/dist-embed/chunks/1f5b2-D6BUULuh.js +1 -0
  32. package/dist-embed/chunks/1f5c3-DjkgJLaz.js +1 -0
  33. package/dist-embed/chunks/1f5d1-C4Kpe4HL.js +1 -0
  34. package/dist-embed/chunks/1f5dc-DyLO7aB9.js +1 -0
  35. package/dist-embed/chunks/1f604-CEekcaYE.js +1 -0
  36. package/dist-embed/chunks/1f917-CyyS9fgf.js +1 -0
  37. package/dist-embed/chunks/1f9d1-200d-2696-D9eSB-0q.js +1 -0
  38. package/dist-embed/chunks/1fa79-DHhIZbWB.js +1 -0
  39. package/dist-embed/chunks/1fa9d-DUI9e77m.js +1 -0
  40. package/dist-embed/chunks/1fa9f-10ALsMaD.js +1 -0
  41. package/dist-embed/chunks/23f3-ASjqFsbT.js +1 -0
  42. package/dist-embed/chunks/2611-BnnmLm-K.js +1 -0
  43. package/dist-embed/chunks/2702-CfLyp35r.js +1 -0
  44. package/dist-embed/chunks/270f-CLwo30mk.js +1 -0
  45. package/dist-embed/chunks/2728-Qg6be-6p.js +1 -0
  46. package/dist-embed/chunks/2b1c-Uc1aRwAf.js +1 -0
  47. package/dist-embed/chunks/{ChatView-kCmBbnDa.js → ChatView-Dy-IgGFL.js} +1 -1
  48. package/dist-embed/datalayer-app-main.js +111 -106
  49. package/lib/apps/plugins/chat/ChatView.js +2 -2
  50. package/lib/chat/ChatFloating.js +1 -1
  51. package/lib/chat/ChatInline.js +43 -22
  52. package/lib/chat/base/ChatBase.js +66 -10
  53. package/lib/chat/messages/ChatMessageList.d.ts +7 -1
  54. package/lib/chat/messages/ChatMessageList.js +8 -2
  55. package/lib/components/agents/AgentSummary.d.ts +2 -24
  56. package/lib/components/agents/AgentSummary.js +3 -19
  57. package/lib/hooks/useChat.js +110 -144
  58. package/lib/portable/activity.d.ts +78 -0
  59. package/lib/portable/activity.js +129 -0
  60. package/lib/portable/fixtures.d.ts +7 -0
  61. package/lib/portable/fixtures.js +50 -0
  62. package/lib/portable/index.d.ts +4 -1
  63. package/lib/portable/index.js +7 -1
  64. package/lib/portable/lifecycle.d.ts +77 -0
  65. package/lib/portable/lifecycle.js +135 -0
  66. package/lib/portable/runtimes.d.ts +68 -0
  67. package/lib/portable/runtimes.js +69 -0
  68. package/lib/protocols/VercelAIAdapter.js +11 -0
  69. package/lib/specs/voices.js +62 -0
  70. package/lib/types/chat.d.ts +10 -0
  71. package/lib/voice/ComposerVoice.js +12 -3
  72. package/lib/voice/VoiceMenu.d.ts +2 -1
  73. package/lib/voice/VoiceMenu.js +2 -2
  74. package/lib/voice/captions.d.ts +31 -0
  75. package/lib/voice/captions.js +156 -0
  76. package/lib/voice/chatVoice.d.ts +5 -2
  77. package/lib/voice/chatVoice.js +10 -4
  78. package/lib/voice/choices.d.ts +2 -0
  79. package/lib/voice/choices.js +6 -0
  80. package/lib/voice/handsFree.d.ts +12 -0
  81. package/lib/voice/handsFree.js +18 -0
  82. package/lib/voice/index.d.ts +1 -0
  83. package/lib/voice/index.js +1 -0
  84. package/lib/voice/sayAloud.d.ts +25 -0
  85. package/lib/voice/sayAloud.js +34 -0
  86. package/lib/voice/serverHearing.d.ts +63 -2
  87. package/lib/voice/serverHearing.js +311 -7
  88. package/lib/voice/settings.d.ts +12 -0
  89. package/lib/voice/settings.js +9 -0
  90. package/package.json +1 -1
  91. package/scripts/voice/measure.py +77 -17
@@ -1702,8 +1702,8 @@ export default function ChatView({ workspace }) {
1702
1702
  // opening stops it, and *Stop speaking* while it speaks.
1703
1703
  speaking: speaking, onStopSpeaking: () => controlsRef.current?.stopSpeaking?.(),
1704
1704
  // Hands-free (VO-12, VO-13): the run in progress, stopped when
1705
- // the person speaks over it.
1706
- running: busy, onStopRun: () => controlsRef.current?.stop?.(), onInterimChange: setVoiceInterim,
1705
+ // the person speaks over it, and the answer marked interrupted.
1706
+ running: busy, onStopRun: () => controlsRef.current?.stop?.(), onInterrupted: () => controlsRef.current?.markInterrupted(), onInterimChange: setVoiceInterim,
1707
1707
  // The menu offers when answers are heard where the
1708
1708
  // application lets them be (VO-21, VO-30).
1709
1709
  answers: Boolean(config?.voice?.output &&
@@ -1030,7 +1030,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
1030
1030
  ...poweredByProps,
1031
1031
  }, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: {
1032
1032
  showAvatars: true,
1033
- }, placeholder: "Type a message...", backgroundColor: "canvas.subtle", frontendTools: frontendTools, showModelSelector: showModelSelector, availableModels: availableModels, showToolsMenu: showToolsMenu, showSkillsMenu: showSkillsMenu, showTokenUsage: showTokenUsage, showContextRing: showContextRing, runtimeId: runtimeId, historyEndpoint: historyEndpoint, historyAuthToken: historyAuthToken, pendingPrompt: pendingPrompt ?? suggestionPrompt, inspector: inspector, showInformation: showInformation, onInformationClick: onInformationClick, onToolCallStart: onToolCallStart, onToolCallComplete: onToolCallComplete, showToolApprovalBanner: showToolApprovalBanner, pendingApprovals: pendingApprovals, onApproveApproval: onApproveApproval, onRejectApproval: onRejectApproval, voice: voice, voiceSpeaking: speakerState.speaking, onStopSpeaking: speaker ? stopSpeaking : undefined, onReadAloud: speaker ? readAnswerAloud : undefined, ...panelProps, ...assistantChat, onLoadingChange: handleLoadingChange, onDisplayItemsChange: handleDisplayItemsChange, onSendReady: handleSendReady, children: conversationBalloon && currentOpen ? (
1033
+ }, placeholder: "Type a message...", backgroundColor: "canvas.subtle", frontendTools: frontendTools, showModelSelector: showModelSelector, availableModels: availableModels, showToolsMenu: showToolsMenu, showSkillsMenu: showSkillsMenu, showTokenUsage: showTokenUsage, showContextRing: showContextRing, runtimeId: runtimeId, historyEndpoint: historyEndpoint, historyAuthToken: historyAuthToken, pendingPrompt: pendingPrompt ?? suggestionPrompt, inspector: inspector, showInformation: showInformation, onInformationClick: onInformationClick, onToolCallStart: onToolCallStart, onToolCallComplete: onToolCallComplete, showToolApprovalBanner: showToolApprovalBanner, pendingApprovals: pendingApprovals, onApproveApproval: onApproveApproval, onRejectApproval: onRejectApproval, voice: voice, voiceSpeaking: speakerState.speaking, voiceSentence: speakerState.sentence, onStopSpeaking: speaker ? stopSpeaking : undefined, onReadAloud: speaker ? readAnswerAloud : undefined, ...panelProps, ...assistantChat, onLoadingChange: handleLoadingChange, onDisplayItemsChange: handleDisplayItemsChange, onSendReady: handleSendReady, children: conversationBalloon && currentOpen ? (
1034
1034
  // Current: the one thing said or done now, over the composer.
1035
1035
  _jsx(Box, { px: 3, pt: 3, pb: 2, overflow: "hidden", children: _jsx(CurrentBalloonBody, { text: toolSaid ? undefined : (currentWords ?? description), fullText: toolSaid ? undefined : currentWhole, tool: toolSaid, busy: atWork, speaking: answering && atWork, waiting: atWork && !answering }) })) : (children) }))] }));
1036
1036
  /*
@@ -6,7 +6,18 @@ import { CopyIcon, ArrowLeftIcon, SyncIcon, TrashIcon, CheckIcon, PlusIcon, Penc
6
6
  import { SparklesIcon } from '@datalayer/icons-react';
7
7
  import { AGUIAdapter } from '../protocols';
8
8
  import { generateMessageId } from '../types/messages';
9
- import { sanitizeAssistantContent } from '../utils';
9
+ import { BROWSER_LIMITS, emptyConversation, reduceConversation, } from '../portable/conversation';
10
+ import { conversationEventsOf } from '../portable/adapterEvents';
11
+ /** The answer so far: the newest assistant message of the conversation. */
12
+ function answerOf(state) {
13
+ for (let index = state.items.length - 1; index >= 0; index -= 1) {
14
+ const item = state.items[index];
15
+ if (item.kind === 'message' && item.role === 'assistant') {
16
+ return item.text;
17
+ }
18
+ }
19
+ return '';
20
+ }
10
21
  /**
11
22
  * Pre-defined AI action groups
12
23
  */
@@ -79,8 +90,15 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
79
90
  const [input, setInput] = useState('');
80
91
  // AI state
81
92
  const [aiState, setAiState] = useState('initial');
82
- // AI response content
83
- const [aiResponse, setAiResponse] = useState('');
93
+ // The one-shot conversation (MOBILE M-030): `reduceConversation`'s, as in
94
+ // ChatBase and on the phone; the response is its newest assistant message.
95
+ const [conversation, setConversation] = useState(() => emptyConversation(BROWSER_LIMITS));
96
+ const conversationRef = useRef(conversation);
97
+ const changeConversation = useCallback((next) => {
98
+ conversationRef.current = next;
99
+ setConversation(next);
100
+ }, []);
101
+ const aiResponse = useMemo(() => answerOf(conversation), [conversation]);
84
102
  // Previous prompt for regeneration
85
103
  const [previousPrompt, setPreviousPrompt] = useState('');
86
104
  // Current page in options menu (for sub-menus like translate)
@@ -112,20 +130,23 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
112
130
  adapterRef.current = adapter;
113
131
  // Subscribe to events
114
132
  unsubscribeRef.current = adapter.subscribe((event) => {
115
- switch (event.type) {
116
- case 'message':
117
- if (event.message?.content) {
118
- const content = typeof event.message.content === 'string'
119
- ? sanitizeAssistantContent(event.message.content)
120
- : '';
121
- setAiResponse(content);
122
- }
123
- break;
124
- case 'error':
125
- console.error('[ChatInline] Protocol error:', event.error);
126
- setError(event.error?.message || 'Unknown error');
127
- setAiState('initial');
128
- break;
133
+ if (event.type === 'error') {
134
+ console.error('[ChatInline] Protocol error:', event.error);
135
+ setError(event.error?.message || 'Unknown error');
136
+ setAiState('initial');
137
+ return;
138
+ }
139
+ // The event, as the conversation reads it: one reducer whatever the protocol.
140
+ try {
141
+ const inputs = conversationEventsOf(event, conversationRef.current);
142
+ if (inputs.length) {
143
+ changeConversation(inputs.reduce((kept, input) => reduceConversation(kept, input), conversationRef.current));
144
+ }
145
+ }
146
+ catch (failure) {
147
+ console.error('[ChatInline] Protocol event not read:', failure);
148
+ setError(failure instanceof Error ? failure.message : String(failure));
149
+ setAiState('initial');
129
150
  }
130
151
  });
131
152
  // Connect
@@ -134,7 +155,7 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
134
155
  unsubscribeRef.current?.();
135
156
  adapterRef.current?.disconnect();
136
157
  };
137
- }, [protocol]);
158
+ }, [protocol, changeConversation]);
138
159
  // Submit prompt to AI
139
160
  const submitPrompt = useCallback(async (prompt) => {
140
161
  if (!adapterRef.current) {
@@ -145,7 +166,7 @@ export function ChatInline({ selectedText, protocol, onReplaceSelection, onInser
145
166
  setInput('');
146
167
  setPreviousPrompt(prompt);
147
168
  setError(null);
148
- setAiResponse('');
169
+ changeConversation(emptyConversation(BROWSER_LIMITS));
149
170
  // Build system message with selected text context
150
171
  const systemContext = `Do not surround your answer in quote marks. Only return the answer, nothing else. The user is selecting this text:
151
172
 
@@ -178,7 +199,7 @@ ${selectedText || ''}
178
199
  setError(err.message);
179
200
  setAiState('initial');
180
201
  }
181
- }, [selectedText]);
202
+ }, [selectedText, changeConversation]);
182
203
  // Handle pending prompt from toolbar dropdown
183
204
  useEffect(() => {
184
205
  if (pendingPrompt && aiState === 'initial') {
@@ -235,8 +256,8 @@ ${selectedText || ''}
235
256
  // Handle back to options
236
257
  const handleBackToOptions = useCallback(() => {
237
258
  setAiState('initial');
238
- setAiResponse('');
239
- }, []);
259
+ changeConversation(emptyConversation(BROWSER_LIMITS));
260
+ }, [changeConversation]);
240
261
  // Handle discard
241
262
  const handleDiscard = useCallback(() => {
242
263
  onClose?.();
@@ -25,7 +25,7 @@ import { SkeletonText } from '@primer/react/experimental';
25
25
  import { SidebarExpandIcon } from '@primer/octicons-react';
26
26
  import { notebookStore, JupyterReactTheme } from '@datalayer/jupyter-react';
27
27
  import { Box, setupPrimerPortals, useThemeStore, getColorPalette, getThemeConfig, useSystemColorMode, } from '@datalayer/primer-addons';
28
- import { AlertIcon, PersonIcon } from '@primer/octicons-react';
28
+ import { AlertIcon, PersonIcon, UnmuteIcon } from '@primer/octicons-react';
29
29
  import { AiAgentIcon } from '@datalayer/icons-react';
30
30
  import { QueryClientProvider, QueryClientContext } from '@tanstack/react-query';
31
31
  import { useCoreStore } from '@datalayer/core';
@@ -52,11 +52,14 @@ import { EphemeralSurfaceControl } from '../EphemeralSurfaceControl';
52
52
  import { ChatMessageList, } from '../messages/ChatMessageList';
53
53
  import { InputPrompt } from '../prompt/InputPrompt';
54
54
  import { ComposerVoice, spokenOf } from '../../voice/ComposerVoice';
55
+ import { answerSpokenOver } from '../../voice/handsFree';
55
56
  import { ReadAloud, readsAloud } from '../../voice/ReadAloud';
56
57
  import { settledVoice } from '../../voice/chatVoice';
57
58
  import { useVoiceSettings } from '../../voice/useVoiceSettings';
58
59
  import { useVoiceSpeaker } from '../../voice/useVoiceSpeaker';
59
60
  import { useSpokenAnswers } from '../../voice/useSpokenAnswers';
61
+ import { saidMessageId, useSaidSentence } from '../../voice/captions';
62
+ import { lineToSay } from '../../voice/sayAloud';
60
63
  import { isFloatingChatViewMode } from '../viewModes';
61
64
  import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
62
65
  import { initialModelId, isOffered, offeredModels as offeredModelsFor, } from './modelChoice';
@@ -423,7 +426,7 @@ function ChatBaseInner({ title, subtitle, showHeader = false, showTokenUsage = t
423
426
  // Mode selection
424
427
  useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady, onLoadingChange, onSpeakingChange, onContextSnapshot, enableStreaming = false,
425
428
  // Extended props
426
- brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, voice, voiceSpeaking = false, onStopSpeaking, onReadAloud, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
429
+ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, voice, voiceSpeaking = false, voiceSentence, onStopSpeaking, onReadAloud, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
427
430
  // Tool invocation hooks
428
431
  onToolCallStart, onToolCallComplete, inspector: inspectorGiven,
429
432
  // Identity/Authorization props
@@ -1819,6 +1822,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
1819
1822
  recorderRef.current = null;
1820
1823
  }
1821
1824
  const inspectAnswerRef = useRef('');
1825
+ // A line the agent says aloud, from a run's `voice.say` (VOICE.md VO-35).
1826
+ const voiceSayRef = useRef(undefined);
1822
1827
  const handleRespondRef = useRef(null);
1823
1828
  // A decision made outside the chat — a host page's own approval banner —
1824
1829
  // is answered exactly as the tool card's buttons answer it: through the
@@ -2801,6 +2806,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2801
2806
  // What the server says besides the conversation's text comes on
2802
2807
  // the ui socket (REACTOR_UI RU-W); the run's own — `loop.session`
2803
2808
  // among them — are the conversation's.
2809
+ if (event.activity) {
2810
+ voiceSayRef.current?.(event.activity);
2811
+ }
2804
2812
  read();
2805
2813
  break;
2806
2814
  case 'done':
@@ -2968,8 +2976,45 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2968
2976
  const ownVoice = useVoiceSpeaker(settled, !hostSpeaks);
2969
2977
  useSpokenAnswers(displayItems, isLoading, !hostSpeaks && settled?.output === 'always', ownVoice.speaker);
2970
2978
  const speakingNow = voiceSpeaking || ownVoice.state.speaking;
2979
+ // The sentence being said, marked in the conversation (VO-65).
2980
+ const sentenceSaid = hostSpeaks ? voiceSentence : ownVoice.state.sentence;
2981
+ const saidConversationRef = useRef(null);
2982
+ useSaidSentence(saidConversationRef, sentenceSaid, displayItems);
2983
+ /*
2984
+ * A line the agent wants heard, not written (VO-35): a caption while the
2985
+ * run goes on, said by the speaker where answers are said by themselves.
2986
+ */
2987
+ const [voiceSaid, setVoiceSaid] = useState('');
2988
+ voiceSayRef.current = activity => {
2989
+ const line = lineToSay(activity);
2990
+ if (!line) {
2991
+ return;
2992
+ }
2993
+ setVoiceSaid(line);
2994
+ if (settled?.output === 'always') {
2995
+ if (onReadAloud) {
2996
+ onReadAloud(line);
2997
+ }
2998
+ else {
2999
+ ownVoice.speaker?.say(line);
3000
+ }
3001
+ }
3002
+ };
3003
+ useEffect(() => {
3004
+ if (!isLoading) {
3005
+ setVoiceSaid('');
3006
+ }
3007
+ }, [isLoading]);
2971
3008
  // Answers spoken over, hands-free (VOICE.md VO-13): marked *interrupted*.
2972
3009
  const [interruptedIds, setInterruptedIds] = useState(() => new Set());
3010
+ // The conversation as shown, read when the person speaks over an answer.
3011
+ const shownItemsRef = useRef([]);
3012
+ const markInterrupted = useCallback(() => {
3013
+ const spokenOver = answerSpokenOver(shownItemsRef.current.filter((item) => !isToolCallMessage(item)));
3014
+ if (spokenOver) {
3015
+ setInterruptedIds(ids => new Set([...ids, spokenOver]));
3016
+ }
3017
+ }, []);
2973
3018
  const stopSpeaking = onStopSpeaking ?? (ownVoice.speaker ? ownVoice.stop : undefined);
2974
3019
  const readAnswerAloud = onReadAloud ?? (ownVoice.speaker ? ownVoice.read : undefined);
2975
3020
  const handleSend = useCallback(async (messageOverride,
@@ -3339,6 +3384,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3339
3384
  thread: () => threadIdRef.current,
3340
3385
  // Its speaker, for a host whose composer holds the microphone (VO-10).
3341
3386
  stopSpeaking,
3387
+ // The answer spoken over, marked for a host's own composer (VO-13).
3388
+ markInterrupted,
3342
3389
  });
3343
3390
  return () => {
3344
3391
  onSendReady(null);
@@ -3351,6 +3398,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3351
3398
  handleStop,
3352
3399
  handleNewChat,
3353
3400
  stopSpeaking,
3401
+ markInterrupted,
3354
3402
  ]);
3355
3403
  // Streaming state, for a host that draws the prompt.
3356
3404
  useEffect(() => {
@@ -3717,6 +3765,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3717
3765
  ...displayItems,
3718
3766
  ]
3719
3767
  : displayItems;
3768
+ shownItemsRef.current = welcomedItems;
3769
+ const saidId = saidMessageId(welcomedItems, sentenceSaid);
3720
3770
  /*
3721
3771
  * *Read aloud* under each answer the agent has finished (VOICE.md VO-21),
3722
3772
  * when the host reads answers aloud on request.
@@ -3732,7 +3782,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3732
3782
  return (_jsxs(_Fragment, { children: [interruptedIds.has(message.id) ? (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontStyle: 'italic' }, "data-interrupted": "", children: "interrupted" })) : null, readAnswerAloud && readsAloud(message, text, writingId) ? (_jsx(ReadAloud, { onRead: () => readAnswerAloud(text) })) : null] }));
3733
3783
  }
3734
3784
  : undefined;
3735
- const messagesContent = children ? (children) : (_jsxs(Box, { display: "flex", flexDirection: "column", minHeight: 0,
3785
+ const messagesContent = children ? (children) : (_jsxs(Box, { ref: saidConversationRef, display: "flex", flexDirection: "column", minHeight: 0,
3736
3786
  /*
3737
3787
  A reading column, not a full-bleed sheet.
3738
3788
 
@@ -3745,16 +3795,12 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3745
3795
  // Tool calls are drawn by the page's Reactor plugins; a named
3746
3796
  // notebook's cell surfaces stand in for the default row of its tools,
3747
3797
  // so every chat with a headless notebook gets the same transcript.
3748
- notebookToolSurfacesId: notebookToolSurfacesId, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, renderMessageFooter: readAloudFooter, 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 })) }), trailingContent] }));
3798
+ notebookToolSurfacesId: notebookToolSurfacesId, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, renderMessageFooter: readAloudFooter, saidMessageId: saidId, 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 })) }), trailingContent] }));
3749
3799
  // The microphone, in the composer's footer (VO-10, VO-11).
3750
3800
  const voiceControl = settled && settled.input !== 'off' ? (_jsx(ComposerVoice, { voice: settled, input: input, setInput: setInput, disabled: disabled || launching, speaking: speakingNow, onStopSpeaking: stopSpeaking,
3751
3801
  // Hands-free (VO-12, VO-13): the run in progress, stopped when the
3752
3802
  // person speaks over it, and the answer marked interrupted.
3753
- running: isLoading, onStopRun: handleStop, onInterrupted: () => {
3754
- if (writingId) {
3755
- setInterruptedIds(ids => new Set([...ids, writingId]));
3756
- }
3757
- }, onInterimChange: setVoiceInterim,
3803
+ running: isLoading, onStopRun: handleStop, onInterrupted: markInterrupted, onInterimChange: setVoiceInterim,
3758
3804
  // The menu offers when answers are heard where the application
3759
3805
  // lets them be (VO-21, VO-30).
3760
3806
  answers: Boolean(voice?.output && voice.output !== 'off' && voice.speechUrl), onSpoken: metadata => {
@@ -3762,7 +3808,17 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3762
3808
  }, send: (text, metadata) => void handleSend(text, undefined, metadata) })) : undefined;
3763
3809
  // Why an answer could not be heard, in a sentence, by the composer.
3764
3810
  const voiceRefused = !hostSpeaks && ownVoice.state.refused ? (_jsx(Text, { role: "alert", sx: { fontSize: 0, color: 'danger.fg', maxWidth: 260 }, "data-voice-refused": "", children: ownVoice.state.refused })) : null;
3765
- const inputToolbar = showInput ? (_jsx(InputPrompt, { footerExtras: voiceRefused ? (_jsxs(_Fragment, { children: [voiceRefused, voiceControl] })) : (voiceControl), interim: voiceInterim, input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
3811
+ // The line said aloud, written as it is said: captions always (VO-65).
3812
+ const voiceSayCaption = voiceSaid ? (_jsxs(Text, { "aria-live": "polite", sx: {
3813
+ fontSize: 0,
3814
+ fontStyle: 'italic',
3815
+ color: 'fg.muted',
3816
+ maxWidth: 260,
3817
+ display: 'inline-flex',
3818
+ alignItems: 'center',
3819
+ gap: 1,
3820
+ }, "data-voice-say": "", children: [_jsx(UnmuteIcon, { size: 12, "aria-label": "Said aloud" }), voiceSaid] })) : null;
3821
+ const inputToolbar = showInput ? (_jsx(InputPrompt, { footerExtras: voiceRefused || voiceSayCaption ? (_jsxs(_Fragment, { children: [voiceSayCaption, voiceRefused, voiceControl] })) : (voiceControl), interim: voiceInterim, input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
3766
3822
  /*
3767
3823
  The same openers the empty state offers, typed into the box they would
3768
3824
  be typed into.
@@ -78,6 +78,12 @@ export interface ChatMessageListProps {
78
78
  * `note`, which has no footer of its own.
79
79
  */
80
80
  renderMessageFooter?: (message: ChatMessage) => ReactNode;
81
+ /**
82
+ * The answer a sentence is being said from (VOICE.md VO-65): its row is
83
+ * marked `aria-current` and `data-voice-saying`, with a bar beside it —
84
+ * the sentence itself is underlined by `useSaidSentence`.
85
+ */
86
+ saidMessageId?: string;
81
87
  /**
82
88
  * The MCP servers the agent has, with the tools each serves: a tool call is
83
89
  * drawn with the marks of the server it belongs to. Skills, frontend tools
@@ -109,5 +115,5 @@ export interface SubagentChatPanelProps {
109
115
  * approval decisions are sent over the shared monitoring socket.
110
116
  */
111
117
  export declare function SubagentChatPanel({ toolCallId, subagentName: subagentNameHint, height, avatarConfig, }: SubagentChatPanelProps): React.ReactElement | null;
112
- export declare function ChatMessageList({ displayItems, actOn, upload, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, notebookToolSurfacesId, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters, agentUsage, onRemoveItems, renderMessageFooter, mcpServers, density, }: ChatMessageListProps): import("react").JSX.Element;
118
+ export declare function ChatMessageList({ displayItems, actOn, upload, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, notebookToolSurfacesId, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters, agentUsage, onRemoveItems, renderMessageFooter, saidMessageId, mcpServers, density, }: ChatMessageListProps): import("react").JSX.Element;
113
119
  export {};
@@ -366,7 +366,7 @@ function DefaultToolCallRenderer({ item, onRespond, mcpServers, density, }) {
366
366
  // ---------------------------------------------------------------------------
367
367
  // Component
368
368
  // ---------------------------------------------------------------------------
369
- export function ChatMessageList({ displayItems, actOn, upload, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, notebookToolSurfacesId, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters = false, agentUsage, onRemoveItems, renderMessageFooter, mcpServers, density = 'comfortable', }) {
369
+ export function ChatMessageList({ displayItems, actOn, upload, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, notebookToolSurfacesId, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters = false, agentUsage, onRemoveItems, renderMessageFooter, saidMessageId, mcpServers, density = 'comfortable', }) {
370
370
  /*
371
371
  * Who draws a tool call: the Reactor plugins of the page, when it has a
372
372
  * platform. Each plugin binds its own tools (`ToolBindings`) and renders its
@@ -541,7 +541,13 @@ export function ChatMessageList({ displayItems, actOn, upload, isLoading, isStre
541
541
  }
542
542
  // The host's footer, asked once: a row only when it draws something.
543
543
  const messageFooter = renderMessageFooter?.(message);
544
- return (_jsx(Box, { "data-chat-message": message.role, display: "flex", flexDirection: "column", alignItems: isUser ? 'flex-end' : 'flex-start', px: padding, py: compact ? '2px' : 1, children: _jsxs(Box, { display: "flex", gap: 2, flexDirection: isUser ? 'row-reverse' : 'row', alignItems: "flex-start",
544
+ // The answer being said (VO-65): marked by a bar and for assistive tech.
545
+ const said = saidMessageId !== undefined && message.id === saidMessageId;
546
+ return (_jsx(Box, { "data-chat-message": message.role, "data-voice-saying": said ? '' : undefined, "aria-current": said ? 'true' : undefined, sx: said
547
+ ? {
548
+ boxShadow: 'inset 3px 0 0 var(--borderColor-accent-emphasis, #0969da)',
549
+ }
550
+ : undefined, display: "flex", flexDirection: "column", alignItems: isUser ? 'flex-end' : 'flex-start', px: padding, py: compact ? '2px' : 1, children: _jsxs(Box, { display: "flex", gap: 2, flexDirection: isUser ? 'row-reverse' : 'row', alignItems: "flex-start",
545
551
  /*
546
552
  Full width, always.
547
553
 
@@ -1,28 +1,6 @@
1
1
  import React from 'react';
2
- export interface AgentSummaryData {
3
- agentName: string;
4
- location: string;
5
- specId?: string;
6
- /**
7
- * The framework turning this agent's loop, as its spec declares it —
8
- * `pydantic-ai` server-side, `vercel-ai` in the page.
9
- */
10
- harness?: string;
11
- /**
12
- * The runtime variant the host chose: `<location>-<harness>`.
13
- *
14
- * Shown next to the harness rather than instead of it, because they answer
15
- * different questions and can disagree: the harness is what the spec asked
16
- * for, the variant is what it actually got.
17
- */
18
- variant?: string;
19
- status?: string;
20
- baseUrl?: string;
21
- sandboxBaseUrl?: string;
22
- agentId?: string;
23
- isReady?: boolean;
24
- error?: string;
25
- }
2
+ import { type AgentSummaryData } from '../../portable/runtimes';
3
+ export type { AgentSummaryData } from '../../portable/runtimes';
26
4
  export interface AgentSummaryProps {
27
5
  summary: AgentSummaryData | null;
28
6
  title?: string;
@@ -7,19 +7,7 @@ import { useEffect, useRef, useState } from 'react';
7
7
  import { Spinner } from '@primer/react';
8
8
  import { Box } from '@datalayer/primer-addons';
9
9
  import { strip } from '@datalayer/core/lib/utils';
10
- /**
11
- * Statuses that represent an agent actively being created/started. The spinner
12
- * only shows for these; app-navigation states ("selected", "switching") and
13
- * settled states ("ready", "running", ...) never spin.
14
- */
15
- const CREATING_STATUSES = new Set([
16
- 'creating',
17
- 'starting',
18
- 'launching',
19
- 'connecting',
20
- 'pending',
21
- 'resuming',
22
- ]);
10
+ import { isCreatingAgent, } from '../../portable/runtimes';
23
11
  const MAX_AGENT_NAME_LENGTH = 32;
24
12
  const toSandboxStatusUrl = (value) => {
25
13
  const raw = String(value || '').trim();
@@ -95,12 +83,8 @@ export const AgentSummary = ({ summary, title = 'Active Agent', }) => {
95
83
  if (!summary) {
96
84
  return null;
97
85
  }
98
- const normalizedStatus = (summary.status || '').toLowerCase();
99
- // Only show the spinner while the agent is genuinely being created: it is not
100
- // ready, has no id yet, and its status is an active creation/startup state.
101
- const isCreating = !summary.isReady &&
102
- !summary.agentId &&
103
- CREATING_STATUSES.has(normalizedStatus);
86
+ // Only show the spinner while the agent is genuinely being created.
87
+ const isCreating = isCreatingAgent(summary);
104
88
  const hasError = Boolean(summary.error);
105
89
  // The agent is optional: some examples run only a code sandbox. Show the
106
90
  // agent status when there is an agent (or one is being created).