@datalayer/agent-runtimes 1.3.60 → 1.3.62

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 (37) hide show
  1. package/lib/chat/ChatFloating.js +138 -23
  2. package/lib/chat/assistant/AssistantStage.d.ts +6 -4
  3. package/lib/chat/assistant/AssistantStage.js +1 -1
  4. package/lib/chat/assistant/ConversationBalloon.d.ts +71 -0
  5. package/lib/chat/assistant/ConversationBalloon.js +102 -0
  6. package/lib/chat/assistant/SpeechBalloon.d.ts +10 -6
  7. package/lib/chat/assistant/SpeechBalloon.js +17 -12
  8. package/lib/chat/assistant/state.d.ts +1 -1
  9. package/lib/chat/assistant/state.js +1 -1
  10. package/lib/chat/base/ChatBase.js +22 -4
  11. package/lib/examples/AgentA2ATeamExample.d.ts +23 -0
  12. package/lib/examples/AgentA2ATeamExample.js +279 -0
  13. package/lib/examples/ChatAssistantExample.js +13 -2
  14. package/lib/examples/ChatAssistantGalleryExample.js +1 -1
  15. package/lib/examples/LoopStrategyExample.d.ts +12 -0
  16. package/lib/examples/{AgentStrategyExample.js → LoopStrategyExample.js} +9 -9
  17. package/lib/examples/example-selector.js +2 -1
  18. package/lib/examples/index.d.ts +2 -1
  19. package/lib/examples/index.js +2 -1
  20. package/lib/examples/main.js +3 -3
  21. package/lib/hooks/useA2A.d.ts +37 -1
  22. package/lib/loop/plugins/assistant/index.js +9 -1
  23. package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +60 -0
  24. package/lib/runtimes/browser/a2aPeer.d.ts +93 -0
  25. package/lib/runtimes/browser/a2aPeer.js +241 -0
  26. package/lib/specs/actions.js +54 -0
  27. package/lib/specs/apps.d.ts +2 -0
  28. package/lib/specs/apps.js +387 -0
  29. package/lib/specs/teams/teams.d.ts +1 -0
  30. package/lib/specs/teams/teams.js +80 -0
  31. package/lib/stories/loop/ReferenceScreens.d.ts +6 -2
  32. package/lib/stories/loop/ReferenceScreens.js +63 -6
  33. package/lib/types/chat.d.ts +12 -0
  34. package/lib/types/teams.d.ts +20 -0
  35. package/package.json +2 -2
  36. package/scripts/codegen/generate_teams.py +42 -3
  37. package/lib/examples/AgentStrategyExample.d.ts +0 -12
@@ -32,6 +32,8 @@ import { useViewportDrag } from './useViewportDrag';
32
32
  import { disabledChatViewModes, resolveMountPoint } from './viewModes';
33
33
  import { AssistantStage } from './assistant/AssistantStage';
34
34
  import { decisionsAskerAt } from './assistant/decisions';
35
+ import { DecisionAsk } from './assistant/DecisionAsk';
36
+ import { BalloonApprovalMessage, CONVERSATION_BALLOON_WIDTH, ConversationBalloonClose, balloonTailAt, conversationBalloonHeight, conversationBalloonSx, peekLine, } from './assistant/ConversationBalloon';
35
37
  import { SpeechBalloon } from './assistant/SpeechBalloon';
36
38
  import { DEFAULT_ASSISTANT_CHARACTER, } from './assistant/characters';
37
39
  import { assistantStateOf, latestSaying, newestIsAnswer, keepAway, keptAway, ASSISTANT_WORDS, } from './assistant/state';
@@ -205,7 +207,14 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
205
207
  : assistantState === 'waiting'
206
208
  ? { text: ASSISTANT_WORDS.waiting }
207
209
  : unheard && saying
208
- ? { text: saying.text, more: saying.more }
210
+ ? {
211
+ // A peek: the first words; the rest is in the conversation.
212
+ ...peekLine(saying.text),
213
+ onDismiss: () => {
214
+ setHeardId(saying.id);
215
+ setFreshSaying(false);
216
+ },
217
+ }
209
218
  : { text: description };
210
219
  const balloonInsists = assistantState === 'greeting' ||
211
220
  assistantState === 'waiting' ||
@@ -433,19 +442,23 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
433
442
  }, [isOpen, handleToggle]);
434
443
  /*
435
444
  * The conversation as the assistant's balloon: above the character, its
436
- * right edge on the character's, and never off the screen.
445
+ * edge on the character's, never off the screen, and its tail toward the
446
+ * character — none when it stands beside it.
437
447
  */
