@marimo-team/frontend 0.25.2-dev66 → 0.25.2-dev89

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 (153) hide show
  1. package/dist/assets/{CellStatus-CRFBVGjI.js → CellStatus-Bj7PvY_u.js} +1 -1
  2. package/dist/assets/{ErrorBoundary-BKmN7zye.js → ErrorBoundary-CG6gtgym.js} +2 -2
  3. package/dist/assets/{JsonOutput-nqqzL-n0.js → JsonOutput-NVH7GWyC.js} +1 -1
  4. package/dist/assets/{MarimoErrorOutput-DCu6fm2y.js → MarimoErrorOutput-z5iFryzf.js} +1 -1
  5. package/dist/assets/{RenderHTML-DRMkieKn.js → RenderHTML-C7QcdAgB.js} +1 -1
  6. package/dist/assets/{RunButton-CPZWjCL9.js → RunButton-A__3wcpF.js} +1 -1
  7. package/dist/assets/{add-cell-with-ai-D20NyZO4.js → add-cell-with-ai-Wyt6GBhk.js} +1 -1
  8. package/dist/assets/{add-connection-dialog-pQcdKIlO.js → add-connection-dialog-bl8dqCVs.js} +1 -1
  9. package/dist/assets/{agent-panel-tNErBgmq.js → agent-panel-BcoO8S6Q.js} +1 -1
  10. package/dist/assets/{ai-model-dropdown-BIeRPKed.js → ai-model-dropdown-CvJsRj9y.js} +1 -1
  11. package/dist/assets/{api-DYL9A86_.js → api-DVLtXyp7.js} +1 -1
  12. package/dist/assets/{app-config-button-DkNGV0b1.js → app-config-button-C7QrZbqr.js} +1 -1
  13. package/dist/assets/{blob-BIYte8oT.js → blob-Bx8A10xl.js} +1 -1
  14. package/dist/assets/{cell-editor-C5RTy4Y0.js → cell-editor-CevZDS63.js} +4 -4
  15. package/dist/assets/cell-link-DC1UZQr_.js +1 -0
  16. package/dist/assets/{cells-h5u00Nz8.js → cells-CxY8RSp7.js} +2 -2
  17. package/dist/assets/{chat-display-DzTkUWMw.js → chat-display-YETh6yXV.js} +1 -1
  18. package/dist/assets/{chat-panel-CKqEpnjN.js → chat-panel-DyaePe7B.js} +1 -1
  19. package/dist/assets/{chat-ui-CGzOsYVb.js → chat-ui-D_QL46qM.js} +1 -1
  20. package/dist/assets/code-visibility-DNzJgrTz.js +1 -0
  21. package/dist/assets/{column-preview-DtrgJzVI.js → column-preview-sL_Jo0hX.js} +1 -1
  22. package/dist/assets/{command-palette-BKYGTfS5.js → command-palette-DMQFuWz1.js} +1 -1
  23. package/dist/assets/{common-BMeq0Bqi.js → common-Cy3lJDvJ.js} +1 -1
  24. package/dist/assets/{components-CWcf8098.js → components-D9ZIJGG8.js} +1 -1
  25. package/dist/assets/{config-BAnYSdpO.js → config-D_c_6ce1.js} +1 -1
  26. package/dist/assets/{datasource-BLHGFSsT.js → datasource-Tst92Pzi.js} +1 -1
  27. package/dist/assets/{dependency-graph-panel-C8X3Nn8W.js → dependency-graph-panel-BwMayTyZ.js} +1 -1
  28. package/dist/assets/{documentation-panel-C8wrSKlX.js → documentation-panel-OMUbXgKW.js} +1 -1
  29. package/dist/assets/{download-CUgiCx7s.js → download-CPMAf7re.js} +1 -1
  30. package/dist/assets/{edit-page-CqPNBpAP.js → edit-page-DD1NFkiu.js} +6 -6
  31. package/dist/assets/{error-entries-DQD4A9cx.js → error-entries-DBpwg0GC.js} +1 -1
  32. package/dist/assets/{error-panel-BJYkwYEY.js → error-panel-ByOEJuZN.js} +1 -1
  33. package/dist/assets/{feedback-button-DbTjGwcX.js → feedback-button-7KY7nL-j.js} +2 -2
  34. package/dist/assets/feedback-button-DxjoRfo3.js +1 -0
  35. package/dist/assets/{file-explorer-panel-BQi88S1o.js → file-explorer-panel-B92ugwxr.js} +1 -1
  36. package/dist/assets/{file-name-input-DPJSxW_X.js → file-name-input-DZehOHLA.js} +1 -1
  37. package/dist/assets/{floating-outline-3PlTr0K1.js → floating-outline-K7cVdOlC.js} +1 -1
  38. package/dist/assets/{gallery-page-BqvVmwde.js → gallery-page-DoriMrLH.js} +1 -1
  39. package/dist/assets/{glide-data-editor-CAa14cIr.js → glide-data-editor-DWyeHe3u.js} +1 -1
  40. package/dist/assets/{home-page-Bg-WqjiD.js → home-page-BcJJsD_Q.js} +1 -1
  41. package/dist/assets/{hooks-DLBvBo8c.js → hooks-DNPsQf79.js} +1 -1
  42. package/dist/assets/index-B7ZZVUcj.css +2 -0
  43. package/dist/assets/{index-BtV91sqJ.js → index-CqJKdR1j.js} +5 -5
  44. package/dist/assets/{layout-mWXZofX2.js → layout-BaluekG6.js} +3 -3
  45. package/dist/assets/{logs-panel-IKL6F2OX.js → logs-panel-OkoO5mbN.js} +1 -1
  46. package/dist/assets/{markdown-renderer-DHvAXF8E.js → markdown-renderer-mDlWBE1M.js} +1 -1
  47. package/dist/assets/{name-cell-input-j4eGLLyd.js → name-cell-input-D_Q2IhP9.js} +1 -1
  48. package/dist/assets/{outline-panel-CprDWmxn.js → outline-panel-Dy2cmYDF.js} +1 -1
  49. package/dist/assets/packages-panel-CIvhx0hb.js +1 -0
  50. package/dist/assets/{panel-accordion-state-8xXVhawu.js → panel-accordion-state-BI4SU22L.js} +1 -1
  51. package/dist/assets/panels-CO6Clgel.js +4 -0
  52. package/dist/assets/{process-output-DOmeJ1VR.js → process-output-Crm0vqbi.js} +1 -1
  53. package/dist/assets/{readonly-code-display-xvH6MyT6.js → readonly-code-display-CNbw6v1-.js} +1 -1
  54. package/dist/assets/{readonly-python-code-D4YGmSvB.js → readonly-python-code-DcYhdd1B.js} +1 -1
  55. package/dist/assets/{reveal-component-CDUG1x74.js → reveal-component-DeZTBbnn.js} +1 -1
  56. package/dist/assets/{run-page-BAqLgcq9.js → run-page-BNEg-Ro9.js} +1 -1
  57. package/dist/assets/runtime-details-state-lxXYwJcI.js +1 -0
  58. package/dist/assets/{scratchpad-panel-BFbUuzdn.js → scratchpad-panel-lDJWcpVs.js} +1 -1
  59. package/dist/assets/{session-panel-BdN43Lzi.js → session-panel-BqutyoYd.js} +1 -1
  60. package/dist/assets/{snippets-panel-5_jR08Zd.js → snippets-panel-YttkYAW9.js} +1 -1
  61. package/dist/assets/state-B2ZbImvm.js +1 -0
  62. package/dist/assets/{state-BxJ_FX3k.js → state-B85OOscw.js} +1 -1
  63. package/dist/assets/{state-7os84dnI.js → state-sbCCk7ZP.js} +1 -1
  64. package/dist/assets/{terminal-DHWTOXu3.js → terminal-3hxM6K9q.js} +1 -1
  65. package/dist/assets/{tracing-CRkNpBaH.js → tracing-DZYfCw6p.js} +1 -1
  66. package/dist/assets/{tracing-panel-DV2-2TUI.js → tracing-panel-BUPJ77jB.js} +2 -2
  67. package/dist/assets/{tree-actions-CShqSonp.js → tree-actions-D5clvgKl.js} +1 -1
  68. package/dist/assets/{useAddCell-DesMfnE4.js → useAddCell-Bhv6GT3m.js} +1 -1
  69. package/dist/assets/{useBoolean-BofUVeKg.js → useBoolean-Co-_MyFz.js} +1 -1
  70. package/dist/assets/{useCellActionButton-BO5SFMxA.js → useCellActionButton-DwAUf_Cn.js} +1 -1
  71. package/dist/assets/{useDeleteCell-DSbUjpSC.js → useDeleteCell-BKN7t4DH.js} +1 -1
  72. package/dist/assets/{useDependencyPanelTab-JIFiQrr2.js → useDependencyPanelTab-CbcGEmqM.js} +1 -1
  73. package/dist/assets/{useInstallPackage-CCQcFc6U.js → useInstallPackage-HTUlGg0U.js} +1 -1
  74. package/dist/assets/{useNotebookActions-CjjED3t-.js → useNotebookActions-pXzVjrlk.js} +1 -1
  75. package/dist/assets/{useRunCells-Bx3wXUWO.js → useRunCells-zMO43eNR.js} +1 -1
  76. package/dist/assets/{useSplitCell-BRmQP02b.js → useSplitCell-xEtFLByO.js} +1 -1
  77. package/dist/assets/{vega-component-C28fL5fD.js → vega-component-Chbfi__Q.js} +1 -1
  78. package/dist/index.html +24 -24
  79. package/package.json +1 -1
  80. package/src/__tests__/mount.test.ts +6 -6
  81. package/src/components/editor/actions/useRestartKernel.tsx +2 -2
  82. package/src/components/editor/alerts/__tests__/connection-notice.test.tsx +224 -17
  83. package/src/components/editor/alerts/connection-details.tsx +61 -0
  84. package/src/components/editor/alerts/connection-notice.tsx +46 -53
  85. package/src/components/editor/alerts/startup-progress.tsx +73 -129
  86. package/src/components/editor/app-container.tsx +2 -2
  87. package/src/components/editor/boundary/__tests__/ErrorBoundary.test.tsx +2 -2
  88. package/src/components/editor/cell/CreateCellButton.tsx +2 -2
  89. package/src/components/editor/cell/DeleteButton.tsx +2 -2
  90. package/src/components/editor/cell/RunButton.tsx +3 -3
  91. package/src/components/editor/cell/StopButton.tsx +2 -2
  92. package/src/components/editor/chrome/components/__tests__/feedback-button.test.tsx +4 -4
  93. package/src/components/editor/chrome/components/feedback-button.tsx +3 -3
  94. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +8 -8
  95. package/src/components/editor/chrome/panels/__tests__/{sandbox-details.test.tsx → runtime-details.test.tsx} +94 -22
  96. package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +4 -4
  97. package/src/components/editor/chrome/panels/packages-panel.tsx +8 -29
  98. package/src/components/editor/chrome/panels/{sandbox-details-state.ts → runtime-details-state.ts} +1 -1
  99. package/src/components/editor/chrome/panels/{sandbox-panel.tsx → runtime-panel.tsx} +38 -66
  100. package/src/components/editor/chrome/panels/runtime-status-toggle.tsx +56 -0
  101. package/src/components/editor/chrome/panels/sandbox-controller.tsx +2 -2
  102. package/src/components/editor/chrome/wrapper/app-chrome.tsx +3 -3
  103. package/src/components/editor/chrome/wrapper/footer-items/machine-stats.tsx +2 -2
  104. package/src/components/editor/controls/Controls.tsx +3 -3
  105. package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +2 -2
  106. package/src/components/editor/header/__tests__/status.test.tsx +4 -4
  107. package/src/components/editor/header/app-header.tsx +2 -2
  108. package/src/components/editor/header/status.tsx +3 -3
  109. package/src/components/ui/progress-stages.tsx +114 -0
  110. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +106 -5
  111. package/src/core/codemirror/lsp/notebook-lsp.ts +16 -2
  112. package/src/core/codemirror/readonly/__tests__/extension.test.ts +6 -6
  113. package/src/core/codemirror/readonly/extension.ts +2 -2
  114. package/src/core/datasets/__tests__/data-source-discovery.test.ts +2 -2
  115. package/src/core/edit-app.tsx +5 -1
  116. package/src/core/export/hooks.ts +2 -2
  117. package/src/core/network/__tests__/connection.test.ts +3 -3
  118. package/src/core/network/connection-notice.ts +5 -5
  119. package/src/core/network/connection.ts +9 -9
  120. package/src/core/network/requests-lazy.ts +2 -2
  121. package/src/core/packages/__tests__/useSandboxController.test.tsx +2 -2
  122. package/src/core/packages/useSandboxController.ts +6 -6
  123. package/src/core/runtime/__tests__/adapter.test.ts +6 -6
  124. package/src/core/runtime/adapter.ts +7 -7
  125. package/src/core/runtime/config.ts +2 -2
  126. package/src/core/saving/filename.ts +2 -2
  127. package/src/core/saving/save-component.tsx +5 -5
  128. package/src/core/saving/useAutoSave.ts +2 -2
  129. package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +30 -25
  130. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +5 -5
  131. package/src/core/websocket/connection-utils.ts +21 -21
  132. package/src/core/websocket/types.ts +8 -9
  133. package/src/core/websocket/useMarimoKernelConnection.tsx +21 -21
  134. package/src/mount.tsx +2 -2
  135. package/src/plugins/core/__test__/registerReactComponent.test.ts +55 -0
  136. package/src/plugins/core/registerReactComponent.tsx +3 -1
  137. package/src/stories/cell.stories.tsx +2 -2
  138. package/src/stories/layout/vertical/one-column.stories.tsx +5 -5
  139. package/src/stories/progress-stages.stories.tsx +62 -0
  140. package/src/stories/runtime-existing.stories.tsx +44 -0
  141. package/src/stories/runtime-maintenance.stories.tsx +40 -0
  142. package/src/stories/runtime-pixi.stories.tsx +28 -0
  143. package/src/stories/{sandbox.stories.tsx → runtime-story.tsx} +191 -152
  144. package/src/stories/runtime-uv.stories.tsx +56 -0
  145. package/dist/assets/cell-link-BeZAiE1D.js +0 -1
  146. package/dist/assets/code-visibility-B5QjA0ie.js +0 -1
  147. package/dist/assets/feedback-button-JqvnTSfN.js +0 -1
  148. package/dist/assets/index-CF8Gfz1R.css +0 -2
  149. package/dist/assets/packages-panel-CDDChziK.js +0 -1
  150. package/dist/assets/panels-CU7deLqc.js +0 -4
  151. package/dist/assets/sandbox-panel-D5bRunTM.js +0 -1
  152. package/dist/assets/state-Bo8h64Rn.js +0 -1
  153. package/src/components/editor/chrome/panels/sandbox-toggle.tsx +0 -52
