@marimo-team/islands 0.25.3-dev222 → 0.25.3-dev224

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 (30) hide show
  1. package/dist/{ErrorBoundary-Cg9q4kUW.js → ErrorBoundary-Ba6GHuhB.js} +1 -1
  2. package/dist/{code-visibility-Dp-DkL1e.js → code-visibility-BXNYYFb5.js} +1 -1
  3. package/dist/{common-DHr2MXY3.js → common-BHSOMSDG.js} +1 -1
  4. package/dist/{data-grid-overlay-editor-B-OJHxuO.js → data-grid-overlay-editor-CGLPw73V.js} +1 -1
  5. package/dist/{feedback-button-DGAcA9xH.js → feedback-button-BUf6rqKQ.js} +2 -2
  6. package/dist/{glide-data-editor-09FJOvJw.js → glide-data-editor-Dm3DXKHl.js} +3 -3
  7. package/dist/{globals-2eTRp9qC.js → globals-DjSaXJeh.js} +1 -1
  8. package/dist/main.js +4179 -4062
  9. package/dist/{number-overlay-editor-XJ1jA8Jv.js → number-overlay-editor-BVAnAmYf.js} +1 -1
  10. package/dist/{reveal-component-DxdLWQiq.js → reveal-component-Cfoa8xAb.js} +2 -2
  11. package/dist/style.css +1 -1
  12. package/package.json +1 -1
  13. package/src/components/app-config/ai-config.tsx +148 -147
  14. package/src/components/app-config/app-config-button.tsx +6 -5
  15. package/src/components/app-config/common.tsx +7 -9
  16. package/src/components/app-config/data-form.tsx +112 -113
  17. package/src/components/app-config/mcp-config.tsx +30 -53
  18. package/src/components/app-config/user-config-form.css +91 -0
  19. package/src/components/app-config/user-config-form.tsx +236 -298
  20. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +9 -5
  21. package/src/components/editor/actions/__tests__/pair-with-agent.test.tsx +3 -3
  22. package/src/components/editor/actions/pair-with-agent-modal.tsx +5 -5
  23. package/src/components/editor/actions/pair-with-agent.tsx +10 -7
  24. package/src/core/notebook/replica/__tests__/executions.test.ts +201 -0
  25. package/src/core/notebook/replica/__tests__/replica.test.ts +6 -0
  26. package/src/core/notebook/replica/__tests__/results.test.ts +193 -0
  27. package/src/core/notebook/replica/index.ts +7 -0
  28. package/src/core/notebook/replica/replica.ts +34 -2
  29. package/src/core/notebook/replica/resources/executions.ts +157 -0
  30. package/src/core/notebook/replica/resources/results.ts +48 -0
