← the Atlas

Deep Links — Every View Addressable by URL

Features·seedling·accepted·

A hash-based URL grammar that opens kolu at a host, a terminal, a file in the Code tab, or settings — reusing the W5 notification cold-start machinery; view-only by law; PWA link-capture via launch_handler.

User-facing description

Any place that can render a link — the orchestrator dashboard, an issue, a Slack message, a bookmark — can point into kolu: click it and the installed PWA window comes to the front already showing the right host, terminal, file, or dialog. No PWA? The same URL opens in any browser. The full grammar (v1 ships all of it):

URL what you land on
#/h/<host> the host binding switched to <host>
#/t/<host>/<terminalId> that host and the terminal tile focused
#/t/<host>/<terminalId>/code?path=<p>&line=<n> the terminal’s right panel on the Code tab, <p> open (optionally at line <n>)
#/t/<host>/<terminalId>/inspector the terminal’s right panel on the Inspector tab
#/settings the settings dialog

<host> is the canonical encoded host key (encodeHostKey — the local host is local); <terminalId> is the terminal’s UUID.The same two facts every attention-notification click already carries (attentionNotify.ts’s AttentionClick), validated the same way: a non-canonical host key or a non-UUID id is dropped, never routed.

Behavior on the edges — always loud, never silent:

What no URL can ever do: mutate. No route creates a terminal, kills one, writes a file, or sends keys. This is a law, not a review item — the router has no mutating routes to hit, so a hostile link’s worst case is a view change.The classic drive-by risk with URL handlers is a GET that acts. Kolu’s grammar is view-only by construction; any future “action URL” proposal reopens this note’s Decision, not a code review.

Architecture-level changes

link click / bookmark launchQueue (PWA capture) hashchange (live) manifest launch_handler: focus-existing deepLink.ts (leaf) parse, don't validate bad → loud toast, never route setActiveHost (binding switch) store.activate (tile focus) right panel · showCode(path) settings dialog all EXISTING seams — the router adds none LAW: a deep link only VIEWS — no URL may create, kill, write, or send. Mutation via URL is unrepresentable (no route exists).
One new leaf. Three URL entry points (boot, hashchange, PWA launchQueue) feed one parser; every route lands on an EXISTING action seam — host switch, tile focus, right panel, settings. The law strip: mutation is unrepresentable.

Implementation details

DL1 — the router + the full menu (one PR)

  1. Manifest: add launch_handler: { client_mode: "focus-existing" } to the PWA manifest (packages/surface-app owns the manifest seam — confirm at the tree; if the manifest is kolu-owned, it’s a kolu file change only).
  2. packages/client/src/deepLink.ts — the grammar as a discriminated union, parse-don’t-validate: split the hash, validate <host> with isEncodedHostKey and <terminalId> with TerminalIdSchema (the attentionNotify.ts pattern, verbatim bar the shape), ?line= a positive int. Output DeepLink | { kind: "invalid"; reason } — never a partially-valid route.
  3. Wire three entry points in App.tsx boot: (a) parse location.hash once at startup; (b) hashchange listener; (c) window.launchQueue?.setConsumer reading the launch targetURL (Chromium; on browsers without it the plain boot parse of the same URL covers the case — one grammar, two delivery paths, no knob).
  4. Route through the existing actions: #/hsetActiveHost; #/t → switch-then-focus (reuse useAttention’s ordering — host first, then focusTerminal, never the reverse); /code → right panel showCode + select path (+ scroll to line if the Code tab exposes line targeting — ground CodeTab’s selectedPath/viewer at build time; if line-scroll needs new Code-tab capability, ship path-only and record ?line= as accepted-but-inert with a pointer here, one line — and in the parsed type, line lives inside the code variant, so line-without-path has no encoding);The right panel is per-terminal state (useRightPanel, persisted via session restore) — which is why file links anchor on a terminal: #/t/…/code?path=…, never a bare #/f/…. A file link without a terminal has no panel to open in. /inspectorshowInspector; #/settings → open the dialog.
    • 4a — sub-terminals (splits): every routed terminal id goes through store.focusTerminal(id); for a split, the verb writes its remembered panel chrome and the one focus fact, from which the parent tile is derived. Done-criterion: an e2e deep-links to a split terminal and asserts the pane is live — the route is not done when only main tiles focus.
    • 4b — the cold-boot membership race: a cold-start link fires before the target host’s terminal membership has loaded; deciding “gone” at that moment would toast every bookmark spuriously. The route defers its verdict until the host’s membership settles (bounded — then the gone-toast). The reuse seam, corrected at build-time grounding: CodeTab’s settle-then-verdict pattern (right-panel/CodeTab.tsx — the pendingOpen effect gated on allPaths.pending()), applied to the terminal list. (The originally-named useAttention sequencing was checked and is the wrong precedent — it is optimistic fire-and-forget: no defer, no gone-verdict.) “Exists” is only knowable at the post-connect store — the guarantee lives there, not at parse time.
  5. Loud edges: gone-target and invalid-route toasts (the existing toast seam); after a handled route, leave the hash in place (durability); never history.replaceState-strip it (that is the notification param’s semantic, not ours).
  6. Tests: parser unit table (valid × each family, invalid host/id/route/line); an e2e that boots with #/t/<host>/<id> and asserts the tile is focused, plus a live hashchange navigation; the negative pin — grep-shaped — that no route handler calls a mutating client verb.
  7. Dashboard integration (rides free): the orchestrator dashboard’s terminal tags become hrefs in this grammar.

Risks, named: launchQueue is Chromium-only — the fallback is the boot parse of the same URL (safe: same truth, not a degraded mode). Host keys in URLs expose ssh targets on screenshares — accepted for v1 (they’re already visible in the host chips); revisit under DL2’s privacy note. The ?line= fork is pinned in step 4 so the implementer never stalls on it.

DL2 — the address bar follows focus (planned, gated on srid’s go)

Write the hash on focus/panel change so the URL always deep-links to the current view — every session becomes bookmarkable and shareable mid-flow. Costs to weigh at the gate: history noise (mitigate with replaceState, no history entries), and terminal IDs + host keys permanently visible in the address bar (the screenshare consideration). One design obligation named now so DL2 can’t ship without it: the hash gains two writers (user navigation and the follow-focus writer), and a naive programmatic write re-fires hashchange → re-route → focus change → write — a feedback loop. DL2 must carry an explicit echo-suppression discipline (write via replaceState and/or a self-write marker the router skips), with the authority rule above (client state owns the view; the URL never routes back its own echo) as the invariant a test pins. Not started until ruled; DL1 is complete without it.