@marimo-team/islands 0.25.3-dev250 → 0.25.3-dev252

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 (147) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BBihxHrr.js → ConnectedDataExplorerComponent-DGOAQFtf.js} +11 -11
  2. package/dist/{ErrorBoundary-Ks8L_DFj.js → ErrorBoundary-By2DNeOL.js} +7 -7
  3. package/dist/{abnfDiagram-VCTEODGH-DBcJVxYP.js → abnfDiagram-VCTEODGH-DzrZZXRD.js} +2 -2
  4. package/dist/{any-language-editor-jGaR0EAo.js → any-language-editor-Be7dA2VK.js} +3 -3
  5. package/dist/{architectureDiagram-5GKGNRK7-BcHZXF4S.js → architectureDiagram-5GKGNRK7-iF_snyFL.js} +1 -1
  6. package/dist/{button-B6RjDKJx.js → button-0-RqY1DW.js} +2 -1
  7. package/dist/{capabilities-CkVzWJEX.js → capabilities-B6yy_CBN.js} +1 -1
  8. package/dist/{chat-ui-DgaVxFrc.js → chat-ui-zbfp80GW.js} +1547 -1482
  9. package/dist/{check-Z_vtI3OL.js → check-CSvxZk5I.js} +1 -1
  10. package/dist/{chunk-IMKFNOWR-CTZlwBAA.js → chunk-IMKFNOWR-B7xytD4B.js} +1 -1
  11. package/dist/{chunk-SHT3W25Y-53g_RZHm.js → chunk-SHT3W25Y-I8ZFP5L1.js} +1 -1
  12. package/dist/{chunk-SVP7TREG-P6abNGV_.js → chunk-SVP7TREG-Di-uAPdT.js} +1 -1
  13. package/dist/{chunk-TICWLB2K-BTJJqkyF.js → chunk-TICWLB2K-BFpUy0ud.js} +1 -1
  14. package/dist/{classDiagram-ZZMXUADV-BUGqOrTm.js → classDiagram-ZZMXUADV-Cqm4JGpx.js} +2 -2
  15. package/dist/{classDiagram-v2-VYDZK3BY-CXpQvO-s.js → classDiagram-v2-VYDZK3BY-DFHRJpN3.js} +2 -2
  16. package/dist/{code-visibility-CCCqFZYh.js → code-visibility-Byb3BN4h.js} +4 -4
  17. package/dist/{common-JXDK9IjO.js → common-CgWyYqpC.js} +262 -262
  18. package/dist/{config-DbjzjwxX.js → config-mwEjAmvP.js} +2 -2
  19. package/dist/{copy-icon-CG57cxwb.js → copy-icon-DD8bdw4L.js} +3 -3
  20. package/dist/{createReducer-DePYJ0F3.js → createReducer-Bx81cw8_.js} +1 -1
  21. package/dist/{cynefinDiagram-5FMLGOSQ-DOcnl8wW.js → cynefinDiagram-5FMLGOSQ-cuGOeTBC.js} +1 -1
  22. package/dist/{dagre-GXQ25YYZ-CuHR9QfG.js → dagre-GXQ25YYZ-DJ76aC4k.js} +1 -1
  23. package/dist/{data-grid-overlay-editor-QAJSeJpH.js → data-grid-overlay-editor-BP_fYnBa.js} +1 -1
  24. package/dist/{diagram-S7CK7UJ4-BPiG4kso.js → diagram-S7CK7UJ4-DLLc0sw7.js} +1 -1
  25. package/dist/{diagram-UQ7AKVKN-_piBOvOC.js → diagram-UQ7AKVKN-Tz_d6qmk.js} +1 -1
  26. package/dist/{diagram-VX7I27RA-DCzVeJ8M.js → diagram-VX7I27RA-D7SzPrDN.js} +1 -1
  27. package/dist/{diagram-Z3DM3KII-D64CF1jQ.js → diagram-Z3DM3KII-qr37XV58.js} +1 -1
  28. package/dist/{dialog-DsZ49mmU.js → dialog-BHYPX87J.js} +3 -3
  29. package/dist/{dropdown-menu-BRiE7Vnp.js → dropdown-menu-Blu7V6Mk.js} +6 -6
  30. package/dist/{ebnfDiagram-PWID7BFC-BapQPML-.js → ebnfDiagram-PWID7BFC-BKOQb4J0.js} +2 -2
  31. package/dist/{erDiagram-RLTQ6QDP-CNQvC2_Q.js → erDiagram-RLTQ6QDP-CM15Vq68.js} +1 -1
  32. package/dist/{error-banner-IsonnvA0.js → error-banner-DbRFOd2O.js} +2 -2
  33. package/dist/{error-entries-CRPfxMqm.js → error-entries-BuIE3Ius.js} +9 -9
  34. package/dist/{feedback-button-Bg7zGcid.js → feedback-button-DPWFUVWl.js} +7 -7
  35. package/dist/{flowDiagram-HODETNUW-BBt6S6oe.js → flowDiagram-HODETNUW-Dt3N5yFT.js} +2 -2
  36. package/dist/{formats-BK4VefeF.js → formats-gGospKWJ.js} +2 -2
  37. package/dist/{glide-data-editor-BuFe7QfV.js → glide-data-editor-GvhMU_kg.js} +10 -10
  38. package/dist/{glide-utils-CQddGpa-.js → glide-utils-qbkRZpEy.js} +3 -3
  39. package/dist/{globals-8EK96XwF.js → globals-RlhgvYCL.js} +2 -2
  40. package/dist/{html-to-image-Gx4q3ZoD.js → html-to-image-BXrAd5q8.js} +460 -455
  41. package/dist/{infoDiagram-27XIBGKW-Hd8Ifz82.js → infoDiagram-27XIBGKW-C7axJMCl.js} +1 -1
  42. package/dist/{ishikawaDiagram-5VMMS53U-CP7hnHWx.js → ishikawaDiagram-5VMMS53U-CdC9Eur7.js} +1 -1
  43. package/dist/{kanban-definition-UXKFOSKX-CX91sa9b.js → kanban-definition-UXKFOSKX-BHnMIgD7.js} +1 -1
  44. package/dist/{loader-BisjOLiW.js → loader-BVEtQ4F3.js} +1 -1
  45. package/dist/main.js +236 -236
  46. package/dist/{mermaid-Djb3Ni9t.js → mermaid-DFYN_PXd.js} +35 -35
  47. package/dist/{mindmap-definition-YA3MSWOX-DgCNo07O.js → mindmap-definition-YA3MSWOX-BagHoCGU.js} +1 -1
  48. package/dist/{number-overlay-editor-BDJF1c8h.js → number-overlay-editor-Dg1EfcEs.js} +1 -1
  49. package/dist/{pegDiagram-XKGWAZYB-SGJhnIKo.js → pegDiagram-XKGWAZYB-8IIZczip.js} +2 -2
  50. package/dist/{pieDiagram-E7YTZNPT-1stXIWCQ.js → pieDiagram-E7YTZNPT-CE6dKBIr.js} +1 -1
  51. package/dist/{process-output-D2ISIyIs.js → process-output-D2ZjxrHe.js} +4 -4
  52. package/dist/{railroadDiagram-O6MQD6OU-DY1mojzB.js → railroadDiagram-O6MQD6OU-CkexPq3b.js} +2 -2
  53. package/dist/{requirementDiagram-BXWQKSXE-gHzhUp0D.js → requirementDiagram-BXWQKSXE-B0-OkuAe.js} +1 -1
  54. package/dist/{reveal-component-B_hNJfj-.js → reveal-component-UPs4Rd1M.js} +8 -8
  55. package/dist/{select-BSxlITBo.js → select-CcidPqlM.js} +5 -5
  56. package/dist/{sequenceDiagram-WJ2MYXX4-rjPtD9fB.js → sequenceDiagram-WJ2MYXX4-Dxysg8DX.js} +1 -1
  57. package/dist/{spec-Dq8RNplp.js → spec-ZwfZcgsm.js} +4 -4
  58. package/dist/{stateDiagram-D77RDMKH-DMNxjUxV.js → stateDiagram-D77RDMKH-DTEN3-OJ.js} +2 -2
  59. package/dist/{stateDiagram-v2-MP3YSRHH-CumAaqGo.js → stateDiagram-v2-MP3YSRHH-vktk3Xc5.js} +2 -2
  60. package/dist/{strings-BGW-fXVJ.js → strings-BYJNyKmg.js} +3 -3
  61. package/dist/style.css +1 -1
  62. package/dist/{swimlanes-42K2YHIH-BU14_Tej.js → swimlanes-42K2YHIH-Cjveh9OM.js} +1 -1
  63. package/dist/{swimlanesDiagram-VR7AAH4N-CieAnPqJ.js → swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js} +2 -2
  64. package/dist/{swiper-component-Dir1o8ml.js → swiper-component-Dvj-goaD.js} +2 -2
  65. package/dist/{timeline-definition-24CTP7MA-CrVj4E1K.js → timeline-definition-24CTP7MA-KIuDcefn.js} +1 -1
  66. package/dist/{toast-BWIPi3aF.js → toast-CkB5Jq9K.js} +4 -4
  67. package/dist/{tooltip-C3UnvsRe.js → tooltip-Bzl4k00l.js} +2 -2
  68. package/dist/{use-restore-focus-BWO0z1lo.js → use-restore-focus-CmflVLWJ.js} +1 -1
  69. package/dist/{useDateFormatter-ChB4eyim.js → useDateFormatter-CYo5fm8s.js} +2 -2
  70. package/dist/{useIframeCapabilities-cMNQ80CK.js → useIframeCapabilities-BIolGsF1.js} +1 -1
  71. package/dist/{useLifecycle-BquH9ET-.js → useLifecycle-GNgvNwI-.js} +2 -2
  72. package/dist/{useTheme-DgOLVISn.js → useTheme-BZ2GGLgm.js} +141 -139
  73. package/dist/{vega-component-Bwg_Kuh-.js → vega-component-DOtem7Hf.js} +7 -7
  74. package/dist/{vennDiagram-4TSXK5OY-ZLxQ8yhm.js → vennDiagram-4TSXK5OY-DrIOUHfb.js} +1 -1
  75. package/dist/{wardleyDiagram-VM6X3IG4-D88pcLY1.js → wardleyDiagram-VM6X3IG4-CJNjf4OH.js} +1 -1
  76. package/dist/{xychartDiagram-S5SC5T6Z-_13d4g8g.js → xychartDiagram-S5SC5T6Z-DEshFho-.js} +1 -1
  77. package/package.json +1 -1
  78. package/src/__tests__/fixtures/pair-config.ts +1 -1
  79. package/src/components/app-config/__tests__/state.test.tsx +50 -0
  80. package/src/components/app-config/ai-config.tsx +1 -1
  81. package/src/components/chat/__tests__/capabilities-popover.test.tsx +0 -1
  82. package/src/components/chat/__tests__/chat-display.test.tsx +125 -0
  83. package/src/components/chat/__tests__/chat-markdown.test.tsx +43 -0
  84. package/src/components/chat/__tests__/chat-utils.test.ts +51 -1
  85. package/src/components/chat/acp/__tests__/agent-panel-initialization.test.tsx +1 -1
  86. package/src/components/chat/acp/__tests__/agent-panel.test.tsx +22 -17
  87. package/src/components/chat/acp/agent-panel.tsx +4 -3
  88. package/src/components/chat/acp/blocks.tsx +10 -10
  89. package/src/components/chat/acp/common.tsx +4 -5
  90. package/src/components/chat/acp/model-selector.tsx +1 -1
  91. package/src/components/chat/capabilities-popover.tsx +1 -6
  92. package/src/components/chat/chat-components.tsx +7 -4
  93. package/src/components/chat/chat-display.tsx +51 -6
  94. package/src/components/chat/chat-history-popover.tsx +6 -1
  95. package/src/components/chat/chat-markdown.tsx +15 -0
  96. package/src/components/chat/chat-panel.tsx +105 -119
  97. package/src/components/chat/chat-typography.css +108 -0
  98. package/src/components/chat/chat-utils.ts +8 -3
  99. package/src/components/chat/notebook-access-selector.tsx +1 -1
  100. package/src/components/chat/reasoning-accordion.tsx +3 -3
  101. package/src/components/chat/tool-call/tool-activity-group.tsx +49 -0
  102. package/src/components/chat/tool-call/tool-history-row.tsx +3 -30
  103. package/src/components/chat/tool-call/tool-result.tsx +1 -1
  104. package/src/components/chat/use-chat-generation.ts +19 -0
  105. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +18 -37
  106. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +5 -8
  107. package/src/components/editor/actions/__tests__/pair-with-agent.test.tsx +5 -19
  108. package/src/components/editor/actions/pair-with-agent-commands.ts +0 -1
  109. package/src/components/editor/actions/pair-with-agent-modal.tsx +7 -11
  110. package/src/components/editor/actions/pair-with-agent.tsx +3 -31
  111. package/src/components/editor/actions/useCellActionButton.tsx +1 -1
  112. package/src/components/editor/ai/ai-completion-editor.tsx +1 -1
  113. package/src/components/editor/ai/prompt-input.tsx +189 -0
  114. package/src/components/editor/cell/StagedAICell.tsx +3 -3
  115. package/src/components/editor/cell/code/cell-editor.tsx +14 -1
  116. package/src/components/editor/chrome/panels/panel-header.tsx +13 -0
  117. package/src/components/editor/chrome/types.ts +1 -1
  118. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +53 -45
  119. package/src/components/editor/chrome/wrapper/__tests__/useAskAiAboutCell.test.tsx +58 -0
  120. package/src/components/editor/chrome/wrapper/app-chrome.tsx +89 -72
  121. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +5 -8
  122. package/src/components/editor/chrome/wrapper/useAskAiAboutCell.ts +36 -0
  123. package/src/components/editor/renderers/__tests__/add-cell-buttons.test.tsx +18 -0
  124. package/src/components/editor/renderers/cell-array.tsx +10 -67
  125. package/src/components/markdown/markdown-renderer.tsx +35 -20
  126. package/src/components/mcp/mcp-status-indicator.tsx +18 -14
  127. package/src/core/ai/__tests__/staged-cells.test.ts +1 -467
  128. package/src/core/ai/context/context.ts +2 -0
  129. package/src/core/ai/context/providers/__tests__/cell.test.ts +49 -0
  130. package/src/core/ai/context/providers/cell.ts +54 -0
  131. package/src/core/ai/staged-cells.ts +4 -280
  132. package/src/core/ai/state.ts +3 -0
  133. package/src/core/codemirror/__tests__/setup.test.ts +32 -0
  134. package/src/core/codemirror/cells/state.ts +1 -0
  135. package/src/core/codemirror/cm.ts +5 -1
  136. package/src/core/codemirror/config/types.ts +1 -1
  137. package/src/core/codemirror/language/languages/python.ts +3 -3
  138. package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +16 -0
  139. package/src/core/codemirror/placeholder/extensions.ts +57 -19
  140. package/src/core/hotkeys/hotkeys.ts +2 -1
  141. package/src/core/notebook/document/__tests__/codemirror.test.ts +1 -0
  142. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +1 -1
  143. package/src/plugins/impl/chat/chat-ui.tsx +1 -1
  144. package/src/components/editor/ai/__tests__/staged-cell-submission.test.ts +0 -106
  145. package/src/components/editor/ai/add-cell-with-ai.tsx +0 -506
  146. package/src/components/editor/ai/staged-cell-submission.ts +0 -46
  147. package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +0 -66
