@datalayer/agent-runtimes 1.3.59 → 1.3.61

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 (58) hide show
  1. package/lib/chat/ChatFloating.d.ts +11 -1
  2. package/lib/chat/ChatFloating.js +142 -23
  3. package/lib/chat/assistant/AssistantStage.d.ts +13 -5
  4. package/lib/chat/assistant/AssistantStage.js +5 -3
  5. package/lib/chat/assistant/ConversationBalloon.d.ts +71 -0
  6. package/lib/chat/assistant/ConversationBalloon.js +102 -0
  7. package/lib/chat/assistant/DecisionAsk.d.ts +31 -0
  8. package/lib/chat/assistant/DecisionAsk.js +96 -0
  9. package/lib/chat/assistant/SpeechBalloon.d.ts +17 -4
  10. package/lib/chat/assistant/SpeechBalloon.js +20 -14
  11. package/lib/chat/assistant/decisions.d.ts +104 -0
  12. package/lib/chat/assistant/decisions.js +161 -0
  13. package/lib/chat/assistant/state.d.ts +1 -1
  14. package/lib/chat/assistant/state.js +1 -1
  15. package/lib/chat/base/ChatBase.js +22 -4
  16. package/lib/chat/marks/toolMarks.js +2 -2
  17. package/lib/config/AgentConfiguration.d.ts +2 -1
  18. package/lib/config/AgentConfiguration.js +2 -2
  19. package/lib/examples/AgentDecideExample.d.ts +24 -0
  20. package/lib/examples/AgentDecideExample.js +73 -0
  21. package/lib/examples/AgentNotificationsExample.js +1 -1
  22. package/lib/examples/AgentSkillsExample.js +1 -1
  23. package/lib/examples/AgentspecsExample.js +3 -3
  24. package/lib/examples/ChatAssistantExample.js +12 -1
  25. package/lib/examples/example-selector.js +1 -0
  26. package/lib/examples/index.d.ts +1 -0
  27. package/lib/examples/index.js +1 -0
  28. package/lib/examples/main.js +3 -0
  29. package/lib/loop/apps/appspec.js +4 -4
  30. package/lib/loop/apps/checks.js +6 -6
  31. package/lib/loop/apps/rules.js +2 -2
  32. package/lib/loop/plugins/assistant/index.d.ts +14 -2
  33. package/lib/loop/plugins/assistant/index.js +22 -5
  34. package/lib/specs/actions.d.ts +1 -1
  35. package/lib/specs/actions.js +1 -1
  36. package/lib/specs/agents/agents.js +197 -192
  37. package/lib/specs/apps.js +13 -13
  38. package/lib/specs/appspecSchema.js +3 -3
  39. package/lib/specs/backendTools.d.ts +31 -0
  40. package/lib/specs/{tools.js → backendTools.js} +48 -48
  41. package/lib/specs/cogs.js +8 -7
  42. package/lib/specs/frames.js +5 -5
  43. package/lib/specs/index.d.ts +1 -1
  44. package/lib/specs/index.js +1 -1
  45. package/lib/stores/agentRuntimeStore.js +2 -2
  46. package/lib/stories/loop/ReferenceScreens.d.ts +6 -2
  47. package/lib/stories/loop/ReferenceScreens.js +63 -6
  48. package/lib/types/agentspecs.d.ts +7 -5
  49. package/lib/types/chat.d.ts +12 -0
  50. package/lib/types/config.d.ts +2 -2
  51. package/lib/types/tools.d.ts +3 -3
  52. package/package.json +1 -1
  53. package/scripts/codegen/generate_agents.py +23 -12
  54. package/scripts/codegen/generate_apps.py +3 -2
  55. package/scripts/codegen/{generate_tools.py → generate_backend_tools.py} +29 -29
  56. package/scripts/codegen/generate_cogs.py +2 -2
  57. package/scripts/codegen/generate_frames.py +2 -2
  58. package/lib/specs/tools.d.ts +0 -31
