From ecbdc1099299eeaf99f9a54edd0a50bdf3d87c60 Mon Sep 17 00:00:00 2001 From: mintaka Date: Tue, 6 Oct 2026 19:25:03 -0400 Subject: [PATCH 1/7] docs(ui): Compass brand-parity design record (RIG-4774) Design record for matching Compass to rigel.build: brand faces, square corners, site greens and color hierarchy, status-glyph data path and cells, message anatomy, text-entry primitives, the needle mark, the surface/line model, and the agent tree view. Four open questions for Matt. Co-authored-by: Matt Wilkinson --- .../designs/ui/compass-brand-parity/design.md | 648 ++++++++++++++++++ 1 file changed, 648 insertions(+) create mode 100644 docs/designs/ui/compass-brand-parity/design.md diff --git a/docs/designs/ui/compass-brand-parity/design.md b/docs/designs/ui/compass-brand-parity/design.md new file mode 100644 index 000000000..299625fd1 --- /dev/null +++ b/docs/designs/ui/compass-brand-parity/design.md @@ -0,0 +1,648 @@ +# Design: Compass Brand Parity with rigel.build + +Builds on: [UX foundation](../compass-ux-foundation/design.md) (DL-148, DL-150, DL-184), [glyph primitives](../compass-glyph-primitives/design.md) (DL-367), [channels in the agent tree](../compass-channels-in-agent-tree/design.md) +Refs: RIG-4774 (parent RIG-4770); RIG-1622 (agent tree); ledger DL-418 to +DL-421, DL-423, DL-424, DL-430 (DL-422 and DL-425 are held for Open +Questions 2 and 4) +Depends on: #1811 (RIG-4771 chrome cleanup), #1814 (RIG-4772 session +stream), #1644 (DL-399 local baselines) +Siblings: RIG-4771 (chrome cleanup, pane edges), RIG-4772 (Session Log +stream), RIG-4773 (Bridge toggle), RIG-4775 (Settings), RIG-4776 +(Backlog/Done). This record does not redo their work. + +## Problem / Intent + +Matt's dogfood review: the Compass UI does not look like +[rigel.build](https://rigel.build). The colors look bad, the agent status +icons do not show, messages are plain text, the composer does not read as a +composer, and the top search, new-topic name, and first-message fields look +unstyled. This record finds the concrete causes in `apps/ui/src/` and plans +ten small PRs that bring Compass to the marketing site's look, with the +agent tree as the highlight. + +Out of scope: Settings (RIG-4775), the Backlog/Done views (RIG-4776), usage +display (RIG-4771 deletes `UsageBar`), and the pane-edge rules themselves. +PR #1811 (RIG-4771) puts the `.right` and `.log-panel` edges on +`--cx-border-strong` and hands the token values back to this record; T2b +sets them. + +## Approach + +The reference is the public site: [rigel.build](https://rigel.build) and +[rigel.build/compass](https://rigel.build/compass), whose shipped CSS uses +the same `--rigel-*` primitive values as `apps/ui/src/design/tokens.css` +(night `#011627`, panel `#0e2a45`, night-2 `#0b2942`, fog `#d6deeb`, haze +`#89a4bb`, mute `#5f7e97`, blue `#82aaff`). The primitives already match. +The gaps are in five places: fonts that never load, a surface and line model +that hides edges, semantic aliases and weights that differ from the site, +component CSS that is never imported, and an agent-state data path that +drops most states. + +### Gap audit + +| Site element (rigel.build) | Compass today | Gap | Task | +| --- | --- | --- | --- | +| Brand faces load from `@font-face` (`/fonts/SpaceMono-Regular.ttf`, `/fonts/DepartureMono-Regular.otf`) | No `@font-face` anywhere under `apps/ui`; `--cx-font-ui: var(--rigel-mono)` names Space Mono, but nothing loads it, so the host falls back to `ui-monospace` | The whole UI renders in the wrong face outside CI (CI pins the faces through fontconfig, so baselines hide it) | T1 | +| Display face for headings (`.bridge-heading` in Departure Mono 22px) | `--cx-font-display` is used by one rule in `app.css`; the face never loads | Fixed when the face loads | T1 | +| Square corners everywhere: cards, inputs, `.seg`, and square status pips (`.dot` 10×10, `.ci-badge` 7×7) | `--cx-radius-sm/md/lg` are 3/6/10px, used by 57 declarations; 26 more literal radii (`.tree-agent` 4px, `.bridge-link .count` 10px, round pips at 50%, 999px pills) | Rounded corners and round pips are off-brand | T2a | +| Night-2 hairlines drawn on the night page (`html,body{background:var(--rigel-night)}`, `.topbar{border-bottom:1px solid var(--rigel-night-2)}`); raised is kept for cards, menus, the root card, and the human message | Topbar, `.left`, `.right`, and `.log-panel` sit on `--cx-bg-raised`; `--cx-border` is `color-mix(in srgb, var(--rigel-panel) 70%, transparent)`; night-2 and raised are the same hex (`#0b2942`) | Pane edges barely show; the site's own line color would vanish on raised chrome | T2b (Open Question 1) | +| Form input: raised ground, mute border, 11×14 padding, blue border on focus (site `input` rule) | Text boxes have no visible edge | No field border token | T7 | +| CI success badge in green (`.ci-badge[data-status=success]{background:var(--rigel-green)}`), approved review in cyan (`.review-badge[data-verdict=approved]{background:var(--rigel-cyan)}`) | `--cx-ci-pass` and `--cx-review-approved` both resolve to `--cx-ok`, the syntax-tier `--rigel-success` (`#22da6e`) | Wrong greens on every board card | T9 | +| Hierarchy by color, case, and letter-spacing; no `font-weight` outside `@font-face` | 48 `font-weight: 600/700/bold` rules in `app.css`; Space Mono ships 400 and 700 only and `base.css` sets `font-synthesis: none`, so every 600 renders bold | Chrome reads heavy and loud | T9 | +| Agent state glyph: 9×9 cells drawn at 16px (`.ac-glyph svg{width:16px;height:16px}`), state-colored, state label beside it; alive glyphs pulse 0.7→1 (`ac-pulse`) | `StateDot` draws 9×9 CSS px; live data reaches only 4 of 8 states; most rows show a grey hollow square; `cx-state-dot-pulse` dips to 0.4 | Icons "do not show" (root cause below) | T3, T4 | +| Manager tree: depth-indented agent cards with spine elbows, glyph, name, colored state label, issue chip; root card raised with a 2px blue left rule; a blue pip runs down live spines (`spine-flow 1.4s` on `.tree-spine[data-flow="1"]`) | Left-sidebar text rows (`.tree-agent`, 12px name, 9px dot); `.tree-children` is a static 1px rule; no overview | No agent overview, no tree motion | T5 (Open Question 2) | +| Thread message: author in fog 12px, kind tag (`MANAGER`, `HUMAN`), `ASK` flag in blue and `STEER` flag in cyan (`.msg-flag.ask`, `.msg-flag.steer`), time right-aligned in mute, body in haze 13px; human messages on raised with a 2px blue left rule | `.msg-role` colors authors blue (`--cx-accent`), bright green (`--cx-ok`), or magenta (`--cx-author-system`), weight 600; `.msg-at` sits beside the name; no tag or flag | Messages read as plain text with loud names | T6 | +| (no composer on the site; the site `input` rule is the reference) | `Composer` is a one-line ``; its border is `--cx-border` on `--cx-bg-raised` | Composer does not read as a composer | T7 | +| Text inputs | `.new-topic-name`/`.new-topic-message` carry only flex sizing (browser-default box); `.topbar-search-input` has no `font: inherit` and a faint border | Fields look unstyled | T7 | +| (site CSS is all loaded) | Nine primitives are never imported: `ask`, `badge`, `button`, `input`, `loader`, `panel`, `tabs`, `toast`, `tree`. Two have users today: `ShortcutsOverlay` renders `cx-search`, and `Palette` renders `.cx-loader[data-topology="bar"]`; both are unstyled | The primitive library is mostly unwired, so users of it fail silently | T7 | +| Compass needle mark (`/compass-c-needle.svg`, purple north half on the navy tile) | Top bar shows the 11×11 `logo` `Glyph` in `--cx-accent` blue | Product mark missing | T8 (Open Question 4) | + +### Why the status icons do not show + +The SVG itself is fine: `StateDot` (`apps/ui/src/components/StateDot.tsx`) +emits a `viewBox="0 0 9 9"` `crispEdges` SVG of `rect` cells filled with +`currentColor`, and `state-dot.css` colors it per `data-state`. The cause is +upstream and in sizing: + +1. **The live set is coarse.** Every agent row renders + `` (`AgentLeaf` in + `LeftSidebar.tsx`). `lifecycle` comes from `joinAgents` + (`apps/ui/src/roster.ts`): `lifecycle: info ? info.lifecycle : "stopped"`, + where `info` is the comms presence entry. Presence is a 4-value enum + (`AgentPresence` in `proto/compass/v1/comms.proto`: IDLE, WORKING, + WAITING, OFFLINE), and `presenceLifecycle` (`apps/ui/src/live/adapt.ts`) + maps `OFFLINE → "stopped"`. The server's `presenceFor` + (`go/internal/presence/presence.go`) folds STOPPED, ERRORED, + DISCONNECTED, and "no session" into OFFLINE. So live Compass can only + show working, idle, waiting, and stopped. Done, paused, error, and + disconnected never render. +2. **The full session state is dropped.** `AgentSessionStatus` (with its + `state`) does arrive on `SubscribeEvents`, but on main `applyPayload` in + `runEventStream` (`apps/ui/src/live/events.ts`) keeps only + `runtime.set(account, adaptRuntimeMarker(payload.value))`. + `adaptRuntimeMarker` documents that "the identity/lifecycle fields the + status also carries are the presence path's job". #1814 (RIG-4772) keeps + the state for the Session Log, but nothing feeds it to the dot. + `agentDotState` (`apps/ui/src/agent-state.ts`), the 8-state projection + built for this, has no production caller. +3. **Most rows are grey.** Any agent without a live session is "stopped": a + hollow outline in `--cx-st-stopped` (`--rigel-mute`). Idle is a mute + block too. In a fleet that is mostly idle or offline, the colored glyphs + (green `»`, amber `?`, cyan tick, red `!`) almost never appear. +4. **The glyph is small.** `.cx-state-dot` is `width: 9px; height: 9px` + (`state-dot.css`). The brand spec names "the 12px row-dot size" + (`docs/specs/brand/state-icons.md`), and the site draws the tree glyph at + 16px. +5. **Geometry drifted.** The site's cells for `working`, `waiting`, and + `done` differ from `STATE_CELLS`. Site `working` is two 5-cell chevrons + (`1,2 4,2 2,3 5,3 3,4 6,4 2,5 5,5 1,6 4,6`); Compass draws two 7-cell + chevrons over rows 1-7. Site `done` is a 7-cell tick + (`1,4 2,5 3,6 4,5 5,4 6,3 7,2`); Compass draws 9 cells. `idle` matches. + +### A1 — Ship the brand faces + +Load Space Mono (regular, bold), Departure Mono, and IBM Plex Mono with +`@font-face` in `tokens.css`, from font files under +`apps/ui/src/design/fonts/`. The files are the OFL faces already committed +in `apps/eng-docs/public/fonts/`. Vite bundles them as assets for both hosts. +`font-display: swap`, as on the site. + +CI pins the faces through fontconfig (`fontDirs` in +`tools/toolchain/chromium-e2e-env.nix`: nixpkgs `google-fonts` Space Mono and +`departure-mono`). The `SpaceMono-Regular.ttf`, `SpaceMono-Bold.ttf`, and +`DepartureMono-Regular.otf` files in `apps/eng-docs/public/fonts/` have the +same SHA-256 as those nixpkgs files, so the glyph outlines in CI do not +change. Shots can still differ if Chromium rasterizes a webfont differently +from a system face; T1 treats any diff as expected and recaptures. + +### A2a — Square corners + +The site draws no rounded corner except one footer tile, and its status pips +are square (`.dot` 10×10, `.ci-badge` 7×7). `docs/specs/brand/surfaces.md` +asks for "flat square cards". Compass has 57 `var(--cx-radius-*)` +declarations and 26 literal radii, including `.tree-agent` 4px, the round +pips (`border-radius: 50%`), and 999px pills. + +Delete `--cx-radius-sm`, `--cx-radius-md`, and `--cx-radius-lg` from +`tokens.css`, and delete every `border-radius` declaration under +`apps/ui/src`. A stylelint rule keeps it that way. Three tokens all set to 0 +would be dead weight. This supersedes the radius line of ux-foundation D1 +("Radius: `--cx-radius-sm|md|lg` (3/6/10)"). + +### A2b — Surfaces and lines (Open Question 1) + +The site draws night-2 lines on night ground: `html,body` are night, +`.topbar` has `border-bottom:1px solid var(--rigel-night-2)`, and its product +mock puts thread and lane heads on panel. Raised is kept for cards, menus, +the root agent card, and the human message. Compass puts all chrome on +`--cx-bg-raised`: `.topbar`, `.left`, `.right`, and `.log-panel`. Night-2 and +raised are the same hex (`#0b2942`), so the site's line color is invisible +on Compass chrome. `--cx-border` (panel at 70%) and `--cx-border-strong` +(panel) are within a few levels of raised, which is why the pane edges in +the ask do not show. Note that `apps/ui/src/design/surfaces.md` already +puts the topbar on `--cx-bg`, so `app.css` has drifted from its own spec. + +The surface model is Open Question 1. Settled under every option: a new +`--cx-border-field: var(--rigel-mute)`, the site's input edge, used only by +`input.css`. + +### A3 — Agent lifecycle is a client-side join over session status + +T3 builds on #1814 (RIG-4772). That PR keeps one `AccountSession +{ sessionId, state }` per agent account from `AgentSessionStatus` events in +`runEventStream`, emits it through `onSessions`, clears it on resync, and +stores it as `accountSessions`. T3 adds no second map. It projects that map +through `agentDotState` in `joinAgents`: + +| Inputs for an account | `lifecycle` | +| --- | --- | +| session known | `agentDotState(session.state, { awaitingInput, turnDoneUnopened })` | +| no session, presence entry | `presenceLifecycle(presence)` (today) | +| neither | `"stopped"` (today) | + +- `awaitingInput` is `presence?.lifecycle === "waiting"`. The server sends + presence WAITING for STARTING or READY with an open ask (`presenceFor`). + Today `agentDotState` reads `awaitingInput` only for STARTING and WORKING; + READY returns `turnDoneUnopened ? "done" : "idle"`. T3 changes the READY + arm so the order is **waiting > done > idle**. Without that change the + join would turn every waiting agent grey. +- `turnDoneUnopened` is `turnEndedAtUnixMs > lastOpenedAt(account)`. + - `AccountSession` gains `turnEndedAtUnixMs?: number`. `applyPayload` sets + it to the event time (`SubscribeEventsResponse.at_unix_ms`) when the + account's previous state in the map is WORKING and the new state is + READY. Other events for the same `sessionId` carry the previous value. + - `lastOpenedAt` is a per-agent time persisted in `localStorage` under + `compass.lastOpened.${workspace}`, the same shape as the pin store + (`loadPinnedAgents`/`savePinnedAgents`). `openAgent(id)` writes + `max(Date.now(), turnEndedAtUnixMs)`, so a client clock behind the + server cannot leave the tick on. While an agent's view is open, a new + turn end also updates its `lastOpenedAt`. + - Because both sides are times, a cold-start ring replay and a page reload + give the same answer as the live stream. A resync clears the map; the + replay re-observes the edge with its original time. The residual: an + agent whose WORKING event fell out of the 1024-event ring shows `idle`, + not `done`. + +No proto change. Done, error, and disconnected become reachable, and +waiting is kept. Whether to seed at cold start is Open Question 3; the plan +assumes no seed. + +### A4 — Site glyph cells and an integer 2× scale + +Replace the `working`, `waiting`, and `done` entries of `STATE_CELLS` with +the site's cells (listed in the root-cause section; site `waiting` is +`3,1 4,1 5,1 2,2 6,2 6,3 4,4 5,4 4,5 4,7`). The other five glyphs have no +site render and stay. `StateDot` gains `scale?: 1 | 2`; `2` sets +`data-scale="2"` and an 18px box. Integer scale keeps every cell a whole +2×2 block; the site's 16px is a non-integer 1.78×. The sidebar tree and the +agent tree (A5) use 2×; dense spots (right-sidebar tab badge, Bridge cards, +agent header) keep 1×. + +The site's working glyph breathes from 0.7 to 1 (`ac-pulse`). Compass +`cx-state-dot-pulse` dips to 0.4, which reads as blinking. Set the low +point to 0.7. + +### A5 — The agent tree, the highlight (Open Question 2) + +The anatomy is settled; where it lives is Open Question 2. The plan assumes +the recommended option: a new main view at `/agents`. The view draws the +site's Manager tree from `agentTree(store.agents())` (`stub-data.ts`): + +- Row: `padding-left: depth × 26px`, with a 1px elbow spine in + `--cx-border` to the parent (site `.tree-row`, `.tree-spine`). +- Card: grid `20px 1fr auto`; 2× `StateDot`; handle; meta line with + `AGENT_STATE_LABEL[state]` in the state color; issue chip on the right. No + child count: the site's `N workers` counts dispatched workers, Compass has + no live worker source, and the spine already shows child agents. +- Root cards: raised ground and a 2px `--cx-accent` left rule. +- Issue chip (`agentIssueChip`): the agent's active issues + (`assignee === account.id && isActiveState(state)`); none → no chip; one → + `issueKey(issue, multiForge)`; more → `N open`. +- Spine flow: a spine to a `working` child carries `data-flow="1"`, and a + 1×6px `--cx-accent` pip runs down it (site `spine-flow`, 1.4s). This is the + chase-light pip that ux-foundation D6 names in the Manager-tree reference. + It is the view's one unbounded motion; the state-dot column stays the + sanctioned exception (`apps/ui/src/design/motion.md`, Budget). The period + is a new token zeroed under reduced motion, like `--cx-pulse-period`. +- Click or Enter on a card calls `store.openAgent(account.id)`. + +The sidebar tree stays the compact navigator and gets the 2× glyph (A4). + +### A6 — Message anatomy + +`MessageRow` (`ChannelView.tsx`) is the only emitter of `.msg`, and the agent +view's chat goes through the same `ChannelView`, so the rules restyle `.msg` +directly with no extra scope. The row emits the site anatomy: + +- `.msg-author`: handle, `--cx-text`, `--cx-text-sm`. +- `.msg-tag[data-kind]`: `AGENT`, `HUMAN`, or `SYSTEM` from the author's + `kind`; 1px border, `--cx-text-xs`, letter-spaced. The site says + `MANAGER`; Compass uses its own domain word. +- `.msg-flag`: `ASK` (`--cx-accent`) when the message has an ask block, and + `STEER` (new `--cx-msg-steer`, cyan) when a text block @-mentions an agent + account. The `Composer` doc defines an @-mention of an agent as a steer. +- `.msg-at`: pushed right (`margin-left: auto`), `--cx-text-faint`. +- Body: `--cx-text-dim` at `--cx-text-md`. A human message gets + `--cx-bg-raised` and a 2px `--cx-accent` left rule, with body in + `--cx-text`. + +Delete the `.msg-role` color rules and `--cx-author-system`; nothing else +uses them. + +### A7 — Text entry uses the primitives + +Each component imports the primitive CSS it uses, as `RuntimeMarker.tsx` +imports `runtime-marker.css` and `Palette.tsx` imports `palette.css`. Nine +primitives are never imported today. This wires the three that have or gain +a user (`input`, `button`, `loader`). The six with no user (`ask`, `badge`, +`panel`, `tabs`, `toast`, `tree`) stay unimported until a component first +renders them; importing unused CSS adds bytes and no fix. + +- `TopBarSearch` imports `input.css`; its input → `cx-search`. +- `ShortcutsOverlay` imports `input.css`; its `cx-search` field starts + rendering as designed. +- `Palette` imports `loader.css`; its `.cx-loader[data-topology="bar"]` + starts rendering. +- `ChannelView` imports `input.css` and `button.css`. + - `NewTopic` name and first-message inputs → `cx-input`; start button → + `cx-btn`. + - `Composer` → `