DESIGN.md@apps/desktop · git:20260911.b68cc9c · 2026-09-11 · sha256 7ddc202ecedade54

DESIGN.md@apps/desktop git:20260911.b68cc9cA

Immutable. This exact content never changes and is served at /api/v1/blob/7ddc202ecedade54.

# Harness Kit v2 — Design Spec (canonical)

**Status:** Signed off 2026-07-03. Direction **A "Instrument"** selected.
**This file is the source of truth for all v2 UI work.** Agents build against this, never invent. When this conflicts with older styling on `main`, this wins. Token values here are expressed against the **existing CSS variable names** in `packages/design-tokens/src/tokens.ts` so rollout is a values-swap + `pnpm generate:tokens` — do not rename variables.

Product framing (why the UI looks the way it does): Harness Kit v2 is **the control plane for your AI coding harnesses** — a dense professional instrument that is scanned and operated, not read top-to-bottom. It is dogfooded daily and must also win a stranger's first five minutes. It is NOT a marketing site; marketing scale lives only on the website.

---

## 0. Direction A "Instrument" — the one-line brief

Warm graphite base + a single azure accent. Linear/Warp lineage but **warm-tinted, not cool slate** — the warm neutral is the whole differentiator; drifting to blue-grey slate makes it generic. Data is mono, dense, and calm. Azure appears only on the active nav rail, primary actions, links, focus, and accented numerals. Everything else is quiet.

---

## 1. Anti-slop rules (hard rejects in review)

Reject any UI that contains:
- Cool blue-grey **slate** neutrals (this direction is warm graphite — `#131215`, not `#0f172a`). Purple/indigo/violet accents. Purple→blue gradients.
- Glassmorphism, gradient hero text, glow-on-everything, floating 3D blobs.
- **Emoji as icons.** Icons are Lucide only, `viewBox="0 0 24 24"`, stroke 1.7, `w:16` default (20 for headers). One set, consistent stroke.
- Visible 1px neutral borders as the default separator. **Borderless is a core principle** — separate with background elevation steps + spacing + soft shadow. `--border-*` alphas are deliberately near-invisible; use them only as hairline reinforcement, never as the primary divider.
- Marketing aesthetics inside the app: oversized type, 48px section gaps, centered hero layouts.
- Four-stat-cards-in-a-row as decoration. Show data the user acts on, or nothing.
- **Inline hex or spacing literals in page code.** Every themed property comes from a token var through a `packages/ui` component. (Lint-enforced in `apps/desktop/src/pages`.)
- Exclamation marks in app copy. Vague errors. System-jargon labels.

---

## 2. Color tokens (map onto existing var names)

Status colors are **shared across themes** and are NOT the accent. Only azure is the accent.

### Dark (default)
```
--bg-base:        #131215   /* warm graphite — NOT slate */
--bg-surface:     #1B191E   /* panels, cards, table body */
--bg-elevated:    #242128   /* header rows, chips, hover targets */
--bg-sidebar:     rgba(27,25,30,0.85)
--fg-base:        #EDEAE6   /* warm off-white */
--fg-muted:       #A29C95
--fg-subtle:      #6C665F
--fg-placeholder: #514C46
--border-base:    rgba(255,255,255,0.05)
--border-strong:  rgba(255,255,255,0.09)
--border-subtle:  rgba(255,255,255,0.03)
--accent:         #2E9BE6   /* azure — active nav, primary btn, links, focus */
--accent-light:   rgba(46,155,230,0.14)   /* accent tint bg (active nav, hover rows) */
--accent-text:    #6BC0F5   /* azure text on dark, AA on --bg-base */
--accent-glow:    rgba(46,155,230,0.35)   /* focus ring / soft accent shadow */
--hover-bg:       rgba(255,255,255,0.04)
--active-bg:      rgba(255,255,255,0.07)
```

### Light
```
--bg-base:        #F4F2EF   /* warm paper-neutral — NOT pure #fff, NOT cool grey */
--bg-surface:     #FFFFFF
--bg-elevated:    #ECEAE5
--bg-sidebar:     rgba(244,242,239,0.85)
--fg-base:        #1A1714
--fg-muted:       #57514A
--fg-subtle:      #8B8479
--fg-placeholder: #A8A199
--border-base:    rgba(20,15,10,0.06)
--border-strong:  rgba(20,15,10,0.10)
--border-subtle:  rgba(20,15,10,0.03)
--accent:         #2E9BE6   /* icons / large accents / fills */
--accent-light:   rgba(46,155,230,0.10)
--accent-text:    #1668A6   /* azure TEXT on light must use this (AA on white); #2E9BE6 fails small-text contrast */
--accent-glow:    rgba(46,155,230,0.25)
--hover-bg:       rgba(20,15,10,0.04)
--active-bg:      rgba(20,15,10,0.07)
```

