@marimo-team/islands 0.25.3-dev275 → 0.25.3-dev279
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/{ErrorBoundary-BuIXiuc5.js → ErrorBoundary-DmTMuDPR.js} +1 -1
- package/dist/{chat-ui-SZ6lSH52.js → chat-ui-v1Fdh0NA.js} +270 -270
- package/dist/{code-visibility-BEAPMrC-.js → code-visibility-Dw4-TXN0.js} +2 -2
- package/dist/{common-CEmzaoJ8.js → common-BXMFi4lr.js} +4947 -4943
- package/dist/{data-grid-overlay-editor-CUo25_W8.js → data-grid-overlay-editor-hZzwX7Ia.js} +1 -1
- package/dist/{error-entries-B-1yUdcH.js → error-entries-C1S-Rmyt.js} +545 -535
- package/dist/{feedback-button-BqiR7zMO.js → feedback-button-6Ui2TDLR.js} +5 -5
- package/dist/{glide-data-editor-B7dguvqk.js → glide-data-editor-PZNlrSDI.js} +3 -3
- package/dist/{globals-CYykXfGI.js → globals-DX6FPZYq.js} +1 -1
- package/dist/{html-to-image-DC_pmZLs.js → html-to-image-CqGUd58T.js} +14452 -14452
- package/dist/main.js +6031 -6036
- package/dist/{number-overlay-editor-C4u7SJAJ.js → number-overlay-editor-D0CsL3_R.js} +1 -1
- package/dist/{process-output-DYu84VfZ.js → process-output-DskX9jLa.js} +2 -2
- package/dist/{reveal-component-DGi-irHV.js → reveal-component-D7xeI1QR.js} +906 -906
- package/package.json +1 -1
- package/src/__mocks__/executions.ts +4 -2
- package/src/__mocks__/notebook.ts +0 -2
- package/src/__tests__/CellStatus.test.tsx +6 -6
- package/src/components/editor/__tests__/data-attributes.test.tsx +0 -2
- package/src/components/editor/__tests__/output-persistence.test.tsx +20 -5
- package/src/components/editor/cell/CellStatus.tsx +6 -5
- package/src/components/editor/cell/__tests__/useRunCells.test.tsx +169 -0
- package/src/components/editor/cell/collapse.tsx +3 -4
- package/src/components/editor/cell/useRunCells.ts +32 -3
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +2 -3
- package/src/components/editor/notebook-cell.tsx +23 -37
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +4 -10
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +8 -5
- package/src/components/slides/minimap.tsx +3 -2
- package/src/components/slides/reveal-component.tsx +3 -2
- package/src/components/slides/slide-cell-view.tsx +8 -8
- package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -101
- package/src/core/cells/__tests__/actions.test.ts +0 -1
- package/src/core/cells/__tests__/cell.test.ts +81 -17
- package/src/core/cells/__tests__/cells.test.ts +0 -100
- package/src/core/cells/__tests__/document.test.ts +2 -8
- package/src/core/cells/__tests__/utils.test.ts +93 -24
- package/src/core/cells/actions.ts +0 -1
- package/src/core/cells/cell.ts +37 -14
- package/src/core/cells/cells.ts +10 -99
- package/src/core/cells/document.ts +2 -13
- package/src/core/cells/types.ts +0 -23
- package/src/core/cells/utils.ts +44 -17
- package/src/core/codemirror/cells/extensions.ts +0 -14
- package/src/core/errors/__tests__/error-entries.test.ts +40 -1
- package/src/core/errors/error-entries.ts +2 -3
- package/src/core/executions/__tests__/freshness.test.ts +420 -0
- package/src/core/executions/__tests__/hooks.test.tsx +106 -0
- package/src/core/executions/atoms.ts +22 -0
- package/src/core/executions/freshness.ts +164 -0
- package/src/core/executions/hooks.ts +17 -1
- package/src/core/executions/index.ts +3 -1
- package/src/core/islands/bootstrap.ts +0 -1
- package/src/core/kernel/handlers.ts +0 -4
- package/src/core/session/useNotebookSession.tsx +2 -7
- package/src/stories/cell-status.stories.tsx +12 -14
- package/src/stories/cell.stories.tsx +44 -10
- package/src/stories/layout/vertical/one-column.stories.tsx +0 -10
- package/src/components/editor/cell/__tests__/useRunCells.test.ts +0 -45
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether each cell's result still reflects the document and the values
|
|
5
|
+
* it read, by the host protocol's rules.
|
|
6
|
+
*
|
|
7
|
+
* A completed execution is current when its code fingerprint equals the
|
|
8
|
+
* cell's, every variable it depends on still holds the recorded revision,
|
|
9
|
+
* every name it depends on that was unbound is still unbound, and the
|
|
10
|
+
* executions that bound those variables are current. See the freshness
|
|
11
|
+
* section of `protocols/host/spec.md`.
|
|
12
|
+
*
|
|
13
|
+
* The protocol also ranks why a result is outdated. The editor asks two
|
|
14
|
+
* questions of its own, so that is what this reports; the ranked reason
|
|
15
|
+
* can come back with a reader. Two editor concerns are folded in: a name
|
|
16
|
+
* bound only by an earlier runtime is unbound in this one, and a queued or
|
|
17
|
+
* running execution for the cell's current source already covers it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type * as Protocol from "@marimo-team/client/protocol";
|
|
21
|
+
import type { Executions, Results } from "@/core/notebook/replica";
|
|
22
|
+
|
|
23
|
+
export interface CellFreshness {
|
|
24
|
+
/** This cell's source changed since its result. */
|
|
25
|
+
readonly sourceChanged: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* A value the result read changed since, by a rerun or an update, here
|
|
28
|
+
* or upstream. An ancestor that was only edited does not count: its
|
|
29
|
+
* values are still the ones this result read.
|
|
30
|
+
*/
|
|
31
|
+
readonly inputsChanged: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Current, and also a cell with no completed execution to compare. */
|
|
35
|
+
const CURRENT: CellFreshness = { sourceChanged: false, inputsChanged: false };
|
|
36
|
+
|
|
37
|
+
/** What freshness is derived from. */
|
|
38
|
+
export interface CellFreshnessInputs {
|
|
39
|
+
/** The document's cells, for the fingerprint of each cell's source. */
|
|
40
|
+
readonly cells: ReadonlyMap<Protocol.CellId, Protocol.Cell>;
|
|
41
|
+
/** The stream's executions, for the variables currently bound. */
|
|
42
|
+
readonly executions: Executions;
|
|
43
|
+
/** Each cell's last completed record, the result being compared. */
|
|
44
|
+
readonly results: Results;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Freshness by cell id, as `freshnessOf` builds it. */
|
|
48
|
+
export type CellFreshnessLookup = (id: Protocol.CellId) => CellFreshness;
|
|
49
|
+
|
|
50
|
+
/** The variables bound in the namespace right now, by name. */
|
|
51
|
+
function boundVariables(
|
|
52
|
+
executions: Executions,
|
|
53
|
+
): ReadonlyMap<string, Protocol.Variable[]> {
|
|
54
|
+
const byName = new Map<string, Protocol.Variable[]>();
|
|
55
|
+
for (const execution of executions.executions.values()) {
|
|
56
|
+
for (const variable of execution.variables.values()) {
|
|
57
|
+
const bound = byName.get(variable.name);
|
|
58
|
+
if (bound === undefined) {
|
|
59
|
+
byName.set(variable.name, [variable]);
|
|
60
|
+
} else {
|
|
61
|
+
bound.push(variable);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return byName;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compares results against the document and the namespace.
|
|
70
|
+
*
|
|
71
|
+
* Build one per set of inputs and ask it about each cell, since the
|
|
72
|
+
* bound variables are indexed once and a binding execution's freshness is
|
|
73
|
+
* computed once however many results depend on it.
|
|
74
|
+
*/
|
|
75
|
+
export function freshnessOf(inputs: CellFreshnessInputs): CellFreshnessLookup {
|
|
76
|
+
const bound = boundVariables(inputs.executions);
|
|
77
|
+
const memo = new Map<Protocol.ExecutionId, CellFreshness>();
|
|
78
|
+
const recordOf = (id: Protocol.ExecutionId) =>
|
|
79
|
+
inputs.executions.executions.get(id)?.record;
|
|
80
|
+
|
|
81
|
+
/** Whether a value the record read has changed, or one upstream has. */
|
|
82
|
+
function inputsChanged(
|
|
83
|
+
record: Protocol.Execution,
|
|
84
|
+
visiting: ReadonlySet<Protocol.ExecutionId>,
|
|
85
|
+
): boolean {
|
|
86
|
+
const path = new Set(visiting).add(record.id);
|
|
87
|
+
for (const [name, revision] of Object.entries(record.depends_on)) {
|
|
88
|
+
const variables = bound.get(name) ?? [];
|
|
89
|
+
if (revision === null) {
|
|
90
|
+
// A result retained from an earlier runtime binds nothing here.
|
|
91
|
+
if (
|
|
92
|
+
variables.some(
|
|
93
|
+
(v) => recordOf(v.execution_id)?.runtime_id === record.runtime_id,
|
|
94
|
+
)
|
|
95
|
+
) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const variable = variables.find(
|
|
101
|
+
(candidate) => candidate.revision.execution === revision.execution,
|
|
102
|
+
);
|
|
103
|
+
if (
|
|
104
|
+
variable === undefined ||
|
|
105
|
+
variable.revision.update !== revision.update
|
|
106
|
+
) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
const binder = recordOf(variable.execution_id);
|
|
110
|
+
if (binder === undefined) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (!path.has(binder.id) && ofRecord(binder, path).inputsChanged) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Whether a newer execution of the cell already runs its current source. */
|
|
121
|
+
function coveredByPending(
|
|
122
|
+
cell: Protocol.Cell,
|
|
123
|
+
record: Protocol.Execution,
|
|
124
|
+
): boolean {
|
|
125
|
+
const latest = inputs.executions.latest.get(cell.id);
|
|
126
|
+
if (latest === undefined || latest === record.id) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const pending = recordOf(latest);
|
|
130
|
+
return (
|
|
131
|
+
pending !== undefined &&
|
|
132
|
+
pending.status !== "completed" &&
|
|
133
|
+
pending.code_fingerprint === cell.code_fingerprint
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ofRecord(
|
|
138
|
+
record: Protocol.Execution,
|
|
139
|
+
visiting: ReadonlySet<Protocol.ExecutionId>,
|
|
140
|
+
): CellFreshness {
|
|
141
|
+
const known = memo.get(record.id);
|
|
142
|
+
if (known !== undefined) {
|
|
143
|
+
return known;
|
|
144
|
+
}
|
|
145
|
+
const cell =
|
|
146
|
+
record.cell_id === null ? undefined : inputs.cells.get(record.cell_id);
|
|
147
|
+
const freshness: CellFreshness = {
|
|
148
|
+
sourceChanged:
|
|
149
|
+
cell === undefined ||
|
|
150
|
+
(cell.code_fingerprint !== record.code_fingerprint &&
|
|
151
|
+
!coveredByPending(cell, record)),
|
|
152
|
+
inputsChanged: inputsChanged(record, visiting),
|
|
153
|
+
};
|
|
154
|
+
memo.set(record.id, freshness);
|
|
155
|
+
return freshness;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (id) => {
|
|
159
|
+
const result = inputs.results.get(id);
|
|
160
|
+
return result === null || result === undefined
|
|
161
|
+
? CURRENT
|
|
162
|
+
: ofRecord(result, new Set());
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -5,7 +5,8 @@ import { dequal } from "dequal";
|
|
|
5
5
|
import { useAtomValue } from "jotai";
|
|
6
6
|
import { selectAtom } from "jotai/utils";
|
|
7
7
|
import { useMemo } from "react";
|
|
8
|
-
import { cellExecutionInputsAtom } from "./atoms";
|
|
8
|
+
import { cellExecutionInputsAtom, cellFreshnessAtom } from "./atoms";
|
|
9
|
+
import type { CellFreshness } from "./freshness";
|
|
9
10
|
import { type CellExecutionView, cellExecutionView } from "./select";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -29,3 +30,18 @@ export function useCellExecution(id: Protocol.CellId): CellExecutionView {
|
|
|
29
30
|
);
|
|
30
31
|
return useAtomValue(cellAtom);
|
|
31
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a cell's result is current, by the host protocol's rules over
|
|
36
|
+
* the confirmed document and the executions mirror.
|
|
37
|
+
*
|
|
38
|
+
* The confirmed document is what the host has accepted, so an edit reads
|
|
39
|
+
* as such once the model has flushed it, not on the keystroke.
|
|
40
|
+
*/
|
|
41
|
+
export function useCellFreshness(id: Protocol.CellId): CellFreshness {
|
|
42
|
+
const cellAtom = useMemo(
|
|
43
|
+
() => selectAtom(cellFreshnessAtom, (freshness) => freshness(id), dequal),
|
|
44
|
+
[id],
|
|
45
|
+
);
|
|
46
|
+
return useAtomValue(cellAtom);
|
|
47
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
export {
|
|
12
12
|
cellExecutionInputsAtom,
|
|
13
|
+
cellFreshnessAtom,
|
|
13
14
|
notebookIsRunningAtom,
|
|
14
15
|
notebookQueuedOrRunningCountAtom,
|
|
15
16
|
observeCellMessage,
|
|
@@ -18,7 +19,8 @@ export {
|
|
|
18
19
|
waitForRuntimeRunning,
|
|
19
20
|
withdrawCells,
|
|
20
21
|
} from "./atoms";
|
|
21
|
-
export {
|
|
22
|
+
export type { CellFreshness, CellFreshnessLookup } from "./freshness";
|
|
23
|
+
export { useCellExecution, useCellFreshness } from "./hooks";
|
|
22
24
|
export {
|
|
23
25
|
type CellExecutionInputs,
|
|
24
26
|
type CellExecutionView,
|
|
@@ -221,7 +221,6 @@ function handleNotification(
|
|
|
221
221
|
handleKernelReady(msg, {
|
|
222
222
|
autoInstantiate: true,
|
|
223
223
|
cells: notebookCells(store.get(notebookAtom)),
|
|
224
|
-
setLastRun: actions.setLastRun,
|
|
225
224
|
resetCellRuntimes: actions.resetCellRuntimes,
|
|
226
225
|
setLayoutData: Functions.NOOP,
|
|
227
226
|
setCapabilities: Functions.NOOP,
|
|
@@ -60,7 +60,6 @@ export function handleKernelReady(
|
|
|
60
60
|
autoInstantiate: boolean;
|
|
61
61
|
/** The notebook's cells, as the host document provided them. */
|
|
62
62
|
cells: CellData[];
|
|
63
|
-
setLastRun: ReducerCellActions["setLastRun"];
|
|
64
63
|
resetCellRuntimes: ReducerCellActions["resetCellRuntimes"];
|
|
65
64
|
setLayoutData: (payload: {
|
|
66
65
|
layoutView: LayoutType;
|
|
@@ -73,7 +72,6 @@ export function handleKernelReady(
|
|
|
73
72
|
const {
|
|
74
73
|
autoInstantiate,
|
|
75
74
|
cells,
|
|
76
|
-
setLastRun,
|
|
77
75
|
resetCellRuntimes,
|
|
78
76
|
setLayoutData,
|
|
79
77
|
setCapabilities,
|
|
@@ -82,8 +80,6 @@ export function handleKernelReady(
|
|
|
82
80
|
const { resumed, ui_values, capabilities, auto_instantiated } = data;
|
|
83
81
|
|
|
84
82
|
resetCellRuntimes();
|
|
85
|
-
// TODO(MO-8403): Delete with `lastCodeRun`.
|
|
86
|
-
setLastRun({ lastCodeRun: data.last_executed_code ?? {} });
|
|
87
83
|
const layoutState = buildLayoutState(data, cells, setLayoutData);
|
|
88
84
|
|
|
89
85
|
setCapabilities(capabilities);
|
|
@@ -92,12 +92,8 @@ export function useNotebookSession(opts: {
|
|
|
92
92
|
const { showBoundary } = useErrorBoundary();
|
|
93
93
|
const store = useStore();
|
|
94
94
|
|
|
95
|
-
const {
|
|
96
|
-
|
|
97
|
-
reconcileDocument,
|
|
98
|
-
setLastRun,
|
|
99
|
-
resetCellRuntimes,
|
|
100
|
-
} = useReducerActions();
|
|
95
|
+
const { handleCellMessage, reconcileDocument, resetCellRuntimes } =
|
|
96
|
+
useReducerActions();
|
|
101
97
|
const { reconcileStagedCells } = useStagedAICellsActions();
|
|
102
98
|
const { addCellNotification } = useRunsActions();
|
|
103
99
|
const setInitialRunCompleted = useSetAtom(initialRunCompletedAtom);
|
|
@@ -154,7 +150,6 @@ export function useNotebookSession(opts: {
|
|
|
154
150
|
const layout = handleKernelReady(msg.data, {
|
|
155
151
|
autoInstantiate,
|
|
156
152
|
cells: notebookCells(store.get(notebookAtom)),
|
|
157
|
-
setLastRun,
|
|
158
153
|
resetCellRuntimes,
|
|
159
154
|
setLayoutData,
|
|
160
155
|
setCapabilities,
|
|
@@ -27,7 +27,7 @@ export const Idle = Template.bind({});
|
|
|
27
27
|
Idle.args = {
|
|
28
28
|
editing: true,
|
|
29
29
|
status: "idle",
|
|
30
|
-
|
|
30
|
+
freshness: { sourceChanged: false, inputsChanged: false },
|
|
31
31
|
interrupted: false,
|
|
32
32
|
disabled: false,
|
|
33
33
|
elapsedTime: null,
|
|
@@ -37,7 +37,7 @@ export const Edited = Template.bind({});
|
|
|
37
37
|
Edited.args = {
|
|
38
38
|
editing: true,
|
|
39
39
|
status: "idle",
|
|
40
|
-
|
|
40
|
+
freshness: { sourceChanged: true, inputsChanged: false },
|
|
41
41
|
interrupted: false,
|
|
42
42
|
disabled: false,
|
|
43
43
|
elapsedTime: 100 as Milliseconds,
|
|
@@ -47,7 +47,7 @@ export const Interrupted = Template.bind({});
|
|
|
47
47
|
Interrupted.args = {
|
|
48
48
|
editing: true,
|
|
49
49
|
status: "idle",
|
|
50
|
-
|
|
50
|
+
freshness: { sourceChanged: false, inputsChanged: false },
|
|
51
51
|
interrupted: true,
|
|
52
52
|
disabled: false,
|
|
53
53
|
elapsedTime: 50 as Milliseconds,
|
|
@@ -57,7 +57,7 @@ export const Running = Template.bind({});
|
|
|
57
57
|
Running.args = {
|
|
58
58
|
editing: true,
|
|
59
59
|
status: "running",
|
|
60
|
-
|
|
60
|
+
freshness: { sourceChanged: false, inputsChanged: false },
|
|
61
61
|
interrupted: true,
|
|
62
62
|
disabled: false,
|
|
63
63
|
elapsedTime: 50 as Milliseconds,
|
|
@@ -67,7 +67,7 @@ export const Queued = Template.bind({});
|
|
|
67
67
|
Queued.args = {
|
|
68
68
|
editing: true,
|
|
69
69
|
status: "queued",
|
|
70
|
-
|
|
70
|
+
freshness: { sourceChanged: false, inputsChanged: false },
|
|
71
71
|
interrupted: true,
|
|
72
72
|
disabled: false,
|
|
73
73
|
elapsedTime: 50 as Milliseconds,
|
|
@@ -77,7 +77,7 @@ export const Disabled = Template.bind({});
|
|
|
77
77
|
Disabled.args = {
|
|
78
78
|
editing: true,
|
|
79
79
|
status: "idle",
|
|
80
|
-
|
|
80
|
+
freshness: { sourceChanged: false, inputsChanged: false },
|
|
81
81
|
interrupted: false,
|
|
82
82
|
disabled: true,
|
|
83
83
|
elapsedTime: null,
|
|
@@ -88,7 +88,7 @@ DisabledTransitively.args = {
|
|
|
88
88
|
editing: true,
|
|
89
89
|
status: "idle",
|
|
90
90
|
blocked: true,
|
|
91
|
-
|
|
91
|
+
freshness: { sourceChanged: false, inputsChanged: false },
|
|
92
92
|
interrupted: false,
|
|
93
93
|
disabled: false,
|
|
94
94
|
elapsedTime: null,
|
|
@@ -99,7 +99,7 @@ DisabledTransitivelyAndEdited.args = {
|
|
|
99
99
|
editing: true,
|
|
100
100
|
status: "idle",
|
|
101
101
|
blocked: true,
|
|
102
|
-
|
|
102
|
+
freshness: { sourceChanged: true, inputsChanged: false },
|
|
103
103
|
interrupted: false,
|
|
104
104
|
disabled: false,
|
|
105
105
|
elapsedTime: null,
|
|
@@ -110,8 +110,7 @@ Stale.args = {
|
|
|
110
110
|
editing: true,
|
|
111
111
|
status: "idle",
|
|
112
112
|
blocked: true,
|
|
113
|
-
|
|
114
|
-
edited: false,
|
|
113
|
+
freshness: { sourceChanged: false, inputsChanged: true },
|
|
115
114
|
interrupted: false,
|
|
116
115
|
disabled: false,
|
|
117
116
|
elapsedTime: null,
|
|
@@ -122,8 +121,7 @@ StaleAndDisabled.args = {
|
|
|
122
121
|
editing: true,
|
|
123
122
|
status: "idle",
|
|
124
123
|
blocked: true,
|
|
125
|
-
|
|
126
|
-
edited: false,
|
|
124
|
+
freshness: { sourceChanged: false, inputsChanged: true },
|
|
127
125
|
interrupted: false,
|
|
128
126
|
disabled: true,
|
|
129
127
|
elapsedTime: null,
|
|
@@ -134,7 +132,7 @@ EditedStaleAndDisabled.args = {
|
|
|
134
132
|
editing: true,
|
|
135
133
|
status: "idle",
|
|
136
134
|
blocked: true,
|
|
137
|
-
|
|
135
|
+
freshness: { sourceChanged: false, inputsChanged: true },
|
|
138
136
|
interrupted: false,
|
|
139
137
|
disabled: true,
|
|
140
138
|
elapsedTime: null,
|
|
@@ -145,7 +143,7 @@ EditedAndStale.args = {
|
|
|
145
143
|
editing: true,
|
|
146
144
|
status: "idle",
|
|
147
145
|
blocked: true,
|
|
148
|
-
|
|
146
|
+
freshness: { sourceChanged: true, inputsChanged: false },
|
|
149
147
|
interrupted: false,
|
|
150
148
|
disabled: false,
|
|
151
149
|
elapsedTime: null,
|
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
4
4
|
import { createStore, Provider } from "jotai";
|
|
5
5
|
import { createRef } from "react";
|
|
6
|
+
import { MockExecutions } from "@/__mocks__/executions";
|
|
6
7
|
import { cellId } from "@/__tests__/branded";
|
|
8
|
+
import type { CellId } from "@/core/cells/ids";
|
|
9
|
+
import {
|
|
10
|
+
cell,
|
|
11
|
+
document as documentOf,
|
|
12
|
+
withCells,
|
|
13
|
+
} from "@/core/notebook/document/__tests__/fixtures";
|
|
7
14
|
import { type NotebookState, notebookAtom } from "@/core/cells/cells";
|
|
8
15
|
import {
|
|
9
16
|
type CellRuntimeState,
|
|
@@ -22,13 +29,41 @@ type Story = StoryObj<typeof Cell>;
|
|
|
22
29
|
|
|
23
30
|
interface CellOverrides {
|
|
24
31
|
output?: CellRuntimeState["output"];
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
/** How the cell's result is outdated, as the host would report it. */
|
|
33
|
+
freshness?: "edited" | "replaced" | "both";
|
|
27
34
|
config?: CellConfig;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
const EMPTY_OVERRIDES: Readonly<CellOverrides> = {};
|
|
31
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Give the replica a completed execution whose source was edited since,
|
|
41
|
+
* whose input a newer execution replaced, or both.
|
|
42
|
+
*/
|
|
43
|
+
function seedFreshness(
|
|
44
|
+
store: ReturnType<typeof createStore>,
|
|
45
|
+
cid: CellId,
|
|
46
|
+
freshness: NonNullable<CellOverrides["freshness"]>,
|
|
47
|
+
) {
|
|
48
|
+
const ran = MockExecutions.completed(cid, {
|
|
49
|
+
code_fingerprint: "ran",
|
|
50
|
+
depends_on:
|
|
51
|
+
freshness === "edited" ? {} : { x: { execution: "gone", update: 0 } },
|
|
52
|
+
});
|
|
53
|
+
const document = withCells(
|
|
54
|
+
documentOf(),
|
|
55
|
+
{
|
|
56
|
+
c1: null,
|
|
57
|
+
c2: null,
|
|
58
|
+
[cid]: cell(cid, "import marimo as mo", {
|
|
59
|
+
code_fingerprint: freshness === "replaced" ? "ran" : "edited",
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
[cid],
|
|
63
|
+
);
|
|
64
|
+
MockExecutions.seed(store, [ran], null, document);
|
|
65
|
+
}
|
|
66
|
+
|
|
32
67
|
const Cell: React.FC<{
|
|
33
68
|
overrides?: CellOverrides;
|
|
34
69
|
}> = ({ overrides = EMPTY_OVERRIDES }) => {
|
|
@@ -39,13 +74,11 @@ const Cell: React.FC<{
|
|
|
39
74
|
id: cid,
|
|
40
75
|
name: "cell_1",
|
|
41
76
|
code: "import marimo as mo",
|
|
42
|
-
edited: overrides.edited ?? false,
|
|
43
77
|
config: {
|
|
44
78
|
hide_code: overrides.config?.hide_code ?? false,
|
|
45
79
|
disabled: overrides.config?.disabled ?? false,
|
|
46
80
|
column: null,
|
|
47
81
|
},
|
|
48
|
-
lastCodeRun: null,
|
|
49
82
|
},
|
|
50
83
|
},
|
|
51
84
|
cellIds: [cid],
|
|
@@ -53,7 +86,6 @@ const Cell: React.FC<{
|
|
|
53
86
|
[cid]: createCellRuntimeState({
|
|
54
87
|
output: overrides.output ?? null,
|
|
55
88
|
consoleOutputs: [],
|
|
56
|
-
staleInputs: overrides.staleInputs ?? false,
|
|
57
89
|
debuggerActive: false,
|
|
58
90
|
outline: null,
|
|
59
91
|
}),
|
|
@@ -68,6 +100,9 @@ const Cell: React.FC<{
|
|
|
68
100
|
|
|
69
101
|
const store = createStore();
|
|
70
102
|
store.set(notebookAtom, notebook);
|
|
103
|
+
if (overrides.freshness !== undefined) {
|
|
104
|
+
seedFreshness(store, cid, overrides.freshness);
|
|
105
|
+
}
|
|
71
106
|
store.set(connectionAtom, { state: ConnectionState.OPEN });
|
|
72
107
|
store.set(requestClientAtom, resolveRequestClient());
|
|
73
108
|
return (
|
|
@@ -145,7 +180,7 @@ export const UnsavedEditsOutput: Story = {
|
|
|
145
180
|
<TooltipProvider>
|
|
146
181
|
<Cell
|
|
147
182
|
overrides={{
|
|
148
|
-
|
|
183
|
+
freshness: "edited",
|
|
149
184
|
output: {
|
|
150
185
|
channel: "output",
|
|
151
186
|
data: "<span class='markdown'><h1>Layout</h1>\n<p><code>marimo</code> provides functions to help you lay out your output, such as\nin rows and columns, accordions, tabs, and callouts. This tutorial\nshows some examples.</p></span>",
|
|
@@ -239,7 +274,7 @@ export const StaleStatus: Story = {
|
|
|
239
274
|
<div className="p-20 max-w-4xl">
|
|
240
275
|
<Cell
|
|
241
276
|
overrides={{
|
|
242
|
-
|
|
277
|
+
freshness: "replaced",
|
|
243
278
|
output: {
|
|
244
279
|
channel: "output",
|
|
245
280
|
data: "This data is stale because a parent is disabled",
|
|
@@ -257,8 +292,7 @@ export const StaleAndEditedStatus: Story = {
|
|
|
257
292
|
<div className="p-20 max-w-4xl">
|
|
258
293
|
<Cell
|
|
259
294
|
overrides={{
|
|
260
|
-
|
|
261
|
-
edited: true,
|
|
295
|
+
freshness: "both",
|
|
262
296
|
output: {
|
|
263
297
|
channel: "output",
|
|
264
298
|
data: "This data is stale because a parent is disabled, but this cell has been edited since.",
|
|
@@ -281,7 +315,7 @@ export const DisabledAndStaleStatus: Story = {
|
|
|
281
315
|
disabled: true,
|
|
282
316
|
hide_code: false,
|
|
283
317
|
},
|
|
284
|
-
|
|
318
|
+
freshness: "replaced",
|
|
285
319
|
output: {
|
|
286
320
|
channel: "output",
|
|
287
321
|
data: "<span class='markdown'><h1>Layout</h1>\n<p><code>marimo</code> provides functions to help you lay out your output, such as\nin rows and columns, accordions, tabs, and callouts. This tutorial\nshows some examples.</p></span>",
|
|
@@ -35,20 +35,11 @@ const createLongReprNotebook = (
|
|
|
35
35
|
|
|
36
36
|
print('hello')
|
|
37
37
|
LongReprA()`,
|
|
38
|
-
edited: false,
|
|
39
38
|
config: {
|
|
40
39
|
hide_code: hideCode,
|
|
41
40
|
disabled: false,
|
|
42
41
|
column: null,
|
|
43
42
|
},
|
|
44
|
-
lastCodeRun: `class LongReprA:
|
|
45
|
-
def __repr__(self):
|
|
46
|
-
return "<dlt.Relation(dataset='" + ("jaffle_ingest_dataset_" * 80) + \\
|
|
47
|
-
"', destination='<dlt.destinations.duckdb(destination_type=\\'duckdb\\'," + \\
|
|
48
|
-
" credentials=\\'local_jaffle.duckdb\\')>')>"
|
|
49
|
-
|
|
50
|
-
print('hello')
|
|
51
|
-
LongReprA()`,
|
|
52
43
|
},
|
|
53
44
|
},
|
|
54
45
|
cellIds: [cellId],
|
|
@@ -68,7 +59,6 @@ LongReprA()`,
|
|
|
68
59
|
timestamp: 1_686_863_705,
|
|
69
60
|
},
|
|
70
61
|
],
|
|
71
|
-
staleInputs: false,
|
|
72
62
|
debuggerActive: false,
|
|
73
63
|
outline: null,
|
|
74
64
|
}),
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { MockExecutions } from "@/__mocks__/executions";
|
|
5
|
-
import { MockNotebook } from "@/__mocks__/notebook";
|
|
6
|
-
import { cellId } from "@/__tests__/branded";
|
|
7
|
-
import {
|
|
8
|
-
cellExecutionInputsAtom,
|
|
9
|
-
cellExecutionView,
|
|
10
|
-
notebookQueuedOrRunningCountAtom,
|
|
11
|
-
} from "@/core/executions";
|
|
12
|
-
import { store } from "@/core/state/jotai";
|
|
13
|
-
import { runCells } from "../useRunCells";
|
|
14
|
-
|
|
15
|
-
const ENABLED = cellId("enabled");
|
|
16
|
-
const DISABLED = cellId("disabled");
|
|
17
|
-
|
|
18
|
-
describe("runCells", () => {
|
|
19
|
-
it("sends a disabled cell to the kernel without reading it as queued", async () => {
|
|
20
|
-
MockExecutions.seed(store, []);
|
|
21
|
-
const notebook = MockNotebook.notebookState({
|
|
22
|
-
cellData: {
|
|
23
|
-
[ENABLED]: { code: "x = 1" },
|
|
24
|
-
[DISABLED]: { code: "y = 2", config: { disabled: true } },
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
const sendRun = vi.fn(async () => null);
|
|
28
|
-
|
|
29
|
-
await runCells({
|
|
30
|
-
cellIds: [ENABLED, DISABLED],
|
|
31
|
-
sendRun,
|
|
32
|
-
prepareForRun: vi.fn(),
|
|
33
|
-
notebook,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
expect(sendRun).toHaveBeenCalledWith({
|
|
37
|
-
cellIds: [ENABLED, DISABLED],
|
|
38
|
-
codes: ["x = 1", "y = 2"],
|
|
39
|
-
});
|
|
40
|
-
const inputs = store.get(cellExecutionInputsAtom);
|
|
41
|
-
expect(cellExecutionView(inputs, ENABLED).status).toBe("queued");
|
|
42
|
-
expect(cellExecutionView(inputs, DISABLED).status).toBe("idle");
|
|
43
|
-
expect(store.get(notebookQueuedOrRunningCountAtom)).toBe(1);
|
|
44
|
-
});
|
|
45
|
-
});
|