@@ -6,19 +6,14 @@ import { createElement, type ReactNode } from "react";
6
6
  import { beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import { cellId } from "@/__tests__/branded";
8
8
  import { CellId } from "@/core/cells/ids";
9
- import { getCellEditorView } from "../../cells/cells";
10
- import { prepareCellCodeUpdate } from "../../codemirror/language/cell-code";
11
9
  import {
12
10
  stagedAICellsAtom,
13
- stagedGenerationInProgressAtom,
14
11
  useStagedAICell,
15
- useStagedCellGeneration,
16
12
  useStagedCells,
17
13
  visibleForTesting,
18
14
  } from "../staged-cells";
19
15
 
20
- const { stagedGenerationAtom, createActions, reducer, initialState } =
21
- visibleForTesting;
16
+ const { createActions, reducer, initialState } = visibleForTesting;
22
17
 
23
18
  // Mock the dependencies
24
19
  const mockCreateCell = vi.fn();
@@ -83,7 +78,6 @@ describe("staged-cells", () => {
83
78
 
84
79
  // Reset the atom state
85
80
  store.set(stagedAICellsAtom, new Map());
86
- store.set(stagedGenerationAtom, null);
87
81
  });
88
82
 
89
83
  describe("reducer and actions", () => {
@@ -379,463 +373,3 @@ describe("staged-cells", () => {
379
373
  });
380
374
  });
381
375
  });
