Skip to content

feat: live sandbox updates via WatchSandbox WebSocket relay (FEATURE_LIVE_UPDATES) - #33

Open
Gkrumbach07 wants to merge 4 commits into
mainfrom
claude/terminal-curl-auto-approval-nh18f7
Open

feat: live sandbox updates via WatchSandbox WebSocket relay (FEATURE_LIVE_UPDATES)#33
Gkrumbach07 wants to merge 4 commits into
mainfrom
claude/terminal-curl-auto-approval-nh18f7

Conversation

@Gkrumbach07

Copy link
Copy Markdown
Owner

Summary

Relays the gateway's WatchSandbox server stream over a WebSocket so the sandbox detail page — draft-policy inbox and logs tab included — updates sub-second instead of waiting out the 5–10s poll intervals. Motivated by the "curl gets blocked → proposal only shows up after a manual refresh" experience: the gateway's watch bus fires on every draft mutation (including auto-approvals under proposal_approval_mode=auto), and the dashboard now listens.

Backend (BFF)

  • gateway.WatchSandbox wrapper + watch_handler.go WS relay: resolves name → id, always follows status, ?logs=true opts into log following with the same filters as the polled logs endpoint; ping/pong keepalive detects half-dead proxy connections
  • Events convert through models.From*() (models.WatchEvent) so secret-field stripping applies — raw proto never reaches the browser
  • Route registered only when FEATURE_LIVE_UPDATES is on (default true), exposed to the frontend as features.liveUpdates

Frontend

  • useSandboxWatch: pushes sandbox snapshots straight into the React Query cache and debounces draft/policy invalidations; reconnects with capped jittered backoff and gives up after repeated failures
  • useSandboxLogStream: separate socket with logs=true for the logs tab (tail replay + live lines, buffer capped at the selected line count, reset on reconnect); both hooks share one reconnecting-socket helper
  • Polling is the fallback, not a parallel system: useSandbox / useDraftPolicy / the polled logs query suspend their refetchInterval while a socket is live; a failed poll no longer clobbers a healthy stream
  • Live/Polling indicator on the detail page header and logs toolbar

Docs: ADR 0004, FEATURE_FLAGS.md, streaming-RPC rule update.

Testing

  • End-to-end (committed, watch_e2e_test.go): real gRPC server → real gateway.Client over TCP → real chi router → real WebSocket client; asserts snapshot and log frames relay through the whole chain, and that the route 404s when the flag is off
  • Handler tests (relay, log params, error path) and 11 jest hook tests (flag gating, cache push, burst coalescing, reconnect/fallback, buffer cap, unmount cleanup)
  • Browser verification: built frontend + BFF + fake gateway driven by Playwright — Live indicator green, a draft proposal pushed ~8s after page load appeared with no page reload, proposals tab listed the chunk, logs tab streamed lines under its own Live label
  • Full suites green: go test ./..., tsc --noEmit, eslint, 179 frontend tests

Deployment notes

FEATURE_LIVE_UPDATES=false restores exactly the previous polling behavior; deployments behind proxies that can't upgrade WebSockets also degrade to polling automatically after the hook's reconnect budget is exhausted.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JkEomBo8gaAzDj3RpVFtF5


Generated by Claude Code

Gkrumbach07 and others added 4 commits August 10, 2026 22:55
…LIVE_UPDATES)

Relay the gateway's WatchSandbox server stream over a WebSocket so the
sandbox detail page — draft-policy inbox included — updates sub-second
instead of waiting out the 5-10s poll intervals.

- BFF: gateway.WatchSandbox wrapper, watch_handler.go WS relay
  (follow_status always, follow_logs opt-in, ping/pong keepalive),
  WatchEvent DTO through models.From*() so secret-stripping applies,
  route gated by FEATURE_LIVE_UPDATES
- Frontend: useSandboxWatch hook pushes snapshots into the React Query
  cache and debounces draft/policy invalidations; useSandbox and
  useDraftPolicy accept {live} to suspend refetchInterval while the
  socket is open, so polling remains the automatic fallback
  (reconnect with capped backoff, give up after repeated failures)
- Live/Polling indicator on the detail page header
- Docs: FEATURE_FLAGS.md, ADR 0004, streaming-RPC rule update

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkEomBo8gaAzDj3RpVFtF5
Signed-off-by: Gage Krumbach <gkrumbach@gmail.com>
Phase 3 of live updates (ADR 0004):

- Extract the reconnecting-socket logic shared by both live hooks into
  hooks/watchSocket.ts
- useSandboxLogStream opens its own watch socket with logs=true (tail
  replay + live lines, buffer capped at the selected line count, buffer
  reset on reconnect so replays don't duplicate)
- SandboxLogsTab prefers the stream and shows a Live label in place of
  the auto-refresh checkbox; the polled query stays as fallback, and a
  failed poll no longer clobbers a healthy stream
- watch_e2e_test.go: end-to-end over a real gRPC server, real
  gateway.Client, real router, and a real WebSocket client — asserts
  snapshot and log frames relay through the whole chain, and that the
  watch route is absent when FEATURE_LIVE_UPDATES is off

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkEomBo8gaAzDj3RpVFtF5
Signed-off-by: Gage Krumbach <gkrumbach@gmail.com>
- Split the watch relay handler into buildWatchRequest / startWatchReader /
  pumpWatchEvents / writeWatchFrames (gocyclo)
- Extract requireWatchRequest helper in the e2e test (gocyclo)
- Reorder WatchEvent fields pointers-first (govet fieldalignment)
- Prettier-format the new frontend files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkEomBo8gaAzDj3RpVFtF5
Signed-off-by: Gage Krumbach <gkrumbach@gmail.com>
CI runs go test -race: the tests polled a variable written by the mock on
the server goroutine. Deliver the captured WatchSandboxRequest over a
channel instead (watchRequestOrFail helper).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkEomBo8gaAzDj3RpVFtF5
Signed-off-by: Gage Krumbach <gkrumbach@gmail.com>
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