@marimo-team/islands 0.25.3-dev308 → 0.25.3-dev310

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 (106) hide show
  1. package/dist/{ConnectedDataExplorerComponent-DJnSxHJB.js → ConnectedDataExplorerComponent-DieU6L4x.js} +1 -1
  2. package/dist/{ErrorBoundary-flfhLFje.js → ErrorBoundary-DjoeT4kv.js} +1 -1
  3. package/dist/{abnfDiagram-VCTEODGH-CR0U5hup.js → abnfDiagram-VCTEODGH-LOVrjbdp.js} +2 -2
  4. package/dist/{any-language-editor-C5dsTpeB.js → any-language-editor-DMC2JE9_.js} +1 -1
  5. package/dist/{architectureDiagram-5GKGNRK7-Bc_fbI1Q.js → architectureDiagram-5GKGNRK7-RZsm312h.js} +1 -1
  6. package/dist/assets/__vite-browser-external-BSp9tJ_k.js +1 -0
  7. package/dist/assets/worker-BdfPA0g5.js +171 -0
  8. package/dist/{chat-ui-C4iLRzY0.js → chat-ui-DkuxGEDp.js} +418 -418
  9. package/dist/{chunk-IMKFNOWR-y9z5oeAY.js → chunk-IMKFNOWR-B6agbLjo.js} +1 -1
  10. package/dist/{chunk-SHT3W25Y-Tu-V-e_v.js → chunk-SHT3W25Y-CfULafYG.js} +1 -1
  11. package/dist/{chunk-SVP7TREG-CJFxs5R_.js → chunk-SVP7TREG-DEOpPMJp.js} +1 -1
  12. package/dist/{chunk-TICWLB2K-zkTP7ya7.js → chunk-TICWLB2K-DaH-gkX5.js} +1 -1
  13. package/dist/{classDiagram-ZZMXUADV-DxJAZI0N.js → classDiagram-ZZMXUADV-C0CyiWQ7.js} +2 -2
  14. package/dist/{classDiagram-v2-VYDZK3BY-TmHo2BpS.js → classDiagram-v2-VYDZK3BY-xfIc9e2Y.js} +2 -2
  15. package/dist/{code-visibility-BDs_r7jH.js → code-visibility-CduMVVFS.js} +32 -32
  16. package/dist/{common-CMTsL35V.js → common-CMcEcucH.js} +919 -919
  17. package/dist/{config-BG2qKzDg.js → config-BVYA1P0C.js} +103 -76
  18. package/dist/{cynefinDiagram-5FMLGOSQ-Eb40hmhD.js → cynefinDiagram-5FMLGOSQ-DwGYhKu0.js} +1 -1
  19. package/dist/{dagre-GXQ25YYZ-_paaVwit.js → dagre-GXQ25YYZ-DfMaWJOV.js} +1 -1
  20. package/dist/{data-grid-overlay-editor-es6V6qf-.js → data-grid-overlay-editor-BYrIVut3.js} +1 -1
  21. package/dist/{diagram-S7CK7UJ4-BtKJm1_4.js → diagram-S7CK7UJ4-Bw_L87GZ.js} +1 -1
  22. package/dist/{diagram-UQ7AKVKN-DQbxhEMd.js → diagram-UQ7AKVKN-nsrs3Fbz.js} +1 -1
  23. package/dist/{diagram-VX7I27RA-DITDIfGC.js → diagram-VX7I27RA-7ST9lEqP.js} +1 -1
  24. package/dist/{diagram-Z3DM3KII-Ct9_zo6F.js → diagram-Z3DM3KII-BnGte-HX.js} +1 -1
  25. package/dist/{ebnfDiagram-PWID7BFC-6cOntwfv.js → ebnfDiagram-PWID7BFC-zaKyJ1YL.js} +2 -2
  26. package/dist/{erDiagram-RLTQ6QDP-CJiOER41.js → erDiagram-RLTQ6QDP-Cx7g95FZ.js} +1 -1
  27. package/dist/{error-banner-DGOrmBk5.js → error-banner-DkQNU4bn.js} +128 -128
  28. package/dist/{error-entries-BWmleiL0.js → error-entries-istW7Xbs.js} +589 -544
  29. package/dist/{feedback-button-D4eVjeNs.js → feedback-button-Zvm26aOQ.js} +107 -106
  30. package/dist/{flowDiagram-HODETNUW-1lPBEUuW.js → flowDiagram-HODETNUW-C7nIqaHP.js} +2 -2
  31. package/dist/{formats-qhBmYb0l.js → formats-CMa7L3E8.js} +1 -1
  32. package/dist/{glide-data-editor-ahfaPdFW.js → glide-data-editor-BE-GgVp4.js} +3 -3
  33. package/dist/{globals-m8oswz--.js → globals-Dbru-WLP.js} +1 -1
  34. package/dist/{html-to-image-B8hE6HIH.js → html-to-image-pPbR2cDx.js} +4512 -4464
  35. package/dist/{infoDiagram-27XIBGKW-C6K4tri5.js → infoDiagram-27XIBGKW-BTM_NXsn.js} +1 -1
  36. package/dist/{ishikawaDiagram-5VMMS53U-DwcwgRNH.js → ishikawaDiagram-5VMMS53U-K0FZrlvv.js} +1 -1
  37. package/dist/{kanban-definition-UXKFOSKX-E489OMdc.js → kanban-definition-UXKFOSKX-ftznOyrz.js} +1 -1
  38. package/dist/main.js +6595 -6186
  39. package/dist/{mermaid-DMPMkVqY.js → mermaid-CNgH43ED.js} +33 -33
  40. package/dist/{mindmap-definition-YA3MSWOX-DWHjasOp.js → mindmap-definition-YA3MSWOX-CmzbxKfM.js} +1 -1
  41. package/dist/{number-overlay-editor-C6klKzEr.js → number-overlay-editor-qvlPZ-l-.js} +1 -1
  42. package/dist/{pegDiagram-XKGWAZYB-COjcPOZT.js → pegDiagram-XKGWAZYB-DfTAWfGE.js} +2 -2
  43. package/dist/{pieDiagram-E7YTZNPT-BHAVZ38h.js → pieDiagram-E7YTZNPT-_kGQstZj.js} +1 -1
  44. package/dist/{process-output-DZDL_j1a.js → process-output-BySO1nIs.js} +3 -3
  45. package/dist/{railroadDiagram-O6MQD6OU-94ThFyKX.js → railroadDiagram-O6MQD6OU-D41WmIff.js} +2 -2
  46. package/dist/{requirementDiagram-BXWQKSXE-CpF-Fxbb.js → requirementDiagram-BXWQKSXE-Bh53ZI3I.js} +1 -1
  47. package/dist/{reveal-component-DwnxPyVz.js → reveal-component-DQaegdcW.js} +4 -4
  48. package/dist/{sequenceDiagram-WJ2MYXX4-CvANK2x7.js → sequenceDiagram-WJ2MYXX4-DBX27dNm.js} +1 -1
  49. package/dist/{stateDiagram-D77RDMKH-WHuJ4mJL.js → stateDiagram-D77RDMKH-CSYz2IZi.js} +2 -2
  50. package/dist/{stateDiagram-v2-MP3YSRHH-DbO7lwYq.js → stateDiagram-v2-MP3YSRHH-DVLvtOqn.js} +2 -2
  51. package/dist/{swimlanes-42K2YHIH-CS2s29Vo.js → swimlanes-42K2YHIH-D59N0ktA.js} +1 -1
  52. package/dist/{swimlanesDiagram-VR7AAH4N-CXaA1vQg.js → swimlanesDiagram-VR7AAH4N-CRgpmVSW.js} +2 -2
  53. package/dist/{timeline-definition-24CTP7MA-B-SQMg5h.js → timeline-definition-24CTP7MA-C_xIIZDM.js} +1 -1
  54. package/dist/toDate-5qPtZ6HL.js +94 -0
  55. package/dist/{vega-component-DwqGfN3_.js → vega-component-Bly2lDhz.js} +10 -10
  56. package/dist/{vennDiagram-4TSXK5OY-etEFgvaG.js → vennDiagram-4TSXK5OY-DlOf0Mjs.js} +1 -1
  57. package/dist/{wardleyDiagram-VM6X3IG4-BEQN7g62.js → wardleyDiagram-VM6X3IG4-zD_2guVe.js} +1 -1
  58. package/dist/{xychartDiagram-S5SC5T6Z-D8-e5Z0R.js → xychartDiagram-S5SC5T6Z-CA0Dy1ZM.js} +1 -1
  59. package/package.json +1 -1
  60. package/src/__mocks__/executions.ts +35 -13
  61. package/src/__mocks__/requests.ts +0 -1
  62. package/src/components/editor/cell/__tests__/useRunCells.test.tsx +86 -101
  63. package/src/components/editor/cell/useRunCells.ts +38 -65
  64. package/src/components/editor/notebook-cell.tsx +32 -20
  65. package/src/components/editor/output/ansi-reduce.ts +7 -0
  66. package/src/core/ai/__tests__/staged-cells.test.ts +1 -1
  67. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +2 -0
  68. package/src/core/cells/__tests__/cells.test.ts +0 -14
  69. package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +15 -0
  70. package/src/core/cells/__tests__/utils.test.ts +1 -1
  71. package/src/core/cells/add-missing-import.ts +2 -5
  72. package/src/core/cells/cell.ts +1 -0
  73. package/src/core/cells/cells.ts +1 -2
  74. package/src/core/executions/__tests__/atoms.test.ts +227 -43
  75. package/src/core/executions/__tests__/hooks.test.tsx +1 -2
  76. package/src/core/executions/__tests__/input.test.ts +86 -0
  77. package/src/core/executions/__tests__/run.test.ts +390 -0
  78. package/src/core/executions/__tests__/select.test.ts +105 -30
  79. package/src/core/executions/atoms.ts +186 -71
  80. package/src/core/executions/index.ts +7 -6
  81. package/src/core/executions/input.ts +55 -0
  82. package/src/core/executions/run.ts +120 -0
  83. package/src/core/executions/select.ts +59 -24
  84. package/src/core/islands/__tests__/bridge.test.ts +35 -54
  85. package/src/core/islands/bootstrap.ts +5 -1
  86. package/src/core/islands/bridge.ts +16 -28
  87. package/src/core/islands/components/IslandControls.tsx +15 -7
  88. package/src/core/islands/components/__tests__/IslandControls.test.tsx +36 -53
  89. package/src/core/islands/worker/worker.tsx +45 -19
  90. package/src/core/network/__tests__/requests-lazy.test.ts +14 -24
  91. package/src/core/network/__tests__/requests-toasting.test.tsx +7 -7
  92. package/src/core/network/requests-lazy.ts +2 -3
  93. package/src/core/network/requests-network.ts +0 -9
  94. package/src/core/network/requests-static.ts +0 -1
  95. package/src/core/network/requests-toasting.tsx +0 -1
  96. package/src/core/network/types.ts +0 -2
  97. package/src/core/notebook/replica/__tests__/fixtures.ts +17 -1
  98. package/src/core/notebook/replica/backend.ts +3 -1
  99. package/src/core/runtime/__tests__/runtime.test.ts +36 -0
  100. package/src/core/runtime/config.ts +36 -1
  101. package/src/core/runtime/runtime.ts +21 -1
  102. package/src/core/transports/static.ts +4 -3
  103. package/src/core/wasm/bridge.ts +0 -17
  104. package/dist/assets/__vite-browser-external-DzTYIHH_.js +0 -1
  105. package/dist/assets/worker-BH08I2Il.js +0 -171
  106. package/dist/static-state-Dtocd9UO.js +0 -68
