Skip to content

Add sandbox startup and recovery UI - #10822

Open
manzt wants to merge 6 commits into
manzt/sandbox-runtimefrom
manzt/sandbox-ui
Open

manzt wants to merge 6 commits into
manzt/sandbox-runtimefrom
manzt/sandbox-ui

Conversation

@manzt

@manzt manzt commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Async startup lets the browser open before the kernel is ready. This PR adds pending states for empty notebooks and notebooks with existing cells.

Packages gains sandbox status, sync, and a full TOML manifest editor. Recovery works before a kernel exists, while failed syncs keep the editor open with the user's changes, and stale saves cannot overwrite external edits. Package changes refresh the panel automatically.


existing-notebook-preparing
starting-kernel
existing-notebook-failed
manifest-editor

@vercel

vercel Bot commented Sep 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
marimo-docs Ready Ready Preview Sep 15, 2026 3:21pm UTC

Request Review

@manzt
manzt added this pull request to stack #10823 September 13, 2026 22:10
@github-actions github-actions Bot added the bash-focus Area to focus on during release bug bash label Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@manzt manzt added the enhancement New feature or request label Sep 13, 2026
@manzt manzt changed the title manzt/sandbox ui Add sandbox startup and recovery UI Sep 13, 2026
@manzt
manzt marked this pull request as ready for review September 13, 2026 22:13
Copilot AI lite review requested due to automatic review settings September 13, 2026 22:13
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for ./frontend

Status Category Percentage Covered / Total
🔵 Lines 58.54% 22424 / 38304
🔵 Statements 58.47% 22818 / 39023
🔵 Functions 51.63% 5064 / 9807
🔵 Branches 52.08% 11798 / 22653
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
frontend/src/__mocks__/requests.ts 100% 100% 100% 100%
frontend/src/components/editor/app-container.tsx 0% 0% 0% 0% 24-67
frontend/src/components/editor/alerts/connecting-alert.tsx 0% 0% 0% 0% 9-26
frontend/src/components/editor/alerts/connection-notice.tsx 100% 92.5% 100% 100%
frontend/src/components/editor/chrome/panels/packages-panel.tsx 76.8% 73.27% 76.66% 76.8% 89-100, 117, 161, 212-214, 220, 224-228, 269-274, 397-400, 422, 463, 467-472, 499, 540-543, 608-641
frontend/src/components/editor/chrome/panels/sandbox-controller.tsx 75% 90% 50% 75% 83
frontend/src/components/editor/chrome/panels/sandbox-panel.tsx 76.19% 89.47% 57.14% 76.19% 44, 80, 103, 154-163
frontend/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx 0% 0% 0% 0% 22-157
frontend/src/components/editor/header/status.tsx 81.25% 69.23% 75% 81.25% 83-97
frontend/src/components/editor/renderers/cell-array.tsx 0% 0% 0% 0% 62-386
frontend/src/core/MarimoApp.tsx 33.33% 10% 22.22% 33.33% 29-30, 32-33, 38-39, 44-51, 61-88, 97-113, 119
frontend/src/core/edit-app.tsx 0% 0% 0% 0% 6-211
frontend/src/core/run-app.tsx 0% 0% 0% 0% 38-117
frontend/src/core/errors/state.ts 50% 100% 28.57% 55.55% 22-34, 42
frontend/src/core/islands/bridge.ts 92.55% 90.47% 77.77% 92.51% 272-274, 85-87, 100-101, 107-109, 273, 427-437
frontend/src/core/network/connection-notice.ts 100% 84.37% 100% 100%
frontend/src/core/network/requests-lazy.ts 81.48% 63.63% 100% 81.48% 177, 186-193, 213
frontend/src/core/network/requests-network.ts 23.72% 33.33% 21.17% 23.72% 23-25, 50-272, 283-301, 307-320, 327-419, 452-461, 504-559, 568-627
frontend/src/core/network/requests-static.ts 66.66% 100% 57.14% 66.66% 20, 30-35
frontend/src/core/network/requests-toasting.tsx 59.37% 58.33% 33.33% 59.37% 105-119, 152-160
frontend/src/core/network/resolve.ts 71.42% 75% 50% 71.42% 16, 21
frontend/src/core/network/types.ts 0% 0% 0% 0%
frontend/src/core/packages/package-data.ts 100% 50% 100% 100%
frontend/src/core/packages/sandbox-state.ts 100% 100% 100% 100%
frontend/src/core/packages/toast-components.tsx 95.45% 83.33% 100% 95.45% 87-91
frontend/src/core/packages/useInstallPackage.ts 93.33% 100% 100% 93.33% 45
frontend/src/core/packages/usePackageAction.ts 100% 100% 100% 100%
frontend/src/core/packages/usePackageDependencies.ts 100% 100% 100% 100%
frontend/src/core/packages/useSandboxController.ts 93.54% 84.9% 94.11% 93.25% 47, 74, 77, 113, 117, 139
frontend/src/core/wasm/bridge.ts 27.13% 30.61% 16.21% 27.34% 213-226, 255-387, 398-543, 567-683, 686-692, 101, 121, 134-136, 142-152, 155-191, 199-200, 209-225, 230-231, 237, 243, 256-386, 391-392, 399-542, 570-723
frontend/src/core/websocket/types.ts 100% 100% 100% 100%
frontend/src/core/websocket/useMarimoKernelConnection.tsx 46.07% 36.11% 50% 46.3% 180, 208-211, 242-243, 272, 287, 295-458, 466-482, 528-535, 549-559, 569-574, 602, 620-626
frontend/src/stories/sandbox.stories.tsx 0% 0% 0% 0% 37-308
Generated in workflow #21467 for commit caef422 by the Vitest Coverage Report Action

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/src/core/network/connection-notice.ts">

