@marimo-team/islands 0.25.3-dev267 → 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 (138) hide show
  1. package/dist/{ConnectedDataExplorerComponent-C4yCN38h.js → ConnectedDataExplorerComponent-eLHPh6uu.js} +8 -8
  2. package/dist/{ErrorBoundary-CewXPyL5.js → ErrorBoundary-CQYAoD7X.js} +4 -4
  3. package/dist/{abnfDiagram-VCTEODGH-o6Ay6Q63.js → abnfDiagram-VCTEODGH-hqd0IBtX.js} +2 -2
  4. package/dist/{architectureDiagram-5GKGNRK7-BhiwwlVg.js → architectureDiagram-5GKGNRK7-BzLAFcc0.js} +1 -1
  5. package/dist/{strings-BCKpLFNU.js → assertNever-DIU3JJbt.js} +14 -14
  6. package/dist/{chat-ui-CbgR5ivy.js → chat-ui-BAnLgpTe.js} +17 -17
  7. package/dist/{chunk-IMKFNOWR-WMSMf4Nj.js → chunk-IMKFNOWR-DOeVvuyE.js} +1 -1
  8. package/dist/{chunk-SHT3W25Y-BRfADrzD.js → chunk-SHT3W25Y-DZoFI8wR.js} +1 -1
  9. package/dist/{chunk-SVP7TREG-CuK5QFre.js → chunk-SVP7TREG-CWu86PSW.js} +1 -1
  10. package/dist/{chunk-TICWLB2K-Bcun_jhJ.js → chunk-TICWLB2K-BrM0GoP3.js} +1 -1
  11. package/dist/{classDiagram-ZZMXUADV-CgWDbUDT.js → classDiagram-ZZMXUADV-DYi4V2bB.js} +2 -2
  12. package/dist/{classDiagram-v2-VYDZK3BY-DYmc9SFU.js → classDiagram-v2-VYDZK3BY-tN7FIk-m.js} +2 -2
  13. package/dist/{code-visibility-BKBl9Rh6.js → code-visibility-DcZ_RwaH.js} +44 -47
  14. package/dist/{common-N3Jd_6m9.js → common-BGeQilvh.js} +4866 -4857
  15. package/dist/{config-CmeG_Pb6.js → config-BsPruWMR.js} +2 -2
  16. package/dist/{createReducer-C_syF8Td.js → createReducer-DdtT_7ji.js} +1 -1
  17. package/dist/{cynefinDiagram-5FMLGOSQ-PPKWipjT.js → cynefinDiagram-5FMLGOSQ-D5nCnDc-.js} +1 -1
  18. package/dist/{dagre-GXQ25YYZ-niRBZh--.js → dagre-GXQ25YYZ-BHkd7f_i.js} +1 -1
  19. package/dist/{data-grid-overlay-editor-Cp59CrqZ.js → data-grid-overlay-editor-Dc4lW4A3.js} +1 -1
  20. package/dist/{diagram-S7CK7UJ4-DPiP9rot.js → diagram-S7CK7UJ4-D1ALppT2.js} +1 -1
  21. package/dist/{diagram-UQ7AKVKN-B0WZuaxV.js → diagram-UQ7AKVKN-DLv0b83L.js} +1 -1
  22. package/dist/{diagram-VX7I27RA-Rn42WBuN.js → diagram-VX7I27RA-7XJ7J6qg.js} +1 -1
  23. package/dist/{diagram-Z3DM3KII-BB93--ry.js → diagram-Z3DM3KII-7M7zbHp_.js} +1 -1
  24. package/dist/{dialog-BtprQCUw.js → dialog-DzCj1qQv.js} +1 -1
  25. package/dist/{dropdown-menu-BpImEfn3.js → dropdown-menu-CdXE15an.js} +3 -3
  26. package/dist/{ebnfDiagram-PWID7BFC-V4t5h6mZ.js → ebnfDiagram-PWID7BFC-DHH1qlmV.js} +2 -2
  27. package/dist/{erDiagram-RLTQ6QDP-C1MsAKXd.js → erDiagram-RLTQ6QDP-fI_0dGef.js} +1 -1
  28. package/dist/{error-entries-CJGdD2ky.js → error-entries-CxN-Nq3V.js} +519 -458
  29. package/dist/{feedback-button-Qc1f9iPq.js → feedback-button-BqaeQcvX.js} +22 -22
  30. package/dist/{flowDiagram-HODETNUW-DHjeJP96.js → flowDiagram-HODETNUW-Brg9MA6i.js} +2 -2
  31. package/dist/{formats-gGospKWJ.js → formats-DbSvAPY-.js} +1 -1
  32. package/dist/{glide-data-editor-hCq3K0sd.js → glide-data-editor-CiijPXA7.js} +11 -11
  33. package/dist/{glide-utils-DC3sk6EX.js → glide-utils-BSw3SSHF.js} +1 -1
  34. package/dist/{globals-B2_uMYDY.js → globals-CpmSWKI_.js} +2 -2
  35. package/dist/{html-to-image-S7spRQCi.js → html-to-image-8sywF4my.js} +5120 -5117
  36. package/dist/{infoDiagram-27XIBGKW-Dovv6iKx.js → infoDiagram-27XIBGKW-E9e403YT.js} +1 -1
  37. package/dist/{ishikawaDiagram-5VMMS53U-D2nyygsc.js → ishikawaDiagram-5VMMS53U-C-RlYDRo.js} +1 -1
  38. package/dist/{kanban-definition-UXKFOSKX-KyVx0uTY.js → kanban-definition-UXKFOSKX-CLFl_uqK.js} +1 -1
  39. package/dist/main.js +5972 -6003
  40. package/dist/{mermaid-B-jKSwGt.js → mermaid-rd09e1JL.js} +34 -34
  41. package/dist/{mindmap-definition-YA3MSWOX-Bz9tKQFl.js → mindmap-definition-YA3MSWOX-BQ8TWtyB.js} +1 -1
  42. package/dist/{number-overlay-editor-CfNYvbdz.js → number-overlay-editor-Ft4NiAEN.js} +1 -1
  43. package/dist/{pegDiagram-XKGWAZYB-C_qpq6Tv.js → pegDiagram-XKGWAZYB-CN8UHFPi.js} +2 -2
  44. package/dist/{pieDiagram-E7YTZNPT-Bp9ucgI4.js → pieDiagram-E7YTZNPT-DGuJ0EBd.js} +1 -1
  45. package/dist/{process-output-f34bkXTm.js → process-output-Dku5POvm.js} +3 -3
  46. package/dist/{railroadDiagram-O6MQD6OU-tq-VEH6j.js → railroadDiagram-O6MQD6OU-BPDA8wy8.js} +2 -2
  47. package/dist/{requirementDiagram-BXWQKSXE-DW-vHadb.js → requirementDiagram-BXWQKSXE-ChzLGbGW.js} +1 -1
  48. package/dist/{reveal-component-CUxv-kkP.js → reveal-component-D0cfwbbl.js} +1094 -1094
  49. package/dist/{select-C33FVPPN.js → select-OxgozOC8.js} +1 -1
  50. package/dist/{sequenceDiagram-WJ2MYXX4-Dv9U9qRP.js → sequenceDiagram-WJ2MYXX4-DFX-bE6O.js} +1 -1
  51. package/dist/{spec-DH1lmF7Q.js → spec-oJn0RhpQ.js} +2 -2
  52. package/dist/{stateDiagram-D77RDMKH-DTmPQ0EB.js → stateDiagram-D77RDMKH-BR7cI3wK.js} +2 -2
  53. package/dist/{stateDiagram-v2-MP3YSRHH-CkCif0Iw.js → stateDiagram-v2-MP3YSRHH-oGqB5UDI.js} +2 -2
  54. package/dist/{static-state-BldGd99T.js → static-state-CuV6Pc3a.js} +1 -1
  55. package/dist/{swimlanes-42K2YHIH-21kCq6l8.js → swimlanes-42K2YHIH-DwSZl1yC.js} +1 -1
  56. package/dist/{swimlanesDiagram-VR7AAH4N-BpRJP6KU.js → swimlanesDiagram-VR7AAH4N-BHlcd-Ri.js} +2 -2
  57. package/dist/{timeline-definition-24CTP7MA-Ckgh4lS4.js → timeline-definition-24CTP7MA-DMmP-G2O.js} +1 -1
  58. package/dist/{toast-DpczUryP.js → toast-DbBHmxmB.js} +1 -1
  59. package/dist/{useAsyncData-DsZMvNXn.js → useAsyncData-BYIAoMqn.js} +1 -1
  60. package/dist/{useDateFormatter-Dk_xFci4.js → useDateFormatter-ButWkdcK.js} +1 -1
  61. package/dist/{useDeepCompareMemoize-KMBBux8C.js → useDeepCompareMemoize-Bz0c5GN6.js} +1 -1
  62. package/dist/{useTheme-BZ2GGLgm.js → useTheme-DPLobSFs.js} +7 -7
  63. package/dist/{utils-DXkj82sK.js → utils-EBklsC5R.js} +31 -31
  64. package/dist/{vega-component-CF9kI6C4.js → vega-component-rxIxAAh9.js} +6 -6
  65. package/dist/{vennDiagram-4TSXK5OY-CDei2Bt3.js → vennDiagram-4TSXK5OY-Db8GtH1G.js} +1 -1
  66. package/dist/{wardleyDiagram-VM6X3IG4-CXavT601.js → wardleyDiagram-VM6X3IG4-DmIiC3FU.js} +1 -1
  67. package/dist/{xychartDiagram-S5SC5T6Z-CvJhcZri.js → xychartDiagram-S5SC5T6Z-DS9OPrNq.js} +1 -1
  68. package/package.json +1 -1
  69. package/src/__mocks__/executions.ts +195 -0
  70. package/src/__mocks__/notebook.ts +0 -1
  71. package/src/__tests__/CellStatus.test.tsx +19 -20
  72. package/src/components/dependency-graph/minimap-content.tsx +3 -2
  73. package/src/components/editor/__tests__/data-attributes.test.tsx +0 -7
  74. package/src/components/editor/__tests__/output-persistence.test.tsx +2 -2
  75. package/src/components/editor/actions/__tests__/useCellActionButton.test.tsx +1 -0
  76. package/src/components/editor/actions/useCellActionButton.tsx +7 -3
  77. package/src/components/editor/alerts/stdin-blocking-alert.tsx +3 -1
  78. package/src/components/editor/cell/CellStatus.tsx +23 -19
  79. package/src/components/editor/cell/RunButton.tsx +16 -6
  80. package/src/components/editor/cell/__tests__/useRunCells.test.ts +45 -0
  81. package/src/components/editor/cell/cell-context-menu.tsx +4 -1
  82. package/src/components/editor/cell/code/cell-editor.tsx +7 -5
  83. package/src/components/editor/cell/collapse.tsx +4 -0
  84. package/src/components/editor/cell/useRunCells.ts +18 -3
  85. package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -4
  86. package/src/components/editor/header/status.tsx +4 -2
  87. package/src/components/editor/navigation/navigation.ts +3 -2
  88. package/src/components/editor/notebook-cell.tsx +82 -83
  89. package/src/components/editor/renderers/grid-layout/__tests__/grid-layout.test.tsx +75 -0
  90. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +22 -16
  91. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
  92. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -33
  93. package/src/components/editor/stdin-blocking-alert.tsx +3 -1
  94. package/src/components/scratchpad/scratchpad.tsx +4 -1
  95. package/src/components/slides/minimap.tsx +4 -2
  96. package/src/components/slides/reveal-component.tsx +14 -10
  97. package/src/components/slides/slide-cell-view.tsx +28 -24
  98. package/src/components/slides/slide.tsx +3 -4
  99. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -389
  100. package/src/core/cells/__tests__/cell.test.ts +49 -154
  101. package/src/core/cells/__tests__/cells.test.ts +18 -127
  102. package/src/core/cells/__tests__/pending-delete-service.test.tsx +11 -20
  103. package/src/core/cells/__tests__/utils.test.ts +75 -247
  104. package/src/core/cells/actions.ts +5 -2
  105. package/src/core/cells/cell.ts +31 -133
  106. package/src/core/cells/cells.ts +14 -29
  107. package/src/core/cells/document.ts +3 -3
  108. package/src/core/cells/focus.ts +27 -6
  109. package/src/core/cells/layout.ts +1 -1
  110. package/src/core/cells/pending-delete-service.ts +5 -7
  111. package/src/core/cells/types.ts +13 -36
  112. package/src/core/cells/utils.ts +37 -68
  113. package/src/core/edit-app.tsx +2 -1
  114. package/src/core/executions/__tests__/atoms.test.ts +99 -0
  115. package/src/core/executions/__tests__/select.test.ts +268 -0
  116. package/src/core/executions/atoms.ts +206 -0
  117. package/src/core/executions/hooks.ts +31 -0
  118. package/src/core/executions/index.ts +31 -0
  119. package/src/core/executions/select.ts +175 -0
  120. package/src/core/export/__tests__/hooks.test.ts +0 -5
  121. package/src/core/islands/bootstrap.ts +4 -1
  122. package/src/core/islands/components/output-wrapper.tsx +5 -5
  123. package/src/core/kernel/handlers.ts +2 -11
  124. package/src/core/kernel/state.ts +0 -15
  125. package/src/core/lifecycle/render-policy.ts +12 -26
  126. package/src/core/network/__tests__/requests-lazy.test.ts +5 -5
  127. package/src/core/network/requests-lazy.ts +3 -3
  128. package/src/core/notebook/operations.ts +1 -1
  129. package/src/core/run-app.tsx +2 -1
  130. package/src/core/runtime/__tests__/adapter.test.ts +3 -3
  131. package/src/core/runtime/adapter.ts +4 -4
  132. package/src/core/session/useNotebookSession.tsx +3 -3
  133. package/src/core/wasm/bridge.ts +1 -1
  134. package/src/plugins/core/registerReactComponent.tsx +4 -13
  135. package/src/stories/cell-status.stories.tsx +17 -10
  136. package/src/stories/cell.stories.tsx +0 -31
  137. package/src/stories/layout/vertical/one-column.stories.tsx +0 -9
  138. package/src/utils/time.ts +10 -0