@@ -110,6 +110,16 @@ export interface ChatFloatingProps extends ChatCommonProps {
110
110
  * @default 'paperclip'
111
111
  */
112
112
  assistantCharacter?: string | AssistantCharacter | AssistantCharacterData;
113
+ /**
114
+ * The runtime the floating assistant asks typed decisions at — its
115
+ * `/api/v1/configure/inference/decisions`, as the `decide` tool asks — and
116
+ * the token to ask with, if it needs one: its balloon offers *Ask a
117
+ * decision*, answered there in plain words with its confidence.
118
+ */
119
+ decisions?: {
120
+ serverUrl: string;
121
+ token?: string;
122
+ };
113
123
  /**
114
124
  * Callback when the user switches view mode via the header toggle.
115
125
  * The parent component receives the new ChatViewMode value.
@@ -144,5 +154,5 @@ export interface ChatFloatingProps extends ChatCommonProps {
144
154
  * ChatFloating component
145
155
  * A floating chat window built on ChatBase
146
156
  */
147
- export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, buttonGlow, offset, animationDuration, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, onViewModeChange, sidebarMountPoint, showPanelBackdrop, availableModels, showModelSelector, showToolsMenu, showSkillsMenu, showTokenUsage, showContextRing, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, assistantCharacter, conversation, }: ChatFloatingProps): React.JSX.Element;
157
+ export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, buttonGlow, offset, animationDuration, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, onViewModeChange, sidebarMountPoint, showPanelBackdrop, availableModels, showModelSelector, showToolsMenu, showSkillsMenu, showTokenUsage, showContextRing, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, assistantCharacter, conversation, decisions, }: ChatFloatingProps): React.JSX.Element;
148
158
  export default ChatFloating;
@@ -31,6 +31,9 @@ import { ButtonGlow } from './display/ButtonGlow';
31
31
  import { useViewportDrag } from './useViewportDrag';
32
32
  import { disabledChatViewModes, resolveMountPoint } from './viewModes';
33
33
  import { AssistantStage } from './assistant/AssistantStage';
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';
34
37
  import { SpeechBalloon } from './assistant/SpeechBalloon';
35
38
  import { DEFAULT_ASSISTANT_CHARACTER, } from './assistant/characters';
36
39
  import { assistantStateOf, latestSaying, newestIsAnswer, keepAway, keptAway, ASSISTANT_WORDS, } from './assistant/state';
@@ -63,7 +66,7 @@ const clamp = (value, max) => Math.max(8, Math.min(value, Math.max(8, max)));
63
66
  * ChatFloating component
64
67
  * A floating chat window built on ChatBase
65
68
  */
66
- export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose = false, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor, buttonGlow = true, offset = 20, animationDuration = 200, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', onViewModeChange, sidebarMountPoint = null, showPanelBackdrop = false, availableModels, showModelSelector = false, showToolsMenu = false, showSkillsMenu = false, showTokenUsage = true, showContextRing = false, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, assistantCharacter = DEFAULT_ASSISTANT_CHARACTER, conversation, }) {
69
+ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose = false, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor, buttonGlow = true, offset = 20, animationDuration = 200, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', onViewModeChange, sidebarMountPoint = null, showPanelBackdrop = false, availableModels, showModelSelector = false, showToolsMenu = false, showSkillsMenu = false, showTokenUsage = true, showContextRing = false, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, assistantCharacter = DEFAULT_ASSISTANT_CHARACTER, conversation, decisions, }) {
67
70
  // Store-based state
68
71
  const storeIsOpen = useChatOpen();
69
72
  const storeMessages = useChatMessages();
@@ -194,6 +197,9 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
194
197
  },
195
198
  }
196
199
  : undefined;
200
+ const assistantDecide = useMemo(() => decisions?.serverUrl
201
+ ? decisionsAskerAt(decisions.serverUrl, decisions.token)
202
+ : undefined, [decisions?.serverUrl, decisions?.token]);
197
203
  const assistantBalloon = assistantState === 'paused'
198
204
  ? { text: ASSISTANT_WORDS.paused }
199
205
  : balloonApproval
@@ -201,7 +207,14 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
201
207
  : assistantState === 'waiting'
202
208
  ? { text: ASSISTANT_WORDS.waiting }
203
209
  : unheard && saying
204
- ? { 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
+ }
205
218
  : { text: description };
206
219
  const balloonInsists = assistantState === 'greeting' ||
207
220
  assistantState === 'waiting' ||
@@ -429,19 +442,23 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
429
442
  }, [isOpen, handleToggle]);
430
443
  /*
431
444
  * The conversation as the assistant's balloon: above the character, its
432
- * 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.
433
447
  */
