@marimo-team/islands 0.23.9-dev9 → 0.23.10-dev0

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 (101) hide show
  1. package/dist/{ConnectedDataExplorerComponent-OzrfMM5L.js → ConnectedDataExplorerComponent-CyV83R2m.js} +4 -4
  2. package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +1 -0
  3. package/dist/assets/{worker-CpBbwbQo.js → worker-ip3AI_sN.js} +2 -2
  4. package/dist/{chat-ui-BDI3FMI8.js → chat-ui-ChD4VvCo.js} +3060 -3033
  5. package/dist/{code-visibility-DgHF4q8X.js → code-visibility-CjGICDxg.js} +1368 -1204
  6. package/dist/{formats-DQ5qjo_Q.js → formats-DHxc-FdY.js} +1 -1
  7. package/dist/{glide-data-editor-DqRY9naW.js → glide-data-editor-BOmK9ETQ.js} +2 -2
  8. package/dist/{html-to-image-CiSinpSR.js → html-to-image-BHv7CEU_.js} +2145 -2153
  9. package/dist/{input-CZD2z6X2.js → input-_2sjvfne.js} +1 -1
  10. package/dist/main.js +680 -705
  11. package/dist/{mermaid-IU93XzmY.js → mermaid-lXOw5Py9.js} +2 -2
  12. package/dist/{process-output-5qJjMRKh.js → process-output-BvySRgli.js} +33 -25
  13. package/dist/{reveal-component-qpHJES_u.js → reveal-component-DVWED--8.js} +312 -291
  14. package/dist/{spec-a6DaqW__.js → spec-B96zNUEA.js} +1 -1
  15. package/dist/style.css +1 -1
  16. package/dist/{toDate-ZVVIBmdk.js → toDate-x-WRDCH7.js} +1 -1
  17. package/dist/{useAsyncData-C008zUPi.js → useAsyncData-iRgKDT5s.js} +1 -1
  18. package/dist/{useDeepCompareMemoize-BrA3_n61.js → useDeepCompareMemoize-CkQ57VS2.js} +1 -1
  19. package/dist/{useLifecycle-BNaoJ5a4.js → useLifecycle-BBO9PIph.js} +1 -1
  20. package/dist/{useTheme-7O0YWlE5.js → useTheme-DHIrRQOe.js} +34 -21
  21. package/dist/{vega-component-DJNmOdUj.js → vega-component-Dq-SH463.js} +5 -5
  22. package/package.json +1 -1
  23. package/src/components/ai/__tests__/ai-utils.test.ts +43 -38
  24. package/src/components/ai/ai-model-dropdown.tsx +2 -2
  25. package/src/components/app-config/ai-config.tsx +147 -16
  26. package/src/components/app-config/user-config-form.tsx +37 -1
  27. package/src/components/chat/__tests__/chat-utils.test.ts +269 -0
  28. package/src/components/chat/chat-panel.tsx +38 -5
  29. package/src/components/chat/chat-utils.ts +14 -58
  30. package/src/components/data-table/TableBottomBar.tsx +5 -8
  31. package/src/components/data-table/__tests__/column-explorer.test.tsx +128 -0
  32. package/src/components/data-table/__tests__/header-items.test.tsx +220 -10
  33. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +95 -29
  34. package/src/components/data-table/column-header.tsx +17 -12
  35. package/src/components/data-table/data-table.tsx +4 -0
  36. package/src/components/data-table/export-actions.tsx +19 -12
  37. package/src/components/data-table/header-items.tsx +40 -16
  38. package/src/components/data-table/hooks/use-column-visibility.ts +14 -0
  39. package/src/components/data-table/schemas.ts +2 -2
  40. package/src/components/data-table/table-explorer-panel/table-explorer-panel.tsx +16 -6
  41. package/src/components/databases/display.tsx +2 -0
  42. package/src/components/datasources/__tests__/utils.test.ts +82 -0
  43. package/src/components/datasources/utils.ts +16 -15
  44. package/src/components/editor/Disconnected.tsx +1 -60
  45. package/src/components/editor/__tests__/viewer-banner.test.tsx +89 -0
  46. package/src/components/editor/actions/pair-with-agent-modal.tsx +1 -0
  47. package/src/components/editor/actions/useCellActionButton.tsx +3 -3
  48. package/src/components/editor/actions/useNotebookActions.tsx +5 -2
  49. package/src/components/editor/cell/code/cell-editor.tsx +25 -5
  50. package/src/components/editor/chrome/types.ts +13 -6
  51. package/src/components/editor/chrome/wrapper/app-chrome.tsx +6 -4
  52. package/src/components/editor/chrome/wrapper/footer-items/ai-status.tsx +10 -1
  53. package/src/components/editor/chrome/wrapper/sidebar.tsx +7 -5
  54. package/src/components/editor/errors/auto-fix.tsx +3 -3
  55. package/src/components/editor/header/__tests__/status.test.tsx +0 -15
  56. package/src/components/editor/header/app-header.tsx +1 -4
  57. package/src/components/editor/header/status.tsx +4 -13
  58. package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -0
  59. package/src/components/editor/navigation/navigation.ts +5 -0
  60. package/src/components/editor/output/MarimoErrorOutput.tsx +103 -25
  61. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -39
  62. package/src/components/editor/renderers/cell-array.tsx +27 -24
  63. package/src/components/editor/renderers/slides-layout/__tests__/compute-slide-cells.test.ts +30 -17
  64. package/src/components/editor/renderers/slides-layout/compute-slide-cells.ts +17 -8
  65. package/src/components/editor/renderers/slides-layout/slides-layout.tsx +10 -12
  66. package/src/components/editor/viewer-banner.tsx +82 -0
  67. package/src/components/slides/minimap.tsx +45 -9
  68. package/src/components/slides/reveal-component.tsx +82 -37
  69. package/src/components/slides/slide-cell-view.tsx +12 -1
  70. package/src/components/slides/slide-form.tsx +11 -3
  71. package/src/components/static-html/static-banner.tsx +28 -22
  72. package/src/core/ai/__tests__/model-registry.test.ts +72 -60
  73. package/src/core/ai/model-registry.ts +33 -28
  74. package/src/core/cells/__tests__/actions.test.ts +48 -0
  75. package/src/core/cells/actions.ts +5 -6
  76. package/src/core/codemirror/__tests__/setup.test.ts +29 -0
  77. package/src/core/codemirror/cells/traceback-decorations.ts +1 -1
  78. package/src/core/codemirror/cm.ts +50 -3
  79. package/src/core/codemirror/completion/hints.ts +4 -1
  80. package/src/core/codemirror/format.ts +1 -0
  81. package/src/core/codemirror/keymaps/vim.ts +63 -0
  82. package/src/core/codemirror/language/languages/sql/sql.ts +1 -0
  83. package/src/core/codemirror/language/languages/sql/utils.ts +2 -0
  84. package/src/core/config/__tests__/config-schema.test.ts +4 -0
  85. package/src/core/config/config-schema.ts +4 -0
  86. package/src/core/config/config.ts +16 -0
  87. package/src/core/edit-app.tsx +3 -0
  88. package/src/core/islands/bootstrap.ts +2 -0
  89. package/src/core/kernel/__tests__/handlers.test.ts +5 -0
  90. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +0 -13
  91. package/src/core/websocket/types.ts +0 -6
  92. package/src/core/websocket/useMarimoKernelConnection.tsx +3 -12
  93. package/src/css/app/Cell.css +0 -1
  94. package/src/plugins/impl/DataTablePlugin.tsx +48 -22
  95. package/src/plugins/impl/chat/ChatPlugin.tsx +7 -1
  96. package/src/plugins/impl/chat/__tests__/chat-ui.test.ts +278 -0
  97. package/src/plugins/impl/chat/chat-ui.tsx +106 -59
  98. package/src/plugins/impl/chat/types.ts +5 -0
  99. package/src/utils/__tests__/json-parser.test.ts +1 -69
  100. package/src/utils/json/json-parser.ts +0 -30
  101. package/dist/assets/__vite-browser-external-CAdMKBac.js +0 -1
