Skip to content

demo: drive Studio against MSW via endpoint swap (no /demo route)#15

Merged
cooper (czxtm) merged 5 commits intomainfrom
claude/demo-via-project-swap
Apr 29, 2026
Merged

demo: drive Studio against MSW via endpoint swap (no /demo route)#15
cooper (czxtm) merged 5 commits intomainfrom
claude/demo-via-project-swap

Conversation

@czxtm
Copy link
Copy Markdown
Contributor

Summary

Replaces the dedicated /demo route + hand-curated UI with a runtime endpoint swap: the real Studio now points at an in-browser MSW agent when the user opts into demo mode. Avoids drift between a parallel demo and the live Studio.

Builds on PR #13's MSW worker/handlers/fixture, supersedes the static /demo/* UI from PR #14.

Architecture

  • AgentEndpointProvider (lib/agent-endpoint.tsx) — root-level provider owning { host, port, token, isDemo }. Persists demo selection to localStorage and boots MSW before flipping the endpoint so in-flight requests don't 404
  • routes/studio.tsx — re-keys AgentSSEProvider/AgentProvider on every endpoint change so EventSource/timer state doesn't leak across local↔demo. Suppresses the pairing overlay and renders <DemoBanner> in demo mode
  • routes/demo.tsx — now a tiny redirect: useDemo() then bounce to /studio/dashboard. Marketing CTAs keep working with zero changes
  • <AgentConnect> — adds a prominent "Try the Demo" button in the no-agent-running empty state. First-time visitors with no CLI install have an obvious escape hatch
  • <ProjectSelector> — detects demo mode and renders a read-only badge with an "exit demo" affordance instead of querying tRPC (cloud bounce can't reach a fake host)

MSW expansion

  • Handlers now cover /api/project/{list,current,open,validate,close,remove} so <ProjectProvider> resolves a single synthetic project on mount, plus /api/process-compose/processes so the overview's process card has data
  • New demoProject and demoProcessComposeProcesses fixtures
  • Adds apps/web/public/mockServiceWorker.js (msw v2.7) — PR Prototype /demo route backed by an MSW-mocked agent #13 forgot this and MSW registration would have failed at runtime

Cleanup

Drops the parallel /demo/{apps,files,index,network,services,variables} static routes and /components/demo/{demo-fixtures,demo-header,demo-sidebar} chrome (~1.9k LOC). Updates routeTree.gen.ts in lockstep so the build stays green without a router-codegen pass.

Net diff: +829, −2220 lines across 21 files.

User flows

  1. Landing → demo — click any "Try the demo" CTA → /demo redirect → MSW boots → /studio/dashboard with demo banner
  2. Studio (no agent) → demo<AgentConnect> shows "Try the Demo" button → endpoint swap → studio remounts in demo mode
  3. Demo → exit — banner or project-picker "Exit demo" → endpoint swap back to local → MSW worker stops
  4. Refresh in demolocalStorage restores demo, useEffect re-boots the worker, pairing overlay stays hidden

Follow-ups (filing as bd issues)

  • PR B: Generate MSW fixtures from proto-nix example fields (nix/stackpanel/db/lib/field.nix infra is already in place; only one schema currently populates example). Will let us delete most hand-written handlers
  • Pre-existing: Repo-wide `bun install` is broken on main — `alchemy-effect: catalog:` is referenced by 5 packages but missing from the root catalog. Unrelated to this PR; blocked local verification

Test plan

  • `bun install` succeeds (blocked on the alchemy-effect catalog fix above)
  • `bun run dev:web` starts without errors
  • `/` → click hero "Try the demo" → lands on `/studio/dashboard` with amber demo banner; no pairing prompt
  • Project picker shows read-only "stackpanel-demo" badge with exit button
  • Click "Exit demo" → endpoint swaps back; if no local agent, "Agent Not Running" empty state shows with "Try the Demo" CTA
  • Click "Try the Demo" from the empty state → returns to demo cleanly
  • Refresh while in demo → still in demo (no flash of pairing UI)
  • Process state card on overview shows the 4 demo processes
  • No console errors in either local or demo mode

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 29, 2026

PR Summary

Cursor Bugbot is generating a summary for commit e05640b. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Null token breaks SSE and AgentProvider localStorage fallback
    • Updated token resolution and guard checks so null correctly falls back to localStorage token discovery in both providers.
  • ✅ Fixed: Demo endpoint set even when MSW worker fails
    • Moved demo localStorage persistence and endpoint switching inside the successful worker-start path so failures no longer force an unreachable demo endpoint.
  • ✅ Fixed: Exported useEnterDemo hook is never imported
    • Removed the unused exported convenience hook to eliminate dead API surface.

Create PR

Or push these changes by commenting:

@cursor push af35a78752
Preview (af35a78752)
diff --git a/apps/web/src/lib/agent-endpoint.tsx b/apps/web/src/lib/agent-endpoint.tsx
--- a/apps/web/src/lib/agent-endpoint.tsx
+++ b/apps/web/src/lib/agent-endpoint.tsx
@@ -136,18 +136,18 @@
 	}, []);
 
 	const useDemo = useCallback(async () => {
-		if (typeof window !== "undefined") {
-			window.localStorage.setItem(STORAGE_KEY, "demo");
-		}
 		setBootingDemo(true);
 		try {
 			await startDemoWorker();
+			if (typeof window !== "undefined") {
+				window.localStorage.setItem(STORAGE_KEY, "demo");
+			}
+			setEndpoint(DEMO_ENDPOINT);
 		} catch (err) {
 			console.error("[demo] failed to start mock worker", err);
 		} finally {
 			setBootingDemo(false);
 		}
-		setEndpoint(DEMO_ENDPOINT);
 	}, []);
 
 	const useLocal = useCallback(() => {
@@ -186,8 +186,3 @@
 	}
 	return ctx;
 }
