@marimo-team/islands 0.21.2-dev6 → 0.21.2-dev61

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 (152) hide show
  1. package/dist/{ConnectedDataExplorerComponent-D0GoOd_c.js → ConnectedDataExplorerComponent-DrWDbHRV.js} +1 -1
  2. package/dist/{any-language-editor-DlsjUw_l.js → any-language-editor-BRpxklRq.js} +1 -1
  3. package/dist/{copy-DIK6DiIA.js → copy-BjkXCUxP.js} +12 -2
  4. package/dist/{esm-BLobyqMs.js → esm-No_6eSQS.js} +1 -1
  5. package/dist/{glide-data-editor-pZyd9UJ_.js → glide-data-editor-858wsVkd.js} +1 -1
  6. package/dist/main.js +821 -417
  7. package/dist/{spec-Bfvf9Hre.js → spec-oVDndBz4.js} +25 -16
  8. package/dist/style.css +1 -1
  9. package/package.json +1 -1
  10. package/src/__mocks__/notebook.ts +9 -9
  11. package/src/__mocks__/requests.ts +1 -0
  12. package/src/__tests__/branded.ts +20 -0
  13. package/src/components/app-config/user-config-form.tsx +5 -4
  14. package/src/components/data-table/__tests__/utils.test.ts +138 -1
  15. package/src/components/data-table/charts/__tests__/storage.test.ts +7 -7
  16. package/src/components/data-table/context-menu.tsx +9 -5
  17. package/src/components/data-table/data-table.tsx +3 -0
  18. package/src/components/data-table/range-focus/__tests__/atoms.test.ts +8 -2
  19. package/src/components/data-table/range-focus/__tests__/test-utils.ts +2 -0
  20. package/src/components/data-table/range-focus/__tests__/utils.test.ts +82 -8
  21. package/src/components/data-table/range-focus/atoms.ts +2 -2
  22. package/src/components/data-table/range-focus/utils.ts +50 -12
  23. package/src/components/data-table/types.ts +7 -0
  24. package/src/components/data-table/utils.ts +87 -0
  25. package/src/components/editor/__tests__/data-attributes.test.tsx +8 -8
  26. package/src/components/editor/ai/__tests__/completion-utils.test.ts +15 -15
  27. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +2 -2
  28. package/src/components/editor/connections/storage/as-code.ts +2 -2
  29. package/src/components/editor/file-tree/file-explorer.tsx +16 -2
  30. package/src/components/editor/file-tree/file-viewer.tsx +17 -3
  31. package/src/components/editor/navigation/__tests__/clipboard.test.ts +2 -2
  32. package/src/components/editor/navigation/__tests__/selection.test.ts +7 -6
  33. package/src/components/editor/navigation/__tests__/state.test.ts +8 -7
  34. package/src/components/editor/output/MarimoErrorOutput.tsx +7 -7
  35. package/src/components/editor/output/__tests__/traceback.test.tsx +4 -4
  36. package/src/components/editor/output/console/__tests__/ConsoleOutput.test.tsx +4 -4
  37. package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +8 -1
  38. package/src/components/storage/storage-file-viewer.tsx +35 -1
  39. package/src/components/storage/storage-inspector.tsx +9 -4
  40. package/src/components/storage/storage-snippets.ts +3 -3
  41. package/src/components/tracing/tracing.tsx +3 -1
  42. package/src/components/ui/range-slider.tsx +108 -1
  43. package/src/core/ai/__tests__/staged-cells.test.ts +9 -8
  44. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +31 -31
  45. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -3
  46. package/src/core/ai/context/providers/__tests__/tables.test.ts +3 -2
  47. package/src/core/ai/context/providers/__tests__/variable.test.ts +84 -63
  48. package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +10 -9
  49. package/src/core/ai/tools/__tests__/run-cells-tool.test.ts +6 -6
  50. package/src/core/ai/tools/edit-notebook-tool.ts +3 -3
  51. package/src/core/cells/__tests__/add-missing-import.test.ts +3 -3
  52. package/src/core/cells/__tests__/apply-transaction.test.ts +279 -0
  53. package/src/core/cells/__tests__/cells.test.ts +198 -135
  54. package/src/core/cells/__tests__/document-changes.test.ts +575 -0
  55. package/src/core/cells/__tests__/document-roundtrip.test.ts +376 -0
  56. package/src/core/cells/__tests__/focus.test.ts +5 -4
  57. package/src/core/cells/__tests__/logs.test.ts +13 -12
  58. package/src/core/cells/__tests__/pending-delete-service.test.tsx +3 -3
  59. package/src/core/cells/__tests__/runs.test.ts +22 -21
  60. package/src/core/cells/__tests__/scrollCellIntoView.test.ts +8 -7
  61. package/src/core/cells/__tests__/session.test.ts +23 -22
  62. package/src/core/cells/cells.ts +29 -4
  63. package/src/core/cells/document-changes.ts +644 -0
  64. package/src/core/cells/ids.ts +5 -5
  65. package/src/core/cells/logs.ts +2 -2
  66. package/src/core/cells/runs.ts +6 -8
  67. package/src/core/codemirror/__tests__/format.test.ts +34 -36
  68. package/src/core/codemirror/__tests__/setup.test.ts +2 -2
  69. package/src/core/codemirror/cells/__tests__/extensions.test.ts +114 -0
  70. package/src/core/codemirror/cells/__tests__/traceback-decorations.test.ts +33 -32
  71. package/src/core/codemirror/cells/extensions.ts +66 -23
  72. package/src/core/codemirror/completion/__tests__/keymap.test.ts +15 -35
  73. package/src/core/codemirror/completion/keymap.ts +14 -4
  74. package/src/core/codemirror/copilot/__tests__/getCodes.test.ts +12 -13
  75. package/src/core/codemirror/language/__tests__/utils.test.ts +3 -3
  76. package/src/core/codemirror/language/embedded/__tests__/embedded-python.test.ts +7 -8
  77. package/src/core/codemirror/language/languages/python.ts +4 -0
  78. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +4 -3
  79. package/src/core/codemirror/lsp/notebook-lsp.ts +28 -2
  80. package/src/core/codemirror/reactive-references/__tests__/analyzer.test.ts +7 -6
  81. package/src/core/codemirror/reactive-references/analyzer.ts +2 -2
  82. package/src/core/codemirror/rtc/loro/__tests__/sync.test.ts +52 -0
  83. package/src/core/codemirror/rtc/loro/sync.ts +1 -0
  84. package/src/core/datasets/__tests__/data-source.test.ts +5 -6
  85. package/src/core/datasets/state.ts +1 -1
  86. package/src/core/errors/__tests__/errors.test.ts +2 -1
  87. package/src/core/export/__tests__/hooks.test.ts +37 -36
  88. package/src/core/islands/bridge.ts +1 -0
  89. package/src/core/islands/main.ts +4 -7
  90. package/src/core/kernel/__tests__/handlers.test.ts +5 -4
  91. package/src/core/kernel/handlers.ts +7 -4
  92. package/src/core/network/DeferredRequestRegistry.ts +2 -2
  93. package/src/core/network/__tests__/CachingRequestRegistry.test.ts +9 -10
  94. package/src/core/network/__tests__/DeferredRequestRegistry.test.ts +4 -6
  95. package/src/core/network/requests-lazy.ts +1 -0
  96. package/src/core/network/requests-network.ts +9 -0
  97. package/src/core/network/requests-static.ts +1 -0
  98. package/src/core/network/requests-toasting.tsx +1 -0
  99. package/src/core/network/types.ts +5 -0
  100. package/src/core/static/__tests__/virtual-file-tracker.test.ts +8 -8
  101. package/src/core/static/virtual-file-tracker.ts +1 -1
  102. package/src/core/storage/__tests__/state.test.ts +31 -21
  103. package/src/core/storage/state.ts +1 -1
  104. package/src/core/variables/__tests__/state.test.ts +6 -6
  105. package/src/core/variables/types.ts +2 -2
  106. package/src/core/wasm/__tests__/state.test.ts +8 -8
  107. package/src/core/wasm/bridge.ts +1 -0
  108. package/src/core/websocket/useMarimoKernelConnection.tsx +31 -16
  109. package/src/css/app/fonts.css +6 -6
  110. package/src/css/md-tooltip.css +4 -39
  111. package/src/css/md.css +7 -0
  112. package/src/fonts/Fira_Mono/FiraMono-Bold.woff2 +0 -0
  113. package/src/fonts/Fira_Mono/FiraMono-Medium.woff2 +0 -0
  114. package/src/fonts/Fira_Mono/FiraMono-Regular.woff2 +0 -0
  115. package/src/fonts/Lora/Lora-VariableFont_wght.woff2 +0 -0
  116. package/src/fonts/PT_Sans/PTSans-Bold.woff2 +0 -0
  117. package/src/fonts/PT_Sans/PTSans-Regular.woff2 +0 -0
  118. package/src/plugins/core/RenderHTML.tsx +17 -0
  119. package/src/plugins/core/__test__/RenderHTML.test.ts +45 -0
  120. package/src/plugins/core/sanitize-html.ts +25 -18
  121. package/src/plugins/impl/DataTablePlugin.tsx +23 -2
  122. package/src/plugins/impl/SliderPlugin.tsx +1 -3
  123. package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +120 -0
  124. package/src/plugins/impl/anywidget/model.ts +1 -2
  125. package/src/stories/cell.stories.tsx +8 -8
  126. package/src/stories/layout/vertical/one-column.stories.tsx +9 -8
  127. package/src/stories/log-viewer.stories.tsx +8 -8
  128. package/src/stories/variables.stories.tsx +2 -2
  129. package/src/utils/__tests__/download.test.tsx +21 -20
  130. package/src/utils/copy.ts +18 -5
  131. package/src/utils/createReducer.ts +26 -11
  132. package/src/utils/download.ts +4 -3
  133. package/src/utils/html-to-image.ts +6 -0
  134. package/src/utils/json/base64.ts +3 -3
  135. package/src/utils/traceback.ts +5 -3
  136. package/src/fonts/Fira_Mono/FiraMono-Bold.ttf +0 -0
  137. package/src/fonts/Fira_Mono/FiraMono-Medium.ttf +0 -0
  138. package/src/fonts/Fira_Mono/FiraMono-Regular.ttf +0 -0
  139. package/src/fonts/Lora/Lora-Italic-VariableFont_wght.ttf +0 -0
  140. package/src/fonts/Lora/Lora-VariableFont_wght.ttf +0 -0
  141. package/src/fonts/Lora/static/Lora-Bold.ttf +0 -0
  142. package/src/fonts/Lora/static/Lora-BoldItalic.ttf +0 -0
  143. package/src/fonts/Lora/static/Lora-Italic.ttf +0 -0
  144. package/src/fonts/Lora/static/Lora-Medium.ttf +0 -0
  145. package/src/fonts/Lora/static/Lora-MediumItalic.ttf +0 -0
  146. package/src/fonts/Lora/static/Lora-Regular.ttf +0 -0
  147. package/src/fonts/Lora/static/Lora-SemiBold.ttf +0 -0
  148. package/src/fonts/Lora/static/Lora-SemiBoldItalic.ttf +0 -0
  149. package/src/fonts/PT_Sans/PTSans-Bold.ttf +0 -0
  150. package/src/fonts/PT_Sans/PTSans-BoldItalic.ttf +0 -0
  151. package/src/fonts/PT_Sans/PTSans-Italic.ttf +0 -0
  152. package/src/fonts/PT_Sans/PTSans-Regular.ttf +0 -0
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { beforeEach, describe, expect, it } from "vitest";
4
+ import { cellId } from "@/__tests__/branded";
4
5
  import type { CellMessage } from "@/core/kernel/messages";
