@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,10 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { Suspense, lazy, useCallback, useEffect, useRef, useState, } from 'react';
3
3
  import { ActionList, ActionMenu, IconButton, useTheme } from '@primer/react';
4
4
  import { Box } from '@datalayer/primer-addons';
5
- import { XIcon } from '@primer/octicons-react';
5
+ import { CommentDiscussionIcon, InfoIcon, MuteIcon, PencilIcon, PlusIcon, PulseIcon, ScreenNormalIcon, SquareFillIcon, TrashIcon, UnmuteIcon, XIcon, } from '@primer/octicons-react';
6
+ import { Dialog, Text } from '@primer/react';
6
7
  import { assistantCharacter } from './characters';
7
- import { SpeechBalloon } from './SpeechBalloon';
8
+ import { BalloonHistoryLarge, SpeechBalloon, } from './SpeechBalloon';
9
+ import { conversationHeaderText } from './BalloonParts';
10
+ import { AssistantContextMenu, LONG_PRESS_MS, opensContextMenu, } from './AssistantContextMenu';
11
+ import { BalloonExpandContext, ExpandedVisual, focusExpanded, } from './BalloonVisual';
8
12
  import { SpriteCharacter } from './SpriteCharacter';
9
13
  import { ASSISTANT_OBSTACLES, POINTER_CALM_MS, boxesMeet, pointerNear, } from './state';
10
14
  /** The motions, by the state the stage is in. */
@@ -57,6 +61,128 @@ const AWAY_CHOICES = [
57
61
  { away: 'session', label: 'Hide for this session' },
58
62
  { away: 'always', label: 'Don\u2019t show again' },
59
63
  ];
64
+ /** The Agent Inspector's dialog: loaded when it is first asked for. */
65
+ const AgentInspectorDialog = lazy(() => import('../../components/inspector/AgentInspectorDialog'));
66
+ /**
67
+ * The entries of the assistant's menu, composed from what the host passes:
68
+ * each shows when what it does is there to do.
69
+ */
70
+ export function assistantMenuItems(options) {
71
+ const items = [];
72
+ if (options.inspect) {
73
+ items.push({
74
+ id: 'inspect',
75
+ label: 'Inspect the agent…',
76
+ icon: PulseIcon,
77
+ onSelect: options.inspect,
78
+ });
79
+ }
80
+ if (options.conversationLabel !== false) {
81
+ items.push({
82
+ id: 'conversation',
83
+ label: options.conversationLabel ??
84
+ (options.open ? 'Close the conversation' : 'Open the conversation'),
85
+ icon: CommentDiscussionIcon,
86
+ onSelect: options.onToggle,
87
+ });
88
+ }
89
+ if (options.busy && options.onStop) {
90
+ items.push({
91
+ id: 'stop',
92
+ label: 'Stop the turn',
93
+ icon: SquareFillIcon,
94
+ onSelect: options.onStop,
95
+ });
96
+ }
97
+ if (options.onNewChat) {
98
+ items.push({
99
+ id: 'new-chat',
100
+ label: 'New conversation',
101
+ icon: PlusIcon,
102
+ onSelect: options.onNewChat,
103
+ });
104
+ }
105
+ if (options.onClear) {
106
+ items.push({
107
+ id: 'clear',
108
+ label: 'Clear the conversation',
109
+ icon: TrashIcon,
110
+ onSelect: options.onClear,
111
+ });
112
+ }
113
+ if (options.suggestions?.length && options.onSuggestion) {
114
+ const send = options.onSuggestion;
115
+ for (const suggestion of options.suggestions) {
116
+ items.push({
117
+ id: `suggestion:${suggestion.label}`,
118
+ label: suggestion.label,
119
+ description: suggestion.prompt,
120
+ group: 'Suggestions',
121
+ disabled: options.busy,
122
+ onSelect: () => send(suggestion),
123
+ });
124
+ }
125
+ }
126
+ if (options.onBalloonDisplayChange) {
127
+ const change = options.onBalloonDisplayChange;
128
+ items.push({
129
+ id: 'balloon-history',
130
+ label: 'History',
131
+ group: 'Balloon',
132
+ checked: options.balloonDisplay !== 'current',
133
+ onSelect: () => change('history'),
134
+ }, {
135
+ id: 'balloon-current',
136
+ label: 'Current',
137
+ group: 'Balloon',
138
+ checked: options.balloonDisplay === 'current',
139
+ onSelect: () => change('current'),
140
+ });
141
+ }
142
+ if (options.onChangeCharacter) {
143
+ items.push({
144
+ id: 'character',
145
+ label: 'Change character…',
146
+ icon: PencilIcon,
147
+ onSelect: options.onChangeCharacter,
148
+ });
149
+ }
150
+ if (options.speech) {
151
+ items.push({
152
+ id: 'speech',
153
+ label: options.speech.muted ? 'Unmute speech' : 'Mute speech',
154
+ icon: options.speech.muted ? UnmuteIcon : MuteIcon,
155
+ onSelect: options.speech.onToggle,
156
+ });
157
+ }
158
+ if (options.onResetPosition) {
159
+ items.push({
160
+ id: 'reset-position',
161
+ label: 'Reset position',
162
+ icon: ScreenNormalIcon,
163
+ onSelect: options.onResetPosition,
164
+ });
165
+ }
166
+ if (options.about) {
167
+ items.push({
168
+ id: 'about',
169
+ label: `About ${options.name}`,
170
+ icon: InfoIcon,
171
+ onSelect: options.about,
172
+ });
173
+ }
174
+ items.push(...(options.extra ?? []));
175
+ for (const choice of AWAY_CHOICES) {
176
+ items.push({
177
+ id: `away:${choice.away}`,
178
+ label: choice.label,
179
+ group: 'Send away',
180
+ icon: XIcon,
181
+ onSelect: () => options.onDismiss(choice.away),
182
+ });
183
+ }
184
+ return items;
185
+ }
60
186
  /** Room the balloon needs above the character before it goes below. */
