@marimo-team/islands 0.25.3-dev275 → 0.25.3-dev279

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 (60) hide show
  1. package/dist/{ErrorBoundary-BuIXiuc5.js → ErrorBoundary-DmTMuDPR.js} +1 -1
  2. package/dist/{chat-ui-SZ6lSH52.js → chat-ui-v1Fdh0NA.js} +270 -270
  3. package/dist/{code-visibility-BEAPMrC-.js → code-visibility-Dw4-TXN0.js} +2 -2
  4. package/dist/{common-CEmzaoJ8.js → common-BXMFi4lr.js} +4947 -4943
  5. package/dist/{data-grid-overlay-editor-CUo25_W8.js → data-grid-overlay-editor-hZzwX7Ia.js} +1 -1
  6. package/dist/{error-entries-B-1yUdcH.js → error-entries-C1S-Rmyt.js} +545 -535
  7. package/dist/{feedback-button-BqiR7zMO.js → feedback-button-6Ui2TDLR.js} +5 -5
  8. package/dist/{glide-data-editor-B7dguvqk.js → glide-data-editor-PZNlrSDI.js} +3 -3
  9. package/dist/{globals-CYykXfGI.js → globals-DX6FPZYq.js} +1 -1
  10. package/dist/{html-to-image-DC_pmZLs.js → html-to-image-CqGUd58T.js} +14452 -14452
  11. package/dist/main.js +6031 -6036
  12. package/dist/{number-overlay-editor-C4u7SJAJ.js → number-overlay-editor-D0CsL3_R.js} +1 -1
  13. package/dist/{process-output-DYu84VfZ.js → process-output-DskX9jLa.js} +2 -2
  14. package/dist/{reveal-component-DGi-irHV.js → reveal-component-D7xeI1QR.js} +906 -906
  15. package/package.json +1 -1
  16. package/src/__mocks__/executions.ts +4 -2
  17. package/src/__mocks__/notebook.ts +0 -2
  18. package/src/__tests__/CellStatus.test.tsx +6 -6
  19. package/src/components/editor/__tests__/data-attributes.test.tsx +0 -2
  20. package/src/components/editor/__tests__/output-persistence.test.tsx +20 -5
  21. package/src/components/editor/cell/CellStatus.tsx +6 -5
  22. package/src/components/editor/cell/__tests__/useRunCells.test.tsx +169 -0
  23. package/src/components/editor/cell/collapse.tsx +3 -4
  24. package/src/components/editor/cell/useRunCells.ts +32 -3
  25. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +2 -3
  26. package/src/components/editor/notebook-cell.tsx +23 -37
  27. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +4 -10
  28. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -2
  29. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +8 -5
  30. package/src/components/slides/minimap.tsx +3 -2
  31. package/src/components/slides/reveal-component.tsx +3 -2
  32. package/src/components/slides/slide-cell-view.tsx +8 -8
  33. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -101
  34. package/src/core/cells/__tests__/actions.test.ts +0 -1
  35. package/src/core/cells/__tests__/cell.test.ts +81 -17
  36. package/src/core/cells/__tests__/cells.test.ts +0 -100
  37. package/src/core/cells/__tests__/document.test.ts +2 -8
  38. package/src/core/cells/__tests__/utils.test.ts +93 -24
  39. package/src/core/cells/actions.ts +0 -1
  40. package/src/core/cells/cell.ts +37 -14
  41. package/src/core/cells/cells.ts +10 -99
  42. package/src/core/cells/document.ts +2 -13
  43. package/src/core/cells/types.ts +0 -23
  44. package/src/core/cells/utils.ts +44 -17
  45. package/src/core/codemirror/cells/extensions.ts +0 -14
  46. package/src/core/errors/__tests__/error-entries.test.ts +40 -1
  47. package/src/core/errors/error-entries.ts +2 -3
  48. package/src/core/executions/__tests__/freshness.test.ts +420 -0
  49. package/src/core/executions/__tests__/hooks.test.tsx +106 -0
  50. package/src/core/executions/atoms.ts +22 -0
  51. package/src/core/executions/freshness.ts +164 -0
  52. package/src/core/executions/hooks.ts +17 -1
  53. package/src/core/executions/index.ts +3 -1
  54. package/src/core/islands/bootstrap.ts +0 -1
  55. package/src/core/kernel/handlers.ts +0 -4
  56. package/src/core/session/useNotebookSession.tsx +2 -7
  57. package/src/stories/cell-status.stories.tsx +12 -14
  58. package/src/stories/cell.stories.tsx +44 -10
  59. package/src/stories/layout/vertical/one-column.stories.tsx +0 -10
  60. package/src/components/editor/cell/__tests__/useRunCells.test.ts +0 -45
