@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
@@ -371,6 +371,8 @@ export class StatefulOutputMessage implements OutputMessage {
371
371
  public readonly timestamp: OutputMessage["timestamp"];
372
372
  public readonly code: OutputMessage["code"];
373
373
  public readonly id: StringOutputMessage["id"];
374
+ /** The prompt this entry is, when it asks for input. A reply names it. */
375
+ public readonly input_id: OutputMessage["input_id"];
374
376
  private ansiReducer = new AnsiReducer();
375
377
  private _data: string;
376
378
 
@@ -388,6 +390,7 @@ export class StatefulOutputMessage implements OutputMessage {
388
390
  ansiReducer,
389
391
  code: message.code,
390
392
  id: message.id,
393
+ input_id: message.input_id,
391
394
  });
392
395
  }
393
396
 
@@ -398,6 +401,7 @@ export class StatefulOutputMessage implements OutputMessage {
398
401
  ansiReducer,
399
402
  code,
400
403
  id,
404
+ input_id,
401
405
  }: {
402
406
  mimetype: OutputMessage["mimetype"];
403
407
  channel: OutputMessage["channel"];
@@ -405,12 +409,14 @@ export class StatefulOutputMessage implements OutputMessage {
405
409
  ansiReducer: AnsiReducer;
406
410
  code: OutputMessage["code"];
407
411
  id: StringOutputMessage["id"];
412
+ input_id: OutputMessage["input_id"];
408
413
  }) {
409
414
  this.mimetype = mimetype;
410
415
  this.channel = channel;
411
416
  this.timestamp = timestamp;
412
417
  this.code = code;
413
418
  this.id = id;
419
+ this.input_id = input_id;
414
420
  this.ansiReducer = ansiReducer;
415
421
  this._data = this.ansiReducer.render();
416
422
  }
@@ -424,6 +430,7 @@ export class StatefulOutputMessage implements OutputMessage {
424
430
  ansiReducer: this.ansiReducer,
425
431
  code: this.code,
426
432
  id: this.id,
433
+ input_id: this.input_id,
427
434
  });
428
435
  }
429
436
 
@@ -47,7 +47,7 @@ vi.mock("@/components/editor/cell/useDeleteCell", () => ({
47
47
  }));
48
48
 
49
49
  vi.mock("@/core/network/requests", () => ({
50
- getRequestClient: () => ({ sendRun: vi.fn() }),
50
+ getRequestClient: () => ({}),
51
51
  }));
52
52
 
53
53
  vi.mock("../../codemirror/language/cell-code", () => ({
@@ -92,6 +92,7 @@ exports[`cell reducer > can initialize stdin > second stdin requested 1`] = `
92
92
  "channel": "stdin",
93
93
  "data": "what is your name?",
94
94
  "id": 2,
95
+ "input_id": undefined,
95
96
  "mimetype": "text/plain",
96
97
  "response": "Marimo!",
97
98
  "timestamp": 2,
@@ -132,6 +133,7 @@ exports[`cell reducer > can initialize stdin > stdin answered 1`] = `
132
133
  "channel": "stdin",
133
134
  "data": "what is your name?",
134
135
  "id": 2,
136
+ "input_id": undefined,
135
137
  "mimetype": "text/plain",
136
138
  "response": "Marimo!",
137
139
  "timestamp": 2,
@@ -18,11 +18,6 @@ import {
18
18
  vi,
19
19
  } from "vitest";
20
20
  import { MockExecutions } from "@/__mocks__/executions";
21
- import {
22
- notebookQueuedOrRunningCountAtom,
23
- submitCells,
24
- } from "@/core/executions";
25
- import { store } from "@/core/state/jotai";
26
21
  import { MockNotebook } from "@/__mocks__/notebook";
27
22
  import { cellId } from "@/__tests__/branded";
28
23
  import type { CellHandle } from "@/components/editor/notebook-cell";
@@ -1109,15 +1104,6 @@ describe("cell reducer", () => {
1109
1104
  expect(handleAtom2).toBe(handleAtom1);
1110
1105
  });
1111
1106
  });
1112
- it("withdraws the submission of a cell that leaves the document", () => {
1113
- MockExecutions.seed(store, []);
1114
- submitCells(store, [firstCellId]);
1115
- expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(1);
1116
-
1117
- actions.reconcileDocument({ order: [], cells: new Map() });
1118
-
1119
- expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(0);
1120
- });
1121
1107
  });
