@marimo-team/frontend 0.25.3-dev3 → 0.25.3-dev31

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 (209) hide show
  1. package/dist/assets/{CellStatus-BP2mavAq.js → CellStatus-wFCGHpDX.js} +1 -1
  2. package/dist/assets/{ConnectedDataExplorerComponent-t8fH_TNc.js → ConnectedDataExplorerComponent-DMdJT25t.js} +1 -1
  3. package/dist/assets/{ErrorBoundary-Gl5nCA8C.js → ErrorBoundary-D1ozl9e0.js} +2 -2
  4. package/dist/assets/JsonOutput-Dqg7qwkl.js +11 -0
  5. package/dist/assets/{LazyAnyLanguageCodeMirror-CKcCbG2G.js → LazyAnyLanguageCodeMirror-B4fCknq1.js} +2 -2
  6. package/dist/assets/MarimoErrorOutput-BYSTEwo1.js +10 -0
  7. package/dist/assets/{RenderHTML-BbOqmHy3.js → RenderHTML-B18UiW8r.js} +1 -1
  8. package/dist/assets/{RunButton-CK3LhnP5.js → RunButton-CWvRnb98.js} +1 -1
  9. package/dist/assets/{add-cell-with-ai-8UBC0xwR.js → add-cell-with-ai-C4pN_ho7.js} +13 -13
  10. package/dist/assets/{add-connection-dialog-DXQnd6eB.js → add-connection-dialog-BErVOfUV.js} +2 -2
  11. package/dist/assets/{agent-panel-D7kMZLG-.js → agent-panel-QQDTP8Vb.js} +5 -5
  12. package/dist/assets/ai-model-dropdown-D5Fe5GS-.js +13 -0
  13. package/dist/assets/{any-language-editor-BLCAD3yx.js → any-language-editor-BdTWdMCI.js} +1 -1
  14. package/dist/assets/{app-config-button-BKEG4x9u.js → app-config-button-DBHN-U2l.js} +1 -1
  15. package/dist/assets/{blob-D5nfNHOo.js → blob-DY_W1wFZ.js} +1 -1
  16. package/dist/assets/{cell-editor-ADVfycpN.js → cell-editor-CCFDuQIT.js} +9 -9
  17. package/dist/assets/{cell-link-LSth_8hT.js → cell-link-BOqoitvE.js} +1 -1
  18. package/dist/assets/{cells-BRkCaSsh.js → cells-CN1I_0eH.js} +31 -31
  19. package/dist/assets/{chat-display-Dnagy4Nv.js → chat-display-D1_UV6v7.js} +1 -1
  20. package/dist/assets/{chat-panel-DQ6HkWEF.js → chat-panel-C4OJDIt6.js} +2 -2
  21. package/dist/assets/{chat-ui-Df8Vcj6n.js → chat-ui-wuDYPZ-w.js} +1 -1
  22. package/dist/assets/code-visibility-CnU9xZw7.js +1 -0
  23. package/dist/assets/coerce-DWjFeXOY.js +1 -0
  24. package/dist/assets/{column-preview-Cm2D6vyX.js → column-preview-z4bqk4HW.js} +1 -1
  25. package/dist/assets/command-palette-TblDi15Q.js +1 -0
  26. package/dist/assets/{common-Cu6CDbuw.js → common-Cbk0KIh7.js} +1 -1
  27. package/dist/assets/{components-CGw16R_N.js → components-C88UxBXz.js} +1 -1
  28. package/dist/assets/{components-CP_UoFtT.js → components-CskjK6xZ.js} +1 -1
  29. package/dist/assets/{components-CXfO-F47.js → components-YBmtV0p6.js} +1 -1
  30. package/dist/assets/{config-VPSq73n8.js → config-CA1V-wjh.js} +1 -1
  31. package/dist/assets/config-DEzFV5-l.js +1 -0
  32. package/dist/assets/config-MaHeMC9o.js +104 -0
  33. package/dist/assets/{datasource-Cq6x_5ML.js → datasource-DZJ6qqVI.js} +1 -1
  34. package/dist/assets/{dates-BbFQ2oE4.js → dates-CfmdTL6B.js} +1 -1
  35. package/dist/assets/{dependency-graph-panel--Kgtktcw.js → dependency-graph-panel-DeZ66gIo.js} +1 -1
  36. package/dist/assets/{documentation-panel-BHmbeznw.js → documentation-panel-Tl5G1cPt.js} +1 -1
  37. package/dist/assets/{download-DCJXy66H.js → download-s8iwxGDb.js} +1 -1
  38. package/dist/assets/edit-page-CjoAJvyN.js +9 -0
  39. package/dist/assets/{error-banner-Cd69vvyX.js → error-banner-COZ7SfYV.js} +1 -1
  40. package/dist/assets/{error-entries-Dk3VZ-OK.js → error-entries-D11Bn1Hk.js} +1 -1
  41. package/dist/assets/{error-panel-BPRYmC5J.js → error-panel-kfxFDscu.js} +1 -1
  42. package/dist/assets/{errors-DCrsa1nH.js → errors-CNsgAw6m.js} +1 -1
  43. package/dist/assets/feedback-button-3bZHpuSh.js +1 -0
  44. package/dist/assets/{feedback-button-Bik2-e33.js → feedback-button-DVABjD9Y.js} +2 -2
  45. package/dist/assets/{file-explorer-panel-BEEK5lcl.js → file-explorer-panel-CvmK-rEU.js} +1 -1
  46. package/dist/assets/{file-name-input-Cdc-TVqO.js → file-name-input-DAj9a3RQ.js} +1 -1
  47. package/dist/assets/{floating-outline-XD9FAV-f.js → floating-outline-DDavqWJx.js} +1 -1
  48. package/dist/assets/{formats-yZ06h1Pl.js → formats-BkelUASr.js} +1 -1
  49. package/dist/assets/{gallery-page-Dm6mGwBi.js → gallery-page-xP6VqDyd.js} +1 -1
  50. package/dist/assets/{ganttDiagram-EL5Y4UJY-2n7Ig5Jm.js → ganttDiagram-EL5Y4UJY-CYqkCfFG.js} +1 -1
  51. package/dist/assets/{glide-data-editor-J9CAb70d.js → glide-data-editor-32vBJJf8.js} +1 -1
  52. package/dist/assets/{home-page-DMHnsRxm.js → home-page-CvGzjVr4.js} +1 -1
  53. package/dist/assets/hooks-C1_gQ8kr.js +1 -0
  54. package/dist/assets/index-6b4mf8Ai.css +2 -0
  55. package/dist/assets/index-Dtv0HH-7.js +30 -0
  56. package/dist/assets/{layout-DmcNEu3P.js → layout-BccvBeZy.js} +5 -5
  57. package/dist/assets/linear-Bma8J3iM.js +1 -0
  58. package/dist/assets/{logs-panel-ABM16htT.js → logs-panel-BhXo1sVJ.js} +1 -1
  59. package/dist/assets/{markdown-renderer-BG3sp2rs.js → markdown-renderer-D2dafr8r.js} +1 -1
  60. package/dist/assets/{mermaid-a3xYymTy.js → mermaid-D2FUGQeW.js} +2 -2
  61. package/dist/assets/{name-cell-input-BgEBmqyL.js → name-cell-input-SHvgfcBR.js} +1 -1
  62. package/dist/assets/{outline-panel-DCXQ3_F6.js → outline-panel-CpY3FH-u.js} +1 -1
  63. package/dist/assets/{packages-panel-BMGOmZkR.js → packages-panel-oZ0qZBec.js} +1 -1
  64. package/dist/assets/{panel-accordion-state-C1sdPp0m.js → panel-accordion-state-ChFIY2WQ.js} +1 -1
  65. package/dist/assets/panels-91ywfwgc.js +4 -0
  66. package/dist/assets/precisionRound-BxlUs8G4.js +1 -0
  67. package/dist/assets/{process-output-D0KGq04R.js → process-output-Dvq4b2kJ.js} +1 -1
  68. package/dist/assets/{quadrantDiagram-AXDQQJYC-yBhCDI7E.js → quadrantDiagram-AXDQQJYC-D9KH5Zge.js} +1 -1
  69. package/dist/assets/react-vega-BTNeboR2.js +189 -0
  70. package/dist/assets/{readonly-code-display-Do50PHI8.js → readonly-code-display-C1eH5BDP.js} +1 -1
  71. package/dist/assets/{readonly-python-code-DCSMIsTX.js → readonly-python-code-BaElyj-4.js} +1 -1
  72. package/dist/assets/{renderShortcut-BkVJQ_Zh.js → renderShortcut-C1JwYcqW.js} +1 -1
  73. package/dist/assets/{reveal-component-ltaepAqw.js → reveal-component-FCuqfC5H.js} +3 -3
  74. package/dist/assets/{run-page-DF3c5cDk.js → run-page-CbyMlRt1.js} +1 -1
  75. package/dist/assets/{runtime-details-state-CSPobEdR.js → runtime-details-state-Bpn67vTN.js} +1 -1
  76. package/dist/assets/{schemas-Dv42wLFq.js → schemas-ChRqMOTL.js} +1 -1
  77. package/dist/assets/{scratchpad-panel-Dwd6W5PE.js → scratchpad-panel-OF9_e0l_.js} +1 -1
  78. package/dist/assets/{secrets-panel-Bctbtkrp.js → secrets-panel-D7KN4tVs.js} +1 -1
  79. package/dist/assets/session-panel-CExxjkjV.js +1 -0
  80. package/dist/assets/{snippets-panel-Bh1oTw4m.js → snippets-panel-DfRtTFJ-.js} +1 -1
  81. package/dist/assets/state-BUqF6L9e.js +3 -0
  82. package/dist/assets/{state-XsPO8hNi.js → state-CHojzKpZ.js} +1 -1
  83. package/dist/assets/{terminal-Ck_vK-34.js → terminal--4gDbK7q.js} +3 -3
  84. package/dist/assets/{time-MgToa8VM.js → time-DswSXBDk.js} +1 -1
  85. package/dist/assets/tooltip-BsSdc1NQ.js +61 -0
  86. package/dist/assets/{tracing-Di6pUmX8.js → tracing-BnfAB-BH.js} +1 -1
  87. package/dist/assets/{tracing-panel-PeRXHD8_.js → tracing-panel-DNabsbtA.js} +2 -2
  88. package/dist/assets/{tree-actions-GM5E_bUs.js → tree-actions-BXvnBv45.js} +1 -1
  89. package/dist/assets/{useAddCell-GR7eEQrT.js → useAddCell-6ECZ0xPG.js} +1 -1
  90. package/dist/assets/{useBoolean-BDD6_xQQ.js → useBoolean-BvAx9Qrr.js} +1 -1
  91. package/dist/assets/{useCellActionButton-B-9eG5z_.js → useCellActionButton-Djyiwayj.js} +1 -1
  92. package/dist/assets/{useDeleteCell-2_bMClHd.js → useDeleteCell-KX6SEwki.js} +1 -1
  93. package/dist/assets/{useDependencyPanelTab-k3C3M18S.js → useDependencyPanelTab-B8ke9iTY.js} +1 -1
  94. package/dist/assets/{useInstallPackage-iX1eufdl.js → useInstallPackage-C9-icL78.js} +1 -1
  95. package/dist/assets/useNotebookActions-CluLEhAd.js +1 -0
  96. package/dist/assets/{useRunCells-B2vUhREg.js → useRunCells-B847kPLO.js} +1 -1
  97. package/dist/assets/{useSplitCell-DEb03htp.js → useSplitCell-CnvTmIw7.js} +1 -1
  98. package/dist/assets/{useTheme-COhSHkCj.js → useTheme-n8Dj1zH7.js} +1 -1
  99. package/dist/assets/{vega-component-CnadTgoE.js → vega-component-DAkdRNSF.js} +1 -1
  100. package/dist/assets/{vega-embed-container-DJPNkHPb.js → vega-embed-container-CxuIHhsS.js} +1 -1
  101. package/dist/assets/vega-loader.browser-Bu25TKax.js +6 -0
  102. package/dist/assets/{xychartDiagram-S5SC5T6Z-KbGzG6-u.js → xychartDiagram-S5SC5T6Z-CeRnBS5e.js} +1 -1
  103. package/dist/index.html +44 -45
  104. package/package.json +4 -2
  105. package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
  106. package/src/__tests__/mount.test.ts +28 -8
  107. package/src/components/data-table/__tests__/context-menu.test.ts +176 -0
  108. package/src/components/data-table/__tests__/date-filter-inputs.test.tsx +24 -1
  109. package/src/components/data-table/__tests__/export-actions.test.tsx +329 -6
  110. package/src/components/data-table/context-menu.tsx +125 -26
  111. package/src/components/data-table/date-filter-inputs.tsx +15 -42
  112. package/src/components/data-table/export-actions.tsx +192 -116
  113. package/src/components/data-table/schemas.ts +14 -36
  114. package/src/components/data-table/utils.ts +18 -0
  115. package/src/components/editor/Output.tsx +9 -2
  116. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  117. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
  118. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
  119. package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
  120. package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
  121. package/src/components/editor/chrome/panels/error-panel.tsx +1 -0
  122. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +366 -0
  123. package/src/components/editor/chrome/wrapper/footer-item.tsx +32 -24
  124. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +237 -0
  125. package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
  126. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  127. package/src/components/editor/controls/command-palette.tsx +22 -10
  128. package/src/components/editor/dynamic-favicon.tsx +21 -40
  129. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +537 -0
  130. package/src/components/editor/errors/send-error-report-button.tsx +69 -0
  131. package/src/components/editor/output/MarimoErrorOutput.tsx +21 -5
  132. package/src/components/editor/output/MarimoTracebackOutput.tsx +35 -20
  133. package/src/components/editor/output/__tests__/ansi-reduce.test.ts +18 -0
  134. package/src/components/editor/output/ansi-reduce.ts +6 -0
  135. package/src/core/attachments/__tests__/toast.test.tsx +58 -0
  136. package/src/core/attachments/state.ts +18 -0
  137. package/src/core/attachments/toast.ts +15 -0
  138. package/src/core/cells/__tests__/cells.test.ts +52 -0
  139. package/src/core/cells/__tests__/error-report.test.ts +139 -0
  140. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  141. package/src/core/cells/__tests__/session.test.ts +50 -1
  142. package/src/core/cells/cells.ts +2 -1
  143. package/src/core/cells/error-report.ts +63 -0
  144. package/src/core/cells/session.ts +28 -1
  145. package/src/core/config/pair.ts +5 -3
  146. package/src/core/edit-app.tsx +2 -2
  147. package/src/core/host/AGENTS.md +5 -0
  148. package/src/core/host/__tests__/attachments.test.ts +57 -0
  149. package/src/core/host/__tests__/document.test.ts +124 -0
  150. package/src/core/host/__tests__/events.test.ts +46 -0
  151. package/src/core/host/__tests__/fixtures.ts +39 -0
  152. package/src/core/host/__tests__/notebook-client.test.ts +433 -0
  153. package/src/core/host/__tests__/protocol.test.ts +15 -0
  154. package/src/core/host/__tests__/stream.test.ts +209 -0
  155. package/src/core/host/connection.ts +81 -0
  156. package/src/core/host/events.ts +61 -0
  157. package/src/core/host/notebook-client.ts +290 -0
  158. package/src/core/host/protocol.ts +11 -6
  159. package/src/core/host/resources/attachments.ts +19 -0
  160. package/src/core/host/resources/document.ts +101 -0
  161. package/src/core/host/resources/resource.ts +47 -0
  162. package/src/core/host/stream.ts +211 -0
  163. package/src/core/network/connection.ts +4 -2
  164. package/src/core/run-app.tsx +2 -2
  165. package/src/core/runtime/__tests__/runtime.test.ts +55 -11
  166. package/src/core/runtime/runtime.ts +15 -1
  167. package/src/core/runtime/types.ts +2 -0
  168. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  169. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  170. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +170 -0
  171. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +181 -65
  172. package/src/core/websocket/connection-state.ts +70 -0
  173. package/src/core/websocket/state.ts +7 -0
  174. package/src/core/websocket/transports/__tests__/sse.test.ts +1 -4
  175. package/src/core/websocket/transports/create.ts +31 -0
  176. package/src/core/websocket/transports/transport.ts +12 -23
  177. package/src/core/websocket/types.ts +9 -39
  178. package/src/core/websocket/useNotebookClient.ts +88 -0
  179. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +59 -225
  180. package/src/mount.tsx +7 -3
  181. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +84 -0
  182. package/src/utils/__tests__/dates.test.ts +116 -0
  183. package/src/utils/dates.ts +94 -0
  184. package/src/utils/once.ts +19 -19
  185. package/dist/assets/JsonOutput-D6HIIsCb.js +0 -11
  186. package/dist/assets/MarimoErrorOutput-D60XkCny.js +0 -6
  187. package/dist/assets/ai-model-dropdown-B0z6-zhp.js +0 -5
  188. package/dist/assets/api-BeC_VSGv.js +0 -1
  189. package/dist/assets/code-visibility-B-_GvwR5.js +0 -1
  190. package/dist/assets/coerce-CpE6kUdX.js +0 -1
  191. package/dist/assets/command-palette-DTSQ7gOC.js +0 -1
  192. package/dist/assets/config-BOKzA3tF.js +0 -1
  193. package/dist/assets/config-DX8Y2WbJ.js +0 -1
  194. package/dist/assets/edit-page-ChcSgpke.js +0 -9
  195. package/dist/assets/feedback-button-B2gKC5cn.js +0 -1
  196. package/dist/assets/hooks-93IwHd2u.js +0 -1
  197. package/dist/assets/index-Bcdzex-3.css +0 -2
  198. package/dist/assets/index-s-M1OXjs.js +0 -29
  199. package/dist/assets/linear-0eij_aAG.js +0 -1
  200. package/dist/assets/pair-CaQyK6En.js +0 -1
  201. package/dist/assets/panels-CxVG8-v2.js +0 -4
  202. package/dist/assets/precisionRound-Bq_nmf5u.js +0 -1
  203. package/dist/assets/react-vega-C3rYGtP6.js +0 -189
  204. package/dist/assets/session-panel-VdDhEBa-.js +0 -1
  205. package/dist/assets/state-_Qai_QyX.js +0 -3
  206. package/dist/assets/tooltip-mT3CGest.js +0 -61
  207. package/dist/assets/useNotebookActions-NHN6A-cN.js +0 -1
  208. package/dist/assets/vega-loader.browser-CLRkNgm7.js +0 -6
  209. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -0,0 +1,81 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ export const ConnectionState = {
4
+ NOT_STARTED: "NOT_STARTED",
5
+ CONNECTING: "CONNECTING",
6
+ OPEN: "OPEN",
7
+ CLOSING: "CLOSING",
8
+ CLOSED: "CLOSED",
9
+ } as const;
10
+
11
+ export type ConnectionState =
12
+ (typeof ConnectionState)[keyof typeof ConnectionState];
13
+
14
+ export const ConnectionClosedReason = {
15
+ KERNEL_DISCONNECTED: "KERNEL_DISCONNECTED",
16
+ KERNEL_STARTUP_ERROR: "KERNEL_STARTUP_ERROR",
17
+ } as const;
18
+
19
+ export type ConnectionClosedReason =
20
+ (typeof ConnectionClosedReason)[keyof typeof ConnectionClosedReason];
21
+
22
+ export type ConnectionStatus =
23
+ | {
24
+ state: typeof ConnectionState.CLOSED;
25
+ code: ConnectionClosedReason;
26
+ reason: string;
27
+ }
28
+ | {
29
+ state: typeof ConnectionState.CONNECTING;
30
+ }
31
+ | {
32
+ state:
33
+ | typeof ConnectionState.OPEN
34
+ | typeof ConnectionState.CLOSING
35
+ | typeof ConnectionState.NOT_STARTED;
36
+ };
37
+
38
+ /** The recovery policy for a closed connection. */
39
+ export interface CloseDecision {
40
+ kind: "terminal" | "gave-up" | "retry";
41
+ status: ConnectionStatus;
42
+ }
43
+
44
+ export type ConnectionEvent = "open" | "close" | "message" | "error";
45
+
46
+ export interface IConnectionTransportMap {
47
+ open: WebSocketEventMap["open"];
48
+ message: WebSocketEventMap["message"];
49
+ close: WebSocketEventMap["close"];
50
+ error: WebSocketEventMap["error"];
51
+ }
52
+
53
+ export type ConnectionTransportCallback<T extends ConnectionEvent> = (
54
+ data: IConnectionTransportMap[T],
55
+ ) => void;
56
+
57
+ /** Transport readiness, using the WebSocket readyState values. */
58
+ export const TransportReadyState = {
59
+ CONNECTING: 0,
60
+ OPEN: 1,
61
+ CLOSING: 2,
62
+ CLOSED: 3,
63
+ } as const;
64
+
65
+ export type TransportReadyState =
66
+ (typeof TransportReadyState)[keyof typeof TransportReadyState];
67
+
68
+ /** A reconnectable source of notebook messages. */
69
+ export interface NotebookTransport {
70
+ reconnect(code?: number | undefined, reason?: string | undefined): void;
71
+ close(): void;
72
+ addEventListener<T extends ConnectionEvent>(
73
+ event: T,
74
+ callback: ConnectionTransportCallback<T>,
75
+ ): void;
76
+ removeEventListener<T extends ConnectionEvent>(
77
+ event: T,
78
+ callback: ConnectionTransportCallback<T>,
79
+ ): void;
80
+ readyState: TransportReadyState;
81
+ }
@@ -0,0 +1,61 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { z } from "zod";
4
+ import * as Protocol from "./protocol";
5
+
6
+ /**
7
+ * Notebook stream events, including native marimo notifications.
8
+ *
9
+ * Known events follow the generated protocol. Unknown events retain their name
10
+ * and cursor so clients can skip them without losing their place in the stream.
11
+ */
12
+
13
+ /** Event kinds requested by the client; removals and controls are implicit. */
14
+ export const NOTEBOOK_EVENT_TYPES = [
15
+ "notebook",
16
+ "document",
17
+ "cell",
18
+ "attachment",
19
+ "marimo",
20
+ ] as const satisfies readonly (Protocol.ObjectType | "marimo")[];
21
+
22
+ const envelope = z.object({
23
+ event: z.string().min(1),
24
+ data: z.unknown(),
25
+ id: z.string().min(1).optional(),
26
+ });
27
+
28
+ const knownNames = new Set<string>(
29
+ Protocol.Event.options.flatMap((schema) => schema.shape.event.options),
30
+ );
31
+
32
+ /** A decoded event, or the name and cursor of an unrecognized event. */
33
+ export type NotebookEvent =
34
+ | Protocol.EventOutput
35
+ | {
36
+ readonly event: "unknown";
37
+ readonly name: string;
38
+ readonly id?: string;
39
+ };
40
+
41
+ const sessionEnvelope = z.object({ data: Protocol.MarimoNotification });
42
+
43
+ /**
44
+ * Decode a host event or a legacy `{ data: notification }` session message.
45
+ *
46
+ * Throws if the envelope or a known event is malformed. Native notification
47
+ * payloads are passed through for the session handler to interpret.
48
+ */
49
+ export function parseNotebookEvent(value: unknown): NotebookEvent {
50
+ if (typeof value === "object" && value !== null && !("event" in value)) {
51
+ return {
52
+ event: "marimo",
53
+ data: { marimo: sessionEnvelope.parse(value).data },
54
+ };
55
+ }
56
+ const parsed = envelope.parse(value);
57
+ if (knownNames.has(parsed.event)) {
58
+ return Protocol.Event.parse(parsed);
59
+ }
60
+ return { event: "unknown", name: parsed.event, id: parsed.id };
61
+ }
@@ -0,0 +1,290 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { Logger } from "@/utils/Logger";
4
+ import { type Atom, atom, type createStore } from "jotai/vanilla";
5
+ import {
6
+ type ConnectionStatus,
7
+ ConnectionState,
8
+ ConnectionClosedReason,
9
+ type CloseDecision,
10
+ type NotebookTransport,
11
+ TransportReadyState,
12
+ } from "./connection";
13
+ import { parseNotebookEvent } from "./events";
14
+ import type { Attachment } from "./protocol";
15
+ import { attachments } from "./resources/attachments";
16
+ import { type Document, document } from "./resources/document";
17
+ import { createStream } from "./stream";
18
+
19
+ /** A connection to a notebook on a marimo host. */
20
+ export interface NotebookClient {
21
+ /** Client connection status, independent of application session readiness. */
22
+ readonly connectionAtom: Atom<ConnectionStatus>;
23
+ /**
24
+ * The most recently confirmed document.
25
+ *
26
+ * Null until a snapshot is complete, after reset or removal, and on
27
+ * connections without host document events. A temporary disconnection
28
+ * retains the last document. Partial updates are not exposed.
29
+ */
30
+ readonly documentAtom: Atom<Document | null>;
31
+ /** Live attachments, empty until the stream is ready or while disconnected. */
32
+ readonly attachmentsAtom: Atom<readonly Attachment[]>;
33
+ /** Attempt to restore a closed connection after checking host health. */
34
+ reconnect(): Promise<void>;
35
+ }
36
+
37
+ /** The transport and application callbacks needed to connect a notebook. */
38
+ export interface NotebookConnection {
39
+ /** Create a transport that reads the URL again on each connection attempt. */
40
+ createTransport: (url: () => string) => NotebookTransport;
41
+ /** The current stream URL, including authentication and subscription options. */
42
+ url: () => string;
43
+ /** Host events require an intact stream; legacy notifications are independent. */
44
+ messageFormat: "host" | "session";
45
+ /** Abort to close the transport and clear the client's state. */
46
+ signal: AbortSignal;
47
+ /** Wait until the host can accept the initial connection. */
48
+ waitUntilReady: () => Promise<void>;
49
+ /** Check whether the host is reachable before an explicit reconnect. */
50
+ checkHealth: () => Promise<boolean>;
51
+ /** Decode wire text without interpreting its event type. */
52
+ decodeJson: (message: string) => unknown;
53
+ /** Handle a native marimo notification. Its payload has not been validated. */
54
+ onNotification: (notification: unknown) => void;
55
+ /** Report a malformed message without coupling the client to editor UI. */
56
+ onMessageError: (error: unknown) => void;
57
+ /** Choose the connection status and retry policy after the transport closes. */
58
+ classifyClose: (event: CloseEvent) => CloseDecision;
59
+ }
60
+
61
+ /** Create a client without opening a connection. */
62
+ export function createNotebookClient(store: ReturnType<typeof createStore>) {
63
+ const connectionAtom = atom<ConnectionStatus>({
64
+ state: ConnectionState.NOT_STARTED,
65
+ });
66
+ // One atom holds the whole stream, which resource atoms derive from.
67
+ const stream = createStream({ document, attachments });
68
+ const streamAtom = atom(stream.initial);
69
+ // A resumed stream is live, but presence waits for this connection's ready.
70
+ const readyAtom = atom(false);
71
+ const documentAtom = atom(
72
+ (get) => get(streamAtom).resources.document.current,
73
+ );
74
+ const attachmentsAtom = atom((get) =>
75
+ get(readyAtom) && get(connectionAtom).state === ConnectionState.OPEN
76
+ ? get(streamAtom).resources.attachments
77
+ : attachments.initial,
78
+ );
79
+ let reconnect: (() => Promise<void>) | undefined;
80
+ const client: NotebookClient = {
81
+ connectionAtom,
82
+ documentAtom,
83
+ attachmentsAtom,
84
+ reconnect: async () => reconnect?.(),
85
+ };
86
+
87
+ function connect({
88
+ createTransport,
89
+ url,
90
+ messageFormat,
91
+ signal,
92
+ waitUntilReady,
93
+ checkHealth,
94
+ onNotification,
95
+ onMessageError,
96
+ decodeJson,
97
+ classifyClose,
98
+ }: NotebookConnection) {
99
+ if (signal.aborted) {
100
+ return;
101
+ }
102
+ let shouldRetry = true;
103
+ let terminal = false;
104
+ let checkingHealth = false;
105
+ let streamKey: string | undefined;
106
+ const transport = createTransport(() => {
107
+ if (messageFormat === "session") {
108
+ return url();
109
+ }
110
+ const next = new URL(url());
111
+ next.searchParams.delete("cursor");
112
+ if (signal.aborted) {
113
+ return next.toString();
114
+ }
115
+ // A cursor is useful with the state that produced it, which a change
116
+ // of session or notebook invalidates. Authentication may change freely.
117
+ const identity = new URL(next);
118
+ identity.searchParams.delete("access_token");
119
+ identity.searchParams.sort();
120
+ const key = identity.toString();
121
+ const state =
122
+ key === streamKey
123
+ ? stream.resume(store.get(streamAtom))
124
+ : stream.initial;
125
+ streamKey = key;
126
+ store.set(readyAtom, false);
127
+ store.set(streamAtom, state);
128
+ if (state.cursor !== null) {
129
+ next.searchParams.set("cursor", state.cursor);
130
+ }
131
+ return next.toString();
132
+ });
133
+ const setStatus = (status: ConnectionStatus) => {
134
+ store.set(readyAtom, false);
135
+ store.set(connectionAtom, status);
136
+ };
137
+ const disconnected = {
138
+ state: ConnectionState.CLOSED,
139
+ code: ConnectionClosedReason.KERNEL_DISCONNECTED,
140
+ reason: "kernel not found",
141
+ } satisfies ConnectionStatus;
142
+
143
+ function retry(code?: number, reason?: string) {
144
+ if (!signal.aborted && !terminal && shouldRetry) {
145
+ shouldRetry = false;
146
+ transport.reconnect(code, reason);
147
+ }
148
+ }
149
+
150
+ function onOpen() {
151
+ if (signal.aborted || terminal) {
152
+ return;
153
+ }
154
+ shouldRetry = true;
155
+ setStatus({ state: ConnectionState.OPEN });
156
+ }
157
+
158
+ function onClose(event: CloseEvent) {
159
+ if (signal.aborted || terminal) {
160
+ return;
161
+ }
162
+ Logger.warn("Notebook connection closed", event.code, event.reason);
163
+ const decision = classifyClose(event);
164
+ setStatus(decision.status);
165
+ if (decision.kind === "terminal") {
166
+ terminal = true;
167
+ transport.close();
168
+ } else if (decision.kind === "retry") {
169
+ retry(event.code, event.reason);
170
+ } else {
171
+ terminal = true;
172
+ }
173
+ }
174
+
175
+ function onError(event: Event) {
176
+ if (signal.aborted || terminal) {
177
+ return;
178
+ }
179
+ Logger.warn("Notebook connection error", event);
180
+ setStatus(disconnected);
181
+ retry();
182
+ }
183
+
184
+ function onMessage(message: MessageEvent<string>) {
185
+ if (signal.aborted || terminal) {
186
+ return;
187
+ }
188
+ let event;
189
+ try {
190
+ event = parseNotebookEvent(decodeJson(message.data));
191
+ store.set(streamAtom, stream.reduce(store.get(streamAtom), event));
192
+ if (event.event === "ready" || event.event === "reset") {
193
+ store.set(readyAtom, event.event === "ready");
194
+ }
195
+ } catch (error) {
196
+ Logger.error("Failed to decode notebook message", error);
197
+ if (messageFormat === "session") {
198
+ onMessageError(error);
199
+ return;
200
+ }
201
+ store.set(streamAtom, stream.initial);
202
+ terminal = true;
203
+ setStatus({
204
+ ...disconnected,
205
+ reason: "Failed to decode notebook message",
206
+ });
207
+ transport.close();
208
+ return;
209
+ }
210
+ if (event.event === "marimo") {
211
+ onNotification(event.data.marimo);
212
+ }
213
+ }
214
+
215
+ reconnect = async () => {
216
+ if (
217
+ signal.aborted ||
218
+ checkingHealth ||
219
+ transport.readyState === TransportReadyState.OPEN ||
220
+ transport.readyState === TransportReadyState.CONNECTING
221
+ ) {
222
+ return;
223
+ }
224
+ checkingHealth = true;
225
+ setStatus({ state: ConnectionState.CONNECTING });
226
+ try {
227
+ const healthy = await checkHealth();
228
+ if (signal.aborted) {
229
+ return;
230
+ }
231
+ if (!healthy) {
232
+ shouldRetry = false;
233
+ setStatus(disconnected);
234
+ return;
235
+ }
236
+ terminal = false;
237
+ shouldRetry = true;
238
+ transport.reconnect();
239
+ } catch (error) {
240
+ if (!signal.aborted) {
241
+ Logger.error("Notebook health check failed", error);
242
+ shouldRetry = false;
243
+ setStatus(disconnected);
244
+ }
245
+ } finally {
246
+ checkingHealth = false;
247
+ }
248
+ };
249
+
250
+ signal.addEventListener(
251
+ "abort",
252
+ () => {
253
+ transport.removeEventListener("open", onOpen);
254
+ transport.removeEventListener("close", onClose);
255
+ transport.removeEventListener("error", onError);
256
+ transport.removeEventListener("message", onMessage);
257
+ transport.close();
258
+ reconnect = undefined;
259
+ store.set(streamAtom, stream.initial);
260
+ setStatus({ state: ConnectionState.NOT_STARTED });
261
+ },
262
+ { once: true },
263
+ );
264
+
265
+ transport.addEventListener("open", onOpen);
266
+ transport.addEventListener("close", onClose);
267
+ transport.addEventListener("error", onError);
268
+ transport.addEventListener("message", onMessage);
269
+
270
+ if (transport.readyState === TransportReadyState.CLOSED) {
271
+ setStatus({ state: ConnectionState.CONNECTING });
272
+ void waitUntilReady()
273
+ .then(() => {
274
+ if (!signal.aborted) {
275
+ transport.reconnect();
276
+ }
277
+ })
278
+ .catch((error) => {
279
+ if (!signal.aborted) {
280
+ Logger.error("Healthy connection never made", error);
281
+ terminal = true;
282
+ setStatus(disconnected);
283
+ transport.close();
284
+ }
285
+ });
286
+ }
287
+ }
288
+
289
+ return { client, connect };
290
+ }
@@ -618,7 +618,7 @@ export const Execution = zod
618
618
  .string()