438
- const balloonPlace = () => {
439
- const widthPx = typeof width === 'number' ? width : 400;
440
- const heightPx = typeof height === 'number' ? height : 550;
448
+ const viewportWidth = typeof window === 'undefined' ? 1280 : window.innerWidth;
449
+ const viewportHeight = typeof window === 'undefined' ? 800 : window.innerHeight;
450
+ const conversationHeight = conversationBalloonHeight(viewportHeight);
451
+ const balloonGeometry = () => {
452
+ const widthPx = CONVERSATION_BALLOON_WIDTH;
453
+ const heightPx = conversationHeight;
441
454
  if (stageDrag.position) {
442
455
  // Toward the page's inside: above the character, below it, or beside
443
456
  // it when the window is too short for either.
444
457
  const { left, top } = stageDrag.position;
445
- const onLeft = left + ASSISTANT_SIZE / 2 < window.innerWidth / 2;
458
+ const onLeft = left + ASSISTANT_SIZE / 2 < viewportWidth / 2;
446
459
  const above = top - heightPx - 16;
447
460
  const below = top + ASSISTANT_SIZE + 16;
448
- const beside = above < 8 && below + heightPx > window.innerHeight - 8;
461
+ const beside = above < 8 && below + heightPx > viewportHeight - 8;
449
462
  const x = beside
450
463
  ? onLeft
451
464
  ? left + ASSISTANT_SIZE + 16
@@ -458,17 +471,38 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
458
471
  : above >= 8
459
472
  ? above
460
473
  : below;
474
+ const placedX = clamp(x, viewportWidth - widthPx - 8);
461
475
  return {
462
- left: px(clamp(x, window.innerWidth - widthPx - 8)),
463
- top: px(clamp(y, window.innerHeight - heightPx - 8)),
464
- right: 'auto',
465
- bottom: 'auto',
476
+ place: {
477
+ left: px(placedX),
478
+ top: px(clamp(y, viewportHeight - heightPx - 8)),
479
+ right: 'auto',
480
+ bottom: 'auto',
481
+ },
482
+ tail: beside
483
+ ? { edge: 'none' }
484
+ : {
485
+ edge: above >= 8 ? 'bottom' : 'top',
486
+ at: balloonTailAt(left + ASSISTANT_SIZE / 2, placedX),
487
+ },
466
488
  };
467
489
  }
490
+ const onRight = position.endsWith('right');
491
+ const characterMiddle = onRight
492
+ ? viewportWidth - offset - ASSISTANT_SIZE / 2
493
+ : offset + ASSISTANT_SIZE / 2;
494
+ const balloonLeft = onRight ? viewportWidth - offset - widthPx : offset;
495
+ const tail = {
496
+ edge: position.startsWith('bottom') ? 'bottom' : 'top',
497
+ at: balloonTailAt(characterMiddle, balloonLeft),
498
+ };
468
499
  const corner = getPositionStyles();
469
- return position.startsWith('bottom')
470
- ? { ...corner, bottom: offset + ASSISTANT_SIZE + 16 }
471
- : { ...corner, top: offset + ASSISTANT_SIZE + 16 };
500
+ return {
501
+ place: position.startsWith('bottom')
502
+ ? { ...corner, bottom: px(offset + ASSISTANT_SIZE + 16) }
503
+ : { ...corner, top: px(offset + ASSISTANT_SIZE + 16) },
504
+ tail,
505
+ };
472
506
  };
473
507
  // Handle new chat
474
508
  const handleNewChat = useCallback(() => {
@@ -520,6 +554,20 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
520
554
  }, animationDuration);
521
555
  return () => clearTimeout(timer);
522
556
  }, [hostsConversation, isOpen, animationDuration, focusTrigger]);
557
+ /*
558
+ * The assistant's balloon opens on the newest message: the history may
559
+ * have grown — or arrived — while it was closed.
560
+ */
561
+ useEffect(() => {
562
+ if (!assistantShown || !isOpen) {
563
+ return undefined;
564
+ }
565
+ const timer = setTimeout(() => {
566
+ const history = popupRef.current?.querySelector('[data-chat-history]');
567
+ history?.scrollTo?.({ top: history.scrollHeight });
568
+ }, animationDuration);
569
+ return () => clearTimeout(timer);
570
+ }, [assistantShown, isOpen, animationDuration]);
523
571
  // Click outside to close
524
572
  useEffect(() => {
525
573
  if (!clickOutsideToClose || !isOpen)
@@ -604,8 +652,18 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
604
652
  })
605
653
  : undefined;
606
654
  // Responsive dimensions
607
- const popupWidth = isMobile ? '100%' : width;
608
- const popupHeight = isMobile ? '100%' : height;
655
+ const conversationBalloon = assistantShown ? balloonGeometry() : undefined;
656
+ // The assistant's conversation is a balloon of its own size (T-23).
657
+ const popupWidth = isMobile
658
+ ? '100%'
659
+ : conversationBalloon
660
+ ? CONVERSATION_BALLOON_WIDTH
661
+ : width;
662
+ const popupHeight = isMobile
663
+ ? '100%'
664
+ : conversationBalloon
665
+ ? conversationHeight
666
+ : height;
609
667
  // Mobile full-screen styles
