@datalayer/agent-runtimes 1.3.73 → 1.3.75

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 (72) hide show
  1. package/lib/chat/ChatFloating.d.ts +25 -2
  2. package/lib/chat/ChatFloating.js +80 -12
  3. package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
  4. package/lib/chat/assistant/AssistantContextMenu.js +68 -0
  5. package/lib/chat/assistant/AssistantStage.d.ts +123 -5
  6. package/lib/chat/assistant/AssistantStage.js +245 -7
  7. package/lib/chat/assistant/BalloonParts.d.ts +15 -1
  8. package/lib/chat/assistant/BalloonParts.js +26 -9
  9. package/lib/chat/assistant/BalloonVisual.d.ts +65 -0
  10. package/lib/chat/assistant/BalloonVisual.js +58 -0
  11. package/lib/chat/assistant/SpeechBalloon.d.ts +68 -2
  12. package/lib/chat/assistant/SpeechBalloon.js +113 -7
  13. package/lib/chat/assistant/state.d.ts +19 -0
  14. package/lib/chat/assistant/state.js +44 -17
  15. package/lib/chat/base/ChatBase.js +51 -1
  16. package/lib/chat/indicators/TypingDots.d.ts +13 -0
  17. package/lib/chat/indicators/TypingDots.js +21 -0
  18. package/lib/chat/messages/ChatMessageList.js +1 -15
  19. package/lib/components/inspector/AgentInspector.d.ts +59 -0
  20. package/lib/components/inspector/AgentInspector.js +220 -0
  21. package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
  22. package/lib/components/inspector/AgentInspectorDialog.js +9 -0
  23. package/lib/components/inspector/JsonTree.d.ts +24 -0
  24. package/lib/components/inspector/JsonTree.js +89 -0
  25. package/lib/components/inspector/a2aInspect.d.ts +56 -0
  26. package/lib/components/inspector/a2aInspect.js +478 -0
  27. package/lib/components/inspector/agentInspector.d.ts +179 -0
  28. package/lib/components/inspector/agentInspector.js +267 -0
  29. package/lib/components/inspector/chatInspect.d.ts +38 -0
  30. package/lib/components/inspector/chatInspect.js +65 -0
  31. package/lib/components/inspector/classify.d.ts +19 -0
  32. package/lib/components/inspector/classify.js +42 -0
  33. package/lib/components/inspector/index.d.ts +14 -0
  34. package/lib/components/inspector/index.js +18 -0
  35. package/lib/components/teams/A2ATeamGraph.d.ts +54 -3
  36. package/lib/components/teams/A2ATeamGraph.js +37 -11
  37. package/lib/components/teams/NotebookPreview.d.ts +11 -2
  38. package/lib/components/teams/NotebookPreview.js +6 -3
  39. package/lib/components/teams/NotebookPreviewView.js +9 -1
  40. package/lib/components/teams/TeamNotebook.d.ts +10 -1
  41. package/lib/components/teams/TeamNotebook.js +15 -3
  42. package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
  43. package/lib/components/teams/jupyterThemeVariables.js +441 -0
  44. package/lib/components/teams/teamConnections.d.ts +9 -1
  45. package/lib/components/teams/teamConnections.js +20 -0
  46. package/lib/components/teams/useA2ATeam.d.ts +17 -4
  47. package/lib/components/teams/useA2ATeam.js +87 -23
  48. package/lib/examples/AgentA2ATeamExample.d.ts +16 -2
  49. package/lib/examples/AgentA2ATeamExample.js +65 -13
  50. package/lib/examples/{ChatAssistantExample.d.ts → AssistantExample.d.ts} +2 -2
  51. package/lib/examples/{ChatAssistantExample.js → AssistantExample.js} +69 -7
  52. package/lib/examples/{ChatAssistantGalleryExample.d.ts → AssistantGalleryExample.d.ts} +2 -2
  53. package/lib/examples/{ChatAssistantGalleryExample.js → AssistantGalleryExample.js} +45 -13
  54. package/lib/examples/example-selector.js +4 -2
  55. package/lib/examples/exampleGroups.d.ts +9 -0
  56. package/lib/examples/exampleGroups.js +56 -0
  57. package/lib/examples/index.d.ts +1 -1
  58. package/lib/examples/index.js +1 -1
  59. package/lib/examples/main.js +3 -43
  60. package/lib/examples/utils/AssistantGalleryGrid.d.ts +6 -1
  61. package/lib/examples/utils/AssistantGalleryGrid.js +16 -3
  62. package/lib/examples/utils/assistantGallery.d.ts +51 -2
  63. package/lib/examples/utils/assistantGallery.js +32 -7
  64. package/lib/loop/apps/AppRenderer.js +1 -1
  65. package/lib/loop/core/index.d.ts +17 -0
  66. package/lib/loop/core/index.js +1 -0
  67. package/lib/loop/plugins/assistant/index.d.ts +1 -1
  68. package/lib/loop/plugins/assistant/index.js +8 -3
  69. package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
  70. package/lib/types/chat.d.ts +7 -0
  71. package/package.json +1 -1
  72. package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