434
- const balloonPlace = () => {
435
- const widthPx = typeof width === 'number' ? width : 400;
436
- 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;
437
454
  if (stageDrag.position) {
438
455
  // Toward the page's inside: above the character, below it, or beside
439
456
  // it when the window is too short for either.
440
457
  const { left, top } = stageDrag.position;
441
- const onLeft = left + ASSISTANT_SIZE / 2 < window.innerWidth / 2;
458
+ const onLeft = left + ASSISTANT_SIZE / 2 < viewportWidth / 2;
442
459
  const above = top - heightPx - 16;
443
460
  const below = top + ASSISTANT_SIZE + 16;
444
- const beside = above < 8 && below + heightPx > window.innerHeight - 8;
461
+ const beside = above < 8 && below + heightPx > viewportHeight - 8;
445
462
  const x = beside
446
463
  ? onLeft
447
464
  ? left + ASSISTANT_SIZE + 16
@@ -454,17 +471,38 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
454
471
  : above >= 8
455
472
  ? above
456
473
  : below;
474
+ const placedX = clamp(x, viewportWidth - widthPx - 8);
457
475
  return {
458
- left: px(clamp(x, window.innerWidth - widthPx - 8)),
459
- top: px(clamp(y, window.innerHeight - heightPx - 8)),
460
- right: 'auto',
461
- 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
+ },
462
488
  };
463
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
+ };
464
499
  const corner = getPositionStyles();
465
- return position.startsWith('bottom')
466
- ? { ...corner, bottom: offset + ASSISTANT_SIZE + 16 }
467
- : { ...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
+ };
468
506
  };
469
507
  // Handle new chat
470
508
  const handleNewChat = useCallback(() => {
@@ -516,6 +554,20 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
516
554
  }, animationDuration);
517
555
  return () => clearTimeout(timer);
518
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]);
519
571
  // Click outside to close
520
572
  useEffect(() => {
521
573
  if (!clickOutsideToClose || !isOpen)
@@ -600,8 +652,18 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
600
652
  })
601
653
  : undefined;
602
654
  // Responsive dimensions
603
- const popupWidth = isMobile ? '100%' : width;
604
- 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;
605
667
  // Mobile full-screen styles
606
668
  const mobileStyles = isMobile
607
669
  ? {
@@ -617,6 +679,44 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
617
679
  : {};
618
680
  // Close button for header
619
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;
620
720
  /* The window itself; where it goes is decided below. */
621
721
  const chatWindow = (_jsxs(Box, { ref: popupRef, className: className, sx: {
622
722
  position: viewMode === 'panel' && !isMobile && (panelInFlow || dockedInMount)
@@ -647,8 +747,8 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
647
747
  }
648
748
  : getPositionStyles()
649
749
  : {}),
650
- ...(assistantShown
651
- ? balloonPlace()
750
+ ...(conversationBalloon
751
+ ? conversationBalloon.place
652
752
  : assistantMode && !isMobile
653
753
  ? getPositionStyles()
654
754
  : {}),
@@ -742,7 +842,16 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
742
842
  visibility: isOpen || isAnimating ? 'visible' : 'hidden',
743
843
  pointerEvents: isOpen ? 'auto' : 'none',
744
844
  ...mobileStyles,
745
- }, 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: {
746
855
  display: 'flex',
747
856
  alignItems: 'center',
748
857
  justifyContent: 'center',
@@ -755,7 +864,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
755
864
  borderColor: 'border.muted',
756
865
  touchAction: 'none',
757
866
  '&:active': { cursor: 'grabbing' },
758
- }, 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: {
759
868
  display: 'flex',
760
869
  alignItems: 'center',
761
870
  gap: 2,
@@ -771,7 +880,17 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
771
880
  overflow: 'hidden',
772
881
  textOverflow: 'ellipsis',
773
882
  whiteSpace: 'nowrap',
774
- }, 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: {
775
894
  showNewChat: showNewChatButton,
776
895
  showClear: showClearButton && messages.length > 0,
777
896
  showSettings: showSettingsButton && !!onSettingsClick,
@@ -787,7 +906,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
787
906
  ...poweredByProps,
788
907
  }, renderToolResult: renderToolResult, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: {
789
908
  showAvatars: true,
790
- }, 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 }))] }));
791
910
  /*
792
911
  * The floating popup's button. Words not yet heard make it blink for the
793
912
  * person's attention, until the chat is opened; hovered, they show in a
@@ -17,6 +17,7 @@
17
17
  import type { JSX, RefObject } from 'react';
18
18
  import { type AssistantCharacter } from './characters';
19
19
  import type { AssistantCharacterData } from './formats/types';
20
+ import type { DecisionAsker } from './decisions';
20
21
  import { type AssistantAway, type AssistantState, type BalloonApproval } from './state';
21
22
  /**
22
23
  * Where the balloon goes so that it stays inside the window (T-23): toward
@@ -67,15 +68,17 @@ export interface AssistantStageProps {
67
68
  /** Open or close the conversation. */