1122
1108
 
1123
1109
  describe("createTracebackInfoAtom", () => {
@@ -759,3 +759,18 @@ describe("collapseConsoleOutputs", () => {
759
759
  });
760
760
  });
761
761
  });
762
+
763
+ it("keeps a prompt's input id when the entry becomes stateful", () => {
764
+ const prompt: OutputMessage = {
765
+ channel: "stdin",
766
+ mimetype: "text/plain",
767
+ data: "Name: ",
768
+ timestamp: 1,
769
+ input_id: "prompt-1",
770
+ };
771
+
772
+ const [collapsed] = collapseConsoleOutputs([prompt]);
773
+
774
+ expect(collapsed.input_id).toBe("prompt-1");
775
+ expect(collapsed.channel).toBe("stdin");
776
+ });
@@ -131,7 +131,7 @@ describe("staleCellIds with a run in progress", () => {
131
131
  const state = notebook(cells);
132
132
  const ran = MockExecutions.completed("cell1");
133
133
  const executions = MockExecutions.inputs([ran], {
134
- submissions: new Map([["cell1", ran.id]]),
134
+ requested: new Set(["cell1"]),
135
135
  });
136
136
 
137
137
  expect(staleCellIds(state, executions, freshness(cells))).toEqual([]);
@@ -1,6 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { getRequestClient } from "../network/requests";
3
+ import { runCells } from "../executions";
4
4
  import { store } from "../state/jotai";
5
5
  import { variablesAtom } from "../variables/state";
6
6
  import { notebookAtom } from "./cells";
@@ -124,10 +124,7 @@ function addImport({
124
124
  });
125
125
  newCellId = created === null ? null : id;
126
126
  if (autoInstantiate && newCellId !== null) {
127
- void getRequestClient().sendRun({
128
- cellIds: [newCellId],
129
- codes: [importStatement],
130
- });
127
+ void runCells(store, [newCellId]);
131
128
  }
132
129
  },
133
130
  });
@@ -72,6 +72,7 @@ export function transitionCell(
72
72
  data: output.data,
73
73
  timestamp: output.timestamp,
74
74
  id: output.id,
75
+ input_id: output.input_id,
75
76
  response: output.response ?? "",
76
77
  };
77
78
  }
@@ -10,7 +10,6 @@ import {
10
10
  cellExecutionInputsAtom,
11
11
  cellExecutionView,
12
12
  cellFreshnessAtom,
13
- withdrawCells,
14
13
  } from "@/core/executions";
15
14
  import type { CellColumnId } from "./layout";
16
15
  import { invariant } from "@/utils/invariant";
@@ -218,6 +217,7 @@ const {
218
217
  data: stdinOutput.data,
219
218
  timestamp: stdinOutput.timestamp,
220
219
  id: stdinOutput.id,
220
+ input_id: stdinOutput.input_id,
221
221
  // An empty response resolves the prompt without retaining the secret.
222
222
  response: stdinOutput.mimetype === "text/password" ? "" : response,
223
223
  };
@@ -550,7 +550,6 @@ export function releaseCellAtoms(cellId: CellId) {
550
550
  cellDataAtom.remove(cellId);
551
551
  cellRuntimeAtom.remove(cellId);
552
552
  cellHandleAtom.remove(cellId);
553
- withdrawCells(store, [cellId]);
554
553
  }
555
554
 
556
555
  /** Subscribes to reactive updates of the cell's data. */
@@ -4,16 +4,49 @@ import { createStore } from "jotai";
4
4
  import { describe, expect, it } from "vitest";
5
5
  import { MockExecutions } from "@/__mocks__/executions";
6
6
  import { cellId } from "@/__tests__/branded";
