DESIGN.md · git:20260916.f72c601 · 2026-09-16 · sha256 6777117796449267
DESIGN.md git:20260916.f72c601A
Immutable. This exact content is served forever at /api/v1/blob/6777117796449267.
# DESIGN.md
How OpenWork UI is designed. Agents read this before building or reviewing any
user-facing surface (`apps/app`, `apps/desktop`, `ee/apps/den-web`, MCP Apps,
generated artifact views). Warden's `design-spec-review` skill checks PRs
against the numbered rules below and *warns* — it never blocks.
## Who we design for
Non-technical knowledge workers doing real work with agents on their own
files and connected services. Chat is the home surface; everything else
(tool activity, apps, panels, settings) supports the conversation. The user is
usually a *perpetual intermediate*: make the common path fast and obvious, keep
advanced options discoverable but quiet.
## Principles
- **P1 · Show state, don't explain the UI.** A surface reports what is true
("Verified 2 min ago", "Blocked by your team", "Not posted") instead of
describing itself. Count the sentences in the first viewport; if a sentence
explains the interface rather than reporting state, delete it.
- **P2 · Title *or* description, never both.** Pages, cards, rows, dialogs,
settings groups get one or the other. Detail lives in a tooltip, an
expandable row, or nowhere.
- **P3 · Progressive disclosure.** Default surface = the decision the user is
here to make. Advanced, rare, or diagnostic detail goes behind a labeled
collapsed row ("Technical details", "Blocked patterns") — label only, no
preview of the contents.
- **P4 · Presence with a lock beats absence.** When policy or permissions
remove a capability, keep the control visible, mark it blocked, and say
*why* and *who can change it*. Never let features silently disappear.
- **P5 · Reuse before you build.** Use the existing `@/components` primitive
(Button, Switch, Dialog, Popover, Command, Tool, Message…). Hand-rolling a
control that already exists is a defect. If no primitive fits, compose a
headless one; never re-implement keyboard/focus/ARIA behavior.
- **P6 · Density from data, not prose.** Linear/Vercel-dense: rows, values,
chips, state. Tight desktop density (12–16px padding), chosen once and held.
- **P7 · One focal element per screen.** Name it before building; make it win
through weight, contrast, or whitespace. Demote everything else.
- **P8 · Undo over confirm.** Confirm only destructive or external actions
(post, send, delete, pay). Everything else is reversible in place
("Requested today · Undo").
- **P9 · Consent names action, data and risk.** Approval cards say exactly
what will happen, to what, who sees it, and what is reversible — in one
state line. No apologies, no "please".
- **P10 · Evidence or it didn't ship.** New or changed UI needs a screenshot
(or short recording) in the PR at the real size. No screenshot, no review.
## Structure
- **S1** Flat, not boxed: no card-in-card, no bordered tile inside a bordered
tile. Group with whitespace and a single hairline.
- **S2** Settings and lists are compact rows: label left, current state right,
at most one action on the right. Rows 40–48px, hairline dividers.
- **S3** Expandable rows use `<details>`-style disclosure with a chevron that
rotates 90°. Open by default only when the content *is* the answer.
- **S4** Inline widgets (MCP Apps, tool cards, artifact previews) fit in one
scroll of the response, carry ≤ 2 primary actions, never scroll internally,
and never duplicate the assistant's text. Escalate to the side panel when
the task needs iteration, comparison, or editing — and say so with the
panel button, not by growing the widget.
- **S5** Nothing auto-navigates, steals focus, or opens a pane because a tool
produced something. A result may *offer* an inline action to open it.
- **S6** One action, one home: keyboard, palette, and visible control invoke
the same command. Show keyboard chords for frequent actions (`⏎`, `⌘⏎`,
`esc`).
## Copy
- **C1** Verb-first, outcome-specific labels: "Post to #launch", "Request
access", "Save changes" — never "Submit", "OK", "Click here".
- **C2** An action keeps its name through the flow: button "Post" → toast
"Posted"; "Publish" → "Published".
- **C3** Name things by what people control, not how the system works: a
person manages "connections", not "MCP server config"; never expose tool
ids, JSON, or `toolName` to end users.
- **C4** Tool activity is sentence-first and past/present tense: "Searched
Linear for open issues · 8 issues", "Read #launch", "Ran `pnpm test`",
"Drafting for #launch". Show duration, not internals.
- **C5** Blocked is not an error. Blocked/locked states use neutral ink and a
lock; red is reserved for failures.
- **C6** Errors and empty states give direction: what happened + the next
action. Empty states invite; errors never blame or say "Oops".
- **C7** Sentence case. No ALL-CAPS eyebrow labels, no middle-dot meta strings
as decoration, no "→" appended to buttons.
## States
Every data-bearing surface designs, and the PR shows, these states where they
apply: loading (layout-matching skeleton, not "Loading…" text), empty,
error, blocked/locked, offline or "couldn't verify" (show the last known
state and when it was confirmed), and success. Never rely on model text alone
to communicate system status.
## Visual system
Tokens live in `apps/app/src/app/index.css`, `styles/colors.css`,
`styles/tailwind-theme.css`. Use them; never hardcode.
- **V1** Type: system stack, 13px/1.5 body, `--text-sm` 0.8125rem, mono for
commands, ids and file chips. Hierarchy comes from weight + opacity
(600 / 500 / 400, `text-foreground` / `text-muted-foreground`) at the same
size before it comes from size. Headings ≤ 20px with slight negative
tracking.
- **V2** Color: Radix slate/blue/amber/red/green/violet scales via semantic
vars (`--background`, `--muted`, `--border`, `--primary`, `--destructive`).
Brand accent `--dls-accent` (#011627) for the primary action. Tinted
neutrals only — never `#000` / `#fff` literals in components. Color marks
the minority that needs attention; a normal state is neutral.
- **V3** Depth: hairlines as `rgba` (`--border`), lifted surfaces use the
layered `--dls-card-shadow` / `--dls-shell-shadow`, never a solid border
plus a shadow. Choose one depth strategy per surface and hold it.
- **V4** Radius: `--radius` (0.45rem) scale for controls, `--dls-radius`
(16px) for shells and the composer, `--dls-radius-lg` (24px) for windows.
Nested elements use concentric radii (inner = outer − padding).
- **V5** Icons: lucide, 16px, 1.5px stroke, round caps. Generic icons only in
navigation and toolbars. When a company/service is shown (Slack, Linear,
Google, GitHub…) use its logo mark, not a generic icon. No emoji as icons.
No sparkle / wand / magic / robot icons for "AI".
- **V6** Motion: 120–200ms, `ease-out` or `cubic-bezier(.2,.8,.2,1)`, no
bounce/elastic, no entrance animation on high-frequency surfaces (command
palette, menus, rows), exit faster than enter, `prefers-reduced-motion`
respected. Shimmer only on the currently running step.
- **V7** Anti-patterns (auto-flagged): purple/blue gradients, glassmorphism,
identical card grids, uniform rounding on everything, `transition: all`,
`outline: none` without a replacement focus ring, gray text on colored
backgrounds, decorative badges/chips soup.
## Chat, tool calls and MCP Apps
- **T1** A turn's steps read as one rail: leading slot (connector logo, file
icon, terminal glyph or reasoning dot), sentence label, duration right.
Finished turns collapse to one line ("Worked for 1m 19s · 12 steps").
- **T2** Failures stay inline with the fix: "Slack needs a fresh sign-in ·
Reconnect", not a stack trace. Raw input/output lives under "Technical
details".
- **T3** MCP Apps and generated views get a 40px header (source logo, title,
explicit state such as "Ready" / "Draft" / "Not posted") and one icon
action ("Open in panel"). Body is the app; footer holds ≤ 2 actions.
- **T4** The consent card is the focal element while a decision is pending.
It sits where the user acts (above the composer), names action · data ·
risk, and offers primary / edit / decline with keyboard chords.
- **T5** Composer: one round send/stop button, model and run-mode as quiet
pills, queue/steer hints as chords only.
## Before opening a UI PR
- [ ] Screenshot(s) of the new/changed UI at real size, including non-happy
states you touched (P10).
- [ ] Every new control reuses a `@/components` primitive or justifies why not
(P5).
- [ ] No surface has both a title and a description (P2).
- [ ] No explanatory sentences in the default viewport (P1).
- [ ] Blocked/locked states visible with reason and owner (P4, C5).
- [ ] Company/service references use logos; no generic icons outside nav
(V5).
- [ ] Tokens only; no hex/px literals that duplicate a token (V2–V4).
- [ ] Motion within V6; focus ring visible; reduced-motion respected.