### Status (shared, both themes)
```
--success:  #3FB07A   --success-light: rgba(63,176,122,0.14)
--warning:  #E0A33D   --warning-light: rgba(224,163,61,0.16)
--danger:   #F2555A   --danger-light:  rgba(242,85,90,0.14)
```
Drift counts use `--warning`; conflicts/denials use `--danger`; in-sync uses `--success`. Status chips = tinted bg + colored text + a 6px dot. **Never solid-fill pills.**

### Shadows (warm-tinted in light)
```
Dark:  --shadow-sm: 0 1px 2px rgba(0,0,0,.4)
       --shadow-md: 0 6px 20px -6px rgba(0,0,0,.55)
       --shadow-lg: 0 16px 40px -12px rgba(0,0,0,.6)
Light: --shadow-sm: 0 1px 2px rgba(20,15,10,.08)
       --shadow-md: 0 8px 24px -8px rgba(20,15,10,.14)
       --shadow-lg: 0 18px 48px -16px rgba(20,15,10,.18)
```

---

## 3. Typography

- **UI face:** system stack `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`. No webfonts in the app.
- **Data/mono face:** `ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace`. Use for: file paths, YAML, versions, counts in tables, diffs, MCP commands, harness IDs. `font-variant-numeric: tabular-nums` anywhere digits align in columns.
- A distinctive display face is allowed on the **website only**, never in the app.

Scale (px / weight):
| Role | Size | Weight | Notes |
|---|---|---|---|
| Page title | 19 / 650 | | `letter-spacing:-.01em` |
| Section headline | 13 / 600 | | |
| Body / table cell | 12.5–13 / 450 | | line-height 1.5 |
| Caption | 11 / 450 | `--fg-muted` | |
| Micro-label (uppercase) | 10.5 / 650 | `--fg-subtle`, `letter-spacing:.07em`, `text-transform:uppercase` | column headers, group labels |
| Big metric | 23 / 660 | tabular-nums, `-.02em` | summary strip, onboarding stats |
| Mono data | 10.5–12 | | versions/paths/values |

Headings get `text-wrap: balance`. Running prose (rare in-app) stays ≤ ~65ch.

---

## 4. Layout, spacing, motion, interaction

- **Density:** compact. Table/list row height **46px** (this direction is data-dense but not cramped — do not go below 40). 8px spacing grid: 4 / 8 / 12 / 16 / 24. Main content padding 24–28px.
- **Sidebar:** 216px (240 acceptable), collapsible, `--bg-sidebar` with `inset -1px 0 0 --border-base` (no hard border).
- **Elevation model:** `--bg-base` (app) → `--bg-surface` (panels/cards) → `--bg-elevated` (header rows, chips, hover). Depth comes from these + `--shadow-*`, not outlines.
- **Radius:** 8px controls/cards, 7px chips, 6px small icon tiles, 11–12px large panels.
- **Motion:** 150–250ms ease-out, `transform`/`opacity` only. No spring/bounce. Respect `prefers-reduced-motion` (kill transitions/animations).
- **Interaction:** `cursor:pointer` on everything clickable. Hover = background tint (`--hover-bg` or `--accent-light` on rows), **never a scale transform that shifts layout**. Visible focus ring: `2px solid --accent` (or `--accent-glow` box-shadow), offset 2px. Cmd+K command palette with a **central command registry** (adding a page auto-registers its command — no hardcoded palette list).
- **Empty states teach:** every empty surface states what it's for + why it's empty + one action button. Never a blank pane.
- **Copy voice:** plain, specific, count-forward ("5 harnesses, 5 drifted", "Drift 3"). Buttons say what happens ("Recompile all" → toast "Recompiled 4 configs"). Errors: what broke + how to fix. No exclamation marks.
- **Labs:** entries carrying a `labs` key in `apps/desktop/src/nav.ts` (Comparator today) are hidden from the sidebar, the ⌘-number shortcuts and the command palette until enabled in Settings › Labs. The route itself stays mounted, so a direct navigation still renders it (ux-consolidation spec AC-8, design D4).

---

## 5. Information architecture

