@marimo-team/islands 0.25.3-dev267 → 0.25.3-dev269

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 (140) hide show
  1. package/dist/{ConnectedDataExplorerComponent-C4yCN38h.js → ConnectedDataExplorerComponent-eLHPh6uu.js} +8 -8
  2. package/dist/{ErrorBoundary-CewXPyL5.js → ErrorBoundary-DCtynjHZ.js} +4 -4
  3. package/dist/{abnfDiagram-VCTEODGH-o6Ay6Q63.js → abnfDiagram-VCTEODGH-hqd0IBtX.js} +2 -2
  4. package/dist/{architectureDiagram-5GKGNRK7-BhiwwlVg.js → architectureDiagram-5GKGNRK7-BzLAFcc0.js} +1 -1
  5. package/dist/{strings-BCKpLFNU.js → assertNever-DIU3JJbt.js} +14 -14
  6. package/dist/{chat-ui-CbgR5ivy.js → chat-ui-BWIkve2R.js} +17 -17
  7. package/dist/{chunk-IMKFNOWR-WMSMf4Nj.js → chunk-IMKFNOWR-DOeVvuyE.js} +1 -1
  8. package/dist/{chunk-SHT3W25Y-BRfADrzD.js → chunk-SHT3W25Y-DZoFI8wR.js} +1 -1
  9. package/dist/{chunk-SVP7TREG-CuK5QFre.js → chunk-SVP7TREG-CWu86PSW.js} +1 -1
  10. package/dist/{chunk-TICWLB2K-Bcun_jhJ.js → chunk-TICWLB2K-BrM0GoP3.js} +1 -1
  11. package/dist/{classDiagram-ZZMXUADV-CgWDbUDT.js → classDiagram-ZZMXUADV-DYi4V2bB.js} +2 -2
  12. package/dist/{classDiagram-v2-VYDZK3BY-DYmc9SFU.js → classDiagram-v2-VYDZK3BY-tN7FIk-m.js} +2 -2
  13. package/dist/{code-visibility-BKBl9Rh6.js → code-visibility-CMF5tcNj.js} +44 -47
  14. package/dist/{common-N3Jd_6m9.js → common-DYAjQ1bK.js} +4866 -4857
  15. package/dist/{config-CmeG_Pb6.js → config-BsPruWMR.js} +2 -2
  16. package/dist/{createReducer-C_syF8Td.js → createReducer-DdtT_7ji.js} +1 -1
  17. package/dist/{cynefinDiagram-5FMLGOSQ-PPKWipjT.js → cynefinDiagram-5FMLGOSQ-D5nCnDc-.js} +1 -1
  18. package/dist/{dagre-GXQ25YYZ-niRBZh--.js → dagre-GXQ25YYZ-BHkd7f_i.js} +1 -1
  19. package/dist/{data-grid-overlay-editor-Cp59CrqZ.js → data-grid-overlay-editor-B6oND8pX.js} +1 -1
  20. package/dist/{diagram-S7CK7UJ4-DPiP9rot.js → diagram-S7CK7UJ4-D1ALppT2.js} +1 -1
  21. package/dist/{diagram-UQ7AKVKN-B0WZuaxV.js → diagram-UQ7AKVKN-DLv0b83L.js} +1 -1
  22. package/dist/{diagram-VX7I27RA-Rn42WBuN.js → diagram-VX7I27RA-7XJ7J6qg.js} +1 -1
  23. package/dist/{diagram-Z3DM3KII-BB93--ry.js → diagram-Z3DM3KII-7M7zbHp_.js} +1 -1
  24. package/dist/{dialog-BtprQCUw.js → dialog-DzCj1qQv.js} +1 -1
  25. package/dist/{dropdown-menu-BpImEfn3.js → dropdown-menu-CdXE15an.js} +3 -3
  26. package/dist/{ebnfDiagram-PWID7BFC-V4t5h6mZ.js → ebnfDiagram-PWID7BFC-DHH1qlmV.js} +2 -2
  27. package/dist/{erDiagram-RLTQ6QDP-C1MsAKXd.js → erDiagram-RLTQ6QDP-fI_0dGef.js} +1 -1
  28. package/dist/{error-entries-CJGdD2ky.js → error-entries-CxN-Nq3V.js} +519 -458
  29. package/dist/{feedback-button-Qc1f9iPq.js → feedback-button-BelKx6R5.js} +22 -22
  30. package/dist/{flowDiagram-HODETNUW-DHjeJP96.js → flowDiagram-HODETNUW-Brg9MA6i.js} +2 -2
  31. package/dist/{formats-gGospKWJ.js → formats-DbSvAPY-.js} +1 -1
  32. package/dist/{glide-data-editor-hCq3K0sd.js → glide-data-editor-CMZ-kv0S.js} +11 -11
  33. package/dist/{glide-utils-DC3sk6EX.js → glide-utils-BSw3SSHF.js} +1 -1
  34. package/dist/{globals-B2_uMYDY.js → globals-DV77npCG.js} +2 -2
  35. package/dist/{html-to-image-S7spRQCi.js → html-to-image-Bc2AvXHG.js} +5121 -5121
  36. package/dist/{infoDiagram-27XIBGKW-Dovv6iKx.js → infoDiagram-27XIBGKW-E9e403YT.js} +1 -1
  37. package/dist/{ishikawaDiagram-5VMMS53U-D2nyygsc.js → ishikawaDiagram-5VMMS53U-C-RlYDRo.js} +1 -1
  38. package/dist/{kanban-definition-UXKFOSKX-KyVx0uTY.js → kanban-definition-UXKFOSKX-CLFl_uqK.js} +1 -1
  39. package/dist/main.js +5972 -6003
  40. package/dist/{mermaid-B-jKSwGt.js → mermaid-rd09e1JL.js} +34 -34
  41. package/dist/{mindmap-definition-YA3MSWOX-Bz9tKQFl.js → mindmap-definition-YA3MSWOX-BQ8TWtyB.js} +1 -1
  42. package/dist/{number-overlay-editor-CfNYvbdz.js → number-overlay-editor-CFTuKnrY.js} +1 -1
  43. package/dist/{pegDiagram-XKGWAZYB-C_qpq6Tv.js → pegDiagram-XKGWAZYB-CN8UHFPi.js} +2 -2
  44. package/dist/{pieDiagram-E7YTZNPT-Bp9ucgI4.js → pieDiagram-E7YTZNPT-DGuJ0EBd.js} +1 -1
  45. package/dist/{process-output-f34bkXTm.js → process-output-qecD7Dk8.js} +3 -3
  46. package/dist/{railroadDiagram-O6MQD6OU-tq-VEH6j.js → railroadDiagram-O6MQD6OU-BPDA8wy8.js} +2 -2
  47. package/dist/{requirementDiagram-BXWQKSXE-DW-vHadb.js → requirementDiagram-BXWQKSXE-ChzLGbGW.js} +1 -1
  48. package/dist/{reveal-component-CUxv-kkP.js → reveal-component-Doix35eL.js} +1094 -1094
  49. package/dist/{select-C33FVPPN.js → select-OxgozOC8.js} +1 -1
  50. package/dist/{sequenceDiagram-WJ2MYXX4-Dv9U9qRP.js → sequenceDiagram-WJ2MYXX4-DFX-bE6O.js} +1 -1
  51. package/dist/{spec-DH1lmF7Q.js → spec-oJn0RhpQ.js} +2 -2
  52. package/dist/{stateDiagram-D77RDMKH-DTmPQ0EB.js → stateDiagram-D77RDMKH-BR7cI3wK.js} +2 -2
  53. package/dist/{stateDiagram-v2-MP3YSRHH-CkCif0Iw.js → stateDiagram-v2-MP3YSRHH-oGqB5UDI.js} +2 -2
  54. package/dist/{static-state-BldGd99T.js → static-state-CuV6Pc3a.js} +1 -1
  55. package/dist/{swimlanes-42K2YHIH-21kCq6l8.js → swimlanes-42K2YHIH-DwSZl1yC.js} +1 -1
  56. package/dist/{swimlanesDiagram-VR7AAH4N-BpRJP6KU.js → swimlanesDiagram-VR7AAH4N-BHlcd-Ri.js} +2 -2
  57. package/dist/{timeline-definition-24CTP7MA-Ckgh4lS4.js → timeline-definition-24CTP7MA-DMmP-G2O.js} +1 -1
  58. package/dist/{toast-DpczUryP.js → toast-DbBHmxmB.js} +1 -1
  59. package/dist/{useAsyncData-DsZMvNXn.js → useAsyncData-BYIAoMqn.js} +1 -1
  60. package/dist/{useDateFormatter-Dk_xFci4.js → useDateFormatter-ButWkdcK.js} +1 -1
  61. package/dist/{useDeepCompareMemoize-KMBBux8C.js → useDeepCompareMemoize-Bz0c5GN6.js} +1 -1
  62. package/dist/{useTheme-BZ2GGLgm.js → useTheme-DPLobSFs.js} +7 -7
  63. package/dist/{utils-DXkj82sK.js → utils-EBklsC5R.js} +31 -31
  64. package/dist/{vega-component-CF9kI6C4.js → vega-component-rxIxAAh9.js} +6 -6
  65. package/dist/{vennDiagram-4TSXK5OY-CDei2Bt3.js → vennDiagram-4TSXK5OY-Db8GtH1G.js} +1 -1
  66. package/dist/{wardleyDiagram-VM6X3IG4-CXavT601.js → wardleyDiagram-VM6X3IG4-DmIiC3FU.js} +1 -1
  67. package/dist/{xychartDiagram-S5SC5T6Z-CvJhcZri.js → xychartDiagram-S5SC5T6Z-DS9OPrNq.js} +1 -1
  68. package/package.json +1 -1
  69. package/src/__mocks__/executions.ts +195 -0
  70. package/src/__mocks__/notebook.ts +0 -1
  71. package/src/__tests__/CellStatus.test.tsx +19 -20
  72. package/src/components/dependency-graph/minimap-content.tsx +3 -2
  73. package/src/components/editor/__tests__/data-attributes.test.tsx +0 -7
  74. package/src/components/editor/__tests__/output-persistence.test.tsx +2 -2
  75. package/src/components/editor/actions/__tests__/useCellActionButton.test.tsx +1 -0
  76. package/src/components/editor/actions/useCellActionButton.tsx +7 -3
  77. package/src/components/editor/alerts/stdin-blocking-alert.tsx +3 -1
  78. package/src/components/editor/cell/CellStatus.tsx +23 -19
  79. package/src/components/editor/cell/RunButton.tsx +16 -6
  80. package/src/components/editor/cell/__tests__/useRunCells.test.ts +45 -0
  81. package/src/components/editor/cell/cell-context-menu.tsx +4 -1
  82. package/src/components/editor/cell/code/cell-editor.tsx +7 -5
  83. package/src/components/editor/cell/collapse.tsx +4 -0
  84. package/src/components/editor/cell/useRunCells.ts +18 -3
  85. package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -4
  86. package/src/components/editor/header/status.tsx +4 -2
  87. package/src/components/editor/navigation/navigation.ts +3 -2
  88. package/src/components/editor/notebook-cell.tsx +82 -83
  89. package/src/components/editor/renderers/grid-layout/__tests__/grid-layout.test.tsx +75 -0
  90. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +22 -16
  91. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
  92. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -33
  93. package/src/components/editor/stdin-blocking-alert.tsx +3 -1
  94. package/src/components/scratchpad/scratchpad.tsx +4 -1
  95. package/src/components/slides/minimap.tsx +4 -2
  96. package/src/components/slides/reveal-component.tsx +14 -10
  97. package/src/components/slides/slide-cell-view.tsx +28 -24
  98. package/src/components/slides/slide.tsx +3 -4
  99. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -389
  100. package/src/core/cells/__tests__/cell.test.ts +49 -154
  101. package/src/core/cells/__tests__/cells.test.ts +18 -127
  102. package/src/core/cells/__tests__/pending-delete-service.test.tsx +11 -20
  103. package/src/core/cells/__tests__/utils.test.ts +75 -247
  104. package/src/core/cells/actions.ts +5 -2
  105. package/src/core/cells/cell.ts +31 -133
  106. package/src/core/cells/cells.ts +14 -29
  107. package/src/core/cells/document.ts +3 -3
  108. package/src/core/cells/focus.ts +27 -6
  109. package/src/core/cells/layout.ts +1 -1
  110. package/src/core/cells/pending-delete-service.ts +5 -7
  111. package/src/core/cells/types.ts +13 -36
  112. package/src/core/cells/utils.ts +37 -68
  113. package/src/core/edit-app.tsx +2 -1
  114. package/src/core/executions/__tests__/atoms.test.ts +99 -0
  115. package/src/core/executions/__tests__/select.test.ts +268 -0
  116. package/src/core/executions/atoms.ts +206 -0
  117. package/src/core/executions/hooks.ts +31 -0
  118. package/src/core/executions/index.ts +31 -0
  119. package/src/core/executions/select.ts +175 -0
  120. package/src/core/export/__tests__/hooks.test.ts +0 -5
  121. package/src/core/islands/bootstrap.ts +4 -1
  122. package/src/core/islands/components/output-wrapper.tsx +5 -5
  123. package/src/core/kernel/handlers.ts +2 -11
  124. package/src/core/kernel/state.ts +0 -15
  125. package/src/core/lifecycle/render-policy.ts +12 -26
  126. package/src/core/network/__tests__/requests-lazy.test.ts +5 -5
  127. package/src/core/network/requests-lazy.ts +3 -3
  128. package/src/core/notebook/operations.ts +1 -1
  129. package/src/core/run-app.tsx +2 -1
  130. package/src/core/runtime/__tests__/adapter.test.ts +3 -3
  131. package/src/core/runtime/adapter.ts +4 -4
  132. package/src/core/session/useNotebookSession.tsx +3 -3
  133. package/src/core/wasm/bridge.ts +1 -1
  134. package/src/plugins/core/RenderHTML.tsx +3 -5
  135. package/src/plugins/core/__test__/RenderHTML-copy.test.tsx +60 -0
  136. package/src/plugins/core/registerReactComponent.tsx +4 -13
  137. package/src/stories/cell-status.stories.tsx +17 -10
  138. package/src/stories/cell.stories.tsx +0 -31
  139. package/src/stories/layout/vertical/one-column.stories.tsx +0 -9
  140. package/src/utils/time.ts +10 -0
@@ -0,0 +1,175 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /** Pure views over the replica's executions and results, by cell. */
4
+
5
+ import type * as Protocol from "@marimo-team/client/protocol";
6
+ import type { Execution, Executions, Results } from "@/core/notebook/replica";
7
+ import { type Milliseconds, Time } from "@/utils/time";
8
+
9
+ /** Where a cell is in its run. */
10
+ export type CellRunStatus = "idle" | "queued" | "running";
11
+
12
+ /** How a cell's last completed execution ended. */
13
+ export type CellOutcome = "value" | "error" | "interrupted" | "skipped";
14
+
15
+ export interface CellExecutionView {
16
+ readonly status: CellRunStatus;
17
+ /** Whether a cell it depends on is disabled, so it cannot run. */
18
+ readonly blocked: boolean;
19
+ /** Null until the cell completes once, and while it runs again. */
20
+ readonly outcome: CellOutcome | null;
21
+ /** When the current run started, or the last one, since the epoch. */
22
+ readonly startedAt: Milliseconds | null;
23
+ /** How long the last completed execution took, null while the cell runs again. */
24
+ readonly duration: Milliseconds | null;
25
+ /** Whether the running execution has shown a value so far. TODO(MO-8404) */
26
+ readonly rendered: boolean;
27
+ }
28
+
29
+ /** What a cell's view is derived from. */
30
+ export interface CellExecutionInputs {
31
+ readonly executions: Executions;
32
+ readonly results: Results;
33
+ /**
34
+ * The execution each submitted cell had when its run was requested.
35
+ *
36
+ * A cell reads as queued until the host shows a newer execution for it.
37
+ */
38
+ readonly submissions: ReadonlyMap<
39
+ Protocol.CellId,
40
+ Protocol.ExecutionId | null
41
+ >;
42
+ /** Cells the kernel reports as blocked by a disabled ancestor. */
43
+ readonly blocked: ReadonlySet<Protocol.CellId>;
44
+ }
45
+
46
+ export function outcomeOf(record: Protocol.Execution): CellOutcome | null {
47
+ const outcome = record.outcome;
48
+ if (outcome === null) {
49
+ return null;
50
+ }
51
+ return outcome.kind;
52
+ }
53
+
54
+ function millis(iso: string | null): Milliseconds | null {
55
+ return iso === null ? null : Time.parse(iso).toMilliseconds();
56
+ }
57
+
58
+ function latestOf(
59
+ executions: Executions,
60
+ id: Protocol.CellId,
61
+ ): Execution | null {
62
+ const executionId = executions.latest.get(id);
63
+ return executionId === undefined
64
+ ? null
65
+ : (executions.executions.get(executionId) ?? null);
66
+ }
67
+
68
+ /**
69
+ * The execution a submission is measured against: the cell's latest on the
70
+ * stream, or its retained result once the stream has let that execution go.
71
+ */
72
+ export function latestExecutionId(
73
+ inputs: Pick<CellExecutionInputs, "executions" | "results">,
74
+ id: Protocol.CellId,
75
+ ): Protocol.ExecutionId | null {
76
+ return inputs.executions.latest.get(id) ?? inputs.results.get(id)?.id ?? null;
77
+ }
78
+
79
+ export function cellExecutionView(
80
+ inputs: CellExecutionInputs,
81
+ id: Protocol.CellId,
82
+ ): CellExecutionView {
83
+ const latest = latestOf(inputs.executions, id);
84
+ const result = inputs.results.get(id) ?? null;
85
+ const submitted =
86
+ inputs.submissions.has(id) &&
87
+ inputs.submissions.get(id) === latestExecutionId(inputs, id);
88
+ let status: CellRunStatus = "idle";
89
+ let startedAt = result === null ? null : millis(result.started_at);
90
+ let rendered = false;
91
+ if (latest !== null && latest.record.status !== "completed") {
92
+ status = latest.record.status;
93
+ startedAt = millis(latest.record.started_at) ?? startedAt;
94
+ rendered = latest.record.outcome !== null;
95
+ } else if (submitted) {
96
+ status = "queued";
97
+ }
98
+ const settled = status === "idle";
99
+ const duration =
100
+ settled &&
101
+ result !== null &&
102
+ result.started_at !== null &&
103
+ result.completed_at !== null
104
+ ? Time.parse(result.completed_at).since(Time.parse(result.started_at))
105
+ : null;
106
+ return {
107
+ status,
108
+ blocked: inputs.blocked.has(id),
109
+ outcome: settled && result !== null ? outcomeOf(result) : null,
110
+ startedAt,
111
+ duration,
112
+ rendered,
113
+ };
114
+ }
115
+
116
+ /** Executions the host has not completed, cells and scratch alike. */
117
+ function unfinished(executions: Executions): Execution[] {
118
+ return [...executions.executions.values()].filter(
119
+ (execution) => execution.record.status !== "completed",
120
+ );
121
+ }
122
+
123
+ export function anyRunning(executions: Executions): boolean {
124
+ return unfinished(executions).some(
125
+ (execution) => execution.record.status === "running",
126
+ );
127
+ }
128
+
129
+ /** Cells and scratch work queued or running, plus submissions the host has not shown yet. */
130
+ export function queuedOrRunningCount(inputs: CellExecutionInputs): number {
131
+ const pending = unfinished(inputs.executions);
132
+ const shown = new Set(
133
+ pending.flatMap((execution) =>
134
+ execution.record.cell_id === null ? [] : [execution.record.cell_id],
135
+ ),
136
+ );
137
+ let awaiting = 0;
138
+ for (const [cellId, executionId] of inputs.submissions) {
139
+ if (
140
+ !shown.has(cellId) &&
141
+ executionId === latestExecutionId(inputs, cellId)
142
+ ) {
143
+ awaiting += 1;
144
+ }
145
+ }
146
+ return pending.length + awaiting;
147
+ }
148
+
149
+ export function onlyScratchRunning(executions: Executions): boolean {
150
+ return unfinished(executions).every(
151
+ (execution) =>
152
+ execution.record.status !== "running" ||
153
+ execution.record.cell_id === null,
154
+ );
155
+ }
156
+
157
+ /** Where the scratchpad's work is, from its unfinished execution if any. */
158
+ export function scratchStatus(executions: Executions): CellRunStatus {
159
+ const scratch = unfinished(executions).find(
160
+ (execution) => execution.record.cell_id === null,
161
+ );
162
+ if (scratch === undefined || scratch.record.status === "completed") {
163
+ return "idle";
164
+ }
165
+ return scratch.record.status;
166
+ }
167
+
168
+ /** Cells with a running execution, in the order the host reported them. */
169
+ export function runningCellIds(executions: Executions): Protocol.CellId[] {
170
+ return unfinished(executions).flatMap((execution) =>
171
+ execution.record.status === "running" && execution.record.cell_id !== null
172
+ ? [execution.record.cell_id]
173
+ : [],
174
+ );
175
+ }
@@ -67,11 +67,6 @@ describe("useEnrichCellOutputs", () => {
67
67
  cellId,
68
68
  {
69
69
  output: cell.output || null,
70
- status: cell.status || "idle",
71
- interrupted: false,
72
- errored: false,
73
- runStartTimestamp: null,
74
- runElapsedTimeMs: null,
75
70
  stallTime: null as unknown as number,
76
71
  ...cell,
77
72
  } as CellRuntimeState,
@@ -25,6 +25,7 @@ import { MarimoValueInputEvent } from "../dom/events";
25
25
  import { UI_ELEMENT_REGISTRY } from "../dom/uiregistry";
26
26
  import { FUNCTIONS_REGISTRY } from "../functions/FunctionRegistry";
27
27
  import { createNotebookReplica } from "../notebook/replica";
28
+ import { observeCellMessage } from "../executions";
28
29
  import {
29
30
  handleCellNotificationeration,
30
31
  handleKernelReady,
@@ -35,6 +36,7 @@ import { queryParamHandlers } from "../kernel/queryParamHandlers";
35
36
  import { RuntimeState } from "../kernel/RuntimeState";
36
37
  import { initialModeAtom } from "../mode";
37
38
  import { requestClientAtom } from "../network/requests";
39
+ import { activeNotebookReplicaAtom } from "../session/active-notebook";
38
40
  import { store as defaultStore } from "../state/jotai";
39
41
  import type { IslandsPyodideBridge } from "./bridge";
40
42
  import { MarimoIslandElement } from "./components/web-components";
@@ -131,6 +133,7 @@ export async function initializeIslands(
131
133
  // The notebook's cells follow the managed session's host document.
132
134
  const replica = createNotebookReplica(store);
133
135
  replica.connect(bridge.notebookBackend, { onNotification: Functions.NOOP });
136
+ store.set(activeNotebookReplicaAtom, replica);
134
137
  const documentModel = createDocumentModel({
135
138
  store,
136
139
  documentAtom: replica.documentAtom,
@@ -222,7 +225,6 @@ function handleNotification(
222
225
  resetCellRuntimes: actions.resetCellRuntimes,
223
226
  setLayoutData: Functions.NOOP,
224
227
  setCapabilities: Functions.NOOP,
225
- setKernelState: Functions.NOOP,
226
228
  onError: Logger.error,
227
229
  });
228
230
  if (!window.customElements?.get(ISLAND_TAG_NAMES.ISLAND)) {
@@ -247,6 +249,7 @@ function handleNotification(
247
249
  return;
248
250
 
249
251
  case "cell-op":
252
+ observeCellMessage(store, msg);
250
253
  handleCellNotificationeration(msg, actions.handleCellMessage);
251
254
  return;
252
255
 
@@ -13,7 +13,7 @@ import { OutputRenderer } from "@/components/editor/Output";
13
13
  import { type NotebookState, notebookAtom } from "@/core/cells/cells";
14
14
  import type { CellId } from "@/core/cells/ids";
15
15
  import { isOutputEmpty } from "@/core/cells/outputs";
16
- import type { CellRuntimeState } from "@/core/cells/types";
16
+ import { type CellRunStatus, useCellExecution } from "@/core/executions";
17
17
  import { ISLAND_TAG_NAMES } from "../constants";
18
18
  import { islandsHydratedAtom } from "../state";
19
19
  import { IslandControls } from "./IslandControls";
@@ -69,7 +69,7 @@ export const MarimoOutputWrapper: React.FC<MarimoOutputWrapperProps> = ({
69
69
 
70
70
  // Sync cell status to the host <marimo-island> element as a data attribute
71
71
  // so downstream consumers can style based on status (e.g. [data-status="running"])
72
- const status = runtime?.status ?? "idle";
72
+ const { status } = useCellExecution(cellId);
73
73
  useSyncStatusToIsland(wrapperRef, status);
74
74
 
75
75
  // If no runtime yet, show static content
@@ -90,7 +90,7 @@ export const MarimoOutputWrapper: React.FC<MarimoOutputWrapperProps> = ({
90
90
  return (
91
91
  <div ref={wrapperRef} className="relative min-h-6">
92
92
  <OutputRenderer message={runtime.output} />
93
- <RunningIndicator state={runtime} />
93
+ <RunningIndicator status={status} />
94
94
  <IslandControls
95
95
  cellId={cellId}
96
96
  codeCallback={codeCallback}
@@ -118,8 +118,8 @@ function useSyncStatusToIsland(
118
118
  /**
119
119
  * Shows a spinning indicator when the cell is running
120
120
  */
121
- const RunningIndicator: React.FC<{ state: CellRuntimeState }> = ({ state }) => {
122
- if (state.status === "running") {
121
+ const RunningIndicator: React.FC<{ status: CellRunStatus }> = ({ status }) => {
122
+ if (status === "running") {
123
123
  return (
124
124
  <DelayRender>
125
125
  <div className="absolute top-1 right-1">
@@ -14,7 +14,6 @@ import type {
14
14
  CellMessage,
15
15
  NotificationMessageData,
16
16
  } from "./messages";
17
- import type { KernelState } from "./state";
18
17
 
19
18
  type KernelReadyData = NotificationMessageData<"kernel-ready">;
20
19
 
@@ -68,7 +67,6 @@ export function handleKernelReady(
68
67
  data: LayoutData;
69
68
  }) => void;
70
69
  setCapabilities: (capabilities: Capabilities) => void;
71
- setKernelState: (state: KernelState) => void;
72
70
  onError: (error: Error) => void;
73
71
  },
74
72
  ): LayoutState {
@@ -79,16 +77,13 @@ export function handleKernelReady(
79
77
  resetCellRuntimes,
80
78
  setLayoutData,
81
79
  setCapabilities,
82
- setKernelState,
83
80
  onError,
84
81
  } = opts;
85
82
  const { resumed, ui_values, capabilities, auto_instantiated } = data;
86
83
 
87
84
  resetCellRuntimes();
88
- setLastRun({
89
- lastCodeRun: data.last_executed_code ?? {},
90
- lastExecutionTime: data.last_execution_time ?? {},
91
- });
85
+ // TODO(MO-8403): Delete with `lastCodeRun`.
86
+ setLastRun({ lastCodeRun: data.last_executed_code ?? {} });
92
87
  const layoutState = buildLayoutState(data, cells, setLayoutData);
93
88
 
94
89
  setCapabilities(capabilities);
@@ -121,11 +116,7 @@ export function handleKernelReady(
121
116
  values,
122
117
  autoRun: autoInstantiate,
123
118
  })
124
- .then(() => {
125
- setKernelState({ isInstantiated: true, error: null });
126
- })
127
119
  .catch((error) => {
128
- setKernelState({ isInstantiated: false, error: error });
129
120
  onError(new Error("Failed to instantiate", { cause: error }));
130
121
  });
131
122
  return layoutState;
@@ -1,19 +1,4 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { atom } from "jotai";
3
- import { waitFor } from "../state/jotai";
4
-
5
- export interface KernelState {
6
- isInstantiated: boolean;
7
- error: Error | null;
8
- }
9
-
10
- export const kernelStateAtom = atom<KernelState>({
11
- isInstantiated: false,
12
- error: null,
13
- });
14
3
 
15
4
  export const initialRunCompletedAtom = atom(false);
16
-
17
- export function waitForKernelToBeInstantiated(): Promise<KernelState> {
18
- return waitFor(kernelStateAtom, (value) => value.isInstantiated);
19
- }
@@ -3,6 +3,7 @@
3
3
  import { atom } from "jotai";
4
4
  import { hasCellsAtom, notebookAtom } from "../cells/cells";
5
5
  import { isOutputEmpty } from "../cells/outputs";
6
+ import { notebookQueuedOrRunningCountAtom } from "../executions";
6
7
  import { KnownQueryParams } from "../constants";
7
8
  import { showCodeInRunModeAtom } from "../meta/state";
8
9
  import { type AppMode, initialModeAtom } from "../mode";
@@ -74,37 +75,22 @@ function computeRenderPolicy(input: {
74
75
  }
75
76
 
76
77
  /**
77
- * Both cell-runtime facts the render policy needs, computed in a single pass
78
- * over `cellRuntime`:
79
- * - `hasCachedOutputs`: any cell has a non-empty output.
80
- * - `allCellsIdle`: every cell's status is idle — nothing queued or running.
81
- * Vacuously true for an empty notebook, so `canPaint` still gates on
82
- * `hasCells`.
83
- *
84
- * Fan out to two boolean selector atoms below so `renderPolicyAtom` only
85
- * recomputes when one of the booleans actually flips, not on every notebook
78
+ * Whether any cell has a non-empty output. A boolean selector so
79
+ * `renderPolicyAtom` only recomputes when it flips, not on every notebook
86
80
  * update.
87
81
  */
88
- const cellRuntimeSummaryAtom = atom((get) => {
89
- let hasCachedOutputs = false;
90
- let allCellsIdle = true;
91
- for (const runtime of Object.values(get(notebookAtom).cellRuntime)) {
92
- if (!isOutputEmpty(runtime.output)) {
93
- hasCachedOutputs = true;
94
- }
95
- if (runtime.status !== "idle") {
96
- allCellsIdle = false;
97
- }
98
- }
99
- return { hasCachedOutputs, allCellsIdle };
100
- });
101
-
102
- const hasCachedOutputsAtom = atom(
103
- (get) => get(cellRuntimeSummaryAtom).hasCachedOutputs,
82
+ const hasCachedOutputsAtom = atom((get) =>
83
+ Object.values(get(notebookAtom).cellRuntime).some(
84
+ (runtime) => !isOutputEmpty(runtime.output),
85
+ ),
104
86
  );
105
87
 
88
+ /**
89
+ * Nothing queued or running, by the host's executions. Vacuously true for an
90
+ * empty notebook, so `canPaint` still gates on `hasCells`.
91
+ */
106
92
  const allCellsIdleAtom = atom(
107
- (get) => get(cellRuntimeSummaryAtom).allCellsIdle,
93
+ (get) => get(notebookQueuedOrRunningCountAtom) === 0,
108
94
  );
109
95
 
110
96
  export const renderPolicyAtom = atom<RenderPolicy>((get) =>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import { cellId, requestId, uiElementId } from "@/__tests__/branded";
5
- import { waitForKernelToBeInstantiated } from "../../kernel/state";
5
+ import { waitForRuntimeRunning } from "../../executions";
6
6
  import type { RuntimeManager } from "../../runtime/runtime";
7
7
  import { waitForConnectionOpen } from "../connection";
8
8
  import { createLazyRequests } from "../requests-lazy";
@@ -22,8 +22,8 @@ vi.mock("../connection", async () => {
22
22
  });
23
23
 
24
24
  // Mock the kernel state module
25
- vi.mock("../../kernel/state", () => ({
26
- waitForKernelToBeInstantiated: vi.fn().mockResolvedValue(undefined),
25
+ vi.mock("../../executions", () => ({
26
+ waitForRuntimeRunning: vi.fn().mockResolvedValue(undefined),
27
27
  }));
28
28
 
29
29
  describe("createLazyRequests", () => {
@@ -119,7 +119,7 @@ describe("createLazyRequests", () => {
119
119
 
120
120
  expect(mockInit).toHaveBeenCalledOnce();
121
121
  expect(waitForConnectionOpen).toHaveBeenCalledOnce();
122
- expect(waitForKernelToBeInstantiated).not.toHaveBeenCalled();
122
+ expect(waitForRuntimeRunning).not.toHaveBeenCalled();
123
123
  expect(mockDelegate.installExportRequirements).toHaveBeenCalledWith({
124
124
  format: "pdf",
125
125
  });
@@ -139,7 +139,7 @@ describe("createLazyRequests", () => {
139
139
 
140
140
  expect(mockInit).toHaveBeenCalledOnce();
141
141
  expect(waitForConnectionOpen).toHaveBeenCalledOnce();
142
- expect(waitForKernelToBeInstantiated).not.toHaveBeenCalled();
142
+ expect(waitForRuntimeRunning).not.toHaveBeenCalled();
143
143
  expect(mockDelegate.sendInstantiate).toHaveBeenCalledWith(request);
144
144
  });
145
145
 
@@ -4,7 +4,7 @@ import { NoKernelConnectedError } from "@/utils/errors";
4
4
  import { Logger } from "@/utils/Logger";
5
5
  import { Objects } from "@/utils/objects";
6
6
  import { memoizeLastValue } from "@/utils/once";
7
- import { waitForKernelToBeInstantiated } from "../kernel/state";
7
+ import { waitForRuntimeRunning } from "../executions";
8
8
  import type { RuntimeManager } from "../runtime/runtime";
9
9
  import { store } from "../state/jotai";
10
10
  import { ConnectionState } from "../session/types";
@@ -192,7 +192,7 @@ export function createLazyRequests(
192
192
  case "waitForConnectionOpen":
193
193
  // Wait for connection but don't start it
194
194
  await waitForConnectionOpen();
195
- await waitForKernelToBeInstantiated();
195
+ await waitForRuntimeRunning();
196
196
  return request(...args);
197
197
 
198
198
  case "startConnectionWithoutKernel":
@@ -201,7 +201,7 @@ export function createLazyRequests(
201
201
  await initOnce(runtimeManager);
202
202
  await waitForConnectionOpen();
203
203
  if (action === "startConnection") {
204
- await waitForKernelToBeInstantiated();
204
+ await waitForRuntimeRunning();
205
205
  }
206
206
  return request(...args);
207
207
 
@@ -18,7 +18,7 @@ export const notebookOperationsAtom = atom((get) => {
18
18
  /**
19
19
  * Whether this client may edit the document.
20
20
  *
21
- * TODO(MO-8028): The host advertises the same operations to every
21
+ * TODO(MO-8352): The host advertises the same operations to every
22
22
  * connection of a notebook, so a kiosk connection, an interactor that may
23
23
  * not edit, narrows it here. Once the host narrows `operations` per
24
24
  * connection by consumer capability, the kiosk term goes.
@@ -11,7 +11,8 @@ import { Spinner } from "@/components/icons/spinner";
11
11
  import { buttonVariants } from "@/components/ui/button";
12
12
  import { cn } from "@/utils/cn";
13
13
  import { CellsRenderer } from "../components/editor/renderers/cells-renderer";
14
- import { hasCellsAtom, notebookIsRunningAtom } from "./cells/cells";
14
+ import { hasCellsAtom } from "./cells/cells";
15
+ import { notebookIsRunningAtom } from "./executions";
15
16
  import type { AppConfig } from "./config/config-schema";
16
17
  import { DocumentProvider } from "./notebook/document";
17
18
  import { RuntimeState } from "./kernel/RuntimeState";
@@ -3,7 +3,7 @@
3
3
  import { createStore } from "jotai";
4
4
  import { describe, expect, it } from "vitest";
5
5
  import { islandsInitializedAtom } from "../../islands/state";
6
- import { kernelStateAtom } from "../../kernel/state";
6
+ import { MockExecutions } from "@/__mocks__/executions";
7
7
  import { connectionAtom } from "../../network/connection";
8
8
  import { wasmInitStateAtom } from "../../wasm/state";
9
9
  import { ConnectionState } from "../../session/types";
@@ -122,13 +122,13 @@ describe("islandsAdapter", () => {
122
122
  });
123
123
 
124
124
  describe("canRunCellsAtom", () => {
125
- it("is false on a remote adapter until kernel acks", () => {
125
+ it("is false on a remote adapter until the host reports the runtime running", () => {
126
126
  const store = createStore();
127
127
  // Default adapter in test env is remote.
128
128
  store.set(connectionAtom, { state: ConnectionState.OPEN });
129
129
  expect(store.get(canRunCellsAtom)).toBe(false);
130
130
 
131
- store.set(kernelStateAtom, { isInstantiated: true, error: null });
131
+ MockExecutions.seed(store, [], MockExecutions.runtime("running"));
132
132
  expect(store.get(canRunCellsAtom)).toBe(true);
133
133
  });
134
134
 
@@ -1,7 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { type Atom, atom } from "jotai";
4
- import { kernelStateAtom } from "../kernel/state";
4
+ import { runtimeStatusAtom } from "../executions";
5
5
  import { islandsInitializedAtom } from "../islands/state";
6
6
  import { isIslands } from "../islands/utils";
7
7
  import { connectionAtom } from "../network/connection";
@@ -163,8 +163,8 @@ function selectAdapter(): RuntimeAdapter {
163
163
  export const runtimeAdapterAtom = atom<RuntimeAdapter>(selectAdapter());
164
164
 
165
165
  /**
166
- * Remote runtimes require both the WS to be open *and* the kernel to have
167
- * ack'd instantiation. WASM/islands have no separate kernel handshake.
166
+ * Remote runtimes require both the WS to be open *and* the host to report
167
+ * the runtime running. WASM/islands have no separate kernel handshake.
168
168
  * Static notebooks never run cells.
169
169
  */
170
170
  export const canRunCellsAtom = atom<boolean>((get) => {
@@ -176,7 +176,7 @@ export const canRunCellsAtom = atom<boolean>((get) => {
176
176
  return false;
177
177
  }
178
178
  if (adapter.kind === "remote") {
179
- return get(kernelStateAtom).isInstantiated;
179
+ return get(runtimeStatusAtom) === "running";
180
180
  }
181
181
  return true;
182
182
  });
@@ -62,7 +62,8 @@ import {
62
62
  type StableSessionId,
63
63
  stableSessionIdAtom,
64
64
  } from "../kernel/session";
65
- import { initialRunCompletedAtom, kernelStateAtom } from "../kernel/state";
65
+ import { observeCellMessage } from "../executions";
66
+ import { initialRunCompletedAtom } from "../kernel/state";
66
67
  import { useLayoutActions } from "../layout/state";
67
68
  import { kioskModeAtom } from "../mode";
68
69
  import { connectionAtom, startupProgressAtom } from "../network/connection";
@@ -99,7 +100,6 @@ export function useNotebookSession(opts: {
99
100
  } = useReducerActions();
100
101
  const { reconcileStagedCells } = useStagedAICellsActions();
101
102
  const { addCellNotification } = useRunsActions();
102
- const setKernelState = useSetAtom(kernelStateAtom);
103
103
  const setInitialRunCompleted = useSetAtom(initialRunCompletedAtom);
104
104
  const { setVariables, setMetadata } = useVariablesActions();
105
105
  const { addColumnPreview } = useDatasetsActions();
@@ -158,7 +158,6 @@ export function useNotebookSession(opts: {
158
158
  resetCellRuntimes,
159
159
  setLayoutData,
160
160
  setCapabilities,
161
- setKernelState,
162
161
  onError: showBoundary,
163
162
  });
164
163
  store.set(savedLayoutAtom, layout);
@@ -222,6 +221,7 @@ export function useNotebookSession(opts: {
222
221
  FUNCTIONS_REGISTRY.resolve(msg.data.function_call_id, msg.data);
223
222
  return;
224
223
  case "cell-op": {
224
+ observeCellMessage(store, msg.data);
225
225
  handleCellNotificationeration(msg.data, handleCellMessage);
226
226
  const cellData = getNotebook().cellData[msg.data.cell_id];
227
227
  if (!cellData) {
@@ -13,7 +13,7 @@ import { reloadSafe } from "@/utils/reload-safe";
13
13
  import { once } from "@/utils/once";
14
14
  import { generateUUID } from "@/utils/uuid";
15
15
  import { createModuleWorker } from "@/utils/worker";
16
- import { notebookIsRunningAtom } from "../cells/cells";
16
+ import { notebookIsRunningAtom } from "../executions";
17
17
  import { activeNotebookReplicaAtom } from "../session/active-notebook";
18
18
  import { flushDocument } from "../cells/document";
19
19
  import { createWasmNotebookBackend } from "../transports/wasm";
@@ -300,11 +300,9 @@ const CopyableCode = ({ children }: { children: ReactNode }) => {
300
300
  tooltip={false}
301
301
  className="p-1"
302
302
  value={() => {
303
- const codeElement = ref.current?.firstChild;
304
- if (codeElement) {
305
- return codeElement.textContent || "";
306
- }
307
- return "";
303
+ const codeElement =
304
+ ref.current?.querySelector("pre code") ?? ref.current?.firstChild;
305
+ return codeElement?.textContent ?? "";
308
306
  }}
309
307
  />
310
308
  </div>
@@ -0,0 +1,60 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { fireEvent, render, waitFor } from "@testing-library/react";
3
+ import { expect, test, vi } from "vitest";
4
+ import { copyToClipboard } from "@/utils/copy";
5
+ import { renderHTML } from "../RenderHTML";
6
+
7
+ vi.mock("@/utils/copy", () => ({
8
+ copyToClipboard: vi.fn().mockResolvedValue(undefined),
9
+ }));
10
+
11
+ vi.mock("../sanitize", async (importOriginal) => ({
12
+ ...(await importOriginal<typeof import("../sanitize")>()),
13
+ useSanitizeHtml: () => true,
14
+ }));
15
+
16
+ test.each([
17
+ {
18
+ name: "syntax highlighting and whitespace",
19
+ html: '<div class="codehilite"><pre><span></span><code><span class="k">if</span> x &lt; 2:\n print(&quot;hello&quot;)\n\n</code></pre></div>',
20
+ expected: 'if x < 2:\n print("hello")\n\n',
21
+ },
22
+ {
23
+ name: "a block title",
24
+ html: '<div class="codehilite"><span class="filename">example.py</span><pre><span></span><code>print("hello")\n</code></pre></div>',
25
+ expected: 'print("hello")\n',
26
+ },
27
+ {
28
+ name: "line numbers in a separate column",
29
+ html: '<div class="codehilite"><table class="codehilitetable"><tbody><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">1</span>\n<span class="normal">2</span></pre></div></td><td class="code"><div><pre><span></span><code>print("hello")\nprint("world")\n</code></pre></div></td></tr></tbody></table></div>',
30
+ expected: 'print("hello")\nprint("world")\n',
31
+ },
32
+ {
33
+ name: "hidden fences outside the code",
34
+ html: '<div class="codehilite"><span hidden>~~~python\n</span><pre><code>print("hello")\n</code></pre><span hidden>~~~</span></div>',
35
+ expected: 'print("hello")\n',
36
+ },
37
+ {
38
+ name: "literal fences inside the code",
39
+ html: '<div class="codehilite"><pre><code>~~~python\nprint("``` and ~~~")\n~~~\n</code></pre></div>',
40
+ expected: '~~~python\nprint("``` and ~~~")\n~~~\n',
41
+ },
42
+ {
43
+ name: "an empty code element",
44
+ html: '<div class="codehilite"><span class="filename">empty.py</span><pre><code></code></pre></div>',
45
+ expected: "",
46
+ },
47
+ {
48
+ name: "highlighted HTML without a code element",
49
+ html: '<div class="codehilite"><pre><span class="nb">print</span>("hello")\n</pre></div>',
50
+ expected: 'print("hello")\n',
51
+ },
52
+ ])("copies code from $name", async ({ html, expected }) => {
53
+ const { getByRole } = render(renderHTML({ html }));
54
+
55
+ fireEvent.click(getByRole("button", { name: "Copy to clipboard" }));
56
+
57
+ await waitFor(() => {
58
+ expect(copyToClipboard).toHaveBeenCalledExactlyOnceWith(expected);
59
+ });
60
+ });