@marimo-team/islands 0.25.3-dev250 → 0.25.3-dev251
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-BBihxHrr.js → ConnectedDataExplorerComponent-DGOAQFtf.js} +11 -11
- package/dist/{ErrorBoundary-Ks8L_DFj.js → ErrorBoundary-CZ19wGWT.js} +7 -7
- package/dist/{abnfDiagram-VCTEODGH-DBcJVxYP.js → abnfDiagram-VCTEODGH-DzrZZXRD.js} +2 -2
- package/dist/{any-language-editor-jGaR0EAo.js → any-language-editor-Be7dA2VK.js} +3 -3
- package/dist/{architectureDiagram-5GKGNRK7-BcHZXF4S.js → architectureDiagram-5GKGNRK7-iF_snyFL.js} +1 -1
- package/dist/{button-B6RjDKJx.js → button-0-RqY1DW.js} +2 -1
- package/dist/{capabilities-CkVzWJEX.js → capabilities-B6yy_CBN.js} +1 -1
- package/dist/{chat-ui-DgaVxFrc.js → chat-ui-zbfp80GW.js} +1547 -1482
- package/dist/{check-Z_vtI3OL.js → check-CSvxZk5I.js} +1 -1
- package/dist/{chunk-IMKFNOWR-CTZlwBAA.js → chunk-IMKFNOWR-B7xytD4B.js} +1 -1
- package/dist/{chunk-SHT3W25Y-53g_RZHm.js → chunk-SHT3W25Y-I8ZFP5L1.js} +1 -1
- package/dist/{chunk-SVP7TREG-P6abNGV_.js → chunk-SVP7TREG-Di-uAPdT.js} +1 -1
- package/dist/{chunk-TICWLB2K-BTJJqkyF.js → chunk-TICWLB2K-BFpUy0ud.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-BUGqOrTm.js → classDiagram-ZZMXUADV-Cqm4JGpx.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-CXpQvO-s.js → classDiagram-v2-VYDZK3BY-DFHRJpN3.js} +2 -2
- package/dist/{code-visibility-CCCqFZYh.js → code-visibility-D2D_sL1T.js} +4 -4
- package/dist/{common-JXDK9IjO.js → common-DSIM5DHJ.js} +262 -262
- package/dist/{config-DbjzjwxX.js → config-mwEjAmvP.js} +2 -2
- package/dist/{copy-icon-CG57cxwb.js → copy-icon-DD8bdw4L.js} +3 -3
- package/dist/{createReducer-DePYJ0F3.js → createReducer-Bx81cw8_.js} +1 -1
- package/dist/{cynefinDiagram-5FMLGOSQ-DOcnl8wW.js → cynefinDiagram-5FMLGOSQ-cuGOeTBC.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-CuHR9QfG.js → dagre-GXQ25YYZ-DJ76aC4k.js} +1 -1
- package/dist/{data-grid-overlay-editor-QAJSeJpH.js → data-grid-overlay-editor-ByfOIO6H.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BPiG4kso.js → diagram-S7CK7UJ4-DLLc0sw7.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-_piBOvOC.js → diagram-UQ7AKVKN-Tz_d6qmk.js} +1 -1
- package/dist/{diagram-VX7I27RA-DCzVeJ8M.js → diagram-VX7I27RA-D7SzPrDN.js} +1 -1
- package/dist/{diagram-Z3DM3KII-D64CF1jQ.js → diagram-Z3DM3KII-qr37XV58.js} +1 -1
- package/dist/{dialog-DsZ49mmU.js → dialog-BHYPX87J.js} +3 -3
- package/dist/{dropdown-menu-BRiE7Vnp.js → dropdown-menu-Blu7V6Mk.js} +6 -6
- package/dist/{ebnfDiagram-PWID7BFC-BapQPML-.js → ebnfDiagram-PWID7BFC-BKOQb4J0.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-CNQvC2_Q.js → erDiagram-RLTQ6QDP-CM15Vq68.js} +1 -1
- package/dist/{error-banner-IsonnvA0.js → error-banner-DbRFOd2O.js} +2 -2
- package/dist/{error-entries-CRPfxMqm.js → error-entries-BuIE3Ius.js} +9 -9
- package/dist/{feedback-button-Bg7zGcid.js → feedback-button-VT03EJ1h.js} +7 -7
- package/dist/{flowDiagram-HODETNUW-BBt6S6oe.js → flowDiagram-HODETNUW-Dt3N5yFT.js} +2 -2
- package/dist/{formats-BK4VefeF.js → formats-gGospKWJ.js} +2 -2
- package/dist/{glide-data-editor-BuFe7QfV.js → glide-data-editor-C94G7kD7.js} +10 -10
- package/dist/{glide-utils-CQddGpa-.js → glide-utils-qbkRZpEy.js} +3 -3
- package/dist/{globals-8EK96XwF.js → globals-D7JZ5hgJ.js} +2 -2
- package/dist/{html-to-image-Gx4q3ZoD.js → html-to-image-BXrAd5q8.js} +460 -455
- package/dist/{infoDiagram-27XIBGKW-Hd8Ifz82.js → infoDiagram-27XIBGKW-C7axJMCl.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-CP7hnHWx.js → ishikawaDiagram-5VMMS53U-CdC9Eur7.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-CX91sa9b.js → kanban-definition-UXKFOSKX-BHnMIgD7.js} +1 -1
- package/dist/{loader-BisjOLiW.js → loader-BVEtQ4F3.js} +1 -1
- package/dist/main.js +236 -236
- package/dist/{mermaid-Djb3Ni9t.js → mermaid-DFYN_PXd.js} +35 -35
- package/dist/{mindmap-definition-YA3MSWOX-DgCNo07O.js → mindmap-definition-YA3MSWOX-BagHoCGU.js} +1 -1
- package/dist/{number-overlay-editor-BDJF1c8h.js → number-overlay-editor-CdcscCHp.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-SGJhnIKo.js → pegDiagram-XKGWAZYB-8IIZczip.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-1stXIWCQ.js → pieDiagram-E7YTZNPT-CE6dKBIr.js} +1 -1
- package/dist/{process-output-D2ISIyIs.js → process-output-D2ZjxrHe.js} +4 -4
- package/dist/{railroadDiagram-O6MQD6OU-DY1mojzB.js → railroadDiagram-O6MQD6OU-CkexPq3b.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-gHzhUp0D.js → requirementDiagram-BXWQKSXE-B0-OkuAe.js} +1 -1
- package/dist/{reveal-component-B_hNJfj-.js → reveal-component-Cr9euMM2.js} +8 -8
- package/dist/{select-BSxlITBo.js → select-CcidPqlM.js} +5 -5
- package/dist/{sequenceDiagram-WJ2MYXX4-rjPtD9fB.js → sequenceDiagram-WJ2MYXX4-Dxysg8DX.js} +1 -1
- package/dist/{spec-Dq8RNplp.js → spec-ZwfZcgsm.js} +4 -4
- package/dist/{stateDiagram-D77RDMKH-DMNxjUxV.js → stateDiagram-D77RDMKH-DTEN3-OJ.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-CumAaqGo.js → stateDiagram-v2-MP3YSRHH-vktk3Xc5.js} +2 -2
- package/dist/{strings-BGW-fXVJ.js → strings-BYJNyKmg.js} +3 -3
- package/dist/style.css +1 -1
- package/dist/{swimlanes-42K2YHIH-BU14_Tej.js → swimlanes-42K2YHIH-Cjveh9OM.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-CieAnPqJ.js → swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js} +2 -2
- package/dist/{swiper-component-Dir1o8ml.js → swiper-component-Dvj-goaD.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-CrVj4E1K.js → timeline-definition-24CTP7MA-KIuDcefn.js} +1 -1
- package/dist/{toast-BWIPi3aF.js → toast-CkB5Jq9K.js} +4 -4
- package/dist/{tooltip-C3UnvsRe.js → tooltip-Bzl4k00l.js} +2 -2
- package/dist/{use-restore-focus-BWO0z1lo.js → use-restore-focus-CmflVLWJ.js} +1 -1
- package/dist/{useDateFormatter-ChB4eyim.js → useDateFormatter-CYo5fm8s.js} +2 -2
- package/dist/{useIframeCapabilities-cMNQ80CK.js → useIframeCapabilities-BIolGsF1.js} +1 -1
- package/dist/{useLifecycle-BquH9ET-.js → useLifecycle-GNgvNwI-.js} +2 -2
- package/dist/{useTheme-DgOLVISn.js → useTheme-BZ2GGLgm.js} +141 -139
- package/dist/{vega-component-Bwg_Kuh-.js → vega-component-DOtem7Hf.js} +7 -7
- package/dist/{vennDiagram-4TSXK5OY-ZLxQ8yhm.js → vennDiagram-4TSXK5OY-DrIOUHfb.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-D88pcLY1.js → wardleyDiagram-VM6X3IG4-CJNjf4OH.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-_13d4g8g.js → xychartDiagram-S5SC5T6Z-DEshFho-.js} +1 -1
- package/package.json +1 -1
- package/src/__tests__/fixtures/pair-config.ts +1 -1
- package/src/components/app-config/__tests__/state.test.tsx +50 -0
- package/src/components/app-config/ai-config.tsx +1 -1
- package/src/components/chat/__tests__/capabilities-popover.test.tsx +0 -1
- package/src/components/chat/__tests__/chat-display.test.tsx +125 -0
- package/src/components/chat/__tests__/chat-markdown.test.tsx +43 -0
- package/src/components/chat/__tests__/chat-utils.test.ts +51 -1
- package/src/components/chat/acp/__tests__/agent-panel-initialization.test.tsx +1 -1
- package/src/components/chat/acp/__tests__/agent-panel.test.tsx +22 -17
- package/src/components/chat/acp/agent-panel.tsx +4 -3
- package/src/components/chat/acp/blocks.tsx +10 -10
- package/src/components/chat/acp/common.tsx +4 -5
- package/src/components/chat/acp/model-selector.tsx +1 -1
- package/src/components/chat/capabilities-popover.tsx +1 -6
- package/src/components/chat/chat-components.tsx +7 -4
- package/src/components/chat/chat-display.tsx +51 -6
- package/src/components/chat/chat-history-popover.tsx +6 -1
- package/src/components/chat/chat-markdown.tsx +15 -0
- package/src/components/chat/chat-panel.tsx +105 -119
- package/src/components/chat/chat-typography.css +108 -0
- package/src/components/chat/chat-utils.ts +8 -3
- package/src/components/chat/notebook-access-selector.tsx +1 -1
- package/src/components/chat/reasoning-accordion.tsx +3 -3
- package/src/components/chat/tool-call/tool-activity-group.tsx +49 -0
- package/src/components/chat/tool-call/tool-history-row.tsx +3 -30
- package/src/components/chat/tool-call/tool-result.tsx +1 -1
- package/src/components/chat/use-chat-generation.ts +19 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +18 -37
- package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +5 -8
- package/src/components/editor/actions/__tests__/pair-with-agent.test.tsx +5 -19
- package/src/components/editor/actions/pair-with-agent-commands.ts +0 -1
- package/src/components/editor/actions/pair-with-agent-modal.tsx +7 -11
- package/src/components/editor/actions/pair-with-agent.tsx +3 -31
- package/src/components/editor/actions/useCellActionButton.tsx +1 -1
- package/src/components/editor/ai/ai-completion-editor.tsx +1 -1
- package/src/components/editor/ai/prompt-input.tsx +189 -0
- package/src/components/editor/cell/StagedAICell.tsx +3 -3
- package/src/components/editor/cell/code/cell-editor.tsx +14 -1
- package/src/components/editor/chrome/panels/panel-header.tsx +13 -0
- package/src/components/editor/chrome/types.ts +1 -1
- package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +53 -45
- package/src/components/editor/chrome/wrapper/__tests__/useAskAiAboutCell.test.tsx +58 -0
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +89 -72
- package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +5 -8
- package/src/components/editor/chrome/wrapper/useAskAiAboutCell.ts +36 -0
- package/src/components/editor/renderers/__tests__/add-cell-buttons.test.tsx +18 -0
- package/src/components/editor/renderers/cell-array.tsx +10 -67
- package/src/components/markdown/markdown-renderer.tsx +35 -20
- package/src/components/mcp/mcp-status-indicator.tsx +18 -14
- package/src/core/ai/__tests__/staged-cells.test.ts +1 -467
- package/src/core/ai/context/context.ts +2 -0
- package/src/core/ai/context/providers/__tests__/cell.test.ts +49 -0
- package/src/core/ai/context/providers/cell.ts +54 -0
- package/src/core/ai/staged-cells.ts +4 -280
- package/src/core/ai/state.ts +3 -0
- package/src/core/codemirror/__tests__/setup.test.ts +32 -0
- package/src/core/codemirror/cells/state.ts +1 -0
- package/src/core/codemirror/cm.ts +5 -1
- package/src/core/codemirror/config/types.ts +1 -1
- package/src/core/codemirror/language/languages/python.ts +3 -3
- package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +16 -0
- package/src/core/codemirror/placeholder/extensions.ts +57 -19
- package/src/core/hotkeys/hotkeys.ts +2 -1
- package/src/core/notebook/document/__tests__/codemirror.test.ts +1 -0
- package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +1 -1
- package/src/plugins/impl/chat/chat-ui.tsx +1 -1
- package/src/components/editor/ai/__tests__/staged-cell-submission.test.ts +0 -106
- package/src/components/editor/ai/add-cell-with-ai.tsx +0 -506
- package/src/components/editor/ai/staged-cell-submission.ts +0 -46
- package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +0 -66
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { Deferred } from "@/utils/Deferred";
|
|
5
|
-
import { StagedCellSubmissionController } from "../staged-cell-submission";
|
|
6
|
-
|
|
7
|
-
describe("StagedCellSubmissionController", () => {
|
|
8
|
-
it("ignores duplicate submissions while preprocessing", async () => {
|
|
9
|
-
const preparation = new Deferred<string>();
|
|
10
|
-
const submit = vi.fn(async () => undefined);
|
|
11
|
-
const onError = vi.fn();
|
|
12
|
-
const controller = new StagedCellSubmissionController();
|
|
13
|
-
|
|
14
|
-
const first = controller.run({
|
|
15
|
-
prepare: () => preparation.promise,
|
|
16
|
-
submit,
|
|
17
|
-
onError,
|
|
18
|
-
});
|
|
19
|
-
await controller.run({
|
|
20
|
-
prepare: vi.fn(async () => "duplicate"),
|
|
21
|
-
submit,
|
|
22
|
-
onError,
|
|
23
|
-
});
|
|
24
|
-
preparation.resolve("prepared");
|
|
25
|
-
await first;
|
|
26
|
-
|
|
27
|
-
expect(submit).toHaveBeenCalledExactlyOnceWith("prepared");
|
|
28
|
-
expect(onError).not.toHaveBeenCalled();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("does not submit after cancellation during preprocessing", async () => {
|
|
32
|
-
const preparation = new Deferred<string>();
|
|
33
|
-
const submit = vi.fn(async () => undefined);
|
|
34
|
-
const onError = vi.fn();
|
|
35
|
-
const controller = new StagedCellSubmissionController();
|
|
36
|
-
|
|
37
|
-
const run = controller.run({
|
|
38
|
-
prepare: () => preparation.promise,
|
|
39
|
-
submit,
|
|
40
|
-
onError,
|
|
41
|
-
});
|
|
42
|
-
controller.cancel();
|
|
43
|
-
preparation.resolve("prepared");
|
|
44
|
-
await run;
|
|
45
|
-
|
|
46
|
-
expect(submit).not.toHaveBeenCalled();
|
|
47
|
-
expect(onError).not.toHaveBeenCalled();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("blocks another submission until a cancelled submission settles", async () => {
|
|
51
|
-
const submission = new Deferred<void>();
|
|
52
|
-
const firstSubmit = vi.fn(() => submission.promise);
|
|
53
|
-
const secondSubmit = vi.fn(async () => undefined);
|
|
54
|
-
const onError = vi.fn();
|
|
55
|
-
const controller = new StagedCellSubmissionController();
|
|
56
|
-
|
|
57
|
-
const firstRun = controller.run({
|
|
58
|
-
prepare: async () => "first",
|
|
59
|
-
submit: firstSubmit,
|
|
60
|
-
onError,
|
|
61
|
-
});
|
|
62
|
-
await vi.waitFor(() => {
|
|
63
|
-
expect(firstSubmit).toHaveBeenCalledExactlyOnceWith("first");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
controller.cancel();
|
|
67
|
-
await controller.run({
|
|
68
|
-
prepare: async () => "second",
|
|
69
|
-
submit: secondSubmit,
|
|
70
|
-
onError,
|
|
71
|
-
});
|
|
72
|
-
expect(secondSubmit).not.toHaveBeenCalled();
|
|
73
|
-
|
|
74
|
-
submission.resolve();
|
|
75
|
-
await firstRun;
|
|
76
|
-
await controller.run({
|
|
77
|
-
prepare: async () => "second",
|
|
78
|
-
submit: secondSubmit,
|
|
79
|
-
onError,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
expect(secondSubmit).toHaveBeenCalledExactlyOnceWith("second");
|
|
83
|
-
expect(onError).not.toHaveBeenCalled();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("reports preprocessing and submission errors", async () => {
|
|
87
|
-
const preprocessingError = new Error("preprocessing failed");
|
|
88
|
-
const submissionError = new Error("submission failed");
|
|
89
|
-
const onError = vi.fn();
|
|
90
|
-
const controller = new StagedCellSubmissionController();
|
|
91
|
-
|
|
92
|
-
await controller.run({
|
|
93
|
-
prepare: async () => Promise.reject(preprocessingError),
|
|
94
|
-
submit: vi.fn(),
|
|
95
|
-
onError,
|
|
96
|
-
});
|
|
97
|
-
await controller.run({
|
|
98
|
-
prepare: async () => "prepared",
|
|
99
|
-
submit: async () => Promise.reject(submissionError),
|
|
100
|
-
onError,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
expect(onError).toHaveBeenNthCalledWith(1, preprocessingError);
|
|
104
|
-
expect(onError).toHaveBeenNthCalledWith(2, submissionError);
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,506 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { useChat } from "@ai-sdk/react";
|
|
4
|
-
import {
|
|
5
|
-
autocompletion,
|
|
6
|
-
type Completion,
|
|
7
|
-
type CompletionContext,
|
|
8
|
-
type CompletionSource,
|
|
9
|
-
} from "@codemirror/autocomplete";
|
|
10
|
-
import { markdown } from "@codemirror/lang-markdown";
|
|
11
|
-
import { Prec } from "@codemirror/state";
|
|
12
|
-
import { promptHistory, storePrompt } from "@marimo-team/codemirror-ai";
|
|
13
|
-
import ReactCodeMirror, {
|
|
14
|
-
EditorView,
|
|
15
|
-
keymap,
|
|
16
|
-
minimalSetup,
|
|
17
|
-
type ReactCodeMirrorRef,
|
|
18
|
-
} from "@uiw/react-codemirror";
|
|
19
|
-
import { useAtom, useAtomValue, useStore } from "jotai";
|
|
20
|
-
import { atomWithStorage } from "jotai/utils";
|
|
21
|
-
import {
|
|
22
|
-
ChevronsUpDown,
|
|
23
|
-
DatabaseIcon,
|
|
24
|
-
SparklesIcon,
|
|
25
|
-
XIcon,
|
|
26
|
-
} from "lucide-react";
|
|
27
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
28
|
-
import useEvent from "react-use-event-hook";
|
|
29
|
-
import { DefaultChatTransport } from "ai";
|
|
30
|
-
import { z } from "zod";
|
|
31
|
-
import { AIModelDropdown } from "@/components/ai/ai-model-dropdown";
|
|
32
|
-
import { PairWithAgentButton } from "@/components/editor/actions/pair-with-agent";
|
|
33
|
-
import {
|
|
34
|
-
AddContextButton,
|
|
35
|
-
AttachFileButton,
|
|
36
|
-
FileAttachmentPill,
|
|
37
|
-
SendButton,
|
|
38
|
-
} from "@/components/chat/chat-components";
|
|
39
|
-
import {
|
|
40
|
-
buildCompletionRequestBody,
|
|
41
|
-
convertToFileUIPart,
|
|
42
|
-
PROVIDERS_THAT_SUPPORT_ATTACHMENTS,
|
|
43
|
-
useFileState,
|
|
44
|
-
} from "@/components/chat/chat-utils";
|
|
45
|
-
import { Button } from "@/components/ui/button";
|
|
46
|
-
import {
|
|
47
|
-
DropdownMenu,
|
|
48
|
-
DropdownMenuContent,
|
|
49
|
-
DropdownMenuItem,
|
|
50
|
-
DropdownMenuSeparator,
|
|
51
|
-
DropdownMenuTrigger,
|
|
52
|
-
} from "@/components/ui/dropdown-menu";
|
|
53
|
-
import { toast } from "@/components/ui/use-toast";
|
|
54
|
-
import { AiModelId } from "@/core/ai/ids/ids";
|
|
55
|
-
import { AI_SDK_UI_THROTTLE_MS } from "@/core/ai/constants";
|
|
56
|
-
import type { CompletionUIMessage } from "@/core/ai/completion-output";
|
|
57
|
-
import { useStagedCellGeneration } from "@/core/ai/staged-cells";
|
|
58
|
-
import { resourceExtension } from "@/core/codemirror/ai/resources";
|
|
59
|
-
import { aiAtom } from "@/core/config/config";
|
|
60
|
-
import { DEFAULT_AI_MODEL } from "@/core/config/config-schema";
|
|
61
|
-
import type { AiCompletionRequest } from "@/core/network/types";
|
|
62
|
-
import { useRuntimeManager } from "@/core/runtime/config";
|
|
63
|
-
import { useTheme } from "@/theme/useTheme";
|
|
64
|
-
import { cn } from "@/utils/cn";
|
|
65
|
-
import { prettyError } from "@/utils/errors";
|
|
66
|
-
import { jotaiJsonStorage } from "@/utils/storage/jotai";
|
|
67
|
-
import { ZodLocalStorage } from "@/utils/storage/typed";
|
|
68
|
-
import { PythonIcon } from "../cell/code/icons";
|
|
69
|
-
import { createAiCompletionOnKeydown } from "./completion-handlers";
|
|
70
|
-
import {
|
|
71
|
-
addContextCompletion,
|
|
72
|
-
CONTEXT_TRIGGER,
|
|
73
|
-
mentionsCompletionSource,
|
|
74
|
-
} from "./completion-utils";
|
|
75
|
-
import { StagedCellSubmissionController } from "./staged-cell-submission";
|
|
76
|
-
|
|
77
|
-
// Persist across sessions
|
|
78
|
-
const languageAtom = atomWithStorage<"python" | "sql">(
|
|
79
|
-
"marimo:ai-language",
|
|
80
|
-
"python",
|
|
81
|
-
jotaiJsonStorage,
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const KEY = "marimo:ai-prompt-history";
|
|
85
|
-
// Store the prompt history in local storage
|
|
86
|
-
const promptHistoryStorage = new ZodLocalStorage(z.array(z.string()), () => []);
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Add a cell with AI.
|
|
90
|
-
*/
|
|
91
|
-
export const AddCellWithAI: React.FC<{
|
|
92
|
-
onClose: () => void;
|
|
93
|
-
}> = ({ onClose }) => {
|
|
94
|
-
const store = useStore();
|
|
95
|
-
const [input, setInput] = useState("");
|
|
96
|
-
|
|
97
|
-
const {
|
|
98
|
-
acceptOwnedStagedCells,
|
|
99
|
-
beginStagedCellGeneration,
|
|
100
|
-
discardOwnedStagedCells,
|
|
101
|
-
finishStagedCellGeneration,
|
|
102
|
-
hasOwnedStagedCells,
|
|
103
|
-
onData,
|
|
104
|
-
} = useStagedCellGeneration(store);
|
|
105
|
-
const [language, setLanguage] = useAtom(languageAtom);
|
|
106
|
-
const runtimeManager = useRuntimeManager();
|
|
107
|
-
|
|
108
|
-
const inputRef = useRef<ReactCodeMirrorRef>(null);
|
|
109
|
-
const [submissionController] = useState(
|
|
110
|
-
() => new StagedCellSubmissionController(),
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
114
|
-
const { files, addFiles, removeFile } = useFileState();
|
|
115
|
-
const aiConfig = useAtomValue(aiAtom);
|
|
116
|
-
|
|
117
|
-
const transport = useMemo(
|
|
118
|
-
() =>
|
|
119
|
-
new DefaultChatTransport<CompletionUIMessage>({
|
|
120
|
-
api: runtimeManager.getAiURL("completion").toString(),
|
|
121
|
-
headers: () => runtimeManager.headers(),
|
|
122
|
-
prepareSendMessagesRequest: async (options) => {
|
|
123
|
-
const completionBody = await buildCompletionRequestBody(
|
|
124
|
-
options.messages,
|
|
125
|
-
);
|
|
126
|
-
const body = {
|
|
127
|
-
...completionBody,
|
|
128
|
-
id: options.id,
|
|
129
|
-
code: "",
|
|
130
|
-
prompt: "", // Don't need prompt since we are using messages
|
|
131
|
-
language: language,
|
|
132
|
-
} satisfies AiCompletionRequest;
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
api: runtimeManager.getAiURL("completion").toString(),
|
|
136
|
-
body: body,
|
|
137
|
-
};
|
|
138
|
-
},
|
|
139
|
-
}),
|
|
140
|
-
[language, runtimeManager],
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
const handleGenerationError = useEvent((error: unknown) => {
|
|
144
|
-
finishStagedCellGeneration(false);
|
|
145
|
-
toast({
|
|
146
|
-
title: "Generate with AI failed",
|
|
147
|
-
description: prettyError(error),
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const {
|
|
152
|
-
sendMessage,
|
|
153
|
-
stop: stopChat,
|
|
154
|
-
status,
|
|
155
|
-
} = useChat<CompletionUIMessage>({
|
|
156
|
-
throttle: AI_SDK_UI_THROTTLE_MS,
|
|
157
|
-
transport,
|
|
158
|
-
onData,
|
|
159
|
-
onError: handleGenerationError,
|
|
160
|
-
onFinish: ({ isAbort, isDisconnect, isError, finishReason }) => {
|
|
161
|
-
finishStagedCellGeneration(
|
|
162
|
-
!isAbort && !isDisconnect && !isError && finishReason === "stop",
|
|
163
|
-
);
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const isLoading = status === "streaming" || status === "submitted";
|
|
168
|
-
const hasCompletion = hasOwnedStagedCells();
|
|
169
|
-
const stop = useEvent(() => {
|
|
170
|
-
submissionController.cancel();
|
|
171
|
-
finishStagedCellGeneration(false);
|
|
172
|
-
void stopChat();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
const reject = useEvent(() => {
|
|
176
|
-
submissionController.cancel();
|
|
177
|
-
discardOwnedStagedCells();
|
|
178
|
-
void stopChat();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Parent-driven unmounts bypass the close handlers, so cancel in-flight work here.
|
|
182
|
-
useEffect(() => {
|
|
183
|
-
return () => {
|
|
184
|
-
stop();
|
|
185
|
-
};
|
|
186
|
-
}, [stop]);
|
|
187
|
-
|
|
188
|
-
const currentModel = aiConfig?.models?.edit_model || DEFAULT_AI_MODEL;
|
|
189
|
-
const currentProvider = AiModelId.parse(currentModel).providerId;
|
|
190
|
-
const isAttachmentSupported =
|
|
191
|
-
PROVIDERS_THAT_SUPPORT_ATTACHMENTS.has(currentProvider);
|
|
192
|
-
|
|
193
|
-
const submit = async () => {
|
|
194
|
-
if (!isLoading) {
|
|
195
|
-
await submissionController.run({
|
|
196
|
-
prepare: async () => {
|
|
197
|
-
if (inputRef.current?.view) {
|
|
198
|
-
storePrompt(inputRef.current.view);
|
|
199
|
-
}
|
|
200
|
-
return files ? await convertToFileUIPart(files) : undefined;
|
|
201
|
-
},
|
|
202
|
-
submit: async (fileParts) => {
|
|
203
|
-
beginStagedCellGeneration();
|
|
204
|
-
await sendMessage({ text: input, files: fileParts });
|
|
205
|
-
},
|
|
206
|
-
onError: handleGenerationError,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const pythonIcon = (
|
|
212
|
-
<>
|
|
213
|
-
<PythonIcon className="size-4 mr-2" />
|
|
214
|
-
Python
|
|
215
|
-
</>
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
const sqlIcon = (
|
|
219
|
-
<>
|
|
220
|
-
<DatabaseIcon className="size-4 mr-2" />
|
|
221
|
-
SQL
|
|
222
|
-
</>
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
const languageDropdown = (
|
|
226
|
-
<DropdownMenu modal={false}>
|
|
227
|
-
<DropdownMenuTrigger
|
|
228
|
-
className="flex items-center justify-between h-7 text-xs px-2 py-0.5 border rounded-md hover:text-accent-foreground"
|
|
229
|
-
data-testid="language-button"
|
|
230
|
-
>
|
|
231
|
-
{language === "python" ? pythonIcon : sqlIcon}
|
|
232
|
-
<ChevronsUpDown className="ml-1 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
233
|
-
</DropdownMenuTrigger>
|
|
234
|
-
<DropdownMenuContent align="center">
|
|
235
|
-
<div className="px-2 py-1 text-sm text-muted-foreground">
|
|
236
|
-
Select language
|
|
237
|
-
</div>
|
|
238
|
-
<DropdownMenuSeparator />
|
|
239
|
-
<DropdownMenuItem onClick={() => setLanguage("python")}>
|
|
240
|
-
{pythonIcon}
|
|
241
|
-
</DropdownMenuItem>
|
|
242
|
-
<DropdownMenuItem onClick={() => setLanguage("sql")}>
|
|
243
|
-
{sqlIcon}
|
|
244
|
-
</DropdownMenuItem>
|
|
245
|
-
</DropdownMenuContent>
|
|
246
|
-
</DropdownMenu>
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
const handleAcceptCompletion = () => {
|
|
250
|
-
if (acceptOwnedStagedCells()) {
|
|
251
|
-
onClose();
|
|
252
|
-
}
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
const handleDeclineCompletion = () => {
|
|
256
|
-
reject();
|
|
257
|
-
// Focus the input so the user can refine the prompt.
|
|
258
|
-
inputRef.current?.view?.focus();
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const handleClose = () => {
|
|
262
|
-
reject();
|
|
263
|
-
onClose();
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
const inputComponent = (
|
|
267
|
-
<div className="flex items-center px-3">
|
|
268
|
-
<SparklesIcon className="size-4 text-(--blue-11) mr-2" />
|
|
269
|
-
<PromptInput
|
|
270
|
-
inputRef={inputRef}
|
|
271
|
-
onClose={handleClose}
|
|
272
|
-
value={input}
|
|
273
|
-
onChange={(newValue) => {
|
|
274
|
-
setInput(newValue);
|
|
275
|
-
}}
|
|
276
|
-
onSubmit={submit}
|
|
277
|
-
onKeyDown={createAiCompletionOnKeydown({
|
|
278
|
-
handleAcceptCompletion,
|
|
279
|
-
handleDeclineCompletion,
|
|
280
|
-
isLoading,
|
|
281
|
-
hasCompletion,
|
|
282
|
-
})}
|
|
283
|
-
/>
|
|
284
|
-
<Button
|
|
285
|
-
variant="text"
|
|
286
|
-
size="sm"
|
|
287
|
-
className="mb-0 px-1"
|
|
288
|
-
onClick={handleClose}
|
|
289
|
-
>
|
|
290
|
-
<XIcon className="size-4" />
|
|
291
|
-
</Button>
|
|
292
|
-
</div>
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
const footerComponent = (
|
|
296
|
-
<div className="px-3 pt-1 flex flex-row items-center justify-between">
|
|
297
|
-
<div className="flex items-center gap-2">
|
|
298
|
-
<AIModelDropdown
|
|
299
|
-
triggerClassName="h-7 text-xs max-w-64"
|
|
300
|
-
iconSize="small"
|
|
301
|
-
forRole="edit"
|
|
302
|
-
showAddCustomModelDocs={true}
|
|
303
|
-
/>
|
|
304
|
-
{languageDropdown}
|
|
305
|
-
<PairWithAgentButton />
|
|
306
|
-
</div>
|
|
307
|
-
<div className="flex flex-row items-center">
|
|
308
|
-
{files.length > 0 && (
|
|
309
|
-
<div className="flex flex-row gap-1 flex-wrap pr-1">
|
|
310
|
-
{files?.map((file, index) => (
|
|
311
|
-
<FileAttachmentPill
|
|
312
|
-
file={file}
|
|
313
|
-
key={`${file.name}-${index}`}
|
|
314
|
-
onRemove={() => removeFile(file)}
|
|
315
|
-
/>
|
|
316
|
-
))}
|
|
317
|
-
</div>
|
|
318
|
-
)}
|
|
319
|
-
<AddContextButton
|
|
320
|
-
handleAddContext={() => addContextCompletion(inputRef)}
|
|
321
|
-
isLoading={isLoading}
|
|
322
|
-
/>
|
|
323
|
-
{isAttachmentSupported && (
|
|
324
|
-
<AttachFileButton
|
|
325
|
-
fileInputRef={fileInputRef}
|
|
326
|
-
isLoading={isLoading}
|
|
327
|
-
onAddFiles={addFiles}
|
|
328
|
-
/>
|
|
329
|
-
)}
|
|
330
|
-
<SendButton
|
|
331
|
-
isLoading={isLoading}
|
|
332
|
-
onStop={stop}
|
|
333
|
-
onSendClick={submit}
|
|
334
|
-
isEmpty={!input.trim()}
|
|
335
|
-
showStopLabel={true}
|
|
336
|
-
/>
|
|
337
|
-
</div>
|
|
338
|
-
</div>
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
return (
|
|
342
|
-
<div className={cn("flex flex-col w-full py-2")}>
|
|
343
|
-
{inputComponent}
|
|
344
|
-
{footerComponent}
|
|
345
|
-
</div>
|
|
346
|
-
);
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
export interface AdditionalCompletions {
|
|
350
|
-
triggerCompletionRegex: RegExp;
|
|
351
|
-
completions: Completion[];
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
interface PromptInputProps {
|
|
355
|
-
inputRef?: React.RefObject<ReactCodeMirrorRef | null>;
|
|
356
|
-
placeholder?: string;
|
|
357
|
-
value: string;
|
|
358
|
-
className?: string;
|
|
359
|
-
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
|
|
360
|
-
onClose: () => void;
|
|
361
|
-
onChange: (value: string) => void;
|
|
362
|
-
onSubmit: (e: KeyboardEvent | undefined, value: string) => void;
|
|
363
|
-
additionalCompletions?: AdditionalCompletions;
|
|
364
|
-
maxHeight?: string;
|
|
365
|
-
onAddFiles?: (files: File[]) => void;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* CodeMirror-based input for the AI prompt.
|
|
370
|
-
*
|
|
371
|
-
* This is just text (no language support), but we use codemirror to get autocomplete
|
|
372
|
-
* for @dataframe and @sql_table.
|
|
373
|
-
*/
|
|
374
|
-
export const PromptInput = ({
|
|
375
|
-
value,
|
|
376
|
-
placeholder,
|
|
377
|
-
inputRef,
|
|
378
|
-
className,
|
|
379
|
-
onChange,
|
|
380
|
-
onSubmit,
|
|
381
|
-
onKeyDown,
|
|
382
|
-
onClose,
|
|
383
|
-
onAddFiles,
|
|
384
|
-
additionalCompletions,
|
|
385
|
-
maxHeight,
|
|
386
|
-
}: PromptInputProps) => {
|
|
387
|
-
const handleSubmit = onSubmit;
|
|
388
|
-
const handleEscape = onClose;
|
|
389
|
-
const store = useStore();
|
|
390
|
-
const { theme } = useTheme();
|
|
391
|
-
|
|
392
|
-
const additionalCompletionsSource: CompletionSource = useEvent(
|
|
393
|
-
(context: CompletionContext) => {
|
|
394
|
-
if (!additionalCompletions) {
|
|
395
|
-
return null;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return mentionsCompletionSource(
|
|
399
|
-
[additionalCompletions.triggerCompletionRegex],
|
|
400
|
-
additionalCompletions.completions,
|
|
401
|
-
)(context);
|
|
402
|
-
},
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
// Changing extensions can be expensive, so
|
|
406
|
-
// it is worth making sure this is memoized well.
|
|
407
|
-
const extensions = useMemo(() => {
|
|
408
|
-
const markdownLanguage = markdown();
|
|
409
|
-
return [
|
|
410
|
-
autocompletion({}),
|
|
411
|
-
markdownLanguage,
|
|
412
|
-
resourceExtension({
|
|
413
|
-
language: markdownLanguage.language,
|
|
414
|
-
store,
|
|
415
|
-
onAddFiles,
|
|
416
|
-
}),
|
|
417
|
-
markdownLanguage.language.data.of({
|
|
418
|
-
autocomplete: additionalCompletionsSource,
|
|
419
|
-
}),
|
|
420
|
-
promptHistory({
|
|
421
|
-
storage: {
|
|
422
|
-
load: () => promptHistoryStorage.get(KEY),
|
|
423
|
-
save: (prompts) => promptHistoryStorage.set(KEY, prompts),
|
|
424
|
-
},
|
|
425
|
-
}),
|
|
426
|
-
EditorView.lineWrapping,
|
|
427
|
-
minimalSetup(),
|
|
428
|
-
Prec.highest(
|
|
429
|
-
keymap.of([
|
|
430
|
-
{
|
|
431
|
-
preventDefault: true,
|
|
432
|
-
stopPropagation: true,
|
|
433
|
-
any: (view, event) => {
|
|
434
|
-
const pressedModOrShift =
|
|
435
|
-
event.metaKey || event.ctrlKey || event.shiftKey;
|
|
436
|
-
// If no mod key is pressed, submit
|
|
437
|
-
if (event.key === "Enter" && !pressedModOrShift) {
|
|
438
|
-
handleSubmit(event, view.state.doc.toString());
|
|
439
|
-
event.preventDefault();
|
|
440
|
-
event.stopPropagation();
|
|
441
|
-
return true;
|
|
442
|
-
}
|
|
443
|
-
// Mod+Enter does add a new line already by codemirror
|
|
444
|
-
// But Shift+Enter does not, so we need to handle it manually
|
|
445
|
-
if (event.key === "Enter" && event.shiftKey) {
|
|
446
|
-
const cursorPosition = view.state.selection.main.from;
|
|
447
|
-
// Insert a new line
|
|
448
|
-
view.dispatch({
|
|
449
|
-
changes: {
|
|
450
|
-
from: cursorPosition,
|
|
451
|
-
to: cursorPosition,
|
|
452
|
-
insert: "\n",
|
|
453
|
-
},
|
|
454
|
-
selection: {
|
|
455
|
-
anchor: cursorPosition + 1,
|
|
456
|
-
head: cursorPosition + 1,
|
|
457
|
-
},
|
|
458
|
-
});
|
|
459
|
-
event.preventDefault();
|
|
460
|
-
event.stopPropagation();
|
|
461
|
-
return true;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
return false;
|
|
465
|
-
},
|
|
466
|
-
},
|
|
467
|
-
]),
|
|
468
|
-
),
|
|
469
|
-
keymap.of([
|
|
470
|
-
{
|
|
471
|
-
key: "Escape",
|
|
472
|
-
preventDefault: true,
|
|
473
|
-
stopPropagation: true,
|
|
474
|
-
run: () => {
|
|
475
|
-
handleEscape();
|
|
476
|
-
return true;
|
|
477
|
-
},
|
|
478
|
-
},
|
|
479
|
-
]),
|
|
480
|
-
];
|
|
481
|
-
}, [
|
|
482
|
-
store,
|
|
483
|
-
onAddFiles,
|
|
484
|
-
additionalCompletionsSource,
|
|
485
|
-
handleSubmit,
|
|
486
|
-
handleEscape,
|
|
487
|
-
]);
|
|
488
|
-
|
|
489
|
-
return (
|
|
490
|
-
<ReactCodeMirror
|
|
491
|
-
ref={inputRef}
|
|
492
|
-
className={cn("flex-1 font-sans overflow-auto my-1", className)}
|
|
493
|
-
width="100%"
|
|
494
|
-
maxHeight={maxHeight}
|
|
495
|
-
value={value}
|
|
496
|
-
basicSetup={false}
|
|
497
|
-
extensions={extensions}
|
|
498
|
-
onChange={onChange}
|
|
499
|
-
onKeyDown={onKeyDown}
|
|
500
|
-
theme={theme === "dark" ? "dark" : "light"}
|
|
501
|
-
placeholder={
|
|
502
|
-
placeholder || `Generate with AI, ${CONTEXT_TRIGGER} to include context`
|
|
503
|
-
}
|
|
504
|
-
/>
|
|
505
|
-
);
|
|
506
|
-
};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
interface SubmissionCallbacks<T> {
|
|
4
|
-
prepare: () => Promise<T>;
|
|
5
|
-
submit: (prepared: T) => Promise<void>;
|
|
6
|
-
onError: (error: unknown) => void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface SubmissionAttempt {
|
|
10
|
-
cancelled: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Coordinates preprocessing and submission without resuming cancelled work. */
|
|
14
|
-
export class StagedCellSubmissionController {
|
|
15
|
-
private activeAttempt: SubmissionAttempt | null = null;
|
|
16
|
-
|
|
17
|
-
public cancel() {
|
|
18
|
-
if (this.activeAttempt) {
|
|
19
|
-
this.activeAttempt.cancelled = true;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public async run<T>({ prepare, submit, onError }: SubmissionCallbacks<T>) {
|
|
24
|
-
if (this.activeAttempt !== null) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const attempt: SubmissionAttempt = { cancelled: false };
|
|
29
|
-
this.activeAttempt = attempt;
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
const prepared = await prepare();
|
|
33
|
-
if (attempt.cancelled) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
await submit(prepared);
|
|
38
|
-
} catch (error) {
|
|
39
|
-
if (!attempt.cancelled) {
|
|
40
|
-
onError(error);
|
|
41
|
-
}
|
|
42
|
-
} finally {
|
|
43
|
-
this.activeAttempt = null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|