@@ -1,13 +1,100 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
2
3
  import { Button, IconButton, Text } from '@primer/react';
3
4
  import { Box } from '@datalayer/primer-addons';
4
5
  import { XIcon } from '@primer/octicons-react';
5
- import { ASSISTANT_WORDS } from './state';
6
+ import { ASSISTANT_WORDS, } from './state';
6
7
  import { moreWaiting } from './ConversationBalloon';
7
8
  import { DecisionAsk } from './DecisionAsk';
8
- import { BalloonToolLineView, CurrentBalloonBody, ToolLineAnnouncer, } from './BalloonParts';
9
- export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, decide, onDecisionActive, wide = false, onDismissPeek, display = 'history', tool, speaking = false, busy = false, attachment, }) {
9
+ import { BalloonToolLineView, CurrentBalloonBody, ToolLineAnnouncer, conversationHeaderText, } from './BalloonParts';
10
+ import { BalloonExpandButton } from './BalloonVisual';
11
+ import { ScreenFullIcon } from '@primer/octicons-react';
12
+ import { TypingDots } from '../indicators/TypingDots';
13
+ /** The suggestions, as chips: each sends its prompt. */
14
+ export function BalloonSuggestions({ suggestions, onSuggestion, }) {
15
+ return (_jsx(Box, { role: "group", "aria-label": "Suggestions", "data-balloon-suggestions": "", sx: { display: 'flex', flexWrap: 'wrap', gap: 1, mt: 2 }, children: suggestions.map(suggestion => (_jsx(Button, { size: "small", "data-balloon-suggestion": suggestion.label, "aria-label": `Ask: ${suggestion.prompt}`, title: suggestion.prompt, onClick: () => onSuggestion(suggestion), sx: { borderRadius: 999 }, children: suggestion.label }, suggestion.label))) }));
16
+ }
17
+ /** How tall the listed conversation grows before it scrolls, in pixels. */
18
+ export const BALLOON_HISTORY_MAX_HEIGHT = 220;
19
+ /** Shorter, when something goes with the words under it (a notebook). */
20
+ export const BALLOON_HISTORY_WITH_ATTACHMENT_MAX_HEIGHT = 120;
21
+ /**
22
+ * The conversation in the `history` balloon: a header that counts it and
23
+ * opens it, then every message, the person's to the right, scrolled to the
24
+ * newest.
25
+ */
26
+ /**
27
+ * The history balloon, large (*Expand*): every message, then the tool being
28
+ * called and what goes with the words, in the overlay or the page's area,
29
+ * scrolled there.
30
+ */
31
+ export function BalloonHistoryLarge({ history, tool, attachment, }) {
32
+ return (_jsxs(Box, { role: "log", "aria-label": "Conversation", "data-balloon-history-large": "", sx: { display: 'flex', flexDirection: 'column', gap: 2, fontSize: 1 }, children: [history.map(message => (_jsx(Box, { "data-balloon-history-message": message.role, sx: {
33
+ alignSelf: message.role === 'user' ? 'flex-end' : 'flex-start',
34
+ maxWidth: '80%',
35
+ px: 3,
36
+ py: 2,
37
+ borderRadius: 2,
38
+ bg: message.role === 'user' ? 'accent.subtle' : 'canvas.subtle',
39
+ whiteSpace: 'pre-wrap',
40
+ overflowWrap: 'anywhere',
41
+ }, children: message.text }, message.id))), tool ? _jsx(BalloonToolLineView, { line: tool }) : null, attachment ? _jsx(Box, { children: attachment }) : null] }));
42
+ }
43
+ function BalloonHistory({ history, onOpen, maxHeight = BALLOON_HISTORY_MAX_HEIGHT, onExpand, }) {
44
+ const listRef = useRef(null);
45
+ const newest = history[history.length - 1]?.id;
46
+ useEffect(() => {
47
+ const list = listRef.current;
48
+ if (list) {
49
+ list.scrollTop = list.scrollHeight;
50
+ }
51
+ }, [history.length, newest]);
52
+ return (_jsxs(Box, { "data-balloon-history": "", sx: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }, children: [_jsx(Box, { as: "button", type: "button", "data-balloon-peek": "", "data-balloon-header": "", onClick: onOpen, sx: {
53
+ m: 0,
54
+ p: 0,
55
+ pb: 1,
56
+ border: 0,
57
+ bg: 'transparent',
58
+ font: 'inherit',
59
+ textAlign: 'left',
60
+ cursor: 'pointer',
61
+ fontSize: 0,
62
+ fontWeight: 'semibold',
63
+ color: 'fg.muted',
64
+ '&:hover': { textDecoration: 'underline' },
65
+ }, children: conversationHeaderText(history.length) }), onExpand ? (_jsx(IconButton, { icon: ScreenFullIcon, size: "small", variant: "invisible", "aria-label": "Expand the conversation", "data-balloon-history-expand": "", onClick: onExpand, sx: { position: 'absolute', top: '4px', right: '28px' } })) : null, _jsx(Box, { ref: listRef, role: "log", "aria-label": "Conversation", "data-balloon-history-list": "", sx: {
66
+ maxHeight,
67
+ overflowY: 'auto',
68
+ display: 'flex',
69
+ flexDirection: 'column',
70
+ gap: 1,
71
+ pr: 1,
72
+ }, children: history.map(message => (_jsx(Box, { "data-balloon-history-message": message.role, sx: {
73
+ alignSelf: message.role === 'user' ? 'flex-end' : 'flex-start',
74
+ maxWidth: '88%',
75
+ px: 2,
76
+ py: 1,
77
+ borderRadius: 2,
78
+ bg: message.role === 'user' ? 'accent.subtle' : 'canvas.subtle',
79
+ overflowWrap: 'anywhere',
80
+ }, children: message.text }, message.id))) })] }));
81
+ }
82
+ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, decide, onDecisionActive, wide = false, onDismissPeek, display = 'history', tool, speaking = false, busy = false, attachment, history, onExpand, expandTitle, suggestions, onSuggestion, waiting = false, fullText, onExpandHistory, }) {
83
+ // *more*: the words whole, when they were cut or overflow their lines.
84
+ const [whole, setWhole] = useState(false);
85
+ const [overflowing, setOverflowing] = useState(false);
86
+ const cut = (!!fullText && fullText !== text) || (!!more && text.endsWith('…'));
87
+ const wholeText = fullText ?? text;
88
+ // Other words, folded again.
89
+ useEffect(() => {
90
+ setWhole(false);
91
+ }, [wholeText.slice(0, 40)]);
92
+ const offered = suggestions && suggestions.length > 0 && onSuggestion && !approval
93
+ ? suggestions
94
+ : null;
10
95
  const current = display === 'current';
96
+ // History with messages to list: all of them, not the newest line alone.
97
+ const listed = !current && history && history.length > 0 ? history : null;
11
98
  return (_jsxs(Box, { "data-speech-balloon": "", "data-balloon-display": display, sx: {
12
99
  position: 'absolute',
13
100
  // Pixels, as strings: a number here is read as the theme's space
@@ -16,8 +103,14 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
16
103
  ? { bottom: `${above}px` }
17
104
  : { top: `${above}px` }),
18
105
  [align]: 0,
19
- maxWidth: wide || (current && attachment) ? 300 : current ? 260 : 280,
20
- width: wide || (current && attachment) ? 300 : 'max-content',
106
+ maxWidth: wide || listed || offered || whole || (current && attachment)
107
+ ? 300
108
+ : current
109
+ ? 260
110
+ : 280,
111
+ width: wide || listed || offered || whole || (current && attachment)
112
+ ? 300
113
+ : 'max-content',
21
114
  px: 3,
22
115
  py: 2,
23
116
  bg: 'canvas.default',
@@ -54,7 +147,9 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
54
147
  font: 'inherit',
55
148
  textAlign: 'left',
56
149
  cursor: 'pointer',
57
- }, children: _jsx(CurrentBalloonBody, { text: text, tool: tool, busy: busy, speaking: speaking }) })) : (_jsx(Box, { as: "button", type: "button", "data-balloon-peek": "", "data-balloon-more": more ? '' : undefined, onClick: onOpen, sx: {
150
+ }, children: _jsx(CurrentBalloonBody, { text: text, tool: tool, busy: busy, speaking: speaking, waiting: waiting, whole: whole, fullText: wholeText, onOverflow: setOverflowing }) })) : listed ? (_jsx(BalloonHistory, { history: listed, onOpen: onOpen, onExpand: onExpandHistory, maxHeight: attachment
151
+ ? BALLOON_HISTORY_WITH_ATTACHMENT_MAX_HEIGHT
152
+ : BALLOON_HISTORY_MAX_HEIGHT })) : (_jsx(Box, { as: "button", type: "button", "data-balloon-peek": "", "data-balloon-more": more ? '' : undefined, onClick: onOpen, sx: {
58
153
  flex: 1,
59
154
  minWidth: 0,
60
155
  m: 0,
@@ -66,6 +161,17 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
66
161
  textAlign: 'left',
67
162
  cursor: 'pointer',
68
163
  '&:hover': { textDecoration: 'underline' },
69
- }, children: tool ? (_jsx(BalloonToolLineView, { line: tool })) : (_jsx(Box, { as: "span", role: "status", "aria-live": "polite", children: text })) })), onDismissPeek && !approval ? (_jsx(IconButton, { icon: XIcon, size: "small", variant: "invisible", "aria-label": "Dismiss", "data-balloon-dismiss": "", onClick: onDismissPeek, sx: { mt: '-4px', mr: '-8px', flexShrink: 0 } })) : null] }), approval && (_jsxs(Box, { "data-balloon-approval": approval.id, sx: { mt: 1 }, children: [_jsx(Text, { as: "p", sx: { m: 0, fontWeight: 'semibold' }, children: approval.asks }), approval.why ? (_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: approval.why })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { size: "small", variant: "primary", disabled: approval.deciding, onClick: approval.onApprove, children: ASSISTANT_WORDS.approve }), _jsx(Button, { size: "small", disabled: approval.deciding, onClick: approval.onDeny, children: ASSISTANT_WORDS.deny })] }), approval.others > 0 ? (_jsx(Text, { as: "p", sx: { m: 0, mt: 1, color: 'fg.muted', fontSize: 0 }, children: moreWaiting(approval.others) })) : null] })), current && attachment ? (_jsx(Box, { "data-balloon-attachment": "", sx: { mt: 2 }, children: attachment })) : null, !current ? _jsx(ToolLineAnnouncer, { line: tool }) : null, decide && _jsx(DecisionAsk, { ask: decide, onActiveChange: onDecisionActive })] }));
164
+ }, children: tool ? (_jsx(BalloonToolLineView, { line: tool })) : (_jsx(Box, { as: "span", role: "status", "aria-live": "polite", children: text })) })), onDismissPeek && !approval ? (_jsx(IconButton, { icon: XIcon, size: "small", variant: "invisible", "aria-label": "Dismiss", "data-balloon-dismiss": "", onClick: onDismissPeek, sx: { mt: '-4px', mr: '-8px', flexShrink: 0 } })) : null] }), current && !approval && !tool && (cut || overflowing || whole) ? (_jsx(Box, { as: "button", type: "button", "aria-expanded": whole, "data-balloon-whole": whole ? 'less' : 'more', onClick: () => setWhole(!whole), sx: {
165
+ m: 0,
166
+ mt: 1,
167
+ p: 0,
168
+ border: 0,
169
+ bg: 'transparent',
170
+ color: 'accent.fg',
171
+ font: 'inherit',
172
+ fontSize: 0,
173
+ cursor: 'pointer',
174
+ '&:hover': { textDecoration: 'underline' },
175
+ }, children: whole ? 'less' : 'more' })) : null, listed && tool ? (_jsx(Box, { "data-balloon-history-tool": "", sx: { mt: 1 }, children: _jsx(BalloonToolLineView, { line: tool }) })) : null, !current && waiting ? (_jsx(Box, { "data-balloon-waiting": "", sx: { mt: 1 }, children: _jsx(TypingDots, { size: 6 }) })) : null, approval && (_jsxs(Box, { "data-balloon-approval": approval.id, sx: { mt: 1 }, children: [_jsx(Text, { as: "p", sx: { m: 0, fontWeight: 'semibold' }, children: approval.asks }), approval.why ? (_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: approval.why })) : null, _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { size: "small", variant: "primary", disabled: approval.deciding, onClick: approval.onApprove, children: ASSISTANT_WORDS.approve }), _jsx(Button, { size: "small", disabled: approval.deciding, onClick: approval.onDeny, children: ASSISTANT_WORDS.deny })] }), approval.others > 0 ? (_jsx(Text, { as: "p", sx: { m: 0, mt: 1, color: 'fg.muted', fontSize: 0 }, children: moreWaiting(approval.others) })) : null] })), offered && onSuggestion ? (_jsx(BalloonSuggestions, { suggestions: offered, onSuggestion: onSuggestion })) : null, attachment ? (_jsxs(Box, { "data-balloon-attachment": "", sx: { mt: 2, display: 'flex', flexDirection: 'column', gap: 1 }, children: [attachment, onExpand ? (_jsx(BalloonExpandButton, { title: expandTitle ?? 'the visual', onExpand: onExpand })) : null] })) : null, !current ? _jsx(ToolLineAnnouncer, { line: tool }) : null, decide && _jsx(DecisionAsk, { ask: decide, onActiveChange: onDecisionActive })] }));
70
176
  }
