@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
@@ -2,7 +2,7 @@ import { i as e } from "./chunk-75Z2AOVW-9jQIEAxd.js";
2
2
  import { n as t } from "./src-D5uZaZib.js";
3
3
  import { n } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
4
4
  import { D as r, H as i, K as a, U as o, a as s, b as c, c as l, v as u, w as d, x as f, y as p } from "./chunk-DU6HZSFF-B20LZ-hE.js";
5
- import { p as m } from "./mermaid-DMPMkVqY.js";
5
+ import { p as m } from "./mermaid-CNgH43ED.js";
6
6
  import { n as h } from "./mermaid-parser.core-DtOnl7m4.js";
7
7
  import { t as g } from "./chunk-JWPE2WC7-DJvv1t8U.js";
8
8
  //#region ../node_modules/.pnpm/mermaid@11.17.2/node_modules/mermaid/dist/chunks/mermaid.core/wardleyDiagram-VM6X3IG4.mjs
@@ -9,7 +9,7 @@ import { n as s } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
9
9
  import { D as c, H as l, K as u, U as d, a as f, b as p, c as m, f as h, v as g, w as _, y as v, z as y } from "./chunk-DU6HZSFF-B20LZ-hE.js";
10
10
  import "./chunk-PWAF6VOD-oT_Qd2OU.js";
11
11
  import { t as b } from "./chunk-GMAD6QVW-DokpSFdf.js";
12
- import { p as x } from "./mermaid-DMPMkVqY.js";
12
+ import { p as x } from "./mermaid-CNgH43ED.js";
13
13
  //#region ../node_modules/.pnpm/d3-scale@4.0.2/node_modules/d3-scale/src/band.js
14
14
  function S() {
15
15
  var e = t().unknown(void 0), r = e.domain, i = e.range, o = 0, s = 1, c, l, u = !1, d = 0, f = 0, p = .5;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.25.3-dev308",
3
+ "version": "0.25.3-dev310",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -11,6 +11,7 @@ import {
11
11
  type Document,
12
12
  type Execution,
13
13
  type Executions,
14
+ type NotebookBackend,
14
15
  type NotebookReplica,
15
16
  type Results,
16
17
  } from "@/core/notebook/replica";
@@ -18,6 +19,12 @@ import { activeNotebookReplicaAtom } from "@/core/session/active-notebook";
18
19
 
19
20
  type Store = ReturnType<typeof createStore>;
20
21
 
22
+ const CONFIG: Protocol.CellConfig = {
23
+ column: null,
24
+ disabled: false,
25
+ hide_code: false,
26
+ expand_output: false,
27
+ };
21
28
  const T0 = "2026-10-01T09:20:11Z";
22
29
  const T1 = "2026-10-01T09:20:12.500Z";
23
30
 
@@ -138,6 +145,17 @@ export const MockExecutions = {
138
145
  });
139
146
  },
140
147
 