@@ -0,0 +1,268 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type * as Protocol from "@marimo-team/client/protocol";
4
+ import { describe, expect, it } from "vitest";
5
+ import type { Execution, Executions, Results } from "@/core/notebook/replica";
6
+ import {
7
+ anyRunning,
8
+ type CellExecutionInputs,
9
+ cellExecutionView,
10
+ onlyScratchRunning,
11
+ queuedOrRunningCount,
12
+ runningCellIds,
13
+ } from "../select";
14
+
15
+ const T0 = "2026-10-01T09:20:11Z";
16
+ const T1 = "2026-10-01T09:20:12.500Z";
17
+ const T2 = "2026-10-01T09:20:20Z";
18
+
19
+ function record(
20
+ overrides: Partial<Protocol.Execution> & { id: string },
21
+ ): Protocol.Execution {
22
+ return {
23
+ run_id: "run-1",
24
+ runtime_id: "r1",
25
+ caused_by: null,
26
+ cell_id: "c1",
27
+ code: null,
28
+ code_fingerprint: "0f71456db2ea83f4",
29
+ depends_on: {},
30
+ status: "completed",
31
+ outcome: { kind: "value", display: null },
32
+ started_at: T0,
33
+ completed_at: T1,
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ function execution(overrides: Partial<Protocol.Execution> & { id: string }) {
39
+ return { record: record(overrides), items: new Map(), variables: new Map() };
40
+ }
41
+
42
+ function inputs({
43
+ executions = [],
44
+ results = new Map(),
45
+ submissions = new Map(),
46
+ blocked = new Set(),
47
+ }: {
48
+ executions?: Execution[];
49
+ results?: Results;
50
+ submissions?: ReadonlyMap<string, string | null>;
51
+ blocked?: ReadonlySet<string>;
52
+ }): CellExecutionInputs {
53
+ const stream: Executions = {
54
+ runs: new Map(),
55
+ executions: new Map(executions.map((e) => [e.record.id, e])),
56
+ latest: new Map(
57
+ executions.flatMap((e) =>
58
+ e.record.cell_id === null ? [] : [[e.record.cell_id, e.record.id]],
59
+ ),
60
+ ),
61
+ };
62
+ return { executions: stream, results, submissions, blocked };
63
+ }
64
+
65
+ describe("cellExecutionView", () => {
66
+ it("is idle with no outcome before a cell runs", () => {
67
+ expect(cellExecutionView(inputs({}), "c1")).toEqual({
68
+ status: "idle",
69
+ blocked: false,
70
+ outcome: null,
71
+ startedAt: null,
72
+ duration: null,
73
+ rendered: false,
74
+ });
75
+ });
76
+
77
+ it("follows the execution on the stream while it is unfinished", () => {
78
+ const queued = inputs({
79
+ executions: [
80
+ execution({
81
+ id: "e1",
82
+ status: "queued",
83
+ outcome: null,
84
+ started_at: null,
85
+ completed_at: null,
86
+ }),
87
+ ],
88
+ });
89
+ expect(cellExecutionView(queued, "c1").status).toBe("queued");
90
+ const running = inputs({
91
+ executions: [
92
+ execution({
93
+ id: "e1",
94
+ status: "running",
95
+ outcome: null,
96
+ started_at: T2,
97
+ completed_at: null,
98
+ }),
99
+ ],
100
+ results: new Map([["c1", record({ id: "e0" })]]),
101
+ });
102
+ expect(cellExecutionView(running, "c1")).toEqual({
103
+ status: "running",
104
+ blocked: false,
105
+ outcome: null,
106
+ startedAt: Date.parse(T2),
107
+ duration: null,
108
+ rendered: false,
109
+ });
110
+ });
111
+
112
+ it("notes when a running execution has shown a value so far", () => {
113
+ const showing = inputs({
114
+ executions: [
115
+ execution({
116
+ id: "e1",
117
+ status: "running",
118
+ outcome: { kind: "value", display: null },
119
+ completed_at: null,
120
+ }),
121
+ ],
122
+ });
123
+ expect(cellExecutionView(showing, "c1").rendered).toBe(true);
124
+ });
125
+
126
+ it("reports how the last completed execution ended, with its timing", () => {
127
+ const ended = (outcome: Protocol.Outcome) =>
128
+ cellExecutionView(
129
+ inputs({
130
+ executions: [execution({ id: "e1", outcome })],
131
+ results: new Map([["c1", record({ id: "e1", outcome })]]),
132
+ }),
133
+ "c1",
134
+ );
135
+ const value = ended({ kind: "value", display: null });
136
+ expect(value).toEqual({
137
+ status: "idle",
138
+ blocked: false,
139
+ outcome: "value",
140
+ startedAt: Date.parse(T0),
141
+ duration: 1500,
142
+ rendered: false,
143
+ });
144
+ const error = (kind: string): Protocol.Outcome => ({
145
+ kind: "error",
146
+ error: {
147
+ kind,
148
+ message: "",
149
+ name: null,
150
+ traceback: null,
151
+ source: "marimo",
152
+ severity: "error",
153
+ range: null,
154
+ related: [],
155
+ },
156
+ });
157
+ expect(ended(error("exception")).outcome).toBe("error");
158
+ const skipped: Protocol.Outcome = {
159
+ kind: "skipped",
160
+ reason: "ancestor-stopped",
161
+ message: "",
162
+ related: [],
163
+ };
164
+ expect(ended(skipped).outcome).toBe("skipped");
165
+ expect(ended({ kind: "interrupted" }).outcome).toBe("interrupted");
166
+ });
167
+
168
+ it("reads as queued from a submission until the host shows a newer execution", () => {
169
+ const before = inputs({
170
+ executions: [execution({ id: "e1" })],
171
+ results: new Map([["c1", record({ id: "e1" })]]),
172
+ submissions: new Map([["c1", "e1"]]),
173
+ });
174
+ expect(cellExecutionView(before, "c1").status).toBe("queued");
175
+ expect(cellExecutionView(before, "c1").outcome).toBeNull();
176
+
177
+ // The stream let e1 go while the request was in flight; its result remains.
178
+ const forgotten = inputs({
179
+ results: new Map([["c1", record({ id: "e1" })]]),
180
+ submissions: new Map([["c1", "e1"]]),
181
+ });
182
+ expect(cellExecutionView(forgotten, "c1").status).toBe("queued");
183
+
184
+ const shown = inputs({
185
+ executions: [
186
+ execution({ id: "e2", status: "running", completed_at: null }),
187
+ ],
188
+ results: new Map([["c1", record({ id: "e1" })]]),
189
+ submissions: new Map([["c1", "e1"]]),
190
+ });
191
+ expect(cellExecutionView(shown, "c1").status).toBe("running");
192
+
193
+ const done = inputs({
194
+ executions: [execution({ id: "e2" })],
195
+ results: new Map([["c1", record({ id: "e2" })]]),
196
+ submissions: new Map([["c1", "e1"]]),
197
+ });
198
+ expect(cellExecutionView(done, "c1").status).toBe("idle");
199
+
200
+ const fresh = inputs({ submissions: new Map([["c1", null]]) });
201
+ expect(cellExecutionView(fresh, "c1").status).toBe("queued");
202
+ });
203
+
204
+ it("shows a blocked cell as disabled transitively, with its last outcome", () => {
205
+ const view = cellExecutionView(
206
+ inputs({
207
+ results: new Map([["c1", record({ id: "e1" })]]),
208
+ blocked: new Set(["c1"]),
209
+ }),
210
+ "c1",
211
+ );
212
+ expect(view.status).toBe("idle");
213
+ expect(view.blocked).toBe(true);
214
+ expect(view.outcome).toBe("value");
215
+ });
216
+ });
217
+
218
+ describe("notebook-level views", () => {
219
+ const stream = inputs({
220
+ executions: [
221
+ execution({ id: "e1", status: "running", completed_at: null }),
222
+ execution({
223
+ id: "e2",
224
+ cell_id: "c2",
225
+ status: "queued",
226
+ started_at: null,
227
+ completed_at: null,
228
+ }),
229
+ execution({ id: "e3", cell_id: "c3" }),
230
+ execution({
231
+ id: "s1",
232
+ cell_id: null,
233
+ status: "running",
234
+ completed_at: null,
235
+ }),
236
+ ],
237
+ results: new Map([["c5", record({ id: "e5", cell_id: "c5" })]]),
238
+ submissions: new Map([
239
+ ["c2", "e2"],
240
+ ["c4", null],
241
+ ["c5", "e5"],
242
+ ]),
243
+ });
244
+
245
+ it("counts unfinished executions and submissions the host has not shown", () => {
246
+ expect(anyRunning(stream.executions)).toBe(true);
247
+ expect(queuedOrRunningCount(stream)).toBe(5);
248
+ expect(runningCellIds(stream.executions)).toEqual(["c1"]);
249
+ expect(onlyScratchRunning(stream.executions)).toBe(false);
250
+ });
251
+
252
+ it("tells scratch work apart from cells", () => {
253
+ const scratch = inputs({
254
+ executions: [
255
+ execution({
256
+ id: "s1",
257
+ cell_id: null,
258
+ status: "running",
259
+ completed_at: null,
260
+ }),
261
+ execution({ id: "e3", cell_id: "c3" }),
262
+ ],
263
+ });
264
+ expect(anyRunning(scratch.executions)).toBe(true);
265
+ expect(onlyScratchRunning(scratch.executions)).toBe(true);
266
+ expect(queuedOrRunningCount(scratch)).toBe(1);
267
+ });
268
+ });
@@ -0,0 +1,206 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /** The active notebook's executions and runtime as the editor reads them. */
4
+
5
+ import type * as Protocol from "@marimo-team/client/protocol";
6
+ import { atom, type createStore } from "jotai";
7
+ import type { CellMessage } from "@/core/kernel/messages";
8
+ import type {
9
+ Executions,
10
+ NotebookReplica,
11
+ Results,
12
+ } from "@/core/notebook/replica";
13
+ import { activeNotebookReplicaAtom } from "@/core/session/active-notebook";
14
+ import { waitFor } from "@/core/state/jotai";
15
+ import {
16
+ anyRunning,
17
+ type CellExecutionInputs,
18
+ latestExecutionId,
19
+ queuedOrRunningCount,
20
+ } from "./select";
21
+
22
+ type Store = ReturnType<typeof createStore>;
23
+
24
+ const NO_EXECUTIONS: Executions = {
25
+ runs: new Map(),
26
+ executions: new Map(),
27
+ latest: new Map(),
28
+ };
29
+ const NO_RESULTS: Results = new Map();
30
+ const NO_SUBMISSIONS: ReadonlyMap<
31
+ Protocol.CellId,
32
+ Protocol.ExecutionId | null
33
+ > = new Map();
34
+ const NO_BLOCKED: ReadonlySet<Protocol.CellId> = new Set();
35
+
36
+ /**
37
+ * What the editor marks about cells of one replica's notebook.
38
+ *
39
+ * The marks are keyed by cell id, which another notebook, or the same one
40
+ * after a reconnect, reuses with different executions behind it. They
41
+ * therefore count only for the replica and host notebook they belong to.
42
+ */
43
+ type Marks<T> = {
44
+ readonly notebook: {
45
+ readonly replica: NotebookReplica;
46
+ readonly id: Protocol.NotebookId;
47
+ };
48
+ readonly value: T;
49
+ } | null;
50
+
51
+ /**
52
+ * Cells a run was requested for, with the execution each had at the time.
53
+ *
54
+ * The host shows a queued execution once the kernel plans the run. Until
55
+ * then the cell reads as queued from this record. The host's newer
56
+ * execution supersedes it; a rejected request, a cell the kernel will not
57
+ * run, or the cell leaving the document withdraws it. Written only by
58
+ * `submitCells`, `withdrawCells`, and `observeCellMessage`.
59
+ */
60
+ const submissionsAtom =
61
+ atom<Marks<ReadonlyMap<Protocol.CellId, Protocol.ExecutionId | null>>>(null);
62
+
63
+ /**
64
+ * Cells the kernel reports as blocked by a disabled ancestor. Written only
65
+ * by `observeCellMessage`.
66
+ *
67
+ * TODO(MO-8463): Derive from the document and the analysis stream.
68
+ */
69
+ const blockedCellsAtom = atom<Marks<ReadonlySet<Protocol.CellId>>>(null);
70
+
71
+ function current<T>(
72
+ marks: Marks<T>,
73
+ replica: NotebookReplica | null,
74
+ notebookId: Protocol.NotebookId | null,
75
+ empty: T,
76
+ ): T {
77
+ return marks !== null &&
78
+ marks.notebook.replica === replica &&
79
+ marks.notebook.id === notebookId
80
+ ? marks.value
81
+ : empty;
82
+ }
83
+
84
+ function mark<T>(store: Store, value: T): Marks<T> {
85
+ const replica = store.get(activeNotebookReplicaAtom);
86
+ const notebook = replica === null ? null : store.get(replica.notebookAtom);
87
+ return replica === null || notebook === null
88
+ ? null
89
+ : { notebook: { replica, id: notebook.id }, value };
90
+ }
91
+
92
+ /** What every cell's view derives from. Pair with `cellExecutionView`. */
93
+ export const cellExecutionInputsAtom = atom((get): CellExecutionInputs => {
94
+ const replica = get(activeNotebookReplicaAtom);
95
+ const notebookId =
96
+ replica === null ? null : (get(replica.notebookAtom)?.id ?? null);
97
+ return {
98
+ executions: replica === null ? NO_EXECUTIONS : get(replica.executionsAtom),
99
+ results: replica === null ? NO_RESULTS : get(replica.resultsAtom),
100
+ submissions: current(
101
+ get(submissionsAtom),
102
+ replica,
103
+ notebookId,
104
+ NO_SUBMISSIONS,
105
+ ),
106
+ blocked: current(get(blockedCellsAtom), replica, notebookId, NO_BLOCKED),
107
+ };
108
+ });
109
+
110
+ /** The runtime's status, null while the notebook has none or the stream is not ready. */
111
+ export const runtimeStatusAtom = atom((get) => {
112
+ const replica = get(activeNotebookReplicaAtom);
113
+ return replica === null ? null : (get(replica.runtimeAtom)?.status ?? null);
114
+ });
115
+
116
+ export const notebookIsRunningAtom = atom((get) =>
117
+ anyRunning(get(cellExecutionInputsAtom).executions),
118
+ );
119
+
120
+ export const notebookQueuedOrRunningCountAtom = atom((get) =>
121
+ queuedOrRunningCount(get(cellExecutionInputsAtom)),
122
+ );
123
+
124
+ /** Resolve once the host reports the runtime running. */
125
+ export async function waitForRuntimeRunning(): Promise<void> {
126
+ await waitFor(runtimeStatusAtom, (status) => status === "running");
127
+ }
128
+
129
+ /**
130
+ * Record a run request for `cellIds` so they read as queued at once.
131
+ *
132
+ * Cells blocked by a disabled ancestor stay blocked: the kernel will not
133
+ * queue them. A disabled cell is not queued either; the caller leaves it
134
+ * out, since its configuration lives in the notebook state.
135
+ */
136
+ export function submitCells(
137
+ store: Store,
138
+ cellIds: readonly Protocol.CellId[],
139
+ ): void {
140
+ const inputs = store.get(cellExecutionInputsAtom);
141
+ const next = new Map(inputs.submissions);
142
+ for (const cellId of cellIds) {
143
+ if (!inputs.blocked.has(cellId)) {
144
+ next.set(cellId, latestExecutionId(inputs, cellId));
145
+ }
146
+ }
147
+ setSubmissions(store, next);
148
+ }
149
+
150
+ function setSubmissions(
151
+ store: Store,
152
+ value: ReadonlyMap<Protocol.CellId, Protocol.ExecutionId | null>,
153
+ ): void {
154
+ store.set(submissionsAtom, mark(store, value));
155
+ }
156
+
157
+ /** Forget a run request the host rejected. */
158
+ export function withdrawCells(
159
+ store: Store,
160
+ cellIds: readonly Protocol.CellId[],
161
+ ): void {
162
+ const next = new Map(store.get(cellExecutionInputsAtom).submissions);
163
+ for (const cellId of cellIds) {
164
+ next.delete(cellId);
165
+ }
166
+ setSubmissions(store, next);
167
+ }
168
+
169
+ function setCellBlocked(
170
+ store: Store,
171
+ cellId: Protocol.CellId,
172
+ blocked: boolean,
173
+ ): void {
174
+ const known = store.get(cellExecutionInputsAtom).blocked;
175
+ if (known.has(cellId) === blocked) {
176
+ return;
177
+ }
178
+ const next = new Set(known);
179
+ if (blocked) {
180
+ next.add(cellId);
181
+ } else {
182
+ next.delete(cellId);
183
+ }
184
+ store.set(blockedCellsAtom, mark(store, next));
185
+ }
186
+
187
+ /**
188
+ * Keep the blocked set and the submissions in step with a cell-op.
189
+ *
190
+ * A status names whether the cell is blocked. A blocked cell and one whose
191
+ * output is an error will not run, so a submission that never became an
192
+ * execution is withdrawn, as it was when the editor set status itself.
193
+ *
194
+ * TODO(MO-8407): Delete with the cell-op case. The blocked read goes with
195
+ * MO-8463, the error read with MO-8346.
196
+ */
197
+ export function observeCellMessage(store: Store, message: CellMessage): void {
198
+ const cellId = message.cell_id;
199
+ const blocked = message.status === "disabled-transitively";
200
+ if (message.status != null) {
201
+ setCellBlocked(store, cellId, blocked);
202
+ }
203
+ if (blocked || message.output?.mimetype === "application/vnd.marimo+error") {
204
+ withdrawCells(store, [cellId]);
205
+ }
206
+ }
@@ -0,0 +1,31 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type * as Protocol from "@marimo-team/client/protocol";
4
+ import { dequal } from "dequal";
5
+ import { useAtomValue } from "jotai";
6
+ import { selectAtom } from "jotai/utils";
7
+ import { useMemo } from "react";
8
+ import { cellExecutionInputsAtom } from "./atoms";
9
+ import { type CellExecutionView, cellExecutionView } from "./select";
10
+
11
+ /**
12
+ * A cell's status, timing, and how it last ended.
13
+ *
14
+ * Read from the host's executions, with the editor's own marks for a run
15
+ * it has requested and the kernel has not planned yet, and for a cell the
16
+ * kernel reports blocked by a disabled ancestor.
17
+ */
18
+ export function useCellExecution(id: Protocol.CellId): CellExecutionView {
19
+ // One atom per mounted cell, kept for its subscription and compared by
20
+ // value so the cell renders only when its own view changes.
21
+ const cellAtom = useMemo(
22
+ () =>
23
+ selectAtom(
24
+ cellExecutionInputsAtom,
25
+ (inputs) => cellExecutionView(inputs, id),
26
+ dequal,
27
+ ),
28
+ [id],
29
+ );
30
+ return useAtomValue(cellAtom);
31
+ }
@@ -0,0 +1,31 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /**
4
+ * Cell status and timing read from the host's executions.
5
+ *
6
+ * Every atom here is derived, so it can be read but not set. The state
7
+ * behind them changes only through `submitCells`, `withdrawCells`, and
8
+ * `observeCellMessage`.
9
+ */
10
+
11
+ export {
12
+ cellExecutionInputsAtom,
13
+ notebookIsRunningAtom,
14
+ notebookQueuedOrRunningCountAtom,
15
+ observeCellMessage,
16
+ runtimeStatusAtom,
17
+ submitCells,
18
+ waitForRuntimeRunning,
19
+ withdrawCells,
20
+ } from "./atoms";
21
+ export { useCellExecution } from "./hooks";
22
+ export {
23
+ type CellExecutionInputs,
24
+ type CellExecutionView,
25
+ type CellOutcome,
26
+ type CellRunStatus,
27
+ cellExecutionView,
28
+ onlyScratchRunning,
29
+ runningCellIds,
30
+ scratchStatus,
31
+ } from "./select";