71
177
  export default SpeechBalloon;
@@ -79,6 +79,25 @@ export interface AssistantSaying {
79
79
  }
80
80
  /** How much of a message is kept as the agent's saying before it is cut. */
81
81
  export declare const SAYING_LIMIT = 220;
82
+ /**
83
+ * A message's content as plain words, for a balloon: its text parts joined,
84
+ * code blocks left out, links and Markdown read as plain words.
85
+ */
86
+ export declare function plainWordsOf(content: unknown): string;
87
+ /** A message of the conversation, as the `history` balloon lists it. */
88
+ export interface BalloonHistoryMessage {
89
+ id: string;
90
+ /** Who said it: the person, or the agent. */
91
+ role: 'user' | 'assistant';
92
+ /** Its words, plain. */
93
+ text: string;
94
+ }
95
+ /**
96
+ * The conversation as the `history` balloon lists it (T-23): the person's
97
+ * and the agent's messages with words, in order — not the tool calls, not
98
+ * an empty message — the same messages its header counts.
99
+ */
100
+ export declare function balloonHistoryOf(items: readonly unknown[]): BalloonHistoryMessage[];
82
101
  /**
83
102
  * The newest thing the agent said, as the Office Assistant said it: in the
84
103
  * balloon, while the conversation is closed (T-23). Only its own messages
@@ -52,6 +52,49 @@ export const ASSISTANT_WORDS = {
52
52
  };
53
53
  /** How much of a message is kept as the agent's saying before it is cut. */
