@marimo-team/islands 0.19.8-dev41 → 0.19.8-dev49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/assets/__vite-browser-external-WSlCcXn_.js +1 -0
  2. package/dist/assets/worker-DUYMdbtA.js +73 -0
  3. package/dist/main.js +1740 -1691
  4. package/dist/style.css +1 -1
  5. package/dist/{useDeepCompareMemoize-CMGprt3H.js → useDeepCompareMemoize-BhZZsis0.js} +7 -3
  6. package/dist/{vega-component-DU3aSp4m.js → vega-component-DCxUyPnb.js} +1 -1
  7. package/package.json +1 -1
  8. package/src/components/app-config/optional-features.tsx +1 -1
  9. package/src/components/chat/__tests__/useFileState.test.tsx +93 -0
  10. package/src/components/chat/acp/agent-panel.tsx +26 -77
  11. package/src/components/chat/chat-components.tsx +114 -1
  12. package/src/components/chat/chat-panel.tsx +32 -104
  13. package/src/components/chat/chat-utils.ts +42 -0
  14. package/src/components/editor/ai/add-cell-with-ai.tsx +85 -53
  15. package/src/components/editor/ai/ai-completion-editor.tsx +15 -38
  16. package/src/components/editor/chrome/panels/packages-panel.tsx +12 -9
  17. package/src/core/islands/__tests__/bridge.test.ts +7 -2
  18. package/src/core/islands/bridge.ts +1 -1
  19. package/src/core/islands/main.ts +7 -0
  20. package/src/core/network/types.ts +2 -2
  21. package/src/core/wasm/bridge.ts +1 -1
  22. package/src/core/websocket/useMarimoKernelConnection.tsx +5 -15
  23. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +86 -167
  24. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +37 -123
  25. package/src/plugins/impl/anywidget/__tests__/model.test.ts +128 -122
  26. package/src/{utils/__tests__/data-views.test.ts → plugins/impl/anywidget/__tests__/serialization.test.ts} +42 -96
  27. package/src/plugins/impl/anywidget/model.ts +348 -223
  28. package/src/plugins/impl/anywidget/schemas.ts +32 -0
  29. package/src/{utils/data-views.ts → plugins/impl/anywidget/serialization.ts} +13 -36
  30. package/src/plugins/impl/anywidget/types.ts +27 -0
  31. package/src/plugins/impl/chat/chat-ui.tsx +22 -20
  32. package/src/utils/Deferred.ts +21 -0
  33. package/src/utils/json/base64.ts +38 -8
  34. package/dist/assets/__vite-browser-external-6-UwTyQC.js +0 -1
  35. package/dist/assets/worker-D3e5wDxM.js +0 -73
