@marimo-team/islands 0.25.3-dev290 → 0.25.3-dev291
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-DwcFY0Nf.js → ErrorBoundary-Df7sOY5r.js} +1 -1
- package/dist/{chat-ui-ByGe2rP1.js → chat-ui-CoAmcc-W.js} +3 -3
- package/dist/{code-visibility-DSS4dPIe.js → code-visibility-DO6O_8h-.js} +2 -2
- package/dist/{common-CHNDbyF2.js → common-Coz-wkOd.js} +2312 -2285
- package/dist/{data-grid-overlay-editor-8fOxyvQO.js → data-grid-overlay-editor-LbsctrY7.js} +1 -1
- package/dist/{error-entries-9U4BFHjf.js → error-entries-CQl9aZz1.js} +539 -530
- package/dist/{feedback-button-C7Wzdmjf.js → feedback-button-DQXS8piQ.js} +3 -3
- package/dist/{glide-data-editor-BA2VyhpI.js → glide-data-editor-CiTOpufo.js} +3 -3
- package/dist/{globals-DLzuY2gk.js → globals-Bmz_qpY8.js} +1 -1
- package/dist/{html-to-image-BYh_MorV.js → html-to-image-DbxMq9QA.js} +1 -1
- package/dist/main.js +10 -10
- package/dist/{number-overlay-editor-BpuvB3ho.js → number-overlay-editor-CXO_e1Yo.js} +1 -1
- package/dist/{process-output-bS3MGJ3f.js → process-output-BHsfwmw9.js} +2 -2
- package/dist/{reveal-component-BKBSU4IA.js → reveal-component-CLRZbwl1.js} +3 -3
- package/package.json +1 -1
- package/src/components/editor/output/MarimoErrorOutput.tsx +39 -3
- package/src/core/dom/outline.ts +4 -1
- package/src/core/executions/__tests__/hooks.test.tsx +46 -1
- package/src/core/executions/__tests__/output.test.ts +266 -0
- package/src/core/executions/atoms.ts +35 -0
- package/src/core/executions/hooks.ts +44 -5
- package/src/core/executions/index.ts +16 -1
- package/src/core/executions/output.ts +242 -0
|
@@ -5,8 +5,14 @@ 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 {
|
|
8
|
+
import { parseOutline } from "@/core/dom/outline";
|
|
9
|
+
import {
|
|
10
|
+
cellExecutionInputsAtom,
|
|
11
|
+
cellFreshnessAtom,
|
|
12
|
+
cellOutputInputsAtom,
|
|
13
|
+
} from "./atoms";
|
|
9
14
|
import type { CellFreshness } from "./freshness";
|
|
15
|
+
import { type CellOutput, cellOutputView, scratchOutputView } from "./output";
|
|
10
16
|
import { type CellExecutionView, cellExecutionView } from "./select";
|
|
11
17
|
|
|
12
18
|
/**
|
|
@@ -32,11 +38,10 @@ export function useCellExecution(id: Protocol.CellId): CellExecutionView {
|
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/**
|
|
35
|
-
* Whether a cell's result is current, by the host protocol's rules
|
|
36
|
-
* the confirmed document and the executions mirror.
|
|
41
|
+
* Whether a cell's result is current, by the host protocol's rules.
|
|
37
42
|
*
|
|
38
|
-
* The
|
|
39
|
-
*
|
|
43
|
+
* The comparison reads the confirmed document, so an edit counts once the
|
|
44
|
+
* host has it, not on the keystroke.
|
|
40
45
|
*/
|
|
41
46
|
export function useCellFreshness(id: Protocol.CellId): CellFreshness {
|
|
42
47
|
const cellAtom = useMemo(
|
|
@@ -45,3 +50,37 @@ export function useCellFreshness(id: Protocol.CellId): CellFreshness {
|
|
|
45
50
|
);
|
|
46
51
|
return useAtomValue(cellAtom);
|
|
47
52
|
}
|
|
53
|
+
|
|
54
|
+
/** What the cell shows: its running value so far, else its result's outcome. */
|
|
55
|
+
export function useCellOutput(id: Protocol.CellId): CellOutput | null {
|
|
56
|
+
const cellAtom = useMemo(
|
|
57
|
+
() =>
|
|
58
|
+
selectAtom(
|
|
59
|
+
cellOutputInputsAtom,
|
|
60
|
+
(inputs) => cellOutputView(inputs, id),
|
|
61
|
+
dequal,
|
|
62
|
+
),
|
|
63
|
+
[id],
|
|
64
|
+
);
|
|
65
|
+
return useAtomValue(cellAtom);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The headings in the cell's output, for the table of contents. */
|
|
69
|
+
export function useCellOutline(id: Protocol.CellId) {
|
|
70
|
+
const output = useCellOutput(id);
|
|
71
|
+
return useMemo(() => parseOutline(output), [output]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** What the scratchpad shows, from its latest scratch execution. */
|
|
75
|
+
export function useScratchOutput(): CellOutput | null {
|
|
76
|
+
const scratchAtom = useMemo(
|
|
77
|
+
() =>
|
|
78
|
+
selectAtom(
|
|
79
|
+
cellOutputInputsAtom,
|
|
80
|
+
(inputs) => scratchOutputView(inputs.executions),
|
|
81
|
+
dequal,
|
|
82
|
+
),
|
|
83
|
+
[],
|
|
84
|
+
);
|
|
85
|
+
return useAtomValue(scratchAtom);
|
|
86
|
+
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
export {
|
|
12
12
|
cellExecutionInputsAtom,
|
|
13
13
|
cellFreshnessAtom,
|
|
14
|
+
cellOutputInputsAtom,
|
|
15
|
+
clearCellOutputs,
|
|
14
16
|
notebookIsRunningAtom,
|
|
15
17
|
notebookQueuedOrRunningCountAtom,
|
|
16
18
|
observeCellMessage,
|
|
@@ -20,7 +22,20 @@ export {
|
|
|
20
22
|
withdrawCells,
|
|
21
23
|
} from "./atoms";
|
|
22
24
|
export type { CellFreshness, CellFreshnessLookup } from "./freshness";
|
|
23
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
useCellExecution,
|
|
27
|
+
useCellFreshness,
|
|
28
|
+
useCellOutline,
|
|
29
|
+
useCellOutput,
|
|
30
|
+
useScratchOutput,
|
|
31
|
+
} from "./hooks";
|
|
32
|
+
export {
|
|
33
|
+
type CellError,
|
|
34
|
+
type CellOutput,
|
|
35
|
+
cellOutputView,
|
|
36
|
+
type HostDiagnostic,
|
|
37
|
+
scratchOutputView,
|
|
38
|
+
} from "./output";
|
|
24
39
|
export {
|
|
25
40
|
type CellExecutionInputs,
|
|
26
41
|
type CellExecutionView,
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a cell shows in its output area, read from the host's executions.
|
|
5
|
+
*
|
|
6
|
+
* A completed result's outcome is the output. While the cell runs again,
|
|
7
|
+
* the running execution's value so far takes its place, so appended
|
|
8
|
+
* output appears as it arrives. The view keeps the message shape the
|
|
9
|
+
* renderers take, so they need not know where it came from.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type * as Protocol from "@marimo-team/client/protocol";
|
|
13
|
+
import type { CellId } from "@/core/cells/ids";
|
|
14
|
+
import type { MarimoError, OutputMessage } from "@/core/kernel/messages";
|
|
15
|
+
import type { Executions, Results } from "@/core/notebook/replica";
|
|
16
|
+
|
|
17
|
+
/** A host error shown as its message and the cells involved. */
|
|
18
|
+
export interface HostDiagnostic {
|
|
19
|
+
readonly type: "diagnostic";
|
|
20
|
+
readonly kind: string;
|
|
21
|
+
readonly msg: string;
|
|
22
|
+
readonly related: readonly {
|
|
23
|
+
readonly cell_id: CellId;
|
|
24
|
+
readonly message: string;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** An error the output area renders: a kernel shape or a host diagnostic. */
|
|
29
|
+
export type CellError = MarimoError | HostDiagnostic;
|
|
30
|
+
|
|
31
|
+
/** An output message whose error data may carry host diagnostics. */
|
|
32
|
+
export type CellOutput = Omit<OutputMessage, "data"> & {
|
|
33
|
+
readonly data: OutputMessage["data"] | readonly CellError[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** What every cell's output derives from. */
|
|
37
|
+
export interface CellOutputInputs {
|
|
38
|
+
readonly executions: Executions;
|
|
39
|
+
readonly results: Results;
|
|
40
|
+
/** Results this browser cleared, by execution id, until a newer one lands. */
|
|
41
|
+
readonly cleared: ReadonlyMap<Protocol.CellId, Protocol.ExecutionId>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ERROR_MIMETYPE = "application/vnd.marimo+error";
|
|
45
|
+
|
|
46
|
+
/** The protocol's ids are plain strings; the editor brands them. */
|
|
47
|
+
function brand(id: Protocol.CellId): CellId {
|
|
48
|
+
return id as CellId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function related(
|
|
52
|
+
cells: readonly Protocol.Related[],
|
|
53
|
+
): HostDiagnostic["related"] {
|
|
54
|
+
return cells.map(({ cell_id, message }) => ({
|
|
55
|
+
cell_id: brand(cell_id),
|
|
56
|
+
message,
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function seconds(iso: string | null): number {
|
|
61
|
+
return iso === null ? 0 : Date.parse(iso) / 1000;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function endedAt(record: Protocol.Execution): number {
|
|
65
|
+
return seconds(record.completed_at ?? record.started_at);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function valueOutput(
|
|
69
|
+
record: Protocol.Execution,
|
|
70
|
+
value: Protocol.DisplayData,
|
|
71
|
+
): CellOutput {
|
|
72
|
+
return {
|
|
73
|
+
channel: "output",
|
|
74
|
+
mimetype: value.mimetype as OutputMessage["mimetype"],
|
|
75
|
+
data: value.data,
|
|
76
|
+
timestamp: endedAt(record),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function errorOutput(
|
|
81
|
+
record: Protocol.Execution,
|
|
82
|
+
data: CellError[],
|
|
83
|
+
): CellOutput {
|
|
84
|
+
return {
|
|
85
|
+
channel: "marimo-error",
|
|
86
|
+
mimetype: ERROR_MIMETYPE,
|
|
87
|
+
data,
|
|
88
|
+
timestamp: endedAt(record),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The cell an exception was blamed on, when the host names one. */
|
|
93
|
+
function raisingCell(error: Protocol.Error): CellId | null {
|
|
94
|
+
const raised = error.related.find((cell) => cell.message === "raised");
|
|
95
|
+
return raised === undefined ? null : brand(raised.cell_id);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A host error in the shape the output area renders.
|
|
100
|
+
*
|
|
101
|
+
* Exceptions, syntax errors, and internal errors keep the kernel's shapes,
|
|
102
|
+
* which the renderer treats with their own affordances. Every other kind
|
|
103
|
+
* is a diagnostic.
|
|
104
|
+
*/
|
|
105
|
+
export function cellError(error: Protocol.Error): CellError {
|
|
106
|
+
switch (error.kind) {
|
|
107
|
+
case "exception":
|
|
108
|
+
return {
|
|
109
|
+
type: "exception",
|
|
110
|
+
exception_type: error.name ?? "Exception",
|
|
111
|
+
msg: error.message,
|
|
112
|
+
raising_cell: raisingCell(error),
|
|
113
|
+
traceback: error.traceback?.data ?? null,
|
|
114
|
+
};
|
|
115
|
+
case "invalid-syntax":
|
|
116
|
+
return {
|
|
117
|
+
type: "syntax",
|
|
118
|
+
msg: error.message,
|
|
119
|
+
lineno: error.range === null ? null : error.range.start.line + 1,
|
|
120
|
+
};
|
|
121
|
+
case "internal":
|
|
122
|
+
return { type: "unknown", msg: error.message, error_type: error.name };
|
|
123
|
+
case "interrupted":
|
|
124
|
+
return { type: "interruption" };
|
|
125
|
+
default:
|
|
126
|
+
return {
|
|
127
|
+
type: "diagnostic",
|
|
128
|
+
kind: error.kind,
|
|
129
|
+
msg: error.message,
|
|
130
|
+
related: related(error.related),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The notice a skipped cell shows in place of a value. */
|
|
136
|
+
function skippedOutput(
|
|
137
|
+
record: Protocol.Execution,
|
|
138
|
+
outcome: Protocol.SkippedOutcome,
|
|
139
|
+
): CellOutput {
|
|
140
|
+
const cause = outcome.related[0];
|
|
141
|
+
if (cause !== undefined && outcome.reason === "upstream-stopped") {
|
|
142
|
+
return errorOutput(record, [
|
|
143
|
+
{
|
|
144
|
+
type: "ancestor-stopped",
|
|
145
|
+
msg: outcome.message,
|
|
146
|
+
raising_cell: brand(cause.cell_id),
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
150
|
+
if (cause !== undefined && outcome.reason === "upstream-failed") {
|
|
151
|
+
const blamed = outcome.related
|
|
152
|
+
.slice(1)
|
|
153
|
+
.find((cell) => cell.cell_id !== record.cell_id);
|
|
154
|
+
return errorOutput(record, [
|
|
155
|
+
{
|
|
156
|
+
type: "ancestor-prevented",
|
|
157
|
+
msg: outcome.message,
|
|
158
|
+
raising_cell: brand(cause.cell_id),
|
|
159
|
+
blamed_cell: blamed === undefined ? null : brand(blamed.cell_id),
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
return errorOutput(record, [
|
|
164
|
+
{
|
|
165
|
+
type: "diagnostic",
|
|
166
|
+
kind: outcome.reason,
|
|
167
|
+
msg: outcome.message,
|
|
168
|
+
related: related(outcome.related),
|
|
169
|
+
},
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** What an execution's outcome shows, or null for an empty value. */
|
|
174
|
+
export function outcomeOutput(record: Protocol.Execution): CellOutput | null {
|
|
175
|
+
const outcome = record.outcome;
|
|
176
|
+
if (outcome === null) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
switch (outcome.kind) {
|
|
180
|
+
case "value":
|
|
181
|
+
return outcome.display === null
|
|
182
|
+
? null
|
|
183
|
+
: valueOutput(record, outcome.display);
|
|
184
|
+
case "error":
|
|
185
|
+
return errorOutput(record, [cellError(outcome.error)]);
|
|
186
|
+
case "interrupted":
|
|
187
|
+
return errorOutput(record, [{ type: "interruption" }]);
|
|
188
|
+
case "skipped":
|
|
189
|
+
return skippedOutput(record, outcome);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* What a cell shows: its running value so far, else its result's outcome.
|
|
195
|
+
*
|
|
196
|
+
* A running execution's outcome is null until it shows something, so the
|
|
197
|
+
* result stays up until then. Once it has shown, an empty display means
|
|
198
|
+
* the cell cleared its output, and nothing shows.
|
|
199
|
+
*/
|
|
200
|
+
export function cellOutputView(
|
|
201
|
+
inputs: CellOutputInputs,
|
|
202
|
+
id: Protocol.CellId,
|
|
203
|
+
): CellOutput | null {
|
|
204
|
+
const latestId = inputs.executions.latest.get(id);
|
|
205
|
+
const latest =
|
|
206
|
+
latestId === undefined
|
|
207
|
+
? undefined
|
|
208
|
+
: inputs.executions.executions.get(latestId)?.record;
|
|
209
|
+
if (latest !== undefined && latest.outcome !== null) {
|
|
210
|
+
if (latest.status !== "completed") {
|
|
211
|
+
return outcomeOutput(latest);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const result = inputs.results.get(id);
|
|
215
|
+
if (result === null || result === undefined) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
if (inputs.cleared.get(id) === result.id) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return outcomeOutput(result);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** What the scratchpad shows, from its latest scratch execution. */
|
|
225
|
+
export function scratchOutputView(executions: Executions): CellOutput | null {
|
|
226
|
+
let scratch: Protocol.Execution | undefined;
|
|
227
|
+
for (const { record } of executions.executions.values()) {
|
|
228
|
+
if (record.cell_id !== null) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
scratch === undefined ||
|
|
233
|
+
seconds(record.started_at) >= seconds(scratch.started_at)
|
|
234
|
+
) {
|
|
235
|
+
scratch = record;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (scratch === undefined) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
return outcomeOutput(scratch);
|
|
242
|
+
}
|