61
187
  const BALLOON_ROOM = 200;
62
188
  /** A place's length in pixels, given as a number or as `"<n>px"`. */
@@ -209,7 +335,7 @@ export function useMouth(stageRef, mouthLevel, speaking) {
209
335
  };
210
336
  }, [stageRef, mouthLevel, speaking]);
211
337
  }
212
- export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, decide, mouthLevel, stayPut = false, balloonDisplay = 'history', }) {
338
+ export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, decide, mouthLevel, stayPut = false, balloonDisplay = 'history', expandTarget, expandOnArrival = false, suggestions, onSuggestion, inspector, inspectAgent, conversationLabel, onBalloonDisplayChange, onStop, onNewChat, onClear, onChangeCharacter, speech, onResetPosition, about, contextMenu, }) {
213
339
  // A shipped one by id, a drawing contributed by a plugin (T-24), or a
214
340
  // character read from a file (T-26).
215
341
  const shipped = typeof character === 'string'
@@ -225,13 +351,76 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
225
351
  const colorMode = colorScheme?.startsWith('dark') ? 'dark' : 'light';
226
352
  const [hovered, setHovered] = useState(false);
227
353
  const [menuOpen, setMenuOpen] = useState(false);
354
+ // Its own menu, where it was opened; the inspector and *About* it opens.
355
+ const [menuAt, setMenuAt] = useState(null);
356
+ const [inspecting, setInspecting] = useState(false);
357
+ const [telling, setTelling] = useState(false);
358
+ const characterRef = useRef(null);
359
+ const longPress = useRef(undefined);
360
+ const closeMenu = useCallback(() => setMenuAt(null), []);
361
+ useEffect(() => () => clearTimeout(longPress.current), []);
228
362
  // Where the press began: a press that moves is a drag, not a click.
229
363
  const pressedAt = useRef(null);
230
- const aside = useKeepClear(stageRef, ownRef, menuOpen || stayPut);
364
+ const aside = useKeepClear(stageRef, ownRef, menuOpen || menuAt !== null || inspecting || telling || stayPut);
231
365
  useMouth(stageRef, mouthLevel, state === 'speaking');
232
366
  // A decision being asked, or its answer, keeps the balloon up.
233
367
  const [deciding, setDeciding] = useState(false);
234
368
  const showBalloon = !aside && !open && !!balloon && (hovered || insist || deciding);
369
+ // Suggestions while it waits for a question.
370
+ const waitsForQuestion = (state === 'idle' || state === 'greeting') &&
371
+ !balloon?.tool &&
372
+ !balloon?.approval &&
373
+ !balloon?.busy;
374
+ const busy = state === 'thinking' || state === 'working' || state === 'speaking';
375
+ const menuItems = assistantMenuItems({
376
+ name,
377
+ open,
378
+ onToggle,
379
+ onDismiss,
380
+ conversationLabel,
381
+ inspect: inspector ? () => setInspecting(true) : undefined,
382
+ balloonDisplay,
383
+ onBalloonDisplayChange,
384
+ suggestions,
385
+ onSuggestion,
386
+ busy,
387
+ onStop,
388
+ onNewChat,
389
+ onClear,
390
+ onChangeCharacter,
391
+ speech,
392
+ onResetPosition,
393
+ about: about ? () => setTelling(true) : undefined,
394
+ extra: contextMenu,
395
+ });
396
+ // The large visual drawn now: kept while the balloon moves on to other
397
+ // words, until it is closed or another one is expanded.
398
+ const visual = balloon?.visual;
399
+ const [expanded, setExpanded] = useState(null);
400
+ const expand = visual
401
+ ? () => {
402
+ setExpanded(visual);
403
+ if (expandTarget) {
404
+ requestAnimationFrame(() => focusExpanded(expandTarget));
405
+ }
406
+ }
407
+ : null;
408
+ // The history, large: drawn from the balloon's messages as they are now.
409
+ const history = balloon?.history;
410
+ const historyVisual = balloonDisplay !== 'current' && history && history.length > 0
411
+ ? {
412
+ id: 'balloon-history',
413
+ title: conversationHeaderText(history.length),
414
+ shrink: true,
415
+ render: () => (_jsx(BalloonHistoryLarge, { history: history, tool: balloon?.tool, attachment: balloon?.attachment })),
416
+ }
417
+ : null;
418
+ const shown = expanded?.id === 'balloon-history' ? historyVisual : expanded;
419
+ useEffect(() => {
420
+ if (visual && expandOnArrival && expandTarget) {
421
+ setExpanded(visual);
422
+ }
423
+ }, [visual?.id, expandOnArrival, !!expandTarget]);
235
424
  return (_jsxs(Box, { ref: stageRef, "data-assistant-state": state, "data-assistant-aside": aside, "data-assistant-balloon": balloonDisplay, sx: {
236
425
  position: 'fixed',
237
426
  zIndex: 1002,
@@ -331,8 +520,43 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
331
520
  '@media (prefers-reduced-motion: reduce)': {
332
521
  '& *': { animation: 'none !important' },
333
522
  },
334
- }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, onDismissPeek: balloon.onDismiss, display: balloonDisplay, tool: balloon.tool, speaking: balloon.speaking, busy: balloon.busy, attachment: balloon.attachment, decide: decide, onDecisionActive: setDeciding, wide: deciding, onOpen: onToggle, above: size + 8, side: balloonSide(place).side, align: balloonSide(place).align, tailAt: size / 2 })), _jsx(Box, { as: "button", type: "button", "aria-label": open ? `Close the conversation with ${name}` : `Talk to ${name}`, "aria-expanded": open, onPointerDown: (event) => {
523
+ }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [shown && (_jsx(ExpandedVisual, { visual: shown, target: expandTarget, onClose: () => setExpanded(null) })), showBalloon && balloon && (_jsx(BalloonExpandContext.Provider, { value: expand, children: _jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, onDismissPeek: balloon.onDismiss, display: balloonDisplay, tool: balloon.tool, speaking: balloon.speaking, busy: balloon.busy, attachment: balloon.attachment, history: balloon.history, onExpand: expand ?? undefined, expandTitle: visual?.title, onExpandHistory: historyVisual
524
+ ? () => {
525
+ setExpanded(historyVisual);
526
+ if (expandTarget) {
527
+ requestAnimationFrame(() => focusExpanded(expandTarget));
528
+ }
529
+ }
530
+ : undefined, suggestions: waitsForQuestion ? suggestions : undefined, fullText: balloon.fullText, waiting: (state === 'thinking' ||
531
+ state === 'working' ||
532
+ state === 'waiting') &&
533
+ !balloon.speaking, onSuggestion: onSuggestion, decide: decide, onDecisionActive: setDeciding, wide: deciding, onOpen: onToggle, above: size + 8, side: balloonSide(place).side, align: balloonSide(place).align, tailAt: size / 2 }) })), _jsx(Box, { as: "button", type: "button", "aria-label": open ? `Close the conversation with ${name}` : `Talk to ${name}`, "aria-expanded": open, "aria-haspopup": "menu", ref: characterRef, "data-assistant-figure": "", onContextMenu: (event) => {
534
+ event.preventDefault();
535
+ clearTimeout(longPress.current);
536
+ setMenuAt({ x: event.clientX, y: event.clientY });
537
+ }, onKeyDown: (event) => {
538
+ if (opensContextMenu(event)) {
539
+ event.preventDefault();
540
+ const box = event.currentTarget.getBoundingClientRect();
541
+ setMenuAt({ x: box.left + box.width / 2, y: box.bottom });
542
+ }
543
+ }, onPointerUp: () => clearTimeout(longPress.current), onPointerCancel: () => clearTimeout(longPress.current), onPointerMove: (event) => {
544
+ const from = pressedAt.current;
545
+ if (from &&
546
+ Math.hypot(event.clientX - from.x, event.clientY - from.y) > 8) {
547
+ clearTimeout(longPress.current);
548
+ }
549
+ }, onPointerDown: (event) => {
335
550
  pressedAt.current = { x: event.clientX, y: event.clientY };
551
+ if (event.pointerType === 'touch') {
552
+ const at = { x: event.clientX, y: event.clientY };
553
+ clearTimeout(longPress.current);
554
+ longPress.current = setTimeout(() => {
555
+ // Held, not tapped: the menu, and no click after it.
556
+ pressedAt.current = { x: -1e6, y: -1e6 };
557
+ setMenuAt(at);
558
+ }, LONG_PRESS_MS);
559
+ }
336
560
  onDragStart(event);
337
561
  }, onClick: (event) => {
338
562
  const from = pressedAt.current;
@@ -363,6 +587,20 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
363
587
  bg: 'canvas.default',
364
588
  borderRadius: '50%',
365
589
  boxShadow: 'shadow.small',
366
- } }) }), _jsx(ActionMenu.Overlay, { width: "auto", children: _jsx(ActionList, { children: AWAY_CHOICES.map(choice => (_jsx(ActionList.Item, { "data-assistant-away": choice.away, onSelect: () => onDismiss(choice.away), children: choice.label }, choice.away))) }) })] }) }))] }));
590
+ } }) }), _jsx(ActionMenu.Overlay, { width: "auto", children: _jsx(ActionList, { children: AWAY_CHOICES.map(choice => (_jsx(ActionList.Item, { "data-assistant-away": choice.away, onSelect: () => onDismiss(choice.away), children: choice.label }, choice.away))) }) })] }) })), _jsx(AssistantContextMenu, { at: menuAt, items: menuItems, onClose: closeMenu, returnFocusRef: characterRef, label: `${name}\u2019s menu` }), inspecting && (_jsx(Suspense, { fallback: null, children: _jsx(AgentInspectorDialog, { sink: inspector ?? null, agent: inspectAgent, title: `${about?.name ?? name} \u00b7 Agent Inspector`, onClose: () => {
591
+ setInspecting(false);
592
+ characterRef.current?.focus();
593
+ } }) })), telling && about && (_jsx(Dialog, { title: `About ${about.name ?? name}`, onClose: () => {
594
+ setTelling(false);
595
+ characterRef.current?.focus();
596
+ }, "data-assistant-about": "", children: [
597
+ ['Agent', about.name],
598
+ ['Spec', about.spec],
599
+ ['Model', about.model],
600
+ ['Runs', about.where],
601
+ ['Character', name],
602
+ ]
603
+ .filter(([, value]) => value)
604
+ .map(([key, value]) => (_jsxs(Text, { as: "p", sx: { m: 0, mb: 1 }, children: [_jsxs(Text, { sx: { color: 'fg.muted' }, children: [key, ": "] }), value] }, key))) }))] }));
367
605
  }