@@ -173,7 +173,6 @@ describe("cell reducer", () => {
173
173
  ...config,
174
174
  ...(hideCode != null && { hide_code: hideCode }),
175
175
  }),
176
- edited: Boolean(code),
177
176
  }),
178
177
  },
179
178
  cellRuntime: { ...state.cellRuntime, [id]: createCellRuntimeState() },
@@ -206,16 +205,6 @@ describe("cell reducer", () => {
206
205
  });
207
206
  }
208
207
 
209
- /** Format a cell's code, as the editor reports it. */
210
- function formatCode(cellId: CellId, code: string) {
211
- actions.codeFormatted({
212
- cellId,
213
- before: state.cellData[cellId].code,
214
- after: code,
215
- });
216
- editCode(cellId, code);
217
- }
218
-
219
208
  let i = 0;
220
209
  const originalCreate = CellId.create.bind(CellId);
221
210
 
@@ -288,7 +277,6 @@ describe("cell reducer", () => {
288
277
  timestamp: 2,
289
278
  });
290
279
  const runtime = state.cellRuntime[firstCellId];
291
- expect(state.cellData[firstCellId].lastCodeRun).toBe("print('new code')");
292
280
  expect(runtime.output).toEqual(output);
293
281
  expect(runtime.consoleOutputs[0].code).toBe(code);
294
282
  expect(runtime.consoleOutputs[0].data).toContain("old failure");
