Skip to content

feat(sandbox): add provider dashboard links#654

Open
hreiten wants to merge 1 commit into
ColeMurray:mainfrom
watchdog-no:feat/sandbox-dashboard-url
Open

feat(sandbox): add provider dashboard links#654
hreiten wants to merge 1 commit into
ColeMurray:mainfrom
watchdog-no:feat/sandbox-dashboard-url

Conversation

@hreiten
Copy link
Copy Markdown
Contributor

@hreiten hreiten commented May 18, 2026

Summary

  • add a provider-neutral sandbox_dashboard_url server message and session state field
  • broadcast the dashboard URL after a sandbox provider object ID is persisted on spawn, restore, or persistent resume
  • render the desktop sandbox status as an external provider-dashboard link when a URL is available

Why

Modal exposes useful per-sandbox diagnostics in its dashboard, but the web UI currently has no way to link users from a session to the corresponding provider sandbox. This makes debugging stuck, slow, or failed sandboxes harder than it needs to be.

The implementation keeps the shared protocol provider-neutral and confines Modal URL construction to the control-plane Modal path. Existing connected clients receive the URL as soon as the provider object ID is known, while newly subscribed clients receive it through the hydrated session state.

Prior upstream work checked

  • refreshed upstream/main before opening this PR; this branch is one commit ahead and zero commits behind
  • checked upstream/main for sandboxDashboardUrl, sandbox_dashboard_url, and modal_sandbox_url; no existing implementation was present
  • searched existing upstream PRs for sandbox/dashboard/Modal dashboard terms; no matching fix for this URL plumbing was found
  • noted open PR fix(modal): Support non-main envs in Modal #629 for non-main Modal environments; this PR defaults the Modal dashboard environment to main and does not add new Terraform/env wiring

Details

  • buildModalSandboxDashboardUrl returns null unless the required Modal workspace and provider object ID are present.
  • SandboxLifecycleManager centralizes provider object ID persistence and broadcasts the dashboard URL only when configured.
  • SessionDO wires the builder only for the Modal backend.
  • The web socket hook clears stale dashboard URLs when a replacement sandbox starts or the current sandbox reaches a terminal state.

Tests

  • npm run build -w @open-inspect/shared
  • npm test -w @open-inspect/control-plane
  • npm test -w @open-inspect/web
  • npm run typecheck -w @open-inspect/control-plane
  • npm run typecheck -w @open-inspect/web
  • npm run lint
  • npm run typecheck
  • npm test
  • git diff --check

Summary by CodeRabbit

  • New Features

    • Session header now shows a clickable link to the sandbox provider dashboard when available; session state includes an optional dashboard URL from the server and suppresses it for blocked statuses.
  • Tests

    • Added tests for dashboard URL construction, websocket delivery, session-state hydration, and clearing/updating of sandbox access across spawn, restore, resume, and error flows.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd5dc085-aaeb-4a67-a17d-0cf651d44164

📥 Commits

Reviewing files that changed from the base of the PR and between 883369c and 1ce5a29.

📒 Files selected for processing (10)
  • packages/control-plane/src/sandbox/client.test.ts
  • packages/control-plane/src/sandbox/client.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.test.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/control-plane/test/integration/websocket-client.test.ts
  • packages/shared/src/types/index.ts
  • packages/web/src/app/(app)/session/[id]/page.tsx
  • packages/web/src/hooks/use-session-socket.test.tsx
  • packages/web/src/hooks/use-session-socket.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/control-plane/src/sandbox/client.ts
  • packages/shared/src/types/index.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.ts
  • packages/web/src/hooks/use-session-socket.test.tsx
  • packages/web/src/hooks/use-session-socket.ts
  • packages/web/src/app/(app)/session/[id]/page.tsx
  • packages/control-plane/src/sandbox/lifecycle/manager.test.ts
  • packages/control-plane/src/sandbox/client.test.ts

📝 Walkthrough

Walkthrough

Adds a Modal sandbox dashboard URL builder, persists and broadcasts dashboard URLs from the lifecycle manager during spawn/restore/resume, extends shared types and session state, updates WebSocket handling to set/clear the URL, and renders it as an external link in the SandboxStatus UI.

Changes

Sandbox Dashboard URL Display