<violation number="1" location="frontend/src/core/network/connection-notice.ts:15">
P2: When a sandbox sync fails while the connection is `CLOSED`, this `OPEN` guard skips `sync.error` and replaces the diagnostic with the generic `connection.reason`. Let `sync.error` take precedence regardless of connection state.</violation>
</file>

<file name="frontend/src/components/editor/chrome/panels/packages-panel.tsx">

<violation number="1" location="frontend/src/components/editor/chrome/panels/packages-panel.tsx:170">
P2: While a sandbox sync is pending, this fieldset disables only package installation; `UpgradeButton` and `RemoveButton` remain clickable outside it. Disable those mutation controls during sync to prevent package changes racing with manifest application.</violation>
</file>

<file name="frontend/src/components/editor/chrome/panels/sandbox-panel.tsx">

<violation number="1" location="frontend/src/components/editor/chrome/panels/sandbox-panel.tsx:80">
P1: When sandbox environment preparation succeeds but the kernel then fails to start, this `Retry sync` action only reapplies the manifest and leaves the notebook disconnected because `syncSandbox` does not request a reconnect for an existing sandbox. Route this recovery action through the kernel reconnect path, or only show it for actual sandbox-sync failures.</violation>
</file>

<file name="marimo/_server/api/endpoints/packages.py">

<violation number="1" location="marimo/_server/api/endpoints/packages.py:280">
P1: When the server is running in single-process sandbox mode, `manager.sandbox` is false even though `GLOBAL_SETTINGS.SANDBOX_MODE` is `"single"`. This branch makes all pre-start sandbox responses empty and prevents the UI from reading or editing the manifest before kernel startup; gate on the configured sandbox mode instead.</violation>
</file>

<file name="marimo/_utils/inline_script_metadata.py">

<violation number="1" location="marimo/_utils/inline_script_metadata.py:411">
P3: This change fixes a real bug: a `pyproject` frontmatter value beginning with a `#` comment used to be left unwrapped, so `script_metadata.loads` returned None and the block was silently ignored. No regression test covers the new behavior. Add a test that a comment-leading raw TOML `pyproject` is wrapped and parsed (e.g. `get_headers_from_markdown`/`_get_pyproject_from_filename` on a `.md` with `pyproject: |\n  # comment\n  dependencies = ["numpy"]`), and that a value already starting with `# /// script` is not double-wrapped.</violation>
</file>

<file name="tests/_environments/test_async_preparation.py">

<violation number="1" location="tests/_environments/test_async_preparation.py:142">
P2: `source.write_text(...)` runs inside the async test and is a blocking pathlib I/O call, which ruff's ASYNC240 flags. Every sibling pathlib call in this file (`markdown.read_text()`, `carrier.exists()`, `Path(owner.path).exists()`) carries `# noqa: ASYNC240`, so this new call without the comment will fail the `make py-check` lint step. Append `# noqa: ASYNC240` to the write.</violation>
</file>