7
+ import type * as Protocol from "@marimo-team/client/protocol";
7
8
  import type { CellMessage } from "@/core/kernel/messages";
8
9
  import {
10
+ document as documentOf,
11
+ withCells,
12
+ } from "@/core/notebook/document/__tests__/fixtures";
13
+ import type { Executions } from "@/core/notebook/replica";
14
+ import {
15
+ acceptRun,
9
16
  cellExecutionInputsAtom,
10
17
  notebookQueuedOrRunningCountAtom,
11
18
  observeCellMessage,
12
- submitCells,
19
+ requestRun,
20
+ type RunRequest,
21
+ withdrawRequest,
13
22
  } from "../atoms";
14
23
  import { cellExecutionView } from "../select";
15
24
 
16
25
  const C1 = cellId("c1");
26
+ const C2 = cellId("c2");
27
+
28
+ type Store = ReturnType<typeof createStore>;
29
+
30
+ function run(
31
+ cells: Protocol.CellId[],
32
+ status: Protocol.Run["status"] = "queued",
33
+ id = "run-1",
34
+ ): Protocol.Run {
35
+ return {
36
+ id,
37
+ notebook_id: "n1",
38
+ runtime_id: "r1",
39
+ cells,
40
+ status,
41
+ outcome: status === "completed" ? "succeeded" : null,
42
+ started_at: null,
43
+ completed_at: null,
44
+ };
45
+ }
46
+
47
+ function withRuns(executions: Executions, runs: Protocol.Run[]): Executions {
48
+ return { ...executions, runs: new Map(runs.map((r) => [r.id, r])) };
49
+ }
17
50
 