Layer / File(s) Summary
URL builder contract and tests
packages/control-plane/src/sandbox/client.ts, packages/control-plane/src/sandbox/client.test.ts
DEFAULT_MODAL_DASHBOARD_ENVIRONMENT and exported buildModalSandboxDashboardUrl construct Modal dashboard URLs from workspace, environment, and provider object ID; returns null when inputs are missing. Tests cover default env, env override, and null cases.
Lifecycle manager broadcasting
packages/control-plane/src/sandbox/lifecycle/manager.ts, packages/control-plane/src/sandbox/lifecycle/manager.test.ts
SandboxLifecycleConfig adds optional sandboxDashboardUrlBuilder; manager uses storeAndBroadcastProviderObjectId during spawn, restore, and resume to persist modal object id and conditionally broadcast sandbox_dashboard_url. Tests verify broadcasting across lifecycle paths and resume mocking.
Type contract and session state integration
packages/shared/src/types/index.ts, packages/control-plane/src/session/durable-object.ts
ServerMessage adds sandbox_dashboard_url; SessionState adds sandboxDashboardUrl. SessionDO wires a modal-only builder via getSandboxDashboardUrl() and includes sandboxDashboardUrl in session state, suppressing URLs for blocked statuses.
Client socket handling and tests
packages/web/src/hooks/use-session-socket.ts, packages/web/src/hooks/use-session-socket.test.tsx, packages/control-plane/test/integration/websocket-client.test.ts
useSessionSocket introduces CLEARED_SANDBOX_ACCESS_STATE (includes sandboxDashboardUrl), handles sandbox_dashboard_url messages to set sessionState.sandboxDashboardUrl, and clears access fields on spawning/status/error transitions. Unit and integration tests validate message handling and clearing behavior.
SandboxStatus component link rendering
packages/web/src/app/(app)/session/[id]/page.tsx
Page now passes status and dashboardUrl to SandboxStatus; the component accepts dashboardUrl and conditionally renders an external anchor when present, otherwise a span.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • ColeMurray

Poem

🐇 I stitched a dashboard link, neat and spry,
From spawn to resume it hops by and by,
A WebSocket whisper, a quick little cheer,
The status now opens the sandbox frontier,
hops — celebrate the URL in the sky!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding provider dashboard links to the sandbox feature, which aligns with the primary objective of the PR across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web/src/hooks/use-session-socket.ts (1)

391-409: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear stale sandboxDashboardUrl on all spawn/failure transitions, not only sandbox_spawning + terminal status messages.

SandboxLifecycleManager broadcasts sandbox_status: "spawning" on new spawn/restore and may emit only sandbox_error on failures. In those paths, this reducer can keep an old dashboard URL visible.