<file name="frontend/src/components/editor/chrome/panels/sandbox-controller.tsx">

<violation number="1" location="frontend/src/components/editor/chrome/panels/sandbox-controller.tsx:85">
P3: `sync()` returns `false` without setting any error when the connection is `WebSocketState.CONNECTING` or when the websocket ends `CLOSED` after reconnect. The visible consequence: in `SandboxStartupPanel` the "Retry sync" button is not disabled during startup (`disabled={!actions}` only), so clicking it during environment preparation silently does nothing, and in `saveAndSync` a reconnect that ends CLOSED silently aborts with the dialog left open and no diagnostic in the dialog itself (only the sidebar notice). Set an error (or disable the button while the connection is CONNECTING) on these false paths so the UI explains why nothing happened.</violation>

<violation number="2" location="frontend/src/components/editor/chrome/panels/sandbox-controller.tsx:100">
P2: `waitFor(connectionAtom, ...)` after `onReconnect()` has no timeout. If reconnection stalls in `WebSocketState.CONNECTING` (network partition, server backoff), the promise never settles: `sync()` stays in the `finally`-guarded region, so `inFlight.current` stays `true`, `operation.pending` stays `true`, and the dialog/footer display an endless "Syncing…" with all sync buttons permanently disabled until reload. Wrap the wait in a timeout and treat a timeout as a sync failure (clear the in-flight flag and set `operation.error`).</violation>
</file>

<file name="frontend/src/core/packages/useInstallPackage.ts">

<violation number="1" location="frontend/src/core/packages/useInstallPackage.ts:40">
P3: When a package install succeeds but the backend responds with restartRequired (success=false, restart_required=true, e.g. sandbox sync), onSuccess is no longer invoked, while it was before this change. Since restartRequired still means the packages were saved successfully, callers like packages-panel.tsx that clear the input on success will now leave the stale package text in the field after a "Changes saved — restart required" install. If that is unintended, invoke onSuccess?.() in the restartRequired branch as well; if it is deliberate, the asymmetry between the two success-like paths is worth a comment.</violation>
</file>

<file name="marimo/_environments/script_metadata.py">

<violation number="1" location="marimo/_environments/script_metadata.py:117">
P2: `write_manifest` does a read-modify-write of the whole notebook file with no lock against the running kernel, so a concurrent notebook autosave can be clobbered. The `_stable_carrier_lock` serializes manifest writers among themselves, but the kernel's save path does not take it; if the kernel writes the file between `source.read()` and `destination.write()`, the freshly read `script` (with cells from before the autosave) is written back and the autosaved cell edits are lost. Replace the in-place truncating write with an atomic write (temp file + `os.replace`) to at least avoid torn/interleaved writes.</violation>
</file>

<file name="frontend/src/core/websocket/useMarimoKernelConnection.tsx">

<violation number="1" location="frontend/src/core/websocket/useMarimoKernelConnection.tsx:597">
P3: A transient close during the initial startup CONNECTING phase drops the phase. When previous.state is CONNECTING with phase 'preparing-environment'/'starting-kernel' (not 'reconnecting'), the CONNECTING branch falls through to plain `{ state: CONNECTING }`, so connection-notice.ts shows the generic 'Connecting…' title and a later startup failure loses the phase needed to label it 'Sandbox setup failed'. Treat any CONNECTING→CONNECTING retry close as reconnecting, or preserve the existing phase.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

variant="text"
size="xs"
disabled={!actions}
onClick={() => actions?.sync()}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When sandbox environment preparation succeeds but the kernel then fails to start, this Retry sync action only reapplies the manifest and leaves the notebook disconnected because syncSandbox does not request a reconnect for an existing sandbox. Route this recovery action through the kernel reconnect path, or only show it for actual sandbox-sync failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/editor/chrome/panels/sandbox-panel.tsx, line 80:

<comment>When sandbox environment preparation succeeds but the kernel then fails to start, this `Retry sync` action only reapplies the manifest and leaves the notebook disconnected because `syncSandbox` does not request a reconnect for an existing sandbox. Route this recovery action through the kernel reconnect path, or only show it for actual sandbox-sync failures.</comment>