610
668
  const mobileStyles = isMobile
611
669
  ? {
@@ -621,6 +679,44 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
621
679
  : {};
622
680
  // Close button for header
623
681
  const closeButton = (_jsx(Tooltip, { text: `Close${escapeToClose ? ' (Esc)' : ''}`, direction: "s", children: _jsx(IconButton, { icon: XIcon, "aria-label": "Close", onClick: handleToggle, variant: "invisible", size: "small" }) }));
682
+ /*
683
+ * The chat as the assistant's balloon holds it (T-23): the history and the
684
+ * composer, its Lexical variant, and nothing else — no header, no footer,
685
+ * none of the session's controls; the welcome as the first message, an
686
+ * approval as a message with Approve and Deny, and *Ask a decision* beside
687
+ * the composer when there is a runtime to ask.
688
+ */
689
+ const assistantChat = conversationBalloon
690
+ ? {
691
+ showHeader: false,
692
+ showPoweredBy: false,
693
+ promptVariant: 'lexical',
694
+ showModelSelector: false,
695
+ showToolsMenu: false,
696
+ showSkillsMenu: false,
697
+ showTokenUsage: false,
698
+ showContextRing: false,
699
+ showInformation: false,
700
+ showToolApprovalBanner: false,
701
+ showAgentsMenu: false,
702
+ showTurnFooter: false,
703
+ compact: true,
704
+ welcome: description,
705
+ backgroundColor: 'canvas.default',
706
+ borderRadius: 'var(--theme-radius-bubble, 16px)',
707
+ trailingContent: balloonApproval ? (_jsx(BalloonApprovalMessage, { approval: balloonApproval })) : undefined,
708
+ footerContent: assistantDecide ? (_jsx(Box, { "data-balloon-decisions": "", sx: {
709
+ px: 3,
710
+ pb: 1,
711
+ display: 'flex',
712
+ flexDirection: 'column',
713
+ alignItems: 'flex-end',
714
+ fontSize: 0,
715
+ '& form': { alignSelf: 'stretch' },
716
+ '& [data-balloon-decision="answered"]': { alignSelf: 'stretch' },
717
+ }, children: _jsx(DecisionAsk, { ask: assistantDecide }) })) : undefined,
718
+ }
719
+ : undefined;
624
720
  /* The window itself; where it goes is decided below. */
625
721
  const chatWindow = (_jsxs(Box, { ref: popupRef, className: className, sx: {
626
722
  position: viewMode === 'panel' && !isMobile && (panelInFlow || dockedInMount)
@@ -651,8 +747,8 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
651
747
  }
652
748
  : getPositionStyles()
653
749
  : {}),
654
- ...(assistantShown
655
- ? balloonPlace()
750
+ ...(conversationBalloon
751
+ ? conversationBalloon.place
656
752
  : assistantMode && !isMobile
657
753
  ? getPositionStyles()
658
754
  : {}),
@@ -746,7 +842,16 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
746
842
  visibility: isOpen || isAnimating ? 'visible' : 'hidden',
747
843
  pointerEvents: isOpen ? 'auto' : 'none',
748
844
  ...mobileStyles,
749
- }, children: [viewMode === 'floating-draggable' && !isMobile && (_jsx(Box, { onPointerDown: drag.onHandlePointerDown, "aria-label": "Move the chat", sx: {
845
+ // The assistant's balloon: its shape and its tail toward the
846
+ // character, which the window's own clipping would cut off.
847
+ ...(conversationBalloon
848
+ ? conversationBalloonSx(conversationBalloon.tail,
849
+ // Out of the composer's band, or of the history's top.
850
+ conversationBalloon.tail.edge === 'bottom'
851
+ ? 'canvas.subtle'
852
+ : 'canvas.default')
853
+ : {}),
854
+ }, "data-conversation-balloon": conversationBalloon ? '' : undefined, children: [conversationBalloon ? (_jsx(ConversationBalloonClose, { onClose: handleToggle })) : null, viewMode === 'floating-draggable' && !isMobile && (_jsx(Box, { onPointerDown: drag.onHandlePointerDown, "aria-label": "Move the chat", sx: {
750
855
  display: 'flex',
751
856
  alignItems: 'center',
752
857
  justifyContent: 'center',
@@ -759,7 +864,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
759
864
  borderColor: 'border.muted',
760
865
  touchAction: 'none',
761
866
  '&:active': { cursor: 'grabbing' },
762
- }, children: _jsx(GrabberIcon, { size: 16 }) })), conversation ? (_jsxs(_Fragment, { children: [showHeader ? (_jsxs(Box, { sx: {
867
+ }, children: _jsx(GrabberIcon, { size: 16 }) })), conversation ? (_jsxs(_Fragment, { children: [showHeader && !conversationBalloon ? (_jsxs(Box, { sx: {
763
868
  display: 'flex',
764
869
  alignItems: 'center',
765
870
  gap: 2,
@@ -775,7 +880,17 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
775
880
  overflow: 'hidden',
776
881
  textOverflow: 'ellipsis',
777
882
  whiteSpace: 'nowrap',
778
- }, children: title }), closeButton] })) : null, _jsx(Box, { "data-floating-conversation": true, sx: { flex: '1 1 auto', minHeight: 0, display: 'flex' }, children: conversation.body })] })) : (_jsx(ChatBase, { title: title, showHeader: showHeader, useStore: useStoreMode, protocol: protocol, themeVariant: themeVariant, themeOverrides: themeOverrides, colorMode: colorMode, autoFocus: isOpen, focusTrigger: focusTrigger, launching: launching, launchingMessage: launchingMessage, brandIcon: brandIcon || _jsx(AiAgentIcon, { colored: true, size: 20 }), headerButtons: {
883
+ }, children: title }), closeButton] })) : null, _jsx(Box, { "data-floating-conversation": true, sx: {
884
+ flex: '1 1 auto',
885
+ minHeight: 0,
886
+ display: 'flex',
887
+ ...(conversationBalloon
888
+ ? {
889
+ borderRadius: 'var(--theme-radius-bubble, 16px)',
890
+ overflow: 'hidden',
891
+ }
892
+ : {}),
893
+ }, children: conversation.body })] })) : (_jsx(ChatBase, { title: title, showHeader: showHeader, useStore: useStoreMode, protocol: protocol, themeVariant: themeVariant, themeOverrides: themeOverrides, colorMode: colorMode, autoFocus: isOpen, focusTrigger: focusTrigger, launching: launching, launchingMessage: launchingMessage, brandIcon: brandIcon || _jsx(AiAgentIcon, { colored: true, size: 20 }), headerButtons: {
779
894
  showNewChat: showNewChatButton,
780
895
  showClear: showClearButton && messages.length > 0,
781
896
  showSettings: showSettingsButton && !!onSettingsClick,
@@ -791,7 +906,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
791
906
  ...poweredByProps,
792
907
  }, renderToolResult: renderToolResult, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: {
793
908
  showAvatars: true,
794
- }, 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, showInformation: showInformation, onInformationClick: onInformationClick, onToolCallStart: onToolCallStart, onToolCallComplete: onToolCallComplete, showToolApprovalBanner: showToolApprovalBanner, pendingApprovals: pendingApprovals, onApproveApproval: onApproveApproval, onRejectApproval: onRejectApproval, ...panelProps, onLoadingChange: handleLoadingChange, onDisplayItemsChange: handleDisplayItemsChange, children: children }))] }));
909
+ }, 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, showInformation: showInformation, onInformationClick: onInformationClick, onToolCallStart: onToolCallStart, onToolCallComplete: onToolCallComplete, showToolApprovalBanner: showToolApprovalBanner, pendingApprovals: pendingApprovals, onApproveApproval: onApproveApproval, onRejectApproval: onRejectApproval, ...panelProps, ...assistantChat, onLoadingChange: handleLoadingChange, onDisplayItemsChange: handleDisplayItemsChange, children: children }))] }));
795
910
  /*
796
911
  * The floating popup's button. Words not yet heard make it blink for the
797
912
  * person's attention, until the chat is opened; hovered, they show in a
@@ -839,7 +954,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
839
954
  left: px(stageDrag.position.left),
840
955
  top: px(stageDrag.position.top),
841
956
  }
842
- : getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef, decide: assistantDecide })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
957
+ : getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
843
958
  ...getPositionStyles(),
844
959
  }, children: _jsxs(Box, { sx: {
845
960
  position: 'relative',
@@ -68,15 +68,17 @@ export interface AssistantStageProps {
68
68
  /** Open or close the conversation. */