5
6
  import { invariant } from "@/utils/invariant";
6
7
  import {
@@ -22,13 +23,13 @@ describe("RunsState Reducer", () => {
22
23
  let state: RunsState;
23
24
 
24
25
  const runId = "run1" as RunId;
25
- const cellId = "cell1";
26
+ const testCellId = cellId("cell1");
26
27
  const timestamp = Date.now();
27
28
  const code = "print('Hello World')";
28
29
 
29
30
  const cellNotification: CellMessage = {
30
31
  run_id: runId,
31
- cell_id: cellId,
32
+ cell_id: testCellId,
32
33
  timestamp,
33
34
  status: "queued",
34
35
  };
@@ -61,9 +62,9 @@ describe("RunsState Reducer", () => {
61
62
  runStartTime: timestamp,
62
63
  cellRuns: new Map([
63
64
  [
64
- cellId,
65
+ testCellId,
65
66
  {
66
- cellId,
67
+ cellId: testCellId,
67
68
  code: code.slice(0, MAX_CODE_LENGTH),
68
69
  elapsedTime: 0,
69
70
  startTime: timestamp,
@@ -98,7 +99,7 @@ describe("RunsState Reducer", () => {
98
99
  payload: {
99
100
  cellNotification: {
100
101
  run_id: runId2,
101
- cell_id: "cell2",
102
+ cell_id: cellId("cell2"),
102
103
  timestamp,
103
104
  status: "queued",
104
105
  },
@@ -124,7 +125,7 @@ describe("RunsState Reducer", () => {
124
125
  payload: {
125
126
  cellNotification: {
126
127
  run_id: runId,
127
- cell_id: cellId,
128
+ cell_id: testCellId,
128
129
  timestamp: timestamp + 1000,
129
130
  status: "running",
130
131
  },
@@ -145,7 +146,7 @@ describe("RunsState Reducer", () => {
145
146
  payload: {
146
147
  cellNotification: {
147
148
  run_id: runId,
148
- cell_id: cellId,
149
+ cell_id: testCellId,
149
150
  timestamp: runStartTimestamp + 5000,
150
151
  status: "success",
151
152
  },
@@ -171,8 +172,8 @@ describe("RunsState Reducer", () => {
171
172
  type: "addCellNotification",
172
173
  payload: {
173
174
  cellNotification: {
174
- run_id: `run${i}`,
175
- cell_id: `cell${i}`,
175
+ run_id: `run${i}` as RunId,
176
+ cell_id: cellId(`cell${i}`),
176
177
  timestamp: timestamp,
177
178
  status: "queued",
178
179
  },
@@ -198,7 +199,7 @@ describe("RunsState Reducer", () => {
198
199
  payload: {
199
200
  cellNotification: {
200
201
  run_id: runId,
201
- cell_id: cellId,
202
+ cell_id: testCellId,
202
203
  timestamp,
203
204
  status: "queued",
204
205
  },
@@ -220,7 +221,7 @@ describe("RunsState Reducer", () => {
220
221
  payload: {
221
222
  cellNotification: {
222
223
  run_id: runId,
223
- cell_id: cellId,
224
+ cell_id: testCellId,
224
225
  timestamp: errorTimestamp,
225
226
  status: "running",
226
227
  output: {
@@ -248,7 +249,7 @@ describe("RunsState Reducer", () => {
248
249
  payload: {
249
250
  cellNotification: {
250
251
  run_id: runId,
251
- cell_id: cellId,
252
+ cell_id: testCellId,
252
253
  timestamp: errorTimestamp,
253
254
  status: "running",
254
255
  output: {
@@ -273,7 +274,7 @@ describe("RunsState Reducer", () => {
273
274
  payload: {
274
275
  cellNotification: {
275
276
  run_id: runId,
276
- cell_id: cellId,
277
+ cell_id: testCellId,
277
278
  timestamp,
278
279
  output: {
279
280
  channel: "marimo-error",
@@ -288,7 +289,7 @@ describe("RunsState Reducer", () => {
288
289
  payload: {
289
290
  cellNotification: {
290
291
  run_id: runId,
291
- cell_id: cellId,
292
+ cell_id: testCellId,
292
293
  timestamp: timestamp + 2000,
293
294
  status: "running", // shouldn't happen
294
295
  },
@@ -312,7 +313,7 @@ describe("RunsState Reducer", () => {
312
313
  payload: {
313
314
  cellNotification: {
314
315
  run_id: runId2,
315
- cell_id: "cell2",
316
+ cell_id: cellId("cell2"),
316
317
  timestamp: timestamp + 1000,
317
318
  status: "queued",
318
319
  },
@@ -325,7 +326,7 @@ describe("RunsState Reducer", () => {
325
326
  payload: {
326
327
  cellNotification: {
327
328
  run_id: runId3,
328
- cell_id: "cell3",
329
+ cell_id: cellId("cell3"),
329
330
  timestamp: timestamp + 2000,
330
331
  status: "queued",
331
332
  },
@@ -345,7 +346,7 @@ describe("RunsState Reducer", () => {
345
346
  payload: {
346
347
  cellNotification: {
347
348
  run_id: runId,
348
- cell_id: "cell2",
349
+ cell_id: cellId("cell2"),
349
350
  timestamp: timestamp + 1000,
350
351
  status: "queued",
351
352
  },
@@ -367,7 +368,7 @@ describe("RunsState Reducer", () => {
367
368
  payload: {
368
369
  cellNotification: {
369
370
  run_id: runId,
370
- cell_id: cellId,
371
+ cell_id: testCellId,
371
372
  timestamp: timestamp + 1000,
372
373
  status: "running",
373
374
  },
@@ -386,7 +387,7 @@ describe("RunsState Reducer", () => {
386
387
  payload: {
387
388
  cellNotification: {
388
389
  run_id: runId,
389
- cell_id: cellId,
390
+ cell_id: testCellId,
390
391
  timestamp,
391
392
  status: "queued",
392
393
  },
@@ -405,7 +406,7 @@ describe("RunsState Reducer", () => {
405
406
  payload: {
406
407
  cellNotification: {
407
408
  run_id: runId,
408
- cell_id: cellId,
409
+ cell_id: testCellId,
409
410
  timestamp,
410
411
  status: "queued",
411
412
  },
@@ -419,7 +420,7 @@ describe("RunsState Reducer", () => {
419
420
  payload: {
420
421
  cellNotification: {
421
422
  run_id: runId,
422
- cell_id: "cell2",
423
+ cell_id: cellId("cell2"),
423
424
  timestamp: timestamp + 1000,
424
425
  status: "queued",
425
426
  },
@@ -1,7 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
- import { type CellId, HTMLCellId } from "@/core/cells/ids";
4
+ import { cellId } from "@/__tests__/branded";
5
+ import { HTMLCellId } from "@/core/cells/ids";
5
6
  import { Logger } from "@/utils/Logger";
6
7
 
7
8
  // Mock the getCellEditorView function
@@ -20,12 +21,12 @@ vi.mock("@/core/codemirror/extensions", () => ({
20
21
  const { scrollCellIntoView } = await import("@/core/cells/scrollCellIntoView");
21
22
 
22
23
  describe("scrollCellIntoView", () => {
23
- const cellId = "test-cell-id" as CellId;
24
+ const cid = cellId("test-cell-id");
24
25
  let cellElement: HTMLElement;
25
26
 
26
27
  beforeEach(() => {
27
28
  cellElement = document.createElement("div");
28
- cellElement.id = HTMLCellId.create(cellId);
29
+ cellElement.id = HTMLCellId.create(cid);
29
30
  cellElement.scrollIntoView = vi.fn();
30
31
  document.body.append(cellElement);
31
32
 
@@ -42,7 +43,7 @@ describe("scrollCellIntoView", () => {
42
43
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
44
  mockGetCellEditorView.mockReturnValue(mockEditor as any);
44
45
 
45
- scrollCellIntoView(cellId);
46
+ scrollCellIntoView(cid);
46
47
 
47
48
  expect(mockScrollActiveLineIntoView).toHaveBeenCalledWith(mockEditor, {
48
49
  behavior: "instant",
@@ -51,7 +52,7 @@ describe("scrollCellIntoView", () => {
51
52
  });
52
53
 
53
54
  it("should scroll cell element when editor is not focused", () => {
54
- scrollCellIntoView(cellId);
55
+ scrollCellIntoView(cid);
55
56
 
56
57
  expect(mockScrollActiveLineIntoView).not.toHaveBeenCalled();
57
58
  expect(cellElement.scrollIntoView).toHaveBeenCalledWith({
@@ -64,10 +65,10 @@ describe("scrollCellIntoView", () => {
64
65
  const warnSpy = vi.spyOn(Logger, "warn").mockImplementation(vi.fn());
65
66
  cellElement.remove();
66
67
 
67
- scrollCellIntoView(cellId);
68
+ scrollCellIntoView(cid);
68
69
 
69
70
  expect(warnSpy).toHaveBeenCalledWith(
70
- `[CellFocusManager] scrollCellIntoView: element not found: ${cellId}`,
71
+ `[CellFocusManager] scrollCellIntoView: element not found: ${cid}`,
71
72
  );
72
73
  warnSpy.mockRestore();
73
74
  });
@@ -4,11 +4,12 @@ import type * as api from "@marimo-team/marimo-api";
4
4
  /* eslint-disable @typescript-eslint/no-explicit-any */
5
5
  import { beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import { Mocks } from "@/__mocks__/common";
7
+ import { cellId } from "@/__tests__/branded";
7
8
  import { parseOutline } from "@/core/dom/outline";
8
9
  import { MultiColumn, visibleForTesting } from "@/utils/id-tree";
9
10
  import { invariant } from "@/utils/invariant";
10
11
  import { Logger } from "@/utils/Logger";
11
- import { type CellId, SETUP_CELL_ID } from "../ids";
12
+ import { SETUP_CELL_ID } from "../ids";
12
13
  import { notebookStateFromSession } from "../session";
13
14
 
14
15
  // Mock dependencies
@@ -22,7 +23,7 @@ type SessionCell = api.Session["NotebookSessionV1"]["cells"][0];
22
23
  type NotebookCell = api.Notebook["NotebookV1"]["cells"][0];
23
24
 
24
25
  // Test constants
25
- const CELL_1 = "cell-1" as CellId;
26
+ const CELL_1 = cellId("cell-1");
26
27
 
27
28
  describe("notebookStateFromSession", () => {
28
29
  beforeEach(() => {
@@ -671,9 +672,9 @@ describe("notebookStateFromSession", () => {
671
672
  );
672
673
  // Should have correct code and output for each cell
673
674
  for (const code of ["a", "b", "c", "d", "e", "f"]) {
674
- const cellId = `cell-${code}` as CellId;
675
- expect(result.cellData[cellId].code).toBe(code);
676
- expect(result.cellRuntime[cellId].output).toEqual({
675
+ const cid = cellId(`cell-${code}`);
676
+ expect(result.cellData[cid].code).toBe(code);
677
+ expect(result.cellRuntime[cid].output).toEqual({
677
678
  channel: "output",
678
679
  data: `${code.toUpperCase()}!`,
679
680
  mimetype: "text/plain",
@@ -751,26 +752,26 @@ describe("notebookStateFromSession", () => {
751
752
  );
752
753
 
753
754
  // Should have correct code for each cell
754
- expect(result.cellData["cell-a" as CellId].code).toBe("a");
755
- expect(result.cellData["cell-c" as CellId].code).toBe("c");
756
- expect(result.cellData["cell-z" as CellId].code).toBe("z");
757
- expect(result.cellData["cell-e" as CellId].code).toBe("e");
758
- expect(result.cellData["cell-g" as CellId].code).toBe("g");
755
+ expect(result.cellData[cellId("cell-a")].code).toBe("a");
756
+ expect(result.cellData[cellId("cell-c")].code).toBe("c");
757
+ expect(result.cellData[cellId("cell-z")].code).toBe("z");
758
+ expect(result.cellData[cellId("cell-e")].code).toBe("e");
759
+ expect(result.cellData[cellId("cell-g")].code).toBe("g");
759
760
 
760
761
  // Should have session outputs for matching cells (a, c, e)
761
- expect(result.cellRuntime["cell-a" as CellId].output).toEqual({
762
+ expect(result.cellRuntime[cellId("cell-a")].output).toEqual({
762
763
  channel: "output",
763
764
  data: "A!",
764
765
  mimetype: "text/plain",
765
766
  timestamp: 0,
766
767
  });
767
- expect(result.cellRuntime["cell-c" as CellId].output).toEqual({
768
+ expect(result.cellRuntime[cellId("cell-c")].output).toEqual({
768
769
  channel: "output",
769
770
  data: "C!",
770
771
  mimetype: "text/plain",
771
772
  timestamp: 0,
772
773
  });
773
- expect(result.cellRuntime["cell-e" as CellId].output).toEqual({
774
+ expect(result.cellRuntime[cellId("cell-e")].output).toEqual({
774
775
  channel: "output",
775
776
  data: "E!",
776
777
  mimetype: "text/plain",
@@ -778,8 +779,8 @@ describe("notebookStateFromSession", () => {
778
779
  });
779
780
 
780
781
  // Should have no output for new cells (z, g) - they get stub session cells
781
- expect(result.cellRuntime["cell-z" as CellId].output).toBeNull();
782
- expect(result.cellRuntime["cell-g" as CellId].output).toBeNull();
782
+ expect(result.cellRuntime[cellId("cell-z")].output).toBeNull();
783
+ expect(result.cellRuntime[cellId("cell-g")].output).toBeNull();
783
784
 
784
785
  // Should log warning about different cells
785
786
  expect(Logger.warn).toHaveBeenCalledWith(
@@ -875,18 +876,18 @@ describe("notebookStateFromSession", () => {
875
876
  );
876
877
 
877
878
  // Should have correct code from notebook
878
- expect(result.cellData["cell-1" as CellId].code).toBe("1 / 0");
879
- expect(result.cellData["cell-2" as CellId].code).toBe('mo.md("Hello")');
880
- expect(result.cellData["cell-3" as CellId].code).toBe(
879
+ expect(result.cellData[cellId("cell-1")].code).toBe("1 / 0");
880
+ expect(result.cellData[cellId("cell-2")].code).toBe('mo.md("Hello")');
881
+ expect(result.cellData[cellId("cell-3")].code).toBe(
881
882
  "x = mo.ui.slider(0, 10)",
882
883
  );
883
884
 
884
885
  // cell-1: No matching session cell (hash is null), gets stub session cell
885
- expect(result.cellRuntime["cell-1" as CellId].output).toBeNull();
886
- expect(result.cellRuntime["cell-1" as CellId].consoleOutputs).toEqual([]);
886
+ expect(result.cellRuntime[cellId("cell-1")].output).toBeNull();
887
+ expect(result.cellRuntime[cellId("cell-1")].consoleOutputs).toEqual([]);
887
888
 
888
889
  // cell-2: Matches session cell-1 by hash (moMd), gets its output
889
- expect(result.cellRuntime["cell-2" as CellId].output).toEqual({
890
+ expect(result.cellRuntime[cellId("cell-2")].output).toEqual({
890
891
  channel: "output",
891
892
  data: "Welcome to marimo!",
892
893
  mimetype: "text/markdown",
@@ -894,7 +895,7 @@ describe("notebookStateFromSession", () => {
894
895
  });
895
896
 
896
897
  // cell-3: Matches session cell-2 by hash (slider), gets its output
897
- expect(result.cellRuntime["cell-3" as CellId].output).toEqual({
898
+ expect(result.cellRuntime[cellId("cell-3")].output).toEqual({
898
899
  channel: "output",
899
900
  data: "",
900
901
  mimetype: "text/plain",
@@ -29,6 +29,7 @@ import type { CellConfig } from "../network/types";
29
29
  import { isRtcEnabled } from "../rtc/state";
30
30
  import { createDeepEqualAtom, store } from "../state/jotai";
31
31
  import { prepareCellForExecution, transitionCell } from "./cell";
32
+ import { documentTransactionMiddleware } from "./document-changes";
32
33
  import { CellId, SCRATCH_CELL_ID, SETUP_CELL_ID } from "./ids";
33
34
  import { type CellLog, getCellLogsForMessage } from "./logs";
34
35
  import {
@@ -174,6 +175,7 @@ export interface CreateNewCellAction {
174
175
  */
175
176
  const {
176
177
  reducer,
178
+ addMiddleware,
177
179
  createActions,
178
180
  useActions,
179
181
  valueAtom: notebookAtom,
@@ -755,14 +757,29 @@ const {
755
757
  });
756
758
  },
757
759
  handleCellMessage: (state, message: CellMessage) => {
758
- const cellId = message.cell_id as CellId;
759
- const nextState = updateCellRuntimeState({
760
+ const cellId = message.cell_id;
761
+ let nextState = updateCellRuntimeState({
760
762
  state,
761
763
  cellId,
762
764
  cellReducer: (cell) => {
763
765
  return transitionCell(cell, message);
764
766
  },
765
767
  });
768
+ // When a cell is queued for execution, snapshot the current code
769
+ // as lastCodeRun. This clears staleness for cells executed by the
770
+ // kernel (e.g. via code_mode). If the user edits during execution,
771
+ // code !== lastCodeRun keeps the cell stale.
772
+ if (message.status === "queued") {
773
+ nextState = updateCellData({
774
+ state: nextState,
775
+ cellId,
776
+ cellReducer: (cell) => ({
777
+ ...cell,
778
+ lastCodeRun: cell.code.trim(),
779
+ edited: false,
780
+ }),
781
+ });
782
+ }
766
783
  return {
767
784
  ...nextState,
768
785
  cellLogs: [...nextState.cellLogs, ...getCellLogsForMessage(message)],
@@ -1405,6 +1422,12 @@ const {
1405
1422
  },
1406
1423
  });
1407
1424
 
1425
+ // We apply the middleware here (rather than inline in createReducerAndAtoms)
1426
+ // so that the document transaction middleware can import CellActions and
1427
+ // strictly type the dispatched actions without creating a circular dependency.
1428
+ // @ts-expect-error - TODO: We should have better types for the middleware that are strict
1429
+ addMiddleware(documentTransactionMiddleware);
1430
+
1408
1431
  function isCellCodeHidden(state: NotebookState, cellId: CellId): boolean {
1409
1432
  return (
1410
1433
  Boolean(state.cellData[cellId].config.hide_code) &&
@@ -1789,8 +1812,10 @@ export function createTracebackInfoAtom(
1789
1812
  * Use this hook to dispatch cell actions. This hook will not cause a re-render
1790
1813
  * when cells change.
1791
1814
  */
1792
- export function useCellActions(): CellActions {
1793
- return useActions();
1815
+ export function useCellActions(
1816
+ options: { skipMiddleware?: boolean } = {},
1817
+ ): CellActions {
1818
+ return useActions(options);
1794
1819
  }
1795
1820
 
1796
1821
  /**