-
-/** Convenience hook for landing-page CTAs that don't care about the rest of the API. */
-export function useEnterDemo(): () => Promise<void> {
-	return useAgentEndpoint().useDemo;
-}

diff --git a/apps/web/src/lib/agent-provider.tsx b/apps/web/src/lib/agent-provider.tsx
--- a/apps/web/src/lib/agent-provider.tsx
+++ b/apps/web/src/lib/agent-provider.tsx
@@ -123,7 +123,7 @@
 
 	// Handle query token persistence and URL cleanup (runs after initial render)
 	useEffect(() => {
-		if (providedToken !== undefined) {
+		if (providedToken != null) {
 			return () => {
 				cleanupRef.current?.();
 				cleanupRef.current = null;

diff --git a/apps/web/src/lib/agent-sse-provider.tsx b/apps/web/src/lib/agent-sse-provider.tsx
--- a/apps/web/src/lib/agent-sse-provider.tsx
+++ b/apps/web/src/lib/agent-sse-provider.tsx
@@ -89,10 +89,10 @@
 	const registeredEventsRef = useRef<Map<string, EventListener>>(new Map());
 	const heartbeatTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
 
-	const resolvedToken = token !== undefined ? token : storedToken;
+	const resolvedToken = token ?? storedToken;
 
 	useEffect(() => {
-		if (token !== undefined) return;
+		if (token != null) return;
 		setStoredToken(localStorage.getItem(STORAGE_KEY));
 
 		const onStorage = (event: StorageEvent) => {

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit e05640b. Configure here.

host: config.host,
port: config.port,
token: config.token ?? null,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Null token breaks SSE and AgentProvider localStorage fallback

High Severity

makeLocalEndpoint and getLocalConfigFromEnv produce token: null for local mode, but both AgentSSEProvider and AgentProvider use token !== undefined checks to decide whether to fall back to localStorage-stored pairing tokens. Previously, the studio passed undefined (via || undefined); now it passes null (via || null and ?? null). Since null !== undefined is true, the SSE provider sets resolvedToken = null instead of reading storedToken from localStorage, and the AgentProvider's token-discovery effect early-returns — skipping URL-param persistence and expiry checks. The comment on AgentEndpoint.token even says "null means use stored pairing token," but the downstream providers don't treat null that way. This breaks SSE connectivity for all local-mode users who pair with the agent.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e05640b. Configure here.

setBootingDemo(false);
}
setEndpoint(DEMO_ENDPOINT);
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Demo endpoint set even when MSW worker fails

Medium Severity

In useDemo, setEndpoint(DEMO_ENDPOINT) executes unconditionally after the try/catch/finally block. If startDemoWorker() throws, the endpoint still switches to the unreachable demo host (demo-agent.stackpanel.local:9876) without MSW intercepting requests, causing all subsequent API calls to fail. The localStorage entry is also written before the worker starts, so refreshes would repeat the broken state.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e05640b. Configure here.

/** Convenience hook for landing-page CTAs that don't care about the rest of the API. */
export function useEnterDemo(): () => Promise<void> {
return useAgentEndpoint().useDemo;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Exported useEnterDemo hook is never imported

Low Severity

useEnterDemo is exported but never imported anywhere in the codebase. All consumers use useAgentEndpoint().useDemo directly. This is dead code that adds unnecessary surface area.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e05640b. Configure here.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e05640b57a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

kind: "local",
host: config.host,
port: config.port,
token: config.token ?? null,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep local token undefined to preserve SSE auth fallback

Do not coerce the local endpoint token to null here; when VITE_STACKPANEL_AGENT_TOKEN is unset, this now passes token={null} into AgentSSEProvider, and that provider only falls back to localStorage when token === undefined. As a result, paired users lose authenticated SSE (no real-time events/heartbeat-based disconnect detection) and silently degrade to polling-only behavior after this change.

Useful? React with 👍 / 👎.

Comment on lines +146 to +150
console.error("[demo] failed to start mock worker", err);
} finally {
setBootingDemo(false);
}
setEndpoint(DEMO_ENDPOINT);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid switching to demo endpoint when worker boot fails

Only flip to the demo endpoint after startDemoWorker() succeeds. In the current flow, failures (e.g. service worker unavailable, registration error, missing script) are logged but still followed by setEndpoint(DEMO_ENDPOINT), which sends the Studio to an unreachable fake host and can strand the user in a broken demo state.

Useful? React with 👍 / 👎.

Claude (claude) and others added 4 commits April 29, 2026 04:10
Adds a self-contained `apps/web/src/demo/` module: a frozen fixture (state.json
shape, Nix config, entity tables), MSW handlers covering the high-traffic REST
endpoints plus a Connect-RPC catch-all, a fake JWT for AgentProvider, and a
lazy `setupWorker(...)` so the demo bundle only loads on `/demo`.

The route mounts the studio sidebar/header against `demo-agent.stackpanel.local`
with no real network involvement, so visitors to stackpanel.com can poke at the
UI without pairing a real agent. Uses `onUnhandledRequest: "bypass"` so missing
handlers surface as blank panels rather than errors; the README documents the
one-time `bunx msw init public/` bootstrap and sketches the proto-driven mock
generation plan.
Deploy CI runs `bun install --frozen-lockfile`; the previous commit added
msw to apps/web/package.json without updating the lockfile, which failed the
deploy job on PR #13.
Replaces the dedicated /demo route + hand-curated UI with a runtime
endpoint swap: the real Studio now points at an in-browser MSW agent
when the user opts into demo mode. Avoids drift between a parallel
demo and the live Studio.

- AgentEndpointProvider (lib/agent-endpoint.tsx) at the React root
  owns { host, port, token, isDemo }; persists to localStorage and
  boots MSW before flipping endpoint so requests don't 404
- routes/studio.tsx re-keys AgentSSEProvider/AgentProvider on each
  endpoint change (avoids stale EventSource/timer state); suppresses
  the pairing overlay and renders DemoBanner in demo mode
- routes/demo.tsx is now a tiny redirect into /studio/dashboard so
  marketing CTAs keep working without code changes
- AgentConnect grows a "Try the Demo" CTA in the no-agent-running
  empty state so first-time visitors have an obvious escape hatch
- ProjectSelector renders a read-only badge in demo mode (the
  cloud-tRPC project queries can't reach a fake host)
- MSW handlers cover /api/project/{list,current,open,validate,
  close,remove} + /api/process-compose/processes; new fixtures
  for the demo project and process snapshot
- Adds apps/web/public/mockServiceWorker.js (msw v2.7) - PR #13
  forgot this and registration would have failed at runtime
- Drops the parallel /demo/{apps,files,...} static routes and
  /components/demo/{demo-fixtures,demo-header,demo-sidebar} chrome
  (~1.9k LOC); routeTree.gen.ts updated in lockstep

Follow-ups (filing as bd issues):
- Generate MSW fixtures from proto-nix example fields
- Repo-wide bun install is broken on main (alchemy-effect:catalog:
  referenced but undeclared) - blocked local verification
Several sibling components (`dashboard-sidebar`, `panels-panel`) pass
the raw search-params object straight into `new URLSearchParams(search)`
to read other keys (`section`, `module`). With `demo` typed as `boolean`
TypeScript rejects the spread because `URLSearchParams` only accepts
string-valued records.

Keep the user-facing API identical (`?demo=1` and `?demo=true` both
flip into demo mode) but normalise the parsed value to `"1" | undefined`
so the search record stays a `Record<string, string | undefined>`.
PR #17 rotated the Cloudflare API token in `.stack/secrets/vars/shared.sops.yaml`
but did not regenerate the codegen-emitted runtime payload at
`packages/gen/env/src/runtime/generated-payloads/_envs/deploy.ts`. The
embedded JSON is what `loaders.deploy()` actually decrypts at runtime, so
both `main` and this branch have continued shipping the old `cfat_KJ57…`
token (which lacks `Workers Scripts: Edit`) into CI — explaining why every
post-rotation deploy still fails with `Unauthorized: Authentication error`
despite the underlying SOPS YAML being correct.

Re-running the devshell hook on this branch regenerates both the encrypted
data file and the TypeScript module from the (already-rotated) source YAML.
Decrypting the regenerated payload yields `cfut_A8wV…` (verified locally)
and the `curl` probe earlier confirmed that token has full read+write
scopes for `workers/scripts`, `workers/subdomain`, `kv/namespaces`,
`zones/.../workers/routes`, and `workers/domains` — i.e. exactly what
`Cloudflare.Worker` needs.

Plaintext for every other secret in the payload is unchanged; only IVs and
ciphertext rotated as a side effect of SOPS re-encrypting the whole file.

Follow-up (separate change): `chore: rekey`-style flows and the source SOPS
edit path should both trigger codegen so this drift can't happen silently
again. Filing as a beads issue.
@github-actions
Copy link
Copy Markdown

Preview deployed to pr-15https://pr-15.stackpanel.com

@czxtm cooper (czxtm) merged commit 0f95da6 into main Apr 29, 2026
5 of 7 checks passed
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.

2 participants