619
619
  .nullable()
620
620
  .describe(
621
- "The submitted code for an execution with no cell. Null for a cell's execution, since the cell holds its code.",
621
+ "The submitted code for an execution with no cell. Null for a cell's execution; its source is available through the cell when the host exposes it.",
622
622
  ),
623
623
  code_hash: Etag.describe(
624
624
  "The `code_hash` of the code that was evaluated, computed as a cell's is.",
@@ -715,7 +715,9 @@ export const CellConfig = zod
715
715
  disabled: zod.boolean().describe("Whether the cell is kept from running."),
716
716
  hide_code: zod
717
717
  .boolean()
718
- .describe("Whether clients hide the code by default."),
718
+ .describe(
719
+ "Whether clients hide the code by default. This is a presentation preference; source availability is represented by `Cell.code`.",
720
+ ),
719
721
  expand_output: zod
720
722
  .boolean()
721
723
  .describe(
@@ -771,8 +773,9 @@ export const Cell = zod
771
773
  notebook_id: NotebookId,
772
774
  code: zod
773
775
  .string()
776
+ .nullable()
774
777
  .describe(
775
- "The cell's source code, with newline line endings. A host normalizes other line endings when it loads a file.",
778
+ "The cell's source code, with newline line endings, or null when the host does not expose source to this client. An empty string is an empty cell. A host normalizes other line endings when it loads a file. Source availability does not grant editing permission; see the notebook's operations.",
776
779
  ),
777
780
  name: zod
778
781
  .string()
@@ -784,7 +787,7 @@ export const Cell = zod
784
787
  "Identifies code, name, and config together, and is the precondition for editing the cell. Its text is `code_hash`, `name`, and the config values `column`, `disabled`, `hide_code`, and `expand_output` in that order, each written as a JSON scalar such as `null`, `0`, or `true`.",
785
788
  ),
786
789
  code_hash: Etag.describe(
787
- "Identifies the code alone. Its text is `code`. Unchanged by renaming the cell or editing its config. Compare with an execution's `code_hash` to tell whether a result came from the current code.",
790
+ "Identifies the code alone. Its text is the actual source, even when `code` is null. Withholding source does not change this hash, the cell or document etags, or their versions. Unchanged by renaming the cell or editing its config. Compare with an execution's `code_hash` to tell whether a result came from the current code without needing the source.",
788
791
  ),
789
792
  version: zod
790
793
  .int()
@@ -1800,7 +1803,7 @@ export const CellInput = zod
1800
1803
  config: CellConfigInput.optional(),
1801
1804
  })
1802
1805
  .describe(
1803
- "A cell as a client writes it. The host derives the etags and the analysis.",
1806
+ "A cell as a client writes it. Code must be a string, even when the host withholds source in cell records. Null is not an edit or a request to preserve existing source. The host derives the etags and the analysis.",
1804
1807
  );
1805
1808
 
1806
1809
  export type CellInput = zod.input<typeof CellInput>;
@@ -1892,7 +1895,9 @@ export const EditResponse = zod
1892
1895
  "The version of each cell the request created or changed, by id, and null for each cell it deleted.",
1893
1896
  ),
1894
1897
  })
1895
- .describe("The document after an edit or a save, and the cells it touched.");
1898
+ .describe(
1899
+ "The document as an edit or save left it, or at a later revision, and the cells it touched.",
1900
+ );
1896
1901
 
1897
1902
  export type EditResponse = zod.input<typeof EditResponse>;
1898
1903
  export type EditResponseOutput = zod.output<typeof EditResponse>;
@@ -0,0 +1,19 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { Attachment } from "../protocol";
4
+ import type { Resource } from "./resource";
5
+
6
+ /** Attachment records retained with the stream's resume cursor. */
7
+ export const attachments: Resource<readonly Attachment[]> = {
8
+ initial: [],
9
+ on: {
10
+ attachment: (state, { data: { attachment } }) =>
11
+ state.some((item) => item.id === attachment.id)
12
+ ? state.map((item) => (item.id === attachment.id ? attachment : item))
13
+ : [...state, attachment],
14
+ "attachment.removed": (state, { data }) =>
15
+ state.some((item) => item.id === data.id)
16
+ ? state.filter((item) => item.id !== data.id)
17
+ : state,
18
+ },
19
+ };
@@ -0,0 +1,101 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type * as Protocol from "../protocol";
4
+ import type { Resource } from "./resource";
5
+
6
+ /**
7
+ * The notebook's content at one revision.
8
+ *
9
+ * The revision record carries the cell order, app config, etags, and
10
+ * version. Each cell in its order has a record in `cells`.
11
+ */
12
+ export interface Document {
13
+ readonly revision: Protocol.Document;
14
+ readonly cells: ReadonlyMap<Protocol.CellId, Protocol.Cell>;
15
+ }
16
+
17
+ /** The records of a revision still being received. */
18
+ interface PendingDocument {
19
+ /** Null until the `document` event that closes the group arrives. */
20
+ readonly revision: Protocol.Document | null;
21
+ readonly cells: ReadonlyMap<Protocol.CellId, Protocol.Cell>;
22
+ }
23
+
24
+ /**
25
+ * The confirmed document and the revision being assembled after it.
26
+ *
27
+ * A snapshot's records are pending until `ready`. A live revision is pending
28
+ * from its first cell until its `document` event.
29
+ */
30
+ interface DocumentState {
31
+ readonly current: Document | null;
32
+ readonly pending: PendingDocument | null;
33
+ }
34
+
35
+ function pendingOf(state: DocumentState): PendingDocument {
36
+ return (
37
+ state.pending ?? {
38
+ revision: state.current?.revision ?? null,
39
+ cells: state.current?.cells ?? new Map(),
40
+ }
41
+ );
42
+ }
43
+
44
+ /** Confirm a revision whose records are complete. */
45
+ function publish(
46
+ revision: Protocol.Document,
47
+ cells: ReadonlyMap<Protocol.CellId, Protocol.Cell>,
48
+ ): DocumentState {
49
+ const order = new Set(revision.order);
50
+ if (
51
+ order.size !== revision.order.length ||
52
+ order.size !== cells.size ||
53
+ [...order].some((id) => !cells.has(id)) ||
54
+ [...cells.values()].some((cell) => cell.version > revision.version)
55
+ ) {
56
+ throw new Error("Document records do not match its revision and order");
57
+ }
58
+ return { current: { revision, cells }, pending: null };
59
+ }
60
+
61
+ export const document: Resource<DocumentState> = {
62
+ initial: { current: null, pending: null },
63
+ on: {
64
+ cell: (state, { data }) => {
65
+ const pending = pendingOf(state);
66
+ return {
67
+ ...state,
68
+ pending: {
69
+ revision: pending.revision,
70
+ cells: new Map(pending.cells).set(data.cell.id, data.cell),
71
+ },
72
+ };
73
+ },
74
+ "cell.removed": (state, { data }) => {
75
+ const pending = pendingOf(state);
76
+ if (!pending.cells.has(data.id)) {
77
+ return state;
78
+ }
79
+ const cells = new Map(pending.cells);
80
+ cells.delete(data.id);
81
+ return { ...state, pending: { ...pending, cells } };
82
+ },
83
+ document: (state, { data }, { phase }) => {
84
+ const { cells } = pendingOf(state);
85
+ return phase === "snapshot"
86
+ ? { ...state, pending: { revision: data.document, cells } }
87
+ : publish(data.document, cells);
88
+ },
89
+ ready: (state, _event, { phase }) => {
90
+ if (state.pending === null) {
91
+ return state;
92
+ }
93
+ if (phase === "live") {
94
+ throw new Error("Stream ended with an incomplete document update");
95
+ }
96
+ return state.pending.revision === null
97
+ ? { current: null, pending: null }
98
+ : publish(state.pending.revision, state.pending.cells);
99
+ },
100
+ },
101
+ };
@@ -0,0 +1,47 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type * as Protocol from "../protocol";
4
+
5
+ /**
6
+ * Events the stream handles itself.
7
+ *
8
+ * They identify the notebook, discard state, or carry a native
9
+ * notification. Resources do not receive them.
10
+ */
11
+ type StreamEventName = "reset" | "notebook" | "notebook.removed" | "marimo";
12
+
13
+ /** Events a resource may handle. `ready` marks the end of the snapshot. */
14
+ export type ResourceEvent = Exclude<
15
+ Protocol.EventOutput,
16
+ { event: StreamEventName }
17
+ >;
18
+
19
+ /** What a handler may know about the stream beyond the event. */
20
+ export interface HandlerContext {
21
+ /** `snapshot` until `ready` arrives and `live` after it. */
22
+ readonly phase: "snapshot" | "live";
23
+ }
24
+
25
+ /**
26
+ * A pure handler for each event the resource reads.
27
+ *
28
+ * A handler returns `state` when the event does not apply to it.
29
+ */
30
+ type EventHandlers<State> = {
31
+ readonly [Name in ResourceEvent["event"]]?: (
32
+ state: State,
33
+ event: Extract<ResourceEvent, { event: Name }>,
34
+ context: HandlerContext,
35
+ ) => State;
36
+ };
37
+
38
+ /**
39
+ * One kind of object assembled from its events.
40
+ *
41
+ * The stream returns a resource to `initial` on `reset` and when the
42
+ * notebook is removed.
43
+ */
44
+ export interface Resource<State> {
45
+ readonly initial: State;
46
+ readonly on: EventHandlers<State>;
47
+ }