69
69
  onToggle: () => void;
70
70
  /**
71
- * What the balloon says while the conversation is closed: the agent's
72
- * newest words, as the Office Assistant said them (T-23), or a welcome.
73
- * `more` adds *Open the conversation* for the rest; `approval`, an
74
- * approval to answer there, with *Approve* and *Deny*.
71
+ * The peek while the conversation is closed: one short line — the agent's
72
+ * newest words, as the Office Assistant said them (T-23), or a welcome —
73
+ * that opens the conversation when clicked; `more` says it was cut;
74
+ * `approval`, an approval to answer there, with *Approve* and *Deny*.
75
75
  */
76
76
  balloon?: {
77
77
  text: string;
78
78
  more?: boolean;
79
79
  approval?: BalloonApproval;
80
+ /** Puts the peek away (a × beside its line); never an approval's. */
81
+ onDismiss?: () => void;
80
82
  };
81
83
  /** Show the balloon without being hovered: something new to say. */
82
84
  insist?: boolean;
@@ -279,7 +279,7 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
279
279
  '@media (prefers-reduced-motion: reduce)': {
280
280
  '& *': { animation: 'none !important' },
281
281
  },
282
- }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, 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) => {
282
+ }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, onDismissPeek: balloon.onDismiss, 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) => {
283
283
  pressedAt.current = { x: event.clientX, y: event.clientY };
284
284
  onDragStart(event);
285
285
  }, onClick: (event) => {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The floating assistant's conversation, open: a balloon over the character
3
+ * that holds the conversation itself — the history as the chat draws it, the
4
+ * welcome first, an approval as a message with *Approve* and *Deny* (T-23),
5
+ * the composer last — with no header and no footer, a tail toward the
6
+ * character, a close control, and a height that leaves the page in sight
7
+ * while the history scrolls inside it.
8
+ *
9
+ * Closed, the character peeks instead (`SpeechBalloon`): one short line, and
10
+ * what it needs to be answered.
11
+ *
12
+ * @module chat/assistant/ConversationBalloon
13
+ */
14
+ import type { JSX } from 'react';
15
+ import { type BalloonApproval } from './state';
16
+ /** The open balloon's width, in pixels. */
17
+ export declare const CONVERSATION_BALLOON_WIDTH = 380;
18
+ /** The open balloon's tallest, in pixels, and its share of the window. */
19
+ export declare const CONVERSATION_BALLOON_MAX_HEIGHT = 560;
20
+ export declare const CONVERSATION_BALLOON_VIEWPORT_SHARE = 0.6;
21
+ /** The open balloon's height for a window this tall: 60% of it, 560px at most. */
22
+ export declare function conversationBalloonHeight(viewportHeight: number): number;
23
+ /**
24
+ * Where the open balloon's tail points: from its bottom edge (the balloon
25
+ * above the character) or its top edge (below it), `at` pixels from its
26
+ * left; none when the balloon stands beside the character.
27
+ */
28
+ export type BalloonTail = {
29
+ edge: 'bottom' | 'top';
30
+ at: number;
31
+ } | {
32
+ edge: 'none';
33
+ };
34
+ /**
35
+ * The tail toward a character whose middle is `characterMiddle` pixels from
36
+ * the window's left, under or over a balloon whose left is `balloonLeft`:
37
+ * kept off the balloon's rounded corners.
38
+ */
39
+ export declare function balloonTailAt(characterMiddle: number, balloonLeft: number, width?: number): number;
40
+ /**
41
+ * The open balloon's shape: the bubble's radius, and its tail — drawn in the
42
+ * colour of what it leaves (`tailBg`): the composer's band under the
43
+ * history, the history over it.
44
+ */
45
+ export declare function conversationBalloonSx(tail: BalloonTail, tailBg?: string): Record<string, unknown>;
46
+ /** The open balloon's close control, over its top right corner. */
47
+ export declare function ConversationBalloonClose({ name, onClose, }: {
48
+ /** Who is talking: "Close the conversation with Paper clip". */
49
+ name?: string;
50
+ onClose: () => void;
51
+ }): JSX.Element;
52
+ /**
53
+ * An approval the agent waits on, as a message of the history: what it asks
54
+ * to do, the rule it was asked under, *Approve* and *Deny*, and how many more
55
+ * wait (T-23). Brought into view when it arrives.
56
+ */
57
+ export declare function BalloonApprovalMessage({ approval, }: {
58
+ approval: BalloonApproval;
59
+ }): JSX.Element;
60
+ /** How many more wait, in two words: "2 more". */
61
+ export declare function moreWaiting(others: number): string;
62
+ /** How much of a message a peek says: its first words. */
63
+ export declare const PEEK_LIMIT = 60;
64
+ /**
65
+ * A message's first words, for a peek: cut at a word, with an ellipsis, and
66
+ * whether there was more.
67
+ */
68
+ export declare function peekLine(text: string, limit?: number): {
69
+ text: string;
70
+ more: boolean;
71
+ };
@@ -0,0 +1,102 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { Button, IconButton, Text } from '@primer/react';
4
+ import { Box } from '@datalayer/primer-addons';
5
+ import { XIcon } from '@primer/octicons-react';
6
+ import { ASSISTANT_WORDS } from './state';
7
+ /** The open balloon's width, in pixels. */
8
+ export const CONVERSATION_BALLOON_WIDTH = 380;
9
+ /** The open balloon's tallest, in pixels, and its share of the window. */
10
+ export const CONVERSATION_BALLOON_MAX_HEIGHT = 560;
11
+ export const CONVERSATION_BALLOON_VIEWPORT_SHARE = 0.6;
12
+ /** The open balloon's height for a window this tall: 60% of it, 560px at most. */
13
+ export function conversationBalloonHeight(viewportHeight) {
14
+ return Math.max(240, Math.min(CONVERSATION_BALLOON_MAX_HEIGHT, Math.round(viewportHeight * CONVERSATION_BALLOON_VIEWPORT_SHARE)));
15
+ }
16
+ /** The tail's half width, in pixels. */
17
+ const TAIL = 7;
18
+ /**
19
+ * The tail toward a character whose middle is `characterMiddle` pixels from
20
+ * the window's left, under or over a balloon whose left is `balloonLeft`:
21
+ * kept off the balloon's rounded corners.
22
+ */
23
+ export function balloonTailAt(characterMiddle, balloonLeft, width = CONVERSATION_BALLOON_WIDTH) {
24
+ return Math.min(width - 24 - TAIL, Math.max(24, characterMiddle - balloonLeft - TAIL));
25
+ }
26
+ /**
27
+ * The open balloon's shape: the bubble's radius, and its tail — drawn in the
28
+ * colour of what it leaves (`tailBg`): the composer's band under the
29
+ * history, the history over it.
30
+ */
31
+ export function conversationBalloonSx(tail, tailBg = 'canvas.default') {
32
+ return {
33
+ borderRadius: 'var(--theme-radius-bubble, 16px)',
34
+ overflow: 'visible',
35
+ ...(tail.edge === 'none'
36
+ ? {}
37
+ : {
38
+ '&::after': {
39
+ content: '""',
40
+ position: 'absolute',
41
+ ...(tail.edge === 'bottom'
42
+ ? { bottom: `-${TAIL}px` }
43
+ : { top: `-${TAIL}px` }),
44
+ left: `${tail.at}px`,
45
+ width: `${TAIL * 2}px`,
46
+ height: `${TAIL * 2}px`,
47
+ bg: tailBg,
48
+ borderRight: '1px solid',
49
+ borderBottom: '1px solid',
50
+ borderColor: 'border.default',
51
+ transform: tail.edge === 'bottom' ? 'rotate(45deg)' : 'rotate(225deg)',
52
+ pointerEvents: 'none',
53
+ },
54
+ }),
55
+ };
56
+ }
57
+ /** The open balloon's close control, over its top right corner. */
58
+ export function ConversationBalloonClose({ name, onClose, }) {
59
+ return (_jsx(Box, { "data-conversation-balloon-close": "", sx: { position: 'absolute', top: '6px', right: '6px', zIndex: 30 }, children: _jsx(IconButton, { icon: XIcon, size: "small", variant: "invisible", "aria-label": name
60
+ ? `Close the conversation with ${name}`
61
+ : 'Close the conversation', onClick: onClose, sx: { bg: 'canvas.default', borderRadius: '50%' } }) }));
62
+ }
63
+ /**
64
+ * An approval the agent waits on, as a message of the history: what it asks
65
+ * to do, the rule it was asked under, *Approve* and *Deny*, and how many more
66
+ * wait (T-23). Brought into view when it arrives.
67
+ */
68
+ export function BalloonApprovalMessage({ approval, }) {
69
+ const ref = useRef(null);
70
+ useEffect(() => {
71
+ ref.current?.scrollIntoView?.({ block: 'nearest' });
72
+ }, [approval.id]);
73
+ return (_jsx(Box, { ref: ref, "data-balloon-approval": approval.id, sx: { px: 3, py: 2, display: 'flex' }, children: _jsxs(Box, { sx: {
74
+ maxWidth: '90%',
75
+ px: 3,
76
+ py: 2,
77
+ bg: 'attention.subtle',
78
+ border: '1px solid',
79
+ borderColor: 'attention.muted',
80
+ borderRadius: 'var(--theme-radius-bubble, 16px)',
81
+ fontSize: 1,
82
+ }, children: [_jsx(Text, { as: "p", sx: { m: 0, color: 'fg.muted', fontSize: 0 }, children: ASSISTANT_WORDS.approval }), _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, alignItems: 'center' }, 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, { sx: { color: 'fg.muted', fontSize: 0 }, children: moreWaiting(approval.others) })) : null] })] }) }));
83
+ }
84
+ /** How many more wait, in two words: "2 more". */
85
+ export function moreWaiting(others) {
86
+ return `${others} more`;
87
+ }
88
+ /** How much of a message a peek says: its first words. */
89
+ export const PEEK_LIMIT = 60;
90
+ /**
91
+ * A message's first words, for a peek: cut at a word, with an ellipsis, and
92
+ * whether there was more.
93
+ */
94
+ export function peekLine(text, limit = PEEK_LIMIT) {
95
+ const plain = text.replace(/\s+/g, ' ').trim();
96
+ if (plain.length <= limit) {
97
+ return { text: plain, more: false };
98
+ }
99
+ const cut = plain.slice(0, limit);
100
+ const atWord = cut.slice(0, Math.max(cut.lastIndexOf(' '), limit * 0.6));
101
+ return { text: `${atWord.trimEnd()}…`, more: true };
102
+ }
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * A speech balloon over something that stands for the agent — the floating
3
3
  * assistant's character, or the floating popup's button while the chat is
