@marimo-team/islands 0.25.3-dev265 → 0.25.3-dev267
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-C_hj7Poi.js → ConnectedDataExplorerComponent-C4yCN38h.js} +1 -1
- package/dist/{ErrorBoundary-DzYPyiOs.js → ErrorBoundary-CewXPyL5.js} +64 -69
- package/dist/{abnfDiagram-VCTEODGH-DzrZZXRD.js → abnfDiagram-VCTEODGH-o6Ay6Q63.js} +2 -2
- package/dist/{any-language-editor-CFeWfC3o.js → any-language-editor-DnG26QXh.js} +1 -1
- package/dist/{architectureDiagram-5GKGNRK7-iF_snyFL.js → architectureDiagram-5GKGNRK7-BhiwwlVg.js} +1 -1
- package/dist/{chat-ui-DFxNgzXF.js → chat-ui-CbgR5ivy.js} +1964 -1740
- package/dist/{chunk-IMKFNOWR-B7xytD4B.js → chunk-IMKFNOWR-WMSMf4Nj.js} +1 -1
- package/dist/{chunk-SHT3W25Y-I8ZFP5L1.js → chunk-SHT3W25Y-BRfADrzD.js} +1 -1
- package/dist/{chunk-SVP7TREG-Di-uAPdT.js → chunk-SVP7TREG-CuK5QFre.js} +1 -1
- package/dist/{chunk-TICWLB2K-BFpUy0ud.js → chunk-TICWLB2K-Bcun_jhJ.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-Cqm4JGpx.js → classDiagram-ZZMXUADV-CgWDbUDT.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-DFHRJpN3.js → classDiagram-v2-VYDZK3BY-DYmc9SFU.js} +2 -2
- package/dist/{code-visibility-CJExrSFe.js → code-visibility-BKBl9Rh6.js} +9 -9
- package/dist/{common-5mM7a8cm.js → common-N3Jd_6m9.js} +5944 -5626
- package/dist/{cynefinDiagram-5FMLGOSQ-cuGOeTBC.js → cynefinDiagram-5FMLGOSQ-PPKWipjT.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-DJ76aC4k.js → dagre-GXQ25YYZ-niRBZh--.js} +1 -1
- package/dist/{data-grid-overlay-editor--Un2Lk6m.js → data-grid-overlay-editor-Cp59CrqZ.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-DLLc0sw7.js → diagram-S7CK7UJ4-DPiP9rot.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-Tz_d6qmk.js → diagram-UQ7AKVKN-B0WZuaxV.js} +1 -1
- package/dist/{diagram-VX7I27RA-D7SzPrDN.js → diagram-VX7I27RA-Rn42WBuN.js} +1 -1
- package/dist/{diagram-Z3DM3KII-qr37XV58.js → diagram-Z3DM3KII-BB93--ry.js} +1 -1
- package/dist/{dropdown-menu-CkJ7ErY6.js → dropdown-menu-BpImEfn3.js} +1211 -1205
- package/dist/{ebnfDiagram-PWID7BFC-BKOQb4J0.js → ebnfDiagram-PWID7BFC-V4t5h6mZ.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-CM15Vq68.js → erDiagram-RLTQ6QDP-C1MsAKXd.js} +1 -1
- package/dist/{error-banner-DbRFOd2O.js → error-banner-DPc9eIw5.js} +3 -3
- package/dist/{error-entries-CMoaUX9V.js → error-entries-CJGdD2ky.js} +49 -42
- package/dist/{feedback-button-C2NUXbzh.js → feedback-button-Qc1f9iPq.js} +70 -70
- package/dist/{flowDiagram-HODETNUW-Dt3N5yFT.js → flowDiagram-HODETNUW-DHjeJP96.js} +2 -2
- package/dist/{glide-data-editor-Dj9U-NK9.js → glide-data-editor-hCq3K0sd.js} +2042 -2041
- package/dist/{glide-utils-Ba9WD2IN.js → glide-utils-DC3sk6EX.js} +28 -34
- package/dist/{globals-CkbsxIrq.js → globals-B2_uMYDY.js} +1 -1
- package/dist/{html-to-image-D_5CMreP.js → html-to-image-S7spRQCi.js} +5526 -5828
- package/dist/{infoDiagram-27XIBGKW-C7axJMCl.js → infoDiagram-27XIBGKW-Dovv6iKx.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-CdC9Eur7.js → ishikawaDiagram-5VMMS53U-D2nyygsc.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-BHnMIgD7.js → kanban-definition-UXKFOSKX-KyVx0uTY.js} +1 -1
- package/dist/main.js +1288 -1287
- package/dist/{mermaid-DFYN_PXd.js → mermaid-B-jKSwGt.js} +33 -33
- package/dist/{mindmap-definition-YA3MSWOX-BagHoCGU.js → mindmap-definition-YA3MSWOX-Bz9tKQFl.js} +1 -1
- package/dist/{number-overlay-editor-H8QAAmdr.js → number-overlay-editor-CfNYvbdz.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-8IIZczip.js → pegDiagram-XKGWAZYB-C_qpq6Tv.js} +2 -2
- package/dist/pencil-DcT8AE0h.js +10 -0
- package/dist/{pieDiagram-E7YTZNPT-CE6dKBIr.js → pieDiagram-E7YTZNPT-Bp9ucgI4.js} +1 -1
- package/dist/{process-output-CGFNVC60.js → process-output-f34bkXTm.js} +2 -2
- package/dist/{railroadDiagram-O6MQD6OU-CkexPq3b.js → railroadDiagram-O6MQD6OU-tq-VEH6j.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-B0-OkuAe.js → requirementDiagram-BXWQKSXE-DW-vHadb.js} +1 -1
- package/dist/{reveal-component-BperrCFw.js → reveal-component-CUxv-kkP.js} +9 -9
- package/dist/{sequenceDiagram-WJ2MYXX4-Dxysg8DX.js → sequenceDiagram-WJ2MYXX4-Dv9U9qRP.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-DTEN3-OJ.js → stateDiagram-D77RDMKH-DTmPQ0EB.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-vktk3Xc5.js → stateDiagram-v2-MP3YSRHH-CkCif0Iw.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-Cjveh9OM.js → swimlanes-42K2YHIH-21kCq6l8.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js → swimlanesDiagram-VR7AAH4N-BpRJP6KU.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-KIuDcefn.js → timeline-definition-24CTP7MA-Ckgh4lS4.js} +1 -1
- package/dist/{toast-CY1_Qv41.js → toast-DpczUryP.js} +1 -1
- package/dist/{vega-component-C-lqjV0q.js → vega-component-CF9kI6C4.js} +1 -1
- package/dist/{vennDiagram-4TSXK5OY-DrIOUHfb.js → vennDiagram-4TSXK5OY-CDei2Bt3.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-CJNjf4OH.js → wardleyDiagram-VM6X3IG4-CXavT601.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-DEshFho-.js → xychartDiagram-S5SC5T6Z-CvJhcZri.js} +1 -1
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-display.test.tsx +157 -86
- package/src/components/chat/__tests__/chat-utils.test.ts +42 -0
- package/src/components/chat/__tests__/turn-timing.test.ts +84 -0
- package/src/components/chat/__tests__/use-assistant-turn.test.ts +95 -0
- package/src/components/chat/__tests__/user-message.test.tsx +27 -0
- package/src/components/chat/acp/agent-panel.tsx +2 -0
- package/src/components/chat/agent-activity.tsx +132 -0
- package/src/components/chat/chat-display.tsx +125 -96
- package/src/components/chat/chat-panel.tsx +123 -106
- package/src/components/chat/chat-utils.ts +12 -24
- package/src/components/chat/tool-call/__tests__/tool-labels.test.ts +76 -0
- package/src/components/chat/tool-call/__tests__/tool-step.test.tsx +57 -0
- package/src/components/chat/tool-call/shared.ts +6 -0
- package/src/components/chat/tool-call/tool-call-view.tsx +22 -76
- package/src/components/chat/tool-call/tool-labels.ts +172 -0
- package/src/components/chat/tool-call/tool-step.tsx +145 -0
- package/src/components/chat/turn-timing.ts +73 -0
- package/src/components/chat/use-assistant-turn.ts +65 -0
- package/src/components/chat/use-chat-generation.ts +5 -4
- package/src/components/chat/user-message.tsx +145 -0
- package/src/components/editor/actions/__tests__/useCellActionButton.test.tsx +18 -1
- package/src/components/editor/actions/useCellActionButton.tsx +3 -0
- package/src/components/editor/ai/completion-handlers.tsx +0 -37
- package/src/components/editor/cell/StagedAICell.tsx +51 -144
- package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +193 -32
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +2 -2
- package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +145 -81
- package/src/components/editor/chrome/wrapper/pending-ai-changes-badge.tsx +25 -0
- package/src/components/editor/chrome/wrapper/sidebar.tsx +6 -0
- package/src/components/editor/links/cell-link.tsx +4 -4
- package/src/components/editor/notebook-cell.tsx +3 -11
- package/src/components/ui/toast.tsx +1 -1
- package/src/core/ai/__tests__/staged-cells.test.ts +74 -126
- package/src/core/ai/staged-cells.ts +48 -38
- package/src/core/cells/scrollCellIntoView.ts +6 -0
- package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +59 -0
- package/src/core/codemirror/placeholder/extensions.ts +23 -0
- package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +34 -0
- package/src/core/session/useNotebookSession.tsx +7 -2
- package/src/css/app/Cell.css +7 -36
- package/src/plugins/impl/chat/chat-ui.tsx +2 -3
- package/src/plugins/impl/common/error-banner.tsx +5 -3
- package/src/components/chat/reasoning-accordion.tsx +0 -61
- package/src/components/chat/tool-call/tool-activity-group.tsx +0 -49
- package/src/components/chat/tool-call/tool-error-card.tsx +0 -81
- package/src/components/chat/tool-call/tool-history-row.tsx +0 -147
- package/src/components/icons/loading-ellipsis.tsx +0 -46
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
21
21
|
import useEvent from "react-use-event-hook";
|
|
22
22
|
import { Button } from "@/components/ui/button";
|
|
23
|
-
import { LoadingEllipsis } from "@/components/icons/loading-ellipsis";
|
|
24
23
|
import { replaceMessagesInChat } from "@/core/ai/chat-utils";
|
|
25
24
|
import { useAIConfigActions } from "@/core/ai/config";
|
|
26
25
|
import { AI_SDK_UI_THROTTLE_MS } from "@/core/ai/constants";
|
|
@@ -43,7 +42,6 @@ import { useRequestClient } from "@/core/network/requests";
|
|
|
43
42
|
import { useRuntimeManager } from "@/core/runtime/config";
|
|
44
43
|
import { isWasm } from "@/core/wasm/utils";
|
|
45
44
|
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
46
|
-
import { cn } from "@/utils/cn";
|
|
47
45
|
import { Logger } from "@/utils/Logger";
|
|
48
46
|
import { AIModelDropdown } from "../ai/ai-model-dropdown";
|
|
49
47
|
import { useOpenSettingsToTab } from "../app-config/state";
|
|
@@ -52,6 +50,7 @@ import {
|
|
|
52
50
|
usePairWithAgentModal,
|
|
53
51
|
} from "../editor/actions/pair-with-agent";
|
|
54
52
|
import { PromptInput } from "../editor/ai/prompt-input";
|
|
53
|
+
import { PendingAICells } from "../editor/chrome/wrapper/pending-ai-cells";
|
|
55
54
|
import {
|
|
56
55
|
addContextCompletion,
|
|
57
56
|
isContextAttachment,
|
|
@@ -76,7 +75,8 @@ import {
|
|
|
76
75
|
getAbortReasonFromChunk,
|
|
77
76
|
resolveChatAbortReason,
|
|
78
77
|
} from "./chat-abort";
|
|
79
|
-
import {
|
|
78
|
+
import { AgentActivity } from "./agent-activity";
|
|
79
|
+
import { getAnswerText, renderUIMessage } from "./chat-display";
|
|
80
80
|
import { ChatHistoryPopover } from "./chat-history-popover";
|
|
81
81
|
import { CapabilitiesPopover } from "./capabilities-popover";
|
|
82
82
|
import { NotebookAccessSelector } from "./notebook-access-selector";
|
|
@@ -87,7 +87,6 @@ import {
|
|
|
87
87
|
handleToolCall,
|
|
88
88
|
hasPendingToolCalls,
|
|
89
89
|
hasUnresolvedToolCalls,
|
|
90
|
-
isLastMessageReasoning,
|
|
91
90
|
PROVIDERS_THAT_SUPPORT_ATTACHMENTS,
|
|
92
91
|
type QueuedUserMessage,
|
|
93
92
|
shouldFlushQueue,
|
|
@@ -98,7 +97,9 @@ import { getCodes } from "@/core/codemirror/copilot/getCodes";
|
|
|
98
97
|
import { focusInputAndMoveToEnd } from "@/core/codemirror/utils";
|
|
99
98
|
import ScrollToBottomButton from "./acp/scroll-to-bottom-button";
|
|
100
99
|
import { useChatControllerId } from "./use-chat-controller-id";
|
|
101
|
-
import { useChatGeneration } from "./use-chat-generation";
|
|
100
|
+
import { isChatBusy, useChatGeneration } from "./use-chat-generation";
|
|
101
|
+
import { useAssistantTurn } from "./use-assistant-turn";
|
|
102
|
+
import { UserMessage } from "./user-message";
|
|
102
103
|
|
|
103
104
|
// Default mode for the AI
|
|
104
105
|
const DEFAULT_MODE = "ask";
|
|
@@ -190,9 +191,9 @@ interface ChatMessageProps {
|
|
|
190
191
|
message: UIMessage;
|
|
191
192
|
index: number;
|
|
192
193
|
onEdit: (index: number, newValue: string) => void;
|
|
193
|
-
isStreamingReasoning: boolean;
|
|
194
194
|
isLast: boolean;
|
|
195
195
|
isActive: boolean;
|
|
196
|
+
turnStartedAt: number | null;
|
|
196
197
|
addToolApprovalResponse?: ChatAddToolApproveResponseFunction;
|
|
197
198
|
}
|
|
198
199
|
|
|
@@ -201,78 +202,40 @@ const ChatMessageDisplay: React.FC<ChatMessageProps> = memo(
|
|
|
201
202
|
message,
|
|
202
203
|
index,
|
|
203
204
|
onEdit,
|
|
204
|
-
isStreamingReasoning,
|
|
205
205
|
isLast,
|
|
206
206
|
isActive,
|
|
207
|
+
turnStartedAt,
|
|
207
208
|
addToolApprovalResponse,
|
|
208
209
|
}) => {
|
|
209
|
-
|
|
210
|
-
const textParts = message.parts?.filter(
|
|
211
|
-
(p): p is TextUIPart => p.type === "text",
|
|
212
|
-
);
|
|
213
|
-
const content = textParts?.map((p) => p.text).join("\n");
|
|
214
|
-
const fileParts = message.parts?.filter(
|
|
215
|
-
(p): p is FileUIPart => p.type === "file",
|
|
216
|
-
);
|
|
217
|
-
|
|
210
|
+
if (message.role === "user") {
|
|
218
211
|
return (
|
|
219
|
-
<div className="
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
key={message.id}
|
|
225
|
-
value={content}
|
|
226
|
-
placeholder="Type your message. Use @ to reference variables and cells."
|
|
227
|
-
onChange={() => {
|
|
228
|
-
// noop
|
|
229
|
-
}}
|
|
230
|
-
onSubmit={(_e, newValue) => {
|
|
231
|
-
if (!newValue.trim()) {
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
onEdit(index, newValue);
|
|
235
|
-
}}
|
|
236
|
-
onClose={() => {
|
|
237
|
-
// noop
|
|
238
|
-
}}
|
|
212
|
+
<div className="group flex justify-end">
|
|
213
|
+
<UserMessage
|
|
214
|
+
message={message}
|
|
215
|
+
canEdit={!isActive}
|
|
216
|
+
onEdit={(newValue) => onEdit(index, newValue)}
|
|
239
217
|
/>
|
|
240
218
|
</div>
|
|
241
219
|
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const renderOtherMessage = (message: UIMessage) => {
|
|
245
|
-
const textParts = message.parts.filter(
|
|
246
|
-
(p): p is TextUIPart => p.type === "text",
|
|
247
|
-
);
|
|
248
|
-
const content = textParts.map((p) => p.text).join("\n");
|
|
220
|
+
}
|
|
249
221
|
|
|
250
|
-
|
|
222
|
+
return (
|
|
223
|
+
<div className="group relative flex justify-start">
|
|
251
224
|
<div className="w-[95%] wrap-break-word">
|
|
252
225
|
<div className="absolute right-1 top-1 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity">
|
|
253
|
-
<CopyClipboardIcon
|
|
226
|
+
<CopyClipboardIcon
|
|
227
|
+
className="h-3 w-3"
|
|
228
|
+
value={() => getAnswerText(message)}
|
|
229
|
+
/>
|
|
254
230
|
</div>
|
|
255
231
|
{renderUIMessage({
|
|
256
232
|
message,
|
|
257
|
-
isStreamingReasoning,
|
|
258
233
|
isLast,
|
|
259
234
|
isActive,
|
|
235
|
+
turnStartedAt,
|
|
260
236
|
addToolApprovalResponse,
|
|
261
237
|
})}
|
|
262
238
|
</div>
|
|
263
|
-
);
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
return (
|
|
267
|
-
<div
|
|
268
|
-
className={cn(
|
|
269
|
-
"flex group relative",
|
|
270
|
-
message.role === "user" ? "justify-end" : "justify-start",
|
|
271
|
-
)}
|
|
272
|
-
>
|
|
273
|
-
{message.role === "user"
|
|
274
|
-
? renderUserMessage(message)
|
|
275
|
-
: renderOtherMessage(message)}
|
|
276
239
|
</div>
|
|
277
240
|
);
|
|
278
241
|
},
|
|
@@ -483,6 +446,8 @@ const ChatPanelBody = () => {
|
|
|
483
446
|
lastAbortReasonRef.current = null;
|
|
484
447
|
setAbortNotice(null);
|
|
485
448
|
});
|
|
449
|
+
const turn = useAssistantTurn();
|
|
450
|
+
const resetTurn = turn.reset;
|
|
486
451
|
const runtimeManager = useRuntimeManager();
|
|
487
452
|
const { invokeAiTool } = useRequestClient();
|
|
488
453
|
|
|
@@ -505,6 +470,7 @@ const ChatPanelBody = () => {
|
|
|
505
470
|
error,
|
|
506
471
|
status,
|
|
507
472
|
regenerate,
|
|
473
|
+
setMessages,
|
|
508
474
|
stop,
|
|
509
475
|
addToolOutput,
|
|
510
476
|
addToolApprovalResponse,
|
|
@@ -512,7 +478,8 @@ const ChatPanelBody = () => {
|
|
|
512
478
|
} = useChat({
|
|
513
479
|
id: chatControllerId,
|
|
514
480
|
throttle: AI_SDK_UI_THROTTLE_MS,
|
|
515
|
-
sendAutomaticallyWhen: ({ messages }) =>
|
|
481
|
+
sendAutomaticallyWhen: ({ messages }) =>
|
|
482
|
+
!turn.hasEnded && hasPendingToolCalls(messages),
|
|
516
483
|
messages: activeChat?.messages || [], // initial messages
|
|
517
484
|
transport: new StreamingChunkTransport(
|
|
518
485
|
{
|
|
@@ -559,7 +526,26 @@ const ChatPanelBody = () => {
|
|
|
559
526
|
}
|
|
560
527
|
},
|
|
561
528
|
),
|
|
562
|
-
onFinish: ({
|
|
529
|
+
onFinish: ({
|
|
530
|
+
messages: finishedMessages,
|
|
531
|
+
isError,
|
|
532
|
+
isAbort,
|
|
533
|
+
finishReason,
|
|
534
|
+
}) => {
|
|
535
|
+
// useChat routes every chat's callbacks to the latest one. Its
|
|
536
|
+
// setMessages isn't a React setter: the updater runs synchronously.
|
|
537
|
+
const finished: { messages?: typeof finishedMessages } = {};
|
|
538
|
+
setMessages((current) => {
|
|
539
|
+
if (current.at(-1)?.id !== finishedMessages.at(-1)?.id) {
|
|
540
|
+
return current;
|
|
541
|
+
}
|
|
542
|
+
finished.messages = turn.finish(finishedMessages, { isAbort, isError });
|
|
543
|
+
return finished.messages;
|
|
544
|
+
});
|
|
545
|
+
const { messages } = finished;
|
|
546
|
+
if (!messages) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
563
549
|
setChatState((prev) => {
|
|
564
550
|
return replaceMessagesInChat({
|
|
565
551
|
chatState: prev,
|
|
@@ -604,6 +590,7 @@ const ChatPanelBody = () => {
|
|
|
604
590
|
|
|
605
591
|
const sendUserMessage = useEvent((parts: ChatMessagePart[]) => {
|
|
606
592
|
clearAbortState();
|
|
593
|
+
turn.start();
|
|
607
594
|
sendMessage({ role: "user", parts });
|
|
608
595
|
});
|
|
609
596
|
|
|
@@ -628,11 +615,43 @@ const ChatPanelBody = () => {
|
|
|
628
615
|
},
|
|
629
616
|
);
|
|
630
617
|
|
|
631
|
-
const isLoading =
|
|
618
|
+
const isLoading = isChatBusy(status);
|
|
619
|
+
const isTurnActive = turn.isActive({
|
|
620
|
+
isLoading,
|
|
621
|
+
hasError: error != null,
|
|
622
|
+
messages,
|
|
623
|
+
});
|
|
624
|
+
useChatGeneration(isTurnActive);
|
|
625
|
+
|
|
626
|
+
const stopTurn = useEvent(() => {
|
|
627
|
+
if (isLoading) {
|
|
628
|
+
turn.stop();
|
|
629
|
+
void stop();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
// Between requests there is no stream to abort, so `onFinish` won't run.
|
|
633
|
+
const stopped = turn.finish(messages, { isAbort: true, isError: false });
|
|
634
|
+
setMessages(stopped);
|
|
635
|
+
setChatState((prev) =>
|
|
636
|
+
replaceMessagesInChat({
|
|
637
|
+
chatState: prev,
|
|
638
|
+
chatId: prev.activeChatId,
|
|
639
|
+
messages: stopped,
|
|
640
|
+
}),
|
|
641
|
+
);
|
|
642
|
+
tryFlushQueuedMessages(stopped, { isError: false, isAbort: true });
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const respondToApproval = useEvent(
|
|
646
|
+
(response: Parameters<ChatAddToolApproveResponseFunction>[0]) => {
|
|
647
|
+
turn.resume(messages.at(-1));
|
|
648
|
+
return addToolApprovalResponse(response);
|
|
649
|
+
},
|
|
650
|
+
);
|
|
632
651
|
// useEvent reads the latest loading state, even when called after an await
|
|
633
652
|
// (e.g. resolving @-context) that outlives the original render's closure.
|
|
634
653
|
const submitOrQueue = useEvent((parts: ChatMessagePart[]) => {
|
|
635
|
-
if (
|
|
654
|
+
if (isTurnActive || hasQueuedRef.current) {
|
|
636
655
|
enqueueUserMessage(parts);
|
|
637
656
|
} else {
|
|
638
657
|
sendUserMessage(parts);
|
|
@@ -665,10 +684,6 @@ const ChatPanelBody = () => {
|
|
|
665
684
|
});
|
|
666
685
|
});
|
|
667
686
|
|
|
668
|
-
// Check if we're currently streaming reasoning in the latest message
|
|
669
|
-
const isStreamingReasoning =
|
|
670
|
-
isLoading && messages.length > 0 && isLastMessageReasoning(messages);
|
|
671
|
-
|
|
672
687
|
// Pin to the bottom while the user is already there.
|
|
673
688
|
useEffect(() => {
|
|
674
689
|
setIsScrolledToBottom(true);
|
|
@@ -676,6 +691,11 @@ const ChatPanelBody = () => {
|
|
|
676
691
|
clearAbortState();
|
|
677
692
|
}, [activeChatId, clearQueuedMessages, clearAbortState]);
|
|
678
693
|
|
|
694
|
+
// Creating a chat keeps the draft's controller, so its turn survives.
|
|
695
|
+
useEffect(() => {
|
|
696
|
+
resetTurn();
|
|
697
|
+
}, [chatControllerId, resetTurn]);
|
|
698
|
+
|
|
679
699
|
useEffect(() => {
|
|
680
700
|
if (!isScrolledToBottom) {
|
|
681
701
|
return;
|
|
@@ -730,6 +750,7 @@ const ChatPanelBody = () => {
|
|
|
730
750
|
const { contextPart, attachments } =
|
|
731
751
|
await resolveChatContext(initialMessage);
|
|
732
752
|
|
|
753
|
+
turn.start();
|
|
733
754
|
sendMessage({
|
|
734
755
|
role: "user",
|
|
735
756
|
parts: [
|
|
@@ -787,6 +808,7 @@ const ChatPanelBody = () => {
|
|
|
787
808
|
const { contextPart, attachments } = await resolveChatContext(newValue);
|
|
788
809
|
|
|
789
810
|
const messageId = editedMessage.id;
|
|
811
|
+
turn.start();
|
|
790
812
|
sendMessage({
|
|
791
813
|
messageId: messageId, // replace the message
|
|
792
814
|
role: "user",
|
|
@@ -825,6 +847,7 @@ const ChatPanelBody = () => {
|
|
|
825
847
|
|
|
826
848
|
const handleReload = () => {
|
|
827
849
|
clearAbortState();
|
|
850
|
+
turn.start();
|
|
828
851
|
regenerate();
|
|
829
852
|
};
|
|
830
853
|
|
|
@@ -889,8 +912,8 @@ const ChatPanelBody = () => {
|
|
|
889
912
|
inputRef={newThreadInputRef}
|
|
890
913
|
setInput={setNewThreadInput}
|
|
891
914
|
onSubmit={handleNewThreadSubmit}
|
|
892
|
-
isLoading={
|
|
893
|
-
onStop={
|
|
915
|
+
isLoading={isTurnActive}
|
|
916
|
+
onStop={stopTurn}
|
|
894
917
|
fileInputRef={fileInputRef}
|
|
895
918
|
onAddFiles={addFiles}
|
|
896
919
|
onClose={handleOnCloseThread}
|
|
@@ -901,8 +924,8 @@ const ChatPanelBody = () => {
|
|
|
901
924
|
setInput={setInput}
|
|
902
925
|
onSubmit={handleChatInputSubmit}
|
|
903
926
|
inputRef={newMessageInputRef}
|
|
904
|
-
isLoading={
|
|
905
|
-
onStop={
|
|
927
|
+
isLoading={isTurnActive}
|
|
928
|
+
onStop={stopTurn}
|
|
906
929
|
onClose={() => newMessageInputRef.current?.editor?.blur()}
|
|
907
930
|
fileInputRef={fileInputRef}
|
|
908
931
|
onAddFiles={addFiles}
|
|
@@ -946,18 +969,35 @@ const ChatPanelBody = () => {
|
|
|
946
969
|
ref={scrollContainerRef}
|
|
947
970
|
onScroll={handleScroll}
|
|
948
971
|
>
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
972
|
+
<TooltipProvider>
|
|
973
|
+
{messages.map((message, idx) => {
|
|
974
|
+
const isLast = idx === messages.length - 1;
|
|
975
|
+
return (
|
|
976
|
+
<ChatMessageDisplay
|
|
977
|
+
key={message.id}
|
|
978
|
+
message={message}
|
|
979
|
+
index={idx}
|
|
980
|
+
onEdit={handleMessageEdit}
|
|
981
|
+
isLast={isLast}
|
|
982
|
+
isActive={isTurnActive}
|
|
983
|
+
turnStartedAt={isLast ? turn.startedAt : null}
|
|
984
|
+
addToolApprovalResponse={respondToApproval}
|
|
985
|
+
/>
|
|
986
|
+
);
|
|
987
|
+
})}
|
|
988
|
+
</TooltipProvider>
|
|
989
|
+
|
|
990
|
+
{isTurnActive && messages.at(-1)?.role === "user" && (
|
|
991
|
+
<AgentActivity
|
|
992
|
+
running={true}
|
|
993
|
+
thinking={true}
|
|
994
|
+
timing={
|
|
995
|
+
turn.startedAt == null
|
|
996
|
+
? undefined
|
|
997
|
+
: { startedAt: turn.startedAt }
|
|
998
|
+
}
|
|
959
999
|
/>
|
|
960
|
-
)
|
|
1000
|
+
)}
|
|
961
1001
|
|
|
962
1002
|
{queuedMessages.map((message) => (
|
|
963
1003
|
<QueuedMessageDisplay key={message.id} message={message} />
|
|
@@ -990,30 +1030,7 @@ const ChatPanelBody = () => {
|
|
|
990
1030
|
/>
|
|
991
1031
|
</div>
|
|
992
1032
|
|
|
993
|
-
{
|
|
994
|
-
<div className="flex shrink-0 items-center gap-2 px-3 pt-2 text-xs text-muted-foreground">
|
|
995
|
-
<div className="flex items-center gap-1.5">
|
|
996
|
-
<div aria-hidden={true}>
|
|
997
|
-
<LoadingEllipsis
|
|
998
|
-
size={3}
|
|
999
|
-
gap="gap-0.5"
|
|
1000
|
-
className="motion-reduce:[&_svg]:animate-none"
|
|
1001
|
-
/>
|
|
1002
|
-
</div>
|
|
1003
|
-
<output>Working</output>
|
|
1004
|
-
</div>
|
|
1005
|
-
<span aria-hidden={true} className="h-3 w-px bg-border" />
|
|
1006
|
-
<Button
|
|
1007
|
-
variant="text"
|
|
1008
|
-
size="xs"
|
|
1009
|
-
className="chat-control"
|
|
1010
|
-
onClick={stop}
|
|
1011
|
-
>
|
|
1012
|
-
Stop generation
|
|
1013
|
-
</Button>
|
|
1014
|
-
</div>
|
|
1015
|
-
)}
|
|
1016
|
-
|
|
1033
|
+
{!isTurnActive && <PendingAICells />}
|
|
1017
1034
|
<div className="shrink-0 p-3">{composer}</div>
|
|
1018
1035
|
</div>
|
|
1019
1036
|
);
|
|
@@ -57,30 +57,6 @@ export async function convertToFileUIPart(
|
|
|
57
57
|
return fileUIParts;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export function isLastMessageReasoning(messages: UIMessage[]): boolean {
|
|
61
|
-
if (messages.length === 0) {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const lastMessage = messages.at(-1);
|
|
66
|
-
if (!lastMessage) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (lastMessage.role !== "assistant" || !lastMessage.parts) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const parts = lastMessage.parts;
|
|
75
|
-
if (parts.length === 0) {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Check if the last part is reasoning
|
|
80
|
-
const lastPart = parts[parts.length - 1];
|
|
81
|
-
return lastPart.type === "reasoning";
|
|
82
|
-
}
|
|
83
|
-
|
|
84
60
|
function stringifyTextParts(parts: UIMessage["parts"]): string {
|
|
85
61
|
return parts
|
|
86
62
|
.map((part) => (part.type === "text" ? part.text : ""))
|
|
@@ -198,6 +174,18 @@ export function hasPendingToolCalls(messages: UIMessage[]): boolean {
|
|
|
198
174
|
);
|
|
199
175
|
}
|
|
200
176
|
|
|
177
|
+
export function willAutoContinue(messages: UIMessage[]): boolean {
|
|
178
|
+
const lastMessage = messages.at(-1);
|
|
179
|
+
const lastPart =
|
|
180
|
+
lastMessage?.role === "assistant" ? lastMessage.parts.at(-1) : undefined;
|
|
181
|
+
return (
|
|
182
|
+
hasPendingToolCalls(messages) ||
|
|
183
|
+
(lastPart != null &&
|
|
184
|
+
isToolUIPart(lastPart) &&
|
|
185
|
+
lastPart.state === "input-available")
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
201
189
|
/**
|
|
202
190
|
* True when the assistant is still waiting on tool execution or user approval.
|
|
203
191
|
* Unlike `hasPendingToolCalls` (ready to auto-resume), these states must block
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { describeToolCall } from "../tool-labels";
|
|
5
|
+
|
|
6
|
+
describe("describeToolCall", () => {
|
|
7
|
+
it("names notebook edits by operation and shows the first line of code", () => {
|
|
8
|
+
const input = {
|
|
9
|
+
edit: {
|
|
10
|
+
type: "add_cell",
|
|
11
|
+
code: "\n def fib(n):\n return n",
|
|
12
|
+
position: {},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
expect(
|
|
16
|
+
describeToolCall("tool-edit_notebook_tool", input, "done"),
|
|
17
|
+
).toMatchObject({
|
|
18
|
+
label: "Added cell",
|
|
19
|
+
detail: "def fib(n):",
|
|
20
|
+
});
|
|
21
|
+
expect(
|
|
22
|
+
describeToolCall("tool-edit_notebook_tool", input, "running").label,
|
|
23
|
+
).toBe("Adding cell");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("falls back to a generic edit label while the input is still streaming", () => {
|
|
27
|
+
expect(
|
|
28
|
+
describeToolCall("tool-edit_notebook_tool", { edit: {} }, "running"),
|
|
29
|
+
).toEqual(expect.objectContaining({ label: "Editing notebook" }));
|
|
30
|
+
expect(
|
|
31
|
+
describeToolCall("tool-edit_notebook_tool", { edit: {} }, "running"),
|
|
32
|
+
).not.toHaveProperty("detail");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("ignores inherited object keys in tool names and edit types", () => {
|
|
36
|
+
expect(describeToolCall("tool-constructor", {}, "done")).toMatchObject({
|
|
37
|
+
label: "Called",
|
|
38
|
+
detail: "constructor",
|
|
39
|
+
});
|
|
40
|
+
expect(
|
|
41
|
+
describeToolCall(
|
|
42
|
+
"edit_notebook_tool",
|
|
43
|
+
{ edit: { type: "toString" } },
|
|
44
|
+
"done",
|
|
45
|
+
).label,
|
|
46
|
+
).toBe("Edited notebook");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("summarizes code-mode runs by their first meaningful line", () => {
|
|
50
|
+
const code =
|
|
51
|
+
"import marimo._code_mode as cm\n# add cells\ncm.add_cell('x = 1')";
|
|
52
|
+
expect(
|
|
53
|
+
describeToolCall("tool-execute_code", { code }, "done"),
|
|
54
|
+
).toMatchObject({
|
|
55
|
+
label: "Ran code",
|
|
56
|
+
detail: "cm.add_cell('x = 1')",
|
|
57
|
+
});
|
|
58
|
+
expect(
|
|
59
|
+
describeToolCall("execute_code", { code: "import numpy as np" }, "done")
|
|
60
|
+
.detail,
|
|
61
|
+
).toBe("import numpy as np");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("uses plain language for known tools", () => {
|
|
65
|
+
expect(describeToolCall("lint_notebook", {}, "done").label).toBe(
|
|
66
|
+
"Linted notebook",
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("shows the raw name for unknown tools such as MCP tools", () => {
|
|
71
|
+
expect(describeToolCall("tool-search_docs", {}, "running")).toMatchObject({
|
|
72
|
+
label: "Calling",
|
|
73
|
+
detail: "search_docs",
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { ToolStep } from "../tool-step";
|
|
6
|
+
|
|
7
|
+
const edit = { edit: { type: "add_cell", code: "x = 1", position: {} } };
|
|
8
|
+
|
|
9
|
+
describe("ToolStep", () => {
|
|
10
|
+
it("describes the call and reveals its input and output on demand", () => {
|
|
11
|
+
render(
|
|
12
|
+
<ToolStep
|
|
13
|
+
toolName="tool-edit_notebook_tool"
|
|
14
|
+
state="output-available"
|
|
15
|
+
input={edit}
|
|
16
|
+
result={{ status: "success" }}
|
|
17
|
+
/>,
|
|
18
|
+
);
|
|
19
|
+
const row = screen.getByRole("button", { name: /Added cell/ });
|
|
20
|
+
expect(row.getAttribute("aria-expanded")).toBe("false");
|
|
21
|
+
expect(screen.queryByText("Tool Request")).toBeNull();
|
|
22
|
+
|
|
23
|
+
fireEvent.click(row);
|
|
24
|
+
expect(row.getAttribute("aria-expanded")).toBe("true");
|
|
25
|
+
expect(screen.getByText("Tool Request")).toBeTruthy();
|
|
26
|
+
expect(screen.getByText("Tool Result")).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("shows the error text of a failed call", () => {
|
|
30
|
+
render(
|
|
31
|
+
<ToolStep
|
|
32
|
+
toolName="lint_notebook"
|
|
33
|
+
state="output-error"
|
|
34
|
+
input={{}}
|
|
35
|
+
errorText="Linter crashed"
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
fireEvent.click(
|
|
39
|
+
screen.getByRole("button", { name: /Linted notebook.*Failed/ }),
|
|
40
|
+
);
|
|
41
|
+
expect(screen.getByText("Linter crashed")).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("marks calls cut off by the end of the turn as stopped", () => {
|
|
45
|
+
render(
|
|
46
|
+
<ToolStep
|
|
47
|
+
toolName="tool-edit_notebook_tool"
|
|
48
|
+
state="input-available"
|
|
49
|
+
input={edit}
|
|
50
|
+
isActive={false}
|
|
51
|
+
/>,
|
|
52
|
+
);
|
|
53
|
+
expect(
|
|
54
|
+
screen.getByRole("button", { name: /Adding cell.*Stopped/ }),
|
|
55
|
+
).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -8,6 +8,12 @@ export type ToolState = ToolUIPart["state"];
|
|
|
8
8
|
// rather than exporting a named type, so we derive ours from there.
|
|
9
9
|
export type ToolApproval = NonNullable<ToolUIPart["approval"]>;
|
|
10
10
|
|
|
11
|
+
export const PENDING_TOOL_STATES: ReadonlySet<ToolState> = new Set([
|
|
12
|
+
"input-streaming",
|
|
13
|
+
"input-available",
|
|
14
|
+
"approval-responded",
|
|
15
|
+
]);
|
|
16
|
+
|
|
11
17
|
export function formatToolName(toolName: string): string {
|
|
12
18
|
return toolName.replace("tool-", "");
|
|
13
19
|
}
|