@@ -20,13 +20,17 @@ import {
20
20
  RotateCwIcon,
21
21
  SendHorizontalIcon,
22
22
  SettingsIcon,
23
+ SquareIcon,
23
24
  Trash2Icon,
24
25
  X,
25
26
  } from "lucide-react";
26
27
  import React, { useEffect, useRef, useState } from "react";
27
28
  import { z } from "zod";
28
29
  import { renderUIMessage } from "@/components/chat/chat-display";
29
- import { convertToFileUIPart } from "@/components/chat/chat-utils";
30
+ import {
31
+ convertToFileUIPart,
32
+ hasPendingToolCalls,
33
+ } from "@/components/chat/chat-utils";
30
34
  import {
31
35
  type AdditionalCompletions,
32
36
  PromptInput,
@@ -60,6 +64,7 @@ import { cn } from "@/utils/cn";
60
64
  import { Logger } from "@/utils/Logger";
61
65
  import { Objects } from "@/utils/objects";
62
66
  import { Strings } from "@/utils/strings";
67
+ import { generateUUID } from "@/utils/uuid";
63
68
  import { ErrorBanner } from "../common/error-banner";
64
69
  import type { PluginFunctions } from "./ChatPlugin";
65
70
  import type { ChatConfig } from "./types";
@@ -86,6 +91,48 @@ const ChatMessageIncomingSchema = z.object({
86
91
  is_final: z.boolean().optional(),
87
92
  });
88
93
 
94
+ type ChatMessageIncoming = z.infer<typeof ChatMessageIncomingSchema>;
95
+
96
+ export interface IncomingChatChunkRefs {
97
+ controllerRef: {
98
+ current: ReadableStreamDefaultController<UIMessageChunk> | null;
99
+ };
100
+ activeRequestIdRef: { current: string | null };
101
+ }
102
+
103
+ /**
104
+ * Route a single incoming chunk to the active stream controller, dropping it
105
+ * if it belongs to a stale (aborted-but-not-yet-cancelled) backend run.
106
+ */
107
+ export function routeIncomingChatChunk(
108
+ message: ChatMessageIncoming,
109
+ refs: IncomingChatChunkRefs,
110
+ ): "enqueued" | "closed" | "dropped-no-controller" | "dropped-stale" {
111
+ const { controllerRef, activeRequestIdRef } = refs;
112
+ const controller = controllerRef.current;
113
+ if (controller === null) {
114
+ return "dropped-no-controller";
115
+ }
116
+ const activeRequestId = activeRequestIdRef.current;
117
+ if (activeRequestId !== null && message.message_id !== activeRequestId) {
118
+ Logger.debug("Dropping stale chat chunk", {
119
+ chunkRequestId: message.message_id,
120
+ activeRequestId,
121
+ });
122
+ return "dropped-stale";
123
+ }
124
+ if (message.content) {
125
+ controller.enqueue(message.content);
126
+ }
127
+ if (message.is_final) {
128
+ controller.close();
129
+ controllerRef.current = null;
130
+ activeRequestIdRef.current = null;
131
+ return "closed";
132
+ }
133
+ return "enqueued";
134
+ }
135
+
89
136
  export const Chatbot: React.FC<Props> = (props) => {
90
137
  const [input, setInput] = useState("");
91
138
  const [config, setConfig] = useState<ChatConfig>(props.config);
@@ -113,16 +160,15 @@ export const Chatbot: React.FC<Props> = (props) => {
113
160
  const configRef = useRef<ChatConfig>(config);
114
161
  configRef.current = config;
115
162
 
116
- // Track streaming state - maps backend message_id to frontend message index
117
- const streamingStateRef = useRef<{
118
- backendMessageId: string | null;
119
- frontendMessageIndex: number | null;
120
- }>({ backendMessageId: null, frontendMessageIndex: null });
121
-
122
163
  // For frontend-managed streaming, create a controller to enqueue chunks to.
123
164
  const frontendStreamControllerRef =
124
165
  useRef<ReadableStreamDefaultController<UIMessageChunk> | null>(null);
125
166
 
167
+ // The request_id of the currently-active prompt run. Chunks arriving with a
168
+ // different message_id are stale (from an aborted-but-not-yet-cancelled run
169
+ // on the kernel) and must be dropped
170
+ const activeRequestIdRef = useRef<string | null>(null);
171
+
126
172
  const { data: backendMessages } = useAsyncData(async () => {
127
173
  const response = await props.get_chat_history({});
128
174
  return response.messages;
@@ -143,7 +189,9 @@ export const Chatbot: React.FC<Props> = (props) => {
143
189
  error,
144
190
  regenerate,
145
191
  clearError,
192
+ addToolApprovalResponse,
146
193
  } = useChat({
194
+ sendAutomaticallyWhen: ({ messages }) => hasPendingToolCalls(messages),
147
195
  transport: new DefaultChatTransport({
148
196
  fetch: async (
149
197
  request: RequestInfo | URL,
@@ -180,17 +228,33 @@ export const Chatbot: React.FC<Props> = (props) => {
180
228
  };
181
229
  });
182
230
 
231
+ // Client-generated id used to (a) route chunks back to this stream
232
+ // and (b) ask the kernel to cancel just this run on Stop.
233
+ const requestId = generateUUID();
234
+
183
235
  const stream = new ReadableStream<UIMessageChunk>({
184
236
  start(controller) {
185
237
  frontendStreamControllerRef.current = controller;
238
+ activeRequestIdRef.current = requestId;
186
239
 
187
240
  const abortHandler = () => {
241
+ // Close the local controller first so the chat status flips to
242
+ // "ready" immediately and any racing chunks are dropped; then
243
+ // fire-and-forget the backend cancel so the kernel stops the
244
+ // model and we don't waste tokens / leak chunks to the next
245
+ // run.
188
246
  try {
189
247
  controller.close();
190
248
  } catch (error) {
191
249
  Logger.debug("Controller may already be closed", { error });
192
250
  }
193
251
  frontendStreamControllerRef.current = null;
252
+ activeRequestIdRef.current = null;
253
+ void props
254
+ .cancel_prompt({ request_id: requestId })
255
+ .catch((error: Error) => {
256
+ Logger.debug("cancel_prompt failed", { error });
257
+ });
194
258
  };
195
259
  signal?.addEventListener("abort", abortHandler);
196
260
 
@@ -200,28 +264,25 @@ export const Chatbot: React.FC<Props> = (props) => {
200
264
  },
201
265
  cancel() {
202
266
  frontendStreamControllerRef.current = null;
267
+ activeRequestIdRef.current = null;
203
268
  },
204
269
  });
205
270
 
206
271
  // Start the prompt, chunks will be sent via events
207
272
  void props
208
273
  .send_prompt({
274
+ request_id: requestId,
209
275
  messages: messages,
210
276
  config: chatConfig,
211
277
  })
212
278
  .catch((error: Error) => {
213
279
  frontendStreamControllerRef.current?.error(error);
214
280
  frontendStreamControllerRef.current = null;
281
+ activeRequestIdRef.current = null;
215
282
  });
216
283
 
217
284
  return createUIMessageStreamResponse({ stream });
218
285
  } catch (error: unknown) {
219
- // Clear streaming state on error
220
- streamingStateRef.current = {
221
- backendMessageId: null,
222
- frontendMessageIndex: null,
223
- };
224
-
225
286
  // Handle abort gracefully without showing an error
226
287
  if (error instanceof Error && error.name === "AbortError") {
227
288
  return new Response("Aborted", { status: 499 });
@@ -244,21 +305,10 @@ export const Chatbot: React.FC<Props> = (props) => {
244
305
  }
245
306
  Logger.debug("Finished streaming message:", message);
246
307
 
247
- // Clear streaming state
248
- streamingStateRef.current = {
249
- backendMessageId: null,
250
- frontendMessageIndex: null,
251
- };
252
-
253
308
  props.setValue(message.messages);
254
309
  },
255
310
  onError: (error) => {
256
311
  Logger.error("An error occurred:", error);
257
- // Clear streaming state on error
258
- streamingStateRef.current = {
259
- backendMessageId: null,
260
- frontendMessageIndex: null,
261
- };
262
312
  },
263
313
  });
264
314
 
@@ -273,23 +323,10 @@ export const Chatbot: React.FC<Props> = (props) => {
273
323
  if (!parsedMessage.success) {
274
324
  return;
275
325
  }
276
- const message = parsedMessage.data;
277
-
278
- // Push to the stream for useChat to process
279
- const controller = frontendStreamControllerRef.current;
280
- if (!controller) {
281
- return;
282
- }
283
-
284
- if (message.content) {
285
- controller.enqueue(message.content);
286
- }
287
- if (message.is_final) {
288
- controller.close();
289
- frontendStreamControllerRef.current = null;
290
- }
291
-
292
- return;
326
+ routeIncomingChatChunk(parsedMessage.data, {
327
+ controllerRef: frontendStreamControllerRef,
328
+ activeRequestIdRef,
329
+ });
293
330
  },
294
331
  );
295
332
 
@@ -408,6 +445,9 @@ export const Chatbot: React.FC<Props> = (props) => {
408
445
  message,
409
446
  isStreamingReasoning: status === "streaming",
410
447
  isLast,
448
+ addToolApprovalResponse: isLast
449
+ ? addToolApprovalResponse
450
+ : undefined,
411
451
  })}
412
452
  </div>
413
453
  <div className="flex justify-end text-xs gap-2 invisible group-hover:visible">
@@ -429,16 +469,8 @@ export const Chatbot: React.FC<Props> = (props) => {
429
469
  })}
430
470
 
431
471
  {isLoading && (
432
- <div className="flex items-center justify-center space-x-2 mb-4">
472
+ <div className="flex items-center justify-center mb-4">
433
473
  <Spinner size="small" />
434
- <Button
435
- variant="link"
436
- size="sm"
437
- onClick={() => stop()}
438
- className="text-(--red-9) hover:text-(--red-11)"
439
- >
440
- Stop
441
- </Button>
442
474
  </div>
443
475
  )}
444
476
 
@@ -569,15 +601,30 @@ export const Chatbot: React.FC<Props> = (props) => {
569
601
  />
570
602
  </>
571
603
  )}
572
- <Button
573
- type="submit"
574
- disabled={isLoading || !input}
575
- variant="outline"
576
- size="xs"
577
- className="text-(--slate-11)"
578
- >
579
- <SendHorizontalIcon className="h-4 w-4" />
580
- </Button>
604
+ {isLoading ? (
605
+ <Tooltip content="Stop generating">
606
+ <Button
607
+ type="button"
608
+ variant="link"
609
+ size="xs"
610
+ onClick={() => stop()}
611
+ className="text-(--red-9) hover:text-(--red-11)"
612
+ >
613
+ <SquareIcon className="h-4 w-4 fill-current" />
614
+ </Button>
615
+ </Tooltip>
616
+ ) : (
617
+ <Button
618
+ type="submit"
619
+ disabled={!input}
620
+ variant="outline"
621
+ size="xs"
622
+ className="text-(--slate-11)"
623
+ aria-label="Send message"
624
+ >
625
+ <SendHorizontalIcon className="h-4 w-4" />
626
+ </Button>
627
+ )}
581
628
  </form>
582
629
  </div>
583
630
  );