68
69
  onToggle: () => void;
69
70
  /**
70
- * What the balloon says while the conversation is closed: the agent's
71
- * newest words, as the Office Assistant said them (T-23), or a welcome.
72
- * `more` adds *Open the conversation* for the rest; `approval`, an
73
- * 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*.
74
75
  */
75
76
  balloon?: {
76
77
  text: string;
77
78
  more?: boolean;
78
79
  approval?: BalloonApproval;
80
+ /** Puts the peek away (a × beside its line); never an approval's. */
81
+ onDismiss?: () => void;
79
82
  };
80
83
  /** Show the balloon without being hovered: something new to say. */
81
84
  insist?: boolean;
@@ -86,6 +89,11 @@ export interface AssistantStageProps {
86
89
  * steps aside for, nor is the pointer working there (T-27).
87
90
  */
88
91
  ownRef?: RefObject<HTMLElement | null>;
92
+ /**
93
+ * Asks a typed decision of the runtime: the balloon offers *Ask a
94
+ * decision*, and stays while its form or its answer is on screen.
95
+ */
96
+ decide?: DecisionAsker;
89
97
  }
90
- export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, }: AssistantStageProps): JSX.Element;
98
+ export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, decide, }: AssistantStageProps): JSX.Element;
91
99
  export default AssistantStage;
@@ -180,7 +180,7 @@ export function useKeepClear(stageRef, own, paused) {
180
180
  }, [stageRef, own, paused]);
181
181
  return obstructed ? 'obstacle' : pointerBusy ? 'pointer' : undefined;
182
182
  }
183
- export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, }) {
183
+ export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, decide, }) {
184
184
  // A shipped one by id, a drawing contributed by a plugin (T-24), or a
185
185
  // character read from a file (T-26).
186
186
  const shipped = typeof character === 'string'
@@ -199,7 +199,9 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
199
199
  // Where the press began: a press that moves is a drag, not a click.
200
200
  const pressedAt = useRef(null);
201
201
  const aside = useKeepClear(stageRef, ownRef, menuOpen);
202
- const showBalloon = !aside && !open && !!balloon && (hovered || insist);
202
+ // A decision being asked, or its answer, keeps the balloon up.
203
+ const [deciding, setDeciding] = useState(false);
204
+ const showBalloon = !aside && !open && !!balloon && (hovered || insist || deciding);
203
205
  return (_jsxs(Box, { ref: stageRef, "data-assistant-state": state, "data-assistant-aside": aside, sx: {
204
206
  position: 'fixed',
205
207
  zIndex: 1002,
@@ -277,7 +279,7 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
277
279
  '@media (prefers-reduced-motion: reduce)': {
278
280
  '& *': { animation: 'none !important' },
279
281
  },
280
- }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, approval: balloon.approval, 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) => {
281
283
  pressedAt.current = { x: event.clientX, y: event.clientY };
282
284
  onDragStart(event);
283
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
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * *Ask a decision* in the floating assistant's balloon: a small form — the
3
+ * situation, the question, its type (yes or no; a choice with its options
4
+ * listed; a score with its range) — asked of Jev through the runtime, and
5
+ * the answer said back in the balloon in plain words with its confidence.
6
+ *
7
+ * @module chat/assistant/DecisionAsk
8
+ */
9
+ import type { JSX } from 'react';
10
+ import { type DecisionAsker } from './decisions';
11
+ /** The balloon's words for a decision. */
12
+ export declare const DECISION_WORDS: {
13
+ readonly ask: "Ask a decision";
14
+ readonly asking: "Asking…";
15
+ readonly submit: "Ask";
16
+ readonly cancel: "Cancel";
17
+ readonly again: "Ask another";
18
+ readonly types: {
19
+ noul: string;
20
+ choice: string;
21
+ score: string;
22
+ };
23
+ };
24
+ export interface DecisionAskProps {
25
+ /** Asks the runtime. */
26
+ ask: DecisionAsker;
27
+ /** The form or its answer is on screen: the balloon must stay. */
28
+ onActiveChange?: (active: boolean) => void;
29
+ }
30
+ export declare function DecisionAsk({ ask, onActiveChange, }: DecisionAskProps): JSX.Element;
31
+ export default DecisionAsk;