@@ -299,15 +287,11 @@ describe("cell reducer", () => {
299
287
  /////////////////
300
288
  // Initial state
301
289
  let cell = cells[0];
302
- expect(cell.lastCodeRun).toBe(null);
303
- expect(cell.edited).toBe(false);
304
290
  expect(cell).toMatchSnapshot("initial state");
305
291
 
306
292
  // Update code
307
293
  editCode(firstCellId, "import marimo as mo");
308
294
  cell = cells[0];
309
- expect(cell.lastCodeRun).toBe(null);
310
- expect(cell.edited).toBe(true);
311
295
  expect(cell).toMatchSnapshot("code edited");
312
296
 
313
297
  // Prepare for run
@@ -315,8 +299,6 @@ describe("cell reducer", () => {
315
299
  cellId: firstCellId,
316
300
  });
317
301
  cell = cells[0];
318
- expect(cell.lastCodeRun).toBe("import marimo as mo");
319
- expect(cell.edited).toBe(false);
320
302
  expect(cell).toMatchSnapshot("prepared for run");
321
303
 
322
304
  // Receive queued messages
@@ -329,8 +311,6 @@ describe("cell reducer", () => {
329
311
  timestamp: new Date(10).getTime() as Seconds,
330
312
  });
331
313
  cell = cells[0];
332
- expect(cell.lastCodeRun).toBe("import marimo as mo");
333
- expect(cell.edited).toBe(false);
334
314
  expect(cell).toMatchSnapshot("queued message received");
335
315
 
336
316
  // Receive running messages
@@ -343,8 +323,6 @@ describe("cell reducer", () => {
343
323
  timestamp: new Date(20).getTime() as Seconds,
344
324
  });
345
325
  cell = cells[0];
346
- expect(cell.lastCodeRun).toBe("import marimo as mo");
347
- expect(cell.edited).toBe(false);
348
326
  expect(cell).toMatchSnapshot("running message received");
349
327
 
350
328
  // Console messages shouldn't transition status
@@ -362,7 +340,6 @@ describe("cell reducer", () => {
362
340
  timestamp: new Date(22).getTime() as Seconds,
363
341
  });
364
342
  cell = cells[0];
365
- expect(cell.edited).toBe(false);
366
343
  expect(cell).toMatchSnapshot("console message received");
367
344
 
368
345
  // Receive output messages
@@ -385,7 +362,6 @@ describe("cell reducer", () => {
385
362
  timestamp: new Date(33).getTime() as Seconds,
386
363
  });
387
364
  cell = cells[0];
388
- expect(cell.edited).toBe(false);
389
365
  expect(cell).toMatchSnapshot("output received");
390
366
 
391
367
  // EDITING BACK AND FORTH
@@ -393,20 +369,16 @@ describe("cell reducer", () => {
393
369
  // Update code again
394
370
  editCode(firstCellId, "import marimo as mo\nimport numpy");
395
371
  cell = cells[0];
396
- expect(cell.edited).toBe(true);
397
372
  expect(cell).toMatchSnapshot("code edited again");
398
373
 
399
374
  // Update code should be unedited
400
375
  editCode(firstCellId, "import marimo as mo");
401
376
  cell = cells[0];
402
- expect(cell.edited).toBe(false);
403
377
  expect(cell).toMatchSnapshot("code reverted");
404
378
 
405
379
  // Update code should be edited again
406
380
  editCode(firstCellId, "import marimo as mo\nimport numpy");
407
381
  cell = cells[0];
408
- expect(cell.lastCodeRun).toBe("import marimo as mo");
409
- expect(cell.edited).toBe(true);
410
382
  expect(cell).toMatchSnapshot("code changed after revert");
411
383
 
412
384
  // ERROR RESPONSE
@@ -457,8 +429,6 @@ describe("cell reducer", () => {
457
429
  });
458
430
  cell = cells[0];
459
431
  expect(cell.output).not.toBe(null); // keep old output
460
- expect(cell.lastCodeRun).toBe("import marimo as mo\nimport numpy");
461
- expect(cell.edited).toBe(false);
462
432
  expect(cell).toMatchSnapshot("error response received");
463
433
 
464
434
  // INTERRUPT RESPONSE
@@ -506,8 +476,6 @@ describe("cell reducer", () => {
506
476
  timestamp: new Date(61).getTime() as Seconds,
507
477
  });
508
478
  cell = cells[0];
509
- expect(cell.lastCodeRun).toBe(cell.code);
510
- expect(cell.edited).toBe(false);
511
479
  expect(cell).toMatchSnapshot("interruption response received");
512
480
  });
513
481
 
@@ -538,8 +506,6 @@ describe("cell reducer", () => {
538
506
  timestamp: new Date(10).getTime() as Seconds,
539
507
  });
540
508
  let cell = cells[1];
541
- expect(cell.lastCodeRun).toBe("mo.slider()");
542
- expect(cell.edited).toBe(false);
543
509
  expect(cell).toMatchSnapshot("queued message received");
544
510
 
545
511
  // Receive stale message
@@ -552,53 +518,9 @@ describe("cell reducer", () => {
552
518
  timestamp: new Date(20).getTime() as Seconds,
553
519
  });
554
520
  cell = cells[1];
555
- expect(cell.lastCodeRun).toBe("mo.slider()");
556
- expect(cell.edited).toBe(false);
557
521
  expect(cell).toMatchSnapshot("stale inputs received");
558
522
  });
559
523
 