382
-
383
- describe("staged cell generation", () => {
384
- let store: ReturnType<typeof getDefaultStore>;
385
- const renderGenerationHook = () =>
386
- renderHook(() => useStagedCellGeneration(store));
387
-
388
- beforeEach(() => {
389
- store = getDefaultStore();
390
- vi.clearAllMocks();
391
- vi.mocked(CellId.create).mockReset();
392
- store.set(stagedAICellsAtom, new Map());
393
- store.set(stagedGenerationAtom, null);
394
- });
395
-
396
- it("should begin generation", () => {
397
- const { result } = renderGenerationHook();
398
- result.current.beginStagedCellGeneration();
399
-
400
- // No cell or cell update should have been called
401
- expect(mockCreateCell).not.toHaveBeenCalled();
402
- expect(prepareCellCodeUpdate).not.toHaveBeenCalled();
403
- expect(mockSetCellCode).not.toHaveBeenCalled();
404
- expect(store.get(stagedGenerationInProgressAtom)).toBe(true);
405
- });
406
-
407
- it("should mark generation complete after a successful finish", () => {
408
- const { result } = renderGenerationHook();
409
- result.current.beginStagedCellGeneration();
410
- result.current.finishStagedCellGeneration(true);
411
-
412
- expect(store.get(stagedGenerationInProgressAtom)).toBe(false);
413
- });
414
-
415
- it("should not couple unrelated staged-cell removal to generation state", () => {
416
- const { result: stagedCells } = renderHook(() => useStagedCells(store));
417
- const { result } = renderGenerationHook();
418
- result.current.beginStagedCellGeneration();
419
-
420
- const chatCellId = cellId("chat-edited-cell");
421
- stagedCells.current.addStagedCell({
422
- cellId: chatCellId,
423
- edit: { type: "update_cell", previousCode: "before" },
424
- });
425
- stagedCells.current.removeStagedCell(chatCellId);
426
-
427
- expect(store.get(stagedGenerationInProgressAtom)).toBe(true);
428
- });
429
-
430
- it("should only accept cells owned by the completed generation", () => {
431
- const firstGeneration = renderGenerationHook();
432
- firstGeneration.result.current.beginStagedCellGeneration();
433
-
434
- vi.mocked(CellId.create).mockReturnValueOnce(cellId("superseded-cell"));
435
- firstGeneration.result.current.onData({
436
- type: "data-notebook-cells-completion",
437
- data: {
438
- cells: [{ language: "python", code: "old" }],
439
- },
440
- });
441
- firstGeneration.result.current.finishStagedCellGeneration(true);
442
-
443
- const activeGeneration = renderGenerationHook();
444
- activeGeneration.result.current.beginStagedCellGeneration();
445
- vi.mocked(CellId.create).mockReturnValueOnce(cellId("active-cell"));
446
- activeGeneration.result.current.onData({
447
- type: "data-notebook-cells-completion",
448
- data: {
449
- cells: [{ language: "python", code: "active" }],
450
- },
451
- });
452
- activeGeneration.result.current.finishStagedCellGeneration(true);
453
-
454
- expect(firstGeneration.result.current.hasOwnedStagedCells()).toBe(false);
455
- expect(firstGeneration.result.current.acceptOwnedStagedCells()).toBe(false);
456
- expect(store.get(stagedAICellsAtom)).toEqual(
457
- new Map([
458
- [cellId("superseded-cell"), { type: "add_cell" }],
459
- [cellId("active-cell"), { type: "add_cell" }],
460
- ]),
461
- );
462
-
463
- expect(activeGeneration.result.current.hasOwnedStagedCells()).toBe(true);
464
- expect(activeGeneration.result.current.acceptOwnedStagedCells()).toBe(true);
465
- expect(store.get(stagedAICellsAtom)).toEqual(
466
- new Map([[cellId("superseded-cell"), { type: "add_cell" }]]),
467
- );
468
- });
469
-
470
- it("should replace only the previous generation owned by this hook", () => {
471
- const { result: stagedCells } = renderHook(() => useStagedCells(store));
472
- const { result } = renderGenerationHook();
473
- stagedCells.current.addStagedCell({
474
- cellId: cellId("chat-edited-cell"),
475
- edit: { type: "update_cell", previousCode: "before" },
476
- });
477
- result.current.beginStagedCellGeneration();
478
-
479
- vi.mocked(CellId.create).mockReturnValueOnce(cellId("generated-cell"));
480
- result.current.onData({
481
- type: "data-notebook-cells-completion",
482
- data: {
483
- cells: [{ language: "python", code: "generated" }],
484
- },
485
- });
486
- result.current.finishStagedCellGeneration(true);
487
- mockDeleteCellCallback.mockClear();
488
-
489
- result.current.beginStagedCellGeneration();
490
-
491
- expect(mockDeleteCellCallback).toHaveBeenCalledExactlyOnceWith({
492
- cellId: cellId("generated-cell"),
493
- });
494
- expect(store.get(stagedAICellsAtom)).toEqual(
495
- new Map([
496
- [
497
- cellId("chat-edited-cell"),
498
- { type: "update_cell", previousCode: "before" },
499
- ],
500
- ]),
501
- );
502
- expect(store.get(stagedGenerationInProgressAtom)).toBe(true);
503
- });
504
-
505
- it("should leave staged cells from another workflow untouched when accepting", () => {
506
- const { result: stagedCells } = renderHook(() => useStagedCells(store));
507
- const { result } = renderGenerationHook();
508
- result.current.beginStagedCellGeneration();
509
-
510
- vi.mocked(CellId.create).mockReturnValueOnce(cellId("generated-cell"));
511
- result.current.onData({
512
- type: "data-notebook-cells-completion",
513
- data: {
514
- cells: [{ language: "python", code: "generated" }],
515
- },
516
- });
517
- result.current.finishStagedCellGeneration(true);
518
- stagedCells.current.addStagedCell({
519
- cellId: cellId("chat-edited-cell"),
520
- edit: { type: "update_cell", previousCode: "before" },
521
- });
522
-
523
- expect(result.current.acceptOwnedStagedCells()).toBe(true);
524
- expect(store.get(stagedAICellsAtom)).toEqual(
525
- new Map([
526
- [
527
- cellId("chat-edited-cell"),
528
- { type: "update_cell", previousCode: "before" },
529
- ],
530
- ]),
531
- );
532
- });
533
-
534
- it("should only discard the generation's remaining staged cells", () => {
535
- const { result: stagedCells } = renderHook(() => useStagedCells(store));
536
- const { result } = renderGenerationHook();
537
- result.current.beginStagedCellGeneration();
538
-
539
- vi.mocked(CellId.create)
540
- .mockReturnValueOnce(cellId("accepted-cell"))
541
- .mockReturnValueOnce(cellId("pending-cell"));
542
- result.current.onData({
543
- type: "data-notebook-cells-completion",
544
- data: {
545
- cells: [
546
- { language: "python", code: "accepted" },
547
- { language: "python", code: "pending" },
548
- ],
549
- },
550
- });
551
- result.current.finishStagedCellGeneration(true);
552
- stagedCells.current.removeStagedCell(cellId("accepted-cell"));
553
- stagedCells.current.addStagedCell({
554
- cellId: cellId("chat-edited-cell"),
555
- edit: { type: "update_cell", previousCode: "before" },
556
- });
557
-
558
- expect(result.current.discardOwnedStagedCells()).toBe(true);
559
- expect(mockDeleteCellCallback).not.toHaveBeenCalledWith({
560
- cellId: cellId("accepted-cell"),
561
- });
562
- expect(mockDeleteCellCallback).toHaveBeenCalledWith({
563
- cellId: cellId("pending-cell"),
564
- });
565
- expect(mockDeleteCellCallback).not.toHaveBeenCalledWith({
566
- cellId: cellId("chat-edited-cell"),
567
- });
568
- expect(store.get(stagedAICellsAtom)).toEqual(
569
- new Map([
570
- [
571
- cellId("chat-edited-cell"),
572
- { type: "update_cell", previousCode: "before" },
573
- ],
574
- ]),
575
- );
576
- });
577
-
578
- it.each([
579
- { name: "no cells", cells: [] },
580
- {
581
- name: "an empty cell",
582
- cells: [{ language: "python" as const, code: "" }],
583
- },
584
- ])("should reject $name", ({ cells }) => {
585
- const { result } = renderGenerationHook();
586
- result.current.beginStagedCellGeneration();
587
-
588
- expect(() =>
589
- result.current.onData({
590
- type: "data-notebook-cells-completion",
591
- data: { cells },
592
- }),
593
- ).toThrow(/cells/i);
594
- });
595
-
596
- it("should create cells from a validated completion", () => {
597
- const { result } = renderGenerationHook();
598
- result.current.beginStagedCellGeneration();
599
-
600
- // Mock CellId.create to return a predictable ID
601
- const mockCellId = cellId("mock-cell-id");
602
- vi.mocked(CellId.create).mockReturnValue(mockCellId);
603
-
604
- result.current.onData({
605
- type: "data-notebook-cells-completion",
606
- data: {
607
- cells: [{ language: "python", code: "print('```')" }],
608
- },
609
- });
610
-
611
- expect(mockCreateCell).toHaveBeenCalledWith({
612
- code: "print('```')",
613
- id: "mock-cell-id",
614
- });
615
- });
616
-
617
- it("should create multiple cells", () => {
618
- const { result } = renderGenerationHook();
619
- result.current.beginStagedCellGeneration();
620
-
621
- vi.mocked(CellId.create)
622
- .mockReturnValueOnce(cellId("first-cell"))
623
- .mockReturnValueOnce(cellId("second-cell"));
624
-
625
- result.current.onData({
626
- type: "data-notebook-cells-completion",
627
- data: {
628
- cells: [
629
- { language: "python", code: "value = 1" },
630
- { language: "python", code: "value" },
631
- ],
632
- },
633
- });
634
-
635
- expect(mockCreateCell).toHaveBeenCalledWith(
636
- expect.objectContaining({
637
- code: "value = 1",
638
- id: "first-cell",
639
- }),
640
- );
641
- expect(mockCreateCell).toHaveBeenCalledWith(
642
- expect.objectContaining({
643
- code: "value",
644
- id: "second-cell",
645
- }),
646
- );
647
- });
648
-
649
- it("should apply cumulative snapshots without recreating cells", () => {
650
- const { result } = renderGenerationHook();
651
- result.current.beginStagedCellGeneration();
652
-
653
- vi.mocked(CellId.create)
654
- .mockReturnValueOnce(cellId("first-cell"))
655
- .mockReturnValueOnce(cellId("second-cell"));
656
-
657
- result.current.onData({
658
- type: "data-notebook-cells-completion",
659
- data: {
660
- cells: [{ language: "python", code: "value" }],
661
- },
662
- });
663
- result.current.onData({
664
- type: "data-notebook-cells-completion",
665
- data: {
666
- cells: [
667
- { language: "python", code: "value = 1" },
668
- { language: "python", code: "value" },
669
- ],
670
- },
671
- });
672
- result.current.onData({
673
- type: "data-notebook-cells-completion",
674
- data: {
675
- cells: [
676
- { language: "python", code: "value = 1" },
677
- { language: "python", code: "value + 1" },
678
- ],
679
- },
680
- });
681
-
682
- expect(mockCreateCell).toHaveBeenCalledTimes(2);
683
- expect(store.get(stagedAICellsAtom).has(cellId("first-cell"))).toBe(true);
684
- expect(store.get(stagedAICellsAtom).has(cellId("second-cell"))).toBe(true);
685
- expect(prepareCellCodeUpdate).toHaveBeenCalledTimes(2);
686
- expect(
687
- mockCellHandle.current.editorViewOrNull.dispatch,
688
- ).toHaveBeenCalledTimes(2);
689
- expect(mockSetCellCode).not.toHaveBeenCalled();
690
- });
691
-
692
- it("writes through the document when the cell editor is not mounted", () => {
693
- const { result } = renderGenerationHook();
694
- result.current.beginStagedCellGeneration();
695
-
696
- vi.mocked(CellId.create).mockReturnValueOnce(cellId("unmounted-cell"));
697
- result.current.onData({
698
- type: "data-notebook-cells-completion",
699
- data: {
700
- cells: [{ language: "python", code: "value" }],
701
- },
702
- });
703
-
704
- vi.mocked(getCellEditorView).mockReturnValueOnce(null);
705
- result.current.onData({
706
- type: "data-notebook-cells-completion",
707
- data: {
708
- cells: [{ language: "python", code: "value = 1" }],
709
- },
710
- });
711
-
712
- expect(mockSetCellCode).toHaveBeenCalledWith({
713
- cellId: cellId("unmounted-cell"),
714
- code: "value = 1",
715
- });
716
- });
717
-
718
- it("should remove trailing cells when a retry produces a shorter snapshot", () => {
719
- const { result } = renderGenerationHook();
720
- result.current.beginStagedCellGeneration();
721
-
722
- vi.mocked(CellId.create)
723
- .mockReturnValueOnce(cellId("first-cell"))
724
- .mockReturnValueOnce(cellId("second-cell"));
725
-
726
- result.current.onData({
727
- type: "data-notebook-cells-completion",
728
- data: {
729
- cells: [
730
- { language: "python", code: "value = 1" },
731
- { language: "python", code: "value" },
732
- ],
733
- },
734
- });
735
- result.current.onData({
736
- type: "data-notebook-cells-completion",
737
- data: {
738
- cells: [{ language: "python", code: "replacement = 2" }],
739
- },
740
- });
741
-
742
- expect(mockDeleteCellCallback).toHaveBeenCalledWith({
743
- cellId: cellId("second-cell"),
744
- });
745
- expect(store.get(stagedAICellsAtom).has(cellId("first-cell"))).toBe(true);
746
- expect(store.get(stagedAICellsAtom).has(cellId("second-cell"))).toBe(false);
747
- });
748
-
749
- it("should remove a provisional marimo import when a retry no longer needs it", () => {
750
- const { result } = renderGenerationHook();
751
- result.current.beginStagedCellGeneration();
752
-
753
- vi.mocked(CellId.create)
754
- .mockReturnValueOnce(cellId("generated-cell"))
755
- .mockReturnValueOnce(cellId("marimo-import"));
756
-
757
- result.current.onData({
758
- type: "data-notebook-cells-completion",
759
- data: {
760
- cells: [{ language: "sql", code: "result = mo.sql('SELECT 1')" }],
761
- },
762
- });
763
- result.current.onData({
764
- type: "data-notebook-cells-completion",
765
- data: {
766
- cells: [{ language: "python", code: "result = 1" }],
767
- },
768
- });
769
-
770
- expect(mockDeleteCellCallback).toHaveBeenCalledWith({
771
- cellId: cellId("marimo-import"),
772
- });
773
- expect(store.get(stagedAICellsAtom).has(cellId("marimo-import"))).toBe(
774
- false,
775
- );
776
- });
777
-
778
- it("should discard provisional cells when generation fails", () => {
779
- const { result } = renderGenerationHook();
780
- result.current.beginStagedCellGeneration();
781
-
782
- vi.mocked(CellId.create).mockReturnValue(cellId("partial-cell"));
783
- result.current.onData({
784
- type: "data-notebook-cells-completion",
785
- data: {
786
- cells: [{ language: "python", code: "partial" }],
787
- },
788
- });
789
- result.current.finishStagedCellGeneration(false);
790
-
791
- expect(mockDeleteCellCallback).toHaveBeenCalledWith({
792
- cellId: cellId("partial-cell"),
793
- });
794
- expect(store.get(stagedAICellsAtom)).toEqual(new Map());
795
- expect(store.get(stagedGenerationInProgressAtom)).toBe(false);
796
- });
797
-
798
- it("should not let a superseded generation mutate active cells", () => {
799
- const firstGeneration = renderGenerationHook();
800
- firstGeneration.result.current.beginStagedCellGeneration();
801
-
802
- vi.mocked(CellId.create).mockReturnValueOnce(cellId("superseded-cell"));
803
- firstGeneration.result.current.onData({
804
- type: "data-notebook-cells-completion",
805
- data: {
806
- cells: [{ language: "python", code: "old" }],
807
- },
808
- });
809
-
810
- const activeGeneration = renderGenerationHook();
811
- activeGeneration.result.current.beginStagedCellGeneration();
812
- expect(mockDeleteCellCallback).toHaveBeenCalledWith({
813
- cellId: cellId("superseded-cell"),
814
- });
815
- mockDeleteCellCallback.mockClear();
816
-
817
- vi.mocked(CellId.create).mockReturnValueOnce(cellId("active-cell"));
818
- activeGeneration.result.current.onData({
819
- type: "data-notebook-cells-completion",
820
- data: {
821
- cells: [{ language: "python", code: "active" }],
822
- },
823
- });
824
-
825
- firstGeneration.result.current.finishStagedCellGeneration(true);
826
- firstGeneration.result.current.onData({
827
- type: "data-notebook-cells-completion",
828
- data: {
829
- cells: [{ language: "python", code: "stale" }],
830
- },
831
- });
832
-
833
- expect(mockDeleteCellCallback).not.toHaveBeenCalledWith({
834
- cellId: cellId("active-cell"),
835
- });
836
- expect(store.get(stagedAICellsAtom)).toEqual(
837
- new Map([[cellId("active-cell"), { type: "add_cell" }]]),
838
- );
839
- expect(store.get(stagedGenerationInProgressAtom)).toBe(true);
840
- });
841
- });
@@ -7,6 +7,7 @@ import {
7
7
  import { getRequestClient } from "@/core/network/requests";
8
8
  import type { JotaiStore } from "@/core/state/jotai";
9
9
  import { variablesAtom } from "@/core/variables/state";
10
+ import { CellContextProvider } from "./providers/cell";
10
11
  import { CellOutputContextProvider } from "./providers/cell-output";
11
12
  import { DatasourceContextProvider } from "./providers/datasource";
12
13
  import { ErrorContextProvider } from "./providers/error";
@@ -25,6 +26,7 @@ export function getAIContextRegistry(store: JotaiStore) {
25
26
  .register(new VariableContextProvider(variables, tablesMap))
26
27
  .register(new ErrorContextProvider(store))
27
28
  .register(new CellOutputContextProvider(store))
29
+ .register(new CellContextProvider(store))
28
30
  .register(
29
31
  new DatasourceContextProvider(datasource.connectionsMap, tablesMap),
30
32
  );
@@ -0,0 +1,49 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { createStore } from "jotai";
4
+ import { expect, it } from "vitest";
5
+ import { MockNotebook } from "@/__mocks__/notebook";
6
+ import { notebookAtom } from "@/core/cells/cells";
7
+ import { CellId } from "@/core/cells/ids";
8
+ import { getAIContextRegistry } from "../../context";
9
+ import { CellContextProvider } from "../cell";
10
+
11
+ it("resolves empty cells and reads their latest code when preparing context", () => {
12
+ const store = createStore();
13
+ const cellId = CellId.create();
14
+ const provider = new CellContextProvider(store);
15
+ store.set(
16
+ notebookAtom,
17
+ MockNotebook.notebookState({ cellData: { [cellId]: { code: "" } } }),
18
+ );
19
+ const uri = provider.asURI(cellId);
20
+ expect(getAIContextRegistry(store).formatContextForAI([uri])).toContain(
21
+ "This cell is empty.",
22
+ );
23
+ store.set(
24
+ notebookAtom,
25
+ MockNotebook.notebookState({ cellData: { [cellId]: { code: "x = 1" } } }),
26
+ );
27
+ expect(getAIContextRegistry(store).formatContextForAI([uri])).toContain(
28
+ "x = 1",
29
+ );
30
+ store.set(notebookAtom, MockNotebook.notebookState());
31
+ expect(getAIContextRegistry(store).formatContextForAI([uri])).toBe("");
32
+ });
33
+
34
+ it("completes cell names while inserting stable cell references", () => {
35
+ const store = createStore();
36
+ const cellId = CellId.create();
37
+ store.set(
38
+ notebookAtom,
39
+ MockNotebook.notebookState({
40
+ cellData: { [cellId]: { name: "my_analysis", code: "x = 1" } },
41
+ }),
42
+ );
43
+ const provider = new CellContextProvider(store);
44
+ expect(provider.formatCompletion(provider.getItems()[0])).toMatchObject({
45
+ label: "@my_analysis",
46
+ displayLabel: "my_analysis",
47
+ apply: `@cell://${cellId}`,
48
+ });
49
+ });
@@ -0,0 +1,54 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { notebookAtom } from "@/core/cells/cells";
4
+ import { displayCellName } from "@/core/cells/names";
5
+ import type { JotaiStore } from "@/core/state/jotai";
6
+ import { AIContextProvider, type AIContextItem } from "../registry";
7
+ import { contextToXml } from "../utils";
8
+
9
+ interface CellContextItem extends AIContextItem {
10
+ type: "cell";
11
+ data: { cellId: string; code: string };
12
+ }
13
+
14
+ export class CellContextProvider extends AIContextProvider<CellContextItem> {
15
+ public readonly title = "Cells";
16
+ public readonly mentionPrefix = "@";
17
+ public readonly contextType = "cell";
18
+
19
+ private readonly store: JotaiStore;
20
+
21
+ public constructor(store: JotaiStore) {
22
+ super();
23
+ this.store = store;
24
+ }
25
+
26
+ public getItems(): CellContextItem[] {
27
+ const notebook = this.store.get(notebookAtom);
28
+ return notebook.cellIds.map((cellId, index) => {
29
+ const cell = notebook.cellData[cellId];
30
+ return {
31
+ uri: this.asURI(cellId),
32
+ name: displayCellName(cell.name, index),
33
+ type: this.contextType,
34
+ description: "Cell code",
35
+ data: { cellId, code: cell.code },
36
+ };
37
+ });
38
+ }
39
+
40
+ public formatCompletion(item: CellContextItem) {
41
+ return {
42
+ ...this.createBasicCompletion(item),
43
+ label: `${this.mentionPrefix}${item.name}`,
44
+ };
45
+ }
46
+
47
+ public formatContext(item: CellContextItem) {
48
+ return contextToXml({
49
+ type: "cell",
50
+ data: { cellId: item.data.cellId, name: item.name },
51
+ details: item.data.code || "This cell is empty.",
52
+ });
53
+ }
54
+ }