4
- * closed: the agent's newest words, as the Office Assistant said them
5
- * (LOOP T-23), with *Open the conversation* when there is more — and an
6
- * approval it waits on, answered there with *Approve* or *Deny* — and, where
7
- * the runtime can be asked, *Ask a decision*: a typed decision asked of Jev,
8
- * its answer said back in the balloon.
4
+ * closed: a peek, one short line — the agent's newest words, as the Office
5
+ * Assistant said them (LOOP T-23), cut to their first words, or a
6
+ * notification — that opens the conversation when clicked, and can be
7
+ * dismissed; an approval it waits on, answered there with *Approve* or
8
+ * *Deny*, and how many more wait; and, where the runtime can be asked and
9
+ * the conversation is elsewhere (the LOOP workspace), *Ask a decision*: a
10
+ * typed decision asked of Jev, its answer said back in the balloon.
9
11
  *
10
12
  * @module chat/assistant/SpeechBalloon
11
13
  */
@@ -34,6 +36,8 @@ export interface SpeechBalloonProps {
34
36
  onDecisionActive?: (active: boolean) => void;
35
37
  /** Wider, for the decision's form. */
36
38
  wide?: boolean;
39
+ /** Puts the peek away: a × beside its line. Never for an approval. */
40
+ onDismissPeek?: () => void;
37
41
  }