<file context>
@@ -0,0 +1,173 @@
+              variant="text"
+              size="xs"
+              disabled={!actions}
+              onClick={() => actions?.sync()}
+            >
+              Retry sync
</file context>

if isinstance(sandbox, NotebookSandbox):
return sandbox, sandbox.source, sandbox.backend
return None, None, None
if not manager.sandbox:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When the server is running in single-process sandbox mode, manager.sandbox is false even though GLOBAL_SETTINGS.SANDBOX_MODE is "single". This branch makes all pre-start sandbox responses empty and prevents the UI from reading or editing the manifest before kernel startup; gate on the configured sandbox mode instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At marimo/_server/api/endpoints/packages.py, line 280:

<comment>When the server is running in single-process sandbox mode, `manager.sandbox` is false even though `GLOBAL_SETTINGS.SANDBOX_MODE` is `"single"`. This branch makes all pre-start sandbox responses empty and prevents the UI from reading or editing the manifest before kernel startup; gate on the configured sandbox mode instead.</comment>

<file context>
@@ -249,3 +260,142 @@ def _get_filename(request: Request) -> str | None:
+        if isinstance(sandbox, NotebookSandbox):
+            return sandbox, sandbox.source, sandbox.backend
+        return None, None, None
+    if not manager.sandbox:
+        return None, None, None
+    key = file_key or manager.workspace.get_unique_file_key()
</file context>
Suggested change
if not manager.sandbox:
if GLOBAL_SETTINGS.SANDBOX_MODE is None:

Comment thread frontend/src/__mocks__/requests.ts Outdated
Comment on lines +15 to +16
(sync.pending || sync.error) &&
connection.state === WebSocketState.OPEN

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a sandbox sync fails while the connection is CLOSED, this OPEN guard skips sync.error and replaces the diagnostic with the generic connection.reason. Let sync.error take precedence regardless of connection state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/core/network/connection-notice.ts, line 15:

<comment>When a sandbox sync fails while the connection is `CLOSED`, this `OPEN` guard skips `sync.error` and replaces the diagnostic with the generic `connection.reason`. Let `sync.error` take precedence regardless of connection state.</comment>