@@ -102,11 +102,14 @@ describe("PairWithAgentModal", () => {
102
102
  expect(
103
103
  panel.textContent?.includes("Agent connections require OpenCode 2.x"),
104
104
  ).toBe(tab === "OpenCode");
105
- expect(panel).toHaveTextContent(
106
- tab === "Other Agents"
107
- ? "1. Copy this prompt"
108
- : "1. Run in your terminal",
109
- );
105
+ expect(
106
+ within(panel).getByText(
107
+ tab === "Other Agents"
108
+ ? "Copy this prompt into your agent"
109
+ : "Run in your terminal",
110
+ { exact: true },
111
+ ),
112
+ ).toBeInTheDocument();
110
113
  fireEvent.click(
111
114
  within(panel).getByRole("button", {
112
115
  name: tab === "Other Agents" ? "Copy prompt" : "Copy command",
@@ -364,6 +367,7 @@ describe("PairWithAgentModal", () => {
364
367
  });
365
368
 
366
369
  const panel = screen.getByRole("tabpanel");
370
+ expect(panel).toHaveTextContent("1. Run in your terminal");
367
371
  expect(panel).toHaveTextContent("2. Paste when prompted for a token");
368
372
  expect(panel).not.toHaveTextContent("secret-token");
369
373
  fireEvent.click(
@@ -60,18 +60,18 @@ describe("PairWithAgent", () => {
60
60
  ).toBeVisible();
61
61
  expect(
62
62
  screen.getByRole("button", {
63
- name: "Pair your agent",
63
+ name: "Set up pairing",
64
64
  }),
65
65
  ).toBeVisible();
66
66
  });
67
67
 
68
- it("opens the pairing dialog from the banner description", () => {
68
+ it("opens the pairing dialog from the banner button", () => {
69
69
  stubTokenFetch();
70
70
  render(<PairWithAgentBanner />, { wrapper });
71
71
 
72
72
  fireEvent.click(
73
73
  screen.getByRole("button", {
74
- name: "Pair your agent",
74
+ name: "Set up pairing",
75
75
  }),
76
76
  );
77
77
 
@@ -118,7 +118,7 @@ export const PairWithAgentModal: React.FC<{
118
118
  <DialogHeader>
119
119
  <DialogTitle>Pair an agent</DialogTitle>
120
120
  <DialogDescription>
121
- Use an AI coding agent to pair-program on this notebook.{" "}
121
+ Use an AI coding agent to pair program on this notebook.{" "}
122
122
  <a
123
123
  href="https://links.marimo.app/marimo-pair"
124
124
  target="_blank"
@@ -159,7 +159,7 @@ export const PairWithAgentModal: React.FC<{
159
159
  className="mt-4 flex flex-col gap-4"
160
160
  >
161
161
  <Step
162
- index={1}
162
+ index={hasToken ? 1 : undefined}
163
163
  title="Run in your terminal"
164
164
  hint={
165
165
  hostEnabled && tab === "opencode"
@@ -191,7 +191,6 @@ export const PairWithAgentModal: React.FC<{
191
191
 
192
192
  <TabsContent value="prompt" className="mt-4 flex flex-col gap-4">
193
193
  <Step
194
- index={1}
195
194
  title="Copy this prompt into your agent"
196
195
  hint={
197
196
  hasToken
@@ -224,7 +223,7 @@ export const PairWithAgentModal: React.FC<{
224
223
  };
225
224
 
226
225
  const Step: React.FC<{
227
- index: number;
226
+ index?: number;
228
227
  title: string;
229
228
  hint?: string;
230
229
  children: React.ReactNode;
@@ -232,7 +231,8 @@ const Step: React.FC<{
232
231
  <div className="flex flex-col gap-2">
233
232
  <div className="flex items-baseline gap-2">
234
233
  <span className="text-sm font-medium">
235
- {index}. {title}
234
+ {index !== undefined && `${index}. `}
235
+ {title}
236
236
  </span>
237
237
  {hint && <span className="text-xs text-muted-foreground">{hint}</span>}
238
238
  </div>
@@ -1,6 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { BotIcon } from "lucide-react";
3
+ import { ArrowUpRightIcon, BotIcon } from "lucide-react";
4
4
  import React from "react";
5
5
  import { AiProviderIcon } from "@/components/ai/ai-provider-icon";
6
6
  import { useImperativeModal } from "@/components/modal/ImperativeModal";
@@ -72,7 +72,7 @@ export const PairWithAgentBanner: React.FC<{
72
72
  return (
73
73
  <div
74
74
  className={cn(
75
- "rounded-lg border border-(--blue-6) bg-(--blue-2)/50 p-3",
75
+ "rounded-lg border border-border bg-muted/30 p-3",
76
76
  className,
77
77
  )}
78
78
  >
@@ -103,18 +103,21 @@ export const PairWithAgentBanner: React.FC<{
103
103
  </fieldset>
104
104
  </div>
105
105
  <div className="mt-1 text-xs leading-relaxed">
106
- <p className="inline">
106
+ <p>
107
107
  Pair your coding agent with this notebook to edit and run cells,
108
108
  inspect variables, and more, straight from your agent's CLI or user
109
109
  interface.
110
110
  </p>
111
- <button
111
+ <Button
112
112
  type="button"
113
+ variant="outline"
114
+ size="xs"
113
115
  onClick={openPairWithAgentModal}
114
- className="text-link hover:underline ml-1"
116
+ className="mt-3 gap-1"
115
117
  >
116
- Pair your agent
117
- </button>
118
+ Set up pairing
119
+ <ArrowUpRightIcon className="size-3.5" aria-hidden={true} />
120
+ </Button>
118
121
  </div>
119
122
  </div>
120
123
  );
@@ -0,0 +1,201 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import * as Protocol from "@marimo-team/client/protocol";
5
+ import { executions, itemsInOrder } from "../resources/executions";
6
+ import { createNotebookReducer } from "@marimo-team/client";
7
+ import { fixtureEvents } from "./fixtures";
8
+
9
+ const reducer = createNotebookReducer({ executions });
10
+ const snapshot = fixtureEvents("notebook-events.sse");
11
+ const runCells = fixtureEvents("run-cells.sse");
12
+ const inputs = fixtureEvents("inputs.sse");
13
+ const time = "2026-10-01T09:30:00Z";
14
+
15
+ function replay(
16
+ events: readonly Protocol.EventOutput[],
17
+ from = reducer.initial,
18
+ ) {
19
+ return events.reduce(reducer.reduce, from);
20
+ }
21
+
22
+ const resource = (state: ReturnType<typeof replay>) =>
23
+ state.resources.executions;
24
+
25
+ /** Build one event as the stream would deliver it. */
26
+ const event = (message: unknown) => Protocol.Event.parse(message);
27
+
28
+ const removed = (kind: string, id: string, cursor: string) =>
29
+ event({ event: `${kind}.removed`, id: cursor, data: { id, time } });
30
+
31
+ /** How the host retires a cell execution: its items first, then itself. */
32
+ const retire = (executionId: string, variableId: string) => [
33
+ removed("variable", variableId, "111"),
34
+ removed("execution", executionId, "112"),
35
+ ];
36
+
37
+ const ids = (holder: Parameters<typeof itemsInOrder>[0]) =>
38
+ itemsInOrder(holder).map((item) => item.id);
39
+
40
+ describe("executions resource", () => {
41
+ it("keeps scratch work from the snapshot and completes it after ready", () => {
42
+ const ready = snapshot.findIndex((item) => item.event === "ready");
43
+ const opening = resource(replay(snapshot.slice(0, ready + 1)));
44
+ const scratch = opening.executions.get("e7");
45
+ expect(scratch?.record.status).toBe("running");
46
+ expect(itemsInOrder(scratch!).map((item) => item.kind)).toEqual([
47
+ "console",
48
+ ]);
49
+ expect(opening.runs.get("run-scratch")?.status).toBe("running");
50
+ expect(opening.latest.size).toBe(0);
51
+
52
+ const done = resource(replay(snapshot));
53
+ expect(done.executions.get("e7")?.record.status).toBe("completed");
54
+ expect(done.runs.get("run-scratch")?.outcome).toBe("succeeded");
55
+ const released = reducer.reduce(
56
+ replay(snapshot),
57
+ removed("run", "run-scratch", "98"),
58
+ );
59
+ expect(resource(released).runs.has("run-scratch")).toBe(false);
60
+ });
61
+
62
+ it("indexes a cell's execution while it is on the stream", () => {
63
+ const start = replay(snapshot);
64
+ const running = resource(replay(runCells.slice(0, 5), start));
65
+ expect(running.latest.get("c3")).toBe("e12");
66
+ expect(running.executions.get("e12")?.record.status).toBe("running");
67
+ expect(running.latest.get("k7f2")).toBe("e13");
68
+ expect(running.runs.get("run-cells")?.status).toBe("running");
69
+
70
+ const done = replay(runCells, start);
71
+ const e12 = resource(done).executions.get("e12");
72
+ expect(e12?.record.status).toBe("completed");
73
+ expect([...e12!.variables.values()].map((v) => v.name)).toEqual(["x"]);
74
+ expect(resource(done).executions.get("e13")?.record.depends_on).toEqual({
75
+ x: { execution: "e12", update: 0 },
76
+ });
77
+ expect(resource(done).runs.get("run-cells")?.outcome).toBe("succeeded");
78
+
79
+ const retired = replay(retire("e12", "e12-x"), done);
80
+ expect(resource(retired).executions.has("e12")).toBe(false);
81
+ expect(resource(retired).latest.has("c3")).toBe(false);
82
+ const queued = reducer.reduce(
83
+ retired,
84
+ event({
85
+ event: "execution",
86
+ id: "113",
87
+ data: {
88
+ execution: {
89
+ ...e12!.record,
90
+ id: "e14",
91
+ run_id: "run-again",
92
+ status: "queued",
93
+ outcome: null,
94
+ started_at: null,
95
+ completed_at: null,
96
+ },
97
+ },
98
+ }),
99
+ );
100
+ expect(resource(queued).latest.get("c3")).toBe("e14");
101
+ expect([...resource(queued).executions.keys()]).toEqual([
102
+ "e7",
103
+ "e13",
104
+ "e14",
105
+ ]);
106
+ });
107
+
108
+ it("applies item and variable removals to the stream's copy", () => {
109
+ const done = replay(runCells, replay(snapshot));
110
+ const late = {
111
+ id: "o9",
112
+ execution_id: "e12",
113
+ seq: 0,
114
+ name: "stdout",
115
+ text: "late\n",
116
+ };
117
+ const shown = {
118
+ id: "d1",
119
+ execution_id: "e12",
120
+ seq: 1,
121
+ mimetype: "text/plain",
122
+ data: "figure",
123
+ };
124
+ const added = replay(
125
+ [
126
+ event({ event: "console", id: "111", data: { console: late } }),
127
+ event({ event: "display", id: "112", data: { display: shown } }),
128
+ ],
129
+ done,
130
+ );
131
+ expect(ids(resource(added).executions.get("e12")!)).toEqual(["o9", "d1"]);
132
+
133
+ const cleared = replay(
134
+ [
135
+ removed("display", "d1", "113"),
136
+ removed("console", "o9", "114"),
137
+ removed("variable", "e12-x", "115"),
138
+ ],
139
+ added,
140
+ );
141
+ const e12 = resource(cleared).executions.get("e12")!;
142
+ expect(ids(e12)).toEqual([]);
143
+ expect(e12.variables.size).toBe(0);
144
+ expect(resource(cleared).latest.get("c3")).toBe("e12");
145
+ });
146
+
147
+ it("orders items by seq, replaces them by id, and drops orphans", () => {
148
+ const start = replay(snapshot);
149
+ const prompted = resource(replay(inputs.slice(0, -1), start));
150
+ const execution = prompted.executions.get("e-input");
151
+ expect(
152
+ itemsInOrder(execution!).map((item) => [
153
+ item.kind,
154
+ item.id,
155
+ item.kind === "input" ? item.status : item.seq,
156
+ ]),
157
+ ).toEqual([
158
+ ["console", "o-input-0", 0],
159
+ ["input", "i1", "answered"],
160
+ ["console", "o-input-2", 2],
161
+ ["input", "i2", "cancelled"],
162
+ ]);
163
+ expect(prompted.latest.size).toBe(0);
164
+ const gone = resource(replay(inputs, start));
165
+ expect(gone.executions.has("e-input")).toBe(false);
166
+ expect(gone.runs.get("run-input")?.outcome).toBe("interrupted");
167
+
168
+ const orphan = reducer.reduce(
169
+ start,
170
+ event({
171
+ event: "console",
172
+ id: "400",
173
+ data: {
174
+ console: {
175
+ id: "o-orphan",
176
+ execution_id: "nope",
177
+ seq: 0,
178
+ name: "stdout",
179
+ text: "",
180
+ },
181
+ },
182
+ }),
183
+ );
184
+ expect(orphan.resources).toBe(start.resources);
185
+ });
186
+
187
+ it("ignores removals delivered again", () => {
188
+ const retired = replay(
189
+ retire("e12", "e12-x"),
190
+ replay(runCells, replay(snapshot)),
191
+ );
192
+ const again = replay(retire("e12", "e12-x"), retired);
193
+ expect(again.resources).toBe(retired.resources);
194
+ });
195
+
196
+ it("clears on reset", () => {
197
+ const done = replay(runCells, replay(snapshot));
198
+ const reset = reducer.reduce(done, { event: "reset", data: {} });
199
+ expect(resource(reset)).toBe(executions.initial);
200
+ });
201
+ });
@@ -231,6 +231,8 @@ describe("NotebookReplica lifetime", () => {
231
231
  "k7f2",
232
232
  ]);
233
233
  expect(store.get(client.runtimeAtom)?.id).toBe("r1");
234
+ expect(store.get(client.executionsAtom).runs.has("run-scratch")).toBe(true);
235
+ expect(store.get(client.resultsAtom).size).toBe(3);
234
236
  attempt.drop();
235
237
  await backoff();
236
238
  expect(store.get(client.documentAtom)?.revision.order).toEqual([
@@ -239,8 +241,12 @@ describe("NotebookReplica lifetime", () => {
239
241
  "k7f2",
240
242
  ]);
241
243
  expect(store.get(client.runtimeAtom)).toBeNull();
244
+ expect(store.get(client.executionsAtom).runs.has("run-scratch")).toBe(true);
245
+ expect(store.get(client.resultsAtom).size).toBe(3);
242
246
  current.abort();
243
247
  expect(store.get(client.documentAtom)).toBeNull();
248
+ expect(store.get(client.executionsAtom).runs.size).toBe(0);
249
+ expect(store.get(client.resultsAtom).size).toBe(0);
244
250
  });
245
251
 
246
252
  it.each([
@@ -0,0 +1,193 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import * as Protocol from "@marimo-team/client/protocol";
5
+ import { results } from "../resources/results";
6
+ import { createNotebookReducer } from "@marimo-team/client";
7
+ import { fixtureEvents } from "./fixtures";
8
+
9
+ const reducer = createNotebookReducer({ results });
10
+ const snapshot = fixtureEvents("notebook-events.sse");
11
+ const runCells = fixtureEvents("run-cells.sse");
12
+ const time = "2026-10-01T09:30:00Z";
13
+
14
+ function replay(
15
+ events: readonly Protocol.EventOutput[],
16
+ from = reducer.initial,
17
+ ) {
18
+ return events.reduce(reducer.reduce, from);
19
+ }
20
+
21
+ const resource = (state: ReturnType<typeof replay>) => state.resources.results;
22
+
23
+ /** Build one event as the stream would deliver it. */
24
+ const event = (message: unknown) => Protocol.Event.parse(message);
25
+
26
+ const removed = (kind: string, id: string, cursor: string) =>
27
+ event({ event: `${kind}.removed`, id: cursor, data: { id, time } });
28
+
29
+ /** How the host retires a cell execution: its items first, then itself. */
30
+ const retire = (executionId: string, variableId: string) => [
31
+ removed("variable", variableId, "111"),
32
+ removed("execution", executionId, "112"),
33
+ ];
34
+
35
+ const restored = {
36
+ id: "e-restored",
37
+ run_id: null,
38
+ runtime_id: null,
39
+ caused_by: null,
40
+ cell_id: "c3",
41
+ code: null,
42
+ code_hash: "0f71456db2ea83f4",
43
+ depends_on: {},
44
+ status: "completed",
45
+ outcome: { kind: "value", display: null },
46
+ started_at: "2026-09-30T09:20:01Z",
47
+ completed_at: "2026-09-30T09:20:02Z",
48
+ };
49
+
50
+ describe("results resource", () => {
51
+ it("has an entry per document cell, filled by a restored result", () => {
52
+ const ready = snapshot.findIndex((item) => item.event === "ready");
53
+ const opening = resource(replay(snapshot.slice(0, ready + 1)));
54
+ expect([...opening]).toEqual([
55
+ ["c1", null],
56
+ ["c3", null],
57
+ ["k7f2", null],
58
+ ]);
59
+
60
+ const opened = resource(
61
+ replay([
62
+ ...snapshot.slice(0, ready),
63
+ event({ event: "execution", id: "94", data: { execution: restored } }),
64
+ ...snapshot.slice(ready),
65
+ ]),
66
+ );
67
+ expect(opened.get("c3")?.id).toBe("e-restored");
68
+ expect(opened.get("c3")?.run_id).toBeNull();
69
+ });
70
+
71
+ it("records a cell's result when its execution completes", () => {
72
+ const start = replay(snapshot);
73
+ const running = resource(replay(runCells.slice(0, 5), start));
74
+ expect(running.get("c3")).toBeNull();
75
+ expect(running.get("k7f2")).toBeNull();
76
+
77
+ const done = resource(replay(runCells, start));
78
+ expect(done.get("c3")?.id).toBe("e12");
79
+ expect(done.get("c3")?.status).toBe("completed");
80
+ expect(done.get("k7f2")?.depends_on).toEqual({
81
+ x: { execution: "e12", update: 0 },
82
+ });
83
+ expect(done.get("c1")).toBeNull();
84
+ });
85
+
86
+ it("keeps a result while the host replaces it", () => {
87
+ const done = replay(runCells, replay(snapshot));
88
+ const result = resource(done).get("c3")!;
89
+ const retired = replay(retire("e12", "e12-x"), done);
90
+ expect(resource(retired).get("c3")).toBe(result);
91
+
92
+ const again = {
93
+ ...result,
94
+ id: "e14",
95
+ run_id: "run-again",
96
+ status: "queued",
97
+ outcome: null,
98
+ started_at: null,
99
+ completed_at: null,
100
+ };
101
+ const queued = reducer.reduce(
102
+ retired,
103
+ event({ event: "execution", id: "113", data: { execution: again } }),
104
+ );
105
+ expect(resource(queued).get("c3")).toBe(result);
106
+
107
+ const completed = reducer.reduce(
108
+ queued,
109
+ event({
110
+ event: "execution",
111
+ id: "114",
112
+ data: {
113
+ execution: {
114
+ ...again,
115
+ status: "completed",
116
+ outcome: { kind: "value", display: null },
117
+ started_at: time,
118
+ completed_at: time,
119
+ },
120
+ },
121
+ }),
122
+ );
123
+ expect(resource(completed).get("c3")?.id).toBe("e14");
124
+ });
125
+
126
+ it("replaces a result with an execution interrupted before it started", () => {
127
+ const done = replay(runCells, replay(snapshot));
128
+ const result = resource(done).get("c3")!;
129
+ const interrupted = {
130
+ ...result,
131
+ id: "e14",
132
+ run_id: "run-again",
133
+ status: "completed",
134
+ outcome: { kind: "interrupted" },
135
+ started_at: null,
136
+ completed_at: time,
137
+ };
138
+ const cancelled = replay(
139
+ [
140
+ ...retire("e12", "e12-x"),
141
+ event({
142
+ event: "execution",
143
+ id: "113",
144
+ data: {
145
+ execution: {
146
+ ...interrupted,
147
+ status: "queued",
148
+ outcome: null,
149
+ completed_at: null,
150
+ },
151
+ },
152
+ }),
153
+ event({
154
+ event: "execution",
155
+ id: "114",
156
+ data: { execution: interrupted },
157
+ }),
158
+ ],
159
+ done,
160
+ );
161
+ expect(resource(cancelled).get("c3")?.outcome).toEqual({
162
+ kind: "interrupted",
163
+ });
164
+ });
165
+
166
+ it("drops a cell deleted while its execution runs", () => {
167
+ const start = replay(runCells.slice(0, 5), replay(snapshot));
168
+ const deleted = reducer.reduce(start, removed("cell", "c3", "105"));
169
+ expect(resource(deleted).has("c3")).toBe(false);
170
+
171
+ const finished = replay(runCells.slice(5), deleted);
172
+ expect(resource(finished).has("c3")).toBe(false);
173
+ expect(resource(finished).get("k7f2")?.id).toBe("e13");
174
+ });
175
+
176
+ it("ignores cells it has not seen and clears on reset", () => {
177
+ const start = replay(snapshot);
178
+ const unknown = reducer.reduce(
179
+ start,
180
+ event({
181
+ event: "execution",
182
+ id: "98",
183
+ data: { execution: { ...restored, id: "e-zz", cell_id: "zz" } },
184
+ }),
185
+ );
186
+ expect(unknown.resources).toBe(start.resources);
187
+ const reset = reducer.reduce(replay(runCells, start), {
188
+ event: "reset",
189
+ data: {},
190
+ });
191
+ expect(resource(reset)).toBe(results.initial);
192
+ });
193
+ });
@@ -11,3 +11,10 @@ export {
11
11
  } from "./connection";
12
12
  export { createNotebookReplica, type NotebookReplica } from "./replica";
13
13
  export type { Document } from "./resources/document";
14
+ export {
15
+ type Execution,
16
+ type Executions,
17
+ type Item,
18
+ itemsInOrder,
19
+ } from "./resources/executions";
20
+ export type { Results } from "./resources/results";
@@ -24,15 +24,26 @@ import {
24
24
  } from "./connection";
25
25
  import { attachments } from "./resources/attachments";
26
26
  import { type Document, document } from "./resources/document";
27
+ import { type Executions, executions } from "./resources/executions";
28
+ import { type Results, results } from "./resources/results";
27
29
  import { runtime } from "./resources/runtime";
28
30
 
29
- /** The kinds the editor reads, with marimo's own notifications. */
31
+ /**
32
+ * The kinds the editor reads, with marimo's own notifications.
33
+ *
34
+ * Console and display items stay out until the editor reads outputs from
35
+ * the replica. The native notifications still carry them.
36
+ */
30
37
  const TYPES = [
31
38
  "notebook",
32
39
  "cell",
33
40
  "document",
34
41
  "attachment",
35
42
  "runtime",
43
+ "run",
44
+ "execution",
45
+ "input",
46
+ "variable",
36
47
  "marimo",
37
48
  ] as const;
38
49
 
@@ -66,6 +77,17 @@ export interface NotebookReplica {
66
77
  readonly attachmentsAtom: Atom<readonly Attachment[]>;
67
78
  /** The notebook's runtime, null while it has none or the stream is not ready. */
68
79
  readonly runtimeAtom: Atom<Runtime | null>;
80
+ /**
81
+ * Runs and executions as the stream reports them.
82
+ *
83
+ * Visible as events arrive, including during a snapshot, so a consumer
84
+ * joins with `documentAtom`, which is null until the snapshot is ready.
85
+ * Kept across a temporary disconnection, like the document, and cleared
86
+ * on reset or removal.
87
+ */
88
+ readonly executionsAtom: Atom<Executions>;
89
+ /** Each document cell's last completed execution record, kept while it runs again. */
90
+ readonly resultsAtom: Atom<Results>;
69
91
  /**
70
92
  * Follow a backend, replacing any current connection.
71
93
  *
@@ -124,7 +146,13 @@ export function createNotebookReplica(
124
146
  state: ConnectionState.NOT_STARTED,
125
147
  });
126
148
  // One atom holds the whole stream, which resource atoms derive from.
127
- const reducer = createNotebookReducer({ document, attachments, runtime });
149
+ const reducer = createNotebookReducer({
150
+ document,
151
+ attachments,
152
+ runtime,
153
+ executions,
154
+ results,
155
+ });
128
156
  const streamAtom = atom(reducer.initial);
129
157
  // A resumed stream is live, but presence waits for this connection's ready.
130
158
  const readyAtom = atom(false);
@@ -146,6 +174,8 @@ export function createNotebookReplica(
146
174
  const runtimeAtom = atom((get) =>
147
175
  get(presentAtom) ? get(streamAtom).resources.runtime : null,
148
176
  );
177
+ const executionsAtom = atom((get) => get(streamAtom).resources.executions);
178
+ const resultsAtom = atom((get) => get(streamAtom).resources.results);
149
179
  const backendAtom = atom<NotebookBackend | null>(null);
150
180
  /** The connection this replica follows. Connecting again ends it. */
151
181
  let current: { end(): void; reconnect(): Promise<void> } | undefined;
@@ -168,6 +198,8 @@ export function createNotebookReplica(
168
198
  backendAtom,
169
199
  attachmentsAtom,
170
200
  runtimeAtom,
201
+ executionsAtom,
202
+ resultsAtom,
171
203
  connect,
172
204
  reconnect: async () => current?.reconnect(),
173
205
  };