148
+ notebook(id: Protocol.NotebookId = "n1"): Protocol.Notebook {
149
+ return {
150
+ id,
151
+ project_id: "p1",
152
+ path: null,
153
+ name: "Notebook",
154
+ operations: [],
155
+ updated_at: null,
156
+ };
157
+ },
158
+
141
159
  runtime(status: Protocol.RuntimeStatus): Protocol.Runtime {
142
160
  return {
143
161
  id: "r1",
@@ -149,15 +167,24 @@ export const MockExecutions = {
149
167
  };
150
168
  },
151
169
 
152
- /** What the selectors read, with `latest` and the results derived from `records`. */
170
+ /**
171
+ * What the selectors read. `latest` and the results are derived from
172
+ * `records`, and the document is taken to have every cell they name,
173
+ * with nothing keeping it from running.
174
+ */
153
175
  inputs(
154
176
  records: Protocol.Execution[],
155
- extra: Partial<Pick<CellExecutionInputs, "submissions" | "blocked">> = {},
177
+ extra: Partial<Pick<CellExecutionInputs, "requested" | "blocked">> = {},
156
178
  ): CellExecutionInputs {
157
179
  return {
158
180
  executions: mirror(records),
159
181
  results: results(records),
160
- submissions: new Map(),
182
+ cells: new Map(
183
+ records.flatMap((record) =>
184
+ record.cell_id === null ? [] : [[record.cell_id, { config: CONFIG }]],
185
+ ),
186
+ ),
187
+ requested: new Set(),
161
188
  blocked: new Set(),
162
189
  ...extra,
163
190
  };
@@ -172,20 +199,15 @@ export const MockExecutions = {
172
199
  ) {
173
200
  const current = store.get(activeNotebookReplicaAtom);
174
201
  const replica = {
175
- connectionAtom: atom<ConnectionStatus>({ state: ConnectionState.OPEN }),
176
- backendAtom: atom(null),
177
202
  attachmentsAtom: atom([]),
178
203
  connect: () => () => undefined,
179
204
  reconnect: () => Promise.resolve(),
180
205
  ...current,
181
- notebookAtom: atom<Protocol.Notebook>({
182
- id: "n1",
183
- project_id: "p1",
184
- path: null,
185
- name: "Notebook",
186
- operations: [],
187
- updated_at: null,
188
- }),
206
+ // Settable, so a test can close the connection or give the replica a
207
+ // backend to command.
208
+ connectionAtom: atom<ConnectionStatus>({ state: ConnectionState.OPEN }),
209
+ backendAtom: atom<NotebookBackend | null>(null),
210
+ notebookAtom: atom<Protocol.Notebook | null>(MockExecutions.notebook()),
189
211
  documentAtom: atom<Document | null>(document),
190
212
  runtimeAtom: atom(runtime),
191
213
  executionsAtom: atom(mirror(records)),
@@ -23,7 +23,6 @@ export const MockRequestClient = {
23
23
  sendFormat: vi.fn().mockResolvedValue({ codes: {} }),
24
24
  sendInterrupt: vi.fn().mockResolvedValue({}),
25
25
  sendShutdown: vi.fn().mockResolvedValue({}),
26
- sendRun: vi.fn().mockResolvedValue({}),
27
26
  sendRunScratchpad: vi.fn().mockResolvedValue({}),
28
27
  sendInstantiate: vi.fn().mockResolvedValue({}),
29
28
  sendCodeCompletionRequest: vi.fn().mockResolvedValue({ items: [] }),
@@ -1,169 +1,154 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import type * as Protocol from "@marimo-team/client/protocol";
3
4
  import { act, renderHook } from "@testing-library/react";
4
- import { Provider } from "jotai";
5
+ import { atom, Provider } from "jotai";
5
6
  import type { ReactNode } from "react";
6
- import { afterEach, describe, expect, it, vi } from "vitest";
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
8
  import { MockExecutions } from "@/__mocks__/executions";
8
9
  import { MockNotebook } from "@/__mocks__/notebook";
9
- import { MockRequestClient } from "@/__mocks__/requests";
10
10
  import { cellId } from "@/__tests__/branded";
11
11
  import { notebookAtom } from "@/core/cells/cells";
12
- import {
13
- cellExecutionInputsAtom,
14
- cellExecutionView,
15
- notebookQueuedOrRunningCountAtom,
16
- } from "@/core/executions";
17
- import { requestClientAtom } from "@/core/network/requests";
18
12
  import type { DocumentModel } from "@/core/notebook/document";
19
13
  import {
20
14
  cell,
21
15
  document as documentOf,
22
16
  withCells,
23
17
  } from "@/core/notebook/document/__tests__/fixtures";
18
+ import {
19
+ type FakeRun,
20
+ fakeBackend,
21
+ } from "@/core/notebook/replica/__tests__/fixtures";
24
22
  import { activeDocumentModelAtom } from "@/core/session/active-notebook";
25
23
  import { store } from "@/core/state/jotai";
26
- import { runCells, useRunStaleCells } from "../useRunCells";
27
-
28
- const ENABLED = cellId("enabled");
29
- const DISABLED = cellId("disabled");
30
-
31
- describe("runCells", () => {
32
- it("sends a disabled cell to the kernel without reading it as queued", async () => {
33
- MockExecutions.seed(store, []);
34
- const notebook = MockNotebook.notebookState({
35
- cellData: {
36
- [ENABLED]: { code: "x = 1" },
37
- [DISABLED]: { code: "y = 2", config: { disabled: true } },
38
- },
39
- });
40
- const sendRun = vi.fn(async () => null);
41
-
42
- await runCells({
43
- cellIds: [ENABLED, DISABLED],
44
- sendRun,
45
- prepareForRun: vi.fn(),
46
- notebook,
47
- });
48
-
49
- expect(sendRun).toHaveBeenCalledWith({
50
- cellIds: [ENABLED, DISABLED],
51
- codes: ["x = 1", "y = 2"],
52
- });
53
- const inputs = store.get(cellExecutionInputsAtom);
54
- expect(cellExecutionView(inputs, ENABLED).status).toBe("queued");
55
- expect(cellExecutionView(inputs, DISABLED).status).toBe("idle");
56
- expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(1);
57
- });
58
- });
24
+ import { useRunStaleCells } from "../useRunCells";
25
+
26
+ const toast = vi.fn();
27
+ vi.mock("@/components/ui/use-toast", () => ({
28
+ toast: (...args: unknown[]) => toast(...args),
29
+ }));
30
+ vi.mock("@/core/runtime/config", () => ({
31
+ startRuntimeIfLazy: async () => undefined,
32
+ }));
33
+
34
+ const C1 = cellId("c1");
35
+ const C2 = cellId("c2");
36
+
37
+ const wrapper = ({ children }: { children: ReactNode }) => (
38
+ <Provider store={store}>{children}</Provider>
39
+ );
40
+
41
+ function accepted(cells: Protocol.CellId[]): Protocol.Run {
42
+ return {
43
+ id: "run-1",
44
+ notebook_id: "n1",
45
+ runtime_id: "r1",
46
+ cells,
47
+ status: "queued",
48
+ outcome: null,
49
+ started_at: null,
50
+ completed_at: null,
51
+ };
52
+ }
59
53
 
60
54
  describe("useRunStaleCells", () => {
61
- const C1 = cellId("c1");
62
- const C2 = cellId("c2");
55
+ let run: ReturnType<typeof vi.fn<FakeRun>>;
56
+
57
+ beforeEach(() => {
58
+ toast.mockClear();
59
+ run = vi.fn<FakeRun>(async (_notebookId, request) => ({
60
+ ok: true,
61
+ status: 202,
62
+ data: accepted(request.cells?.map((cell) => cell.id) ?? []),
63
+ }));
64
+ });
63
65
 
64
66
  afterEach(() => {
65
67
  store.set(activeDocumentModelAtom, null);
66
68
  });
67
69
 
68
- it("flushes pending edits before choosing the stale cells", async () => {
70
+ function seed(records: Protocol.Execution[]) {
69
71
  const document = documentOf();
70
72
  const replica = MockExecutions.seed(
71
73
  store,
72
- [
73
- MockExecutions.completed(C1, { code_fingerprint: "c1-hash" }),
74
- MockExecutions.completed(C2, { code_fingerprint: "c2-hash" }),
75
- ],
76
- null,
74
+ records,
75
+ MockExecutions.runtime("running"),
77
76
  document,
78
77
  );
78
+ store.set(replica.backendAtom, fakeBackend({ run }));
79
+ return { replica, document };
80
+ }
81
+
82
+ function model(sync: () => Promise<Protocol.Document>) {
83
+ store.set(activeDocumentModelAtom, {
84
+ retry: sync,
85
+ documentAtom: atom(null),
86
+ } as unknown as DocumentModel);
87
+ }
88
+
89
+ it("syncs pending edits before choosing the stale cells", async () => {
90
+ const { replica, document } = seed([
91
+ MockExecutions.completed(C1, { code_fingerprint: "c1-hash" }),
92
+ MockExecutions.completed(C2, { code_fingerprint: "c2-hash" }),
93
+ ]);
79
94
  store.set(
80
95
  notebookAtom,
81
96
  MockNotebook.notebookState({
82
97
  cellData: { [C1]: { code: "x = 2" }, [C2]: { code: "y = 2" } },
83
98
  }),
84
99
  );
85
- const client = MockRequestClient.create();
86
- store.set(requestClientAtom, client);
87
- // The edit to c1 is still in flight: the host confirms it on flush.
88
- const flush = vi.fn(async () => {
100
+ // The edit to c1 is still in flight: the host confirms it on sync.
101
+ const sync = vi.fn(async () => {
89
102
  await new Promise((resolve) => setTimeout(resolve, 0));
90
- store.set(
91
- replica.documentAtom,
92
- withCells(document, {
93
- [C1]: cell(C1, "x = 2", { code_fingerprint: "c1-hash-2" }),
94
- }),
95
- );
103
+ const confirmed = withCells(document, {
104
+ [C1]: cell(C1, "x = 2", { code_fingerprint: "c1-hash-2" }),
105
+ });
106
+ store.set(replica.documentAtom, confirmed);
107
+ return confirmed.revision;
96
108
  });
97
- store.set(activeDocumentModelAtom, { flush } as unknown as DocumentModel);
98
- const wrapper = ({ children }: { children: ReactNode }) => (
99
- <Provider store={store}>{children}</Provider>
100
- );
109
+ model(sync);
101
110
  const { result } = renderHook(useRunStaleCells, { wrapper });
102
111
 
103
112
  await act(() => result.current());
104
113
 
105
- expect(flush).toHaveBeenCalledOnce();
106
- expect(client.sendRun).toHaveBeenCalledWith({
107
- cellIds: [C1],
108
- codes: ["x = 2"],
109
- });
114
+ expect(sync).toHaveBeenCalled();
115
+ expect(run).toHaveBeenCalledWith("n1", { cells: [{ id: C1 }] });
110
116
  });
111
117
 
112
118
  it("does nothing when the notebook changed while the flush was pending", async () => {
113
- MockExecutions.seed(
114
- store,
115
- [MockExecutions.completed(C1, { code_fingerprint: "stale" })],
116
- null,
117
- documentOf(),
118
- );
119
+ seed([MockExecutions.completed(C1, { code_fingerprint: "stale" })]);
119
120
  store.set(
120
121
  notebookAtom,
121
122
  MockNotebook.notebookState({ cellData: { [C1]: { code: "x = 1" } } }),
122
123
  );
123
- const client = MockRequestClient.create();
124
- store.set(requestClientAtom, client);
125
- const flush = vi.fn(async () => {
124
+ model(async () => {
126
125
  store.set(activeDocumentModelAtom, null);
127
126
  throw new Error("aborted");
128
127
  });
129
- store.set(activeDocumentModelAtom, { flush } as unknown as DocumentModel);
130
- const wrapper = ({ children }: { children: ReactNode }) => (
131
- <Provider store={store}>{children}</Provider>
132
- );
133
128
  const { result } = renderHook(useRunStaleCells, { wrapper });
134
129
 
135
130
  await act(() => result.current());
136
131
 
137
- expect(client.sendRun).not.toHaveBeenCalled();
132
+ expect(run).not.toHaveBeenCalled();
133
+ expect(toast).not.toHaveBeenCalled();
138
134
  });
139
135
 
140
- it("still runs what is stale when the flush fails", async () => {
141
- MockExecutions.seed(
142
- store,
143
- [MockExecutions.completed(C1, { code_fingerprint: "stale" })],
144
- null,
145
- documentOf(),
146
- );
136
+ it("runs nothing while edits cannot reach the host", async () => {
137
+ seed([MockExecutions.completed(C1, { code_fingerprint: "stale" })]);
147
138
  store.set(
148
139
  notebookAtom,
149
140
  MockNotebook.notebookState({ cellData: { [C1]: { code: "x = 1" } } }),
150
141
  );
151
- const client = MockRequestClient.create();
152
- store.set(requestClientAtom, client);
153
- const flush = vi.fn(async () => {
142
+ model(async () => {
154
143
  throw new Error("offline");
155
144
  });
156
- store.set(activeDocumentModelAtom, { flush } as unknown as DocumentModel);
157
- const wrapper = ({ children }: { children: ReactNode }) => (
158
- <Provider store={store}>{children}</Provider>
159
- );
160
145
  const { result } = renderHook(useRunStaleCells, { wrapper });
161
146
 
162
147
  await act(() => result.current());
163
148
 
164
- expect(client.sendRun).toHaveBeenCalledWith({
165
- cellIds: [C1],
166
- codes: ["x = 1"],
167
- });
149
+ expect(run).not.toHaveBeenCalled();
150
+ expect(toast).toHaveBeenCalledWith(
151
+ expect.objectContaining({ title: "Edits not yet synced" }),
152
+ );
168
153
  });
169
154
  });
@@ -4,22 +4,21 @@ import { closeCompletion } from "@codemirror/autocomplete";
4
4
  import { atom, useSetAtom } from "jotai";
5
5
  import useEvent from "react-use-event-hook";
6
6
  import { toast } from "@/components/ui/use-toast";
7
- import { getNotebook, type NotebookState } from "@/core/cells/cells";
7
+ import { getNotebook } from "@/core/cells/cells";
8
8
  import { useCellActions } from "@/core/cells/actions";
9
9
  import type { CellId } from "@/core/cells/ids";
10
10
  import { enabledCellIds, staleCellIds } from "@/core/cells/utils";
11
11
  import {
12
12
  cellExecutionInputsAtom,
13
13
  cellFreshnessAtom,
14
- submitCells,
15
- withdrawCells,
14
+ runCells,
15
+ syncEdits,
16
+ waitForRuntime,
16
17
  } from "@/core/executions";
17
18
  import { getCurrentLanguageAdapter } from "@/core/codemirror/language/commands";
18
- import { getEditorCodeAsPython } from "@/core/codemirror/language/utils";
19
- import { useRequestClient } from "@/core/network/requests";
20
- import type { ExecuteCellsRequest } from "@/core/network/types";
21
19
  import { activeDocumentModelAtom } from "@/core/session/active-notebook";
22
20
  import { store } from "@/core/state/jotai";
21
+ import { prettyError } from "@/utils/errors";
23
22
  import { Logger } from "@/utils/Logger";
24
23
 
25
24
  export const hasRunAnyCellAtom = atom<boolean>(false);
@@ -33,18 +32,36 @@ export function useRunStaleCells() {
33
32
  // Freshness reads the confirmed document, so an edit still in flight
34
33
  // reaches the host before the stale cells are chosen.
35
34
  const model = store.get(activeDocumentModelAtom);
35
+ if (model === null) {
36
+ return;
37
+ }
36
38
  try {
37
- await model?.flush();
39
+ await waitForRuntime(store);
38
40
  } catch (error) {
39
41
  if (store.get(activeDocumentModelAtom) !== model) {
40
42
  return;
41
43
  }
42
- Logger.warn("Running stale cells with unsynced edits", error);
44
+ Logger.warn("Stale cells not run without a runtime", error);
45
+ toast({
46
+ title: "Failed to run",
47
+ description: prettyError(error),
48
+ variant: "danger",
49
+ });
50
+ return;
51
+ }
52
+ try {
53
+ await syncEdits(model);
54
+ } catch (error) {
55
+ if (store.get(activeDocumentModelAtom) !== model) {
56
+ return;
57
+ }
58
+ Logger.warn("Stale cells not run with unsynced edits", error);
43
59
  toast({
44
60
  title: "Edits not yet synced",
45
- description: "Stale cells were chosen from the last synced source.",
61
+ description: "Stale cells were not run. Retry once the edits sync.",
46
62
  variant: "danger",
47
63
  });
64
+ return;
48
65
  }
49
66
  // The request was for the notebook that was open when it was made.
50
67
  if (store.get(activeDocumentModelAtom) !== model) {
@@ -82,78 +99,34 @@ export function useRunAllCells() {
82
99
  }
83
100
 
84
101
  /**
85
- * Creates a function that runs the given cells.
102
+ * Creates a function that runs the given cells through the host.
103
+ *
104
+ * The editor settles each cell first: an open completion closes and the
105
+ * debugger view clears, as they would on any run.
86
106
  */
87
107
  export function useRunCells() {
88
108
  const { prepareForRun } = useCellActions();
89
- const { sendRun } = useRequestClient();
90
109
  const setHasRunAnyCell = useSetAtom(hasRunAnyCellAtom);
91
110
 
92
111
  const runCellsMemoized = useEvent(async (cellIds: CellId[]) => {
93
112
  if (cellIds.length === 0) {
94
113
  return;
95
114
  }
96
-
97
- const notebook = getNotebook();
98
-
99
115
  // Set a flag that a user has manually run at least one cell.
100
116
  setHasRunAnyCell(true);
101
117
 
102
- return runCells({ cellIds, sendRun, prepareForRun, notebook });
103
- });
104
-
105
- return runCellsMemoized;
106
- }
107
-
108
- export async function runCells({
109
- cellIds,
110
- sendRun,
111
- prepareForRun,
112
- notebook,
113
- }: {
114
- cellIds: CellId[];
115
- sendRun: (request: ExecuteCellsRequest) => Promise<null>;
116
- prepareForRun: (action: { cellId: CellId }) => void;
117
- notebook: NotebookState;
118
- }) {
119
- if (cellIds.length === 0) {
120
- return;
121
- }
122
-
123
- const { cellHandles, cellData } = notebook;
124
-
125
- const codes: string[] = [];
126
- for (const cellId of cellIds) {
127
- const ref = cellHandles[cellId];
128
- const ev = ref?.current?.editorView;
129
- let code: string;
130
- // Performs side-effects that must run whenever the cell is run, but doesn't
131
- // actually run the cell.
132
- if (ev) {
118
+ const { cellHandles } = getNotebook();
119
+ for (const cellId of cellIds) {
120
+ const ev = cellHandles[cellId]?.current?.editorView;
133
121
  // Skip close on markdown, since we autorun, otherwise we'll close the
134
122
  // completion each time.
135
- if (getCurrentLanguageAdapter(ev) !== "markdown") {
123
+ if (ev && getCurrentLanguageAdapter(ev) !== "markdown") {
136
124
  closeCompletion(ev);
137
125
  }
138
- // Prefer code from editor
139
- code = getEditorCodeAsPython(ev);
140
- } else {
141
- code = cellData[cellId]?.code || "";
126
+ prepareForRun({ cellId });
142
127
  }
143
-
144
- codes.push(code);
145
- prepareForRun({ cellId });
146
- }
147
- // The kernel sets a disabled cell aside without planning an execution,
148
- // so a submission for it would never be superseded.
149
- submitCells(
150
- store,
151
- cellIds.filter((cellId) => !cellData[cellId]?.config.disabled),
152
- );
153
-
154
- // TODO(MO-8346): Submit through the host's `runtime.run`.
155
- await sendRun({ cellIds: cellIds, codes: codes }).catch((error) => {
156
- withdrawCells(store, cellIds);
157
- Logger.error(error);
128
+ return runCells(store, cellIds);
158
129
  });
130
+
131
+ return runCellsMemoized;
159
132
  }
@@ -2,7 +2,7 @@
2
2
  import { closeCompletion, completionStatus } from "@codemirror/autocomplete";
3
3
  import type { EditorView } from "@codemirror/view";
4
4
  import clsx from "clsx";
5
- import { useAtomValue, useSetAtom } from "jotai";
5
+ import { useAtomValue, useSetAtom, useStore } from "jotai";
6
6
  import {
7
7
  HelpCircleIcon,
8
8
  MoreHorizontalIcon,
@@ -40,14 +40,16 @@ import { canCollapseOutline } from "@/core/dom/outline";
40
40
  import { isErrorMime } from "@/core/mime";
41
41
  import type { AppMode } from "@/core/mode";
42
42
  import { connectionAtom } from "@/core/network/connection";
43
- import { useRequestClient } from "@/core/network/requests";
44
43
  import {
45
44
  type CellExecutionView,
46
45
  type CellFreshness,
47
46
  type CellRunStatus,
47
+ replyInput,
48
48
  useCellExecution,
49
49
  useCellFreshness,
50
50
  } from "@/core/executions";
51
+ import type { WithResponse } from "@/core/cells/types";
52
+ import type { OutputMessage } from "@/core/kernel/messages";
51
53
  import type { CellConfig } from "@/core/network/types";
52
54
  import { useResizeObserver } from "@/hooks/useResizeObserver";
53
55
  import { cn } from "@/utils/cn";
@@ -241,6 +243,30 @@ export interface CellHandle {
241
243
  editorViewOrNull: EditorView | null;
242
244
  }
243
245
 
246
+ /**
247
+ * Answer the prompt at `index` of the cell's console through the host.
248
+ *
249
+ * The console entry is marked answered right away, as before. The reply
250
+ * itself names the prompt, so the host can mark it answered and turn away
251
+ * a late reply meant for an earlier prompt.
252
+ */
253
+ function useAnswerPrompt(
254
+ cellId: CellId,
255
+ consoleOutputs: readonly WithResponse<OutputMessage>[],
256
+ ) {
257
+ const store = useStore();
258
+ const actions = useCellActions();
259
+ return useEvent((text: string, index: number) => {
260
+ actions.setStdinResponse({ cellId, response: text, outputIndex: index });
261
+ const prompt = consoleOutputs[index];
262
+ if (prompt?.input_id === undefined) {
263
+ Logger.error("The prompt has no id to answer it by", prompt);
264
+ return;
265
+ }
266
+ void replyInput(store, prompt.input_id, text);
267
+ });
268
+ }
269
+
244
270
  export interface CellProps {
245
271
  cellId: CellId;
246
272
  theme: Theme;
@@ -404,7 +430,7 @@ const EditableCellComponent = ({
404
430
  const setAiCompletionCell = useSetAtom(aiCompletionCellAtom);
405
431
  const deleteCell = useDeleteCellCallback();
406
432
  const runCell = useRunCell(cellId);
407
- const { sendStdin } = useRequestClient();
433
+ const answerPrompt = useAnswerPrompt(cellId, cellRuntime.consoleOutputs);
408
434
  const isPendingCut = useIsPendingCut(cellId);
409
435
 
410
436
  const [languageAdapter, setLanguageAdapter] = useState<LanguageAdapterType>();
@@ -823,14 +849,7 @@ const EditableCellComponent = ({
823
849
  // breakpoints; no-op when none are set.
824
850
  clearCellBreakpoints(cellId);
825
851
  }}
826
- onSubmitDebugger={(text, index) => {
827
- actions.setStdinResponse({
828
- cellId,
829
- response: text,
830
- outputIndex: index,
831
- });
832
- sendStdin({ text });
833
- }}
852
+ onSubmitDebugger={answerPrompt}
834
853
  cellId={cellId}
835
854
  debuggerActive={cellRuntime.debuggerActive}
836
855
  />
@@ -1060,10 +1079,10 @@ const SetupCellComponent = ({
1060
1079
  const connection = useAtomValue(connectionAtom);
1061
1080
 
1062
1081
  const actions = useCellActions();
1063
- const requestClient = useRequestClient();
1064
1082
  const deleteCell = useDeleteCellCallback();
1065
1083
  const setAiCompletionCell = useSetAtom(aiCompletionCellAtom);
1066
1084
  const runCell = useRunCell(cellId);
1085
+ const answerPrompt = useAnswerPrompt(cellId, cellRuntime.consoleOutputs);
1067
1086
 
1068
1087
  const uninstantiated = isUninstantiated(execution);
1069
1088
 
@@ -1293,14 +1312,7 @@ const SetupCellComponent = ({
1293
1312
  // breakpoints; no-op when none are set.
1294
1313
  clearCellBreakpoints(cellId);
1295
1314
  }}
1296
- onSubmitDebugger={(text, index) => {
1297
- actions.setStdinResponse({
1298
- cellId,
1299
- response: text,
1300
- outputIndex: index,
1301
- });
1302
- requestClient.sendStdin({ text });
1303
- }}
1315
+ onSubmitDebugger={answerPrompt}
1304
1316
  cellId={cellId}
1305
1317
  debuggerActive={cellRuntime.debuggerActive}
1306
1318
  />