@marimo-team/islands 0.23.15-dev2 → 0.23.15-dev21

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 (42) hide show
  1. package/dist/{chat-ui-DinOvbWu.js → chat-ui-FiwuOgQU.js} +3094 -3094
  2. package/dist/{code-visibility-B-sydhvS.js → code-visibility-Bqg6aos7.js} +925 -842
  3. package/dist/{html-to-image-_wGfk8V-.js → html-to-image-JXL8cvmt.js} +2218 -2205
  4. package/dist/main.js +1066 -1073
  5. package/dist/{process-output-Mh4UrjwM.js → process-output-CnDIXtM-.js} +1 -1
  6. package/dist/{reveal-component-MK2nUbwt.js → reveal-component-BzkgqXit.js} +614 -602
  7. package/dist/style.css +1 -1
  8. package/package.json +1 -1
  9. package/src/components/chat/__tests__/chat-utils.test.ts +244 -1
  10. package/src/components/chat/__tests__/message-queue.test.tsx +121 -0
  11. package/src/components/chat/chat-panel.tsx +196 -67
  12. package/src/components/chat/chat-utils.ts +111 -2
  13. package/src/components/editor/SortableCell.tsx +6 -2
  14. package/src/components/editor/__tests__/output-persistence.test.tsx +241 -0
  15. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +153 -0
  16. package/src/components/editor/actions/pair-with-agent-commands.ts +109 -0
  17. package/src/components/editor/actions/pair-with-agent-modal.tsx +20 -64
  18. package/src/components/editor/cell/cell-context-menu.tsx +7 -0
  19. package/src/components/editor/chrome/panels/packages-panel.tsx +11 -1
  20. package/src/components/editor/columns/__tests__/cell-column.test.tsx +105 -0
  21. package/src/components/editor/columns/cell-column.tsx +34 -4
  22. package/src/components/editor/columns/sortable-column.tsx +24 -4
  23. package/src/components/editor/notebook-cell.tsx +203 -106
  24. package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +66 -0
  25. package/src/components/editor/renderers/cell-array.tsx +26 -13
  26. package/src/components/editor/renderers/cells-renderer.tsx +8 -2
  27. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +29 -0
  28. package/src/components/editor/renderers/slides-layout/types.ts +4 -0
  29. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
  30. package/src/components/slides/reveal-component.tsx +33 -4
  31. package/src/components/slides/slide-form.tsx +72 -0
  32. package/src/core/cells/__tests__/utils.test.ts +59 -0
  33. package/src/core/cells/utils.ts +51 -0
  34. package/src/css/app/Cell.css +10 -0
  35. package/src/hooks/__tests__/useHotkey.test.tsx +88 -0
  36. package/src/hooks/useHotkey.ts +29 -4
  37. package/src/plugins/impl/anywidget/__tests__/host.test.ts +6 -9
  38. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +22 -61
  39. package/src/plugins/impl/anywidget/model-proxy.ts +0 -13
  40. package/src/plugins/impl/anywidget/widget-binding.ts +4 -34
  41. package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +71 -3
  42. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.15-dev2",
3
+ "version": "0.23.15-dev21",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -2,7 +2,11 @@
2
2
 
3
3
  import type { UIMessage } from "ai";
4
4
  import { describe, expect, it } from "vitest";
5
- import { hasPendingToolCalls } from "../chat-utils";
5
+ import {
6
+ hasPendingToolCalls,
7
+ hasUnresolvedToolCalls,
8
+ shouldFlushQueue,
9
+ } from "../chat-utils";
6
10
 