54
54
  export const SAYING_LIMIT = 220;
55
+ /**
56
+ * A message's content as plain words, for a balloon: its text parts joined,
57
+ * code blocks left out, links and Markdown read as plain words.
58
+ */
59
+ export function plainWordsOf(content) {
60
+ const raw = typeof content === 'string'
61
+ ? content
62
+ : Array.isArray(content)
63
+ ? content
64
+ .map(part => part &&
65
+ typeof part === 'object' &&
66
+ part.type === 'text'
67
+ ? String(part.text ?? '')
68
+ : '')
69
+ .join(' ')
70
+ : '';
71
+ return raw
72
+ .replace(/```[\s\S]*?```/g, ' ')
73
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
74
+ .replace(/[*_`#>]+/g, '')
75
+ .replace(/\s+/g, ' ')
76
+ .trim();
77
+ }
78
+ /**
79
+ * The conversation as the `history` balloon lists it (T-23): the person's
80
+ * and the agent's messages with words, in order — not the tool calls, not
81
+ * an empty message — the same messages its header counts.
82
+ */
83
+ export function balloonHistoryOf(items) {
84
+ const history = [];
85
+ items.forEach((entry, index) => {
86
+ const item = entry;
87
+ if ((item.role !== 'user' && item.role !== 'assistant') ||
88
+ typeof item.toolName === 'string') {
89
+ return;
90
+ }
91
+ const text = plainWordsOf(item.content);
92
+ if (text) {
93
+ history.push({ id: String(item.id ?? index), role: item.role, text });
94
+ }
95
+ });
96
+ return history;
97
+ }
55
98
  /**
56
99
  * The newest thing the agent said, as the Office Assistant said it: in the
57
100
  * balloon, while the conversation is closed (T-23). Only its own messages
@@ -64,23 +107,7 @@ export function latestSaying(items) {
64
107
  if (item.role !== 'assistant' || typeof item.toolName === 'string') {
65
108
  continue;
66
109
  }
67
- const raw = typeof item.content === 'string'
68
- ? item.content
69
- : Array.isArray(item.content)
70
- ? item.content
71
- .map(part => part &&
72
- typeof part === 'object' &&
73
- part.type === 'text'
74
- ? String(part.text ?? '')
75
- : '')
76
- .join(' ')
77
- : '';
78
- const plain = raw
79
- .replace(/```[\s\S]*?```/g, ' ')
80
- .replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
81
- .replace(/[*_`#>]+/g, '')
82
- .replace(/\s+/g, ' ')
83
- .trim();
110
+ const plain = plainWordsOf(item.content);
84
111
  if (!plain) {
85
112
  continue;
86
113
  }
@@ -58,6 +58,8 @@ const EphemeralDocument = React.lazy(() => import('../document/EphemeralDocument
58
58
  default: m.EphemeralDocument,
59
59
  })));
60
60
  import { useNotebookTools } from '../../tools/adapters/agent-runtimes/notebookHooks';
61
+ import { useAgentInspectorSink } from '../../components/inspector/agentInspector';
62
+ import { chatInspectorRecorder, } from '../../components/inspector/chatInspect';
61
63
  // Tracks pending prompts already auto-sent for a given conversation scope.
62
64
  // This prevents layout-driven unmount/remount cycles from re-sending prompts.
63
65
  const sentPendingPromptKeys = new Set();
@@ -513,7 +515,7 @@ useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady
513
515
  // Extended props
514
516
  brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, voice, voiceSpeaking = false, onStopSpeaking, 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,
515
517
  // Tool invocation hooks
516
- onToolCallStart, onToolCallComplete,
518
+ onToolCallStart, onToolCallComplete, inspector: inspectorGiven,
517
519
  // Identity/Authorization props
518
520
  onAuthorizationRequired: _onAuthorizationRequired, connectedIdentities,
519
521
  // Conversation persistence
@@ -1589,6 +1591,25 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
1589
1591
  onToolCallStartRef.current = onToolCallStart;
1590
1592
  const onToolCallCompleteRef = useRef(onToolCallComplete);
1591
1593
  onToolCallCompleteRef.current = onToolCallComplete;
1594
+ // The Agent Inspector: the turns and tool calls, recorded as they happen.
1595
+ const inspectorFromContext = useAgentInspectorSink();
1596
+ const inspectorSink = inspectorGiven ?? inspectorFromContext;
1597
+ const inspectorActorRef = useRef('Agent');
1598
+ inspectorActorRef.current =
1599
+ (typeof title === 'string' && title) || activeAgentId || 'Agent';
1600
+ const recorderRef = useRef(null);
1601
+ if (inspectorSink && recorderRef.current?.sink !== inspectorSink) {
1602
+ recorderRef.current = {
1603
+ sink: inspectorSink,
1604
+ recorder: chatInspectorRecorder(inspectorSink, () => inspectorActorRef.current, {
1605
+ frontendTools: () => (frontendToolsRef.current ?? []).map(tool => tool.name),
1606
+ }),
1607
+ };
1608
+ }
1609
+ else if (!inspectorSink) {
1610
+ recorderRef.current = null;
1611
+ }
1612
+ const inspectAnswerRef = useRef('');
1592
1613
  const handleRespondRef = useRef(null);
1593
1614
  // A decision made outside the chat — a host page's own approval banner —
1594
1615
  // is answered exactly as the tool card's buttons answer it: through the
@@ -2415,6 +2436,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2415
2436
  const sanitizedContent = typeof rawContent === 'string'
2416
2437
  ? sanitizeAssistantContent(rawContent)
2417
2438
  : (rawContent ?? '');
2439
+ if (typeof sanitizedContent === 'string' && sanitizedContent) {
2440
+ inspectAnswerRef.current = sanitizedContent;
2441
+ }
2418
2442
  setDisplayItems(prev => {
2419
2443
  const newItems = [...prev];
2420
2444
  const idx = newItems.findIndex(item => !isToolCallMessage(item) && item.id === currentId);
@@ -2520,6 +2544,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2520
2544
  toolCallId,
2521
2545
  args,
2522
2546
  });
2547
+ recorderRef.current?.recorder.toolStarted({
2548
+ id: toolCallId,
2549
+ name: toolName,
2550
+ args,
2551
+ });
2523
2552
  const frontendTool = frontendToolsRef.current?.find(t => t.name === toolName);
2524
2553
  const toolHandler = frontendTool?.handler;
2525
2554
  // Execute when the args are complete. The protocol says so
@@ -2604,6 +2633,13 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2604
2633
  setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
2605
2634
  ? updatedToolCall
2606
2635
  : item));