368
606
  export default AssistantStage;
@@ -45,10 +45,24 @@ export declare function ConversationBalloonHeader({ count, }: {
45
45
  * done — the tool line while a tool runs, else the words — cut to a few
46
46
  * lines, never scrolled; and what goes with them (`attachment`).
47
47
  */
48
- export declare function CurrentBalloonBody({ text, tool, busy, speaking, attachment, }: {
48
+ export declare function CurrentBalloonBody({ text, tool, busy, speaking, attachment, waiting, whole, fullText, onOverflow, }: {
49
+ /**
50
+ * Shown whole: the full text (`fullText`, else `text`), unclamped, in a
51
+ * taller box that scrolls.
52
+ */
53
+ whole?: boolean;
54
+ /** The text uncut, when `text` was cut to fit (`…`). */
55
+ fullText?: string;
56
+ /** Whether the clamped text overflows its lines: *more* has more to show. */
57
+ onOverflow?: (overflowing: boolean) => void;
49
58
  text?: string;
50
59
  tool?: BalloonToolLine;
51
60
  busy?: boolean;
61
+ /**
62
+ * At work with nothing written yet (thinking, working, waiting): the
63
+ * chat's three dots follow the line, or stand alone.
64
+ */
65
+ waiting?: boolean;
52
66
  /** Words are arriving: read out once they have. */
53
67
  speaking?: boolean;
54
68
  attachment?: ReactNode;
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
3
3
  import { Text } from '@primer/react';
4
4
  import { Box } from '@datalayer/primer-addons';
5
5
  import { CheckIcon, ToolsIcon, XIcon } from '@primer/octicons-react';
6
6
  import { SpecMark, hasMark, marksOfToolCall } from '../marks';
7
+ import { TypingDots } from '../indicators/TypingDots';
7
8
  import { toolAnnouncement, toolLineParts, } from './toolLine';
8
9
  /** Out of sight, still heard. */
9
10
  const VISUALLY_HIDDEN = {
@@ -129,12 +130,28 @@ export function ConversationBalloonHeader({ count, }) {
129
130
  * done — the tool line while a tool runs, else the words — cut to a few
130
131
  * lines, never scrolled; and what goes with them (`attachment`).
131
132
  */
132
- export function CurrentBalloonBody({ text, tool, busy = false, speaking = false, attachment, }) {
133
- return (_jsxs(Box, { "data-balloon-current": "", sx: { display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }, children: [_jsx(BalloonNow, { busy: busy }), tool ? (_jsx(BalloonToolLineView, { line: tool })) : text ? (_jsx(Box, { "aria-live": "polite", "aria-busy": speaking, "data-balloon-current-text": "", sx: {
134
- display: '-webkit-box',
135
- WebkitLineClamp: 4,
136
- WebkitBoxOrient: 'vertical',
137
- overflow: 'hidden',
138
- overflowWrap: 'anywhere',
139
- }, children: text })) : null, attachment, _jsx(ToolLineAnnouncer, { line: tool })] }));
133
+ export function CurrentBalloonBody({ text, tool, busy = false, speaking = false, attachment, waiting = false, whole = false, fullText, onOverflow, }) {
134
+ const textRef = useRef(null);
135
+ useLayoutEffect(() => {
136
+ const element = textRef.current;
137
+ if (!element || whole || !onOverflow) {
138
+ return;
139
+ }
140
+ onOverflow(element.scrollHeight > element.clientHeight + 1);
141
+ }, [text, whole, onOverflow]);
142
+ return (_jsxs(Box, { "data-balloon-current": "", sx: { display: 'flex', flexDirection: 'column', gap: 1, minWidth: 0 }, children: [_jsx(BalloonNow, { busy: busy }), tool ? (_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(BalloonToolLineView, { line: tool }), waiting && _jsx(TypingDots, { size: 5 })] })) : text ? (_jsxs(Box, { "aria-live": "polite", "aria-busy": speaking, ref: textRef, "data-balloon-current-text": whole ? 'whole' : '', sx: whole
143
+ ? {
144
+ maxHeight: 240,
145
+ overflowY: 'auto',
146
+ overflowWrap: 'anywhere',
147
+ whiteSpace: 'pre-wrap',
148
+ pr: 1,
149
+ }
150
+ : {
151
+ display: '-webkit-box',
152
+ WebkitLineClamp: 4,
153
+ WebkitBoxOrient: 'vertical',
154
+ overflow: 'hidden',
155
+ overflowWrap: 'anywhere',
156
+ }, children: [whole ? (fullText ?? text) : text, waiting && (_jsx(Box, { as: "span", sx: { ml: 2, display: 'inline-flex' }, children: _jsx(TypingDots, { size: 5 }) }))] })) : waiting ? (_jsx(TypingDots, { size: 6 })) : null, attachment, _jsx(ToolLineAnnouncer, { line: tool })] }));
140
157
  }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * A large visual a balloon carries (LOOP T-23): a notebook today, a chart or
3
+ * a table later. The balloon shows it compact — its `attachment`, such as
4
+ * the read-only preview of a notebook — with an *Expand* button; expanded,
5
+ * the visual is drawn large: into the element the host names
6
+ * (`expandTarget`, through a portal), or, when it names none, in a large
7
+ * dialog over the page — Esc closes it, and the focus stays in it while it
8
+ * is open.
9
+ *
10
+ * What the expanded visual is, the visual says (`render`): for a notebook,
11
+ * the one that runs and is edited on the browser sandbox (`TeamNotebook`).
12
+ *
13
+ * @module chat/assistant/BalloonVisual
14
+ */
15
+ import type { JSX, ReactNode, RefObject } from 'react';
16
+ /** A large visual a balloon carries, and how it is drawn large. */
17
+ export interface BalloonVisual {
18
+ /** Its id: a new one is a new visual. */
19
+ id: string;
20
+ /** What it is: `Accounting's notebook`; the dialog's title. */
21
+ title: string;
22
+ /**
23
+ * The visual, large: drawn only once it is expanded — in the page's
24
+ * target, or in a dialog, which already shows `title`.
25
+ */
26
+ render: (place: 'target' | 'overlay') => ReactNode;
27
+ /**
28
+ * Large, it shrinks back into the balloon (*Shrink*) rather than closes:
29
+ * the balloon's own content drawn large, such as its history.
30
+ */
31
+ shrink?: boolean;
32
+ }
33
+ /** Where an expanded visual goes: an element of the page, or none (a dialog). */
34
+ export type BalloonExpandTarget = RefObject<HTMLElement | null>;
35
+ /**
36
+ * Expands the visual of the balloon it is in: what its compact form (a
37
+ * notebook's preview) calls when it is clicked. None outside such a balloon.
38
+ */
39
+ export declare const BalloonExpandContext: import("react").Context<(() => void) | null>;
40
+ /** The balloon's way to expand its visual, if it carries one. */
41
+ export declare function useBalloonExpand(): (() => void) | null;
42
+ /** The balloon's *Expand* button. */
43
+ export declare function BalloonExpandButton({ title, onExpand, }: {
44
+ title: string;
45
+ onExpand: () => void;
46
+ }): JSX.Element;
47
+ /** How large the dialog is when no target is named: most of the window. */
48
+ export declare const EXPANDED_DIALOG_SIZE: {
49
+ readonly width: "80vw";
50
+ readonly height: "80vh";
51
+ };
52
+ /**
53
+ * The visual, expanded: into `target` when it names an element, else in a
54
+ * large dialog over the page, closed with Esc or its close button.
55
+ */
56
+ export declare function ExpandedVisual({ visual, target, onClose, }: {
57
+ visual: BalloonVisual;
58
+ target?: BalloonExpandTarget;
59
+ onClose: () => void;
60
+ }): JSX.Element | null;
61
+ /**
62
+ * Brings an expanded visual into view in its target: scrolled to, and the
63
+ * first element that takes the focus there (a notebook's section) focused.
64
+ */
65
+ export declare function focusExpanded(target: BalloonExpandTarget | undefined): void;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useContext } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { Button, Dialog, IconButton } from '@primer/react';
5
+ import { Box } from '@datalayer/primer-addons';
6
+ import { ScreenFullIcon, ScreenNormalIcon, XIcon, } from '@primer/octicons-react';
7
+ /**
8
+ * Expands the visual of the balloon it is in: what its compact form (a
9
+ * notebook's preview) calls when it is clicked. None outside such a balloon.
10
+ */
11
+ export const BalloonExpandContext = createContext(null);
12
+ /** The balloon's way to expand its visual, if it carries one. */
13
+ export function useBalloonExpand() {
14
+ return useContext(BalloonExpandContext);
15
+ }
16
+ /** The balloon's *Expand* button. */
17
+ export function BalloonExpandButton({ title, onExpand, }) {
18
+ return (_jsx(Button, { size: "small", variant: "invisible", leadingVisual: ScreenFullIcon, "aria-label": `Expand ${title}`, "data-balloon-expand": "", onClick: onExpand, sx: { alignSelf: 'flex-start', px: 1 }, children: "Expand" }));
19
+ }
20
+ /** How large the dialog is when no target is named: most of the window. */
21
+ export const EXPANDED_DIALOG_SIZE = { width: '80vw', height: '80vh' };
22
+ /**
23
+ * The visual, expanded: into `target` when it names an element, else in a
24
+ * large dialog over the page, closed with Esc or its close button.
25
+ */
26
+ export function ExpandedVisual({ visual, target, onClose, }) {
27
+ const element = target?.current;
28
+ if (target) {
29
+ if (!element) {
30
+ return null;
31
+ }
32
+ return createPortal(_jsxs(Box, { "data-balloon-expanded": "target", "data-balloon-visual": visual.id, sx: { position: 'relative', minWidth: 0 }, children: [_jsx(Box, { sx: { position: 'absolute', top: 0, right: 0, zIndex: 1 }, children: _jsx(IconButton, { icon: visual.shrink ? ScreenNormalIcon : XIcon, size: "small", variant: "invisible", "aria-label": `${visual.shrink ? 'Shrink' : 'Close'} ${visual.title}`, "data-balloon-expanded-close": "", ...(visual.shrink ? { 'data-balloon-shrink': '' } : {}), onClick: onClose }) }), visual.render('target')] }), element);
33
+ }
34
+ return (_jsx(Dialog, { title: visual.title, onClose: onClose, sx: {
35
+ width: EXPANDED_DIALOG_SIZE.width,
36
+ maxWidth: EXPANDED_DIALOG_SIZE.width,
37
+ height: EXPANDED_DIALOG_SIZE.height,
38
+ maxHeight: EXPANDED_DIALOG_SIZE.height,
39
+ }, children: _jsxs(Box, { "data-balloon-expanded": "overlay", "data-balloon-visual": visual.id, sx: { minWidth: 0 }, children: [visual.shrink && (_jsx(Button, { size: "small", variant: "invisible", leadingVisual: ScreenNormalIcon, onClick: onClose, "data-balloon-shrink": "", sx: { float: 'right' }, children: "Shrink" })), visual.render('overlay')] }) }));
40
+ }
41
+ /**
42
+ * Brings an expanded visual into view in its target: scrolled to, and the
43
+ * first element that takes the focus there (a notebook's section) focused.
44
+ */
45
+ export function focusExpanded(target) {
46
+ const element = target?.current;
47
+ if (!element) {
48
+ return;
49
+ }
50
+ const still = typeof window !== 'undefined' &&
51
+ !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
52
+ element.scrollIntoView?.({
53
+ behavior: still ? 'auto' : 'smooth',
54
+ block: 'start',
55
+ });
56
+ const focusable = element.querySelector('[tabindex="-1"]') ?? element;
57
+ focusable.focus?.({ preventScroll: true });
58
+ }
@@ -9,6 +9,13 @@
9
9
  * the conversation is elsewhere (the LOOP workspace), *Ask a decision*: a
10
10
  * typed decision asked of Jev, its answer said back in the balloon.
11
11
  *
12
+ * Shown `history` with the conversation's messages (`history`), it lists
13
+ * them all — the person's and the agent's, under a header that counts them,
14
+ * scrolled to the newest — rather than the newest line alone.
15
+ *
16
+ * Given suggestions while the agent waits for a question, it offers them as
17
+ * chips under its words: one clicked is sent as the person's prompt.
18
+ *
12
19
  * Shown `current` (LOOP T-23), it is the one thing being said or done now:
13
20
  * *Now*, then the words as they are written, or the tool being called
14
21
  * ("Using **list_invoices**…"), cut to a few lines, and what goes with them
@@ -19,9 +26,19 @@
19
26
  * @module chat/assistant/SpeechBalloon
20
27
  */
21
28
  import type { JSX, ReactNode } from 'react';
22
- import { type BalloonApproval } from './state';
29
+ import { type BalloonApproval, type BalloonHistoryMessage } from './state';
23
30
  import type { DecisionAsker } from './decisions';
24
31
  import type { BalloonDisplay, BalloonToolLine } from './toolLine';
32
+ /** A suggestion the balloon offers: its label, and the prompt it sends. */
33
+ export type BalloonSuggestion = {
34
+ label: string;
35
+ prompt: string;
36
+ };
37
+ /** The suggestions, as chips: each sends its prompt. */
38
+ export declare function BalloonSuggestions({ suggestions, onSuggestion, }: {
39
+ suggestions: readonly BalloonSuggestion[];
40
+ onSuggestion: (suggestion: BalloonSuggestion) => void;
41
+ }): JSX.Element;
25
42
  export interface SpeechBalloonProps {
26
43
  text: string;
27
44
  /** There is more than the balloon holds. */
@@ -59,6 +76,55 @@ export interface SpeechBalloonProps {
59
76
  busy?: boolean;
60
77
  /** What goes with the words, under them: a notebook given, read-only. */
61
78
  attachment?: ReactNode;
79
+ /**
80
+ * The conversation's messages (`history`): listed in the balloon, every
81
+ * one, in place of the newest line alone.
82
+ */
83
+ history?: readonly BalloonHistoryMessage[];
84
+ /**
85
+ * What goes with the words is a large visual (a notebook): an *Expand*
86
+ * button under it draws it large, named by `expandTitle`.
87
+ */
88
+ onExpand?: () => void;
89
+ /** What the large visual is, for the *Expand* button: `Accounting's notebook`. */
90
+ expandTitle?: string;
91
+ /** Draws the listed history large (`history` display): its *Expand*. */
92
+ onExpandHistory?: () => void;
93
+ /**
94
+ * What the person may ask, as chips under the words: one clicked is sent
95
+ * as the prompt (`onSuggestion`), as if typed and asked.
96
+ */
97
+ suggestions?: readonly BalloonSuggestion[];
98
+ onSuggestion?: (suggestion: BalloonSuggestion) => void;
99
+ /**
100
+ * At work with nothing written yet: the chat's three dots — after the
101
+ * current line, or last in the history — until words arrive.
102
+ */
103
+ waiting?: boolean;
104
+ /**
105
+ * The words uncut, when `text` was cut to fit: *more* shows them whole,
106
+ * in the balloon, *less* folds them back.
107
+ */
108
+ fullText?: string;
62
109
  }
63
- export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, decide, onDecisionActive, wide, onDismissPeek, display, tool, speaking, busy, attachment, }: SpeechBalloonProps): JSX.Element;
110
+ /** How tall the listed conversation grows before it scrolls, in pixels. */
111
+ export declare const BALLOON_HISTORY_MAX_HEIGHT = 220;
112
+ /** Shorter, when something goes with the words under it (a notebook). */
113
+ export declare const BALLOON_HISTORY_WITH_ATTACHMENT_MAX_HEIGHT = 120;
114
+ /**
115
+ * The conversation in the `history` balloon: a header that counts it and
116
+ * opens it, then every message, the person's to the right, scrolled to the
117
+ * newest.
118
+ */
119
+ /**
120
+ * The history balloon, large (*Expand*): every message, then the tool being
121
+ * called and what goes with the words, in the overlay or the page's area,
122
+ * scrolled there.
123
+ */
124
+ export declare function BalloonHistoryLarge({ history, tool, attachment, }: {
125
+ history: readonly BalloonHistoryMessage[];
126
+ tool?: BalloonToolLine;
127
+ attachment?: ReactNode;
128
+ }): JSX.Element;
129
+ export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, decide, onDecisionActive, wide, onDismissPeek, display, tool, speaking, busy, attachment, history, onExpand, expandTitle, suggestions, onSuggestion, waiting, fullText, onExpandHistory, }: SpeechBalloonProps): JSX.Element;
64
130
  export default SpeechBalloon;