Sidebar, top→bottom, declared once in `apps/desktop/src/nav.ts` — the sidebar, the ⌘-number shortcuts, the command palette and the default-section setting all derive from that one module (ux-consolidation spec AC-2, AC-4; design D2):
```
[logo] Harness Kit
  Machine        (home; ⌘1)   cross-surface grid + an embedded Drift section
  Profile        (⌘2)         harness.yaml · Compile to project
  Claude Code    (⌘3)         Instructions · MCP servers · Plugins · Hooks · Permissions · Usage
  Marketplace    (⌘4)
  Comparator     (Labs only — hidden until enabled, see §4)
  ─────
  Settings       (pinned bottom; ⌘,)
```
No "Configure" group (retired — spec AC-3, design D5). No Fleet. No Drift or Observatory as top-level items — Drift is a section embedded inside Machine, not a destination of its own, and Observatory's pages render as "Usage" inside Claude Code (spec AC-9; see §6). No Services/Board/Roadmap/Chat/Memory/Terminals. Nothing else.

`/fleet` and `/drift` (and the other retired routes: `/agents`, `/harness/settings`, `/security/*`) redirect to their consolidated destination and preserve any query parameter the destination reads (spec AC-6). `/machine?drift=1&harness=<id>` opens Machine's Drift section pre-filtered and scrolls it into view (spec AC-7).

Two groups grow further capability in later phases without changing this shape: Machine's grid gains a persistent legend and clickable Gaps/Diffs filtering in P2 (spec AC-13, AC-14) and a team-baseline column in P3 (spec AC-26), and Profile gains "Share" in P3 (spec AC-25). Neither is built yet — see the Machine contract in §6 and the Profile note there for what ships today.

Active nav item: `--accent-light` background + a 2.5px inset azure rail on the left (`--accent`), `font-weight:550`. Inactive: `--fg-muted`, hover → `--bg-elevated` + `--fg-base`.

---

## 6. Screen contracts

Reference implementation of the visual language: the signed-off mock (Fleet — now shipped as Machine, see D1 in [ux-consolidation/design.md](../../specs/ux-consolidation/design.md) — + Onboarding, Direction A). Match its structure and token usage.