2636
+ recorderRef.current?.recorder.toolEnded(toolCallId, {
2637
+ result: event.toolResult.result,
2638
+ error: event.toolResult.error ??
2639
+ (updatedToolCall.status === 'error'
2640
+ ? (executionError ?? 'failed')
2641
+ : undefined),
2642
+ });
2607
2643
  // Fire post-hook for tool results
2608
2644
  onToolCallCompleteRef.current?.({
2609
2645
  toolName: existingToolCall.toolName,
@@ -2657,6 +2693,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2657
2693
  * The bar and its ring simply never appeared for a browser agent.
2658
2694
  */
2659
2695
  {
2696
+ recorderRef.current?.recorder.turnEnded({
2697
+ text: inspectAnswerRef.current || undefined,
2698
+ usage: event.usage,
2699
+ });
2700
+ inspectAnswerRef.current = '';
2660
2701
  const turnInput = event.usage?.promptTokens ?? 0;
2661
2702
  const turnOutput = event.usage?.completionTokens ?? 0;
2662
2703
  // The parts sum to the total when the harness did not send one:
@@ -2710,6 +2751,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2710
2751
  break;
2711
2752
  case 'error':
2712
2753
  console.error('[ChatBase] Protocol error:', event.error);
2754
+ recorderRef.current?.recorder.turnEnded({
2755
+ error: event.error?.message ?? 'error',
2756
+ });
2713
2757
  if (event.error?.message &&
2714
2758
  /exceeded maximum retries/i.test(event.error.message) &&
2715
2759
  hideMessagesAfterToolUIRef.current) {
@@ -2777,6 +2821,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2777
2821
  result,
2778
2822
  status: 'complete',
2779
2823
  };
2824
+ recorderRef.current?.recorder.toolEnded(toolCallId, { result });
2780
2825
  toolCallsRef.current.set(toolCallId, completedToolCall);
2781
2826
  setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
2782
2827
  ? completedToolCall
@@ -2789,6 +2834,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2789
2834
  status: 'error',
2790
2835
  error: err.message,
2791
2836
  };
2837
+ recorderRef.current?.recorder.toolEnded(toolCallId, {
2838
+ error: err.message,
2839
+ });
2792
2840
  toolCallsRef.current.set(toolCallId, errorToolCall);
2793
2841
  setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
2794
2842
  ? errorToolCall
@@ -2838,6 +2886,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
2838
2886
  if (!adapterRef.current && !onSendMessage)
2839
2887
  return;
2840
2888
  stoppedRef.current = false;
2889
+ inspectAnswerRef.current = '';
2890
+ recorderRef.current?.recorder.turnStarted(messageContent, selectedModel || undefined);
2841
2891
  suppressAssistantTextForToolOnlyRef.current =
2842
2892
  isToolCallOnlyPrompt(messageContent);
2843
2893
  // Said, then sent: by the microphone at once, or from the composer it
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Three pulsing dots: a speaker is still at work and has not written yet —
3
+ * the chat's waiting indicator, and the assistant's balloon's. Still, but
4
+ * shown, for a reader who asks for reduced motion. Hidden from a screen
5
+ * reader: the live region beside it says the state.
6
+ *
7
+ * @module chat/indicators/TypingDots
8
+ */
9
+ import type { ReactElement } from 'react';
10
+ export declare function TypingDots({ size }: {
11
+ size?: number;
12
+ }): ReactElement;
13
+ export default TypingDots;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box } from '@datalayer/primer-addons';
3
+ export function TypingDots({ size = 8 }) {
4
+ return (_jsx(Box, { "aria-hidden": "true", "data-typing-dots": "", sx: { display: 'inline-flex', alignItems: 'center', gap: '4px' }, children: [0, 0.2, 0.4].map((delay, index) => (_jsx(Box, { sx: {
5
+ width: size,
6
+ height: size,
7
+ borderRadius: '50%',
8
+ bg: 'fg.muted',
9
+ animation: 'typingPulse 1.4s ease-in-out infinite',
10
+ animationDelay: `${delay}s`,
11
+ '@keyframes typingPulse': {
12
+ '0%, 60%, 100%': { transform: 'scale(0.6)', opacity: 0.4 },
13
+ '30%': { transform: 'scale(1)', opacity: 1 },
14
+ },
15
+ '@media (prefers-reduced-motion: reduce)': {
16
+ animation: 'none',
17
+ opacity: 0.7,
18
+ },
19
+ } }, index))) }));
20
+ }
21
+ export default TypingDots;
@@ -18,6 +18,7 @@ import { Streamdown } from 'streamdown';
18
18
  import { streamdownMarkdownStyles, streamdownCodeBlockStyles, } from '../styles/streamdownStyles';
19
19
  import { ToolCallDisplay } from '../tools/ToolCallDisplay';
20
20
  import { marksOfToolCall, skillIdOfCall } from '../marks';
21
+ import { TypingDots } from '../indicators/TypingDots';
21
22
  import { TurnFooter } from './TurnFooter';
22
23
  import { normalizeAssistantMarkdown } from './assistantMarkdown';
23
24
  import { parseFormSubmission } from './formSubmission';
@@ -62,21 +63,6 @@ function toneColorsOf(tone) {
62
63
  fg: `${tone}.fg`,
63
64
  };
64
65
  }