560
- it("can format code and update cell", () => {
561
- const firstCellId = cells[0].id;
562
- editCode(firstCellId, "import marimo as mo");
563
- let cell = cells[0];
564
- expect(cell.lastCodeRun).toBe(null);
565
- expect(cell.edited).toBe(true);
566
-
567
- // Run
568
- actions.prepareForRun({
569
- cellId: firstCellId,
570
- });
571
- actions.handleCellMessage({
572
- cell_id: firstCellId,
573
- output: {
574
- channel: "output",
575
- mimetype: "text/plain",
576
- data: "ok",
577
- timestamp: 0,
578
- },
579
- console: {
580
- channel: "stdout",
581
- mimetype: "text/plain",
582
- data: "hello!",
583
- timestamp: 0,
584
- },
585
- status: "idle",
586
- stale_inputs: null,
587
- timestamp: new Date(33).getTime() as Seconds,
588
- });
589
-
590
- // Check steady state
591
- cell = cells[0];
592
- expect(cell.edited).toBe(false);
593
- expect(cell.lastCodeRun).toBe("import marimo as mo");
594
-
595
- // Format code
596
- formatCode(firstCellId, "import marimo as mo");
597
- cell = cells[0];
598
- expect(cell.lastCodeRun).toBe("import marimo as mo");
599
- expect(cell.edited).toBe(false);
600
- });
601
-
602
524
  it("can run a stopped cell", () => {
603
525
  // Update code of first cell
604
526
  editCode(firstCellId, "mo.md('This has an ancestor that was stopped')");
@@ -618,10 +540,6 @@ describe("cell reducer", () => {
618
540
  timestamp: new Date(10).getTime() as Seconds,
619
541
  });
620
542
  let cell = cells[0];
621
- expect(cell.lastCodeRun).toBe(
622
- "mo.md('This has an ancestor that was stopped')",
623
- );
624
- expect(cell.edited).toBe(false);
625
543
  expect(cell).toMatchSnapshot("queued message received");
626
544
 
627
545
  // Receive idle message
@@ -1494,24 +1412,6 @@ describe("kernel-ready", () => {
1494
1412
  },
1495
1413
  });
1496
1414
 
1497
- it("setLastRun records what the kernel last ran", () => {
1498
- const state = hydratedState();
1499
- const next = exportedForTesting.reducer(state, {
1500
- type: "setLastRun",
1501
- payload: { lastCodeRun: { [CELL_A]: "0" } },
1502
- });
1503
-
1504
- expect(next.cellData[CELL_A]).toMatchObject({
1505
- lastCodeRun: "0",
1506
- edited: true,
1507
- });
1508
- expect(next.cellData[CELL_B]).toMatchObject({
1509
- lastCodeRun: null,
1510
- edited: false,
1511
- });
1512
- expect(next.cellRuntime).toBe(state.cellRuntime);
1513
- });
1514
-
1515
1415
  it("resetCellRuntimes starts every cell's runtime over", () => {
1516
1416
  const next = exportedForTesting.reducer(hydratedState(), {
1517
1417
  type: "resetCellRuntimes",
@@ -53,13 +53,8 @@ describe("applyDocument", () => {
53
53
  const state = notebookOf([cell(a), cell(b, { code: "" })]);
54
54
 
55
55
  expect(state.cellIds).toEqual([a, b]);
56
- expect(state.cellData[a]).toMatchObject({
57
- code: "a = 1",
58
- name: "_",
59
- edited: true,
60
- lastCodeRun: null,
61
- });
62
- expect(state.cellData[b].edited).toBe(false);
56
+ expect(state.cellData[a]).toMatchObject({ code: "a = 1", name: "_" });
57
+ expect(state.cellData[b]).toMatchObject({ code: "", name: "_" });
63
58
  expect(state.cellRuntime[a]).toBeDefined();
64
59
  expect(state.cellHandles[a]).toBeDefined();
65
60
  expect(state.cellData[SCRATCH_CELL_ID]).toBeDefined();
@@ -96,7 +91,6 @@ describe("applyDocument", () => {
96
91
  expect(next.cellData[a]).toMatchObject({
97
92
  name: "renamed",
98
93
  config: { hide_code: true },
99
- edited: true,
100
94
  });
101
95
  });
102
96
 
@@ -5,8 +5,10 @@ import { MockExecutions } from "@/__mocks__/executions";
5
5
  import { initialNotebookState, type NotebookState } from "@/core/cells/cells";
6
6
  import { CellId } from "@/core/cells/ids";
7
7
  import type { CellData } from "@/core/cells/types";
8
+ import type { CellFreshnessLookup } from "@/core/executions";
8
9
  import type { OutputMessage } from "@/core/kernel/messages";
9
10
  import {
11
+ cellNeedsRun,
10
12
  disabledCellIds,
11
13
  enabledCellIds,
12
14
  isUninstantiated,
@@ -14,80 +16,147 @@ import {
14
16
  staleCellIds,
15
17
  } from "../utils";
16
18
 
17
- function notebook(
18
- cells: Record<
19
- string,
20
- { edited?: boolean; disabled?: boolean; staleInputs?: boolean }
21
- >,
22
- ): NotebookState {
19
+ type Cells = Record<
20
+ string,
21
+ { edited?: boolean; disabled?: boolean; staleInputs?: boolean }
22
+ >;
23
+
24
+ function notebook(cells: Cells): NotebookState {
23
25
  return {
24
26
  cellIds: Object.keys(cells),
25
27
  cellData: Object.fromEntries(
26
28
  Object.entries(cells).map(([id, cell]) => [
27
29
  id,
28
- { edited: cell.edited ?? false, config: { disabled: cell.disabled } },
29
- ]),
30
- ),
31
- cellRuntime: Object.fromEntries(
32
- Object.entries(cells).map(([id, cell]) => [
33
- id,
34
- { staleInputs: cell.staleInputs ?? false },
30
+ { config: { disabled: cell.disabled } },
35
31
  ]),
36
32
  ),
33
+ cellRuntime: Object.fromEntries(Object.keys(cells).map((id) => [id, {}])),
37
34
  } as any;
38
35
  }
39
36
 
37
+ /** Freshness as the host would report it for `cells`. */
38
+ function freshness(cells: Cells): CellFreshnessLookup {
39
+ return (id) => {
40
+ const cell = cells[id];
41
+ return {
42
+ sourceChanged: cell?.edited ?? false,
43
+ inputsChanged: cell?.staleInputs ?? false,
44
+ };
45
+ };
46
+ }
47
+
40
48
  describe("staleCellIds", () => {
41
49
  it("should return cell IDs that have never run", () => {
42
- const state = notebook({ cell1: {}, cell2: {} });
50
+ const cells: Cells = { cell1: {}, cell2: {} };
51
+ const state = notebook(cells);
43
52
  const executions = MockExecutions.inputs([
44
53
  MockExecutions.completed("cell2"),
45
54
  ]);
46
55
 
47
- expect(staleCellIds(state, executions)).toEqual(["cell1"]);
56
+ expect(staleCellIds(state, executions, freshness(cells))).toEqual([
57
+ "cell1",
58
+ ]);
48
59
  });
49
60
 
50
61
  it("should return cell IDs that are edited", () => {
51
- const state = notebook({ cell1: { edited: true }, cell2: {} });
62
+ const cells: Cells = { cell1: { edited: true }, cell2: {} };
63
+ const state = notebook(cells);
52
64
  const executions = MockExecutions.inputs([
53
65
  MockExecutions.completed("cell1"),
54
66
  MockExecutions.completed("cell2"),
55
67
  ]);
56
68
 
57
- expect(staleCellIds(state, executions)).toEqual(["cell1"]);
69
+ expect(staleCellIds(state, executions, freshness(cells))).toEqual([
70
+ "cell1",
71
+ ]);
58
72
  });
59
73
 
60
74
  it("should return cell IDs that are interrupted", () => {
61
- const state = notebook({ cell1: {}, cell2: {} });
75
+ const cells: Cells = { cell1: {}, cell2: {} };
76
+ const state = notebook(cells);
62
77
  const executions = MockExecutions.inputs([
63
78
  MockExecutions.interrupted("cell1"),
64
79
  MockExecutions.completed("cell2"),
65
80
  ]);
66
81
 
67
- expect(staleCellIds(state, executions)).toEqual(["cell1"]);
82
+ expect(staleCellIds(state, executions, freshness(cells))).toEqual([
83
+ "cell1",
84
+ ]);
68
85
  });
69
86
 
70
87
  it("should return cell IDs with stale inputs", () => {
71
- const state = notebook({ cell1: { staleInputs: true }, cell2: {} });
88
+ const cells: Cells = { cell1: { staleInputs: true }, cell2: {} };
89
+ const state = notebook(cells);
72
90
  const executions = MockExecutions.inputs([
73
91
  MockExecutions.completed("cell1"),
74
92
  MockExecutions.completed("cell2"),
75
93
  ]);
76
94
 
77
- expect(staleCellIds(state, executions)).toEqual(["cell1"]);
95
+ expect(staleCellIds(state, executions, freshness(cells))).toEqual([
96
+ "cell1",
97
+ ]);
78
98
  });
79
99
 
80
100
  it("should not return cell IDs that are disabled", () => {
81
- const state = notebook({
101
+ const cells: Cells = {
82
102
  cell1: { disabled: true, staleInputs: true },
83
103
  cell2: { staleInputs: true },
84
- });
104
+ };
105
+ const state = notebook(cells);
85
106
  const executions = MockExecutions.inputs(
86
107
  [MockExecutions.completed("cell1"), MockExecutions.completed("cell2")],
87
108
  { blocked: new Set(["cell2"]) },
88
109
  );
89
110
 
90
- expect(staleCellIds(state, executions)).toEqual([]);
111
+ expect(staleCellIds(state, executions, freshness(cells))).toEqual([]);
112
+ });
113
+ });
114
+
115
+ describe("staleCellIds with a run in progress", () => {
116
+ it("leaves a running cell alone unless its source changed since", () => {
117
+ const cells: Cells = { cell1: {}, cell2: { edited: true } };
118
+ const state = notebook(cells);
119
+ const executions = MockExecutions.inputs([
120
+ MockExecutions.running("cell1"),
121
+ MockExecutions.running("cell2"),
122
+ ]);
123
+
124
+ expect(staleCellIds(state, executions, freshness(cells))).toEqual([
125
+ "cell2",
126
+ ]);
127
+ });
128
+
129
+ it("leaves a submitted cell alone until the host has planned its run", () => {
130
+ const cells: Cells = { cell1: { edited: true } };
131
+ const state = notebook(cells);
132
+ const ran = MockExecutions.completed("cell1");
133
+ const executions = MockExecutions.inputs([ran], {
134
+ submissions: new Map([["cell1", ran.id]]),
135
+ });
136
+
137
+ expect(staleCellIds(state, executions, freshness(cells))).toEqual([]);
138
+ });
139
+ });
140
+
141
+ describe("cellNeedsRun", () => {
142
+ const blocked = { interrupted: false, disabled: false, blocked: true };
143
+
144
+ it("flags a blocked cell whose source changed, whatever its inputs did", () => {
145
+ expect(
146
+ cellNeedsRun({
147
+ ...blocked,
148
+ freshness: freshness({ c: { edited: true, staleInputs: true } })("c"),
149
+ }),
150
+ ).toBe(true);
151
+ });
152
+
153
+ it("does not flag a blocked cell for changed inputs alone", () => {
154
+ expect(
155
+ cellNeedsRun({
156
+ ...blocked,
157
+ freshness: freshness({ c: { staleInputs: true } })("c"),
158
+ }),
159
+ ).toBe(false);
91
160
  });
92
161
  });
93
162
 
@@ -603,7 +603,6 @@ export function useCellActions() {
603
603
  handleCellMessage: _handleCellMessage,
604
604
  addLogs: _addLogs,
605
605
  reconcileDocument: _reconcileDocument,
606
- setLastRun: _setLastRun,
607
606
  resetCellRuntimes: _resetCellRuntimes,
608
607
  ...operations
609
608
  } = useReducerActions();
@@ -1,5 +1,9 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import type { CellExecutionView, CellRunStatus } from "@/core/executions";
2
+ import type {
3
+ CellExecutionView,
4
+ CellFreshness,
5
+ CellRunStatus,
6
+ } from "@/core/executions";
3
7
  import { extractAllTracebackInfo, type TracebackInfo } from "@/utils/traceback";
4
8
  import { parseOutline } from "../dom/outline";
5
9
  import type { CellMessage, OutputMessage } from "../kernel/messages";
@@ -41,7 +45,6 @@ export function transitionCell(
41
45
  }
42
46
 
43
47
  nextCell.output = message.output ?? nextCell.output;
44
- nextCell.staleInputs = message.stale_inputs ?? nextCell.staleInputs;
45
48
  // Tri-state partial update: an omitted field (undefined) leaves the hint
46
49
  // unchanged; null explicitly clears it; a string sets it.
47
50
  if (message.serialization !== undefined) {
@@ -115,31 +118,51 @@ export function outputIsLoading(status: CellRunStatus): boolean {
115
118
  }
116
119
 
117
120
  /**
118
- * A cell's output is stale if it has been edited, is loading, or has errored.
121
+ * A cell's output is stale if it is loading, an input it read has changed,
122
+ * or, while `editing`, its source has changed. A read view keeps showing
123
+ * the last result of edited source.
119
124
  */
120
125
  export function outputIsStale(
121
126
  execution: Pick<CellExecutionView, "status" | "outcome" | "rendered">,
122
- staleInputs: boolean,
123
- edited: boolean,
127
+ freshness: CellFreshness,
128
+ editing: boolean,
124
129
  ): boolean {
125
130
  const { status, outcome, rendered } = execution;
126
131
 
127
- // If interrupted, the output is not stale
132
+ // A run in progress owns the output area: loading until the running cell
133
+ // has shown output (e.g. mo.output.append()), whatever the last result was.
134
+ if (outputIsLoading(status)) {
135
+ return !(status === "running" && rendered);
136
+ }
137
+ if (editing && freshness.sourceChanged) {
138
+ return true;
139
+ }
140
+ // An interrupted run's partial output stands as the record of that run.
128
141
  if (outcome === "interrupted") {
129
142
  return false;
130
143
  }
144
+ return freshness.inputsChanged;
145
+ }
131
146
 
132
- // If edited, the cell's output is stale
133
- if (edited) {
134
- return true;
147
+ /**
148
+ * Console output is stale while a run is queued, since the run will clear
149
+ * it, and once a settled result is outdated. A run in progress owns it.
150
+ */
151
+ export function consoleOutputIsStale(
152
+ execution: Pick<CellExecutionView, "status" | "outcome">,
153
+ freshness: CellFreshness,
154
+ ): boolean {
155
+ const { status, outcome } = execution;
156
+ if (outcome === "interrupted") {
157
+ return false;
135
158
  }
136
-
137
- // Loading, unless the running cell has shown output (e.g. mo.output.append())
138
- if (outputIsLoading(status) && !(status === "running" && rendered)) {
159
+ if (status === "queued") {
139
160
  return true;
140
161
  }
141
-
142
- return staleInputs;
162
+ if (status === "running") {
163
+ return false;
164
+ }
165
+ return freshness.sourceChanged || freshness.inputsChanged;
143
166
  }
144
167
 
145
168
  /**
@@ -9,6 +9,7 @@ import type { EditableDocument, Placement } from "@/core/notebook/document";
9
9
  import {
10
10
  cellExecutionInputsAtom,
11
11
  cellExecutionView,
12
+ cellFreshnessAtom,
12
13
  withdrawCells,
13
14
  } from "@/core/executions";
14
15
  import type { CellColumnId } from "./layout";
@@ -38,7 +39,6 @@ import {
38
39
  type CellRuntimeState,
39
40
  createCell,
40
41
  createCellRuntimeState,
41
- isEdited,
42
42
  } from "./types";
43
43
  import {
44
44
  canUndoDeletes,
@@ -161,76 +161,21 @@ const {
161
161
  useActions,
162
162
  valueAtom: notebookAtom,
163
163
  } = createReducerAndAtoms(initialNotebookState, {
164
- /**
165
- * Keep a cell as run, or as edited, as it was before formatting rewrote
166
- * its code from `before` to `after`. The code itself arrives through the
167
- * document.
168
- */
169
- codeFormatted: (
170
- state,
171
- action: { cellId: CellId; before: string; after: string },
172
- ) => {
173
- const { cellId, before, after } = action;
174
- if (!state.cellData[cellId]) {
175
- return state;
176
- }
177
- return updateCellData({
178
- state,
179
- cellId,
180
- cellReducer: (cell) => {
181
- const lastCodeRun = isEdited(cell.lastCodeRun, before)
182
- ? cell.lastCodeRun
183
- : after;
184
- return {
185
- ...cell,
186
- lastCodeRun,
187
- edited: isEdited(lastCodeRun, cell.code),
188
- };
189
- },
190
- });
191
- },
192
- prepareForRun: (state, action: { cellId: CellId }) => {
193
- const newState = updateCellRuntimeState({
164
+ prepareForRun: (state, action: { cellId: CellId }) =>
165
+ updateCellRuntimeState({
194
166
  state,
195
167
  cellId: action.cellId,
196
168
  cellReducer: (cell) => ({ ...cell, debuggerActive: false }),
197
- });
198
- return updateCellData({
199
- state: newState,
200
- cellId: action.cellId,
201
- cellReducer: (cell) => {
202
- return {
203
- ...cell,
204
- edited: false,
205
- lastCodeRun: cell.code.trim(),
206
- };
207
- },
208
- });
209
- },
169
+ }),
210
170
  handleCellMessage: (state, message: CellMessage) => {
211
171
  const cellId = message.cell_id;
212
- let nextState = updateCellRuntimeState({
172
+ const nextState = updateCellRuntimeState({
213
173
  state,
214
174
  cellId,
215
175
  cellReducer: (cell) => {
216
176
  return transitionCell(cell, message);
217
177
  },
218
178
  });
219
- // When a cell is queued for execution, snapshot the current code
220
- // as lastCodeRun. This clears staleness for cells executed by the
221
- // kernel (e.g. via code_mode). If the user edits during execution,
222
- // code !== lastCodeRun keeps the cell stale.
223
- if (message.status === "queued") {
224
- nextState = updateCellData({
225
- state: nextState,
226
- cellId,
227
- cellReducer: (cell) => ({
228
- ...cell,
229
- lastCodeRun: cell.code.trim(),
230
- edited: false,
231
- }),
232
- });
233
- }
234
179
  return {
235
180
  ...nextState,
236
181
  cellLogs: [...nextState.cellLogs, ...getCellLogsForMessage(message)],
@@ -286,21 +231,6 @@ const {
286
231
  },
287
232
  });
288
233
  },
289
- /** Record what the kernel last ran for each cell. */
290
- setLastRun: (state, action: { lastCodeRun: Record<CellId, string> }) => {
291
- const cellData = { ...state.cellData };
292
- for (const cellId of state.cellIds) {
293
- const cell = state.cellData[cellId];
294
- const lastCodeRun = action.lastCodeRun[cellId] ?? null;
295
- cellData[cellId] = {
296
- ...cell,
297
- lastCodeRun,
298
- // A cell the kernel never ran is not stale on load.
299
- edited: lastCodeRun !== null && isEdited(lastCodeRun, cell.code),
300
- };
301
- }
302
- return { ...state, cellData };
303
- },
304
234
  /**
305
235
  * Start each cell's runtime over for a new kernel. WASM keeps the outputs
306
236
  * its mount-time snapshot gave it.
@@ -429,29 +359,6 @@ function updateCellRuntimeState({
429
359
  };
430
360
  }
431
361
 
432
- function updateCellData({
433
- state,
434
- cellId,
435
- cellReducer,
436
- }: {
437
- state: NotebookState;
438
- cellId: CellId;
439
- cellReducer: ReducerWithoutAction<CellData>;
440
- }) {
441
- if (!(cellId in state.cellData)) {
442
- Logger.warn(`Cell ${cellId} not found in state`);
443
- return state;
444
- }
445
-
446
- return {
447
- ...state,
448
- cellData: {
449
- ...state.cellData,
450
- [cellId]: cellReducer(state.cellData[cellId]),
451
- },
452
- };
453
- }
454
-
455
362
  /** Each cell's config in order. The document's `column` configs are canonical. */
456
363
  export function getCellConfigs(state: NotebookState): CellConfig[] {
457
364
  return state.cellIds.map((cellId) => ({
@@ -492,7 +399,11 @@ export const canUndoDeletesAtom = atom((get) =>
492
399
  export const undoLabelAtom = atom((get) => getUndoLabel(get(notebookAtom)));
493
400
 
494
401
  export const needsRunAtom = atom((get) =>
495
- notebookNeedsRun(get(notebookAtom), get(cellExecutionInputsAtom)),
402
+ notebookNeedsRun(
403
+ get(notebookAtom),
404
+ get(cellExecutionInputsAtom),
405
+ get(cellFreshnessAtom),
406
+ ),
496
407
  );
497
408
 
498
409
  export const cellErrorsAtom = atom((get) => {