38
- export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, decide, onDecisionActive, wide, }: SpeechBalloonProps): JSX.Element;
42
+ export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, approval, decide, onDecisionActive, wide, onDismissPeek, }: SpeechBalloonProps): JSX.Element;
39
43
  export default SpeechBalloon;
@@ -1,9 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, Text } from '@primer/react';
2
+ import { Button, IconButton, Text } from '@primer/react';
3
3
  import { Box } from '@datalayer/primer-addons';
4
+ import { XIcon } from '@primer/octicons-react';
4
5
  import { ASSISTANT_WORDS } from './state';
6
+ import { moreWaiting } from './ConversationBalloon';
5
7
  import { DecisionAsk } from './DecisionAsk';
6
- export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, decide, onDecisionActive, wide = false, }) {
8
+ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, approval, decide, onDecisionActive, wide = false, onDismissPeek, }) {
7
9
  return (_jsxs(Box, { role: "status", "aria-live": "polite", "data-speech-balloon": "", sx: {
8
10
  position: 'absolute',
9
11
  // Pixels, as strings: a number here is read as the theme's space
@@ -38,15 +40,18 @@ export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above
38
40
  borderColor: 'border.default',
39
41
  transform: side === 'above' ? 'rotate(45deg)' : 'rotate(225deg)',
40
42
  },
41
- }, children: [_jsx(Text, { as: "p", sx: { m: 0 }, children: text }), 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: `${approval.others} more waiting.` })) : null] })), decide && _jsx(DecisionAsk, { ask: decide, onActiveChange: onDecisionActive }), more && (_jsx(Box, { as: "button", type: "button", onClick: onOpen, sx: {
42
- mt: 1,
43
- p: 0,
44
- border: 0,
45
- bg: 'transparent',
46
- color: 'accent.fg',
47
- textDecoration: 'underline',
48
- cursor: 'pointer',
49
- fontSize: 1,
50
- }, children: "Open the conversation" }))] }));
43
+ }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'flex-start', gap: 1 }, children: [_jsx(Box, { as: "button", type: "button", "data-balloon-peek": "", "data-balloon-more": more ? '' : undefined, onClick: onOpen, sx: {
44
+ flex: 1,
45
+ minWidth: 0,
46
+ m: 0,
47
+ p: 0,
48
+ border: 0,
49
+ bg: 'transparent',
50
+ color: 'inherit',
51
+ font: 'inherit',
52
+ textAlign: 'left',
53
+ cursor: 'pointer',
54
+ '&:hover': { textDecoration: 'underline' },
55
+ }, 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] })), decide && _jsx(DecisionAsk, { ask: decide, onActiveChange: onDecisionActive })] }));
51
56
  }
