@marimo-team/islands 0.23.15-dev33 → 0.23.15-dev4
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-7p7rt9iw.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
- package/dist/{ErrorBoundary-B_CAG7_e.js → ErrorBoundary-DE6tzZf-.js} +1 -1
- package/dist/{ImageComparisonComponent-1i5IBhon.js → ImageComparisonComponent-CHrI72em.js} +129 -173
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/worker-DAWRHcPq.js +73 -0
- package/dist/{chat-ui-C0wkSwxB.js → chat-ui-DinOvbWu.js} +3111 -3111
- package/dist/{code-visibility-BI94mK4u.js → code-visibility-Dk8cVOny.js} +864 -946
- package/dist/{debounce-BOD3DbfP.js → constants-T20xxyNf.js} +24 -1
- package/dist/{formats-BX3uDQbB.js → formats-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-CwZz71BD.js → glide-data-editor-CjTu7ukN.js} +3 -3
- package/dist/{html-to-image-D8Q5SLSJ.js → html-to-image-_wGfk8V-.js} +2241 -2277
- package/dist/{input-BGPrFH3g.js → input-DtsN7xm-.js} +2 -2
- package/dist/main.js +1201 -1297
- package/dist/{mermaid-UdmxG2PZ.js → mermaid-BYqXy_NE.js} +2 -2
- package/dist/{process-output-C_A12w9J.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-ELidGmPF.js → reveal-component-B5DXLyO7.js} +605 -618
- package/dist/{spec-DSs9v0xx.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-CKpRx4TS.js → toDate-CWNNlFEX.js} +4 -3
- package/dist/{useAsyncData-BSOyAbac.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-BTLeWzuR.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-C6wHjkhW.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-Df_vGflw.js → useTheme-6eZ3GOTS.js} +27 -62
- package/dist/{vega-component-DNHEV0u0.js → vega-component-DzyyM9fc.js} +6 -6
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +1 -244
- package/src/components/chat/chat-panel.tsx +67 -196
- package/src/components/chat/chat-utils.ts +2 -111
- package/src/components/editor/SortableCell.tsx +2 -6
- package/src/components/editor/actions/pair-with-agent-modal.tsx +64 -20
- package/src/components/editor/ai/add-cell-with-ai.tsx +5 -14
- package/src/components/editor/ai/ai-completion-editor.tsx +1 -4
- package/src/components/editor/cell/cell-context-menu.tsx +0 -7
- package/src/components/editor/chrome/panels/packages-panel.tsx +1 -11
- package/src/components/editor/columns/cell-column.tsx +4 -34
- package/src/components/editor/columns/sortable-column.tsx +4 -24
- package/src/components/editor/navigation/__tests__/navigation.test.ts +0 -33
- package/src/components/editor/navigation/navigation.ts +1 -8
- package/src/components/editor/notebook-cell.tsx +106 -203
- package/src/components/editor/renderers/cell-array.tsx +13 -26
- package/src/components/editor/renderers/cells-renderer.tsx +2 -8
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -29
- package/src/components/editor/renderers/slides-layout/types.ts +0 -4
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/components/slides/reveal-component.tsx +4 -37
- package/src/components/slides/slide-form.tsx +0 -72
- package/src/core/cells/__tests__/utils.test.ts +0 -59
- package/src/core/cells/utils.ts +0 -51
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +3 -97
- package/src/core/codemirror/completion/signature-hint.ts +11 -78
- package/src/core/constants.ts +0 -6
- package/src/core/islands/__tests__/bridge.test.ts +46 -341
- package/src/core/islands/__tests__/parse.test.ts +0 -105
- package/src/core/islands/bootstrap.ts +3 -8
- package/src/core/islands/bridge.ts +23 -116
- package/src/core/islands/components/web-components.tsx +15 -76
- package/src/core/islands/constants.ts +0 -1
- package/src/core/islands/main.ts +3 -69
- package/src/core/islands/parse.ts +23 -70
- package/src/core/islands/worker/worker.tsx +57 -145
- package/src/core/wasm/worker/bootstrap.ts +20 -113
- package/src/css/app/Cell.css +0 -10
- package/src/hooks/useHotkey.ts +4 -29
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +9 -6
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +61 -22
- package/src/plugins/impl/anywidget/model-proxy.ts +13 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +34 -4
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +2 -53
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +3 -71
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +0 -1
- package/src/theme/useTheme.ts +1 -16
- package/dist/assets/__vite-browser-external-CjNAy01L.js +0 -1
- package/dist/assets/worker-B3XPCb6y.js +0 -98
- package/src/components/chat/__tests__/message-queue.test.tsx +0 -121
- package/src/components/editor/__tests__/output-persistence.test.tsx +0 -241
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +0 -153
- package/src/components/editor/actions/pair-with-agent-commands.ts +0 -109
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +0 -105
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +0 -66
- package/src/core/islands/__tests__/main.test.ts +0 -176
- package/src/core/islands/components/__tests__/web-components.test.tsx +0 -214
- package/src/core/islands/worker/__tests__/controller.test.ts +0 -173
- package/src/hooks/__tests__/useHotkey.test.tsx +0 -88
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +0 -71
- package/src/theme/__tests__/useTheme.test.ts +0 -68
|
@@ -86,53 +86,20 @@ import {
|
|
|
86
86
|
import { renderUIMessage } from "./chat-display";
|
|
87
87
|
import { ChatHistoryPopover } from "./chat-history-popover";
|
|
88
88
|
import {
|
|
89
|
-
type ChatMessagePart,
|
|
90
89
|
convertToFileUIPart,
|
|
91
90
|
generateChatTitle,
|
|
92
91
|
handleToolCall,
|
|
93
92
|
hasPendingToolCalls,
|
|
94
|
-
hasUnresolvedToolCalls,
|
|
95
93
|
isLastMessageReasoning,
|
|
96
94
|
PROVIDERS_THAT_SUPPORT_ATTACHMENTS,
|
|
97
|
-
type QueuedUserMessage,
|
|
98
|
-
shouldFlushQueue,
|
|
99
95
|
useFileState,
|
|
100
|
-
useMessageQueue,
|
|
101
96
|
} from "./chat-utils";
|
|
102
97
|
import { getCodes } from "@/core/codemirror/copilot/getCodes";
|
|
103
98
|
import { focusInputAndMoveToEnd } from "@/core/codemirror/utils";
|
|
104
|
-
import ScrollToBottomButton from "./acp/scroll-to-bottom-button";
|
|
105
99
|
|
|
106
100
|
// Default mode for the AI
|
|
107
101
|
const DEFAULT_MODE = "manual";
|
|
108
102
|
|
|
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
|
-
|
|
136
103
|
interface ChatHeaderProps {
|
|
137
104
|
onNewChat: () => void;
|
|
138
105
|
activeChatId: ChatId | undefined;
|
|
@@ -520,19 +487,12 @@ const ChatPanelBody = () => {
|
|
|
520
487
|
const [pendingPrompt, setPendingPrompt] = useAtom(pendingAiPromptAtom);
|
|
521
488
|
const [input, setInput] = useState("");
|
|
522
489
|
const [newThreadInput, setNewThreadInput] = useState("");
|
|
523
|
-
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
|
|
524
490
|
const { files, addFiles, clearFiles, removeFile } = useFileState();
|
|
525
|
-
const {
|
|
526
|
-
messages: queuedMessages,
|
|
527
|
-
enqueue: enqueueUserMessage,
|
|
528
|
-
flushNext: flushNextQueuedMessage,
|
|
529
|
-
clear: clearQueuedMessages,
|
|
530
|
-
hasQueuedRef,
|
|
531
|
-
} = useMessageQueue();
|
|
532
491
|
const newThreadInputRef = useRef<ReactCodeMirrorRef>(null);
|
|
533
492
|
const newMessageInputRef = useRef<ReactCodeMirrorRef>(null);
|
|
534
493
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
535
494
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
495
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
536
496
|
const runtimeManager = useRuntimeManager();
|
|
537
497
|
const { invokeAiTool, sendRun } = useRequestClient();
|
|
538
498
|
const { openModal, closeModal } = useImperativeModal();
|
|
@@ -600,7 +560,7 @@ const ChatPanelBody = () => {
|
|
|
600
560
|
};
|
|
601
561
|
},
|
|
602
562
|
}),
|
|
603
|
-
onFinish: ({ messages
|
|
563
|
+
onFinish: ({ messages }) => {
|
|
604
564
|
setChatState((prev) => {
|
|
605
565
|
return replaceMessagesInChat({
|
|
606
566
|
chatState: prev,
|
|
@@ -608,7 +568,6 @@ const ChatPanelBody = () => {
|
|
|
608
568
|
messages: messages,
|
|
609
569
|
});
|
|
610
570
|
});
|
|
611
|
-
tryFlushQueuedMessages(messages, { isError, isAbort });
|
|
612
571
|
},
|
|
613
572
|
onToolCall: async ({ toolCall }) => {
|
|
614
573
|
await handleToolCall({
|
|
@@ -627,103 +586,23 @@ const ChatPanelBody = () => {
|
|
|
627
586
|
},
|
|
628
587
|
});
|
|
629
588
|
|
|
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
|
-
|
|
655
589
|
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
|
-
});
|
|
695
590
|
|
|
696
591
|
// Check if we're currently streaming reasoning in the latest message
|
|
697
592
|
const isStreamingReasoning =
|
|
698
593
|
isLoading && messages.length > 0 && isLastMessageReasoning(messages);
|
|
699
594
|
|
|
700
|
-
//
|
|
701
|
-
useEffect(() => {
|
|
702
|
-
setIsScrolledToBottom(true);
|
|
703
|
-
clearQueuedMessages();
|
|
704
|
-
}, [activeChatId, clearQueuedMessages]);
|
|
705
|
-
|
|
595
|
+
// Scroll to the latest chat message at the bottom
|
|
706
596
|
useEffect(() => {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
return () => cancelAnimationFrame(frame);
|
|
714
|
-
}, [messages, queuedMessages, isLoading, isScrolledToBottom, scrollToBottom]);
|
|
597
|
+
const scrollToBottom = () => {
|
|
598
|
+
if (scrollContainerRef.current) {
|
|
599
|
+
const container = scrollContainerRef.current;
|
|
600
|
+
container.scrollTop = container.scrollHeight;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
715
603
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
useEffect(() => {
|
|
719
|
-
if (isLoading) {
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
tryFlushQueuedMessages(messages, {
|
|
723
|
-
isError: error != null,
|
|
724
|
-
isAbort: false,
|
|
725
|
-
});
|
|
726
|
-
}, [messages, isLoading, error, tryFlushQueuedMessages]);
|
|
604
|
+
requestAnimationFrame(scrollToBottom);
|
|
605
|
+
}, [activeChatId]);
|
|
727
606
|
|
|
728
607
|
const startNewChatState = useEvent((initialMessage: string) => {
|
|
729
608
|
const now = Date.now();
|
|
@@ -775,7 +654,6 @@ const ChatPanelBody = () => {
|
|
|
775
654
|
setActiveChat(null);
|
|
776
655
|
setInput("");
|
|
777
656
|
setNewThreadInput("");
|
|
778
|
-
clearQueuedMessages();
|
|
779
657
|
clearFiles();
|
|
780
658
|
});
|
|
781
659
|
|
|
@@ -821,12 +699,15 @@ const ChatPanelBody = () => {
|
|
|
821
699
|
const { contextPart, attachments } = await resolveChatContext(newValue);
|
|
822
700
|
|
|
823
701
|
e?.preventDefault();
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
702
|
+
sendMessage({
|
|
703
|
+
role: "user",
|
|
704
|
+
parts: [
|
|
705
|
+
{ type: "text", text: newValue },
|
|
706
|
+
...(contextPart ? [contextPart] : []),
|
|
707
|
+
...(fileParts ?? []),
|
|
708
|
+
...attachments,
|
|
709
|
+
],
|
|
710
|
+
});
|
|
830
711
|
setInput("");
|
|
831
712
|
clearFiles();
|
|
832
713
|
},
|
|
@@ -856,11 +737,14 @@ const ChatPanelBody = () => {
|
|
|
856
737
|
setInput(newThreadInput);
|
|
857
738
|
}
|
|
858
739
|
const { contextPart, attachments } = await resolveChatContext(prompt);
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
740
|
+
sendMessage({
|
|
741
|
+
role: "user",
|
|
742
|
+
parts: [
|
|
743
|
+
{ type: "text", text: prompt },
|
|
744
|
+
...(contextPart ? [contextPart] : []),
|
|
745
|
+
...attachments,
|
|
746
|
+
],
|
|
747
|
+
});
|
|
864
748
|
});
|
|
865
749
|
|
|
866
750
|
const isNewThread = messages.length === 0;
|
|
@@ -940,62 +824,49 @@ const ChatPanelBody = () => {
|
|
|
940
824
|
/>
|
|
941
825
|
</TooltipProvider>
|
|
942
826
|
|
|
943
|
-
<div
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
{filesPills}
|
|
953
|
-
{chatInput}
|
|
954
|
-
</div>
|
|
955
|
-
<PairWithAgentCallout onPairWithAgent={handlePairWithAgent} />
|
|
827
|
+
<div
|
|
828
|
+
className="flex-1 px-3 bg-(--slate-1) gap-4 py-3 flex flex-col overflow-y-auto"
|
|
829
|
+
ref={scrollContainerRef}
|
|
830
|
+
>
|
|
831
|
+
{isNewThread && (
|
|
832
|
+
<div className="flex flex-col gap-2">
|
|
833
|
+
<div className="rounded-md border bg-background">
|
|
834
|
+
{filesPills}
|
|
835
|
+
{chatInput}
|
|
956
836
|
</div>
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
))}
|
|
837
|
+
<PairWithAgentCallout onPairWithAgent={handlePairWithAgent} />
|
|
838
|
+
</div>
|
|
839
|
+
)}
|
|
971
840
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
841
|
+
{messages.map((message, idx) => (
|
|
842
|
+
<ChatMessageDisplay
|
|
843
|
+
key={message.id}
|
|
844
|
+
message={message}
|
|
845
|
+
index={idx}
|
|
846
|
+
onEdit={handleMessageEdit}
|
|
847
|
+
isStreamingReasoning={isStreamingReasoning}
|
|
848
|
+
isLast={idx === messages.length - 1}
|
|
849
|
+
isActive={isLoading}
|
|
850
|
+
addToolApprovalResponse={addToolApprovalResponse}
|
|
851
|
+
/>
|
|
852
|
+
))}
|
|
975
853
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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>
|
|
854
|
+
{isLoading && (
|
|
855
|
+
<div className="flex justify-center py-4">
|
|
856
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
857
|
+
</div>
|
|
858
|
+
)}
|
|
991
859
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
860
|
+
{error && (
|
|
861
|
+
<div className="flex items-center justify-center space-x-2 mb-4">
|
|
862
|
+
<ErrorBanner error={error || new Error("Unknown error")} />
|
|
863
|
+
<Button variant="outline" size="sm" onClick={handleReload}>
|
|
864
|
+
Retry
|
|
865
|
+
</Button>
|
|
866
|
+
</div>
|
|
867
|
+
)}
|
|
868
|
+
|
|
869
|
+
<div ref={messagesEndRef} />
|
|
999
870
|
</div>
|
|
1000
871
|
|
|
1001
872
|
{isLoading && (
|
|
@@ -7,10 +7,9 @@ import {
|
|
|
7
7
|
isToolUIPart,
|
|
8
8
|
lastAssistantMessageIsCompleteWithApprovalResponses,
|
|
9
9
|
lastAssistantMessageIsCompleteWithToolCalls,
|
|
10
|
-
type ToolUIPart,
|
|
11
10
|
type UIMessage,
|
|
12
11
|
} from "ai";
|
|
13
|
-
import {
|
|
12
|
+
import { useState } from "react";
|
|
14
13
|
import useEvent from "react-use-event-hook";
|
|
15
14
|
import type { ProviderId } from "@/core/ai/ids/ids";
|
|
16
15
|
import type { ToolNotebookContext } from "@/core/ai/tools/base";
|
|
@@ -21,7 +20,6 @@ import type {
|
|
|
21
20
|
} from "@/core/network/types";
|
|
22
21
|
import { blobToString } from "@/utils/fileToBase64";
|
|
23
22
|
import { Logger } from "@/utils/Logger";
|
|
24
|
-
import { generateUUID } from "@/utils/uuid";
|
|
25
23
|
import { getAICompletionBodyWithAttachments } from "../editor/ai/completion-utils";
|
|
26
24
|
import { toast } from "../ui/use-toast";
|
|
27
25
|
|
|
@@ -188,44 +186,10 @@ export function hasPendingToolCalls(messages: UIMessage[]): boolean {
|
|
|
188
186
|
}
|
|
189
187
|
return (
|
|
190
188
|
lastAssistantMessageIsCompleteWithToolCalls({ messages }) ||
|
|
191
|
-
lastAssistantMessageIsCompleteWithApprovalResponses({ messages })
|
|
192
|
-
(lastPart.state === "output-denied" && !lastPart.providerExecuted)
|
|
189
|
+
lastAssistantMessageIsCompleteWithApprovalResponses({ messages })
|
|
193
190
|
);
|
|
194
191
|
}
|
|
195
192
|
|
|
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
|
-
|
|
229
193
|
export function useFileState() {
|
|
230
194
|
const [files, setFiles] = useState<File[]>([]);
|
|
231
195
|
|
|
@@ -253,76 +217,3 @@ export function useFileState() {
|
|
|
253
217
|
|
|
254
218
|
return { files, addFiles, clearFiles, removeFile };
|
|
255
219
|
}
|
|
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,10 +15,6 @@ 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;
|
|
22
18
|
}
|
|
23
19
|
|
|
24
20
|
/**
|
|
@@ -46,7 +42,7 @@ function isTransformNoop(transform: Transform | null) {
|
|
|
46
42
|
|
|
47
43
|
export const SortableCell = React.forwardRef(
|
|
48
44
|
(
|
|
49
|
-
{ cellId, canMoveX,
|
|
45
|
+
{ cellId, canMoveX, ...props }: Props,
|
|
50
46
|
ref: React.ForwardedRef<HTMLDivElement>,
|
|
51
47
|
) => {
|
|
52
48
|
// This hook re-renders every time _any_ cell is dragged,
|
|
@@ -58,7 +54,7 @@ export const SortableCell = React.forwardRef(
|
|
|
58
54
|
transform,
|
|
59
55
|
transition,
|
|
60
56
|
isDragging,
|
|
61
|
-
} = useSortable({ id: cellId.toString()
|
|
57
|
+
} = useSortable({ id: cellId.toString() });
|
|
62
58
|
|
|
63
59
|
// Perf:
|
|
64
60
|
// If the transform is a noop, keep it as null
|
|
@@ -14,20 +14,67 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
|
14
14
|
import { copyToClipboard } from "@/utils/copy";
|
|
15
15
|
import { Events } from "@/utils/events";
|
|
16
16
|
import { Tooltip } from "@/components/ui/tooltip";
|
|
17
|
+
import { assertNever } from "@/utils/assertNever";
|
|
17
18
|
import { asRemoteURL, useRuntimeManager } from "@/core/runtime/config";
|
|
18
19
|
import { API } from "@/core/network/api";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
|
|
21
|
+
type AgentTab = "claude" | "codex" | "opencode" | "prompt";
|
|
22
|
+
|
|
23
|
+
const TERMINAL_TABS = ["claude", "codex", "opencode"] as const;
|
|
24
|
+
|
|
25
|
+
function getMarimoCommand(): string {
|
|
26
|
+
return import.meta.env.DEV ? "uv run marimo" : "uvx marimo@latest";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getTerminalCommand(
|
|
30
|
+
agent: Exclude<AgentTab, "prompt">,
|
|
31
|
+
url: string,
|
|
32
|
+
withToken: boolean,
|
|
33
|
+
): string {
|
|
34
|
+
const tokenFlag = withToken ? " --with-token" : "";
|
|
35
|
+
const base = `${getMarimoCommand()} pair prompt --url '${url}'${tokenFlag}`;
|
|
36
|
+
switch (agent) {
|
|
37
|
+
case "claude":
|
|
38
|
+
return `claude "$(${base} --claude)"`;
|
|
39
|
+
case "codex":
|
|
40
|
+
return `codex "$(${base} --codex)"`;
|
|
41
|
+
case "opencode":
|
|
42
|
+
return `opencode --prompt "$(${base} --opencode)"`;
|
|
43
|
+
default:
|
|
44
|
+
assertNever(agent);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getRawPrompt(url: string, token: string | null): string {
|
|
49
|
+
const tokenHint = token
|
|
50
|
+
? `\n\nUse this auth token when calling \`execute-code.sh\`: \`execute-code.sh --url '${url}' --token '${token}'\`.`
|
|
51
|
+
: "";
|
|
52
|
+
return [
|
|
53
|
+
"Use the /marimo-pair skill to pair-program on a running marimo notebook.",
|
|
54
|
+
"",
|
|
55
|
+
`Connect to the notebook at: ${url}`,
|
|
56
|
+
"",
|
|
57
|
+
`Use \`execute-code.sh --url ${url}\` from the marimo-pair skill to execute code in the notebook.${tokenHint}`,
|
|
58
|
+
"",
|
|
59
|
+
"Once you are connected, send a fun toast (mo.status.toast(...)) to the user inside marimo letting them know you're ready to pair.",
|
|
60
|
+
].join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function maskToken(token: string): string {
|
|
64
|
+
if (token.length <= 4) {
|
|
65
|
+
return "****";
|
|
66
|
+
}
|
|
67
|
+
return `${"*".repeat(Math.min(token.length - 4, 8))}${token.slice(-4)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const SKILL_INSTALL = "npx skills add marimo-team/marimo-pair";
|
|
71
|
+
|
|
72
|
+
const AGENT_LABELS: Record<AgentTab, string> = {
|
|
73
|
+
claude: "Claude",
|
|
74
|
+
codex: "Codex",
|
|
75
|
+
opencode: "OpenCode",
|
|
76
|
+
prompt: "Prompt",
|
|
77
|
+
};
|
|
31
78
|
|
|
32
79
|
function useAuthToken(): string | null {
|
|
33
80
|
const [token, setToken] = useState<string | null>(null);
|
|
@@ -51,10 +98,7 @@ export const PairWithAgentModal: React.FC<{
|
|
|
51
98
|
const runtimeManager = useRuntimeManager();
|
|
52
99
|
const authToken = useAuthToken();
|
|
53
100
|
const hasToken = Boolean(authToken);
|
|
54
|
-
const
|
|
55
|
-
url: runtimeManager.httpURL.toString(),
|
|
56
|
-
sessionId: getSessionId(),
|
|
57
|
-
};
|
|
101
|
+
const remoteUrl = runtimeManager.httpURL.toString();
|
|
58
102
|
|
|
59
103
|
return (
|
|
60
104
|
<DialogContent className="sm:max-w-2xl">
|
|
@@ -81,7 +125,7 @@ export const PairWithAgentModal: React.FC<{
|
|
|
81
125
|
onValueChange={(v) => setActiveTab(v as AgentTab)}
|
|
82
126
|
>
|
|
83
127
|
<TabsList className="w-full">
|
|
84
|
-
{
|
|
128
|
+
{(["claude", "codex", "opencode", "prompt"] as const).map((tab) => (
|
|
85
129
|
<TabsTrigger key={tab} value={tab} className="flex-1">
|
|
86
130
|
{AGENT_LABELS[tab]}
|
|
87
131
|
</TabsTrigger>
|
|
@@ -103,7 +147,7 @@ export const PairWithAgentModal: React.FC<{
|
|
|
103
147
|
</Step>
|
|
104
148
|
<Step index={2} title="Run in your terminal">
|
|
105
149
|
<CommandBlock
|
|
106
|
-
command={getTerminalCommand(tab,
|
|
150
|
+
command={getTerminalCommand(tab, remoteUrl, hasToken)}
|
|
107
151
|
/>
|
|
108
152
|
</Step>
|
|
109
153
|
{hasToken && authToken && (
|
|
@@ -135,9 +179,9 @@ export const PairWithAgentModal: React.FC<{
|
|
|
135
179
|
}
|
|
136
180
|
>
|
|
137
181
|
<CommandBlock
|
|
138
|
-
command={getRawPrompt(
|
|
182
|
+
command={getRawPrompt(remoteUrl, authToken)}
|
|
139
183
|
display={getRawPrompt(
|
|
140
|
-
|
|
184
|
+
remoteUrl,
|
|
141
185
|
authToken ? maskToken(authToken) : null,
|
|
142
186
|
)}
|
|
143
187
|
multiline={true}
|
|
@@ -231,13 +231,6 @@ export const AddCellWithAI: React.FC<{
|
|
|
231
231
|
|
|
232
232
|
const handleDeclineCompletion = () => {
|
|
233
233
|
deleteAllStagedCells();
|
|
234
|
-
// Focus the input so the user can refine the prompt.
|
|
235
|
-
inputRef.current?.view?.focus();
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
const handleClose = () => {
|
|
239
|
-
deleteAllStagedCells();
|
|
240
|
-
onClose();
|
|
241
234
|
};
|
|
242
235
|
|
|
243
236
|
const inputComponent = (
|
|
@@ -245,7 +238,10 @@ export const AddCellWithAI: React.FC<{
|
|
|
245
238
|
<SparklesIcon className="size-4 text-(--blue-11) mr-2" />
|
|
246
239
|
<PromptInput
|
|
247
240
|
inputRef={inputRef}
|
|
248
|
-
onClose={
|
|
241
|
+
onClose={() => {
|
|
242
|
+
deleteAllStagedCells();
|
|
243
|
+
onClose();
|
|
244
|
+
}}
|
|
249
245
|
value={input}
|
|
250
246
|
onChange={(newValue) => {
|
|
251
247
|
setInput(newValue);
|
|
@@ -258,12 +254,7 @@ export const AddCellWithAI: React.FC<{
|
|
|
258
254
|
hasCompletion,
|
|
259
255
|
})}
|
|
260
256
|
/>
|
|
261
|
-
<Button
|
|
262
|
-
variant="text"
|
|
263
|
-
size="sm"
|
|
264
|
-
className="mb-0 px-1"
|
|
265
|
-
onClick={handleClose}
|
|
266
|
-
>
|
|
257
|
+
<Button variant="text" size="sm" className="mb-0 px-1" onClick={onClose}>
|
|
267
258
|
<XIcon className="size-4" />
|
|
268
259
|
</Button>
|
|
269
260
|
</div>
|