@@ -33,15 +33,19 @@ function isWasm() {
33
33
  var Deferred = class {
34
34
  constructor() {
35
35
  __publicField(this, "status", "pending");
36
+ __publicField(this, "value");
36
37
  this.promise = new Promise((e, x) => {
37
38
  this.reject = (e2) => {
38
39
  this.status = "rejected", x(e2);
39
40
  }, this.resolve = (x2) => {
40
- this.status = "resolved", e(x2);
41
+ this.status = "resolved", isPromiseLike(x2) || (this.value = x2), e(x2);
41
42
  };
42
43
  });
43
44
  }
44
45
  };
46
+ function isPromiseLike(e) {
47
+ return typeof e == "object" && !!e && "then" in e && typeof e.then == "function";
48
+ }
45
49
  const WebSocketState = {
46
50
  NOT_STARTED: "NOT_STARTED",
47
51
  CONNECTING: "CONNECTING",
@@ -147,7 +151,7 @@ var require__u64 = /* @__PURE__ */ __commonJSMin(((e) => {
147
151
  })), require_crypto = /* @__PURE__ */ __commonJSMin(((e) => {
148
152
  Object.defineProperty(e, "__esModule", { value: true }), e.crypto = void 0, e.crypto = typeof globalThis == "object" && "crypto" in globalThis ? globalThis.crypto : void 0;
149
153
  })), require_utils = /* @__PURE__ */ __commonJSMin(((e) => {
150
- Object.defineProperty(e, "__esModule", { value: true }), e.wrapXOFConstructorWithOpts = e.wrapConstructorWithOpts = e.wrapConstructor = e.Hash = e.nextTick = e.swap32IfBE = e.byteSwapIfBE = e.swap8IfBE = e.isLE = void 0, e.isBytes = S, e.anumber = C, e.abytes = w, e.ahash = T, e.aexists = E, e.aoutput = D, e.u8 = O, e.u32 = k, e.clean = A, e.createView = j, e.rotr = M, e.rotl = N, e.byteSwap = P, e.byteSwap32 = F, e.bytesToHex = R, e.hexToBytes = V, e.asyncLoop = U, e.utf8ToBytes = W, e.bytesToUtf8 = G, e.toBytes = K, e.kdfInputToBytes = q, e.concatBytes = J, e.checkOpts = Y, e.createHasher = X, e.createOptHasher = Z, e.createXOFer = Q, e.randomBytes = $;
154
+ Object.defineProperty(e, "__esModule", { value: true }), e.wrapXOFConstructorWithOpts = e.wrapConstructorWithOpts = e.wrapConstructor = e.Hash = e.nextTick = e.swap32IfBE = e.byteSwapIfBE = e.swap8IfBE = e.isLE = void 0, e.isBytes = S, e.anumber = C, e.abytes = w, e.ahash = T, e.aexists = E, e.aoutput = D, e.u8 = O, e.u32 = k, e.clean = A, e.createView = j, e.rotr = M, e.rotl = N, e.byteSwap = P, e.byteSwap32 = F, e.bytesToHex = R, e.hexToBytes = V, e.asyncLoop = H, e.utf8ToBytes = W, e.bytesToUtf8 = G, e.toBytes = K, e.kdfInputToBytes = q, e.concatBytes = J, e.checkOpts = Y, e.createHasher = X, e.createOptHasher = Z, e.createXOFer = Q, e.randomBytes = $;
151
155
  var x = require_crypto();
152
156
  function S(e2) {
153
157
  return e2 instanceof Uint8Array || ArrayBuffer.isView(e2) && e2.constructor.name === "Uint8Array";
@@ -238,7 +242,7 @@ var require__u64 = /* @__PURE__ */ __commonJSMin(((e) => {
238
242
  }
239
243
  e.nextTick = async () => {
240
244
  };
241
- async function U(x2, S2, C2) {
245
+ async function H(x2, S2, C2) {
242
246
  let w2 = Date.now();
243
247
  for (let T2 = 0; T2 < x2; T2++) {
244
248
  C2(T2);
@@ -2,7 +2,7 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_react } from "./react-Bs6Z0kvn.js";
3
3
  import { t as require_compiler_runtime } from "./compiler-runtime-B_OLMU9S.js";
4
4
  import "./Combination-BTMrlhzT.js";
5
- import { S as CircleQuestionMark, a as AlertTitle, m as asRemoteURL, n as arrow, o as isValid, r as Alert, t as useDeepCompareMemoize } from "./useDeepCompareMemoize-CMGprt3H.js";
5
+ import { S as CircleQuestionMark, a as AlertTitle, m as asRemoteURL, n as arrow, o as isValid, r as Alert, t as useDeepCompareMemoize } from "./useDeepCompareMemoize-BhZZsis0.js";
6
6
  import { l as Events } from "./button-Cy0ElmIm.js";
7
7
  import { o as Objects, s as Logger } from "./hotkeys-B5WnGZXF.js";
8
8
  import { t as require_jsx_runtime } from "./jsx-runtime-CTBg5pdT.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.19.8-dev41",
3
+ "version": "0.19.8-dev49",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -211,7 +211,7 @@ const InstallButton: React.FC<{
211
211
  return pkg.name;
212
212
  })
213
213
  .join(" ");
214
- const response = await addPackage({ package: packageSpec, dev: true });
214
+ const response = await addPackage({ package: packageSpec, group: "dev" });
215
215
  if (response.success) {
216
216
  onSuccess();
217
217
  toast({
@@ -0,0 +1,93 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { toast } from "@/components/ui/use-toast";
6
+ import { useFileState } from "../chat-utils";
7
+
8
+ vi.mock("@/components/ui/use-toast", () => ({
9
+ toast: vi.fn(),
10
+ }));
11
+
12
+ describe("useFileState", () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ it("initializes with no files", () => {
18
+ const { result } = renderHook(() => useFileState());
19
+ expect(result.current.files).toEqual([]);
20
+ });
21
+
22
+ it("adds files when under size limit", () => {
23
+ const { result } = renderHook(() => useFileState());
24
+ const fileA = { name: "a.txt", size: 10 } as File;
25
+ const fileB = { name: "b.txt", size: 20 } as File;
26
+
27
+ act(() => result.current.addFiles([fileA, fileB]));
28
+
29
+ expect(result.current.files).toEqual([fileA, fileB]);
30
+ expect(toast).not.toHaveBeenCalled();
31
+ });
32
+
33
+ it("appends new files to existing files", () => {
34
+ const { result } = renderHook(() => useFileState());
35
+ const fileA = { name: "a.txt", size: 10 } as File;
36
+ const fileB = { name: "b.txt", size: 20 } as File;
37
+
38
+ act(() => result.current.addFiles([fileA]));
39
+ act(() => result.current.addFiles([fileB]));
40
+
41
+ expect(result.current.files).toEqual([fileA, fileB]);
42
+ });
43
+
44
+ it("ignores empty file list", () => {
45
+ const { result } = renderHook(() => useFileState());
46
+ const fileA = { name: "a.txt", size: 10 } as File;
47
+
48
+ act(() => result.current.addFiles([fileA]));
49
+ act(() => result.current.addFiles([]));
50
+
51
+ expect(result.current.files).toEqual([fileA]);
52
+ });
53
+
54
+ it("shows toast and skips adding when size exceeds limit", () => {
55
+ const { result } = renderHook(() => useFileState());
56
+ const bigFile = {
57
+ name: "big.txt",
58
+ size: 1024 * 1024 * 50 + 1, // > 50MB
59
+ } as File;
60
+
61
+ act(() => result.current.addFiles([bigFile]));
62
+
63
+ expect(result.current.files).toEqual([]);
64
+ expect(toast).toHaveBeenCalledWith({
65
+ title: "File size exceeded",
66
+ description: "Attachments must be under 50 MB",
67
+ variant: "danger",
68
+ });
69
+ });
70
+
71
+ it("removes a file", () => {
72
+ const { result } = renderHook(() => useFileState());
73
+ const fileA = { name: "a.txt", size: 10 } as File;
74
+ const fileB = { name: "b.txt", size: 20 } as File;
75
+
76
+ act(() => result.current.addFiles([fileA, fileB]));
77
+ act(() => result.current.removeFile(fileA));
78
+
79
+ expect(result.current.files).toEqual([fileB]);
80
+ });
81
+
82
+ it("clears all files", () => {
83
+ const { result } = renderHook(() => useFileState());
84
+ const fileA = { name: "a.txt", size: 10 } as File;
85
+ const fileB = { name: "b.txt", size: 20 } as File;
86
+
87
+ act(() => result.current.addFiles([fileA, fileB]));
88
+ expect(result.current.files).toEqual([fileA, fileB]);
89
+
90
+ act(() => result.current.clearFiles());
91
+ expect(result.current.files).toEqual([]);
92
+ });
93
+ });
@@ -3,12 +3,8 @@
3
3
  import { useAtom, useAtomValue } from "jotai";
4
4
  import { capitalize } from "lodash-es";
5
5
  import {
6
- AtSignIcon,
7
6
  BotMessageSquareIcon,
8
- PaperclipIcon,
9
7
  RefreshCwIcon,
10
- SendIcon,
11
- SquareIcon,
12
8
  StopCircleIcon,
13
9
  } from "lucide-react";
14
10
  import React, { memo, useEffect, useMemo, useRef, useState } from "react";
@@ -25,8 +21,7 @@ import {
25
21
  import { PanelEmptyState } from "@/components/editor/chrome/panels/empty-state";
26
22
  import { Spinner } from "@/components/icons/spinner";
27
23
  import { Button } from "@/components/ui/button";
28
- import { Input } from "@/components/ui/input";
29
- import { Tooltip, TooltipProvider } from "@/components/ui/tooltip";
24
+ import { TooltipProvider } from "@/components/ui/tooltip";
30
25
  import { cn } from "@/utils/cn";
31
26
  import { Logger } from "@/utils/Logger";
32
27
  import { AgentDocs } from "./agent-docs";
@@ -70,7 +65,13 @@ import { store } from "@/core/state/jotai";
70
65
  import { ErrorBanner } from "@/plugins/impl/common/error-banner";
71
66
  import { Functions } from "@/utils/functions";
72
67
  import { Paths } from "@/utils/paths";
73
- import { FileAttachmentPill } from "../chat-components";
68
+ import {
69
+ AddContextButton,
70
+ AttachFileButton,
71
+ FileAttachmentPill,
72
+ SendButton,
73
+ } from "../chat-components";
74
+ import { useFileState } from "../chat-utils";
74
75
  import { ReadyToChatBlock } from "./blocks";
75
76
  import {
76
77
  convertFilesToResourceLinks,
@@ -89,9 +90,6 @@ import type {
89
90
 
90
91
  const logger = Logger.get("agents");
91
92
 
92
- // File attachment constants
93
- const SUPPORTED_ATTACHMENT_TYPES = ["image/*", "text/*"];
94
-
95
93
  interface AgentTitleProps {
96
94
  currentAgentId?: ExternalAgentId;
97
95
  }
@@ -392,55 +390,21 @@ const PromptArea = memo<PromptAreaProps>(
392
390
  )}
393
391
  </div>
394
392
  <div className="flex flex-row">
395
- <Tooltip content="Add context">
396
- <Button
397
- variant="text"
398
- size="icon"
399
- onClick={handleAddContext}
400
- disabled={isLoading}
401
- >
402
- <AtSignIcon className="h-3.5 w-3.5" />
403
- </Button>
404
- </Tooltip>
405
- <Tooltip content="Attach a file">
406
- <Button
407
- variant="text"
408
- size="icon"
409
- className="cursor-pointer"
410
- onClick={() => fileInputRef.current?.click()}
411
- title="Attach a file"
412
- disabled={isLoading}
413
- >
414
- <PaperclipIcon className="h-3.5 w-3.5" />
415
- </Button>
416
- </Tooltip>
417
- <Input
418
- ref={fileInputRef}
419
- type="file"
420
- multiple={true}
421
- hidden={true}
422
- onChange={(event) => {
423
- if (event.target.files) {
424
- onAddFiles([...event.target.files]);
425
- }
426
- }}
427
- accept={SUPPORTED_ATTACHMENT_TYPES.join(",")}
393
+ <AddContextButton
394
+ handleAddContext={handleAddContext}
395
+ isLoading={isLoading}
396
+ />
397
+ <AttachFileButton
398
+ fileInputRef={fileInputRef}
399
+ isLoading={isLoading}
400
+ onAddFiles={onAddFiles}
401
+ />
402
+ <SendButton
403
+ isLoading={isLoading}
404
+ onStop={onStop}
405
+ onSendClick={handleSendClick}
406
+ isEmpty={!promptValue.trim()}
428
407
  />
429
- <Tooltip content={isLoading ? "Stop" : "Submit"}>
430
- <Button
431
- variant="text"
432
- size="sm"
433
- className="h-6 w-6 p-0 hover:bg-muted/30 cursor-pointer"
434
- onClick={isLoading ? onStop : handleSendClick}
435
- disabled={isLoading ? false : !promptValue.trim()}
436
- >
437
- {isLoading ? (
438
- <SquareIcon className="h-3 w-3 fill-current" />
439
- ) : (
440
- <SendIcon className="h-3 w-3" />
441
- )}
442
- </Button>
443
- </Tooltip>
444
408
  </div>
445
409
  </div>
446
410
  </TooltipProvider>
@@ -664,7 +628,7 @@ const AgentPanel: React.FC = () => {
664
628
  const [isLoading, setIsLoading] = useState(false);
665
629
  const [error, setError] = useState<Error | string | null>(null);
666
630
  const [promptValue, setPromptValue] = useState("");
667
- const [files, setFiles] = useState<File[]>();
631
+ const { files, addFiles, clearFiles, removeFile } = useFileState();
668
632
  const [sessionModels, setSessionModels] = useState<SessionModelState | null>(
669
633
  null,
670
634
  );
@@ -891,7 +855,7 @@ const AgentPanel: React.FC = () => {
891
855
  });
892
856
  setIsLoading(true);
893
857
  setPromptValue("");
894
- setFiles(undefined);
858
+ clearFiles();
895
859
 
896
860
  // Update session title with first message if it's still the default
897
861
  if (selectedTab?.title.startsWith("New ")) {
@@ -967,21 +931,6 @@ const AgentPanel: React.FC = () => {
967
931
  setIsLoading(false);
968
932
  });
969
933
 
970
- // Handler for adding files
971
- const handleAddFiles = useEvent((newFiles: File[]) => {
972
- if (newFiles.length === 0) {
973
- return;
974
- }
975
- setFiles((prev) => [...(prev ?? []), ...newFiles]);
976
- });
977
-
978
- // Handler for removing files
979
- const handleRemoveFile = useEvent((fileToRemove: File) => {
980
- if (files) {
981
- setFiles(files.filter((f) => f !== fileToRemove));
982
- }
983
- });
984
-
985
934
  // Handler for manual connect
986
935
  const handleManualConnect = useEvent(() => {
987
936
  logger.debug("Manual connect requested", {
@@ -1148,7 +1097,7 @@ const AgentPanel: React.FC = () => {
1148
1097
  <FileAttachmentPill
1149
1098
  file={file}
1150
1099
  key={file.name}
1151
- onRemove={() => handleRemoveFile(file)}
1100
+ onRemove={() => removeFile(file)}
1152
1101
  />
1153
1102
  ))}
1154
1103
  </div>
@@ -1160,7 +1109,7 @@ const AgentPanel: React.FC = () => {
1160
1109
  promptValue={promptValue}
1161
1110
  onPromptValueChange={setPromptValue}
1162
1111
  onPromptSubmit={handlePromptSubmit}
1163
- onAddFiles={handleAddFiles}
1112
+ onAddFiles={addFiles}
1164
1113
  onStop={handleStop}
1165
1114
  fileInputRef={fileInputRef}
1166
1115
  commands={availableCommands}
@@ -1,9 +1,23 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import type { FileUIPart } from "ai";
4
- import { FileIcon, FileTextIcon, ImageIcon, XIcon } from "lucide-react";
4
+ import {
5
+ AtSignIcon,
6
+ FileIcon,
7
+ FileTextIcon,
8
+ ImageIcon,
9
+ PaperclipIcon,
10
+ SendHorizontalIcon,
11
+ StopCircleIcon,
12
+ XIcon,
13
+ } from "lucide-react";
5
14
  import { useState } from "react";
6
15
  import { cn } from "@/utils/cn";
16
+ import { Spinner } from "../icons/spinner";
17
+ import { Button } from "../ui/button";
18
+ import { Input } from "../ui/input";
19
+ import { Tooltip } from "../ui/tooltip";
20
+ import { SUPPORTED_ATTACHMENT_TYPES } from "./chat-utils";
7
21
 
8
22
  export const AttachmentRenderer = ({
9
23
  attachment,
@@ -58,6 +72,105 @@ export const FileAttachmentPill = ({
58
72
  );
59
73
  };
60
74
 
75
+ export const SendButton = ({
76
+ isLoading,
77
+ onStop,
78
+ onSendClick,
79
+ isEmpty,
80
+ showStopLabel = false, // Show a stop label and spinner instead of just the stop icon when loading
81
+ }: {
82
+ isLoading: boolean;
83
+ onStop: () => void;
84
+ onSendClick: () => void;
85
+ isEmpty: boolean;
86
+ showStopLabel?: boolean;
87
+ }) => {
88
+ const loadingContent = showStopLabel ? (
89
+ <div className="flex flex-row items-center gap-1 px-1.5">
90
+ <span className="text-xs text-error">Stop</span>
91
+ <Spinner size="small" />
92
+ </div>
93
+ ) : (
94
+ <StopCircleIcon className="size-4 text-error" />
95
+ );
96
+
97
+ return (
98
+ <Tooltip content={isLoading ? "Stop" : "Submit"}>
99
+ <Button
100
+ variant="text"
101
+ size="sm"
102
+ className="h-6 min-w-6 p-0 hover:bg-muted/30 cursor-pointer"
103
+ onClick={isLoading ? onStop : onSendClick}
104
+ disabled={isLoading ? false : isEmpty}
105
+ >
106
+ {isLoading ? (
107
+ loadingContent
108
+ ) : (
109
+ <SendHorizontalIcon className="h-3.5 w-3.5" />
110
+ )}
111
+ </Button>
112
+ </Tooltip>
113
+ );
114
+ };
115
+
116
+ export const AddContextButton = ({
117
+ handleAddContext,
118
+ isLoading,
119
+ }: {
120
+ handleAddContext: () => void;
121
+ isLoading: boolean;
122
+ }) => {
123
+ return (
124
+ <Tooltip content="Add context">
125
+ <Button
126
+ variant="text"
127
+ size="icon"
128
+ onClick={handleAddContext}
129
+ disabled={isLoading}
130
+ >
131
+ <AtSignIcon className="h-3.5 w-3.5" />
132
+ </Button>
133
+ </Tooltip>
134
+ );
135
+ };
136
+
137
+ export const AttachFileButton = ({
138
+ fileInputRef,
139
+ isLoading,
140
+ onAddFiles,
141
+ }: {
142
+ fileInputRef: React.RefObject<HTMLInputElement | null>;
143
+ isLoading: boolean;
144
+ onAddFiles: (files: File[]) => void;
145
+ }) => {
146
+ return (
147
+ <>
148
+ <Tooltip content="Attach a file">
149
+ <Button
150
+ variant="text"
151
+ size="icon"
152
+ onClick={() => fileInputRef.current?.click()}
153
+ disabled={isLoading}
154
+ >
155
+ <PaperclipIcon className="h-3.5 w-3.5" />
156
+ </Button>
157
+ </Tooltip>
158
+ <Input
159
+ ref={fileInputRef}
160
+ type="file"
161
+ multiple={true}
162
+ hidden={true}
163
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
164
+ if (event.target.files) {
165
+ onAddFiles([...event.target.files]);
166
+ }
167
+ }}
168
+ accept={SUPPORTED_ATTACHMENT_TYPES.join(",")}
169
+ />
170
+ </>
171
+ );
172
+ };
173
+
61
174
  function renderFileIcon(file: File): React.ReactNode {
62
175
  const classNames = "h-3 w-3 mt-0.5";
63
176