demo: drive Studio against MSW via endpoint swap (no /demo route)#15
demo: drive Studio against MSW via endpoint swap (no /demo route)#15cooper (czxtm) merged 5 commits intomainfrom
Conversation
PR SummaryCursor Bugbot is generating a summary for commit e05640b. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
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
nullcorrectly falls back to localStorage token discovery in both providers.
- Updated token resolution and guard checks so
- ✅ 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
useEnterDemohook is never imported- Removed the unused exported convenience hook to eliminate dead API surface.
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, | ||
| }; |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit e05640b. Configure here.
| setBootingDemo(false); | ||
| } | ||
| setEndpoint(DEMO_ENDPOINT); | ||
| }, []); |
There was a problem hiding this comment.
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.
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; | ||
| } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit e05640b. Configure here.
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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 👍 / 👎.
| console.error("[demo] failed to start mock worker", err); | ||
| } finally { | ||
| setBootingDemo(false); | ||
| } | ||
| setEndpoint(DEMO_ENDPOINT); |
There was a problem hiding this comment.
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 👍 / 👎.
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>`.
e05640b to
bfa5503
Compare
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.
|
Preview deployed to |



Summary
Replaces the dedicated
/demoroute + 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 tolocalStorageand boots MSW before flipping the endpoint so in-flight requests don't 404routes/studio.tsx— re-keysAgentSSEProvider/AgentProvideron every endpoint change so EventSource/timer state doesn't leak across local↔demo. Suppresses the pairing overlay and renders<DemoBanner>in demo moderoutes/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
/api/project/{list,current,open,validate,close,remove}so<ProjectProvider>resolves a single synthetic project on mount, plus/api/process-compose/processesso the overview's process card has datademoProjectanddemoProcessComposeProcessesfixturesapps/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 runtimeCleanup
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). UpdatesrouteTree.gen.tsin lockstep so the build stays green without a router-codegen pass.Net diff: +829, −2220 lines across 21 files.
User flows
/demoredirect → MSW boots →/studio/dashboardwith demo banner<AgentConnect>shows "Try the Demo" button → endpoint swap → studio remounts in demo modelocalStoragerestores demo,useEffectre-boots the worker, pairing overlay stays hiddenFollow-ups (filing as bd issues)
proto-nixexamplefields (nix/stackpanel/db/lib/field.nixinfra is already in place; only one schema currently populatesexample). Will let us delete most hand-written handlersTest plan