@marimo-team/frontend 0.19.3-dev8 → 0.19.4-dev0

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 (179) hide show
  1. package/dist/assets/{CellStatus-BwPGnX3z.js → CellStatus--kUu6N2K.js} +1 -1
  2. package/dist/assets/{ConnectedDataExplorerComponent-KlUs_Sz3.js → ConnectedDataExplorerComponent-BKJwCHu7.js} +1 -1
  3. package/dist/assets/{ErrorBoundary-Drf1manw.js → ErrorBoundary-C7JBxSzd.js} +1 -1
  4. package/dist/assets/{ImperativeModal-q6QlC2aZ.js → ImperativeModal-DVhvP4lH.js} +1 -1
  5. package/dist/assets/{JsonOutput--AuyEErr.js → JsonOutput-BSGE-MRo.js} +5 -5
  6. package/dist/assets/{LazyAnyLanguageCodeMirror-jpEDlD0M.js → LazyAnyLanguageCodeMirror-Cp2punaU.js} +2 -2
  7. package/dist/assets/{MarimoErrorOutput-BZjY8e2w.js → MarimoErrorOutput-CX0SCJOZ.js} +2 -2
  8. package/dist/assets/{RenderHTML-BTLaM20B.js → RenderHTML-Do_PVqRy.js} +1 -1
  9. package/dist/assets/VisuallyHidden-B9t3FhTP.js +1 -0
  10. package/dist/assets/{add-cell-with-ai-BWWVs9qV.js → add-cell-with-ai-manh7kBT.js} +21 -21
  11. package/dist/assets/{add-database-form-Bw_YRH1r.js → add-database-form-CgkV0MRs.js} +2 -2
  12. package/dist/assets/agent-panel-D-OmT-rw.js +287 -0
  13. package/dist/assets/{ai-model-dropdown-BrUOgnWS.js → ai-model-dropdown-DzyBY5VA.js} +1 -1
  14. package/dist/assets/{alert-dialog-k5KxevGr.js → alert-dialog-jcHA5geR.js} +1 -1
  15. package/dist/assets/{any-language-editor-DQu1Tt2N.js → any-language-editor-Cm83E7D_.js} +1 -1
  16. package/dist/assets/{app-config-button-B8CXELx0.js → app-config-button-DC3alCuB.js} +1 -1
  17. package/dist/assets/button-B8cGZzP5.js +1 -0
  18. package/dist/assets/{cache-panel-C1So4Zu3.js → cache-panel-1FqnpB9y.js} +1 -1
  19. package/dist/assets/cell-editor-RHFZmO74.js +23 -0
  20. package/dist/assets/cell-link-Dqj_nfXA.js +1 -0
  21. package/dist/assets/{cells-DU3EySUd.js → cells-BNQUQiDS.js} +49 -49
  22. package/dist/assets/{chat-components-Bc9j9ls4.js → chat-components-CWiXtKu6.js} +1 -1
  23. package/dist/assets/{chat-display-BrTi6c8V.js → chat-display-CGnOamQG.js} +1 -1
  24. package/dist/assets/{chat-panel-8Dym5Gv3.js → chat-panel-Dh1M55c9.js} +2 -2
  25. package/dist/assets/client-CDjmJmVw.js +4 -0
  26. package/dist/assets/{column-preview-Ck6B_-sQ.js → column-preview-CKxT2s-S.js} +1 -1
  27. package/dist/assets/{command-B_minI8b.js → command-YPFTinLj.js} +1 -1
  28. package/dist/assets/{command-palette-BT3u6JBB.js → command-palette-7fVEhKGc.js} +1 -1
  29. package/dist/assets/common-DJkPpBxC.js +1 -0
  30. package/dist/assets/config-D6nhy4FA.js +1 -0
  31. package/dist/assets/context-DHfVoQfl.js +1 -0
  32. package/dist/assets/{copy-icon-B69c-352.js → copy-icon-jWsqdLn1.js} +1 -1
  33. package/dist/assets/{datasource-DCvPlnaJ.js → datasource-DerBLc6V.js} +2 -2
  34. package/dist/assets/{dependency-graph-panel-C9jYZ6pA.js → dependency-graph-panel-Vd-OsVLa.js} +4 -4
  35. package/dist/assets/{dialog-DUEuLcT2.js → dialog-CF5DtF1E.js} +1 -1
  36. package/dist/assets/{dist-DOFFh6Ii.js → dist-Dg7UO_Vw.js} +1 -1
  37. package/dist/assets/{documentation-panel-AsatrTfg.js → documentation-panel-xG2-zpwg.js} +1 -1
  38. package/dist/assets/{download-PR1bF3g_.js → download-B6EJS7Ar.js} +1 -1
  39. package/dist/assets/edit-page-7Hkti2j_.js +12 -0
  40. package/dist/assets/{error-banner-DU5Qb8a8.js → error-banner-DvT0IGDZ.js} +1 -1
  41. package/dist/assets/{error-panel-D_wVKV6I.js → error-panel-BxBpZYvt.js} +1 -1
  42. package/dist/assets/{es-CEE_7T0w.js → es-BoHEdemq.js} +1 -1
  43. package/dist/assets/{field-DDKGFzpC.js → field-Clr_fqUr.js} +1 -1
  44. package/dist/assets/{file-explorer-panel-DltK8JVp.js → file-explorer-panel-C9K0vIPl.js} +1 -1
  45. package/dist/assets/{floating-outline-BfdazXWm.js → floating-outline-DCrTuu2G.js} +1 -1
  46. package/dist/assets/{focus-CtlWIiQr.js → focus-DM53w5BH.js} +1 -1
  47. package/dist/assets/{form-Cy5TkLzh.js → form-BcKfhfZc.js} +2 -2
  48. package/dist/assets/{glide-data-editor-D_bRnWfy.js → glide-data-editor-CRb9AiCG.js} +1 -1
  49. package/dist/assets/{globals-BSLm1nlz.js → globals-Bf30kOQF.js} +1 -1
  50. package/dist/assets/{home-page-CEnaUutq.js → home-page-BRyNf7fl.js} +2 -2
  51. package/dist/assets/index-CBMqMxiq.js +43 -0
  52. package/dist/assets/index-DDc_1b-N.css +2 -0
  53. package/dist/assets/input-B80Yt1uu.js +1 -0
  54. package/dist/assets/{kiosk-mode-BnTZR6mM.js → kiosk-mode-P-NYHJID.js} +1 -1
  55. package/dist/assets/{label-qwandMoh.js → label-CNZLffHW.js} +1 -1
  56. package/dist/assets/{layout-BTiWDrbh.js → layout-DT91GUei.js} +4 -4
  57. package/dist/assets/links-D529u6GQ.js +1 -0
  58. package/dist/assets/{logs-panel-Cnp9tO_1.js → logs-panel-C2dfrRig.js} +1 -1
  59. package/dist/assets/{markdown-renderer-BSrbBHwX.js → markdown-renderer-BPnVa0ym.js} +2 -2
  60. package/dist/assets/{mermaid-BPkO79lo.js → mermaid--ZwxKP7u.js} +1 -1
  61. package/dist/assets/mode-Dq8MKjNR.js +1 -0
  62. package/dist/assets/{multi-map-fjX9ImVF.js → multi-map-CQd4MZr5.js} +1 -1
  63. package/dist/assets/name-cell-input-BaEPC7ON.js +1 -0
  64. package/dist/assets/{outline-panel-DCfj1bI-.js → outline-panel-Cca864H0.js} +1 -1
  65. package/dist/assets/{packages-panel-BiEckVdM.js → packages-panel-Cy_KAYmq.js} +1 -1
  66. package/dist/assets/{panels-BXRys72u.js → panels-BzlLZfye.js} +1 -1
  67. package/dist/assets/{process-output-wGlHkL-Q.js → process-output-Dn1rOp26.js} +1 -1
  68. package/dist/assets/{readonly-python-code-xbh7G2Y2.js → readonly-python-code-CXeF74Iq.js} +1 -1
  69. package/dist/assets/{renderShortcut-D0Pei-OA.js → renderShortcut-eU5Hsfml.js} +1 -1
  70. package/dist/assets/{run-page-BCwJRhCq.js → run-page-CM_n6pXD.js} +1 -1
  71. package/dist/assets/scratchpad-panel-XCkVY3Hp.js +1 -0
  72. package/dist/assets/{secrets-panel-CDWmmmBS.js → secrets-panel-BMY6PPth.js} +1 -1
  73. package/dist/assets/{select-D0g5GnIs.js → select-D9lTzMzP.js} +1 -1
  74. package/dist/assets/{session-panel-DuQl_oQp.js → session-panel-BDt6Y_mU.js} +1 -1
  75. package/dist/assets/{slides-component-MkPkpql1.js → slides-component-Dp0Yv5b0.js} +1 -1
  76. package/dist/assets/{snippets-panel-R_ql6HGu.js → snippets-panel-K-JKJQBf.js} +1 -1
  77. package/dist/assets/state-DWRZTH2y.js +1 -0
  78. package/dist/assets/state-JzO-Ni5T.js +1 -0
  79. package/dist/assets/{switch-CWzL-0WF.js → switch-RowEjq0T.js} +1 -1
  80. package/dist/assets/{terminal-BWM0fOMh.js → terminal-BhbNfCNw.js} +1 -1
  81. package/dist/assets/{textarea-CfvBt_Xm.js → textarea-Di1KKcL4.js} +1 -1
  82. package/dist/assets/{tracing-Kscqc1t3.js → tracing-nvbrZdpf.js} +1 -1
  83. package/dist/assets/{tracing-panel-BEzOflWc.js → tracing-panel-CTXJaO-A.js} +2 -2
  84. package/dist/assets/{types-DhuSHMNQ.js → types-CT2U5Ljy.js} +1 -1
  85. package/dist/assets/{useAddCell-C9lbOVO1.js → useAddCell-COb93CUl.js} +1 -1
  86. package/dist/assets/{useBoolean-B-A0dyIW.js → useBoolean-B_S7yTZz.js} +1 -1
  87. package/dist/assets/{useCellActionButton-fsh9MTAX.js → useCellActionButton-D5Zt1dDz.js} +1 -1
  88. package/dist/assets/{useDateFormatter-CV0QXb5P.js → useDateFormatter-DsANziQR.js} +1 -1
  89. package/dist/assets/useDeleteCell-DHF_xvAh.js +1 -0
  90. package/dist/assets/{useDependencyPanelTab-CngFbla0.js → useDependencyPanelTab-D59iW_MD.js} +1 -1
  91. package/dist/assets/useInterval-BGPIviJp.js +1 -0
  92. package/dist/assets/{useNotebookActions-D01w160c.js → useNotebookActions-DEl-rH-3.js} +1 -1
  93. package/dist/assets/{useNumberFormatter-D8ks3oPN.js → useNumberFormatter-FoXhpyAb.js} +1 -1
  94. package/dist/assets/usePress-DTwIUo40.js +7 -0
  95. package/dist/assets/useRunCells-CKEmgeKM.js +1 -0
  96. package/dist/assets/useSplitCell-D9YiO-z5.js +1 -0
  97. package/dist/assets/{useTheme-DfP1CWaW.js → useTheme-CNj0G_ol.js} +1 -1
  98. package/dist/assets/utilities.esm-DG4qccZc.js +3 -0
  99. package/dist/assets/utils-pfqq9IdB.js +1 -0
  100. package/dist/assets/{vega-component-B8ghmMYW.js → vega-component-C1voDf5W.js} +1 -1
  101. package/dist/assets/{write-secret-modal-CLm48gMe.js → write-secret-modal-hOetwavI.js} +1 -1
  102. package/dist/index.html +56 -56
  103. package/package.json +5 -5
  104. package/src/__mocks__/requests.ts +1 -0
  105. package/src/__tests__/mount.test.ts +128 -0
  106. package/src/components/app-config/__tests__/get-dirty-values.test.ts +1 -1
  107. package/src/components/app-config/ai-config.tsx +328 -28
  108. package/src/components/app-config/user-config-form.tsx +10 -3
  109. package/src/components/chat/acp/agent-panel.tsx +56 -43
  110. package/src/components/chat/chat-utils.ts +0 -19
  111. package/src/components/data-table/column-header.tsx +1 -1
  112. package/src/components/editor/KernelStartupErrorModal.tsx +2 -2
  113. package/src/components/editor/actions/name-cell-input.tsx +10 -4
  114. package/src/components/editor/ai/completion-handlers.tsx +1 -1
  115. package/src/components/editor/alerts/connecting-alert.tsx +33 -6
  116. package/src/components/editor/chrome/types.ts +2 -4
  117. package/src/components/editor/chrome/wrapper/app-chrome.tsx +55 -58
  118. package/src/components/editor/chrome/wrapper/footer-items/runtime-settings.tsx +150 -96
  119. package/src/components/editor/renderers/vertical-layout/__tests__/useFocusFirstEditor.test.ts +27 -0
  120. package/src/components/editor/renderers/vertical-layout/useFocusFirstEditor.ts +6 -0
  121. package/src/components/utils/lazy-mount.tsx +29 -8
  122. package/src/core/ai/ids/ids.ts +12 -4
  123. package/src/core/cells/cells.ts +2 -0
  124. package/src/core/cells/scrollCellIntoView.ts +3 -2
  125. package/src/core/codemirror/cm.ts +2 -0
  126. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +123 -0
  127. package/src/core/codemirror/lsp/notebook-lsp.ts +44 -4
  128. package/src/core/codemirror/misc/__tests__/string-braces.test.ts +200 -0
  129. package/src/core/codemirror/misc/string-braces.ts +37 -0
  130. package/src/core/config/__tests__/config-schema.test.ts +36 -0
  131. package/src/core/config/config-schema.ts +1 -0
  132. package/src/core/export/__tests__/hooks.test.ts +504 -0
  133. package/src/core/export/hooks.ts +93 -4
  134. package/src/core/islands/bridge.ts +1 -0
  135. package/src/core/kernel/__tests__/handlers.test.ts +2 -2
  136. package/src/core/kernel/state.ts +1 -0
  137. package/src/core/network/__tests__/requests-lazy.test.ts +1 -1
  138. package/src/core/network/__tests__/requests-network.test.ts +0 -18
  139. package/src/core/network/requests-lazy.ts +3 -2
  140. package/src/core/network/requests-network.ts +10 -7
  141. package/src/core/network/requests-static.ts +1 -0
  142. package/src/core/network/requests-toasting.tsx +1 -0
  143. package/src/core/network/types.ts +2 -0
  144. package/src/core/wasm/bridge.ts +1 -0
  145. package/src/css/globals.css +2 -0
  146. package/src/hooks/__tests__/useInterval.test.tsx +104 -0
  147. package/src/hooks/useInterval.ts +32 -6
  148. package/src/mount.tsx +6 -0
  149. package/src/plugins/impl/chat/ChatPlugin.tsx +2 -4
  150. package/src/plugins/impl/chat/chat-ui.tsx +62 -191
  151. package/src/plugins/impl/chat/types.ts +5 -12
  152. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +3 -1
  153. package/src/utils/events.ts +1 -0
  154. package/dist/assets/VisuallyHidden-BodIky8L.js +0 -1
  155. package/dist/assets/agent-panel-Bm-vW8YL.js +0 -287
  156. package/dist/assets/button-DuYGqRtX.js +0 -1
  157. package/dist/assets/cell-editor-Cdtc1m3g.js +0 -23
  158. package/dist/assets/cell-link-CFAPzUg5.js +0 -1
  159. package/dist/assets/client-DfkWorYM.js +0 -4
  160. package/dist/assets/common-jorbwXZC.js +0 -1
  161. package/dist/assets/config-Ba3eeYri.js +0 -1
  162. package/dist/assets/context-BAYdLMF_.js +0 -1
  163. package/dist/assets/edit-page-B1Ed6RKp.js +0 -12
  164. package/dist/assets/index-DNg7_e7t.js +0 -43
  165. package/dist/assets/index-__6MNWbe.css +0 -2
  166. package/dist/assets/input-CaEtLL8p.js +0 -1
  167. package/dist/assets/links-Bpd4gqTj.js +0 -1
  168. package/dist/assets/mode-yhfN-4ye.js +0 -1
  169. package/dist/assets/name-cell-input-CmuWqgFR.js +0 -1
  170. package/dist/assets/scratchpad-panel-C6PpCYtK.js +0 -1
  171. package/dist/assets/state-DEHWsmkM.js +0 -1
  172. package/dist/assets/state-DXAf-ejz.js +0 -1
  173. package/dist/assets/useDeleteCell-ByImoTpm.js +0 -1
  174. package/dist/assets/useInterval-DpipYmgs.js +0 -1
  175. package/dist/assets/usePress-C2LPFxyv.js +0 -7
  176. package/dist/assets/useRunCells-CmnSPQtM.js +0 -1
  177. package/dist/assets/useSplitCell-BTH64tve.js +0 -1
  178. package/dist/assets/utilities.esm-CMQs6YPp.js +0 -3
  179. package/dist/assets/utils-CJJIceVn.js +0 -1
