@marimo-team/islands 0.25.3-dev266 → 0.25.3-dev268

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 (184) hide show
  1. package/dist/{ConnectedDataExplorerComponent-C_hj7Poi.js → ConnectedDataExplorerComponent-eLHPh6uu.js} +9 -9
  2. package/dist/{ErrorBoundary-CvxlC3F1.js → ErrorBoundary-CQYAoD7X.js} +64 -69
  3. package/dist/{abnfDiagram-VCTEODGH-DzrZZXRD.js → abnfDiagram-VCTEODGH-hqd0IBtX.js} +2 -2
  4. package/dist/{any-language-editor-CFeWfC3o.js → any-language-editor-DnG26QXh.js} +1 -1
  5. package/dist/{architectureDiagram-5GKGNRK7-iF_snyFL.js → architectureDiagram-5GKGNRK7-BzLAFcc0.js} +1 -1
  6. package/dist/{strings-BCKpLFNU.js → assertNever-DIU3JJbt.js} +14 -14
  7. package/dist/{chat-ui-DFxNgzXF.js → chat-ui-BAnLgpTe.js} +1964 -1740
  8. package/dist/{chunk-IMKFNOWR-B7xytD4B.js → chunk-IMKFNOWR-DOeVvuyE.js} +1 -1
  9. package/dist/{chunk-SHT3W25Y-I8ZFP5L1.js → chunk-SHT3W25Y-DZoFI8wR.js} +1 -1
  10. package/dist/{chunk-SVP7TREG-Di-uAPdT.js → chunk-SVP7TREG-CWu86PSW.js} +1 -1
  11. package/dist/{chunk-TICWLB2K-BFpUy0ud.js → chunk-TICWLB2K-BrM0GoP3.js} +1 -1
  12. package/dist/{classDiagram-ZZMXUADV-Cqm4JGpx.js → classDiagram-ZZMXUADV-DYi4V2bB.js} +2 -2
  13. package/dist/{classDiagram-v2-VYDZK3BY-DFHRJpN3.js → classDiagram-v2-VYDZK3BY-tN7FIk-m.js} +2 -2
  14. package/dist/{code-visibility-SRM1JiD_.js → code-visibility-DcZ_RwaH.js} +44 -47
  15. package/dist/{common-yiRhZ6ZZ.js → common-BGeQilvh.js} +5846 -5519
  16. package/dist/{config-CmeG_Pb6.js → config-BsPruWMR.js} +2 -2
  17. package/dist/{createReducer-C_syF8Td.js → createReducer-DdtT_7ji.js} +1 -1
  18. package/dist/{cynefinDiagram-5FMLGOSQ-cuGOeTBC.js → cynefinDiagram-5FMLGOSQ-D5nCnDc-.js} +1 -1
  19. package/dist/{dagre-GXQ25YYZ-DJ76aC4k.js → dagre-GXQ25YYZ-BHkd7f_i.js} +1 -1
  20. package/dist/{data-grid-overlay-editor-DLTzcQ4O.js → data-grid-overlay-editor-Dc4lW4A3.js} +1 -1
  21. package/dist/{diagram-S7CK7UJ4-DLLc0sw7.js → diagram-S7CK7UJ4-D1ALppT2.js} +1 -1
  22. package/dist/{diagram-UQ7AKVKN-Tz_d6qmk.js → diagram-UQ7AKVKN-DLv0b83L.js} +1 -1
  23. package/dist/{diagram-VX7I27RA-D7SzPrDN.js → diagram-VX7I27RA-7XJ7J6qg.js} +1 -1
  24. package/dist/{diagram-Z3DM3KII-qr37XV58.js → diagram-Z3DM3KII-7M7zbHp_.js} +1 -1
  25. package/dist/{dialog-BtprQCUw.js → dialog-DzCj1qQv.js} +1 -1
  26. package/dist/{dropdown-menu-CkJ7ErY6.js → dropdown-menu-CdXE15an.js} +1214 -1208
  27. package/dist/{ebnfDiagram-PWID7BFC-BKOQb4J0.js → ebnfDiagram-PWID7BFC-DHH1qlmV.js} +2 -2
  28. package/dist/{erDiagram-RLTQ6QDP-CM15Vq68.js → erDiagram-RLTQ6QDP-fI_0dGef.js} +1 -1
  29. package/dist/{error-banner-DbRFOd2O.js → error-banner-DPc9eIw5.js} +3 -3
  30. package/dist/{error-entries-CMoaUX9V.js → error-entries-CxN-Nq3V.js} +524 -456
  31. package/dist/{feedback-button-DBUrE4OL.js → feedback-button-BqaeQcvX.js} +77 -77
  32. package/dist/{flowDiagram-HODETNUW-Dt3N5yFT.js → flowDiagram-HODETNUW-Brg9MA6i.js} +2 -2
  33. package/dist/{formats-gGospKWJ.js → formats-DbSvAPY-.js} +1 -1
  34. package/dist/{glide-data-editor-Cfv4HeOh.js → glide-data-editor-CiijPXA7.js} +2042 -2041
  35. package/dist/{glide-utils-Ba9WD2IN.js → glide-utils-BSw3SSHF.js} +29 -35
  36. package/dist/{globals-dIbBIR5-.js → globals-CpmSWKI_.js} +2 -2
  37. package/dist/{html-to-image-D_5CMreP.js → html-to-image-8sywF4my.js} +5452 -5751
  38. package/dist/{infoDiagram-27XIBGKW-C7axJMCl.js → infoDiagram-27XIBGKW-E9e403YT.js} +1 -1
  39. package/dist/{ishikawaDiagram-5VMMS53U-CdC9Eur7.js → ishikawaDiagram-5VMMS53U-C-RlYDRo.js} +1 -1
  40. package/dist/{kanban-definition-UXKFOSKX-BHnMIgD7.js → kanban-definition-UXKFOSKX-CLFl_uqK.js} +1 -1
  41. package/dist/main.js +6294 -6324
  42. package/dist/{mermaid-DFYN_PXd.js → mermaid-rd09e1JL.js} +35 -35
  43. package/dist/{mindmap-definition-YA3MSWOX-BagHoCGU.js → mindmap-definition-YA3MSWOX-BQ8TWtyB.js} +1 -1
  44. package/dist/{number-overlay-editor-CTiJp5Mj.js → number-overlay-editor-Ft4NiAEN.js} +1 -1
  45. package/dist/{pegDiagram-XKGWAZYB-8IIZczip.js → pegDiagram-XKGWAZYB-CN8UHFPi.js} +2 -2
  46. package/dist/pencil-DcT8AE0h.js +10 -0
  47. package/dist/{pieDiagram-E7YTZNPT-CE6dKBIr.js → pieDiagram-E7YTZNPT-DGuJ0EBd.js} +1 -1
  48. package/dist/{process-output-CGFNVC60.js → process-output-Dku5POvm.js} +3 -3
  49. package/dist/{railroadDiagram-O6MQD6OU-CkexPq3b.js → railroadDiagram-O6MQD6OU-BPDA8wy8.js} +2 -2
  50. package/dist/{requirementDiagram-BXWQKSXE-B0-OkuAe.js → requirementDiagram-BXWQKSXE-ChzLGbGW.js} +1 -1
  51. package/dist/{reveal-component-DH09ygHo.js → reveal-component-D0cfwbbl.js} +1094 -1094
  52. package/dist/{select-C33FVPPN.js → select-OxgozOC8.js} +1 -1
  53. package/dist/{sequenceDiagram-WJ2MYXX4-Dxysg8DX.js → sequenceDiagram-WJ2MYXX4-DFX-bE6O.js} +1 -1
  54. package/dist/{spec-DH1lmF7Q.js → spec-oJn0RhpQ.js} +2 -2
  55. package/dist/{stateDiagram-D77RDMKH-DTEN3-OJ.js → stateDiagram-D77RDMKH-BR7cI3wK.js} +2 -2
  56. package/dist/{stateDiagram-v2-MP3YSRHH-vktk3Xc5.js → stateDiagram-v2-MP3YSRHH-oGqB5UDI.js} +2 -2
  57. package/dist/{static-state-BldGd99T.js → static-state-CuV6Pc3a.js} +1 -1
  58. package/dist/style.css +1 -1
  59. package/dist/{swimlanes-42K2YHIH-Cjveh9OM.js → swimlanes-42K2YHIH-DwSZl1yC.js} +1 -1
  60. package/dist/{swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js → swimlanesDiagram-VR7AAH4N-BHlcd-Ri.js} +2 -2
  61. package/dist/{timeline-definition-24CTP7MA-KIuDcefn.js → timeline-definition-24CTP7MA-DMmP-G2O.js} +1 -1
  62. package/dist/{toast-CY1_Qv41.js → toast-DbBHmxmB.js} +2 -2
  63. package/dist/{useAsyncData-DsZMvNXn.js → useAsyncData-BYIAoMqn.js} +1 -1
  64. package/dist/{useDateFormatter-Dk_xFci4.js → useDateFormatter-ButWkdcK.js} +1 -1
  65. package/dist/{useDeepCompareMemoize-KMBBux8C.js → useDeepCompareMemoize-Bz0c5GN6.js} +1 -1
  66. package/dist/{useTheme-BZ2GGLgm.js → useTheme-DPLobSFs.js} +7 -7
  67. package/dist/{utils-DXkj82sK.js → utils-EBklsC5R.js} +31 -31
  68. package/dist/{vega-component-C-lqjV0q.js → vega-component-rxIxAAh9.js} +7 -7
  69. package/dist/{vennDiagram-4TSXK5OY-DrIOUHfb.js → vennDiagram-4TSXK5OY-Db8GtH1G.js} +1 -1
  70. package/dist/{wardleyDiagram-VM6X3IG4-CJNjf4OH.js → wardleyDiagram-VM6X3IG4-DmIiC3FU.js} +1 -1
  71. package/dist/{xychartDiagram-S5SC5T6Z-DEshFho-.js → xychartDiagram-S5SC5T6Z-DS9OPrNq.js} +1 -1
  72. package/package.json +1 -1
  73. package/src/__mocks__/executions.ts +195 -0
  74. package/src/__mocks__/notebook.ts +0 -1
  75. package/src/__tests__/CellStatus.test.tsx +19 -20
  76. package/src/components/chat/__tests__/chat-display.test.tsx +157 -86
  77. package/src/components/chat/__tests__/chat-utils.test.ts +42 -0
  78. package/src/components/chat/__tests__/turn-timing.test.ts +84 -0
  79. package/src/components/chat/__tests__/use-assistant-turn.test.ts +95 -0
  80. package/src/components/chat/__tests__/user-message.test.tsx +27 -0
  81. package/src/components/chat/acp/agent-panel.tsx +2 -0
  82. package/src/components/chat/agent-activity.tsx +132 -0
  83. package/src/components/chat/chat-display.tsx +125 -96
  84. package/src/components/chat/chat-panel.tsx +123 -106
  85. package/src/components/chat/chat-utils.ts +12 -24
  86. package/src/components/chat/tool-call/__tests__/tool-labels.test.ts +76 -0
  87. package/src/components/chat/tool-call/__tests__/tool-step.test.tsx +57 -0
  88. package/src/components/chat/tool-call/shared.ts +6 -0
  89. package/src/components/chat/tool-call/tool-call-view.tsx +22 -76
  90. package/src/components/chat/tool-call/tool-labels.ts +172 -0
  91. package/src/components/chat/tool-call/tool-step.tsx +145 -0
  92. package/src/components/chat/turn-timing.ts +73 -0
  93. package/src/components/chat/use-assistant-turn.ts +65 -0
  94. package/src/components/chat/use-chat-generation.ts +5 -4
  95. package/src/components/chat/user-message.tsx +145 -0
  96. package/src/components/dependency-graph/minimap-content.tsx +3 -2
  97. package/src/components/editor/__tests__/data-attributes.test.tsx +0 -7
  98. package/src/components/editor/__tests__/output-persistence.test.tsx +2 -2
  99. package/src/components/editor/actions/__tests__/useCellActionButton.test.tsx +19 -1
  100. package/src/components/editor/actions/useCellActionButton.tsx +10 -3
  101. package/src/components/editor/ai/completion-handlers.tsx +0 -37
  102. package/src/components/editor/alerts/stdin-blocking-alert.tsx +3 -1
  103. package/src/components/editor/cell/CellStatus.tsx +23 -19
  104. package/src/components/editor/cell/RunButton.tsx +16 -6
  105. package/src/components/editor/cell/StagedAICell.tsx +51 -144
  106. package/src/components/editor/cell/__tests__/useRunCells.test.ts +45 -0
  107. package/src/components/editor/cell/cell-context-menu.tsx +4 -1
  108. package/src/components/editor/cell/code/cell-editor.tsx +7 -5
  109. package/src/components/editor/cell/collapse.tsx +4 -0
  110. package/src/components/editor/cell/useRunCells.ts +18 -3
  111. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +193 -32
  112. package/src/components/editor/chrome/wrapper/app-chrome.tsx +2 -2
  113. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +145 -81
  114. package/src/components/editor/chrome/wrapper/pending-ai-changes-badge.tsx +25 -0
  115. package/src/components/editor/chrome/wrapper/sidebar.tsx +8 -4
  116. package/src/components/editor/header/status.tsx +4 -2
  117. package/src/components/editor/links/cell-link.tsx +4 -4
  118. package/src/components/editor/navigation/navigation.ts +3 -2
  119. package/src/components/editor/notebook-cell.tsx +85 -94
  120. package/src/components/editor/renderers/grid-layout/__tests__/grid-layout.test.tsx +75 -0
  121. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +22 -16
  122. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
  123. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -33
  124. package/src/components/editor/stdin-blocking-alert.tsx +3 -1
  125. package/src/components/scratchpad/scratchpad.tsx +4 -1
  126. package/src/components/slides/minimap.tsx +4 -2
  127. package/src/components/slides/reveal-component.tsx +14 -10
  128. package/src/components/slides/slide-cell-view.tsx +28 -24
  129. package/src/components/slides/slide.tsx +3 -4
  130. package/src/components/ui/toast.tsx +1 -1
  131. package/src/core/ai/__tests__/staged-cells.test.ts +74 -126
  132. package/src/core/ai/staged-cells.ts +48 -38
  133. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -389
  134. package/src/core/cells/__tests__/cell.test.ts +49 -154
  135. package/src/core/cells/__tests__/cells.test.ts +18 -127
  136. package/src/core/cells/__tests__/pending-delete-service.test.tsx +11 -20
  137. package/src/core/cells/__tests__/utils.test.ts +75 -247
  138. package/src/core/cells/actions.ts +5 -2
  139. package/src/core/cells/cell.ts +31 -133
  140. package/src/core/cells/cells.ts +14 -29
  141. package/src/core/cells/document.ts +3 -3
  142. package/src/core/cells/focus.ts +27 -6
  143. package/src/core/cells/layout.ts +1 -1
  144. package/src/core/cells/pending-delete-service.ts +5 -7
  145. package/src/core/cells/scrollCellIntoView.ts +6 -0
  146. package/src/core/cells/types.ts +13 -36
  147. package/src/core/cells/utils.ts +37 -68
  148. package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +59 -0
  149. package/src/core/codemirror/placeholder/extensions.ts +23 -0
  150. package/src/core/edit-app.tsx +2 -1
  151. package/src/core/executions/__tests__/atoms.test.ts +99 -0
  152. package/src/core/executions/__tests__/select.test.ts +268 -0
  153. package/src/core/executions/atoms.ts +206 -0
  154. package/src/core/executions/hooks.ts +31 -0
  155. package/src/core/executions/index.ts +31 -0
  156. package/src/core/executions/select.ts +175 -0
  157. package/src/core/export/__tests__/hooks.test.ts +0 -5
  158. package/src/core/islands/bootstrap.ts +4 -1
  159. package/src/core/islands/components/output-wrapper.tsx +5 -5
  160. package/src/core/kernel/handlers.ts +2 -11
  161. package/src/core/kernel/state.ts +0 -15
  162. package/src/core/lifecycle/render-policy.ts +12 -26
  163. package/src/core/network/__tests__/requests-lazy.test.ts +5 -5
  164. package/src/core/network/requests-lazy.ts +3 -3
  165. package/src/core/notebook/operations.ts +1 -1
  166. package/src/core/run-app.tsx +2 -1
  167. package/src/core/runtime/__tests__/adapter.test.ts +3 -3
  168. package/src/core/runtime/adapter.ts +4 -4
  169. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +34 -0
  170. package/src/core/session/useNotebookSession.tsx +10 -5
  171. package/src/core/wasm/bridge.ts +1 -1
  172. package/src/css/app/Cell.css +7 -36
  173. package/src/plugins/core/registerReactComponent.tsx +4 -13
  174. package/src/plugins/impl/chat/chat-ui.tsx +2 -3
  175. package/src/plugins/impl/common/error-banner.tsx +5 -3
  176. package/src/stories/cell-status.stories.tsx +17 -10
  177. package/src/stories/cell.stories.tsx +0 -31
  178. package/src/stories/layout/vertical/one-column.stories.tsx +0 -9
  179. package/src/utils/time.ts +10 -0
  180. package/src/components/chat/reasoning-accordion.tsx +0 -61
  181. package/src/components/chat/tool-call/tool-activity-group.tsx +0 -49
  182. package/src/components/chat/tool-call/tool-error-card.tsx +0 -81
  183. package/src/components/chat/tool-call/tool-history-row.tsx +0 -147
  184. package/src/components/icons/loading-ellipsis.tsx +0 -46