<file context>
@@ -0,0 +1,84 @@
+  const sync = get(sandboxSyncAtom);
+  if (
+    sandbox &&
+    (sync.pending || sync.error) &&
+    connection.state === WebSocketState.OPEN
+  ) {
</file context>
Suggested change
(sync.pending || sync.error) &&
connection.state === WebSocketState.OPEN
(sync.error ||
(sync.pending && connection.state === WebSocketState.OPEN))

<div className="flex-1 flex flex-col overflow-hidden">
<InstallPackageForm context={dependencies.context} onSuccess={refetch} />
{(isTreeSupported || isSandbox) && (
<fieldset disabled={syncing} className="contents">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: While a sandbox sync is pending, this fieldset disables only package installation; UpgradeButton and RemoveButton remain clickable outside it. Disable those mutation controls during sync to prevent package changes racing with manifest application.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/editor/chrome/panels/packages-panel.tsx, line 170:

<comment>While a sandbox sync is pending, this fieldset disables only package installation; `UpgradeButton` and `RemoveButton` remain clickable outside it. Disable those mutation controls during sync to prevent package changes racing with manifest application.</comment>

<file context>
@@ -122,58 +163,49 @@ const PackagesPanel: React.FC = () => {
     <div className="flex-1 flex flex-col overflow-hidden">
-      <InstallPackageForm context={dependencies.context} onSuccess={refetch} />
-      {(isTreeSupported || isSandbox) && (
+      <fieldset disabled={syncing} className="contents">
+        <InstallPackageForm context={dependencies.context} />
+      </fieldset>
</file context>

}, [requests, filename, connection.state, setSandbox]);

const sync = useEvent(async () => {
if (inFlight.current || connection.state === WebSocketState.CONNECTING) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: sync() returns false without setting any error when the connection is WebSocketState.CONNECTING or when the websocket ends CLOSED after reconnect. The visible consequence: in SandboxStartupPanel the "Retry sync" button is not disabled during startup (disabled={!actions} only), so clicking it during environment preparation silently does nothing, and in saveAndSync a reconnect that ends CLOSED silently aborts with the dialog left open and no diagnostic in the dialog itself (only the sidebar notice). Set an error (or disable the button while the connection is CONNECTING) on these false paths so the UI explains why nothing happened.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/editor/chrome/panels/sandbox-controller.tsx, line 85:

<comment>`sync()` returns `false` without setting any error when the connection is `WebSocketState.CONNECTING` or when the websocket ends `CLOSED` after reconnect. The visible consequence: in `SandboxStartupPanel` the "Retry sync" button is not disabled during startup (`disabled={!actions}` only), so clicking it during environment preparation silently does nothing, and in `saveAndSync` a reconnect that ends CLOSED silently aborts with the dialog left open and no diagnostic in the dialog itself (only the sidebar notice). Set an error (or disable the button while the connection is CONNECTING) on these false paths so the UI explains why nothing happened.</comment>

<file context>
@@ -0,0 +1,257 @@
+  }, [requests, filename, connection.state, setSandbox]);
+
+  const sync = useEvent(async () => {
+    if (inFlight.current || connection.state === WebSocketState.CONNECTING) {
+      return false;
+    }
</file context>

Comment thread frontend/src/components/editor/header/status.tsx
showPackageRestartToast();
} else if (response.success) {
showAddPackageToast(packages);
onSuccess?.();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: When a package install succeeds but the backend responds with restartRequired (success=false, restart_required=true, e.g. sandbox sync), onSuccess is no longer invoked, while it was before this change. Since restartRequired still means the packages were saved successfully, callers like packages-panel.tsx that clear the input on success will now leave the stale package text in the field after a "Changes saved — restart required" install. If that is unintended, invoke onSuccess?.() in the restartRequired branch as well; if it is deliberate, the asymmetry between the two success-like paths is worth a comment.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/core/packages/useInstallPackage.ts, line 40:

<comment>When a package install succeeds but the backend responds with restartRequired (success=false, restart_required=true, e.g. sandbox sync), onSuccess is no longer invoked, while it was before this change. Since restartRequired still means the packages were saved successfully, callers like packages-panel.tsx that clear the input on success will now leave the stale package text in the field after a "Changes saved — restart required" install. If that is unintended, invoke onSuccess?.() in the restartRequired branch as well; if it is deliberate, the asymmetry between the two success-like paths is worth a comment.</comment>

<file context>
@@ -37,10 +37,10 @@ export function useInstallPackages(): {
         showPackageRestartToast();
       } else if (response.success) {
         showAddPackageToast(packages);
+        onSuccess?.();
       } else {
         showAddPackageToast(packages, response.error);
</file context>

Comment thread marimo/_environments/script_metadata.py Outdated
Comment on lines +597 to +602
(previous.state === WebSocketState.CONNECTING &&
previous.phase === "reconnecting"))
) {
return { ...status, phase: "reconnecting" };
}
return status;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: A transient close during the initial startup CONNECTING phase drops the phase. When previous.state is CONNECTING with phase 'preparing-environment'/'starting-kernel' (not 'reconnecting'), the CONNECTING branch falls through to plain { state: CONNECTING }, so connection-notice.ts shows the generic 'Connecting…' title and a later startup failure loses the phase needed to label it 'Sandbox setup failed'. Treat any CONNECTING→CONNECTING retry close as reconnecting, or preserve the existing phase.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/core/websocket/useMarimoKernelConnection.tsx, line 597:

<comment>A transient close during the initial startup CONNECTING phase drops the phase. When previous.state is CONNECTING with phase 'preparing-environment'/'starting-kernel' (not 'reconnecting'), the CONNECTING branch falls through to plain `{ state: CONNECTING }`, so connection-notice.ts shows the generic 'Connecting…' title and a later startup failure loses the phase needed to label it 'Sandbox setup failed'. Treat any CONNECTING→CONNECTING retry close as reconnecting, or preserve the existing phase.</comment>

<file context>
@@ -564,7 +581,26 @@ export function useMarimoKernelConnection(opts: {
+        if (
+          status.state === WebSocketState.CONNECTING &&
+          (previous.state === WebSocketState.OPEN ||
+            (previous.state === WebSocketState.CONNECTING &&
+              previous.phase === "reconnecting"))
+        ) {
</file context>
Suggested change
(previous.state === WebSocketState.CONNECTING &&
previous.phase === "reconnecting"))
) {
return { ...status, phase: "reconnecting" };
}
return status;
if (
status.state === WebSocketState.CONNECTING &&
(previous.state === WebSocketState.OPEN ||
previous.state === WebSocketState.CONNECTING)
) {
return { ...status, phase: "reconnecting" };
}
return status;

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate findings affect sandbox synchronization, notebook isolation, startup guards, and manifest handling.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds asynchronous sandbox startup and recovery, pre-kernel manifest APIs, conflict-safe editing, and package-panel state handling.

Changes:

  • Added sandbox manifest and synchronization APIs.
  • Added conflict-safe manifest editing and recovery workflows.
  • Added frontend startup, package, connection, and testing updates.
File summaries
File Review summary
tests/_server/api/endpoints/test_packages.py No final comment.
tests/_environments/test_script_metadata.py No final comment.
tests/_environments/test_sandbox_interface.py No final comment.
tests/_environments/test_async_preparation.py No final comment.
packages/openapi/src/api.ts No final comment.
packages/openapi/api.yaml No final comment.
marimo/_utils/inline_script_metadata.py Moderate (1 vote): Check for the exact # /// script marker rather than a prefix to avoid misclassifying raw TOML comments.
marimo/_server/session_manager.py No final comment.
marimo/_server/models/packages.py No final comment.
marimo/_server/api/endpoints/packages.py Critical (1 vote): Guard pending mutations by notebook file rather than session ID.
Moderate (1 vote): Mirror or exclude explicitly configured virtual environments in the pre-session branch.
Moderate (1 vote): Map OSError and PermissionError from manifest writes to an operation error.
marimo/_environments/script_metadata.py Critical (2 votes): Reject already-commented script markers before wrapping to prevent nested blocks and manifest corruption.
marimo/_environments/sandbox.py Moderate (1 vote): Keep runtime-managed marimo display-only and suppress its upgrade action.
marimo/_environments/backends.py No final comment.
marimo/_cli/development/commands.py No final comment.
frontend/src/core/websocket/useMarimoKernelConnection.tsx No final comment.
frontend/src/core/websocket/types.ts No final comment.
frontend/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx No final comment.
frontend/src/core/wasm/bridge.ts No final comment.
frontend/src/core/run-app.tsx No final comment.
frontend/src/core/packages/useInstallPackage.ts No final comment.
frontend/src/core/packages/toast-components.tsx No final comment.
frontend/src/core/packages/sandbox-state.ts No final comment.
frontend/src/core/packages/package-data.ts No final comment.
frontend/src/core/network/types.ts No final comment.
frontend/src/core/network/resolve.ts No final comment.
frontend/src/core/network/requests-toasting.tsx No final comment.
frontend/src/core/network/requests-static.ts No final comment.
frontend/src/core/network/requests-network.ts No final comment.
frontend/src/core/network/requests-lazy.ts No final comment.
frontend/src/core/network/connection-notice.ts No final comment.
frontend/src/core/MarimoApp.tsx No final comment.
frontend/src/core/islands/bridge.ts No final comment.
frontend/src/core/errors/state.ts No final comment.
frontend/src/core/edit-app.tsx No final comment.
frontend/src/components/editor/renderers/cell-array.tsx No final comment.
frontend/src/components/editor/header/status.tsx No final comment.
frontend/src/components/editor/header/__tests__/status.test.tsx No final comment.
frontend/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx No final comment.
frontend/src/components/editor/chrome/panels/sandbox-panel.tsx Moderate (1 vote): Disable manifest editing while startup is pending.
frontend/src/components/editor/chrome/panels/sandbox-controller.tsx Critical (1 vote): Associate draft state with the current filename to prevent cross-notebook edits.
Moderate (1 vote): Clear or key global sandbox state by filename to prevent stale metadata.
frontend/src/components/editor/chrome/panels/packages-panel.tsx Critical (1 vote): Disable dependency-tree Upgrade and Remove actions during synchronization or serialize sandbox mutations.
frontend/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx No final comment.
frontend/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx No final comment.
frontend/src/components/editor/app-container.tsx No final comment.
frontend/src/components/editor/alerts/connection-notice.tsx No final comment.
frontend/src/components/editor/alerts/connecting-alert.tsx No final comment.
frontend/src/__mocks__/requests.ts No final comment.
Review details

Suppressed comments (6)

frontend/src/components/editor/chrome/panels/sandbox-controller.tsx:75

  • sandboxAtom is global and is only updated after this request resolves, so it retains the previous notebook's manifest/backend while filename has already changed. The Packages panel and connection notice can therefore show stale sandbox metadata (and expose edit/sync actions for it); if this request fails, the stale value can remain indefinitely. Clear or key the atom by filename and only render metadata matching the current notebook.
    let cancelled = false;
    requests
      .getSandbox({ fileKey: filename })
      .then((value) => {
        if (!cancelled) {
          setSandbox(value);
        }
      })

frontend/src/components/editor/chrome/panels/sandbox-panel.tsx:161

  • While startup is pending, this item remains enabled even though both manifest mutation endpoints reject _sandbox_source(..., mutation=True) with 409. Clicking Edit manifest from the footer therefore opens an editor that cannot save and is reported as a stale-manifest conflict; disable it during pending, as the Sync item already does.
            disabled={!actions || sandbox.manifest === null}

marimo/_environments/sandbox.py:489

  • Returning the runtime marimo package here makes it a top-level actionable node in packages-panel.tsx: the tree renders UpgradeButton for every top-level package, while only RemoveButton suppresses marimo. Clicking Upgrade therefore calls sandbox.add("marimo", upgrade=true), which can add marimo to the notebook manifest even though the sandbox treats it as runtime-managed and explicitly forbids removing it. Keep runtime-only packages display-only or suppress the upgrade action for marimo.
    marimo/_server/api/endpoints/packages.py:284
  • This pre-session branch treats every manager.sandbox session as an ephemeral script sandbox, but IPCKernelManagerImpl.start_kernel gives an explicitly configured [tool.marimo.venv] precedence and strips the sandbox metadata routing. Before the kernel starts, this endpoint therefore advertises a manifest/editor for a runtime that will not use it, then changes to backend: null once the session exists. Exclude configured-venv sessions here or mirror the kernel's sandbox selection.
    marimo/_server/api/endpoints/packages.py:357
  • write_manifest performs direct file writes for Python notebooks and can raise OSError/PermissionError (frontmatter commits can also do so), but this endpoint does not catch it. A read-only or otherwise unwritable notebook therefore escapes as a 500 instead of returning the manifest-edit error that the recovery UI can display; the sync handler below already treats OSError as an operation failure. Include OSError in this exception mapping.
    marimo/_utils/inline_script_metadata.py:411
  • This prefix check also treats valid raw TOML beginning with a comment such as # /// script-not-a-block as an already wrapped PEP 723 block. The frontmatter is then returned without wrap_block, so the metadata reader will not recognize its dependencies; check the first line for the exact # /// script marker instead of using a prefix.
  • Files reviewed: 47/47 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +170 to +172
<fieldset disabled={syncing} className="contents">
<InstallPackageForm context={dependencies.context} />
</fieldset>
Comment on lines +52 to +55
const [document, setDocument] = useState<{
original: string;
draft: string;
} | null>(null);
Comment thread marimo/_environments/script_metadata.py Outdated
def write_manifest(path: str, contents: str, *, previous: str) -> str:
"""Replace only metadata, rejecting stale edits and invalid TOML."""
toml_reader.reads(contents)
if any(line.startswith("///") for line in contents.splitlines()):
Comment on lines +270 to +272
if mutation and manager.is_session_starting(
state.require_current_session_id()
):
@codecov

codecov Bot commented Sep 14, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 15.12kB (0.06%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
marimo-esm 26.5MB 15.12kB (0.06%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: marimo-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/terminal-*.js -5 bytes 449.75kB -0.0%
assets/index-*.js -1.9kB 449.28kB -0.42%
assets/index-*.css 1.02kB 380.65kB 0.27%
assets/edit-*.js 4.47kB 327.74kB 1.38%
assets/reveal-*.js -5 bytes 257.01kB -0.0%
assets/layout-*.js 6 bytes 207.6kB 0.0%
assets/file-*.js -263 bytes 2.61kB -9.17%
assets/file-*.js -105.32kB 372 bytes -99.65%
assets/file-*.js (New) 105.69kB 105.69kB 100.0% 🚀
assets/useNotebookActions-*.js 33 bytes 64.81kB 0.05%
assets/panels-*.js 192 bytes 49.13kB 0.39%
assets/state-*.js -145 bytes 432 bytes -25.13%
assets/state-*.js -32.94kB 577 bytes -98.28%
assets/state-*.js (New) 33.38kB 33.38kB 100.0% 🚀
assets/packages-*.js 946 bytes 18.77kB 5.31% ⚠️
assets/run-*.js 3.12kB 13.9kB 28.89% ⚠️
assets/config-*.js -43 bytes 7.76kB -0.55%
assets/sandbox-*.js (New) 6.37kB 6.37kB 100.0% 🚀
assets/markdown-*.js -264 bytes 146 bytes -64.39%
assets/useInstallPackage-*.js 207 bytes 3.94kB 5.54% ⚠️
assets/package-*.js -78 bytes 294 bytes -20.97%
assets/package-*.js (New) 372 bytes 372 bytes 100.0% 🚀
assets/house-*.js (New) 281 bytes 281 bytes 100.0% 🚀

Files in assets/index-*.js:

  • ./src/core/MarimoApp.tsx → Total Size: 3.88kB

  • ./src/core/network/requests-toasting.tsx → Total Size: 5.43kB

  • ./src/core/network/requests-lazy.ts → Total Size: 4.13kB

  • ./src/core/network/resolve.ts → Total Size: 358 bytes

  • ./src/core/network/requests-network.ts → Total Size: 13.67kB

  • ./src/core/network/requests-static.ts → Total Size: 3.75kB

Files in assets/edit-*.js:

  • ./src/components/editor/alerts/connection-notice.tsx → Total Size: 6.35kB

  • ./src/components/editor/chrome/panels/sandbox-controller.tsx → Total Size: 6.51kB

  • ./src/components/editor/chrome/wrapper/footer-items/backend-status.tsx → Total Size: 5.51kB

  • ./src/core/edit-app.tsx → Total Size: 9.82kB

  • ./src/components/editor/alerts/connecting-alert.tsx → Total Size: 1.17kB

  • ./src/components/editor/renderers/cell-array.tsx → Total Size: 14.42kB

Files in assets/panels-*.js:

  • ./src/components/editor/header/status.tsx → Total Size: 4.92kB

  • ./src/components/editor/app-container.tsx → Total Size: 2.1kB

Files in assets/state-*.js:

  • ./src/core/errors/state.ts → Total Size: 651 bytes

Files in assets/state-*.js:

  • ./src/core/wasm/bridge.ts → Total Size: 13.31kB

Files in assets/packages-*.js:

  • ./src/components/editor/chrome/panels/packages-panel.tsx → Total Size: 32.95kB

Files in assets/run-*.js:

  • ./src/core/run-app.tsx → Total Size: 4.11kB

Files in assets/sandbox-*.js:

  • ./src/components/editor/chrome/panels/sandbox-panel.tsx → Total Size: 8.01kB

  • ./src/core/network/connection-notice.ts → Total Size: 2.21kB

  • ./src/core/packages/sandbox-state.ts → Total Size: 246 bytes

Files in assets/useInstallPackage-*.js:

  • ./src/core/packages/toast-components.tsx → Total Size: 3.79kB

  • ./src/core/packages/useInstallPackage.ts → Total Size: 709 bytes

Files in assets/package-*.js:

  • ./src/core/packages/package-data.ts → Total Size: 574 bytes

mscolnick
mscolnick previously approved these changes Sep 15, 2026
manzt and others added 5 commits September 15, 2026 11:05
Show startup progress after 500 ms, with layouts for empty notebooks
and notebooks with existing cells. Present failures immediately and
link to diagnostics and recovery actions in Packages.

Let users edit the full manifest and sync without leaving the notebook.
Keep drafts through failures and stale saves; close the editor only
after successful sync. Refresh package data after mutations, include
marimo in the package view, and show sandbox state beside its actions.

Keep healthy footers quiet and retain the connecting indicator. Cover
startup timing, manifest recovery, stale drafts, and package refreshes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bash-focus Area to focus on during release bug bash enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants