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

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 +2 -1
  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
@@ -21,14 +21,9 @@ import {
21
21
  vi,
22
22
  } from "vitest";
23
23
 
24
- vi.mock("@/core/websocket/useWebSocket", async () => {
25
- const actual =
26
- await vi.importActual<typeof import("../useWebSocket")>("../useWebSocket");
27
- return {
28
- ...actual,
29
- useConnectionTransport: vi.fn(),
30
- };
31
- });
24
+ vi.mock("../transports/create", () => ({
25
+ createConnectionTransport: vi.fn(),
26
+ }));
32
27
 
33
28
  vi.mock("@/core/runtime/config", async () => {
34
29
  const actual = await vi.importActual<typeof import("@/core/runtime/config")>(
@@ -41,21 +36,28 @@ vi.mock("@/core/runtime/config", async () => {
41
36
  });
42
37
 
43
38
  import { MockNotebook } from "@/__mocks__/notebook";
39
+ import * as Toast from "@/components/ui/use-toast";
40
+ import { Logger } from "@/utils/Logger";
41
+ import { Deferred } from "@/utils/Deferred";
44
42
  import { cellId } from "@/__tests__/branded";
45
43
  import { alertAtom, getPackageAlert } from "@/core/alerts/state";
46
44
  import { notebookAtom } from "@/core/cells/cells";
47
45
  import { AppConfigSchema } from "@/core/config/config-schema";
48
46
  import { ConnectionNotice } from "@/components/editor/alerts/connection-notice";
49
47
  import { kernelStartupErrorAtom } from "@/core/errors/state";
48
+ import type { RunEvent } from "@/core/host/protocol";
50
49
  import type { NotificationPayload } from "@/core/kernel/messages";
51
50
  import { useRuntimeManager } from "@/core/runtime/config";
52
51
  import { initialRunCompletedAtom } from "../../kernel/state";
53
52
  import { connectionAtom, startupProgressAtom } from "../../network/connection";
54
- import type { SessionId } from "../../kernel/session";
53
+ import { generateSessionId } from "../../kernel/session";
54
+ import { RuntimeManager } from "../../runtime/runtime";
55
55
  import { WebSocketClosedReason, ConnectionState } from "../types";
56
56
  import type { IConnectionTransport } from "../transports/transport";
57
- import { useMarimoKernelConnection } from "../useMarimoKernelConnection";
58
- import { useConnectionTransport } from "../useWebSocket";
57
+ import { useNotebookSession } from "../useNotebookSession";
58
+ import { createConnectionTransport } from "../transports/create";
59
+
60
+ const SESSION_ID = generateSessionId();
59
61
 
60
62
  interface MockTransport {
61
63
  readyState: 0 | 1 | 2 | 3;
@@ -79,16 +81,47 @@ function makeTransport(
79
81
  };
80
82
  }
81
83
 
84
+ function getTransportHandlers() {
85
+ const result = vi.mocked(createConnectionTransport).mock.results.at(-1);
86
+ if (!result || result.type !== "return") {
87
+ throw new Error("No connection transport was created");
88
+ }
89
+ const transport = result.value;
90
+ function emit(
91
+ event: "open" | "close" | "error" | "message",
92
+ message: Event | CloseEvent | MessageEvent,
93
+ ) {
94
+ const callback = vi
95
+ .mocked(transport.addEventListener)
96
+ .mock.calls.find(([name]) => name === event)?.[1];
97
+ callback?.(message);
98
+ }
99
+ return {
100
+ onError: (event: Event) => emit("error", event),
101
+ onOpen: (event: Event) => emit("open", event),
102
+ onClose: (event: CloseEvent) => emit("close", event),
103
+ onMessage: (event: MessageEvent) => emit("message", event),
104
+ };
105
+ }
106
+
82
107
  function makeRuntimeManager(
83
108
  reconcileFromHealth = vi.fn<() => Promise<boolean>>().mockResolvedValue(true),
84
109
  ) {
85
- return {
110
+ const runtime = new RuntimeManager(
111
+ { url: "http://localhost", lazy: true },
112
+ true,
113
+ );
114
+ vi.spyOn(runtime, "reconcileFromHealth").mockImplementation(
86
115
  reconcileFromHealth,
87
- probeHealth: vi.fn().mockResolvedValue(true),
88
- getWsURL: () => new URL("ws://localhost/ws"),
89
- waitForHealthy: vi.fn().mockResolvedValue(undefined),
90
- isSameOrigin: true,
91
- };
116
+ );
117
+ vi.spyOn(runtime, "probeHealth").mockResolvedValue(true);
118
+ vi.spyOn(runtime, "getWsURL").mockReturnValue(new URL("ws://localhost/ws"));
119
+ // Keep the initial connection pending; these tests exercise session retries.
120
+ vi.spyOn(runtime, "waitForHealthy").mockReturnValue(
121
+ new Deferred<void>().promise,
122
+ );
123
+ vi.spyOn(runtime, "isSameOrigin", "get").mockReturnValue(false);
124
+ return runtime;
92
125
  }
93
126
 
94
127
  function renderConnectionHook(store: ReturnType<typeof createStore>) {
@@ -99,16 +132,16 @@ function renderConnectionHook(store: ReturnType<typeof createStore>) {
99
132
  );
100
133
  return renderHook(
101
134
  () =>
102
- useMarimoKernelConnection({
103
- sessionId: "test-session" as SessionId,
135
+ useNotebookSession({
136
+ sessionId: SESSION_ID,
104
137
  autoInstantiate: false,
105
- setCells: () => {},
138
+ setCells: vi.fn(),
106
139
  }),
107
140
  { wrapper },
108
141
  );
109
142
  }
110
143
 
111
- describe("useMarimoKernelConnection.reconnect()", () => {
144
+ describe("useNotebookSession.reconnect()", () => {
112
145
  let transport: MockTransport;
113
146
  let isHealthy: Mock<() => Promise<boolean>>;
114
147
  let store: ReturnType<typeof createStore>;
@@ -122,12 +155,8 @@ describe("useMarimoKernelConnection.reconnect()", () => {
122
155
  code: WebSocketClosedReason.KERNEL_DISCONNECTED,
123
156
  reason: "kernel not found",
124
157
  });
125
- vi.mocked(useConnectionTransport).mockReturnValue(transport);
126
- vi.mocked(useRuntimeManager).mockReturnValue(
127
- makeRuntimeManager(isHealthy) as unknown as ReturnType<
128
- typeof useRuntimeManager
129
- >,
130
- );
158
+ vi.mocked(createConnectionTransport).mockReturnValue(transport);
159
+ vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager(isHealthy));
131
160
  });
132
161
 
133
162
  function renderUseHook() {
@@ -183,31 +212,124 @@ describe("useMarimoKernelConnection.reconnect()", () => {
183
212
  });
184
213
  });
185
214
 
186
- describe("useMarimoKernelConnection messages", () => {
187
- it("records completion of the initial run", () => {
188
- const store = createStore();
189
- vi.mocked(useConnectionTransport).mockClear();
190
- vi.mocked(useConnectionTransport).mockReturnValue(
215
+ describe("useNotebookSession messages", () => {
216
+ let store: ReturnType<typeof createStore>;
217
+
218
+ beforeEach(() => {
219
+ store = createStore();
220
+ vi.mocked(createConnectionTransport).mockClear();
221
+ vi.mocked(createConnectionTransport).mockReturnValue(
191
222
  makeTransport(WebSocket.OPEN),
192
223
  );
193
- vi.mocked(useRuntimeManager).mockReturnValue(
194
- makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
195
- );
224
+ vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
196
225
  renderConnectionHook(store);
226
+ });
197
227
 
198
- const options = vi.mocked(useConnectionTransport).mock.calls.at(-1)?.[0];
199
- expect(store.get(initialRunCompletedAtom)).toBe(false);
228
+ function receive(payload: unknown) {
229
+ const options = getTransportHandlers();
200
230
  act(() => {
201
- options?.onMessage(
202
- new MessageEvent("message", {
203
- data: JSON.stringify({
204
- op: "completed-run",
205
- data: { op: "completed-run", run_id: null },
206
- }),
207
- }),
231
+ options.onMessage(
232
+ new MessageEvent("message", { data: JSON.stringify(payload) }),
208
233
  );
209
234
  });
210
- expect(store.get(initialRunCompletedAtom)).toBe(true);
235
+ }
236
+
237
+ it.each(["{broken", "null"])(
238
+ "reports a malformed legacy frame and continues applying notifications: %s",
239
+ (data) => {
240
+ const toast = vi.spyOn(Toast, "toast");
241
+ try {
242
+ receive({ data: { op: "reconnected" } });
243
+ act(() =>
244
+ getTransportHandlers().onMessage(
245
+ new MessageEvent("message", { data }),
246
+ ),
247
+ );
248
+ expect(toast).toHaveBeenCalledExactlyOnceWith({
249
+ title: "Failed to handle message",
250
+ description: expect.any(String),
251
+ variant: "danger",
252
+ });
253
+ expect(store.get(connectionAtom)).toEqual({
254
+ state: ConnectionState.OPEN,
255
+ });
256
+ receive({
257
+ data: {
258
+ op: "completed-run",
259
+ run_id: "r1",
260
+ outcome: "succeeded",
261
+ error: null,
262
+ },
263
+ });
264
+ expect(store.get(initialRunCompletedAtom)).toBe(true);
265
+ } finally {
266
+ toast.mockRestore();
267
+ }
268
+ },
269
+ );
270
+
271
+ it.each(["session", "host"])(
272
+ "records completion of the initial run from the %s stream",
273
+ (stream) => {
274
+ const notification = {
275
+ op: "completed-run",
276
+ run_id: null,
277
+ outcome: "succeeded",
278
+ error: null,
279
+ } satisfies NotificationPayload["data"];
280
+ expect(store.get(initialRunCompletedAtom)).toBe(false);
281
+ receive(
282
+ stream === "host"
283
+ ? { id: "42", event: "marimo", data: { marimo: notification } }
284
+ : { op: notification.op, data: notification },
285
+ );
286
+ expect(store.get(initialRunCompletedAtom)).toBe(true);
287
+ },
288
+ );
289
+
290
+ it("ignores other host events before handling the next notification", () => {
291
+ const errors = vi.spyOn(Logger, "error").mockImplementation(vi.fn());
292
+ try {
293
+ receive({ id: "1", event: "ready", data: {} });
294
+ receive({
295
+ id: "2",
296
+ event: "run",
297
+ data: {
298
+ run: {
299
+ id: "r1",
300
+ notebook_id: "n1",
301
+ caused_by: null,
302
+ status: "completed",
303
+ outcome: "succeeded",
304
+ started_at: "2026-10-19T12:00:00Z",
305
+ completed_at: "2026-10-19T12:00:01Z",
306
+ },
307
+ },
308
+ } satisfies RunEvent);
309
+ receive({
310
+ id: "3",
311
+ event: "future-event",
312
+ data: { marimo: { op: "completed-run", run_id: "r1" } },
313
+ });
314
+ receive({ id: "4", event: "another-event", data: null });
315
+ expect(store.get(initialRunCompletedAtom)).toBe(false);
316
+ receive({
317
+ id: "5",
318
+ event: "marimo",
319
+ data: {
320
+ marimo: {
321
+ op: "completed-run",
322
+ run_id: "r1",
323
+ outcome: "succeeded",
324
+ error: null,
325
+ },
326
+ },
327
+ });
328
+ expect(store.get(initialRunCompletedAtom)).toBe(true);
329
+ expect(errors).not.toHaveBeenCalled();
330
+ } finally {
331
+ errors.mockRestore();
332
+ }
211
333
  });
212
334
  });
213
335
 
@@ -216,15 +338,13 @@ it.each(["kernel-ready", "reconnected"])(
216
338
  async (op) => {
217
339
  const store = createStore();
218
340
  store.set(connectionAtom, { state: ConnectionState.CONNECTING });
219
- vi.mocked(useConnectionTransport).mockClear();
220
- vi.mocked(useConnectionTransport).mockReturnValue(
341
+ vi.mocked(createConnectionTransport).mockClear();
342
+ vi.mocked(createConnectionTransport).mockReturnValue(
221
343
  makeTransport(WebSocket.OPEN),
222
344
  );
223
- vi.mocked(useRuntimeManager).mockReturnValue(
224
- makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
225
- );
345
+ vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
226
346
  renderConnectionHook(store);
227
- const options = vi.mocked(useConnectionTransport).mock.calls.at(-1)![0];
347
+ const options = getTransportHandlers();
228
348
 
229
349
  await act(async () => {
230
350
  await options.onOpen(new Event("open"));
@@ -269,15 +389,13 @@ describe("connection notice", () => {
269
389
  const store = createStore();
270
390
  store.set(connectionAtom, { state: ConnectionState.CONNECTING });
271
391
  const transport = makeTransport(WebSocket.OPEN);
272
- vi.mocked(useConnectionTransport).mockReturnValue(transport);
273
- vi.mocked(useRuntimeManager).mockReturnValue(
274
- makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
275
- );
392
+ vi.mocked(createConnectionTransport).mockReturnValue(transport);
393
+ vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
276
394
  const Connection = () => {
277
- const { reconnect } = useMarimoKernelConnection({
278
- sessionId: "test-session" as SessionId,
395
+ const { reconnect } = useNotebookSession({
396
+ sessionId: SESSION_ID,
279
397
  autoInstantiate: false,
280
- setCells: () => {},
398
+ setCells: vi.fn(),
281
399
  });
282
400
  return (
283
401
  <ConnectionNotice
@@ -293,7 +411,7 @@ describe("connection notice", () => {
293
411
  </ErrorBoundary>
294
412
  </JotaiProvider>,
295
413
  );
296
- const options = vi.mocked(useConnectionTransport).mock.calls.at(-1)![0];
414
+ const options = getTransportHandlers();
297
415
  const send = (data: NotificationPayload["data"]) =>
298
416
  act(() => {
299
417
  options.onMessage(
@@ -482,15 +600,13 @@ describe("connection notice", () => {
482
600
 
483
601
  it("replaces environment state from snapshots, then continues live progress", () => {
484
602
  const store = createStore();
485
- vi.mocked(useConnectionTransport).mockClear();
486
- vi.mocked(useConnectionTransport).mockReturnValue(
603
+ vi.mocked(createConnectionTransport).mockClear();
604
+ vi.mocked(createConnectionTransport).mockReturnValue(
487
605
  makeTransport(WebSocket.OPEN),
488
606
  );
489
- vi.mocked(useRuntimeManager).mockReturnValue(
490
- makeRuntimeManager() as unknown as ReturnType<typeof useRuntimeManager>,
491
- );
607
+ vi.mocked(useRuntimeManager).mockReturnValue(makeRuntimeManager());
492
608
  renderConnectionHook(store);
493
- const options = vi.mocked(useConnectionTransport).mock.calls.at(-1)?.[0];
609
+ const options = getTransportHandlers();
494
610
  function receive(data: NotificationPayload["data"]) {
495
611
  act(() =>
496
612
  options?.onMessage(
@@ -0,0 +1,70 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { CloseDecision } from "../host/connection";
4
+ import { Logger } from "@/utils/Logger";
5
+ import { TRANSPORT_EXHAUSTED_REASON } from "./transports/ws";
6
+ import { ConnectionState, WebSocketClosedReason } from "./types";
7
+
8
+ // Keep server reasons in sync with ws_endpoint.py and ws/*.py.
9
+ // SSE carries the same reasons in close events. Unclassified reasons retry.
10
+ export function classifyCloseEvent(event: { reason?: string }): CloseDecision {
11
+ switch (event.reason) {
12
+ case TRANSPORT_EXHAUSTED_REASON:
13
+ return {
14
+ kind: "gave-up",
15
+ status: {
16
+ state: ConnectionState.CLOSED,
17
+ code: WebSocketClosedReason.KERNEL_DISCONNECTED,
18
+ reason: "kernel not found",
19
+ },
20
+ };
21
+ case "MARIMO_NO_FILE_KEY":
22
+ case "MARIMO_NO_SESSION_ID":
23
+ case "MARIMO_NO_SESSION":
24
+ case "MARIMO_SHUTDOWN":
25
+ return {
26
+ kind: "terminal",
27
+ status: {
28
+ state: ConnectionState.CLOSED,
29
+ code: WebSocketClosedReason.KERNEL_DISCONNECTED,
30
+ reason: "kernel not found",
31
+ },
32
+ };
33
+ case "MARIMO_UNAUTHORIZED":
34
+ return {
35
+ kind: "terminal",
36
+ status: {
37
+ state: ConnectionState.CLOSED,
38
+ code: WebSocketClosedReason.KERNEL_DISCONNECTED,
39
+ reason: "not authorized",
40
+ },
41
+ };
42
+ case "MARIMO_KIOSK_NOT_ALLOWED":
43
+ return {
44
+ kind: "terminal",
45
+ status: {
46
+ state: ConnectionState.CLOSED,
47
+ code: WebSocketClosedReason.KERNEL_DISCONNECTED,
48
+ reason: "kiosk mode is not available for this session",
49
+ },
50
+ };
51
+ case "MARIMO_KERNEL_STARTUP_ERROR":
52
+ return {
53
+ kind: "terminal",
54
+ status: {
55
+ state: ConnectionState.CLOSED,
56
+ code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
57
+ reason: "Failed to start kernel sandbox",
58
+ },
59
+ };
60
+ default:
61
+ if (event.reason) {
62
+ Logger.warn("Unrecognized connection close reason", event.reason);
63
+ }
64
+ }
65
+
66
+ return {
67
+ kind: "retry",
68
+ status: { state: ConnectionState.CONNECTING },
69
+ };
70
+ }
@@ -0,0 +1,7 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { atom } from "jotai";
4
+ import type { NotebookClient } from "../host/notebook-client";
5
+
6
+ /** The client owned by the mounted notebook session. */
7
+ export const notebookClientAtom = atom<NotebookClient | null>(null);
@@ -108,8 +108,6 @@ describe("SseTransport", () => {
108
108
  transport.reconnect();
109
109
  await vi.waitFor(() => expect(opens).toHaveLength(1));
110
110
 
111
- // Stop retrying when the consumer treats the close as terminal,
112
- // mirroring useMarimoKernelConnection's terminal path.
113
111
  transport.addEventListener("close", () => transport.close());
114
112
  push('event: close\ndata: {"code": 1000, "reason": "MARIMO_SHUTDOWN"}\n\n');
115
113
  await vi.waitFor(() => expect(closes).toHaveLength(1));
@@ -146,8 +144,7 @@ describe("SseTransport", () => {
146
144
  fetchMock
147
145
  .mockResolvedValueOnce(sseResponse(first.stream))
148
146
  .mockResolvedValueOnce(sseResponse(second.stream));
149
- // Mirror useMarimoKernelConnection's retry path: reconnect()
150
- // synchronously from inside the close handler.
147
+ // Reconnect synchronously from the close handler.
151
148
  transport.addEventListener("close", () => transport.reconnect());
152
149
 
153
150
  transport.reconnect();
@@ -0,0 +1,31 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { createPyodideConnection } from "../../wasm/bridge";
4
+ import { isWasm } from "../../wasm/utils";
5
+ import { BasicTransport } from "./basic";
6
+ import { SseTransport } from "./sse";
7
+ import { WsTransport } from "./ws";
8
+ import type { IConnectionTransport } from "./transport";
9
+
10
+ export interface ConnectionTransportOptions {
11
+ url: () => string;
12
+ static: boolean;
13
+ transportType: "websocket" | "sse";
14
+ headers: () => Record<string, string>;
15
+ }
16
+
17
+ export function createConnectionTransport(
18
+ options: ConnectionTransportOptions,
19
+ ): IConnectionTransport {
20
+ if (options.static) {
21
+ return BasicTransport.empty();
22
+ }
23
+ if (isWasm()) {
24
+ return createPyodideConnection();
25
+ }
26
+ // Read the URL lazily to follow runtime redirects.
27
+ if (options.transportType === "sse") {
28
+ return new SseTransport(options.url, options.headers);
29
+ }
30
+ return new WsTransport(options.url);
31
+ }
@@ -1,30 +1,19 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- export type ConnectionEvent = "open" | "close" | "message" | "error";
3
2
 
4
- export interface IConnectionTransportMap {
5
- open: WebSocketEventMap["open"];
6
- message: WebSocketEventMap["message"];
7
- close: WebSocketEventMap["close"];
8
- error: WebSocketEventMap["error"];
9
- }
10
-
11
- export type ConnectionTransportCallback<T extends ConnectionEvent> = (
12
- data: IConnectionTransportMap[T],
13
- ) => void;
3
+ import type {
4
+ NotebookTransport,
5
+ ConnectionEvent,
6
+ ConnectionTransportCallback,
7
+ IConnectionTransportMap,
8
+ } from "../../host/connection";
9
+ export type {
10
+ ConnectionEvent,
11
+ ConnectionTransportCallback,
12
+ IConnectionTransportMap,
13
+ } from "../../host/connection";
14
14
 
15
- export interface IConnectionTransport {
16
- reconnect(code?: number | undefined, reason?: string | undefined): void;
17
- close(): void;
15
+ export interface IConnectionTransport extends NotebookTransport {
18
16
  send(data: string | ArrayBuffer | Blob | ArrayBufferView<ArrayBuffer>): void;
19
- addEventListener<T extends ConnectionEvent>(
20
- event: T,
21
- callback: ConnectionTransportCallback<T>,
22
- ): void;
23
- removeEventListener<T extends ConnectionEvent>(
24
- event: T,
25
- callback: ConnectionTransportCallback<T>,
26
- ): void;
27
- readyState: WebSocket["readyState"];
28
17
  }
29
18
 
30
19
  export class ConnectionSubscriptions {
@@ -1,47 +1,17 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import type { ConnectionStatus as HostConnectionStatus } from "../host/connection";
3
4
  import type { NotificationMessageData } from "../kernel/messages";
4
5
 
6
+ export {
7
+ ConnectionState,
8
+ ConnectionClosedReason as WebSocketClosedReason,
9
+ } from "../host/connection";
10
+
5
11
  export type ConnectionPhase =
6
12
  | NotificationMessageData<"startup-progress">["phase"]
7
13
  | "reconnecting";
8
14
 
9
- export const ConnectionState = {
10
- NOT_STARTED: "NOT_STARTED",
11
- CONNECTING: "CONNECTING",
12
- OPEN: "OPEN",
13
- CLOSING: "CLOSING",
14
- CLOSED: "CLOSED",
15
- } as const;
16
-
17
- export type ConnectionState =
18
- (typeof ConnectionState)[keyof typeof ConnectionState];
19
-
20
- export const WebSocketClosedReason = {
21
- KERNEL_DISCONNECTED: "KERNEL_DISCONNECTED",
22
- KERNEL_STARTUP_ERROR: "KERNEL_STARTUP_ERROR",
23
- } as const;
24
-
25
- export type WebSocketClosedReason =
26
- (typeof WebSocketClosedReason)[keyof typeof WebSocketClosedReason];
27
-
28
- export type ConnectionStatus =
29
- | {
30
- state: typeof ConnectionState.CLOSED;
31
- code: WebSocketClosedReason;
32
- /**
33
- * Human-readable reason for closing the connection.
34
- */
35
- reason: string;
36
- phase?: ConnectionPhase;
37
- }
38
- | {
39
- state: typeof ConnectionState.CONNECTING;
40
- phase?: ConnectionPhase;
41
- }
42
- | {
43
- state:
44
- | typeof ConnectionState.OPEN
45
- | typeof ConnectionState.CLOSING
46
- | typeof ConnectionState.NOT_STARTED;
47
- };
15
+ export type ConnectionStatus = HostConnectionStatus & {
16
+ phase?: ConnectionPhase;
17
+ };
@@ -0,0 +1,88 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useStore } from "jotai";
4
+ import { useEffect, useMemo } from "react";
5
+ import useEvent from "react-use-event-hook";
6
+ import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
7
+ import type {
8
+ NotificationMessage,
9
+ NotificationPayload,
10
+ } from "../kernel/messages";
11
+ import {
12
+ createNotebookClient,
13
+ type NotebookClient,
14
+ type NotebookConnection,
15
+ } from "../host/notebook-client";
16
+ import { classifyCloseEvent } from "./connection-state";
17
+ import { notebookClientAtom } from "./state";
18
+ import {
19
+ createConnectionTransport,
20
+ type ConnectionTransportOptions,
21
+ } from "./transports/create";
22
+
23
+ type Options = ConnectionTransportOptions & {
24
+ sessionId: string;
25
+ messageFormat: NotebookConnection["messageFormat"];
26
+ waitUntilReady: () => Promise<void>;
27
+ checkHealth: () => Promise<boolean>;
28
+ onNotification: (notification: NotificationPayload) => void;
29
+ onMessageError: NotebookConnection["onMessageError"];
30
+ };
31
+
32
+ export function useNotebookClient(options: Options): NotebookClient {
33
+ const store = useStore();
34
+ const { sessionId, static: isStatic, transportType, messageFormat } = options;
35
+ const controller = useMemo(() => createNotebookClient(store), [store]);
36
+ const url = useEvent(options.url);
37
+ const headers = useEvent(options.headers);
38
+ const waitUntilReady = useEvent(options.waitUntilReady);
39
+ const checkHealth = useEvent(options.checkHealth);
40
+ const onNotification = useEvent(options.onNotification);
41
+ const onMessageError = useEvent(options.onMessageError);
42
+
43
+ useEffect(() => {
44
+ const lifetime = new AbortController();
45
+ store.set(notebookClientAtom, controller.client);
46
+ controller.connect({
47
+ createTransport: (url) =>
48
+ createConnectionTransport({
49
+ url,
50
+ headers,
51
+ static: isStatic,
52
+ transportType,
53
+ }),
54
+ url,
55
+ messageFormat,
56
+ signal: lifetime.signal,
57
+ waitUntilReady,
58
+ checkHealth,
59
+ onMessageError,
60
+ // The host validates the envelope, not the native notification contents.
61
+ onNotification: (notification) =>
62
+ onNotification({ data: notification as NotificationMessage }),
63
+ decodeJson: jsonParseWithSpecialChar,
64
+ classifyClose: classifyCloseEvent,
65
+ });
66
+ return () => {
67
+ lifetime.abort();
68
+ if (store.get(notebookClientAtom) === controller.client) {
69
+ store.set(notebookClientAtom, null);
70
+ }
71
+ };
72
+ }, [
73
+ controller,
74
+ store,
75
+ sessionId,
76
+ isStatic,
77
+ transportType,
78
+ messageFormat,
79
+ url,
80
+ headers,
81
+ waitUntilReady,
82
+ checkHealth,
83
+ onNotification,
84
+ onMessageError,
85
+ ]);
86
+
87
+ return controller.client;
88
+ }