@@ -22,6 +22,11 @@ export interface ChatConfig {
22
22
  }
23
23
 
24
24
  export interface SendMessageRequest {
25
+ request_id: string;
25
26
  messages: ChatMessage[];
26
27
  config: ChatConfig;
27
28
  }
29
+
30
+ export interface CancelPromptRequest {
31
+ request_id: string;
32
+ }
@@ -1,10 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { expect, it } from "vitest";
3
- import {
4
- jsonParseWithSpecialChar,
5
- jsonToMarkdown,
6
- jsonToTSV,
7
- } from "../json/json-parser";
3
+ import { jsonParseWithSpecialChar, jsonToMarkdown } from "../json/json-parser";
8
4
 
9
5
  it("can jsonParseWithSpecialChar happy path", () => {
10
6
  expect(jsonParseWithSpecialChar('"hello"')).toEqual("hello");
@@ -72,70 +68,6 @@ it("can parse bigInts", () => {
72
68
  });
73
69
  });
74
70
 
75
- it("can convert json to tsv with en-US locale", () => {
76
- const locale = "en-US";
77
-
78
- expect(jsonToTSV([], locale)).toEqual("");
79
-
80
- expect(jsonToTSV([{ a: 1, b: 2 }], locale)).toEqual("a\tb\n1\t2");
81
-
82
- expect(
83
- jsonToTSV(
84
- [
85
- { a: 1, b: 2 },
86
- { a: 3, b: 4 },
87
- ],
88
- locale,
89
- ),
90
- ).toEqual("a\tb\n1\t2\n3\t4");
91
-
92
- // Does not handle sparse arrays
93
- expect(jsonToTSV([{ a: 1 }, { a: 2, b: 3 }], locale)).toMatchInlineSnapshot(
94
- '"a\n1\n2"',
95
- );
96
-
97
- // Handles special characters
98
- expect(
99
- jsonToTSV([{ a: "hello\tworld", b: "new\nline" }], locale),
100
- ).toMatchInlineSnapshot('"a\tb\nhello\tworld\tnew\nline"');
101
-
102
- // Handles floats with en-US locale (uses . as decimal separator)
103
- expect(jsonToTSV([{ a: 1.5, b: 2.7 }], locale)).toEqual("a\tb\n1.5\t2.7");
104
- });
105
-
106
- it("can convert json to tsv with de-DE locale", () => {
107
- const locale = "de-DE";
108
-
109
- // Handles floats with de-DE locale (uses , as decimal separator)
110
- expect(jsonToTSV([{ a: 1.5, b: 2.7 }], locale)).toEqual("a\tb\n1,5\t2,7");
111
-
112
- // Handles integers (no change)
113
- expect(jsonToTSV([{ a: 1, b: 2 }], locale)).toEqual("a\tb\n1\t2");
114
- });
115
-
116
- it("can convert json to tsv with fr-FR locale", () => {
117
- const locale = "fr-FR";
118
-
119
- // Handles floats with fr-FR locale (uses , as decimal separator)
120
- expect(jsonToTSV([{ a: 3.14, b: 2.123_45 }], locale)).toEqual(
121
- "a\tb\n3,14\t2,12345",
122
- );
123
- });
124
-
125
- it("handles null and undefined values in TSV", () => {
126
- const locale = "en-US";
127
-
128
- expect(jsonToTSV([{ a: null, b: undefined, c: 1 }], locale)).toEqual(
129
- "a\tb\tc\n\t\t1",
130
- );
131
- });
132
-
133
- it("handles NaN values in TSV", () => {
134
- const locale = "en-US";
135
-
136
- expect(jsonToTSV([{ a: Number.NaN, b: 1 }], locale)).toEqual("a\tb\nNaN\t1");
137
- });
138
-
139
71
  it("can convert json to markdown - basic table", () => {
140
72
  expect(jsonToMarkdown([])).toMatchInlineSnapshot(`""`);
141
73
 
@@ -66,36 +66,6 @@ export function jsonParseWithSpecialChar<T = unknown>(
66
66
  }
67
67
  }
68
68
 
69
- /**
70
- * Formats a value for TSV export, respecting user's locale for numbers
71
- */
72
- function formatValueForTSV(value: unknown, locale: string): string {
73
- if (value === null || value === undefined) {
74
- return "";
75
- }
76
- if (typeof value === "number" && !Number.isNaN(value)) {
77
- // Use toLocaleString to format numbers according to user's locale
78
- // This will use the appropriate decimal separator (e.g., "," in European locales)
79
- return value.toLocaleString(locale, {
80
- useGrouping: false,
81
- maximumFractionDigits: 20,
82
- });
83
- }
84
- return String(value);
85
- }
86
-
87
- export function jsonToTSV(json: Record<string, unknown>[], locale: string) {
88
- if (json.length === 0) {
89
- return "";
90
- }
91
-
92
- const keys = Object.keys(json[0]);
93
- const values = json.map((row) =>
94
- keys.map((key) => formatValueForTSV(row[key], locale)).join("\t"),
95
- );
96
- return `${keys.join("\t")}\n${values.join("\n")}`;
97
- }
98
-
99
69
  /**
100
70
  * Converts JSON data to a Markdown table format
101
71
  * Detects URLs and converts them to markdown links [url](url)
@@ -1 +0,0 @@
1
- import{t as e}from"./worker-CpBbwbQo.js";var t=e(((e,t)=>{t.exports={}}));export default t();