7
11
  /**
8
12
  * `hasPendingToolCalls` powers `sendAutomaticallyWhen` in `mo.ui.chat`:
@@ -81,6 +85,23 @@ describe("hasPendingToolCalls", () => {
81
85
  ).toBe(true);
82
86
  });
83
87
 
88
+ it("returns true when the user denied a tool approval", () => {
89
+ expect(
90
+ hasPendingToolCalls([
91
+ userMessage("delete it"),
92
+ assistantToolMessage([
93
+ {
94
+ type: "tool-delete_file",
95
+ toolCallId: "call-1",
96
+ state: "output-denied",
97
+ input: { path: "secrets.env" },
98
+ approval: { id: "approval-1", approved: false },
99
+ } as unknown as UIMessage["parts"][number],
100
+ ]),
101
+ ]),
102
+ ).toBe(true);
103
+ });
104
+
84
105
  it("returns true when a tool reached a terminal output state", () => {
85
106
  expect(
86
107
  hasPendingToolCalls([
@@ -267,3 +288,225 @@ describe("hasPendingToolCalls", () => {
267
288
  ).toBe(true);
268
289
  });
269
290
  });
291
+
292
+ describe("hasUnresolvedToolCalls", () => {
293
+ it("returns false when there are no messages", () => {
294
+ expect(hasUnresolvedToolCalls([])).toBe(false);
295
+ });
296
+
297
+ it("returns true while approval is still requested", () => {
298
+ expect(
299
+ hasUnresolvedToolCalls([
300
+ userMessage("delete it"),
301
+ assistantToolMessage([
302
+ {
303
+ type: "tool-delete_file",
304
+ toolCallId: "call-1",
305
+ state: "approval-requested",
306
+ input: { path: "secrets.env" },
307
+ approval: { id: "approval-1" },
308
+ } as unknown as UIMessage["parts"][number],
309
+ ]),
310
+ ]),
311
+ ).toBe(true);
312
+ });
313
+
314
+ it("returns true while a tool is still running", () => {
315
+ expect(
316
+ hasUnresolvedToolCalls([
317
+ userMessage("run it"),
318
+ assistantToolMessage([
319
+ {
320
+ type: "tool-run_query",
321
+ toolCallId: "call-1",
322
+ state: "input-available",
323
+ input: { sql: "select 1" },
324
+ } as unknown as UIMessage["parts"][number],
325
+ ]),
326
+ ]),
327
+ ).toBe(true);
328
+ });
329
+
330
+ it("returns false once tools are ready to round-trip", () => {
331
+ expect(
332
+ hasUnresolvedToolCalls([
333
+ userMessage("run it"),
334
+ assistantToolMessage([
335
+ {
336
+ type: "tool-run_query",
337
+ toolCallId: "call-1",
338
+ state: "output-available",
339
+ input: { sql: "select 1" },
340
+ output: 1,
341
+ } as unknown as UIMessage["parts"][number],
342
+ ]),
343
+ ]),
344
+ ).toBe(false);
345
+ });
346
+
347
+ it("returns false once the user denied a tool approval", () => {
348
+ expect(
349
+ hasUnresolvedToolCalls([
350
+ userMessage("delete it"),
351
+ assistantToolMessage([
352
+ {
353
+ type: "tool-delete_file",
354
+ toolCallId: "call-1",
355
+ state: "output-denied",
356
+ input: { path: "secrets.env" },
357
+ approval: { id: "approval-1", approved: false },
358
+ } as unknown as UIMessage["parts"][number],
359
+ ]),
360
+ ]),
361
+ ).toBe(false);
362
+ });
363
+ });
364
+
365
+ describe("shouldFlushQueue", () => {
366
+ it("flushes when the turn completed without error or pending tools", () => {
367
+ expect(
368
+ shouldFlushQueue({
369
+ isError: false,
370
+ isAbort: false,
371
+ hasPendingToolCalls: false,
372
+ hasUnresolvedToolCalls: false,
373
+ }),
374
+ ).toBe(true);
375
+ });
376
+
377
+ it("does not flush on error", () => {
378
+ expect(
379
+ shouldFlushQueue({
380
+ isError: true,
381
+ isAbort: false,
382
+ hasPendingToolCalls: false,
383
+ hasUnresolvedToolCalls: false,
384
+ }),
385
+ ).toBe(false);
386
+ });
387
+
388
+ it("does not flush while a tool round-trip is still pending", () => {
389
+ expect(
390
+ shouldFlushQueue({
391
+ isError: false,
392
+ isAbort: false,
393
+ hasPendingToolCalls: true,
394
+ hasUnresolvedToolCalls: false,
395
+ }),
396
+ ).toBe(false);
397
+ });
398
+
399
+ it("does not flush while approval is still requested", () => {
400
+ expect(
401
+ shouldFlushQueue({
402
+ isError: false,
403
+ isAbort: false,
404
+ hasPendingToolCalls: false,
405
+ hasUnresolvedToolCalls: true,
406
+ }),
407
+ ).toBe(false);
408
+ });
409
+
410
+ it("flushes on abort even when a tool is still streaming", () => {
411
+ expect(
412
+ shouldFlushQueue({
413
+ isError: false,
414
+ isAbort: true,
415
+ hasPendingToolCalls: false,
416
+ hasUnresolvedToolCalls: true,
417
+ }),
418
+ ).toBe(true);
419
+ });
420
+
421
+ it("does not flush on abort when the run ended in error", () => {
422
+ expect(
423
+ shouldFlushQueue({
424
+ isError: true,
425
+ isAbort: true,
426
+ hasPendingToolCalls: false,
427
+ hasUnresolvedToolCalls: true,
428
+ }),
429
+ ).toBe(false);
430
+ });
431
+
432
+ it("flushes once trailing text is present and tools are resolved", () => {
433
+ const messages = [
434
+ userMessage("run it"),
435
+ assistantToolMessage([
436
+ {
437
+ type: "tool-run_query",
438
+ toolCallId: "call-1",
439
+ state: "output-available",
440
+ input: {},
441
+ output: 1,
442
+ } as unknown as UIMessage["parts"][number],
443
+ { type: "text", text: "Done." },
444
+ ]),
445
+ ];
446
+
447
+ expect(
448
+ shouldFlushQueue({
449
+ isError: false,
450
+ isAbort: false,
451
+ hasPendingToolCalls: hasPendingToolCalls(messages),
452
+ hasUnresolvedToolCalls: hasUnresolvedToolCalls(messages),
453
+ }),
454
+ ).toBe(true);
455
+ });
456
+
457
+ it("blocks flush while text trails a still-running tool, then allows it after resolution", () => {
458
+ const running = [
459
+ userMessage("run it"),
460
+ assistantToolMessage([
461
+ {
462
+ type: "tool-run_query",
463
+ toolCallId: "call-1",
464
+ state: "input-available",
465
+ input: {},
466
+ } as unknown as UIMessage["parts"][number],
467
+ { type: "text", text: "Running your query..." },
468
+ ]),
469
+ ];
470
+ const resolved = [
471
+ userMessage("run it"),
472
+ assistantToolMessage([
473
+ {
474
+ type: "tool-run_query",
475
+ toolCallId: "call-1",
476
+ state: "output-available",
477
+ input: {},
478
+ output: 1,
479
+ } as unknown as UIMessage["parts"][number],
480
+ { type: "text", text: "Running your query..." },
481
+ ]),
482
+ ];
483
+
484
+ expect(
485
+ shouldFlushQueue({
486
+ isError: false,
487
+ isAbort: false,
488
+ hasPendingToolCalls: hasPendingToolCalls(running),
489
+ hasUnresolvedToolCalls: hasUnresolvedToolCalls(running),
490
+ }),
491
+ ).toBe(false);
492
+ expect(
493
+ shouldFlushQueue({
494
+ isError: false,
495
+ isAbort: false,
496
+ hasPendingToolCalls: hasPendingToolCalls(resolved),
497
+ hasUnresolvedToolCalls: hasUnresolvedToolCalls(resolved),
498
+ }),
499
+ ).toBe(true);
500
+ });
501
+
502
+ it("does not flush on error even when tools are also pending", () => {
503
+ expect(
504
+ shouldFlushQueue({
505
+ isError: true,
506
+ isAbort: false,
507
+ hasPendingToolCalls: true,
508
+ hasUnresolvedToolCalls: true,
509
+ }),
510
+ ).toBe(false);
511
+ });
512
+ });
@@ -0,0 +1,121 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { type ChatMessagePart, useMessageQueue } from "../chat-utils";
6
+
7
+ function textPart(text: string): ChatMessagePart {
8
+ return { type: "text", text };
9
+ }
10
+
11
+ describe("useMessageQueue", () => {
12
+ it("initializes empty", () => {
13
+ const { result } = renderHook(() => useMessageQueue());
14
+ expect(result.current.messages).toEqual([]);
15
+ });
16
+
17
+ it("enqueues messages in order with unique ids", () => {
18
+ const { result } = renderHook(() => useMessageQueue());
19
+
20
+ act(() => result.current.enqueue([textPart("first")]));
21
+ act(() => result.current.enqueue([textPart("second")]));
22
+
23
+ const [a, b] = result.current.messages;
24
+ expect(a.parts).toEqual([textPart("first")]);
25
+ expect(b.parts).toEqual([textPart("second")]);
26
+ expect(a.id).toBeTypeOf("string");
27
+ expect(a.id).not.toBe(b.id);
28
+ });
29
+
30
+ it("flushNext sends the oldest message's parts and dequeues it", () => {
31
+ const { result } = renderHook(() => useMessageQueue());
32
+ const send = vi.fn();
33
+
34
+ act(() => result.current.enqueue([textPart("first")]));
35
+ act(() => result.current.enqueue([textPart("second")]));
36
+ act(() => result.current.flushNext(send));
37
+
38
+ expect(send).toHaveBeenCalledTimes(1);
39
+ expect(send).toHaveBeenCalledWith([textPart("first")]);
40
+ expect(result.current.messages).toEqual([
41
+ expect.objectContaining({ parts: [textPart("second")] }),
42
+ ]);
43
+ });
44
+
45
+ it("flushNext sees a message enqueued in the same act (before re-render)", () => {
46
+ const { result } = renderHook(() => useMessageQueue());
47
+ const send = vi.fn();
48
+
49
+ // Mirrors enqueue + onFinish in the same tick: the ref must be updated
50
+ // synchronously inside enqueue, not only on the next render.
51
+ act(() => {
52
+ result.current.enqueue([textPart("queued while finishing")]);
53
+ result.current.flushNext(send);
54
+ });
55
+
56
+ expect(send).toHaveBeenCalledTimes(1);
57
+ expect(send).toHaveBeenCalledWith([textPart("queued while finishing")]);
58
+ expect(result.current.messages).toEqual([]);
59
+ });
60
+
61
+ it("drains sequentially, even across synchronous flushNext calls", () => {
62
+ const { result } = renderHook(() => useMessageQueue());
63
+ const send = vi.fn();
64
+
65
+ act(() => result.current.enqueue([textPart("first")]));
66
+ act(() => result.current.enqueue([textPart("second")]));
67
+
68
+ // Two flushes in the same act: the ref must stay in sync so the second
69
+ // flush releases the next message rather than re-sending the first.
70
+ act(() => {
71
+ result.current.flushNext(send);
72
+ result.current.flushNext(send);
73
+ });
74
+
75
+ expect(send).toHaveBeenNthCalledWith(1, [textPart("first")]);
76
+ expect(send).toHaveBeenNthCalledWith(2, [textPart("second")]);
77
+ expect(result.current.messages).toEqual([]);
78
+ });
79
+
80
+ it("flushNext on an empty queue is a no-op", () => {
81
+ const { result } = renderHook(() => useMessageQueue());
82
+ const send = vi.fn();
83
+
84
+ act(() => result.current.flushNext(send));
85
+
86
+ expect(send).not.toHaveBeenCalled();
87
+ expect(result.current.messages).toEqual([]);
88
+ });
89
+
90
+ it("clear empties the queue", () => {
91
+ const { result } = renderHook(() => useMessageQueue());
92
+
93
+ act(() => result.current.enqueue([textPart("first")]));
94
+ act(() => result.current.enqueue([textPart("second")]));
95
+ act(() => result.current.clear());
96
+
97
+ expect(result.current.messages).toEqual([]);
98
+ expect(result.current.hasQueuedRef.current).toBe(false);
99
+ });
100
+
101
+ it("hasQueuedRef stays in sync when enqueue and flush run in the same act", () => {
102
+ const { result } = renderHook(() => useMessageQueue());
103
+ const send = vi.fn();
104
+
105
+ act(() => {
106
+ result.current.enqueue([textPart("first")]);
107
+ result.current.enqueue([textPart("second")]);
108
+ });
109
+ expect(result.current.hasQueuedRef.current).toBe(true);
110
+
111
+ act(() => {
112
+ result.current.flushNext(send);
113
+ });
114
+ expect(result.current.hasQueuedRef.current).toBe(true);
115
+
116
+ act(() => {
117
+ result.current.flushNext(send);
118
+ });
119
+ expect(result.current.hasQueuedRef.current).toBe(false);
120
+ });
121
+ });