@@ -5,19 +5,18 @@
5
5
  import type * as Protocol from "@marimo-team/client/protocol";
6
6
  import { atom, type createStore } from "jotai";
7
7
  import type { CellMessage } from "@/core/kernel/messages";
8
- import type {
9
- Executions,
10
- NotebookReplica,
11
- Results,
8
+ import {
9
+ ConnectionState,
10
+ type Executions,
11
+ type NotebookReplica,
12
+ type Results,
12
13
  } from "@/core/notebook/replica";
13
14
  import { activeNotebookReplicaAtom } from "@/core/session/active-notebook";
14
- import { waitFor } from "@/core/state/jotai";
15
15
  import { type CellFreshnessInputs, freshnessOf } from "./freshness";
16
16
  import type { CellOutputInputs } from "./output";
17
17
  import {
18
18
  anyRunning,
19
19
  type CellExecutionInputs,
20
- latestExecutionId,
21
20
  queuedOrRunningCount,
22
21
  } from "./select";
23
22
 
@@ -30,40 +29,52 @@ const NO_EXECUTIONS: Executions = {
30
29
  };
31
30
  const NO_RESULTS: Results = new Map();
32
31
  const NO_CELLS: ReadonlyMap<Protocol.CellId, Protocol.Cell> = new Map();
33
- const NO_SUBMISSIONS: ReadonlyMap<
34
- Protocol.CellId,
35
- Protocol.ExecutionId | null
36
- > = new Map();
32
+ const NO_REQUESTS: ReadonlySet<RunRequest> = new Set();
33
+ const NO_REQUESTED: ReadonlySet<Protocol.CellId> = new Set();
37
34
  const NO_BLOCKED: ReadonlySet<Protocol.CellId> = new Set();
38
35
  const NO_CLEARED: ReadonlyMap<Protocol.CellId, Protocol.ExecutionId> =
39
36
  new Map();
40
37
 
38
+ /** The notebook a replica is showing, which the editor's marks belong to. */
39
+ interface ShownNotebook {
40
+ readonly replica: NotebookReplica;
41
+ readonly id: Protocol.NotebookId;
42
+ }
43
+
41
44
  /**
42
45
  * What the editor marks about cells of one replica's notebook.
43
46
  *
44
- * The marks are keyed by cell id, which another notebook, or the same one
47
+ * The marks refer to cells by id, which another notebook, or the same one
45
48
  * after a reconnect, reuses with different executions behind it. They
46
49
  * therefore count only for the replica and host notebook they belong to.
47
50
  */
48
51
  type Marks<T> = {
49
- readonly notebook: {
50
- readonly replica: NotebookReplica;
51
- readonly id: Protocol.NotebookId;
52
- };
52
+ readonly notebook: ShownNotebook;
53
53
  readonly value: T;
54
54
  } | null;
55
55
 
56
56
  /**
57
- * Cells a run was requested for, with the execution each had at the time.
57
+ * A run this browser has asked the host for.
58
+ *
59
+ * The host answers some time after the request is made, and by then the
60
+ * notebook may have been replaced or the same cells asked for again. So
61
+ * the functions that handle the answer are given the request back, and
62
+ * they act only on it, in the notebook it was made in.
63
+ */
64
+ export interface RunRequest {
65
+ readonly notebook: ShownNotebook;
66
+ readonly cells: readonly Protocol.CellId[];
67
+ }
68
+
69
+ /**
70
+ * Requests whose run has not appeared on the stream yet.
58
71
  *
59
- * The host shows a queued execution once the kernel plans the run. Until
60
- * then the cell reads as queued from this record. The host's newer
61
- * execution supersedes it; a rejected request, a cell the kernel will not
62
- * run, or the cell leaving the document withdraws it. Written only by
63
- * `submitCells`, `withdrawCells`, and `observeCellMessage`.
72
+ * They let a requested cell read as queued right away. Once the host
73
+ * accepts the run, the stream shows it with the same cells named on it,
74
+ * and the request is no longer needed. A request the host refuses, or
75
+ * that the browser gives up on, is withdrawn instead.
64
76
  */
65
- const submissionsAtom =
66
- atom<Marks<ReadonlyMap<Protocol.CellId, Protocol.ExecutionId | null>>>(null);
77
+ const requestsAtom = atom<Marks<ReadonlySet<RunRequest>>>(null);
67
78
 
68
79
  /**
69
80
  * Cells the kernel reports as blocked by a disabled ancestor. Written only
@@ -86,28 +97,58 @@ function current<T>(
86
97
  : empty;
87
98
  }
88
99
 
89
- function mark<T>(store: Store, value: T): Marks<T> {
100
+ function shownNotebook(store: Store): ShownNotebook | null {
90
101
  const replica = store.get(activeNotebookReplicaAtom);
91
102
  const notebook = replica === null ? null : store.get(replica.notebookAtom);
92
103
  return replica === null || notebook === null
93
104
  ? null
94
- : { notebook: { replica, id: notebook.id }, value };
105
+ : { replica, id: notebook.id };
95
106
  }
96
107
 
108
+ function mark<T>(store: Store, value: T): Marks<T> {
109
+ const notebook = shownNotebook(store);
110
+ return notebook === null ? null : { notebook, value };
111
+ }
112
+
113
+ /**
114
+ * Whether `request` still belongs to the notebook being shown.
115
+ *
116
+ * Between a reset and the next snapshot the replica has no notebook for a
117
+ * moment. That says nothing about where the request belongs, so it does
118
+ * not count against it. Another replica, or another notebook on the same
119
+ * replica, does.
120
+ */
121
+ export function isCurrent(store: Store, request: RunRequest): boolean {
122
+ const replica = store.get(activeNotebookReplicaAtom);
123
+ if (replica !== request.notebook.replica) {
124
+ return false;
125
+ }
126
+ const notebook = store.get(replica.notebookAtom);
127
+ return notebook === null || notebook.id === request.notebook.id;
128
+ }
129
+
130
+ /** Cells named by pending requests made in the notebook now shown. */
131
+ const requestedCellsAtom = atom((get) => {
132
+ const replica = get(activeNotebookReplicaAtom);
133
+ const notebookId =
134
+ replica === null ? null : (get(replica.notebookAtom)?.id ?? null);
135
+ const requests = current(get(requestsAtom), replica, notebookId, NO_REQUESTS);
136
+ return requests.size === 0
137
+ ? NO_REQUESTED
138
+ : new Set([...requests].flatMap((request) => request.cells));
139
+ });
140
+
97
141
  /** What every cell's view derives from. Pair with `cellExecutionView`. */
98
142
  export const cellExecutionInputsAtom = atom((get): CellExecutionInputs => {
99
143
  const replica = get(activeNotebookReplicaAtom);
100
144
  const notebookId =
101
145
  replica === null ? null : (get(replica.notebookAtom)?.id ?? null);
146
+ const document = replica === null ? null : get(replica.documentAtom);
102
147
  return {
103
148
  executions: replica === null ? NO_EXECUTIONS : get(replica.executionsAtom),
104
149
  results: replica === null ? NO_RESULTS : get(replica.resultsAtom),
105
- submissions: current(
106
- get(submissionsAtom),
107
- replica,
108
- notebookId,
109
- NO_SUBMISSIONS,
110
- ),
150
+ cells: document?.cells ?? NO_CELLS,
151
+ requested: get(requestedCellsAtom),
111
152
  blocked: current(get(blockedCellsAtom), replica, notebookId, NO_BLOCKED),
112
153
  };
113
154
  });
@@ -178,49 +219,126 @@ export const notebookQueuedOrRunningCountAtom = atom((get) =>
178
219
  queuedOrRunningCount(get(cellExecutionInputsAtom)),
179
220
  );
180
221
 
181
- /** Resolve once the host reports the runtime running. */
182
- export async function waitForRuntimeRunning(): Promise<void> {
183
- await waitFor(runtimeStatusAtom, (status) => status === "running");
222
+ type RuntimeReadiness = "running" | "waiting" | { refused: string };
223
+
224
+ /**
225
+ * Whether a run can go to the host now, should wait, or has nothing to wait for.
226
+ *
227
+ * A runtime that is starting, or a stream that has not reported yet, will
228
+ * settle one way or the other. A failed or absent runtime, or a connection
229
+ * that has closed, will not, so a run waiting on it is refused instead.
230
+ */
231
+ const runtimeReadinessAtom = atom((get): RuntimeReadiness => {
232
+ const replica = get(activeNotebookReplicaAtom);
233
+ if (replica === null) {
234
+ return { refused: "No notebook is connected" };
235
+ }
236
+ if (get(replica.connectionAtom).state === ConnectionState.CLOSED) {
237
+ return { refused: "The notebook is disconnected" };
238
+ }
239
+ // Until the stream's snapshot is complete there is no notebook to speak
240
+ // of, and nothing it reports about the runtime counts yet.
241
+ if (get(replica.notebookAtom) === null) {
242
+ return "waiting";
243
+ }
244
+ const runtime = get(replica.runtimeAtom);
245
+ if (runtime === null) {
246
+ return { refused: "The notebook has no runtime" };
247
+ }
248
+ switch (runtime.status) {
249
+ case "running":
250
+ return "running";
251
+ case "failed":
252
+ return { refused: runtime.message ?? "The runtime failed" };
253
+ default:
254
+ return "waiting";
255
+ }
256
+ });
257
+
258
+ /** Resolve once the host reports the runtime running; reject once it cannot. */
259
+ export function waitForRuntimeRunning(store: Store): Promise<void> {
260
+ return new Promise((resolve, reject) => {
261
+ const unsubscribe = store.sub(runtimeReadinessAtom, check);
262
+ function check() {
263
+ const readiness = store.get(runtimeReadinessAtom);
264
+ if (readiness === "waiting") {
265
+ return;
266
+ }
267
+ unsubscribe();
268
+ if (readiness === "running") {
269
+ resolve();
270
+ } else {
271
+ reject(new Error(readiness.refused));
272
+ }
273
+ }
274
+ check();
275
+ });
184
276
  }
185
277
 
186
278
  /**
187
- * Record a run request for `cellIds` so they read as queued at once.
279
+ * Record that this browser is asking the host to run `cellIds`, so that
280
+ * they read as queued before the host has answered.
188
281
  *
189
- * Cells blocked by a disabled ancestor stay blocked: the kernel will not
190
- * queue them. A disabled cell is not queued either; the caller leaves it
191
- * out, since its configuration lives in the notebook state.
282
+ * A disabled cell, or one the kernel reports as blocked, may be among them
283
+ * so the kernel can set it aside, but the views do not show it as queued.
284
+ * Returns null while no notebook is shown, since there is nothing to
285
+ * record the request against.
192
286
  */
193
- export function submitCells(
287
+ export function requestRun(
194
288
  store: Store,
195
289
  cellIds: readonly Protocol.CellId[],
196
- ): void {
197
- const inputs = store.get(cellExecutionInputsAtom);
198
- const next = new Map(inputs.submissions);
199
- for (const cellId of cellIds) {
200
- if (!inputs.blocked.has(cellId)) {
201
- next.set(cellId, latestExecutionId(inputs, cellId));
202
- }
290
+ ): RunRequest | null {
291
+ const notebook = shownNotebook(store);
292
+ if (notebook === null) {
293
+ return null;
203
294
  }
204
- setSubmissions(store, next);
295
+ const request: RunRequest = { notebook, cells: [...cellIds] };
296
+ const marks = store.get(requestsAtom);
297
+ const kept =
298
+ marks !== null &&
299
+ marks.notebook.replica === notebook.replica &&
300
+ marks.notebook.id === notebook.id
301
+ ? marks.value
302
+ : NO_REQUESTS;
303
+ store.set(requestsAtom, { notebook, value: new Set(kept).add(request) });
304
+ return request;
205
305
  }
206
306
 
207
- function setSubmissions(
208
- store: Store,
209
- value: ReadonlyMap<Protocol.CellId, Protocol.ExecutionId | null>,
210
- ): void {
211
- store.set(submissionsAtom, mark(store, value));
307
+ /** Forget a request the host refused or the browser gave up on. */
308
+ export function withdrawRequest(store: Store, request: RunRequest): void {
309
+ const marks = store.get(requestsAtom);
310
+ if (marks === null || !marks.value.has(request)) {
311
+ return;
312
+ }
313
+ const value = new Set(marks.value);
314
+ value.delete(request);
315
+ store.set(requestsAtom, { notebook: marks.notebook, value });
212
316
  }
213
317
 
214
- /** Forget a run request the host rejected. */
215
- export function withdrawCells(
318
+ /**
319
+ * Keep a request until the run the host accepted for it appears on the
320
+ * stream, which can happen after the response has arrived. From then on
321
+ * the run itself says which cells are queued. If the notebook is no
322
+ * longer shown by then, the request is simply dropped.
323
+ */
324
+ export function acceptRun(
216
325
  store: Store,
217
- cellIds: readonly Protocol.CellId[],
326
+ request: RunRequest,
327
+ run: Protocol.Run,
218
328
  ): void {
219
- const next = new Map(store.get(cellExecutionInputsAtom).submissions);
220
- for (const cellId of cellIds) {
221
- next.delete(cellId);
329
+ const { replica } = request.notebook;
330
+ const unsubscribe = store.sub(replica.executionsAtom, check);
331
+ function check() {
332
+ if (
333
+ isCurrent(store, request) &&
334
+ !store.get(replica.executionsAtom).runs.has(run.id)
335
+ ) {
336
+ return;
337
+ }
338
+ unsubscribe();
339
+ withdrawRequest(store, request);
222
340
  }
223
- setSubmissions(store, next);
341
+ check();
224
342
  }
225
343
 
226
344
  function setCellBlocked(
@@ -242,22 +360,19 @@ function setCellBlocked(
242
360
  }
243
361
 
244
362
  /**
245
- * Keep the blocked set and the submissions in step with a cell-op.
246
- *
247
- * A status names whether the cell is blocked. A blocked cell and one whose
248
- * output is an error will not run, so a submission that never became an
249
- * execution is withdrawn, as it was when the editor set status itself.
363
+ * Learn from a cell-op whether the kernel considers the cell blocked by a
364
+ * disabled ancestor. A blocked cell will not run, so a run that names it
365
+ * does not show it as queued.
250
366
  *
251
367
  * TODO(MO-8407): Delete with the cell-op case. The blocked read goes with
252
- * MO-8463, the error read with MO-8346.
368
+ * MO-8463.
253
369
  */
254
370
  export function observeCellMessage(store: Store, message: CellMessage): void {
255
- const cellId = message.cell_id;
256
- const blocked = message.status === "disabled-transitively";
257
371
  if (message.status != null) {
258
- setCellBlocked(store, cellId, blocked);
259
- }
260
- if (blocked || message.output?.mimetype === "application/vnd.marimo+error") {
261
- withdrawCells(store, [cellId]);
372
+ setCellBlocked(
373
+ store,
374
+ message.cell_id,
375
+ message.status === "disabled-transitively",
376
+ );
262
377
  }
263
378
  }
@@ -1,11 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  /**
4
- * Cell status and timing read from the host's executions.
4
+ * Cell status, timing, and output, as the editor reads them.
5
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`.
6
+ * The host's notebook stream is the source of truth. Its runs, executions,
7
+ * and results say what every cell is doing. The atoms here derive views
8
+ * from that stream, adding only what the host cannot know yet, such as a
9
+ * run this browser has asked for but not heard back about.
9
10
  */
10
11
 
11
12
  export {
@@ -17,11 +18,11 @@ export {
17
18
  notebookQueuedOrRunningCountAtom,
18
19
  observeCellMessage,
19
20
  runtimeStatusAtom,
20
- submitCells,
21
21
  waitForRuntimeRunning,
22
- withdrawCells,
23
22
  } from "./atoms";
24
23
  export type { CellFreshness, CellFreshnessLookup } from "./freshness";
24
+ export { replyInput } from "./input";
25
+ export { runCells, syncEdits, waitForRuntime } from "./run";
25
26
  export {
26
27
  useCellExecution,
27
28
  useCellFreshness,
@@ -0,0 +1,55 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /** Answer a prompt the runtime is waiting on. */
4
+
5
+ import type * as Protocol from "@marimo-team/client/protocol";
6
+ import type { createStore } from "jotai";
7
+ import { toast } from "@/components/ui/use-toast";
8
+ import { activeNotebookReplicaAtom } from "@/core/session/active-notebook";
9
+ import { prettyError } from "@/utils/errors";
10
+ import { Logger } from "@/utils/Logger";
11
+
12
+ type Store = ReturnType<typeof createStore>;
13
+
14
+ /**
15
+ * Send `value` to the host as the answer to the prompt `inputId`.
16
+ *
17
+ * The reply names its prompt, so the host marks that prompt answered for
18
+ * every client and refuses a reply to one that has already been answered
19
+ * or cancelled. A refusal is reported, since the user typed the answer and
20
+ * would otherwise wait on a prompt that is not going to take it.
21
+ */
22
+ export async function replyInput(
23
+ store: Store,
24
+ inputId: Protocol.ItemId,
25
+ value: string,
26
+ ): Promise<void> {
27
+ const replica = store.get(activeNotebookReplicaAtom);
28
+ const notebook = replica === null ? null : store.get(replica.notebookAtom);
29
+ const backend = replica === null ? null : store.get(replica.backendAtom);
30
+ if (notebook === null || backend === null) {
31
+ Logger.warn("No notebook is connected to answer the prompt in");
32
+ return;
33
+ }
34
+ try {
35
+ const result = await backend
36
+ .notebook(notebook.id)
37
+ .input(inputId, { value });
38
+ if (result.ok) {
39
+ return;
40
+ }
41
+ Logger.error("The host refused the input reply", result.problem);
42
+ toast({
43
+ title: "Failed to send input",
44
+ description: result.problem.detail ?? result.problem.title,
45
+ variant: "danger",
46
+ });
47
+ } catch (error) {
48
+ Logger.error("Failed to send input", error);
49
+ toast({
50
+ title: "Failed to send input",
51
+ description: prettyError(error),
52
+ variant: "danger",
53
+ });
54
+ }
55
+ }
@@ -0,0 +1,120 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /** Submit cell runs to the host. */
4
+
5
+ import type * as Protocol from "@marimo-team/client/protocol";
6
+ import type { createStore } from "jotai";
7
+ import type { DocumentModel } from "@/core/notebook/document";
8
+ import { toast } from "@/components/ui/use-toast";
9
+ import { startRuntimeIfLazy } from "@/core/runtime/config";
10
+ import { activeDocumentModelAtom } from "@/core/session/active-notebook";
11
+ import { prettyError } from "@/utils/errors";
12
+ import { Logger } from "@/utils/Logger";
13
+ import {
14
+ acceptRun,
15
+ isCurrent,
16
+ requestRun,
17
+ waitForRuntimeRunning,
18
+ withdrawRequest,
19
+ } from "./atoms";
20
+
21
+ type Store = ReturnType<typeof createStore>;
22
+
23
+ /**
24
+ * Start a lazy runtime and wait until the host reports it running.
25
+ *
26
+ * The host refuses a run while its runtime is not running, so a request
27
+ * made during startup waits here instead of failing. It rejects once the
28
+ * runtime cannot come up, so the request is not left waiting.
29
+ */
30
+ export async function waitForRuntime(store: Store): Promise<void> {
31
+ await startRuntimeIfLazy();
32
+ await waitForRuntimeRunning(store);
33
+ }
34
+
35
+ /**
36
+ * Get this browser's edits to the host before a command that names cells.
37
+ *
38
+ * A run asks for the edits explicitly, so a synchronization that stalled
39
+ * earlier is retried here rather than left to the footer's retry.
40
+ */
41
+ export function syncEdits(model: DocumentModel): Promise<Protocol.Document> {
42
+ return model.retry();
43
+ }
44
+
45
+ /**
46
+ * Run `cellIds` through the host once this browser's edits have reached it.
47
+ *
48
+ * The cells read as queued right away. When the host accepts the run, the
49
+ * stream shows it with the same cells named on it and takes over from the
50
+ * request. A request that never reaches the host is withdrawn, and the
51
+ * user is told why. The request belongs to the notebook it was made in, so
52
+ * if another notebook is shown by the time the edits have synced, it is
53
+ * dropped rather than sent there.
54
+ */
55
+ export async function runCells(
56
+ store: Store,
57
+ cellIds: readonly Protocol.CellId[],
58
+ ): Promise<void> {
59
+ if (cellIds.length === 0) {
60
+ return;
61
+ }
62
+ const model = store.get(activeDocumentModelAtom);
63
+ if (model === null) {
64
+ Logger.warn("No notebook is open to run cells in");
65
+ return;
66
+ }
67
+ // The stream may have no notebook for a moment, as during a reconnect.
68
+ // The request is then recorded once the runtime, and with it the
69
+ // notebook, has been seen.
70
+ let request = requestRun(store, cellIds);
71
+ try {
72
+ await waitForRuntime(store);
73
+ request ??= requestRun(store, cellIds);
74
+ if (request === null) {
75
+ throw new Error("No notebook is shown");
76
+ }
77
+ const { notebook_id } = await syncEdits(model);
78
+ if (
79
+ store.get(activeDocumentModelAtom) !== model ||
80
+ !isCurrent(store, request) ||
81
+ notebook_id !== request.notebook.id
82
+ ) {
83
+ withdrawRequest(store, request);
84
+ return;
85
+ }
86
+ const backend = store.get(request.notebook.replica.backendAtom);
87
+ if (backend === null) {
88
+ throw new Error("The notebook is not connected");
89
+ }
90
+ const result = await backend
91
+ .notebook(notebook_id)
92
+ .run({ cells: cellIds.map((id) => ({ id })) });
93
+ if (result.ok) {
94
+ acceptRun(store, request, result.data);
95
+ return;
96
+ }
97
+ withdrawRequest(store, request);
98
+ // The connection is read-only for this action, which the UI shows.
99
+ if (result.status === 403) {
100
+ Logger.warn("Run refused, connection is read-only", result.problem);
101
+ return;
102
+ }
103
+ Logger.error("The host refused the run", result.problem);
104
+ toast({
105
+ title: "Failed to run",
106
+ description: result.problem.detail ?? result.problem.title,
107
+ variant: "danger",
108
+ });
109
+ } catch (error) {
110
+ if (request !== null) {
111
+ withdrawRequest(store, request);
112
+ }
113
+ Logger.error("Failed to run cells", error);
114
+ toast({
115
+ title: "Failed to run",
116
+ description: prettyError(error),
117
+ variant: "danger",
118
+ });
119
+ }
120
+ }
@@ -31,14 +31,15 @@ export interface CellExecutionInputs {
31
31
  readonly executions: Executions;
32
32
  readonly results: Results;
33
33
  /**
34
- * The execution each submitted cell had when its run was requested.
35
- *
36
- * A cell reads as queued until the host shows a newer execution for it.
34
+ * The cells of the confirmed document. A run can only reach a cell the
35
+ * document has, and the kernel sets aside one the document has disabled.
37
36
  */
38
- readonly submissions: ReadonlyMap<
39
- Protocol.CellId,
40
- Protocol.ExecutionId | null
41
- >;
37
+ readonly cells: ReadonlyMap<Protocol.CellId, Pick<Protocol.Cell, "config">>;
38
+ /**
39
+ * Cells this browser has asked to run, while the stream has not yet
40
+ * shown the run.
41
+ */
42
+ readonly requested: ReadonlySet<Protocol.CellId>;
42
43
  /** Cells the kernel reports as blocked by a disabled ancestor. */
43
44
  readonly blocked: ReadonlySet<Protocol.CellId>;
44
45
  }
@@ -66,14 +67,46 @@ function latestOf(
66
67
  }
67
68
 
68
69
  /**
69
- * The execution a submission is measured against: the cell's latest on the
70
- * stream, or its retained result once the stream has let that execution go.
70
+ * Whether a run can reach `id`, meaning the document has the cell and
71
+ * nothing keeps it from running.
71
72
  */
72
- export function latestExecutionId(
73
- inputs: Pick<CellExecutionInputs, "executions" | "results">,
74
- id: Protocol.CellId,
75
- ): Protocol.ExecutionId | null {
76
- return inputs.executions.latest.get(id) ?? inputs.results.get(id)?.id ?? null;
73
+ function runnable(inputs: CellExecutionInputs, id: Protocol.CellId): boolean {
74
+ const cell = inputs.cells.get(id);
75
+ return cell !== undefined && !cell.config.disabled && !inputs.blocked.has(id);
76
+ }
77
+
78
+ /**
79
+ * Whether `id` is waiting on a run that has not planned an execution for
80
+ * it yet.
81
+ *
82
+ * When the host accepts a run it names the cells that were requested, and
83
+ * this happens before the kernel plans their executions. Until the cell's
84
+ * execution in that run appears, the run itself is what makes the cell
85
+ * queued. A request this browser has made works the same way until its
86
+ * run shows up on the stream. The host keeps one execution per cell, so
87
+ * the cell's latest execution, or its retained result once the stream has
88
+ * let that execution go, tells which run has already planned it.
89
+ */
90
+ function awaited(inputs: CellExecutionInputs, id: Protocol.CellId): boolean {
91
+ if (!runnable(inputs, id)) {
92
+ return false;
93
+ }
94
+ if (inputs.requested.has(id)) {
95
+ return true;
96
+ }
97
+ const planned =
98
+ latestOf(inputs.executions, id)?.record.run_id ??
99
+ inputs.results.get(id)?.run_id;
100
+ for (const run of inputs.executions.runs.values()) {
101
+ if (
102
+ run.status !== "completed" &&
103
+ run.id !== planned &&
104
+ run.cells.includes(id)
105
+ ) {
106
+ return true;
107
+ }
108
+ }
109
+ return false;
77
110
  }
78
111
 
79
112
  export function cellExecutionView(
@@ -82,9 +115,6 @@ export function cellExecutionView(
82
115
  ): CellExecutionView {
83
116
  const latest = latestOf(inputs.executions, id);
84
117
  const result = inputs.results.get(id) ?? null;
85
- const submitted =
86
- inputs.submissions.has(id) &&
87
- inputs.submissions.get(id) === latestExecutionId(inputs, id);
88
118
  let status: CellRunStatus = "idle";
89
119
  let startedAt = result === null ? null : millis(result.started_at);
90
120
  let rendered = false;
@@ -92,7 +122,7 @@ export function cellExecutionView(
92
122
  status = latest.record.status;
93
123
  startedAt = millis(latest.record.started_at) ?? startedAt;
94
124
  rendered = latest.record.outcome !== null;
95
- } else if (submitted) {
125
+ } else if (awaited(inputs, id)) {
96
126
  status = "queued";
97
127
  }
98
128
  const settled = status === "idle";
@@ -126,7 +156,7 @@ export function anyRunning(executions: Executions): boolean {
126
156
  );
127
157
  }
128
158
 
129
- /** Cells and scratch work queued or running, plus submissions the host has not shown yet. */
159
+ /** Cells and scratch work queued or running, plus cells awaiting a run the kernel has not planned. */
130
160
  export function queuedOrRunningCount(inputs: CellExecutionInputs): number {
131
161
  const pending = unfinished(inputs.executions);
132
162
  const shown = new Set(
@@ -134,12 +164,17 @@ export function queuedOrRunningCount(inputs: CellExecutionInputs): number {
134
164
  execution.record.cell_id === null ? [] : [execution.record.cell_id],
135
165
  ),
136
166
  );
167
+ const named = new Set<Protocol.CellId>(inputs.requested);
168
+ for (const run of inputs.executions.runs.values()) {
169
+ if (run.status !== "completed") {
170
+ for (const cellId of run.cells) {
171
+ named.add(cellId);
172
+ }
173
+ }
174
+ }
137
175
  let awaiting = 0;
138
- for (const [cellId, executionId] of inputs.submissions) {
139
- if (
140
- !shown.has(cellId) &&
141
- executionId === latestExecutionId(inputs, cellId)
142
- ) {
176
+ for (const cellId of named) {
177
+ if (!shown.has(cellId) && awaited(inputs, cellId)) {
143
178
  awaiting += 1;
144
179
  }
145
180
  }