52
57
  export default SpeechBalloon;
@@ -71,7 +71,7 @@ export interface AssistantSaying {
71
71
  /** Whether there was more than the balloon holds. */
72
72
  more: boolean;
73
73
  }
74
- /** How much of a message the balloon says before *Open the conversation*. */
74
+ /** How much of a message is kept as the agent's saying before it is cut. */
75
75
  export declare const SAYING_LIMIT = 220;
76
76
  /**
77
77
  * The newest thing the agent said, as the Office Assistant said it: in the
@@ -49,7 +49,7 @@ export const ASSISTANT_WORDS = {
49
49
  /** Its deployment is paused (R-17). */
50
50
  paused: 'Paused — it answers again once it is resumed.',
51
51
  };
52
- /** How much of a message the balloon says before *Open the conversation*. */
52
+ /** How much of a message is kept as the agent's saying before it is cut. */
53
53
  export const SAYING_LIMIT = 220;
54
54
  /**
55
55
  * The newest thing the agent said, as the Office Assistant said it: in the
@@ -463,6 +463,8 @@ function parseApprovedMcpToolsByServer(mcpStatusData) {
463
463
  // ---------------------------------------------------------------------------
464
464
  // ChatBase (outer wrapper — ensures QueryClient is available)
465
465
  // ---------------------------------------------------------------------------
466
+ /** When the welcome is said: it has no time of its own. */
467
+ const WELCOME_AT = new Date(0);
466
468
  /**
467
469
  * ChatBase component — Universal chat panel supporting store, protocol, and custom modes.
468
470
  */