@@ -1,4 +1,4 @@
1
- /* Copyright 2024 Marimo. All rights reserved. */
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { NoKernelConnectedError } from "@/utils/errors";
4
4
  import { Logger } from "@/utils/Logger";
@@ -67,6 +67,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
67
67
  autoExportAsHTML: "waitForConnectionOpen",
68
68
  autoExportAsMarkdown: "waitForConnectionOpen",
69
69
  autoExportAsIPYNB: "waitForConnectionOpen",
70
+ updateCellOutputs: "waitForConnectionOpen",
70
71
 
71
72
  // Sidebar operations that wait for connection
72
73
  listSecretKeys: "throwError",
@@ -152,7 +153,7 @@ export function createLazyRequests(
152
153
  `Dropping request: ${key}, since not connected to a kernel.`,
153
154
  );
154
155
  // Silently drop the request
155
- return Promise.resolve();
156
+ return;
156
157
 
157
158
  case "throwError":
158
159
  throw new NoKernelConnectedError();
@@ -2,9 +2,8 @@
2
2
 
3
3
  import { once } from "lodash-es";
4
4
  import { getRuntimeManager } from "../runtime/config";
5
- import { store } from "../state/jotai";
6
5
  import { API, createClientWithRuntimeManager } from "./api";
7
- import { isConnectedAtom, waitForConnectionOpen } from "./connection";
6
+ import { waitForConnectionOpen } from "./connection";
8
7
  import type { EditRequests, RunRequests } from "./types";
