cl_route-every-surface
Use whenever you add a new page/view or a dialog/modal to the web UI (apps/web), or when a user says "I want to link to X", "refresh loses my place", "make this bookmarkable", "this dialog doesn't survive reload". Every page and cold-load-coherent dialog gets a URL param.
- License 许可
- MIT
- 最近更新
- 2026-11-12
- Stars
- ★ 4
- Forks
- 0
01 前言 Front Matter
- name
- cl_route-every-surface
- description
- Use whenever you add a new page/view or a dialog/modal to the web UI (apps/web), or when a user says "I want to link to X", "refresh loses my place", "make this bookmarkable", "this dialog doesn't survive reload". Every page and cold-load-coherent dialog gets a URL param.
03 SKILL.md 正文
<!-- GENERATED FROM .claude/skills — DO NOT EDIT.
Run npm run gen:skills after editing the source skill (the build does this too). -->
Route Every Surface — A New Page/Dialog Gets a URL
The pattern the project enforces: the URL is the source of truth for where you are. A view, a selected run, and an open dialog all serialize to the URL so a refresh, a copy-pasted link, or a new tab rehydrates the exact same screen.
Forgetting this is how a new dialog "vanishes on refresh" and a new page can't be linked to.
The cold-load test (decides whether to route a dialog)
Route a dialog only if landing on its URL cold — fresh tab, no prior in-memory state — produces something coherent.
| Route it | Don't route it |
|---|---|
| Pages/views (coverage, cleanup, flights) | Collision-confirm (fires off a live run event) |
| Selected run | Services / Runs-list / Portify-picker (transient pickers) |
| Config / settings panels; changed-tests review and notifications | Any modal that reacts to a momentary event you can't reconstruct |
| Wizards (open-state, not per-step) | Confirmation prompts |
| Verification, flight-start, flight-new |
Pages always route. Dialogs route selectively. When unsure, ask: if I paste this URL into a clean browser, does the dialog have everything it needs? If it depends on live in-memory state, leave it ephemeral.
The one home — extend, don't fork
All route serialization lives in one module:
apps/web/src/shared/lib/workspace-view-state.ts (readPersistedView /
persistView / onViewChangedInOtherTab). Navigation state and dialog
precedence live in apps/web/src/shared/state/nav-state.ts; the
use-workspace-navigation.ts hook hydrates and persists them. Add a param in
that path — never a second URL-writing mechanism, and never react-router
(deliberate: query-param style, no router dependency). See [[cl_reuse-shared-logic]].
The server already serves index.html for any deep link (catch-all in
apps/web-server/src/server.ts) — refreshing /?... never 404s. No server change is
needed for new routes.
Two tiers — pick the right one
| Tier | Params | Channels | Use for |
|---|---|---|---|
| Durable nav | view, feature |
URL + localStorage + cross-tab storage |
top-level location shared across tabs |
| URL-only | run, dialog, flight, and their qualifiers |
URL only | run selection (two tabs may compare runs) + dialog open-state (a dialog open in one tab must NOT pop open in another) + stage, tab, review, and drill-through context |
A new dialog is almost always URL-only (the dialog param). Do not mirror
dialog open-state to localStorage or broadcast it cross-tab.
Current URL schema
?view=coverage&feature=checkout → coverage page
?feature=checkout&run=7cvh → run selected in detail pane
?feature=checkout&dialog=config → feature config (Playwright — the no-tab default)
?feature=checkout&dialog=config&tab=ports → feature config, on a named tab
?feature=checkout&dialog=verification → Verify-config dialog
?feature=checkout&dialog=flight-start → flight stage-entry launcher (feature-scoped)
?feature=checkout&dialog=flight-fresh → same launcher in START-FRESH intent
?dialog=flight-new → new-flight launcher (intent + repo picker)
?dialog=demo → demo chooser (needs only GET /api/onboarding)
?dialog=settings → Project Settings (needs only GET /api/project-config)
?view=flights&flight=fl_abc → flight detail (omit flight = flights list)
?view=flights&flight=fl_abc&stage=docs → that flight, on a named stage (omit = follow-mode)
?dialog=notifications → notifications center
?dialog=tests-review&reviewFile=e2e/cart.spec.ts → changed-tests review focused on a file
?dialog=settings&models=codex → Project Settings with Codex models open
?feature=checkout&run=7cvh&test=cart%20fails → run detail focused on a failure
?feature=checkout&run=7cvh&tests=recorded → recorded tests for that run
?feature=checkout&run=7cvh&runtab=changes → arrival: the run's Changes dialog
?feature=checkout&run=7cvh&changes=1 → the run's Changes dialog open (legacy rview=run-wide reads as this)
?feature=checkout&run=7cvh&runtab=results&test=cart%20fails&attempt=2&journal=entry
→ that test's dialog on its 2nd run, its journal entry open
?feature=checkout&run=7cvh&runtab=results&test=cart%20fails&ttab=artifacts
→ that test's dialog on its Artifacts tab
?feature=checkout&run=7cvh&runtab=results&test=cart%20fails&cycle=2
→ older link: the run that checked repair cycle 2
?feature=checkout&run=7cvh&runtab=services&svc=api&exec=3&lines=120-140
→ api's retained log for execution 3, lines highlighted
RouteDialog = 'config' | 'verification' | 'flight-start' | 'flight-fresh' | 'flight-new' | 'demo' | 'settings' | 'tests-review' | 'notifications'.
flight only qualifies view=flights (absent = the flights landing list), and
stage only qualifies an open flight. tab only qualifies dialog=config;
models only qualifies dialog=settings; review* params only qualify
dialog=tests-review; test, runtab, and tests qualify a selected run.
Inside a run, the detail reports the reader's place (RunLocation: runtab,
rview, changes, test, attempt, cycle, ttab, journal, svc, exec/lines/match) through
setRunLocation; each qualifier is gated on the one it belongs to, a reported
place supersedes the arrival that opened the run, and a stale name is explained
by the view (stale-*-link) rather than dropped. Embedded run details pass no
onLocationChange and stay unrouted.
from records the flight a workspace/coverage drill-through came from and is
dropped on the flights view. persistView/readPersistedView own these gates.
Because the config dialog is qualified by the DURABLE feature param, any opener
that opens it for a feature other than the selected one must setSelectedFeature
too — otherwise the deep link names the wrong suite.
wf, task and draft are tombstoned qualifiers (see Gotchas). Unknown
dialog values are ignored on read.
Checklist — adding a new page
- Add the value to the
WorkspaceViewunion inworkspace-view-state.tsand theVIEWSarray. - Render it from
App.tsxand add the nav entry that callssetView(...)fromuse-workspace-navigation.ts. - The hook's persist effect already serializes
view— nothing else to wire. - Confirm refresh restores it (it reads from the URL on load).
Checklist — adding a new routed dialog
- Add it to the enum —
RouteDialog+DIALOGSarray inworkspace-view-state.ts. If it needs an id qualifier, follow the live precedent —flightqualifyingview=flights— add a param and gate it inpersistView/readPersistedViewso it's dropped unless your dialog/view is active. Don't reusewf,taskordraft— all three are tombstoned (see Gotchas). - Hydrate on mount — add the dialog to
initialNavStateinnav-state.tsand seed its open-state fromSEEDinuse-workspace-navigation.ts. - Derive it into the route — add it to
routedDialoginnav-state.ts(precedence = z-order), and expose its setter from the hook so the persist effect writes thedialogparam when it is open. - Where does the open-state live?
- Navigation-owned (like config and settings): seed it in the navigation
hook and render from
App.tsx. - In a context: hydrate the navigation state first, then pass its open flag to the context instead of adding a second URL writer.
- In a child component (like the Verify dialog in
RunsColumn): make the child's open-state a controlled/uncontrolled hybrid — acceptopen?/onOpenChange?props, fall back to internal state when absent (keeps the child's own unit tests working), and have the navigation hook own the state + drive the route. Lift, don't duplicate.
- Navigation-owned (like config and settings): seed it in the navigation
hook and render from
- Add a round-trip test to
workspace-view-state.test.ts(URL round-trip + the URL-only/localStorage-exclusion assertion).
Gotchas
- Hook order — keep any state the persist effect reads in
use-workspace-navigation.tsbefore that effect and include its primitive fields in the dependency list. A dialog can stay open while a qualifier changes, so depending only ondialogleaves its URL stale. - Stale-run guard wipes a hydrated run — the run-selection reconciliation
effect clears
selectedRunIdto "latest" before runs arrive over the WS. SeedpendingRunSelectionRefwith the persisted run so the hydrated run survives until its run loads. - Cross-tab scope —
onViewChangedInOtherTabemits the durable tier only (view/feature). Never push run/dialog through it. - Qualifiers belong to their dialog/view only —
persistViewdropsflightunlessview === 'flights'(the live precedent to copy — same drop-unless-active gate).wf(old portify-revisit id),task(old evaluation-dialog id) anddraft(old external-authoring id) are tombstoned:persistViewforce-deletes all three on every write regardless of state, so stale deep links don't carry them forward. If your dialog needs its own id, add a new param with the flight-style gate; never reusewf,taskordraft.
Deliberately NOT done (don't "fix" these without a reason)
- Back-button closes dialog — would need user-vs-programmatic transition
tracking (auto-select-first-feature would spam history). The goal is revisit
(deep-link + refresh), which
replaceStatedelivers. Left out on purpose. - Pretty paths (
/coverage/checkout) — query-param style is intentional; a router buys nothing for a single-screen internal tool.
Relationship to neighbours
- [[cl_reuse-shared-logic]] — routing has ONE home (
workspace-view-state.ts); extend it, never add a second URL-writing path or a router lib. - [[cl_ui-design-philosophy]] — a new surface's look is governed here; its route is governed by this skill.
- [[cl_ws-driven-state]] — that skill keeps UI state live after a server mutation; this skill keeps UI state addressable in the URL. Different axes, both required for a surface to feel native.
- [[cl_verify-changes]] —
apps/web/**changes need the canary-apply cycle (Tier 3 says who runs it) to confirm refresh/deep-link behaviour end-to-end; unit tests cover the serialization.
04 同仓库更多 Skills
off-by-one
暂无简介
multi-failure-cascade
暂无简介
broken-delete-contract
暂无简介
canary-lab
Use when the user wants Canary Lab to take a product repo end to end — "test this app", "onboard this repo", "run a flight", "evaluate what I just built" — through the flight pipeline (start_flight / get_flight / respond_flight_checkpoint over MCP). One conducted pipeline continues a configured suite or takes bare repo(s) to a green, covered, healed run and publishes a downloadable Report; Parallel setup continues as Canary-owned background work. Use a focused skill only when the user asks for that single capability without asking for a Flight or /canary-lab.
canary-lab-verify
Use when verifying a Canary Lab feature against a RUNNING target — a deployed environment ("verify checkout on staging", "run the saved verify config", "check the deployed app") or a locally booted app ("verify a running app") — through the verify MCP tools (list/get/create/update_verification_config, boot_services, execute_verification, get_verification_result). No heal loop. For run + heal use canary-lab-run; for end-to-end onboarding use canary-lab (flight).
canary-lab-run
Use when running or healing an EXISTING Canary Lab feature locally — "run <feature>", "fix the failing run", "rerun 7cvh", "drive the heal loop" — through the repair MCP tools (start_run, wait_for_heal_task, signal_run, get_heal_context, pause_run, cancel_heal, abort_run). Fix failing runs by editing app/service code, not tests. For end-to-end onboarding of a new repo use canary-lab (flight); for deployed-environment checks use canary-lab-verify.