@@ -5,8 +5,9 @@ import { getDefaultStore, Provider } from "jotai";
5
5
  import { createElement, type ReactNode } from "react";
6
6
  import { beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import { cellId } from "@/__tests__/branded";
8
- import { CellId } from "@/core/cells/ids";
8
+ import type { CellId } from "@/core/cells/ids";
9
9
  import {
10
+ type Edit,
10
11
  stagedAICellsAtom,
11
12
  useStagedAICell,
12
13
  useStagedCells,
@@ -16,7 +17,6 @@ import {
16
17
  const { createActions, reducer, initialState } = visibleForTesting;
17
18
 
18
19
  // Mock the dependencies
19
- const mockCreateCell = vi.fn();
20
20
  const mockSetCellCode = vi.fn();
21
21
  const mockDeleteCellCallback = vi.fn();
22
22
 
@@ -31,7 +31,6 @@ const mockCellHandle = {
31
31
 
32
32
  vi.mock("../../cells/actions", () => ({
33
33
  useCellActions: () => ({
34
- createCell: mockCreateCell,
35
34
  setCellCode: mockSetCellCode,
36
35
  }),
37
36
  }));
@@ -55,13 +54,6 @@ vi.mock("../../codemirror/language/cell-code", () => ({
55
54
  prepareCellCodeUpdate: vi.fn(() => ({ effects: [] })),
56
55
  }));
57
56
 
58
- // Mock CellId.create
59
- vi.mock("@/core/cells/ids", () => ({
60
- CellId: {
61
- create: vi.fn(),
62
- },
63
- }));
64
-
65
57
  describe("staged-cells", () => {
66
58
  let store: ReturnType<typeof getDefaultStore>;
67
59
  let cellId1: CellId;
@@ -74,7 +66,6 @@ describe("staged-cells", () => {
74
66
 
75
67
  // Reset mocks
76
68
  vi.clearAllMocks();
77
- vi.mocked(CellId.create).mockReset();
78
69
 
79
70
  // Reset the atom state
80
71
  store.set(stagedAICellsAtom, new Map());
@@ -143,6 +134,31 @@ describe("staged-cells", () => {
143
134
  });
144
135
  });
145
136
 
137
+ it("drops a replaced cell while preserving the four remaining pending edits", () => {
138
+ const original = cellId("original");
139
+ const remaining = [cellId("a"), cellId("b"), cellId("c"), cellId("d")];
140
+ const state = new Map<CellId, Edit>([
141
+ [original, { type: "add_cell" }],
142
+ [remaining[0], { type: "add_cell" }],
143
+ [remaining[1], { type: "add_cell" }],
144
+ [remaining[2], { type: "update_cell", previousCode: "x = 1" }],
145
+ [remaining[3], { type: "delete_cell", previousCode: "y = 2" }],
146
+ ]);
147
+ const next = reducer(state, {
148
+ type: "reconcileStagedCells",
149
+ payload: remaining,
150
+ });
151
+ expect(next).toEqual(
152
+ new Map([...state].filter(([id]) => id !== original)),
153
+ );
154
+ expect(
155
+ reducer(next, { type: "reconcileStagedCells", payload: remaining }),
156
+ ).toBe(next);
157
+ expect(
158
+ reducer(next, { type: "reconcileStagedCells", payload: [] }),
159
+ ).toEqual(new Map());
160
+ });
161
+
146
162
  it("should remove cell IDs", () => {
147
163
  const state = new Map([
148
164
  [cellId1, { type: "add_cell" as const }],
@@ -242,134 +258,66 @@ describe("staged-cells", () => {
242
258
  });
243
259
  });
244
260
 
245
- describe("useStagedCells hook", () => {
246
- it("should create a staged cell with code", () => {
247
- const { result } = renderHook(() => useStagedCells(store));
248
- const testCode = "print('hello world')";
249
-
250
- // Mock CellId.create to return a predictable ID
251
- const mockCellId = cellId("mock-cell-id");
252
- vi.mocked(CellId.create).mockReturnValue(mockCellId);
253
-
254
- const returnedCellId = result.current.createStagedCell(testCode);
255
-
256
- expect(returnedCellId).toBe(mockCellId);
257
- expect(mockCreateCell).toHaveBeenCalledWith({
258
- code: testCode,
259
- id: mockCellId,
260
- });
261
- });
262
-
263
- it("should delete a staged cell", () => {
264
- const { result } = renderHook(() => useStagedCells(store));
265
- const testCellId = cellId("test-cell-id");
266
-
267
- result.current.deleteStagedCell(testCellId);
261
+ describe("useStagedCells", () => {
262
+ const stage = (id: CellId, edit: Edit) =>
263
+ store.set(stagedAICellsAtom, new Map([[id, edit]]));
268
264
 
269
- expect(mockDeleteCellCallback).toHaveBeenCalledWith({
270
- cellId: testCellId,
271
- });
272
- });
273
-
274
- it("should add staged cell with edit info", () => {
275
- const { result } = renderHook(() => useStagedCells(store));
276
-
277
- result.current.addStagedCell({
278
- cellId: cellId1,
279
- edit: { type: "update_cell", previousCode: "old code" },
280
- });
281
-
282
- // Check that the cell was added to the atom with edit info
283
- const state = store.get(stagedAICellsAtom);
284
- expect(state.has(cellId1)).toBe(true);
285
- expect(state.get(cellId1)).toEqual({
265
+ it("keeps added and updated cells by unstaging them", () => {
266
+ const { result } = renderHook(() => useStagedCells());
267
+ stage(cellId1, { type: "update_cell", previousCode: "x = 1" });
268
+ result.current.acceptStagedCell(cellId1, {
286
269
  type: "update_cell",
287
- previousCode: "old code",
270
+ previousCode: "x = 1",
288
271
  });
272
+ expect(store.get(stagedAICellsAtom).size).toBe(0);
273
+ expect(mockDeleteCellCallback).not.toHaveBeenCalled();
289
274
  });
290
275
 
291
- it("should remove staged cell", () => {
292
- const { result } = renderHook(() => useStagedCells(store));
293
-
294
- // First add cells
295
- result.current.addStagedCell({
296
- cellId: cellId1,
297
- edit: { type: "add_cell" },
298
- });
299
- result.current.addStagedCell({
300
- cellId: cellId2,
301
- edit: { type: "add_cell" },
302
- });
303
-
304
- // Then remove one
305
- result.current.removeStagedCell(cellId1);
306
-
307
- // Check that only the remaining cell is in the map
308
- const state = store.get(stagedAICellsAtom);
309
- expect(state.has(cellId1)).toBe(false);
310
- expect(state.has(cellId2)).toBe(true);
276
+ it("deletes the cell when a proposed deletion is kept", () => {
277
+ const { result } = renderHook(() => useStagedCells());
278
+ const edit: Edit = { type: "delete_cell", previousCode: "x = 1" };
279
+ stage(cellId1, edit);
280
+ result.current.acceptStagedCell(cellId1, edit);
281
+ expect(mockDeleteCellCallback).toHaveBeenCalledWith({ cellId: cellId1 });
282
+ expect(store.get(stagedAICellsAtom).size).toBe(0);
311
283
  });
312
284
 
313
- it("should handle multiple operations correctly", () => {
314
- const { result } = renderHook(() => useStagedCells(store));
315
-
316
- // Create a staged cell
317
- const mockCellId = cellId("mock-cell-id");
318
- vi.mocked(CellId.create).mockReturnValue(mockCellId);
319
-
320
- const createdCellId = result.current.createStagedCell("test code");
321
-
322
- // Verify it was created and added
323
- expect(createdCellId).toBe(mockCellId);
324
- expect(mockCreateCell).toHaveBeenCalled();
325
-
326
- let state = store.get(stagedAICellsAtom);
327
- expect(state.has(mockCellId)).toBe(true);
328
- expect(state.get(mockCellId)).toEqual({ type: cellId("add_cell") });
329
-
330
- // Delete the staged cell
331
- result.current.deleteStagedCell(mockCellId);
332
- expect(mockDeleteCellCallback).toHaveBeenCalledWith({
333
- cellId: mockCellId,
334
- });
335
-
336
- // Verify it was removed from staged cells
337
- state = store.get(stagedAICellsAtom);
338
- expect(state.has(mockCellId)).toBe(false);
285
+ it("restores the previous code when an update is discarded", () => {
286
+ const { result } = renderHook(() => useStagedCells());
287
+ const edit: Edit = { type: "update_cell", previousCode: "x = 1" };
288
+ stage(cellId1, edit);
289
+ result.current.rejectStagedCell(cellId1, edit);
290
+ expect(
291
+ mockCellHandle.current.editorViewOrNull.dispatch,
292
+ ).toHaveBeenCalled();
293
+ expect(store.get(stagedAICellsAtom).size).toBe(0);
339
294
  });
340
295
 
341
- it("should track edit history for updated cells", () => {
342
- const { result } = renderHook(() => useStagedCells(store));
343
-
344
- // Add a cell with update_cell edit type
345
- result.current.addStagedCell({
296
+ it("writes to the document when the editor is not mounted", async () => {
297
+ const { getCellEditorView } = await import("../../cells/cells");
298
+ vi.mocked(getCellEditorView).mockReturnValueOnce(undefined);
299
+ const { result } = renderHook(() => useStagedCells());
300
+ const edit: Edit = { type: "update_cell", previousCode: "x = 1" };
301
+ stage(cellId1, edit);
302
+ result.current.rejectStagedCell(cellId1, edit);
303
+ expect(mockSetCellCode).toHaveBeenCalledWith({
346
304
  cellId: cellId1,
347
- edit: { type: "update_cell", previousCode: "previous code" },
348
- });
349
-
350
- const state = store.get(stagedAICellsAtom);
351
- const edit = state.get(cellId1);
352
- expect(edit).toEqual({
353
- type: "update_cell",
354
- previousCode: "previous code",
305
+ code: "x = 1",
355
306
  });
356
307
  });
357
308
 
358
- it("should track edit history for deleted cells", () => {
359
- const { result } = renderHook(() => useStagedCells(store));
360
-
361
- // Add a cell with delete_cell edit type
362
- result.current.addStagedCell({
363
- cellId: cellId1,
364
- edit: { type: "delete_cell", previousCode: "deleted content" },
365
- });
366
-
367
- const state = store.get(stagedAICellsAtom);
368
- const edit = state.get(cellId1);
369
- expect(edit).toEqual({
370
- type: "delete_cell",
371
- previousCode: "deleted content",
372
- });
309
+ it("deletes an added cell and leaves a proposed deletion in place when discarded", () => {
310
+ const { result } = renderHook(() => useStagedCells());
311
+ stage(cellId1, { type: "add_cell" });
312
+ result.current.rejectStagedCell(cellId1, { type: "add_cell" });
313
+ expect(mockDeleteCellCallback).toHaveBeenCalledWith({ cellId: cellId1 });
314
+
315
+ mockDeleteCellCallback.mockClear();
316
+ const deletion: Edit = { type: "delete_cell", previousCode: "x = 1" };
317
+ stage(cellId2, deletion);
318
+ result.current.rejectStagedCell(cellId2, deletion);
319
+ expect(mockDeleteCellCallback).not.toHaveBeenCalled();
320
+ expect(store.get(stagedAICellsAtom).size).toBe(0);
373
321
  });
374
322
  });
375
323
  });
@@ -4,13 +4,11 @@ import { useAtomValue } from "jotai";
4
4
  import { selectAtom } from "jotai/utils";
5
5
  import { useMemo } from "react";
6
6
  import { useDeleteCellCallback } from "@/components/editor/cell/useDeleteCell";
7
- import { CellId } from "@/core/cells/ids";
7
+ import type { CellId } from "@/core/cells/ids";
8
8
  import { createReducerAndAtoms } from "@/utils/createReducer";
9
- import { Logger } from "@/utils/Logger";
10
9
  import { getCellEditorView } from "../cells/cells";
11
10
  import { useCellActions } from "../cells/actions";
12
11
  import { prepareCellCodeUpdate } from "../codemirror/language/cell-code";
13
- import type { JotaiStore } from "../state/jotai";
14
12
  import type { EditType } from "./tools/edit-notebook-tool";
15
13
 
16
14
  /** Pending AI edits shared by chat and agent workflows. */
@@ -36,6 +34,18 @@ const {
36
34
  const { cellId, edit } = action;
37
35
  return new Map([...state, [cellId, edit]]);
38
36
  },
37
+ reconcileStagedCells: (state, cellIds: readonly string[]) => {
38
+ const existing = new Set(cellIds);
39
+ const removed = [...state.keys()].filter((id) => !existing.has(id));
40
+ if (removed.length === 0) {
41
+ return state;
42
+ }
43
+ const next = new Map(state);
44
+ for (const id of removed) {
45
+ next.delete(id);
46
+ }
47
+ return next;
48
+ },
39
49
  removeStagedCell: (state, cellId: CellId) => {
40
50
  if (!state.has(cellId)) {
41
51
  return state;
@@ -48,44 +58,27 @@ const {
48
58
 
49
59
  export { useStagedAICellsActions };
50
60
 
51
- export function useStagedAICell(cellId: CellId): Edit | undefined {
61
+ export function useStagedAICell(cellId: CellId | undefined): Edit | undefined {
52
62
  const stagedCellAtom = useMemo(
53
63
  () =>
54
- selectAtom(stagedAICellsAtom, (cells) => cells.get(cellId), Object.is),
64
+ selectAtom(
65
+ stagedAICellsAtom,
66
+ (cells) => (cellId === undefined ? undefined : cells.get(cellId)),
67
+ Object.is,
68
+ ),
55
69
  [cellId],
56
70
  );
57
71
  return useAtomValue(stagedCellAtom);
58
72
  }
59
73
 
60
- interface UpdateStagedCellAction {
61
- cellId: CellId;
62
- code: string;
63
- }
64
-
65
- /** Manage staged cells without assuming which AI workflow owns them. */
66
- export function useStagedCells(store: JotaiStore) {
67
- const { addStagedCell, removeStagedCell } = useStagedAICellsActions();
68
- const { setCellCode, createCell } = useCellActions();
74
+ /** Keep or discard staged cells, whichever AI workflow staged them. */
75
+ export function useStagedCells() {
76
+ const { removeStagedCell } = useStagedAICellsActions();
77
+ const { setCellCode } = useCellActions();
69
78
  const deleteCellCallback = useDeleteCellCallback();
70
79
 
71
- const createStagedCell = (code: string): CellId => {
72
- const newCellId = CellId.create();
73
- addStagedCell({ cellId: newCellId, edit: { type: "add_cell" } });
74
- createCell({ code, id: newCellId });
75
- return newCellId;
76
- };
77
-
78
- const updateStagedCell = (opts: UpdateStagedCellAction) => {
79
- const { cellId, code } = opts;
80
- const stagedAICells = store.get(stagedAICellsAtom);
81
-
82
- if (!stagedAICells.has(cellId)) {
83
- Logger.error("Staged cell not found", { cellId });
84
- return;
85
- }
86
-
87
- // The AI may change the cell's language, and only the editor infers
88
- // one. Without a mounted editor the code arrives through the document.
80
+ // Only a mounted editor infers the cell's language; otherwise use the document.
81
+ const writeCellCode = (cellId: CellId, code: string) => {
89
82
  const editorView = getCellEditorView(cellId);
90
83
  if (editorView) {
91
84
  const update = prepareCellCodeUpdate(editorView.state, code, {
@@ -104,13 +97,30 @@ export function useStagedCells(store: JotaiStore) {
104
97
  deleteCellCallback({ cellId });
105
98
  };
106
99
 
107
- return {
108
- createStagedCell,
109
- updateStagedCell,
110
- addStagedCell,
111
- removeStagedCell,
112
- deleteStagedCell,
100
+ const acceptStagedCell = (cellId: CellId, edit: Edit) => {
101
+ if (edit.type === "delete_cell") {
102
+ deleteStagedCell(cellId);
103
+ } else {
104
+ removeStagedCell(cellId);
105
+ }
106
+ };
107
+
108
+ const rejectStagedCell = (cellId: CellId, edit: Edit) => {
109
+ switch (edit.type) {
110
+ case "update_cell":
111
+ writeCellCode(cellId, edit.previousCode);
112
+ removeStagedCell(cellId);
113
+ break;
114
+ case "add_cell":
115
+ deleteStagedCell(cellId);
116
+ break;
117
+ case "delete_cell":
118
+ removeStagedCell(cellId);
119
+ break;
120
+ }
113
121
  };
122
+
123
+ return { acceptStagedCell, rejectStagedCell };
114
124
  }
115
125
 
116
126
  export { stagedAICellsAtom };