@marimo-team/islands 0.23.15-dev4 → 0.23.15-dev40
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.
- package/dist/{ConnectedDataExplorerComponent-CU4fZJzG.js → ConnectedDataExplorerComponent-7p7rt9iw.js} +4 -4
- package/dist/{ErrorBoundary-DE6tzZf-.js → ErrorBoundary-B_CAG7_e.js} +1 -1
- package/dist/{ImageComparisonComponent-CHrI72em.js → ImageComparisonComponent-1i5IBhon.js} +173 -129
- package/dist/assets/__vite-browser-external-CjNAy01L.js +1 -0
- package/dist/assets/worker-B3XPCb6y.js +98 -0
- package/dist/{chat-ui-DinOvbWu.js → chat-ui-C0z13gJB.js} +3111 -3111
- package/dist/{code-visibility-Dk8cVOny.js → code-visibility-CSsHnVZu.js} +946 -864
- package/dist/{constants-T20xxyNf.js → debounce-BOD3DbfP.js} +1 -24
- package/dist/{formats-Dzx4J_z1.js → formats-CitsMtUm.js} +1 -1
- package/dist/{glide-data-editor-CjTu7ukN.js → glide-data-editor-CwZz71BD.js} +3 -3
- package/dist/{html-to-image-_wGfk8V-.js → html-to-image-CEo5pRYw.js} +2291 -2250
- package/dist/{input-DtsN7xm-.js → input-BGPrFH3g.js} +2 -2
- package/dist/main.js +1297 -1201
- package/dist/{mermaid-BYqXy_NE.js → mermaid-UdmxG2PZ.js} +2 -2
- package/dist/{process-output-Mh4UrjwM.js → process-output-BOvvilRG.js} +1 -1
- package/dist/{reveal-component-B5DXLyO7.js → reveal-component-DGjJVNMQ.js} +618 -605
- package/dist/{spec-Cz-Bj1JI.js → spec-DSs9v0xx.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-CWNNlFEX.js → toDate-BRJgtOGm.js} +14 -5
- package/dist/{useAsyncData-KfHB8wQR.js → useAsyncData-BSOyAbac.js} +1 -1
- package/dist/{useDeepCompareMemoize-nJMtxhm4.js → useDeepCompareMemoize-BTLeWzuR.js} +1 -1
- package/dist/{useLifecycle-DegSo0lV.js → useLifecycle-C6wHjkhW.js} +1 -1
- package/dist/{useTheme-6eZ3GOTS.js → useTheme-Df_vGflw.js} +62 -27
- package/dist/{vega-component-DzyyM9fc.js → vega-component-BAvmvTjO.js} +6 -6
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +244 -1
- package/src/components/chat/__tests__/message-queue.test.tsx +121 -0
- package/src/components/chat/chat-panel.tsx +196 -67
- package/src/components/chat/chat-utils.ts +111 -2
- package/src/components/editor/SortableCell.tsx +6 -2
- package/src/components/editor/__tests__/output-persistence.test.tsx +241 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +153 -0
- package/src/components/editor/actions/pair-with-agent-commands.ts +109 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +20 -64
- package/src/components/editor/ai/add-cell-with-ai.tsx +14 -5
- package/src/components/editor/ai/ai-completion-editor.tsx +4 -1
- package/src/components/editor/cell/cell-context-menu.tsx +7 -0
- package/src/components/editor/chrome/panels/packages-panel.tsx +11 -1
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +105 -0
- package/src/components/editor/columns/cell-column.tsx +34 -4
- package/src/components/editor/columns/sortable-column.tsx +24 -4
- package/src/components/editor/file-tree/download.ts +46 -0
- package/src/components/editor/file-tree/file-explorer.tsx +3 -21
- package/src/components/editor/file-tree/file-viewer.tsx +4 -27
- package/src/components/editor/navigation/__tests__/navigation.test.ts +33 -0
- package/src/components/editor/navigation/navigation.ts +8 -1
- package/src/components/editor/notebook-cell.tsx +203 -106
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +66 -0
- package/src/components/editor/renderers/cell-array.tsx +26 -13
- package/src/components/editor/renderers/cells-renderer.tsx +8 -2
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +29 -0
- package/src/components/editor/renderers/slides-layout/types.ts +4 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/components/slides/reveal-component.tsx +37 -4
- package/src/components/slides/slide-form.tsx +72 -0
- package/src/components/ui/toast.tsx +11 -3
- package/src/components/ui/toaster.tsx +65 -9
- package/src/core/cells/__tests__/utils.test.ts +59 -0
- package/src/core/cells/utils.ts +51 -0
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +97 -3
- package/src/core/codemirror/completion/signature-hint.ts +78 -11
- package/src/core/constants.ts +6 -0
- package/src/core/islands/__tests__/bridge.test.ts +341 -46
- package/src/core/islands/__tests__/main.test.ts +176 -0
- package/src/core/islands/__tests__/parse.test.ts +105 -0
- package/src/core/islands/bootstrap.ts +8 -3
- package/src/core/islands/bridge.ts +116 -23
- package/src/core/islands/components/__tests__/web-components.test.tsx +214 -0
- package/src/core/islands/components/web-components.tsx +76 -15
- package/src/core/islands/constants.ts +1 -0
- package/src/core/islands/main.ts +69 -3
- package/src/core/islands/parse.ts +70 -23
- package/src/core/islands/worker/__tests__/controller.test.ts +173 -0
- package/src/core/islands/worker/worker.tsx +145 -57
- package/src/core/runtime/__tests__/runtime.test.ts +64 -0
- package/src/core/runtime/runtime.ts +30 -10
- package/src/core/wasm/worker/bootstrap.ts +113 -20
- package/src/css/app/Cell.css +10 -0
- package/src/hooks/__tests__/useHotkey.test.tsx +88 -0
- package/src/hooks/useHotkey.ts +29 -4
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +6 -9
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +22 -61
- package/src/plugins/impl/anywidget/model-proxy.ts +0 -13
- package/src/plugins/impl/anywidget/widget-binding.ts +4 -34
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +53 -2
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +71 -0
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +71 -3
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -0
- package/src/theme/__tests__/useTheme.test.ts +68 -0
- package/src/theme/useTheme.ts +16 -1
- package/dist/assets/__vite-browser-external-BQCLNwri.js +0 -1
- package/dist/assets/worker-DAWRHcPq.js +0 -73
|
@@ -86,20 +86,53 @@ import {
|
|
|
86
86
|
import { renderUIMessage } from "./chat-display";
|
|
87
87
|
import { ChatHistoryPopover } from "./chat-history-popover";
|
|
88
88
|
import {
|
|
89
|
+
type ChatMessagePart,
|
|
89
90
|
convertToFileUIPart,
|
|
90
91
|
generateChatTitle,
|
|
91
92
|
handleToolCall,
|
|
92
93
|
hasPendingToolCalls,
|
|
94
|
+
hasUnresolvedToolCalls,
|
|
93
95
|
isLastMessageReasoning,
|
|
94
96
|
PROVIDERS_THAT_SUPPORT_ATTACHMENTS,
|
|
97
|
+
type QueuedUserMessage,
|
|
98
|
+
shouldFlushQueue,
|
|
95
99
|
useFileState,
|
|
100
|
+
useMessageQueue,
|
|
96
101
|
} from "./chat-utils";
|
|
97
102
|
import { getCodes } from "@/core/codemirror/copilot/getCodes";
|
|
98
103
|
import { focusInputAndMoveToEnd } from "@/core/codemirror/utils";
|
|
104
|
+
import ScrollToBottomButton from "./acp/scroll-to-bottom-button";
|
|
99
105
|
|
|
100
106
|
// Default mode for the AI
|
|
101
107
|
const DEFAULT_MODE = "manual";
|
|
102
108
|
|
|
109
|
+
const QueuedMessageDisplay = memo(
|
|
110
|
+
({ message }: { message: QueuedUserMessage }) => {
|
|
111
|
+
const textParts = message.parts.filter(
|
|
112
|
+
(p): p is TextUIPart => p.type === "text",
|
|
113
|
+
);
|
|
114
|
+
const content = textParts.map((p) => p.text).join("\n");
|
|
115
|
+
const fileParts = message.parts.filter(
|
|
116
|
+
(p): p is FileUIPart => p.type === "file",
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className="flex justify-end">
|
|
121
|
+
<div className="w-[95%] bg-background border border-dashed p-2 rounded-sm opacity-70">
|
|
122
|
+
{fileParts.map((filePart, idx) => (
|
|
123
|
+
<AttachmentRenderer attachment={filePart} key={idx} />
|
|
124
|
+
))}
|
|
125
|
+
<div className="flex items-center justify-end gap-2 text-sm text-muted-foreground">
|
|
126
|
+
<span className="wrap-break-word">{content}</span>
|
|
127
|
+
<Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin" />
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
QueuedMessageDisplay.displayName = "QueuedMessageDisplay";
|
|
135
|
+
|
|
103
136
|
interface ChatHeaderProps {
|
|
104
137
|
onNewChat: () => void;
|
|
105
138
|
activeChatId: ChatId | undefined;
|
|
@@ -487,12 +520,19 @@ const ChatPanelBody = () => {
|
|
|
487
520
|
const [pendingPrompt, setPendingPrompt] = useAtom(pendingAiPromptAtom);
|
|
488
521
|
const [input, setInput] = useState("");
|
|
489
522
|
const [newThreadInput, setNewThreadInput] = useState("");
|
|
523
|
+
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
|
|
490
524
|
const { files, addFiles, clearFiles, removeFile } = useFileState();
|
|
525
|
+
const {
|
|
526
|
+
messages: queuedMessages,
|
|
527
|
+
enqueue: enqueueUserMessage,
|
|
528
|
+
flushNext: flushNextQueuedMessage,
|
|
529
|
+
clear: clearQueuedMessages,
|
|
530
|
+
hasQueuedRef,
|
|
531
|
+
} = useMessageQueue();
|
|
491
532
|
const newThreadInputRef = useRef<ReactCodeMirrorRef>(null);
|
|
492
533
|
const newMessageInputRef = useRef<ReactCodeMirrorRef>(null);
|
|
493
534
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
494
535
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
495
|
-
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
496
536
|
const runtimeManager = useRuntimeManager();
|
|
497
537
|
const { invokeAiTool, sendRun } = useRequestClient();
|
|
498
538
|
const { openModal, closeModal } = useImperativeModal();
|
|
@@ -560,7 +600,7 @@ const ChatPanelBody = () => {
|
|
|
560
600
|
};
|
|
561
601
|
},
|
|
562
602
|
}),
|
|
563
|
-
onFinish: ({ messages }) => {
|
|
603
|
+
onFinish: ({ messages, isError, isAbort }) => {
|
|
564
604
|
setChatState((prev) => {
|
|
565
605
|
return replaceMessagesInChat({
|
|
566
606
|
chatState: prev,
|
|
@@ -568,6 +608,7 @@ const ChatPanelBody = () => {
|
|
|
568
608
|
messages: messages,
|
|
569
609
|
});
|
|
570
610
|
});
|
|
611
|
+
tryFlushQueuedMessages(messages, { isError, isAbort });
|
|
571
612
|
},
|
|
572
613
|
onToolCall: async ({ toolCall }) => {
|
|
573
614
|
await handleToolCall({
|
|
@@ -586,23 +627,103 @@ const ChatPanelBody = () => {
|
|
|
586
627
|
},
|
|
587
628
|
});
|
|
588
629
|
|
|
630
|
+
const sendUserMessage = useEvent((parts: ChatMessagePart[]) => {
|
|
631
|
+
sendMessage({ role: "user", parts });
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
const tryFlushQueuedMessages = useEvent(
|
|
635
|
+
(
|
|
636
|
+
chatMessages: typeof messages,
|
|
637
|
+
opts: { isError: boolean; isAbort: boolean },
|
|
638
|
+
) => {
|
|
639
|
+
if (!hasQueuedRef.current) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (
|
|
643
|
+
shouldFlushQueue({
|
|
644
|
+
isError: opts.isError,
|
|
645
|
+
isAbort: opts.isAbort,
|
|
646
|
+
hasPendingToolCalls: hasPendingToolCalls(chatMessages),
|
|
647
|
+
hasUnresolvedToolCalls: hasUnresolvedToolCalls(chatMessages),
|
|
648
|
+
})
|
|
649
|
+
) {
|
|
650
|
+
flushNextQueuedMessage(sendUserMessage);
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
);
|
|
654
|
+
|
|
589
655
|
const isLoading = status === "submitted" || status === "streaming";
|
|
656
|
+
// Read via a ref so the queue-vs-send decision stays correct even when it is
|
|
657
|
+
// made after an `await` (e.g. resolving @-context), by which point the render
|
|
658
|
+
// closure's `isLoading` may be stale.
|
|
659
|
+
const isLoadingRef = useRef(isLoading);
|
|
660
|
+
isLoadingRef.current = isLoading;
|
|
661
|
+
|
|
662
|
+
const submitOrQueue = useEvent((parts: ChatMessagePart[]) => {
|
|
663
|
+
if (isLoadingRef.current || hasQueuedRef.current) {
|
|
664
|
+
enqueueUserMessage(parts);
|
|
665
|
+
} else {
|
|
666
|
+
sendUserMessage(parts);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const handleScroll = useEvent(() => {
|
|
671
|
+
const container = scrollContainerRef.current;
|
|
672
|
+
if (!container) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
677
|
+
const hasOverflow = scrollHeight > clientHeight;
|
|
678
|
+
const isAtBottom = hasOverflow
|
|
679
|
+
? Math.abs(scrollHeight - clientHeight - scrollTop) < 5
|
|
680
|
+
: true;
|
|
681
|
+
setIsScrolledToBottom(isAtBottom);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
const scrollToBottom = useEvent((smooth = false) => {
|
|
685
|
+
const container = scrollContainerRef.current;
|
|
686
|
+
if (!container) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
container.scrollTo({
|
|
691
|
+
top: container.scrollHeight,
|
|
692
|
+
behavior: smooth ? "smooth" : "auto",
|
|
693
|
+
});
|
|
694
|
+
});
|
|
590
695
|
|
|
591
696
|
// Check if we're currently streaming reasoning in the latest message
|
|
592
697
|
const isStreamingReasoning =
|
|
593
698
|
isLoading && messages.length > 0 && isLastMessageReasoning(messages);
|
|
594
699
|
|
|
595
|
-
//
|
|
700
|
+
// Pin to the bottom while the user is already there.
|
|
596
701
|
useEffect(() => {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
container.scrollTop = container.scrollHeight;
|
|
601
|
-
}
|
|
602
|
-
};
|
|
702
|
+
setIsScrolledToBottom(true);
|
|
703
|
+
clearQueuedMessages();
|
|
704
|
+
}, [activeChatId, clearQueuedMessages]);
|
|
603
705
|
|
|
604
|
-
|
|
605
|
-
|
|
706
|
+
useEffect(() => {
|
|
707
|
+
if (!isScrolledToBottom) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const frame = requestAnimationFrame(() => {
|
|
711
|
+
scrollToBottom();
|
|
712
|
+
});
|
|
713
|
+
return () => cancelAnimationFrame(frame);
|
|
714
|
+
}, [messages, queuedMessages, isLoading, isScrolledToBottom, scrollToBottom]);
|
|
715
|
+
|
|
716
|
+
// Retry when tool parts resolve after the stream ends (e.g. assistant text
|
|
717
|
+
// trails a still-running tool call, so `onFinish` alone cannot flush).
|
|
718
|
+
useEffect(() => {
|
|
719
|
+
if (isLoading) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
tryFlushQueuedMessages(messages, {
|
|
723
|
+
isError: error != null,
|
|
724
|
+
isAbort: false,
|
|
725
|
+
});
|
|
726
|
+
}, [messages, isLoading, error, tryFlushQueuedMessages]);
|
|
606
727
|
|
|
607
728
|
const startNewChatState = useEvent((initialMessage: string) => {
|
|
608
729
|
const now = Date.now();
|
|
@@ -654,6 +775,7 @@ const ChatPanelBody = () => {
|
|
|
654
775
|
setActiveChat(null);
|
|
655
776
|
setInput("");
|
|
656
777
|
setNewThreadInput("");
|
|
778
|
+
clearQueuedMessages();
|
|
657
779
|
clearFiles();
|
|
658
780
|
});
|
|
659
781
|
|
|
@@ -699,15 +821,12 @@ const ChatPanelBody = () => {
|
|
|
699
821
|
const { contextPart, attachments } = await resolveChatContext(newValue);
|
|
700
822
|
|
|
701
823
|
e?.preventDefault();
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
...attachments,
|
|
709
|
-
],
|
|
710
|
-
});
|
|
824
|
+
submitOrQueue([
|
|
825
|
+
{ type: "text", text: newValue },
|
|
826
|
+
...(contextPart ? [contextPart] : []),
|
|
827
|
+
...(fileParts ?? []),
|
|
828
|
+
...attachments,
|
|
829
|
+
]);
|
|
711
830
|
setInput("");
|
|
712
831
|
clearFiles();
|
|
713
832
|
},
|
|
@@ -737,14 +856,11 @@ const ChatPanelBody = () => {
|
|
|
737
856
|
setInput(newThreadInput);
|
|
738
857
|
}
|
|
739
858
|
const { contextPart, attachments } = await resolveChatContext(prompt);
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
...attachments,
|
|
746
|
-
],
|
|
747
|
-
});
|
|
859
|
+
submitOrQueue([
|
|
860
|
+
{ type: "text", text: prompt },
|
|
861
|
+
...(contextPart ? [contextPart] : []),
|
|
862
|
+
...attachments,
|
|
863
|
+
]);
|
|
748
864
|
});
|
|
749
865
|
|
|
750
866
|
const isNewThread = messages.length === 0;
|
|
@@ -824,49 +940,62 @@ const ChatPanelBody = () => {
|
|
|
824
940
|
/>
|
|
825
941
|
</TooltipProvider>
|
|
826
942
|
|
|
827
|
-
<div
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
943
|
+
<div className="flex-1 flex flex-col overflow-hidden relative min-h-0">
|
|
944
|
+
<div
|
|
945
|
+
className="flex-1 px-3 bg-(--slate-1) gap-4 py-3 flex flex-col overflow-y-auto"
|
|
946
|
+
ref={scrollContainerRef}
|
|
947
|
+
onScroll={handleScroll}
|
|
948
|
+
>
|
|
949
|
+
{isNewThread && (
|
|
950
|
+
<div className="flex flex-col gap-2">
|
|
951
|
+
<div className="rounded-md border bg-background">
|
|
952
|
+
{filesPills}
|
|
953
|
+
{chatInput}
|
|
954
|
+
</div>
|
|
955
|
+
<PairWithAgentCallout onPairWithAgent={handlePairWithAgent} />
|
|
836
956
|
</div>
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
/>
|
|
852
|
-
))}
|
|
957
|
+
)}
|
|
958
|
+
|
|
959
|
+
{messages.map((message, idx) => (
|
|
960
|
+
<ChatMessageDisplay
|
|
961
|
+
key={message.id}
|
|
962
|
+
message={message}
|
|
963
|
+
index={idx}
|
|
964
|
+
onEdit={handleMessageEdit}
|
|
965
|
+
isStreamingReasoning={isStreamingReasoning}
|
|
966
|
+
isLast={idx === messages.length - 1}
|
|
967
|
+
isActive={isLoading}
|
|
968
|
+
addToolApprovalResponse={addToolApprovalResponse}
|
|
969
|
+
/>
|
|
970
|
+
))}
|
|
853
971
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
</div>
|
|
858
|
-
)}
|
|
972
|
+
{queuedMessages.map((message) => (
|
|
973
|
+
<QueuedMessageDisplay key={message.id} message={message} />
|
|
974
|
+
))}
|
|
859
975
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
976
|
+
{isLoading && (
|
|
977
|
+
<div className="flex justify-center py-4">
|
|
978
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
979
|
+
</div>
|
|
980
|
+
)}
|
|
981
|
+
|
|
982
|
+
{error && (
|
|
983
|
+
<div className="flex items-center justify-center space-x-2 mb-4">
|
|
984
|
+
<ErrorBanner error={error || new Error("Unknown error")} />
|
|
985
|
+
<Button variant="outline" size="sm" onClick={handleReload}>
|
|
986
|
+
Retry
|
|
987
|
+
</Button>
|
|
988
|
+
</div>
|
|
989
|
+
)}
|
|
990
|
+
</div>
|
|
868
991
|
|
|
869
|
-
<
|
|
992
|
+
<ScrollToBottomButton
|
|
993
|
+
isVisible={
|
|
994
|
+
!isScrolledToBottom &&
|
|
995
|
+
(messages.length > 0 || queuedMessages.length > 0)
|
|
996
|
+
}
|
|
997
|
+
onScrollToBottom={() => scrollToBottom(true)}
|
|
998
|
+
/>
|
|
870
999
|
</div>
|
|
871
1000
|
|
|
872
1001
|
{isLoading && (
|
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
isToolUIPart,
|
|
8
8
|
lastAssistantMessageIsCompleteWithApprovalResponses,
|
|
9
9
|
lastAssistantMessageIsCompleteWithToolCalls,
|
|
10
|
+
type ToolUIPart,
|
|
10
11
|
type UIMessage,
|
|
11
12
|
} from "ai";
|
|
12
|
-
import { useState } from "react";
|
|
13
|
+
import { useRef, useState } from "react";
|
|
13
14
|
import useEvent from "react-use-event-hook";
|
|
14
15
|
import type { ProviderId } from "@/core/ai/ids/ids";
|
|
15
16
|
import type { ToolNotebookContext } from "@/core/ai/tools/base";
|
|
@@ -20,6 +21,7 @@ import type {
|
|
|
20
21
|
} from "@/core/network/types";
|
|
21
22
|
import { blobToString } from "@/utils/fileToBase64";
|
|
22
23
|
import { Logger } from "@/utils/Logger";
|
|
24
|
+
import { generateUUID } from "@/utils/uuid";
|
|
23
25
|
import { getAICompletionBodyWithAttachments } from "../editor/ai/completion-utils";
|
|
24
26
|
import { toast } from "../ui/use-toast";
|
|
25
27
|
|
|
@@ -186,10 +188,44 @@ export function hasPendingToolCalls(messages: UIMessage[]): boolean {
|
|
|
186
188
|
}
|
|
187
189
|
return (
|
|
188
190
|
lastAssistantMessageIsCompleteWithToolCalls({ messages }) ||
|
|
189
|
-
lastAssistantMessageIsCompleteWithApprovalResponses({ messages })
|
|
191
|
+
lastAssistantMessageIsCompleteWithApprovalResponses({ messages }) ||
|
|
192
|
+
(lastPart.state === "output-denied" && !lastPart.providerExecuted)
|
|
190
193
|
);
|
|
191
194
|
}
|
|
192
195
|
|
|
196
|
+
/**
|
|
197
|
+
* True when the assistant is still waiting on tool execution or user approval.
|
|
198
|
+
* Unlike `hasPendingToolCalls` (ready to auto-resume), these states must block
|
|
199
|
+
* releasing queued user messages.
|
|
200
|
+
*/
|
|
201
|
+
export function hasUnresolvedToolCalls(messages: UIMessage[]): boolean {
|
|
202
|
+
const lastMessage = messages.at(-1);
|
|
203
|
+
if (!lastMessage || lastMessage.role !== "assistant") {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return lastMessage.parts.some(
|
|
207
|
+
(part) => isToolUIPart(part) && isUnresolvedToolState(part.state),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isUnresolvedToolState(state: ToolUIPart["state"]): boolean {
|
|
212
|
+
switch (state) {
|
|
213
|
+
case "approval-requested":
|
|
214
|
+
case "input-streaming":
|
|
215
|
+
case "input-available":
|
|
216
|
+
return true;
|
|
217
|
+
case "approval-responded":
|
|
218
|
+
case "output-available":
|
|
219
|
+
case "output-error":
|
|
220
|
+
case "output-denied":
|
|
221
|
+
return false;
|
|
222
|
+
default: {
|
|
223
|
+
const _exhaustive: never = state;
|
|
224
|
+
return _exhaustive;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
193
229
|
export function useFileState() {
|
|
194
230
|
const [files, setFiles] = useState<File[]>([]);
|
|
195
231
|
|
|
@@ -217,3 +253,76 @@ export function useFileState() {
|
|
|
217
253
|
|
|
218
254
|
return { files, addFiles, clearFiles, removeFile };
|
|
219
255
|
}
|
|
256
|
+
|
|
257
|
+
export type ChatMessagePart = UIMessage["parts"][number];
|
|
258
|
+
|
|
259
|
+
export interface QueuedUserMessage {
|
|
260
|
+
id: string;
|
|
261
|
+
parts: ChatMessagePart[];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Decide whether the message queue should release its next message after a
|
|
266
|
+
* chat request settles. `onFinish` fires on every completion — including
|
|
267
|
+
* aborts, errors, and each intermediate tool-call round — so the queue must
|
|
268
|
+
* only advance once the assistant has genuinely finished its turn.
|
|
269
|
+
*/
|
|
270
|
+
export function shouldFlushQueue(opts: {
|
|
271
|
+
isError: boolean;
|
|
272
|
+
isAbort: boolean;
|
|
273
|
+
hasPendingToolCalls: boolean;
|
|
274
|
+
hasUnresolvedToolCalls: boolean;
|
|
275
|
+
}): boolean {
|
|
276
|
+
if (opts.isError) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
// User stopped the stream; release queued input even if a tool part is still
|
|
280
|
+
// mid-flight (e.g. `input-streaming`).
|
|
281
|
+
if (opts.isAbort) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
return !opts.hasPendingToolCalls && !opts.hasUnresolvedToolCalls;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Queue of user messages typed while the assistant is still responding.
|
|
289
|
+
* Messages are enqueued in order and released one at a time.
|
|
290
|
+
*/
|
|
291
|
+
export function useMessageQueue() {
|
|
292
|
+
const [messages, setMessages] = useState<QueuedUserMessage[]>([]);
|
|
293
|
+
// Mirror the queue in a ref so `flushNext` reads the latest value even when
|
|
294
|
+
// invoked from a callback (e.g. `onFinish`) captured on an earlier render.
|
|
295
|
+
const messagesRef = useRef<QueuedUserMessage[]>([]);
|
|
296
|
+
const hasQueuedRef = useRef(false);
|
|
297
|
+
messagesRef.current = messages;
|
|
298
|
+
hasQueuedRef.current = messages.length > 0;
|
|
299
|
+
|
|
300
|
+
const enqueue = useEvent((parts: ChatMessagePart[]) => {
|
|
301
|
+
setMessages((prev) => {
|
|
302
|
+
const next = [...prev, { id: generateUUID(), parts }];
|
|
303
|
+
messagesRef.current = next;
|
|
304
|
+
hasQueuedRef.current = true;
|
|
305
|
+
return next;
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const flushNext = useEvent((send: (parts: ChatMessagePart[]) => void) => {
|
|
310
|
+
const queue = messagesRef.current;
|
|
311
|
+
if (queue.length === 0) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const [next, ...rest] = queue;
|
|
315
|
+
messagesRef.current = rest;
|
|
316
|
+
hasQueuedRef.current = rest.length > 0;
|
|
317
|
+
setMessages(rest);
|
|
318
|
+
send(next.parts);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const clear = useEvent(() => {
|
|
322
|
+
messagesRef.current = [];
|
|
323
|
+
hasQueuedRef.current = false;
|
|
324
|
+
setMessages([]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return { messages, enqueue, flushNext, clear, hasQueuedRef };
|
|
328
|
+
}
|
|
@@ -15,6 +15,10 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
15
15
|
children: React.ReactNode;
|
|
16
16
|
cellId: CellId;
|
|
17
17
|
canMoveX?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* If true, dragging is disabled (e.g. while presenting).
|
|
20
|
+
*/
|
|
21
|
+
disabled?: boolean;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -42,7 +46,7 @@ function isTransformNoop(transform: Transform | null) {
|
|
|
42
46
|
|
|
43
47
|
export const SortableCell = React.forwardRef(
|
|
44
48
|
(
|
|
45
|
-
{ cellId, canMoveX, ...props }: Props,
|
|
49
|
+
{ cellId, canMoveX, disabled, ...props }: Props,
|
|
46
50
|
ref: React.ForwardedRef<HTMLDivElement>,
|
|
47
51
|
) => {
|
|
48
52
|
// This hook re-renders every time _any_ cell is dragged,
|
|
@@ -54,7 +58,7 @@ export const SortableCell = React.forwardRef(
|
|
|
54
58
|
transform,
|
|
55
59
|
transition,
|
|
56
60
|
isDragging,
|
|
57
|
-
} = useSortable({ id: cellId.toString() });
|
|
61
|
+
} = useSortable({ id: cellId.toString(), disabled });
|
|
58
62
|
|
|
59
63
|
// Perf:
|
|
60
64
|
// If the transform is a noop, keep it as null
|