@@ -22,10 +22,10 @@ import { withPackageInvalidation } from "@/core/packages/package-data";
22
22
  import { useInstallPackages } from "@/core/packages/useInstallPackage";
23
23
  import { sandboxAtom, sandboxSyncAtom } from "@/core/packages/sandbox-state";
24
24
  import { store } from "@/core/state/jotai";
25
- import { WebSocketState } from "@/core/websocket/types";
25
+ import { ConnectionState } from "@/core/websocket/types";
26
26
  import PackagesPanel from "../packages-panel";
27
27
  import { PanelSectionProvider } from "../panel-context";
28
- import { SandboxToggle } from "../sandbox-toggle";
28
+ import { RuntimeStatusToggle } from "../runtime-status-toggle";
29
29
 
30
30
  const { openSettings } = vi.hoisted(() => ({
31
31
  openSettings: vi.fn(),
@@ -71,7 +71,7 @@ function renderPanel(
71
71
  ? { backend: context.backend, manifest: "", filename: "notebook.py" }
72
72
  : null,
73
73
  );
74
- store.set(connectionAtom, { state: WebSocketState.OPEN });
74
+ store.set(connectionAtom, { state: ConnectionState.OPEN });
75
75
  store.set(sandboxSyncAtom, { kind: "succeeded" });
76
76
  const getPackageList = vi.fn().mockResolvedValue({ packages: [] });
77
77
  const client = MockRequestClient.create({
@@ -87,7 +87,7 @@ function renderPanel(
87
87
  <Provider store={store}>
88
88
  <TooltipProvider>
89
89
  <PanelSectionProvider value="sidebar">
90
- <SandboxToggle section="sidebar" />
90
+ <RuntimeStatusToggle section="sidebar" />
91
91
  <PackagesPanel />
92
92
  </PanelSectionProvider>
93
93
  </TooltipProvider>
@@ -186,7 +186,7 @@ it("refreshes an open panel when a package is installed elsewhere, after install
186
186
  manifest: "",
187
187
  filename: "notebook.py",
188
188
  });
189
- store.set(connectionAtom, { state: WebSocketState.OPEN });
189
+ store.set(connectionAtom, { state: ConnectionState.OPEN });
190
190
  store.set(sandboxSyncAtom, { kind: "succeeded" });
191
191
  store.set(requestClientAtom, withPackageInvalidation(client));
192
192
  function InstallElsewhere() {
@@ -201,7 +201,7 @@ it("refreshes an open panel when a package is installed elsewhere, after install
201
201
  <Provider store={store}>
202
202
  <TooltipProvider>
203
203
  <PanelSectionProvider value="sidebar">
204
- <SandboxToggle section="sidebar" />
204
+ <RuntimeStatusToggle section="sidebar" />
205
205
  <PackagesPanel />
206
206
  </PanelSectionProvider>
207
207
  <InstallElsewhere />
@@ -221,7 +221,7 @@ it("refreshes an open panel when a package is installed elsewhere, after install
221
221
  });
222
222
 
223
223
  it("keeps packages and the install draft visible while blocking mutations during sync", async () => {
224
- store.set(connectionAtom, { state: WebSocketState.OPEN });
224
+ store.set(connectionAtom, { state: ConnectionState.OPEN });
225
225
  renderPanel({ kind: "sandbox", backend: "pixi" }, populatedTree);
226
226
  await screen.findByRole("treeitem", { name: /polars/ });
227
227
  const input = screen.getByPlaceholderText("Add packages to pixi sandbox...");
@@ -235,7 +235,7 @@ it("keeps packages and the install draft visible while blocking mutations during
235
235
  expect(
236
236
  screen.queryByRole("list", { name: "Notebook startup stages" }),
237
237
  ).not.toBeInTheDocument();
238
- const toggle = screen.getByRole("button", { name: "pixi sandbox" });
238
+ const toggle = screen.getByRole("button", { name: /^pixi sandbox, / });
239
239
  fireEvent.click(toggle);
240
240
  expect(toggle).toHaveAttribute("aria-expanded", "false");
241
241
  act(() =>
@@ -11,10 +11,10 @@ import { kernelStartupErrorAtom } from "@/core/errors/state";
11
11
  import { connectionAtom, startupProgressAtom } from "@/core/network/connection";
12
12
  import { requestClientAtom } from "@/core/network/requests";
13
13
  import { sandboxAtom } from "@/core/packages/sandbox-state";
14
- import { WebSocketClosedReason, WebSocketState } from "@/core/websocket/types";
14
+ import { WebSocketClosedReason, ConnectionState } from "@/core/websocket/types";
15
15
  import PackagesPanel from "../packages-panel";
16
16
  import { PanelSectionProvider } from "../panel-context";
17
- import { SandboxToggle } from "../sandbox-toggle";
17
+ import { RuntimeStatusToggle } from "../runtime-status-toggle";
18
18
 
19
19
  vi.mock("@/utils/reload-safe", () => ({ reloadSafe: vi.fn() }));
20
20
 
@@ -43,8 +43,8 @@ function mount({
43
43
  store.set(
44
44
  connectionAtom,
45
45
  preparing
46
- ? { state: WebSocketState.CONNECTING, phase: "preparing-environment" }
47
- : { state: WebSocketState.OPEN },
46
+ ? { state: ConnectionState.CONNECTING, phase: "preparing-environment" }
47
+ : { state: ConnectionState.OPEN },
48
48
  );
49
49
  store.set(alertAtom, (state) => ({
50
50
  ...state,
@@ -89,7 +89,7 @@ function mount({
89
89
  <Provider store={store}>
90
90
  <TooltipProvider>
91
91
  <ModalProvider>
92
- <SandboxToggle section="sidebar" />
92
+ <RuntimeStatusToggle section="sidebar" />
93
93
  <PanelSectionProvider value="sidebar">
94
94
  <PackagesPanel />
95
95
  </PanelSectionProvider>
@@ -103,7 +103,7 @@ function mount({
103
103
  it("opens restored preparation and kernel logs without reopening completed startup on refresh", async () => {
104
104
  mount();
105
105
  await screen.findByText("numpy");
106
- const toggle = screen.getByRole("button", { name: "uv sandbox" });
106
+ const toggle = screen.getByRole("button", { name: /^uv sandbox, / });
107
107
  expect(toggle).toHaveAttribute("aria-expanded", "false");
108
108
  expect(
109
109
  screen.queryByRole("region", { name: "Sandbox details" }),
@@ -164,7 +164,9 @@ it("restores a restart-required sync with the kernel restart action", async () =
164
164
  })),
165
165
  );
166
166
  await screen.findByText("numpy");
167
- expect(screen.getByText("Sandbox restart required")).toBeVisible();
167
+ expect(
168
+ screen.getByRole("heading", { name: "Sandbox restart required" }),
169
+ ).toBeVisible();
168
170
  expect(
169
171
  screen.queryByRole("button", { name: "Retry sync" }),
170
172
  ).not.toBeInTheDocument();
@@ -194,7 +196,9 @@ it("restores a restart-required sync with the kernel restart action", async () =
194
196
  },
195
197
  })),
196
198
  );
197
- expect(screen.getByText("Sandbox restart required")).toBeVisible();
199
+ expect(
200
+ screen.getByRole("heading", { name: "Sandbox restart required" }),
201
+ ).toBeVisible();
198
202
  fireEvent.click(screen.getByRole("button", { name: "Restart Kernel" }));
199
203
  expect(screen.getByRole("alertdialog")).toHaveTextContent("Restart Kernel");
200
204
  expect(client.sendRestart).not.toHaveBeenCalled();
@@ -251,7 +255,7 @@ it("keeps sync output open through completion and restores it quietly after refr
251
255
  unmount();
252
256
  const refreshed = mount();
253
257
  act(() => refreshed.store.set(alertAtom, snapshot));
254
- const toggle = screen.getByRole("button", { name: "uv sandbox" });
258
+ const toggle = screen.getByRole("button", { name: /^uv sandbox, / });
255
259
  expect(toggle).toHaveAttribute("aria-expanded", "false");
256
260
  fireEvent.click(toggle);
257
261
  fireEvent.click(
@@ -266,7 +270,7 @@ it.each([false, true])(
266
270
  "retains the progression after completion with inspecting=%s",
267
271
  async (inspecting) => {
268
272
  const { store, client } = mount({ preparing: true });
269
- const toggle = screen.getByRole("button", { name: "uv sandbox" });
273
+ const toggle = screen.getByRole("button", { name: /^uv sandbox, / });
270
274
  expect(toggle).toHaveAttribute("aria-expanded", "true");
271
275
  expect(client.getDependencyTree).not.toHaveBeenCalled();
272
276
  if (inspecting) {
@@ -283,11 +287,11 @@ it.each([false, true])(
283
287
  log_mode: "replace",
284
288
  });
285
289
  store.set(connectionAtom, {
286
- state: WebSocketState.CONNECTING,
290
+ state: ConnectionState.CONNECTING,
287
291
  phase: "starting-kernel",
288
292
  });
289
293
  });
290
- act(() => store.set(connectionAtom, { state: WebSocketState.OPEN }));
294
+ act(() => store.set(connectionAtom, { state: ConnectionState.OPEN }));
291
295
  await screen.findByText("numpy");
292
296
  expect(toggle).toHaveAttribute("aria-expanded", String(inspecting));
293
297
  if (!inspecting) {
@@ -308,7 +312,7 @@ it.each([false, true])(
308
312
 
309
313
  it("opens a startup error even after collapsing preparation and keeps both step outputs available", () => {
310
314
  const { store } = mount({ preparing: true, backend: "pixi" });
311
- const toggle = screen.getByRole("button", { name: "pixi sandbox" });
315
+ const toggle = screen.getByRole("button", { name: /^pixi sandbox, / });
312
316
  fireEvent.click(toggle);
313
317
  act(() => {
314
318
  store.set(startupProgressAtom, {
@@ -318,13 +322,14 @@ it("opens a startup error even after collapsing preparation and keeps both step
318
322
  });
319
323
  store.set(kernelStartupErrorAtom, "Could not import the runtime");
320
324
  store.set(connectionAtom, {
321
- state: WebSocketState.CLOSED,
325
+ state: ConnectionState.CLOSED,
322
326
  phase: "starting-kernel",
323
327
  code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
324
328
  reason: "Kernel startup failed",
325
329
  });
326
330
  });
327
331
  expect(toggle).toHaveAttribute("aria-expanded", "true");
332
+ expect(toggle).toHaveTextContent("pixi sandbox");
328
333
  expect(within(toggle).getByRole("status")).toHaveAccessibleName(
329
334
  "Kernel failed to start",
330
335
  );
@@ -346,13 +351,80 @@ it("opens a startup error even after collapsing preparation and keeps both step
346
351
  expect(screen.getByLabelText("Error details")).toBeVisible();
347
352
  });
348
353
 
349
- it("does not add a toggle or startup details to an existing environment", async () => {
350
- mount({ backend: null });
354
+ it.each(["uv", "pixi"] as const)(
355
+ "shows connection status and errors before the %s startup phase arrives",
356
+ (backend) => {
357
+ const { store } = mount({ backend, preparing: true });
358
+ act(() => store.set(connectionAtom, { state: ConnectionState.CONNECTING }));
359
+ const details = within(
360
+ screen.getByRole("region", { name: "Sandbox details" }),
361
+ );
362
+ expect(details.getByRole("status")).toHaveTextContent("Connecting…");
363
+ expect(details.queryByRole("list")).not.toBeInTheDocument();
364
+
365
+ act(() => {
366
+ store.set(kernelStartupErrorAtom, "Kernel process exited unexpectedly");
367
+ store.set(connectionAtom, {
368
+ state: ConnectionState.CLOSED,
369
+ code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
370
+ reason: "Kernel startup failed",
371
+ });
372
+ });
373
+ expect(details.getByRole("status")).toHaveTextContent(
374
+ "Kernel failed to start",
375
+ );
376
+ expect(details.getByLabelText("Error details")).toHaveTextContent(
377
+ "Kernel process exited unexpectedly",
378
+ );
379
+ expect(details.queryByRole("list")).not.toBeInTheDocument();
380
+ },
381
+ );
382
+
383
+ it("uses the same disclosure for an existing environment and updates it through connection changes", async () => {
384
+ const { store } = mount({ backend: null });
351
385
  await screen.findByText("numpy");
352
- expect(
353
- screen.queryByRole("button", { name: /sandbox/ }),
354
- ).not.toBeInTheDocument();
355
- expect(
356
- screen.queryByRole("region", { name: "Sandbox details" }),
357
- ).not.toBeInTheDocument();
386
+ const toggle = screen.getByRole("button", { name: /^Runtime details, / });
387
+ expect(toggle).toHaveTextContent(/^Runtime$/);
388
+ expect(toggle).toHaveAccessibleName("Runtime details, Kernel ready");
389
+ expect(within(toggle).getByRole("status")).toHaveAccessibleName(
390
+ "Kernel ready",
391
+ );
392
+ expect(toggle).toHaveAttribute("aria-expanded", "false");
393
+ fireEvent.click(toggle);
394
+ const details = screen.getByRole("region", { name: "Runtime details" });
395
+ expect(toggle).toHaveAttribute("aria-controls", details.id);
396
+ expect(within(details).getByText("Kernel started")).toBeVisible();
397
+ expect(within(details).getAllByRole("listitem")).toHaveLength(1);
398
+ expect(within(details).queryByRole("button", { name: "Sync" })).toBeNull();
399
+
400
+ act(() => store.set(connectionAtom, { state: ConnectionState.CONNECTING }));
401
+ expect(toggle).toHaveTextContent(/^Runtime$/);
402
+ expect(within(toggle).getByRole("status")).toHaveAccessibleName(
403
+ "Connecting…",
404
+ );
405
+ expect(within(details).getByText("Starting kernel")).toBeVisible();
406
+ fireEvent.click(toggle);
407
+ expect(details).not.toBeVisible();
408
+ act(() =>
409
+ store.set(connectionAtom, {
410
+ state: ConnectionState.CLOSED,
411
+ code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
412
+ reason: "Kernel startup failed",
413
+ }),
414
+ );
415
+ expect(toggle).toHaveAttribute("aria-expanded", "true");
416
+ expect(toggle).toHaveTextContent(/^Runtime$/);
417
+ expect(within(toggle).getByRole("status")).toHaveAccessibleName(
418
+ "Kernel failed to start",
419
+ );
420
+ expect(details).toBeVisible();
421
+ expect(toggle).toHaveAccessibleName(
422
+ "Runtime details, Kernel failed to start",
423
+ );
424
+
425
+ act(() => store.set(connectionAtom, { state: ConnectionState.NOT_STARTED }));
426
+ expect(toggle).toHaveTextContent(/^Runtime$/);
427
+ expect(within(toggle).getByRole("status")).toHaveAccessibleName(
428
+ "Kernel not connected",
429
+ );
358
430
  });
@@ -24,7 +24,7 @@ import {
24
24
  } from "@/core/packages/sandbox-state";
25
25
  import { filenameAtom } from "@/core/saving/file-state";
26
26
  import { store } from "@/core/state/jotai";
27
- import { WebSocketClosedReason, WebSocketState } from "@/core/websocket/types";
27
+ import { WebSocketClosedReason, ConnectionState } from "@/core/websocket/types";
28
28
  import { chromeAtom } from "../../state";
29
29
  import PackagesPanel from "../packages-panel";
30
30
  import { PanelSectionProvider } from "../panel-context";
@@ -52,7 +52,7 @@ vi.mock("@/plugins/impl/code/LazyAnyLanguageCodeMirror", () => ({
52
52
  const manifest =
53
53
  'dependencies = ["numpy==0.0.0"]\n\n[tool.custom]\nlabel = "experiment"\n';
54
54
  const failed = {
55
- state: WebSocketState.CLOSED,
55
+ state: ConnectionState.CLOSED,
56
56
  code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
57
57
  phase: "preparing-environment",
58
58
  reason: "Kernel startup failed",
@@ -117,7 +117,7 @@ it("keeps a whole-manifest repair open through failure and closes only after syn
117
117
  const reconnect = vi.fn(async () => {
118
118
  store.set(kernelStartupErrorAtom, null);
119
119
  store.set(connectionAtom, {
120
- state: WebSocketState.CONNECTING,
120
+ state: ConnectionState.CONNECTING,
121
121
  phase: "preparing-environment",
122
122
  });
123
123
  });
@@ -161,7 +161,7 @@ it("keeps a whole-manifest repair open through failure and closes only after syn
161
161
  );
162
162
  fireEvent.click(dialog.getByRole("button", { name: "Sync" }));
163
163
  await waitFor(() => expect(reconnect).toHaveBeenCalledTimes(2));
164
- act(() => store.set(connectionAtom, { state: WebSocketState.OPEN }));
164
+ act(() => store.set(connectionAtom, { state: ConnectionState.OPEN }));
165
165
  await waitFor(() =>
166
166
  expect(screen.queryByRole("dialog")).not.toBeInTheDocument(),
167
167
  );
@@ -7,7 +7,6 @@ import {
7
7
  HelpCircleIcon,
8
8
  } from "lucide-react";
9
9
  import React from "react";
10
- import { useConnectionNotice } from "@/core/network/useConnectionNotice";
11
10
  import { useOpenSettingsToTab } from "@/components/app-config/state";
12
11
  import { Spinner } from "@/components/icons/spinner";
13
12
  import { SearchInput } from "@/components/ui/input";
@@ -26,7 +25,7 @@ import type {
26
25
  DependencyTreeNode,
27
26
  DependencyTreeResponse,
28
27
  } from "@/core/network/types";
29
- import { sandboxAtom, sandboxSyncAtom } from "@/core/packages/sandbox-state";
28
+ import { sandboxSyncAtom } from "@/core/packages/sandbox-state";
30
29
  import { stripPackageManagerPrefix } from "@/core/packages/package-input-utils";
31
30
  import { usePackageAction } from "@/core/packages/usePackageAction";
32
31
  import { usePackageDependencies } from "@/core/packages/usePackageDependencies";
@@ -36,7 +35,7 @@ import { ErrorBanner } from "@/plugins/impl/common/error-banner";
36
35
  import { cn } from "@/utils/cn";
37
36
  import { copyToClipboard } from "@/utils/copy";
38
37
  import { Events } from "@/utils/events";
39
- import { SandboxDetails } from "./sandbox-panel";
38
+ import { RuntimeDetails } from "./runtime-panel";
40
39
  import { PanelEmptyState } from "./empty-state";
41
40
  import { PACKAGES_INPUT_ID, packagesToInstallAtom } from "./packages-utils";
42
41
 
@@ -69,33 +68,13 @@ const PackageActionButton: React.FC<{
69
68
  };
70
69
 
71
70
  const PackagesPanel: React.FC = () => {
72
- const sandbox = useAtomValue(sandboxAtom);
73
71
  const connected = useAtomValue(isConnectedAtom);
74
- const notice = useConnectionNotice(0);
75
- if (sandbox?.backend) {
76
- return (
77
- <div className="flex flex-col flex-1 min-h-0 overflow-hidden">
78
- <SandboxDetails />
79
- {connected && <PackageContents />}
80
- </div>
81
- );
82
- }
83
- if (notice) {
84
- return (
85
- <div className="p-4 text-sm text-muted-foreground">
86
- <div className="flex items-center gap-2">
87
- {notice.pending && <Spinner className="size-3" />}
88
- {notice.title}
89
- </div>
90
- {notice.error && (
91
- <pre className="mt-3 text-xs whitespace-pre-wrap break-words max-h-52 overflow-auto">
92
- {notice.error}
93
- </pre>
94
- )}
95
- </div>
96
- );
97
- }
98
- return <PackageContents />;
72
+ return (
73
+ <div className="flex flex-col flex-1 min-h-0 overflow-hidden">
74
+ <RuntimeDetails />
75
+ {connected && <PackageContents />}
76
+ </div>
77
+ );
99
78
  };
100
79
 
101
80
  const PackageContents: React.FC = () => {
@@ -17,7 +17,7 @@ const disclosureContextAtom = atom((get) => {
17
17
  };
18
18
  });
19
19
 
20
- export const sandboxDetailsExpandedAtom = atom(
20
+ export const runtimeDetailsExpandedAtom = atom(
21
21
  (get) => {
22
22
  const context = get(disclosureContextAtom);
23
23
  const preference = get(preferenceAtom);
@@ -1,13 +1,13 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { useAtom, useAtomValue } from "jotai";
3
3
  import { RefreshCwIcon } from "lucide-react";
4
- import { connectionNoticeAtom } from "@/core/network/connection-notice";
5
4
  import { Button } from "@/components/ui/button";
6
5
  import { useRestartKernel } from "@/components/editor/actions/useRestartKernel";
7
6
  import {
8
7
  isConnectedAtom,
9
8
  startupProgressAtom,
10
9
  } from "@/core/network/connection";
10
+ import { connectionNoticeAtom } from "@/core/network/connection-notice";
11
11
  import type { DisplayConnectionNotice } from "@/core/network/useConnectionNotice";
12
12
  import {
13
13
  preparationAtom,
@@ -17,25 +17,10 @@ import {
17
17
  sandboxSyncOperationAtom,
18
18
  } from "@/core/packages/sandbox-state";
19
19
  import { cn } from "@/utils/cn";
20
- import { usePanelSection } from "./panel-context";
21
- import { sandboxDetailsExpandedAtom } from "./sandbox-details-state";
22
20
  import { StartupOutput } from "../../alerts/startup-output";
23
- import {
24
- ConnectionStatusIcon,
25
- StartupProgress,
26
- } from "../../alerts/startup-progress";
27
-
28
- export function SandboxErrorOutput({ error }: { error: string }) {
29
- return (
30
- <pre
31
- tabIndex={0}
32
- aria-label="Error details"
33
- className="mt-3 p-3 rounded border bg-muted/30 text-xs text-muted-foreground whitespace-pre min-w-0 max-w-full max-h-52 overflow-auto"
34
- >
35
- {error}
36
- </pre>
37
- );
38
- }
21
+ import { ConnectionDetails } from "../../alerts/connection-details";
22
+ import { usePanelSection } from "./panel-context";
23
+ import { runtimeDetailsExpandedAtom } from "./runtime-details-state";
39
24
 
40
25
  function SandboxRecovery({ notice }: { notice: DisplayConnectionNotice }) {
41
26
  const sandbox = useAtomValue(sandboxAtom);
@@ -46,8 +31,6 @@ function SandboxRecovery({ notice }: { notice: DisplayConnectionNotice }) {
46
31
  }
47
32
  return (
48
33
  <>
49
- <p className="mt-2 text-foreground">{notice.description}</p>
50
- {notice.error && <SandboxErrorOutput error={notice.error} />}
51
34
  <div className="flex items-center gap-2 mt-3">
52
35
  <Button
53
36
  variant="outline"
@@ -88,36 +71,11 @@ function SandboxRestart() {
88
71
  );
89
72
  }
90
73
 
91
- function SandboxStartupPanel({ notice }: { notice: DisplayConnectionNotice }) {
92
- const showSteps =
93
- notice.kind === "startup" &&
94
- (notice.ready ||
95
- notice.phase === "preparing-environment" ||
96
- notice.phase === "starting-kernel");
97
- return (
98
- <div className="min-w-0 text-sm">
99
- {showSteps ? (
100
- <StartupProgress notice={notice} surface="sidebar" />
101
- ) : (
102
- <div className="flex items-center gap-2" role="status">
103
- <ConnectionStatusIcon notice={notice} />
104
- <h2>{notice.title}</h2>
105
- </div>
106
- )}
107
- <SandboxRecovery notice={notice} />
108
- </div>
109
- );
110
- }
111
-
112
- function SandboxSyncStatus({ notice }: { notice: DisplayConnectionNotice }) {
74
+ function RuntimeNoticeDetails({ notice }: { notice: DisplayConnectionNotice }) {
113
75
  return (
114
- <div className="mb-5 text-sm">
115
- <output className="flex items-center gap-2 text-xs">
116
- <ConnectionStatusIcon notice={notice} />
117
- <span>{notice.title}</span>
118
- </output>
119
- <SandboxRecovery notice={notice} />
120
- </div>
76
+ <ConnectionDetails notice={notice}>
77
+ {notice.sandbox && <SandboxRecovery notice={notice} />}
78
+ </ConnectionDetails>
121
79
  );
122
80
  }
123
81
 
@@ -133,7 +91,9 @@ function SandboxSyncDetails({
133
91
  return (
134
92
  <div className="mt-5 min-w-0">
135
93
  {notice ? (
136
- <SandboxSyncStatus notice={notice} />
94
+ <div className="mb-5">
95
+ <RuntimeNoticeDetails notice={notice} />
96
+ </div>
137
97
  ) : (
138
98
  <p className="text-sm">Environment synced</p>
139
99
  )}
@@ -153,20 +113,25 @@ function SandboxSyncDetails({
153
113
  );
154
114
  }
155
115
 
156
- export function SandboxDetails() {
116
+ export function RuntimeDetails() {
157
117
  const section = usePanelSection();
118
+ const sandbox = Boolean(useAtomValue(sandboxAtom)?.backend);
158
119
  const connected = useAtomValue(isConnectedAtom);
159
- const notice = useAtomValue(connectionNoticeAtom);
120
+ const connectionNotice = useAtomValue(connectionNoticeAtom);
121
+ const notice: DisplayConnectionNotice | null = connectionNotice && {
122
+ ...connectionNotice,
123
+ ready: false,
124
+ };
160
125
  const preparation = useAtomValue(preparationAtom);
161
126
  const progress = useAtomValue(startupProgressAtom);
162
- const [expanded, setExpanded] = useAtom(sandboxDetailsExpandedAtom);
127
+ const [expanded, setExpanded] = useAtom(runtimeDetailsExpandedAtom);
163
128
  const startup: DisplayConnectionNotice | null =
164
129
  notice?.kind === "startup"
165
- ? { ...notice, ready: false }
166
- : preparation || progress
130
+ ? notice
131
+ : preparation || progress || (connected && !sandbox)
167
132
  ? {
168
133
  kind: "startup",
169
- sandbox: true,
134
+ sandbox,
170
135
  title: "Notebook started",
171
136
  description: "The notebook is ready to run.",
172
137
  pending: false,
@@ -176,8 +141,8 @@ export function SandboxDetails() {
176
141
  : null;
177
142
  return (
178
143
  <section
179
- id={`sandbox-details-${section}`}
180
- aria-label="Sandbox details"
144
+ id={`runtime-details-${section}`}
145
+ aria-label={sandbox ? "Sandbox details" : "Runtime details"}
181
146
  hidden={!expanded}
182
147
  className={cn(
183
148
  "min-w-0 overflow-auto p-4",
@@ -187,15 +152,22 @@ export function SandboxDetails() {
187
152
  onFocusCapture={() => setExpanded(true)}
188
153
  >
189
154
  {notice?.kind === "connection" && (
190
- <SandboxSyncStatus notice={{ ...notice, ready: false }} />
155
+ <div className="mb-5">
156
+ <RuntimeNoticeDetails notice={notice} />
157
+ </div>
191
158
  )}
192
- {startup && <SandboxStartupPanel notice={startup} />}
193
- {connected && (
194
- <SandboxSyncDetails
195
- notice={notice?.kind === "sync" ? { ...notice, ready: false } : null}
196
- />
159
+ {startup && <RuntimeNoticeDetails notice={startup} />}
160
+ {sandbox && connected && (
161
+ <SandboxSyncDetails notice={notice?.kind === "sync" ? notice : null} />
162
+ )}
163
+ {sandbox && connected && (!notice || notice.pending) && (
164
+ <SandboxActions />
165
+ )}
166
+ {!notice && !startup && !sandbox && (
167
+ <p className="text-sm text-muted-foreground">
168
+ Not connected to a runtime.
169
+ </p>
197
170
  )}
198
- {connected && (!notice || notice.pending) && <SandboxActions />}
199
171
  </section>
200
172
  );
201
173
  }
@@ -0,0 +1,56 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { useAtom, useAtomValue } from "jotai";
3
+ import { ChevronDownIcon } from "lucide-react";
4
+ import {
5
+ ProgressStatusIcon,
6
+ type ProgressState,
7
+ } from "@/components/ui/progress-stages";
8
+ import { isConnectedAtom } from "@/core/network/connection";
9
+ import { connectionNoticeAtom } from "@/core/network/connection-notice";
10
+ import { sandboxAtom } from "@/core/packages/sandbox-state";
11
+ import { cn } from "@/utils/cn";
12
+ import type { PanelSection } from "./panel-context";
13
+ import { runtimeDetailsExpandedAtom } from "./runtime-details-state";
14
+
15
+ export function RuntimeStatusToggle({ section }: { section: PanelSection }) {
16
+ const sandbox = useAtomValue(sandboxAtom);
17
+ const notice = useAtomValue(connectionNoticeAtom);
18
+ const [expanded, setExpanded] = useAtom(runtimeDetailsExpandedAtom);
19
+ const connected = useAtomValue(isConnectedAtom);
20
+ const label = sandbox?.backend ? `${sandbox.backend} sandbox` : "Runtime";
21
+ const statusLabel =
22
+ notice?.title ?? (connected ? "Kernel ready" : "Kernel not connected");
23
+ const status: ProgressState = notice
24
+ ? notice.pending
25
+ ? "running"
26
+ : "failed"
27
+ : connected
28
+ ? "succeeded"
29
+ : "waiting";
30
+ return (
31
+ <button
32
+ type="button"
33
+ aria-label={`${sandbox?.backend ? label : "Runtime details"}, ${statusLabel}`}
34
+ aria-expanded={expanded}
35
+ aria-controls={`runtime-details-${section}`}
36
+ title={statusLabel}
37
+ className={cn(
38
+ "flex min-w-0 items-center gap-1.5 rounded px-1.5 py-1 text-xs whitespace-nowrap hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring",
39
+ status === "failed" ? "text-(--red-11)" : "text-foreground",
40
+ )}
41
+ onClick={() => setExpanded(!expanded)}
42
+ >
43
+ <ProgressStatusIcon state={status} variant="dot" />
44
+ <output aria-label={statusLabel} className="truncate">
45
+ {label}
46
+ </output>
47
+ <ChevronDownIcon
48
+ aria-hidden={true}
49
+ className={cn(
50
+ "size-3 shrink-0 text-muted-foreground",
51
+ expanded && "rotate-180",
52
+ )}
53
+ />
54
+ </button>
55
+ );
56
+ }
@@ -14,7 +14,7 @@ import { useSandboxController } from "@/core/packages/useSandboxController";
14
14
  import { LazyAnyLanguageCodeMirror } from "@/plugins/impl/code/LazyAnyLanguageCodeMirror";
15
15
  import { useTheme } from "@/theme/useTheme";
16
16
  import { Paths } from "@/utils/paths";
17
- import { SandboxErrorOutput } from "./sandbox-panel";
17
+ import { ConnectionErrorOutput } from "../../alerts/connection-details";
18
18
 
19
19
  export function SandboxController({
20
20
  onReconnect,
@@ -64,7 +64,7 @@ export function SandboxController({
64
64
  ) : (
65
65
  !error && <Spinner />
66
66
  )}
67
- {diagnostic && <SandboxErrorOutput error={diagnostic} />}
67
+ {diagnostic && <ConnectionErrorOutput error={diagnostic} />}
68
68
  {conflict && (
69
69
  <Button variant="text" size="sm" onClick={loadManifest}>
70
70
  Discard draft and reload manifest
@@ -34,7 +34,7 @@ import { ErrorBoundary } from "../../boundary/ErrorBoundary";
34
34
  import { raf2 } from "../../navigation/focus-utils";
35
35
  import { ContextAwarePanel } from "../panels/context-aware-panel/context-aware-panel";
36
36
  import { PanelSectionProvider } from "../panels/panel-context";
37
- import { SandboxToggle } from "../panels/sandbox-toggle";
37
+ import { RuntimeStatusToggle } from "../panels/runtime-status-toggle";
38
38
  import { useTheme } from "@/theme/useTheme";
39
39
  import {
40
40
  LazyAgentPanel,
@@ -391,7 +391,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
391
391
  </span>
392
392
  )}
393
393
  {selectedPanel === "packages" && (
394
- <SandboxToggle section="sidebar" />
394
+ <RuntimeStatusToggle section="sidebar" />
395
395
  )}
396
396
  <Button
397
397
  data-testid="close-helper-pane"
@@ -549,7 +549,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
549
549
  <LspStatus />
550
550
  <div className="flex-1" />
551
551
  {selectedDeveloperPanelTab === "packages" && (
552
- <SandboxToggle section="developer-panel" />
552
+ <RuntimeStatusToggle section="developer-panel" />
553
553
  )}
554
554
  <Button
555
555
  size="xs"