65
- /** Three pulsing dots: a speaker is still writing and has not settled yet. */
66
- function TypingDots({ size = 8 }) {
67
- return (_jsx(Box, { sx: { display: 'flex', alignItems: 'center', gap: '4px' }, children: [0, 0.2, 0.4].map((delay, index) => (_jsx(Box, { sx: {
68
- width: size,
69
- height: size,
70
- borderRadius: '50%',
71
- bg: 'fg.muted',
72
- animation: 'typingPulse 1.4s ease-in-out infinite',
73
- animationDelay: `${delay}s`,
74
- '@keyframes typingPulse': {
75
- '0%, 60%, 100%': { transform: 'scale(0.6)', opacity: 0.4 },
76
- '30%': { transform: 'scale(1)', opacity: 1 },
77
- },
78
- } }, index))) }));
79
- }
80
66
  const SUBAGENT_AVATAR_CONFIG = {
81
67
  userAvatar: _jsx(PersonIcon, { size: 14 }),
82
68
  assistantAvatar: _jsx(DependabotIcon, { size: 14 }),
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The Agent Inspector: what agents do, one row a thing, newest last — the
3
+ * time (to the millisecond, and since the first), who did it and toward
4
+ * whom, its source and kind, its name with its mark, its status and
5
+ * duration, and a line that says it. A row unfolds to its payload as a JSON
6
+ * tree (a call's arguments and its result), and to what was made of it.
7
+ *
8
+ * Filters by source, kind and actor; *Pause* holds the rows still while the
9
+ * record goes on, *Clear* empties it, *Export* saves the session as JSON. A
10
+ * large payload (a notebook) says its size and stays folded until asked.
11
+ *
12
+ * Fed by a sink (`useAgentInspector`) or given its entries.
13
+ *
14
+ * @module components/inspector/AgentInspector
15
+ */
16
+ import type { JSX } from 'react';
17
+ import { type AgentInspectorEntry, type AgentInspectorExport, type AgentInspectorSink, type AgentInspectorSource } from './agentInspector';
18
+ export type AgentInspectorProps = {
19
+ /** The record to show, kept current. */
20
+ sink?: AgentInspectorSink | null;
21
+ /** Or its entries, as given. */
22
+ entries?: readonly AgentInspectorEntry[];
23
+ /** How tall the rows grow before they scroll. */
24
+ maxHeight?: number | string;
25
+ /** Payloads larger than this, in bytes, stay folded until asked. */
26
+ largeBytes?: number;
27
+ /** What is said while nothing is recorded. */
28
+ emptyText?: string;
29
+ /** The file name the session is exported as, without `.json`. */
30
+ exportName?: string;
31
+ /**
32
+ * One agent's own record: what it did (`actor`), and the A2A messages it
33
+ * sent or received — one message shows in the sender's and in the
34
+ * receiver's, the same entry, as sent in one and received in the other.
35
+ */
36
+ agent?: string;
37
+ };
38
+ /** Whether an entry is one agent's: done by it, or sent to or by it. */
39
+ export declare function isAgentsEntry(entry: AgentInspectorEntry, agent: string): boolean;
40
+ /** Which way an entry went, from one agent's side: `→ Accounting`, `← Accounting`. */
41
+ export declare function directionFrom(entry: AgentInspectorEntry, agent: string | undefined): string;
42
+ /** A time of day, to the millisecond: `15:44:31.299`. */
43
+ export declare function clockTime(at: number): string;
44
+ /** Time since the first entry: `+1.204s`. */
45
+ export declare function sinceFirst(at: number, first: number): string;
46
+ /** How long a call took: `412 ms`, `3.4 s`. */
47
+ export declare function durationText(ms: number): string;
48
+ export type AgentInspectorFilter = {
49
+ sources: ReadonlySet<AgentInspectorSource>;
50
+ kinds: ReadonlySet<string>;
51
+ actors: ReadonlySet<string>;
52
+ };
53
+ /** The entries a filter keeps: an empty set keeps every value. */
54
+ export declare function filterEntries(entries: readonly AgentInspectorEntry[], filter: AgentInspectorFilter): AgentInspectorEntry[];
55
+ /** A session, as JSON. */
56
+ export declare function exportEntries(entries: readonly AgentInspectorEntry[]): AgentInspectorExport;
57
+ /** The Agent Inspector. */
58
+ export declare function AgentInspector({ sink, entries: given, maxHeight, largeBytes, emptyText, exportName, agent, }: AgentInspectorProps): JSX.Element;
59
+ export default AgentInspector;