Suggested fix
         case "sandbox_status": {
-          const isTerminal =
-            data.status === "stale" || data.status === "stopped" || data.status === "failed";
+          const shouldClearAccessState =
+            data.status === "spawning" ||
+            data.status === "stale" ||
+            data.status === "stopped" ||
+            data.status === "failed";
           setSessionState((prev) =>
             prev
               ? {
                   ...prev,
                   sandboxStatus: data.status,
-                  ...(isTerminal && {
+                  ...(shouldClearAccessState && {
                     codeServerUrl: undefined,
                     codeServerPassword: undefined,
                     tunnelUrls: undefined,
                     ttydUrl: undefined,
                     ttydToken: undefined,
                     sandboxDashboardUrl: undefined,
                   }),
                 }
               : null
           );
           break;
         }
@@
         case "sandbox_error":
           console.error("Sandbox error:", data.error);
-          setSessionState((prev) => (prev ? { ...prev, sandboxStatus: "failed" } : null));
+          setSessionState((prev) =>
+            prev
+              ? {
+                  ...prev,
+                  sandboxStatus: "failed",
+                  codeServerUrl: undefined,
+                  codeServerPassword: undefined,
+                  tunnelUrls: undefined,
+                  ttydUrl: undefined,
+                  ttydToken: undefined,
+                  sandboxDashboardUrl: undefined,
+                }
+              : null
+          );
           break;

Also applies to: 478-481

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/hooks/use-session-socket.ts` around lines 391 - 409, Update
the reducer logic in the socket handler so the old sandboxDashboardUrl is
cleared on spawn and failure transitions: in the "sandbox_status" case (the
setSessionState updater) include sandboxDashboardUrl: undefined whenever
data.status === "spawning" or when the existing isTerminal branch runs (status
in "stale","stopped","failed"); likewise, in the "sandbox_error" handler (the
other reducer block around lines ~478-481) add sandboxDashboardUrl: undefined to
the fields that are cleared so any dashboard URL is removed on error-only
failure paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/web/src/hooks/use-session-socket.ts`:
- Around line 391-409: Update the reducer logic in the socket handler so the old
sandboxDashboardUrl is cleared on spawn and failure transitions: in the
"sandbox_status" case (the setSessionState updater) include sandboxDashboardUrl:
undefined whenever data.status === "spawning" or when the existing isTerminal
branch runs (status in "stale","stopped","failed"); likewise, in the
"sandbox_error" handler (the other reducer block around lines ~478-481) add
sandboxDashboardUrl: undefined to the fields that are cleared so any dashboard
URL is removed on error-only failure paths.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a1513d7c-cbe4-4432-bab0-c2e6f1772014

📥 Commits

Reviewing files that changed from the base of the PR and between a8a14c4 and 8a4a6ec.

📒 Files selected for processing (9)
  • packages/control-plane/src/sandbox/client.test.ts
  • packages/control-plane/src/sandbox/client.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.test.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/shared/src/types/index.ts
  • packages/web/src/app/(app)/session/[id]/page.tsx
  • packages/web/src/hooks/use-session-socket.test.tsx
  • packages/web/src/hooks/use-session-socket.ts

@hreiten hreiten force-pushed the feat/sandbox-dashboard-url branch from 8a4a6ec to 883369c Compare May 18, 2026 15:38
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/web/src/hooks/use-session-socket.test.tsx (1)

486-491: ⚡ Quick win

Consider waiting for URLs to be set before asserting they're cleared.

This test combines socket open, subscription, access messages, and spawning status in a single act() call without verifying that the URLs were actually set before being cleared. While the test would fail if clearing logic is broken, it's less clear and robust than the previous test (lines 443-454), which explicitly waits for the URLs to be set before clearing them.

♻️ Suggested test structure for improved clarity
 act(() => {
   socket.open();
   socket.receive(createSubscribedMessage());
   sendSandboxAccessMessages(socket, "old-sandbox");
+});
+
+await waitFor(() => {
+  expect(result.current.sessionState?.sandboxDashboardUrl).toBe(
+    "https://provider.example/old-sandbox"
+  );
+  expect(result.current.sessionState?.codeServerUrl).toBe("https://code.example/old-sandbox");
+});
+
+act(() => {
   socket.receive({ type: "sandbox_status", status: "spawning" });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/hooks/use-session-socket.test.tsx` around lines 486 - 491,
Split the combined act() into two steps so the test first opens the socket and
sends subscription/access messages and then waits for the URLs to be set before
triggering the spawning status; specifically call socket.open(),
socket.receive(createSubscribedMessage()), and sendSandboxAccessMessages(socket,
"old-sandbox") inside the first act(), then use an async wait (e.g. waitFor or
findBy* against whatever component state/selectors expose the sandbox URLs) to
assert the URLs were set, and only after that call socket.receive({ type:
"sandbox_status", status: "spawning" }) in a second act() and assert the URLs
were cleared. Ensure the test references the same helpers (socket.open,
createSubscribedMessage, sendSandboxAccessMessages) so the sequence remains
identical but with an explicit wait between setting and clearing assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/web/src/hooks/use-session-socket.test.tsx`:
- Around line 486-491: Split the combined act() into two steps so the test first
opens the socket and sends subscription/access messages and then waits for the
URLs to be set before triggering the spawning status; specifically call
socket.open(), socket.receive(createSubscribedMessage()), and
sendSandboxAccessMessages(socket, "old-sandbox") inside the first act(), then
use an async wait (e.g. waitFor or findBy* against whatever component
state/selectors expose the sandbox URLs) to assert the URLs were set, and only
after that call socket.receive({ type: "sandbox_status", status: "spawning" })
in a second act() and assert the URLs were cleared. Ensure the test references
the same helpers (socket.open, createSubscribedMessage,
sendSandboxAccessMessages) so the sequence remains identical but with an
explicit wait between setting and clearing assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b728a824-4af8-4d05-9efb-8f2ae9f4628e

📥 Commits

Reviewing files that changed from the base of the PR and between 8a4a6ec and 883369c.

📒 Files selected for processing (9)
  • packages/control-plane/src/sandbox/client.test.ts
  • packages/control-plane/src/sandbox/client.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.test.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/shared/src/types/index.ts
  • packages/web/src/app/(app)/session/[id]/page.tsx
  • packages/web/src/hooks/use-session-socket.test.tsx
  • packages/web/src/hooks/use-session-socket.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/control-plane/src/sandbox/client.test.ts
  • packages/shared/src/types/index.ts
  • packages/control-plane/src/sandbox/client.ts
  • packages/web/src/app/(app)/session/[id]/page.tsx
  • packages/web/src/hooks/use-session-socket.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.test.ts
  • packages/control-plane/src/session/durable-object.ts
  • packages/control-plane/src/sandbox/lifecycle/manager.ts

@hreiten hreiten force-pushed the feat/sandbox-dashboard-url branch from 883369c to 1ce5a29 Compare May 18, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant