@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.
- package/dist/{ConnectedDataExplorerComponent-DJnSxHJB.js → ConnectedDataExplorerComponent-DieU6L4x.js} +1 -1
- package/dist/{ErrorBoundary-flfhLFje.js → ErrorBoundary-DjoeT4kv.js} +1 -1
- package/dist/{abnfDiagram-VCTEODGH-CR0U5hup.js → abnfDiagram-VCTEODGH-LOVrjbdp.js} +2 -2
- package/dist/{any-language-editor-C5dsTpeB.js → any-language-editor-DMC2JE9_.js} +1 -1
- package/dist/{architectureDiagram-5GKGNRK7-Bc_fbI1Q.js → architectureDiagram-5GKGNRK7-RZsm312h.js} +1 -1
- package/dist/assets/__vite-browser-external-BSp9tJ_k.js +1 -0
- package/dist/assets/worker-BdfPA0g5.js +171 -0
- package/dist/{chat-ui-C4iLRzY0.js → chat-ui-DkuxGEDp.js} +418 -418
- package/dist/{chunk-IMKFNOWR-y9z5oeAY.js → chunk-IMKFNOWR-B6agbLjo.js} +1 -1
- package/dist/{chunk-SHT3W25Y-Tu-V-e_v.js → chunk-SHT3W25Y-CfULafYG.js} +1 -1
- package/dist/{chunk-SVP7TREG-CJFxs5R_.js → chunk-SVP7TREG-DEOpPMJp.js} +1 -1
- package/dist/{chunk-TICWLB2K-zkTP7ya7.js → chunk-TICWLB2K-DaH-gkX5.js} +1 -1
- package/dist/{classDiagram-ZZMXUADV-DxJAZI0N.js → classDiagram-ZZMXUADV-C0CyiWQ7.js} +2 -2
- package/dist/{classDiagram-v2-VYDZK3BY-TmHo2BpS.js → classDiagram-v2-VYDZK3BY-xfIc9e2Y.js} +2 -2
- package/dist/{code-visibility-BDs_r7jH.js → code-visibility-CduMVVFS.js} +32 -32
- package/dist/{common-CMTsL35V.js → common-CMcEcucH.js} +919 -919
- package/dist/{config-BG2qKzDg.js → config-BVYA1P0C.js} +103 -76
- package/dist/{cynefinDiagram-5FMLGOSQ-Eb40hmhD.js → cynefinDiagram-5FMLGOSQ-DwGYhKu0.js} +1 -1
- package/dist/{dagre-GXQ25YYZ-_paaVwit.js → dagre-GXQ25YYZ-DfMaWJOV.js} +1 -1
- package/dist/{data-grid-overlay-editor-es6V6qf-.js → data-grid-overlay-editor-BYrIVut3.js} +1 -1
- package/dist/{diagram-S7CK7UJ4-BtKJm1_4.js → diagram-S7CK7UJ4-Bw_L87GZ.js} +1 -1
- package/dist/{diagram-UQ7AKVKN-DQbxhEMd.js → diagram-UQ7AKVKN-nsrs3Fbz.js} +1 -1
- package/dist/{diagram-VX7I27RA-DITDIfGC.js → diagram-VX7I27RA-7ST9lEqP.js} +1 -1
- package/dist/{diagram-Z3DM3KII-Ct9_zo6F.js → diagram-Z3DM3KII-BnGte-HX.js} +1 -1
- package/dist/{ebnfDiagram-PWID7BFC-6cOntwfv.js → ebnfDiagram-PWID7BFC-zaKyJ1YL.js} +2 -2
- package/dist/{erDiagram-RLTQ6QDP-CJiOER41.js → erDiagram-RLTQ6QDP-Cx7g95FZ.js} +1 -1
- package/dist/{error-banner-DGOrmBk5.js → error-banner-DkQNU4bn.js} +128 -128
- package/dist/{error-entries-BWmleiL0.js → error-entries-istW7Xbs.js} +589 -544
- package/dist/{feedback-button-D4eVjeNs.js → feedback-button-Zvm26aOQ.js} +107 -106
- package/dist/{flowDiagram-HODETNUW-1lPBEUuW.js → flowDiagram-HODETNUW-C7nIqaHP.js} +2 -2
- package/dist/{formats-qhBmYb0l.js → formats-CMa7L3E8.js} +1 -1
- package/dist/{glide-data-editor-ahfaPdFW.js → glide-data-editor-BE-GgVp4.js} +3 -3
- package/dist/{globals-m8oswz--.js → globals-Dbru-WLP.js} +1 -1
- package/dist/{html-to-image-B8hE6HIH.js → html-to-image-pPbR2cDx.js} +4512 -4464
- package/dist/{infoDiagram-27XIBGKW-C6K4tri5.js → infoDiagram-27XIBGKW-BTM_NXsn.js} +1 -1
- package/dist/{ishikawaDiagram-5VMMS53U-DwcwgRNH.js → ishikawaDiagram-5VMMS53U-K0FZrlvv.js} +1 -1
- package/dist/{kanban-definition-UXKFOSKX-E489OMdc.js → kanban-definition-UXKFOSKX-ftznOyrz.js} +1 -1
- package/dist/main.js +6595 -6186
- package/dist/{mermaid-DMPMkVqY.js → mermaid-CNgH43ED.js} +33 -33
- package/dist/{mindmap-definition-YA3MSWOX-DWHjasOp.js → mindmap-definition-YA3MSWOX-CmzbxKfM.js} +1 -1
- package/dist/{number-overlay-editor-C6klKzEr.js → number-overlay-editor-qvlPZ-l-.js} +1 -1
- package/dist/{pegDiagram-XKGWAZYB-COjcPOZT.js → pegDiagram-XKGWAZYB-DfTAWfGE.js} +2 -2
- package/dist/{pieDiagram-E7YTZNPT-BHAVZ38h.js → pieDiagram-E7YTZNPT-_kGQstZj.js} +1 -1
- package/dist/{process-output-DZDL_j1a.js → process-output-BySO1nIs.js} +3 -3
- package/dist/{railroadDiagram-O6MQD6OU-94ThFyKX.js → railroadDiagram-O6MQD6OU-D41WmIff.js} +2 -2
- package/dist/{requirementDiagram-BXWQKSXE-CpF-Fxbb.js → requirementDiagram-BXWQKSXE-Bh53ZI3I.js} +1 -1
- package/dist/{reveal-component-DwnxPyVz.js → reveal-component-DQaegdcW.js} +4 -4
- package/dist/{sequenceDiagram-WJ2MYXX4-CvANK2x7.js → sequenceDiagram-WJ2MYXX4-DBX27dNm.js} +1 -1
- package/dist/{stateDiagram-D77RDMKH-WHuJ4mJL.js → stateDiagram-D77RDMKH-CSYz2IZi.js} +2 -2
- package/dist/{stateDiagram-v2-MP3YSRHH-DbO7lwYq.js → stateDiagram-v2-MP3YSRHH-DVLvtOqn.js} +2 -2
- package/dist/{swimlanes-42K2YHIH-CS2s29Vo.js → swimlanes-42K2YHIH-D59N0ktA.js} +1 -1
- package/dist/{swimlanesDiagram-VR7AAH4N-CXaA1vQg.js → swimlanesDiagram-VR7AAH4N-CRgpmVSW.js} +2 -2
- package/dist/{timeline-definition-24CTP7MA-B-SQMg5h.js → timeline-definition-24CTP7MA-C_xIIZDM.js} +1 -1
- package/dist/toDate-5qPtZ6HL.js +94 -0
- package/dist/{vega-component-DwqGfN3_.js → vega-component-Bly2lDhz.js} +10 -10
- package/dist/{vennDiagram-4TSXK5OY-etEFgvaG.js → vennDiagram-4TSXK5OY-DlOf0Mjs.js} +1 -1
- package/dist/{wardleyDiagram-VM6X3IG4-BEQN7g62.js → wardleyDiagram-VM6X3IG4-zD_2guVe.js} +1 -1
- package/dist/{xychartDiagram-S5SC5T6Z-D8-e5Z0R.js → xychartDiagram-S5SC5T6Z-CA0Dy1ZM.js} +1 -1
- package/package.json +1 -1
- package/src/__mocks__/executions.ts +35 -13
- package/src/__mocks__/requests.ts +0 -1
- package/src/components/editor/cell/__tests__/useRunCells.test.tsx +86 -101
- package/src/components/editor/cell/useRunCells.ts +38 -65
- package/src/components/editor/notebook-cell.tsx +32 -20
- package/src/components/editor/output/ansi-reduce.ts +7 -0
- package/src/core/ai/__tests__/staged-cells.test.ts +1 -1
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +2 -0
- package/src/core/cells/__tests__/cells.test.ts +0 -14
- package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +15 -0
- package/src/core/cells/__tests__/utils.test.ts +1 -1
- package/src/core/cells/add-missing-import.ts +2 -5
- package/src/core/cells/cell.ts +1 -0
- package/src/core/cells/cells.ts +1 -2
- package/src/core/executions/__tests__/atoms.test.ts +227 -43
- package/src/core/executions/__tests__/hooks.test.tsx +1 -2
- package/src/core/executions/__tests__/input.test.ts +86 -0
- package/src/core/executions/__tests__/run.test.ts +390 -0
- package/src/core/executions/__tests__/select.test.ts +105 -30
- package/src/core/executions/atoms.ts +186 -71
- package/src/core/executions/index.ts +7 -6
- package/src/core/executions/input.ts +55 -0
- package/src/core/executions/run.ts +120 -0
- package/src/core/executions/select.ts +59 -24
- package/src/core/islands/__tests__/bridge.test.ts +35 -54
- package/src/core/islands/bootstrap.ts +5 -1
- package/src/core/islands/bridge.ts +16 -28
- package/src/core/islands/components/IslandControls.tsx +15 -7
- package/src/core/islands/components/__tests__/IslandControls.test.tsx +36 -53
- package/src/core/islands/worker/worker.tsx +45 -19
- package/src/core/network/__tests__/requests-lazy.test.ts +14 -24
- package/src/core/network/__tests__/requests-toasting.test.tsx +7 -7
- package/src/core/network/requests-lazy.ts +2 -3
- package/src/core/network/requests-network.ts +0 -9
- package/src/core/network/requests-static.ts +0 -1
- package/src/core/network/requests-toasting.tsx +0 -1
- package/src/core/network/types.ts +0 -2
- package/src/core/notebook/replica/__tests__/fixtures.ts +17 -1
- package/src/core/notebook/replica/backend.ts +3 -1
- package/src/core/runtime/__tests__/runtime.test.ts +36 -0
- package/src/core/runtime/config.ts +36 -1
- package/src/core/runtime/runtime.ts +21 -1
- package/src/core/transports/static.ts +4 -3
- package/src/core/wasm/bridge.ts +0 -17
- package/dist/assets/__vite-browser-external-DzTYIHH_.js +0 -1
- package/dist/assets/worker-BH08I2Il.js +0 -171
- 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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
34
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
|
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
|
|
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
|
-
: {
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
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
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
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
|
|
287
|
+
export function requestRun(
|
|
194
288
|
store: Store,
|
|
195
289
|
cellIds: readonly Protocol.CellId[],
|
|
196
|
-
):
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
)
|
|
211
|
-
|
|
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
|
-
/**
|
|
215
|
-
|
|
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
|
-
|
|
326
|
+
request: RunRequest,
|
|
327
|
+
run: Protocol.Run,
|
|
218
328
|
): void {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
341
|
+
check();
|
|
224
342
|
}
|
|
225
343
|
|
|
226
344
|
function setCellBlocked(
|
|
@@ -242,22 +360,19 @@ function setCellBlocked(
|
|
|
242
360
|
}
|
|
243
361
|
|
|
244
362
|
/**
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
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
|
|
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(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
4
|
+
* Cell status, timing, and output, as the editor reads them.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
*
|
|
70
|
-
*
|
|
70
|
+
* Whether a run can reach `id`, meaning the document has the cell and
|
|
71
|
+
* nothing keeps it from running.
|
|
71
72
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
}
|