18
51
  function cellOp(status: CellMessage["status"]): CellMessage {
19
52
  return {
@@ -25,14 +58,83 @@ function cellOp(status: CellMessage["status"]): CellMessage {
25
58
  };
26
59
  }
27
60
 
28
- describe("observeCellMessage", () => {
29
- it("withdraws a submission once the kernel reports the cell blocked", () => {
61
+ /** A store whose replica shows the two-cell fixture document. */
62
+ function shown() {
63
+ const store = createStore();
64
+ const replica = MockExecutions.seed(store, [], null, documentOf());
65
+ return { store, replica };
66
+ }
67
+
68
+ function request(store: Store, cells: Protocol.CellId[]): RunRequest {
69
+ const made = requestRun(store, cells);
70
+ if (made === null) {
71
+ throw new Error("no notebook is shown");
72
+ }
73
+ return made;
74
+ }
75
+
76
+ function statusOf(store: Store, id = C1) {
77
+ return cellExecutionView(store.get(cellExecutionInputsAtom), id).status;
78
+ }
79
+
80
+ function requested(store: Store) {
81
+ return [...store.get(cellExecutionInputsAtom).requested];
82
+ }
83
+
84
+ describe("requestRun", () => {
85
+ it("reads the cells as queued until the request is withdrawn", () => {
86
+ const { store } = shown();
87
+ const made = request(store, [C1, C2]);
88
+ expect(statusOf(store, C1)).toBe("queued");
89
+ expect(statusOf(store, C2)).toBe("queued");
90
+ expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(2);
91
+
92
+ withdrawRequest(store, made);
93
+
94
+ expect(statusOf(store, C1)).toBe("idle");
95
+ expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(0);
96
+ });
97
+
98
+ it("records nothing while no notebook is shown", () => {
30
99
  const store = createStore();
31
- MockExecutions.seed(store, []);
32
- submitCells(store, [C1]);
33
- expect(
34
- cellExecutionView(store.get(cellExecutionInputsAtom), C1).status,
35
- ).toBe("queued");
100
+ const replica = MockExecutions.seed(store, [], null, documentOf());
101
+ store.set(replica.notebookAtom, null);
102
+
103
+ expect(requestRun(store, [C1])).toBeNull();
104
+ expect(statusOf(store)).toBe("idle");
105
+ });
106
+
107
+ it("stops showing a requested cell that leaves the document", () => {
108
+ const { store, replica } = shown();
109
+ request(store, [C1]);
110
+ expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(1);
111
+
112
+ store.set(replica.documentAtom, withCells(documentOf(), { c1: null }));
113
+
114
+ expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(0);
115
+ });
116
+
117
+ it("forgets requests and blocked cells with the notebook they were made in", () => {
118
+ const { store, replica } = shown();
119
+ request(store, [C1]);
120
+ observeCellMessage(store, {
121
+ ...cellOp("disabled-transitively"),
122
+ cell_id: C2,
123
+ });
124
+
125
+ store.set(replica.notebookAtom, MockExecutions.notebook("n2"));
126
+
127
+ expect(requested(store)).toEqual([]);
128
+ expect(statusOf(store)).toBe("idle");
129
+ expect(store.get(cellExecutionInputsAtom).blocked.size).toBe(0);
130
+ });
131
+ });
132
+
133
+ describe("observeCellMessage", () => {
134
+ it("does not show a requested cell as queued once the kernel reports it blocked", () => {
135
+ const { store } = shown();
136
+ request(store, [C1]);
137
+ expect(statusOf(store)).toBe("queued");
36
138
 
37
139
  observeCellMessage(store, cellOp("disabled-transitively"));
38
140
 
@@ -42,13 +144,12 @@ describe("observeCellMessage", () => {
42
144
  expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(0);
43
145
  });
44
146
 
45
- it("forgets submissions and blocked cells with the replica they were made for", () => {
46
- const store = createStore();
47
- MockExecutions.seed(store, []);
48
- submitCells(store, [C1]);
147
+ it("forgets requests and blocked cells with the replica they were made for", () => {
148
+ const { store } = shown();
149
+ request(store, [C1]);
49
150
  observeCellMessage(store, cellOp("disabled-transitively"));
50
151
 
51
- MockExecutions.seed(store, []);
152
+ MockExecutions.seed(store, [], null, documentOf());
52
153
 
53
154
  const view = cellExecutionView(store.get(cellExecutionInputsAtom), C1);
54
155
  expect(view.status).toBe("idle");
@@ -56,43 +157,126 @@ describe("observeCellMessage", () => {
56
157
  expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(0);
57
158
  });
58
159
 
59
- it("keeps a submission while the cell is merely queued", () => {
60
- const store = createStore();
61
- MockExecutions.seed(store, []);
62
- submitCells(store, [C1]);
160
+ it("keeps a request while the cell is merely queued", () => {
161
+ const { store } = shown();
162
+ request(store, [C1]);
63
163
 
64
164
  observeCellMessage(store, cellOp("queued"));
65
165
 
66
- expect(
67
- cellExecutionView(store.get(cellExecutionInputsAtom), C1).status,
68
- ).toBe("queued");
166
+ expect(statusOf(store)).toBe("queued");
69
167
  });
168
+ });
70
169
 
71
- it("forgets acknowledged submissions and blocked cells when the replica follows a new notebook", () => {
72
- const store = createStore();
73
- const replica = MockExecutions.seed(store, []);
74
- submitCells(store, [C1]);
75
- observeCellMessage(store, {
76
- ...cellOp("disabled-transitively"),
77
- cell_id: cellId("c2"),
78
- });
170
+ describe("acceptRun", () => {
171
+ it("hands the request over to the run once the stream shows it", () => {
172
+ const { store, replica } = shown();
173
+ const made = request(store, [C1]);
174
+ acceptRun(store, made, run([C1]));
175
+ expect(statusOf(store)).toBe("queued");
176
+ expect(requested(store)).toEqual([C1]);
79
177
 
80
- const completed = MockExecutions.inputs([MockExecutions.completed(C1)]);
81
- store.set(replica.executionsAtom, completed.executions);
82
- store.set(replica.resultsAtom, completed.results);
83
- expect(
84
- cellExecutionView(store.get(cellExecutionInputsAtom), C1).status,
85
- ).toBe("idle");
86
-
87
- const empty = MockExecutions.inputs([]);
88
- store.set(replica.executionsAtom, empty.executions);
89
- store.set(replica.resultsAtom, empty.results);
90
- store.set(replica.notebookAtom, {
91
- ...store.get(replica.notebookAtom),
92
- id: "n2",
93
- });
178
+ // The response landed first, and the run event follows.
179
+ const stream = store.get(replica.executionsAtom);
180
+ store.set(replica.executionsAtom, withRuns(stream, [run([C1])]));
181
+ expect(requested(store)).toEqual([]);
182
+ expect(statusOf(store)).toBe("queued");
183
+ expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(1);
94
184
 
95
- expect(store.get(cellExecutionInputsAtom)).toEqual(empty);
185
+ // The run ends without ever planning the cell.
186
+ store.set(
187
+ replica.executionsAtom,
188
+ withRuns(stream, [run([C1], "completed")]),
189
+ );
190
+ expect(statusOf(store)).toBe("idle");
96
191
  expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(0);
97
192
  });
193
+
194
+ it("hands over at once when the stream showed the run first", () => {
195
+ const { store, replica } = shown();
196
+ const made = request(store, [C1]);
197
+ const stream = store.get(replica.executionsAtom);
198
+ store.set(replica.executionsAtom, withRuns(stream, [run([C1])]));
199
+
200
+ acceptRun(store, made, run([C1]));
201
+
202
+ expect(requested(store)).toEqual([]);
203
+ expect(statusOf(store)).toBe("queued");
204
+ });
205
+
206
+ it("leaves a later request for the same cell when an earlier run ends", () => {
207
+ const { store, replica } = shown();
208
+ const first = request(store, [C1]);
209
+ const second = request(store, [C1]);
210
+ acceptRun(store, first, run([C1]));
211
+
212
+ const stream = store.get(replica.executionsAtom);
213
+ store.set(replica.executionsAtom, withRuns(stream, [run([C1])]));
214
+ expect(requested(store)).toEqual([C1]);
215
+ store.set(
216
+ replica.executionsAtom,
217
+ withRuns(stream, [run([C1], "completed")]),
218
+ );
219
+ expect(statusOf(store)).toBe("queued");
220
+
221
+ acceptRun(store, second, run([C1], "queued", "run-2"));
222
+ store.set(
223
+ replica.executionsAtom,
224
+ withRuns(stream, [run([C1], "completed"), run([C1], "queued", "run-2")]),
225
+ );
226
+ expect(requested(store)).toEqual([]);
227
+ expect(statusOf(store)).toBe("queued");
228
+
229
+ store.set(
230
+ replica.executionsAtom,
231
+ withRuns(stream, [run([C1], "completed", "run-2")]),
232
+ );
233
+ expect(statusOf(store)).toBe("idle");
234
+ });
235
+
236
+ it("keeps waiting while the stream is between a reset and its snapshot", () => {
237
+ const { store, replica } = shown();
238
+ const made = request(store, [C1]);
239
+ acceptRun(store, made, run([C1]));
240
+
241
+ const notebook = store.get(replica.notebookAtom);
242
+ store.set(replica.notebookAtom, null);
243
+ const stream = store.get(replica.executionsAtom);
244
+ store.set(replica.executionsAtom, { ...stream });
245
+ store.set(replica.notebookAtom, notebook);
246
+ expect(requested(store)).toEqual([C1]);
247
+ expect(statusOf(store)).toBe("queued");
248
+
249
+ store.set(replica.executionsAtom, withRuns(stream, [run([C1])]));
250
+ expect(requested(store)).toEqual([]);
251
+ expect(statusOf(store)).toBe("queued");
252
+ });
253
+
254
+ it("drops the request once the replica shows another notebook", () => {
255
+ const { store, replica } = shown();
256
+ const made = request(store, [C1]);
257
+ acceptRun(store, made, run([C1]));
258
+
259
+ store.set(replica.notebookAtom, MockExecutions.notebook("n2"));
260
+ const stream = store.get(replica.executionsAtom);
261
+ store.set(replica.executionsAtom, withRuns(stream, [run([C1])]));
262
+ store.set(replica.notebookAtom, MockExecutions.notebook("n1"));
263
+
264
+ expect(requested(store)).toEqual([]);
265
+ });
266
+
267
+ it("stops following a run once the replica changes", () => {
268
+ const { store, replica } = shown();
269
+ const made = request(store, [C1]);
270
+ acceptRun(store, made, run([C1]));
271
+
272
+ MockExecutions.seed(store, [], null, documentOf());
273
+ request(store, [C1]);
274
+ const stream = store.get(replica.executionsAtom);
275
+ store.set(
276
+ replica.executionsAtom,
277
+ withRuns(stream, [run([C1], "completed")]),
278
+ );
279
+
280
+ expect(statusOf(store)).toBe("queued");
281
+ });
98
282
  });
@@ -143,8 +143,7 @@ describe("useCellOutput", () => {
143
143
  expect(result.current).toBeNull();
144
144
 
145
145
  act(() => {
146
- const notebook = store.get(replica.notebookAtom);
147
- store.set(replica.notebookAtom, { ...notebook, id: "n2" });
146
+ store.set(replica.notebookAtom, MockExecutions.notebook("n2"));
148
147
  });
149
148
  expect(result.current?.data).toBe("c1 value");
150
149
  });
@@ -0,0 +1,86 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { MockExecutions } from "@/__mocks__/executions";
5
+ import { document as documentOf } from "@/core/notebook/document/__tests__/fixtures";
6
+ import {
7
+ type FakeInput,
8
+ fakeBackend,
9
+ } from "@/core/notebook/replica/__tests__/fixtures";
10
+ import { activeNotebookReplicaAtom } from "@/core/session/active-notebook";
11
+ import { store } from "@/core/state/jotai";
12
+ import { replyInput } from "../input";
13
+
14
+ const toast = vi.fn();
15
+ vi.mock("@/components/ui/use-toast", () => ({
16
+ toast: (...args: unknown[]) => toast(...args),
17
+ }));
18
+
19
+ describe("replyInput", () => {
20
+ let input: ReturnType<typeof vi.fn<FakeInput>>;
21
+
22
+ beforeEach(() => {
23
+ toast.mockClear();
24
+ input = vi.fn<FakeInput>(async (_notebookId, inputId) => ({
25
+ ok: true,
26
+ status: 200,
27
+ data: {
28
+ id: inputId,
29
+ execution_id: "e1",
30
+ seq: 0,
31
+ prompt: "Name: ",
32
+ password: false,
33
+ status: "answered",
34
+ },
35
+ }));
36
+ const replica = MockExecutions.seed(
37
+ store,
38
+ [],
39
+ MockExecutions.runtime("running"),
40
+ documentOf(),
41
+ );
42
+ store.set(replica.backendAtom, fakeBackend({ input }));
43
+ });
44
+
45
+ afterEach(() => {
46
+ store.set(activeNotebookReplicaAtom, null);
47
+ });
48
+
49
+ it("answers the prompt by id in the connected notebook", async () => {
50
+ await replyInput(store, "prompt-1", "marimo");
51
+
52
+ expect(input).toHaveBeenCalledWith("n1", "prompt-1", { value: "marimo" });
53
+ expect(toast).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it("reports a reply the host refuses", async () => {
57
+ input.mockResolvedValue({
58
+ ok: false,
59
+ status: 409,
60
+ problem: {
61
+ type: "tag:marimo.io,2026:host/conflict",
62
+ title: "Conflict",
63
+ status: 409,
64
+ detail: "input prompt-1 has already ended.",
65
+ },
66
+ });
67
+
68
+ await replyInput(store, "prompt-1", "marimo");
69
+
70
+ expect(toast).toHaveBeenCalledWith(
71
+ expect.objectContaining({
72
+ title: "Failed to send input",
73
+ description: "input prompt-1 has already ended.",
74
+ }),
75
+ );
76
+ });
77
+
78
+ it("does nothing without a connected notebook", async () => {
79
+ store.set(activeNotebookReplicaAtom, null);
80
+
81
+ await replyInput(store, "prompt-1", "marimo");
82
+
83
+ expect(input).not.toHaveBeenCalled();
84
+ expect(toast).not.toHaveBeenCalled();
85
+ });
86
+ });