9
8
 
10
9
  const { handleResponse, handleResponseReturnNull } = API;
@@ -105,11 +104,7 @@ export function createNetworkRequests(): EditRequests & RunRequests {
105
104
  .then(handleResponseReturnNull);
106
105
  },
107
106
  sendRun: async (request) => {
108
- // Rather than waiting, we just drop all sendRun requests if the connection is not open.
109
- // Otherwise we can get into a weird state of sending requests for cells that no longer exist.
110
- if (!store.get(isConnectedAtom)) {
111
- return null;
112
- }
107
+ await waitForConnectionOpen();
113
108
  return getClient()
114
109
  .POST("/api/kernel/run", {
115
110
  body: request,
@@ -410,6 +405,14 @@ export function createNetworkRequests(): EditRequests & RunRequests {
410
405
  })
411
406
  .then(handleResponseReturnNull);
412
407
  },
408
+ updateCellOutputs: async (request) => {
409
+ return getClient()
410
+ .POST("/api/export/update_cell_outputs", {
411
+ body: request,
412
+ params: getParams(),
413
+ })
414
+ .then(handleResponseReturnNull);
415
+ },
413
416
  addPackage: (request) => {
414
417
  return getClient()
415
418
  .POST("/api/packages/add", {
@@ -79,6 +79,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
79
79
  autoExportAsHTML: throwNotInEditMode,
80
80
  autoExportAsMarkdown: throwNotInEditMode,
81
81
  autoExportAsIPYNB: throwNotInEditMode,
82
+ updateCellOutputs: throwNotInEditMode,
82
83
  addPackage: throwNotInEditMode,
83
84
  removePackage: throwNotInEditMode,
84
85
  getPackageList: throwNotInEditMode,
@@ -64,6 +64,7 @@ export function createErrorToastingRequests(
64
64
  autoExportAsHTML: "", // No toast
65
65
  autoExportAsMarkdown: "", // No toast
66
66
  autoExportAsIPYNB: "", // No toast
67
+ updateCellOutputs: "", // No toast
67
68
  addPackage: "Failed to add package",
68
69
  removePackage: "Failed to remove package",
69
70
  getPackageList: "Failed to get package list",
@@ -21,6 +21,7 @@ export type ExportAsHTMLRequest = schemas["ExportAsHTMLRequest"];
21
21
  export type ExportAsMarkdownRequest = schemas["ExportAsMarkdownRequest"];
22
22
  export type ExportAsIPYNBRequest = schemas["ExportAsIPYNBRequest"];
23
23
  export type ExportAsScriptRequest = schemas["ExportAsScriptRequest"];
24
+ export type UpdateCellOutputsRequest = schemas["UpdateCellOutputsRequest"];
24
25
  export type FileCreateRequest = schemas["FileCreateRequest"];
25
26
  export type FileCreateResponse = schemas["FileCreateResponse"];
26
27
  export type FileDeleteRequest = schemas["FileDeleteRequest"];
@@ -175,6 +176,7 @@ export interface EditRequests {
175
176
  autoExportAsHTML: (request: ExportAsHTMLRequest) => Promise<null>;
176
177
  autoExportAsMarkdown: (request: ExportAsMarkdownRequest) => Promise<null>;
177
178
  autoExportAsIPYNB: (request: ExportAsIPYNBRequest) => Promise<null>;
179
+ updateCellOutputs: (request: UpdateCellOutputsRequest) => Promise<null>;
178
180
  // Package requests
179
181
  getPackageList: () => Promise<ListPackagesResponse>;
180
182
  getDependencyTree: () => Promise<DependencyTreeResponse>;
@@ -589,6 +589,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
589
589
  autoExportAsHTML = throwNotImplemented;
590
590
  autoExportAsMarkdown = throwNotImplemented;
591
591
  autoExportAsIPYNB = throwNotImplemented;
592
+ updateCellOutputs = throwNotImplemented;
592
593
  writeSecret = throwNotImplemented;
593
594
  invokeAiTool = throwNotImplemented;
594
595
  clearCache = throwNotImplemented;
@@ -184,8 +184,10 @@
184
184
  --shadow-2xl-solid: 10px 12px 0px 0px var(--base-shadow-darker), 0 0px 8px 0px hsl(0deg 0% 90% / 50%);
185
185
 
186
186
  /* Solid shadows with lighter shade color */
187
+
187
188
  /* biome-ignore format: definition needs to be oneline or breaks variants */
188
189
  --shadow-sm-solid-shade: 2px 2px 0px 0px var(--base-shadow), 0px 0px 2px 0px hsl(0deg 0% 50% / 20%);
190
+
189
191
  /* biome-ignore format: definition needs to be oneline or breaks variants */
190
192
  --shadow-md-solid-shade: 4px 4px 0px 0px var(--base-shadow), 0 0px 2px 0px hsl(0deg 0% 60% / 50%);
191
193
  }
@@ -67,4 +67,108 @@ describe("useInterval", () => {
67
67
  vi.advanceTimersByTime(1000);
68
68
  expect(callback).not.toHaveBeenCalled();
69
69
  });
70
+
71
+ describe("skipIfRunning", () => {
72
+ it("should allow overlapping async calls by default", async () => {
73
+ let concurrentCalls = 0;
74
+ let maxConcurrentCalls = 0;
75
+
76
+ const callback = vi.fn(async () => {
77
+ concurrentCalls++;
78
+ maxConcurrentCalls = Math.max(maxConcurrentCalls, concurrentCalls);
79
+ // Simulate slow async work
80
+ await new Promise((resolve) => setTimeout(resolve, 2000));
81
+ concurrentCalls--;
82
+ });
83
+
84
+ renderHook(() =>
85
+ useInterval(callback, { delayMs: 500, whenVisible: false }),
86
+ );
87
+
88
+ // First call at 500ms
89
+ vi.advanceTimersByTime(500);
90
+ expect(callback).toHaveBeenCalledTimes(1);
91
+
92
+ // Second call at 1000ms (while first is still running)
93
+ vi.advanceTimersByTime(500);
94
+ expect(callback).toHaveBeenCalledTimes(2);
95
+
96
+ // Third call at 1500ms
97
+ vi.advanceTimersByTime(500);
98
+ expect(callback).toHaveBeenCalledTimes(3);
99
+
100
+ // Multiple concurrent calls should have occurred
101
+ expect(maxConcurrentCalls).toBeGreaterThan(1);
102
+ });
103
+
104
+ it("should skip calls when skipIfRunning is true", async () => {
105
+ let concurrentCalls = 0;
106
+ let maxConcurrentCalls = 0;
107
+
108
+ const callback = vi.fn(async () => {
109
+ concurrentCalls++;
110
+ maxConcurrentCalls = Math.max(maxConcurrentCalls, concurrentCalls);
111
+ // Simulate slow async work (3 seconds)
112
+ await new Promise<void>((resolve) => setTimeout(resolve, 3000));
113
+ concurrentCalls--;
114
+ });
115
+
116
+ renderHook(() =>
117
+ useInterval(callback, {
118
+ delayMs: 500,
119
+ whenVisible: false,
120
+ skipIfRunning: true,
121
+ }),
122
+ );
123
+
124
+ // First call at 500ms
125
+ await vi.advanceTimersByTimeAsync(500);
126
+ expect(callback).toHaveBeenCalledTimes(1);
127
+
128
+ // Second interval tick at 1000ms - should be skipped since first is still running
129
+ await vi.advanceTimersByTimeAsync(500);
130
+ expect(callback).toHaveBeenCalledTimes(1); // Still 1, not 2
131
+
132
+ // Third interval tick at 1500ms - should still be skipped
133
+ await vi.advanceTimersByTimeAsync(500);
134
+ expect(callback).toHaveBeenCalledTimes(1); // Still 1
135
+
136
+ // Only one concurrent call should have occurred
137
+ expect(maxConcurrentCalls).toBe(1);
138
+
139
+ // Advance past the 3 second timeout to complete first callback
140
+ await vi.advanceTimersByTimeAsync(2000);
141
+
142
+ // Next interval tick should now be able to run
143
+ await vi.advanceTimersByTimeAsync(500);
144
+ expect(callback).toHaveBeenCalledTimes(2);
145
+ });
146
+
147
+ it("should allow next call after previous async call completes with skipIfRunning true", async () => {
148
+ const callback = vi.fn(async () => {
149
+ // Quick async operation
150
+ await Promise.resolve();
151
+ });
152
+
153
+ renderHook(() =>
154
+ useInterval(callback, {
155
+ delayMs: 1000,
156
+ whenVisible: false,
157
+ skipIfRunning: true,
158
+ }),
159
+ );
160
+
161
+ // First call
162
+ await vi.advanceTimersByTimeAsync(1000);
163
+ expect(callback).toHaveBeenCalledTimes(1);
164
+
165
+ // Second call - should proceed since first completed
166
+ await vi.advanceTimersByTimeAsync(1000);
167
+ expect(callback).toHaveBeenCalledTimes(2);
168
+
169
+ // Third call
170
+ await vi.advanceTimersByTimeAsync(1000);
171
+ expect(callback).toHaveBeenCalledTimes(3);
172
+ });
173
+ });
70
174
  });
@@ -1,9 +1,16 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { useEffect, useRef } from "react";
2
+ import { useCallback, useEffect, useRef } from "react";
3
3
  import { useEventListener } from "./useEventListener";
4
4
 
5
5
  /**
6
6
  * Creates an interval that runs a callback every `delayMs` milliseconds.
7
+ *
8
+ * @param callback - The callback to run.
9
+ * @param opts - The options for the interval.
10
+ * @param opts.delayMs - The delay in milliseconds between runs.
11
+ * @param opts.whenVisible - Whether to run the callback when the document is visible.
12
+ * @param opts.disabled - Whether to disable the interval.
13
+ * @param opts.skipIfRunning - Whether to skip the callback if it is already running.
7
14
  */
8
15
  export function useInterval(
9
16
  callback: () => void,
@@ -11,16 +18,35 @@ export function useInterval(
11
18
  delayMs: number | null;
12
19
  whenVisible: boolean;
13
20
  disabled?: boolean;
21
+ skipIfRunning?: boolean;
14
22
  },
15
23
  ) {
16
- const { delayMs, whenVisible, disabled = false } = opts;
17
- const savedCallback = useRef<() => void>(undefined);
24
+ const {
25
+ delayMs,
26
+ whenVisible,
27
+ disabled = false,
28
+ skipIfRunning = false,
29
+ } = opts;
30
+ const savedCallback = useRef<() => void | Promise<void>>(undefined);
31
+ const isRunning = useRef(false);
18
32
 
19
33
  // Store the callback
20
34
  useEffect(() => {
21
35
  savedCallback.current = callback;
22
36
  }, [callback]);
23
37
 
38
+ const runCallback = useCallback(async () => {
39
+ if (isRunning.current && skipIfRunning) {
40
+ return;
41
+ }
42
+ isRunning.current = true;
43
+ try {
44
+ await savedCallback.current?.();
45
+ } finally {
46
+ isRunning.current = false;
47
+ }
48
+ }, [skipIfRunning]);
49
+
24
50
  // Run the interval
25
51
  useEffect(() => {
26
52
  if (delayMs === null || disabled) {
@@ -32,16 +58,16 @@ export function useInterval(
32
58
  return;
33
59
  }
34
60
 
35
- savedCallback.current?.();
61
+ runCallback();
36
62
  }, delayMs);
37
63
 
38
64
  return () => clearInterval(id);
39
- }, [delayMs, whenVisible, disabled]);
65
+ }, [delayMs, whenVisible, disabled, runCallback]);
40
66
 
41
67
  // When the document becomes visible, run the callback
42
68
  useEventListener(document, "visibilitychange", () => {
43
69
  if (document.visibilityState === "visible" && whenVisible && !disabled) {
44
- savedCallback.current?.();
70
+ runCallback();
45
71
  }
46
72
  });
47
73
 
package/src/mount.tsx CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  import { MarimoApp, preloadPage } from "./core/MarimoApp";
30
30
  import { type AppMode, initialModeAtom, viewStateAtom } from "./core/mode";
31
31
  import { cleanupAuthQueryParams } from "./core/network/auth";
32
+ import { connectionAtom } from "./core/network/connection";
32
33
  import { requestClientAtom } from "./core/network/requests";
33
34
  import { resolveRequestClient } from "./core/network/resolve";
34
35
  import {
@@ -42,6 +43,7 @@ import { isStaticNotebook } from "./core/static/static-state";
42
43
  import { maybeRegisterVSCodeBindings } from "./core/vscode/vscode-bindings";
43
44
  import type { FileStore } from "./core/wasm/store";
44
45
  import { notebookFileStore } from "./core/wasm/store";
46
+ import { WebSocketState } from "./core/websocket/types";
45
47
  import { vegaLoader } from "./plugins/impl/vega/loader";
46
48
  import { initializePlugins } from "./plugins/plugins";
47
49
  import { ThemeProvider } from "./theme/ThemeProvider";
@@ -304,6 +306,10 @@ function initStore(options: unknown) {
304
306
  ...firstRuntimeConfig,
305
307
  serverToken: parsedOptions.data.serverToken,
306
308
  });
309
+ // If the remote runtime is not lazy, start it in CONNECTING
310
+ if (!firstRuntimeConfig.lazy && !isStaticNotebook()) {
311
+ store.set(connectionAtom, { state: WebSocketState.CONNECTING });
312
+ }
307
313
  } else {
308
314
  store.set(runtimeConfigAtom, {
309
315
  ...DEFAULT_RUNTIME_CONFIG,
@@ -15,7 +15,7 @@ export type PluginFunctions = {
15
15
  get_chat_history: (req: {}) => Promise<{ messages: UIMessage[] }>;
16
16
  delete_chat_history: (req: {}) => Promise<null>;
17
17
  delete_chat_message: (req: { index: number }) => Promise<null>;
18
- send_prompt: (req: SendMessageRequest) => Promise<string | null>;
18
+ send_prompt: (req: SendMessageRequest) => Promise<unknown>;
19
19
  };
20
20
 
21
21
  const messageSchema = z.array(
@@ -47,7 +47,6 @@ export const ChatPlugin = createPlugin<{ messages: UIMessage[] }>(
47
47
  maxHeight: z.number().optional(),
48
48
  config: configSchema,
49
49
  allowAttachments: z.union([z.boolean(), z.string().array()]),
50
- frontendManaged: z.boolean(),
51
50
  }),
52
51
  )
53
52
  .withFunctions<PluginFunctions>({
@@ -67,7 +66,7 @@ export const ChatPlugin = createPlugin<{ messages: UIMessage[] }>(
67
66
  config: configSchema,
68
67
  }),
69
68
  )
70
- .output(z.union([z.string(), z.null()])),
69
+ .output(z.unknown()),
71
70
  })
72
71
  .renderer((props) => (
73
72
  <TooltipProvider>
@@ -77,7 +76,6 @@ export const ChatPlugin = createPlugin<{ messages: UIMessage[] }>(
77
76
  showConfigurationControls={props.data.showConfigurationControls}
78
77
  maxHeight={props.data.maxHeight}
79
78
  allowAttachments={props.data.allowAttachments}
80
- frontendManaged={props.data.frontendManaged}
81
79
  config={props.data.config}
82
80
  get_chat_history={props.functions.get_chat_history}
83
81
  delete_chat_history={props.functions.delete_chat_history}