@@ -507,7 +509,7 @@ function ChatBaseInner({ title, subtitle, showHeader = false, showTokenUsage = t
507
509
  // Mode selection
508
510
  useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady, onLoadingChange, onContextSnapshot, enableStreaming = false,
509
511
  // Extended props
510
- brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, 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,
512
+ brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, 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,
511
513
  // Tool invocation hooks
512
514
  onToolCallStart, onToolCallComplete,
513
515
  // Identity/Authorization props
@@ -3478,7 +3480,23 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3478
3480
  })
3479
3481
  .filter(item => item.message)
3480
3482
  : undefined;
3481
- const messagesContent = children ? (children) : (_jsx(Box, { sx: {
3483
+ /*
3484
+ * The welcome, as the first message: drawn as the agent's own words are,
3485
+ * never sent nor kept. With it, the history is never empty, so the empty
3486
+ * state gives way to it.
3487
+ */
3488
+ const welcomedItems = welcome
3489
+ ? [
3490
+ {
3491
+ id: '__welcome__',
3492
+ role: 'assistant',
3493
+ content: welcome,
3494
+ createdAt: WELCOME_AT,
3495
+ },
3496
+ ...displayItems,
3497
+ ]
3498
+ : displayItems;
3499
+ const messagesContent = children ? (children) : (_jsxs(Box, { sx: {
3482
3500
  display: 'flex',
3483
3501
  flexDirection: 'column',
3484
3502
  minHeight: 0,
@@ -3494,7 +3512,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3494
3512
  maxWidth: 920,
3495
3513
  mx: 'auto',
3496
3514
  bg: 'canvas.default',
3497
- }, children: _jsx(ChatMessageList, { displayItems: displayItems, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding, renderToolResult: effectiveRenderToolResult, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, agentUsage: agentUsage, onRemoveItems: handleRemoveItems, mcpServers: filteredMcpServers, emptyContent: launching ? null : (_jsx(ChatEmptyState, { emptyState: emptyState, brandIcon: brandIcon, description: resolvedDescription, suggestions: resolvedSuggestions, submitOnSuggestionClick: submitOnSuggestionClick, onSuggestionSubmit: handleSuggestionSubmit, onSuggestionFill: handleSuggestionFill })) }) }));
3515
+ }, children: [_jsx(ChatMessageList, { displayItems: welcomedItems, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding, renderToolResult: effectiveRenderToolResult, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, agentUsage: agentUsage, onRemoveItems: handleRemoveItems, mcpServers: filteredMcpServers, emptyContent: launching ? null : (_jsx(ChatEmptyState, { emptyState: emptyState, brandIcon: brandIcon, description: resolvedDescription, suggestions: resolvedSuggestions, submitOnSuggestionClick: submitOnSuggestionClick, onSuggestionSubmit: handleSuggestionSubmit, onSuggestionFill: handleSuggestionFill })) }), trailingContent] }));
3498
3516
  const inputToolbar = showInput ? (_jsx(InputPrompt, { input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
3499
3517
  /*
3500
3518
  The same openers the empty state offers, typed into the box they would
@@ -3695,7 +3713,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
3695
3713
  top: 8,
3696
3714
  right: 8,
3697
3715
  zIndex: 6,
3698
- }, children: _jsx(IconButton, { icon: SidebarExpandIcon, "aria-label": "Open chat", onClick: onExpandFromCollapsed, variant: "default", size: "small" }) })) : (_jsx(FloatingBrandButton, { isOpen: false, onToggle: onExpandFromCollapsed, position: "bottom-right", tooltip: "Open chat" })))] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { ref: messagesContainerRef, sx: {
3716
+ }, children: _jsx(IconButton, { icon: SidebarExpandIcon, "aria-label": "Open chat", onClick: onExpandFromCollapsed, variant: "default", size: "small" }) })) : (_jsx(FloatingBrandButton, { isOpen: false, onToggle: onExpandFromCollapsed, position: "bottom-right", tooltip: "Open chat" })))] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { ref: messagesContainerRef, "data-chat-history": "", sx: {
3699
3717
  flex: 1,
3700
3718
  flexGrow: 1,
3701
3719
  minHeight: 0,