gateway: phase-9 kill-switch UI — run/agent kill + live hot state - #1685
Merged
Merged
Conversation
Operator half of the phase-6 kill switches (#1683, #1684) in the admin SPA. Scope is kill/unkill + live hot state only; no config editing, no bulk actions, no analytics changes. - api/types.ts: RunStateResponse, AgentStateResponse, KillRunResponse, KillAgentResponse mirroring hotstate.go (hand-maintained; no tygo). - api/queries.ts: useRunState (2s in flight / 30s done / 500ms for 30s after a kill), useAgentState (10s), useAgentStates (30s per row), useKillRun / useUnkillRun / useKillAgent / useUnkillAgent. 503 (no Redis) folds into data === null with a 60s retry. - components/KillConfirmModal: one modal, two modes — plain confirm for runs, typed agent name for agents. Explains scope, TTL (1h / 24h) and "takes effect on the next LLM call; enforced only with enforce_macaroons=true". No window.confirm (Hive iframe sandbox). - components/StatusBadge: running / killed / exceeded / done with the derivation documented in one place. - RunDetail: live-state card (cost, steps, last tools, kill flag, "state expires in") + Kill / Unkill; 503 renders as an inline note with the switch disabled. - AgentDetail: killed / exceeded badge in the header + Kill / Unkill agent. Agents: kill-state column read from /state per row. - icons: StopIcon. - vite.config: bypass /_plugin/ui/* in the dev proxy so Vite serves the SPA instead of the gateway's embedded bundle (local edits never showed otherwise); GATEWAY_URL override for the proxy target. - .claude/launch.json: gateway-ui dev-server entry. dist/ stays the tracked placeholder — the repo doesn't commit builds.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Operator-facing kill switch in the gateway admin SPA — the UI half of phase 9 (
gateway/plans/phases/phase-9-operator-ui.md) over the phase-6 backend that landed in #1683 / #1684. Scope is kill/unkill + live hot state only.What's in
api/types.ts—RunStateResponse,AgentStateResponse,KillRunResponse,KillAgentResponse, mirroringhotstate.gokey-for-key. Hand-maintained;make tygowas not run (Gateway: reconcile tygo codegen with SPA and enforce in CI #1563 is the reconciliation PR).api/queries.ts—useRunState,useAgentState,useAgentStates(per-row fan-out for the list) anduseKillRun/useUnkillRun/useKillAgent/useUnkillAgent. Cadence lives in the hooks per phase 9: run state polls at 2s while in flight, 30s once done, and 500ms for 30s right after a kill/unkill; agent state 10s on the detail page, 30s per list row. A 503 (Redis unconfigured) folds intodata === nullwith a 60s retry instead of surfacing as an error. Mutations have no optimistic update, invalidate the matchingstatequery (the list rows share the same cache entry), and rely onapiFetch's unconditionalX-Bifrost-CSRFheader — same path the catalog toggles already use.components/KillConfirmModal.tsx— one modal, two modes: plain confirm for runs, typed agent name for agents (Enter submits once the name matches, ESC/backdrop close unless a request is in flight). Copy spells out blast radius (run kill cascades to sub-agents; agent kill is swarm-wide but does not touch differently-named sub-agents), TTL (1h / 24h), and that a kill takes effect on the next LLM call and is only enforced whenenforce_macaroons=true. Custom element, notwindow.confirm(Hive's iframe sandbox suppresses it).components/StatusBadge.tsx—running / killed / exceeded / donewith the derivation documented in one place:killedfromstate.killed;exceededfor agents when current-bucket spend ≥ configured cap (runs carry caps in the macaroon, which/statedoesn't expose, so a capped run reads asdoneonce it stops);runningwhen a call landed in the last 5 minutes or the/statestep counter moved between polls; otherwisedone.pages/RunDetail.tsx— live-state card (live cost, steps, last tools, kill flag, "state expires in" fromttl_seconds) with Kill / Unkill. Rendered above the log-backed content so it works even while the call log loads. Redis-off renders an inline "hot state unavailable on this swarm" note with the switch disabled;ttl_seconds = -2renders as "no hot state yet" with kill still allowed. After a kill the card shows the killed-at time and the shadow-mode hint ("if calls keep landing, the swarm is most likely in shadow mode").pages/AgentDetail.tsx— killed / exceeded badge in the header, Kill / Unkill agent button next to the window picker.pages/Agents.tsx— a Kill state column read from/agents/:name/stateper visible row (sorts killed > exceeded > clear).components/icons.tsx—StopIcon(stroke 1.8, 24×24,currentColor).AGENTS.md— page table, component list, and a "Kill switches" conventions section.Two small dev-workflow fixes rode along because verification depended on them:
vite.config.ts— the dev proxy rule for/_pluginalso matched/_plugin/ui/*, so the browser was served the gateway's embedded production bundle and local edits never showed. Added abypassfor the SPA's own base path, plus aGATEWAY_URLoverride for the proxy target..claude/launch.json— agateway-uidev-server entry.npx tsc -b --noEmitandnpm run buildare clean.dist/is untouched: only the placeholder is tracked, so no build output is committed.Verification
Backend:
go test ./internal/adminapi -run 'HotState|Kill|RunState|AgentState'passes onmain, which is the contract the types mirror.UI: I could not rebuild the gateway image on this machine — Docker Hub pulls hang inside the Docker daemon (host
curlreaches the registry fine; the daemon can't even pullalpine), the cachedstakgraph-gateway:devimage predates #1683, and the Go/Node base images aren't cached. So the dashboard was exercised against a ~100-line mock of the six hot-state routes (exact response shapes, id validation, TTLs, CSRF and cookie checks, and aMOCK_REDIS_OFFmode) reverse-proxying everything else to the real local gateway — real login, real runs fromlogs.db, real call log. Checked end to end: kill flipskilledand the badge within the poll interval, unkill flips it back, the 500ms poll boost after a kill is visible in the network log, the typed confirmation gates the agent button until the name matches exactly, Enter submits, and the Redis-off mode degrades to the inline note with kill buttons disabled. Worth a re-run against a real swarm before merging; the first hit ofGET /_plugin/runs/<id>/statefrom a session cookie is the only thing the mock can't prove.Screenshots
Images live on the disposable branch
gateway-kill-switch-ui-screenshots(delete after merge).Run detail, live state (in flight)
Kill run confirm modal
Run detail after the kill
Unkill modal
Kill agent modal — typed confirmation (partial name: button disabled; exact name: enabled)
Agent detail, killed
Agents list with a killed row
Redis off (503): inline note, switch disabled
Out of scope (deliberately)
/_plugin/config/*budget / ceiling editing (phase 9 "Edits").enforce_macaroons/enforce_budgetsin the UI — the modal carries a static hint instead.types.tswith tygo.