### Machine (home) — this screen IS the product pitch
1. **Page head:** title "Machine" + one-line subtitle ("Every AI-harness resource on this machine, across all supported surfaces — read-only.") + primary action "Refresh" (right; reads "Scanning…" while a scan is in flight).
2. **Project directory:** an optional monospace text input + "Browse…" (native picker) + "Clear" (shown once a directory is set). The scan is machine-only by default; picking a directory adds project-scope stores to it. A `--warning`-toned notice ("Project directory could not be scanned — showing machine-only results") appears if the chosen directory degrades.
3. **Summary strip:** a single elevated bar — Resources (n) · Gaps (n, `--warning` when > 0) · Diffs (n, `--warning` when > 0) · Surfaces detected (n/total). Big-metric type, tabular-nums, uppercase micro-labels. The persistent legend and clickable Gaps/Diffs filtering described in spec AC-13/AC-14 (design D7) have not shipped yet — today these are static counts, not controls.
4. **Grid:** rows = resources grouped by kind (uppercase group header), columns = surfaces grouped by product family (grouped header row); an undetected surface's column is dimmed and labelled "not installed". Cells: `—` = no such concept on this surface, a dimmed `·` = has the concept but keeps no store here (unmanaged locally, not a gap), `?` = unknown (surface installed, store may be hidden), an azure dot + mono `u`/`p` = present (tooltip lists every entry's digest, file and scope), blank = absent. Surface headers carry a skipped-entries count badge and, where readable, a marketplace-count badge. Row click opens the drawer; selected row = `--bg-elevated`.
5. **Row drawer:** a 380px right-side panel — "Where it lives" (per-surface entries: scope, file path, short digest) and, when the row has cross-surface differences, "Differences" (each delta's path, kind, and left→right values). Below that, the copy action: a target-surface select, the resolved CLI command, a capability-loss or overwrite-confirmation checkbox when the plan requires one, a secret-value notice when applicable, and Apply / Copy CLI command / Copy prompt buttons. The source side of this is fixed today — always the first present surface in the row (`presentSources(row)[0]`); the selectable *source* control design D8 describes has not shipped.
6. **Drift section:** an embedded, collapsed-by-default disclosure below the grid ("Drift from harness.yaml") that mounts the existing Drift page in place when opened. The redirected `/drift` route and `/machine?drift=1&harness=<id>` expand it and scroll it into view automatically (spec AC-6, AC-7). Its own summary-strip cell and the view-toggle redesign in design D6 are P2 work; today it stays a manually-expandable section, not a `view=drift` query state.
7. **Skipped diagnostics:** a collapsible disclosure listing, per surface, each skipped file and the reason it was skipped.

### Drift (embedded in Machine) — actionable, never a dead-end
Items grouped by project → harness. Each item shows: classification badge (repairable-inside-markers / user-edited-outside / missing / orphaned), an inline **mono diff viewer** (syntax-highlighted, red/green gutters using `--danger`/`--success` tints), and a per-item **Fix** button that previews the exact plan before applying. Header offers **Fix all** (dry-run preview first). User-content drift offers only Acknowledge / Review — never silent overwrite. Every row has an action.

### Profile and Claude Code (editors)
The generic "Configure" group is retired — no group by that name returns (spec AC-3, design D5). Editors now live under two sidebar groups: **Claude Code** (Instructions, MCP servers, Plugins, Hooks, Permissions, Usage — each edits that one surface's own files) and **Profile** (harness.yaml). Today Profile's `/harness/file` is a single editor whose empty state offers three entry paths — "Generate from Claude Code setup" (scans existing config), "Start from a profile", and "Create from scratch" (the only one that opens a blank template) — each of which drops into the same Monaco editor, where "Save harness.yaml" then appears. Its planned end state (P3, spec AC-22–AC-25, design D11) is a Monaco YAML editor (left/main) + a structured side panel (plugins, MCP servers, instruction slots, permissions) that round-trips to the YAML, with the empty state replaced by the full **import flow** ("Scan this machine"). That full flow is not built yet, but "Generate from Claude Code setup" is already a real scan-based precursor to it today — narrower in scope (one surface, not Machine's planned multi-surface scan), not absent.

### Onboarding (first run) — the highest-craft screen
Full-bleed within the window (no app sidebar yet — it's a wizard). Sequence:
1. Scan progress ("Machine scan complete · 1.8s", azure pulse eyebrow).
2. **The sprawl reveal:** honest headline ("You run 5 harnesses. *They don't agree.*", the disagreement clause in azure), lede explaining "we read the config you already have — no authoring required", then a 4-up **stat row** (harnesses found / config files / overlapping instruction sets [warning] / direct conflicts [danger]).
3. **Convergence map:** source harness chips (each with file counts, drift counts in warning) → converging lines → a single `harness.yaml` card (mono preview: plugins/mcp-servers/skills counts in azure). Schematic, not heavy.
4. **Conflicts list:** concrete, specific rows in warning tint ("'Run tests before commit' appears in 3 tools with 2 wordings"; "MCP server `github` points at two commands"; "Copilot allows `rm -rf` where Claude Code denies it").
5. **CTA:** "Preview harness.yaml" (primary) + "Explore read-only" (ghost) + note "Nothing is written until you confirm." Declining leaves a fully usable read-only app. Every step carries a "Skip setup" ghost action top-right; a failed scan renders its error with "Retry scan" instead of an empty frame (ux-consolidation AC-32, AC-33).

### Comparator / Marketplace / Settings
Re-skin existing data behavior onto `packages/ui` + these tokens. No new features, no external-service affordances. Permissions lives under Claude Code; Secrets and Activity live under Settings (spec AC-10, AC-11). Observatory's pages render as "Usage" inside Claude Code, not as a standalone sidebar item (spec AC-9; see §5).

---

## 7. Component library (`packages/ui`) — minimum inventory

Build these token-driven, borderless-by-default (surface via `--bg-elevated`/`--bg-surface` + `--shadow-sm`, no neutral outline). Components live only in `packages/ui/src/components/`; the desktop-local `apps/desktop/src/components/ui/` set was removed (ux-consolidation AC-41) and must not return.

- `Button` (primary=azure fill+white / ghost=elevated / danger), `Sidebar`+`NavItem`, `SummaryStrip`, `Matrix`/`Table`, `StatusChip` (success/warning/danger/subtle), `DiffViewer` (mono), `Modal` (one implementation, kills the 3 legacy modal styles), `Toast`, `EmptyState`, `Stat`, `Card`, form controls (input/select/toggle), `CommandPalette`.

Every v2 page uses only these + token vars for themed properties. Theme toggle re-themes **every** page — no page may hardcode its own palette (the old Board mistake is banned).

---

## 8. Verification for any UI change

1. Build the screen, then Playwright-screenshot **dark + light** at 1440 / 1024 / 768.
2. Check against §1 (anti-slop) and §2–§7 before requesting review.
3. Final validation for desktop = `pnpm install:desktop`, launch the real `.app` (not a browser), confirm not-blank and theme toggle works. (macOS 26 release builds blank — use the debug binary + dev server; see repo memory.)
4. Contrast: azure text on light MUST use `--accent-text` (#1668A6). Verify 4.5:1 for any small text.