Source of truth for every visual and UI decision. Read this before building any view. v2 (2026-06-20): re-grounded the visual language (editorial, not card-heavy), made the foundation enforceable (bundled fonts + a shared widget vocabulary + a visual definition of done), redesigned the trust-critical confirm, and defined the agent interaction model. v3 (2026-07-01): closed the doc-vs-code enforcement gap. Named every scale as a token (spacing, type, radii, motion, elevation, opacity, stroke, icon) so the code can carry a const instead of a magic number; gave the semantic roles their own names (the one warm light still serves them all, but on purpose now); promoted the light theme to a full token table; and versioned the golden references in-repo. The token names below are the contract the crate's
theme.rs/tokensmodule implement β a view that hardcodes a value the token layer already names is a review failure. v4 (2026-07-02): an IA / flow / interaction pass on top of the grounded flow audit (docs/research/11-ia-flows-audit.md) and a/design-consultation(docs/research/12-design-direction-v4.md). The visual system is unchanged (fonts, grayscale, the two-signal actor axis, tokens,ββ΅confirm); the structure changed: a three-pane always-on right rail, the request-origin model (you / dapp / agent β one shared review, one attributed feed), the Projects layer dropped, auto-assigned agent handles (no fixed "Atlas"), a Transaction detail view, the identity masthead + the no-"Wallet"-label rule, holdings with a$value column, and the honest cap ledger.Golden references (the pixel ground-truth agents build against), versioned in-repo under
designs/:
designs/deckard-v4.htmlβ the current v4 IA (wallet home, the one shared clear-signing Review across origins, Activity, Transaction detail, three-pane rail; light + dark). Authoritative on IA/layout/flow; defers to the v3 refs below on the editorial atoms + the confirm styling.designs/deckard-editorial-v3.htmlβ home, send confirm, swap compose, swap review, activity (the editorial language + atoms, light + dark).designs/deckard-agent-v4.htmlβ the redesigned transaction-as-hero confirm (this confirm styling supersedes the v3 send confirm; the v4 IA ref above reuses it). When this doc and a reference disagree, the doc wins on rules and the reference wins on pixels; fix whichever is stale. (These were unversioned under~/.gstack/...through v2; v3 checked them into the repo so the "matches golden reference" definition-of-done item is actually verifiable.)
Your money on autopilot, and you can see and stop everything. Calm sovereign control plus total auditability. Linear's calm precision and Conductor's parallel-agent cockpit, with a terminal's honesty and a thin Blade Runner edge (deep near-black, warm light for the human, cold light for the machine). Every decision below serves that feeling. Not a casino.
- What: Deckard, a native desktop (macOS + Linux) self-custodial Ethereum wallet, built in Rust + GPUI 0.2 + gpui-component 0.5. Offline-first, privacy-focused, open source (AGPL-3.0).
- Model: human-sovereign, agent-delegated. The human owns the keys and sets policy; AI agents act on the human's behalf within budget/scope/policy bounds. The agent is a key-less proposer: it never holds the key, the daemon does, and every move passes a policy gate the human controls (see Β§The agent interaction model).
- Who: onchain operators who live in crypto and refuse custodians; later, anyone delegating money-movement to agents.
- Peers in feel: Linear, Superhuman, Stripe, Splits. Dense, calm, keyboard-fast, premium. NOT consumer-crypto casino UIs.
The first drafts read as generic AI slop because they were built from prose descriptions of Linear/Conductor. The fix is to re-derive from real product pixels. The v2 visual language was chosen by generating three elevation directions (Instrument / Editorial / Vault) and picking Editorial against real Linear/Superhuman/Stripe/Splits reference. Rule for all future design work: ground every decision in real reference screenshots and the golden-reference HTML, never in remembered descriptions.
The old DESIGN.md was good prose, and the app still drifted into slop. Root cause, found by a source audit: the foundation (theme tokens, money rendering, the composeβreview engine, the palette) is disciplined, but the leaf widgets were hand-rolled per file (3-4 divergent copies of every helper), and the brand fonts were never bundled so the app ran in the OS system font. Prose cannot fix that. Three things make this foundation enforceable:
Schibsted Grotesk (UI) and JetBrains Mono (money/addresses) are both OFL-1.1 (free to redistribute,
even in a public repo) and must be embedded via cx.text_system().add_fonts(...) in main.rs with the files in
crates/deckard-app/assets/fonts/. Until they are, every typography rule below is fiction (the app
falls back to the system font). This is the single highest-leverage visual fix.
Every view composes from these primitives. Re-rolling any of them inline is a review failure. A primitive bakes in the correct DESIGN values so a screen cannot drift.
| primitive | what it is | the rule it enforces |
|---|---|---|
identity_mark(seed, size) |
project/wallet square or human round, with a deterministic monogram/identicon | never a blank fill; round = human principal, square = project/wallet, cyan squircle = agent |
agent_squircle(...) |
the cyan agent glyph (exists, keep) | cyan = agent class only; static, pulse only when acting |
section_label(text) |
10-11px uppercase, +0.07em tracking, text.muted |
one label treatment everywhere (kills the 3 divergent copies) |
short_addr(s) |
middle-truncate first-6 + last-4 | one truncation rule (kills short_mid/short_tx variants) |
truncated_address(addr) |
identicon + short_addr + ENS, self-send warning |
every address surface is distinguishable + identifiable |
caution_line(severity, text) |
Lucide TriangleAlert icon + inline text, no box |
one caution language (kills the β emoji); amber = caution, red = danger |
kv_row(label, value) |
label-left muted / mono-value-right, min_w_0+truncate |
one key/value row (clear-signing, policy, holdings) |
money(...) / usd(...) |
mono dimmed-decimals (exists in money.rs, keep) |
integer primary, decimals+ticker muted by color only |
amount_field(...) |
mono input + asset chip + USD-equiv + Max + over-balance gate | the compose input always knows the balance |
budget_gauge(spent, cap) |
thin 4px track, neutralβamberβ₯90%βredβ₯100% | over-cap can never render calm |
confirm_button(label, keys) |
the ββ΅ key-cap confirm with arm-delay (see Β§Confirm) |
no hold-to-confirm; spam-proof |
key_hint_chip(keys) |
a bordered key-cap | shortcuts live in chips/menus, never as on-canvas filler rows |
status_glyph(state) |
circular check/x-ring/clock-ring/minus | one status vocabulary across feed + chips |
nav_row(...) |
sidebar/footer row: rest/hover/selected lift, one resting color | one row anatomy, one resting treatment |
page_header(mark, title, subtitle) (v3) |
mark/glyph + H1 at text-h1 + muted one-line subtitle |
one page-header anatomy at one title size (kills the 6 hand-rolled copies at 3 sizes) |
divider() (v3) |
a 1px border.hairline full-width rule |
one hairline (kills the 6 inline div().h(px(1.)) copies) |
skeleton_row(shape) (v3) |
a money-/row-shaped loading placeholder | one loading vocabulary (never a spinner, never a bare β) |
stop_brake(state) (v3) |
the STOP control: amber-idle β danger-armed, ββ΅ to fire |
one kill-switch treatment across Activity + the agent surface |
QA and /code-review enforce it. A GUI PR that fails any item is not done.
Hierarchy comes from type, weight, whitespace, and hairlines. Not from cards. This is the Linear/Superhuman/Stripe discipline and the v2 decision: defaulting to elevated/floating cards is the AI-coded-design tell, and we reject it.
- Surfaces/cards are rare and purposeful. Default to whitespace + a tiny section label for grouping; a single hairline only where a list needs row separation. At most one faint frame for a genuine contained object, and never an elevation/shadow stack of cards.
- The hero is the largest object on screen. The balance (and, on a confirm, the transaction
amount) is the biggest thing:
text-hero(64) mono on the wallet home,text-tx-hero(44) on a confirm. Money is the load-bearing object; make it unmistakable. - Cockpit layout. Main surfaces are left-anchored, full-bleed, hairline-ruled columns with a
right-edge metadata rail (Linear's row anatomy: lead glyph Β· subject Β· object on the left,
signed-amount Β· status-glyph Β· time pinned right). Every row clamps: left cluster
min_w_0+ truncate, right railflex-none, so content can never run off either edge (this is also the fix for the historical horizontal-overflow bug). - Density is compact-comfortable, calm not crowded; Settings is more spacious.
Three-pane shell (sidebar + main + a persistent right metadata rail + a thin top breadcrumb bar + a bottom status strip). The rail shows contextual detail for the focused object (a wallet β its holdings/status + cap ledger; a pending request β its clear-signing detail; an activity row β its receipt). It is always on, not collapsible β the Linear navΒ·contentΒ·rail model. (v4: this replaces the v2/v3 "two-pane, no third inspector pane" rule; a request/agent/tx always has detail worth seeing without navigating away, so the rail earns its place.)
Sidebar = a single-column tree of the account's real entities. There is no Projects layer β
Deckard has no project-switching, so the top-level groups are the things that actually exist (v4:
dropped the Projects parent; it named a concept the product doesn't have):
- Wallets β identity mark + name + truncated mono address + right-aligned balance. Every wallet
has a real name, and the literal word "Wallet" never appears as a label anywhere (an unnamed
wallet gets a deterministic default, never "Wallet"). The breadcrumb names the entity (
Meridian,Meridian βΊ Swap) β never a project prefix, never the word "Wallet". - Agents β cyan squircle + an auto-assigned, human-renamable handle (a rotating codename/city
list, e.g.
Kyotoβ not an invented persona like "Atlas") + spend magnitude + status glyph. - Connections β dapp origins (favicon + domain + a trust dot). First-class group; deep connection management is deferred (ADR-0001 / #44), but the slot and the origin model exist now.
Every request to move money has an origin: you (in-app), a dapp (browser bridge today, plugins later), or an agent (MCP). One shared review, one attributed feed, one Rules vocabulary span them all β this is the product's spine, not the agent loop alone. The origin is rendered by identity + a header rail, never a third signal color: amber = human and cyan = agent stay the only two signal colors; a dapp/external origin is a neutral identity (favicon + domain) + a trust badge that borrows the state colors (verified = success, first-seen = amber caution, flagged = danger). Non-human sessions get the auto-assigned handle above. Approval is policy / per-origin, not one global posture β "approve every move" is itself a valid policy; agents run on policy, dapps default to approve-per-request (a scoped per-origin grant is deferred, ADR-0001).
Views are contextual to selection:
- Select a wallet β its home: an identity masthead (name + mark above the mono hero) + the hero USD/synced/verified meta line + allocation + left-anchored actions + a compact "Waiting on you" strip (one line; the home flips supervision-forward β the queue surfaces above the portfolio β only when a request/agent is live) + the holdings ledger (mono, a $ value column + 24h, decimal-point aligned).
- Select an agent β the agent surface (Β§The agent interaction model). (v4: kept light this pass; the in-wallet agent-interaction model needs more thought β documented as an expandable slot.)
- Activity is a destination: the origin-attributed see-and-stop feed (a
NEEDS YOUqueue above a day-grouped log; rows are scannable, not prose β identity + an action tag + mono amount + a warning tag + hash/glyph/time) with the STOP emergency control (Stop all agents, shown only when an agent is active so it never reads as a start/stop toggle). - Select a transaction β a Transaction detail view (v4 addition): the clear-signed receipt + hash (copy + explorer) + from/to + amount + gas + block + time + Authorized by (the rule/origin that permitted it).
- Settings is a bottom gear (global). Send / Receive / Swap / Shield are actions on the selected wallet; the confirm step is the one shared clear-signing Review (Β§Clear-signing), which renders identically for you, a dapp, or an agent β only the origin header rail changes.
A machine spends the human's money, so every attributable row must answer "who did this, me or which agent?" in under a second. A two-signal axis plus shape, never a rainbow:
- amber = the human / "where you are" / caution. cyan = the agent class. The Blade-Runner warm/cold duality made load-bearing. Disciplined 2-color system, not categorical coloring.
- Shape is the accessibility backup: human principal = full-round identicon; agent = cyan squircle monogram; project/wallet = rounded square mark. Survives grayscale.
- Identity, not state: cyan marks agent identity (squircle + status); it is never a page
title, body, or link color. Page titles are always
text.primary. - Accountability chain is rendered: an agent-proposed + human-approved action shows both actors ("Atlas proposed β You approved").
~95% grayscale. Two signal colors spent sparingly; semantics kept distinct from both.
| token | hex | use |
|---|---|---|
bg.base |
#0A0B0D |
app canvas |
bg.rail |
#0B0C0F |
sidebar / status strip / top bar |
bg.raise |
#121419 |
active/selected lift, the rare surface, inputs |
bg.raise2 |
#161922 |
primary-button fill, popovers, palette |
bg.hover |
#14161B |
hover lift |
border.hairline |
#1B1E25 |
dividers (~8% step from base) |
border.strong |
#262A33 |
input outlines, key-caps, stronger separation |
text.primary |
#E7E9EC |
headings, values (never pure white) |
text.secondary |
#9298A2 |
labels, body |
text.muted |
#646A73 |
metadata, addresses, dimmed decimals |
accent (amber) |
#F2A43B |
hover #FFB454, tint rgba(242,164,59,.14) |
agent (cyan) |
#3CC9BC |
tint rgba(60,201,188,.12) |
success |
#4FB463 Β· error |
#E5565B (tint rgba(229,86,91,.12)) |
identity slate |
#3A4250 |
desaturated identity-mark fill (off amber + success) |
shield slate |
#33424C |
private/shielded tone (off the actor axis) |
bg.overlay |
rgba(0,0,0,.5) |
the one dimming scrim under a modal/palette/slide-over; no shadow stacks |
border.focus |
= accent |
the 1px focus ring β the sole amber-colored border on the app |
Same token names, same roles β only the values change. Light has one fewer surface level:
raise and raise2 collapse to #FFFFFF (there is no second lift on a white surface), so a
primary button reads as a hairline-framed white, not a darker fill.
| token | hex | use |
|---|---|---|
bg.base |
#F6F5F1 |
app canvas |
bg.rail |
#EEEDE6 |
sidebar / status strip / top bar |
bg.raise |
#FFFFFF |
active/selected lift, the rare surface, inputs |
bg.raise2 |
#FFFFFF |
primary-button fill, popovers, palette (collapses to raise in light) |
bg.hover |
#ECEBE4 |
hover lift |
border.hairline |
#DDDBD2 |
dividers |
border.strong |
#CFCCC2 |
input outlines, key-caps |
text.primary |
#17191E |
headings, values |
text.secondary |
#52585F |
labels, body |
text.muted |
#6B7280 |
metadata, addresses (β₯4.5:1 on base) |
accent (amber) |
#A8650C |
deepened for AA on light; tint rgba(168,101,12,.14) |
agent (cyan) |
#0C7E75 |
tint rgba(12,126,117,.12) |
success |
#2F8F47 |
Β· error #C23B40 (tint .12) |
identity slate |
#A7AEBA |
identity-mark fill |
shield slate |
#94A2AC |
private/shielded tone |
bg.overlay |
rgba(0,0,0,.45) |
scrim (a touch lighter than dark's .5) |
border.focus |
= accent |
the amber focus ring |
- Amber is the one warm light, <1% of pixels. It means only: the human acting, "where you are" (active step, "awaiting you"), caution, the armed-confirm key-cap, the focus ring. Not a primary-button fill, not a routine toggle, not a chart segment.
- Primary buttons are neutral (
bg.raise2fill,text.primary, weight 600). - Cyan is only the agent class, low-chroma, on the squircle + agent status. Never title/link.
- Identity/token marks are desaturated tinted-neutral, off the warm/amber band and off the
successhue. Two different wallets must be visually distinguishable (deterministic mark). - Allocation/category bars use neutral/low-chroma tonal steps, never amber.
- Danger stays loud red, early. Unlimited approvals, unknown contracts, fresh-address sends,
over-cap, irreversible-loss surface in
error. - Caution = an amber
TriangleAlerticon + the risk text, inline. No box, no left keyline. (Always viacaution_line.) - Budget/utilization bars = thin 4px, neutral track, neutral/cyan fill at rest; amber β₯90%, red β₯100%. Never a saturated slab.
- Verified vs unverified is a real signal: a verified mainnet read reads
success; the downgrade reads the loudNOT VERIFIEDamber tag (Β§Per-chain trust tiers). Never fake verified.
The base palette above is the raw material; these are the roles a view actually reaches for. The
point of naming them: the single warm light (accent) does five jobs, and that overload is a
deliberate decision (one warm light, <1% of pixels) β not an accident to be "cleaned up" by
tinting each role differently. Naming the roles makes the overload visible and lets exactly one of
them peel off to its own token if it ever must, without touching the other four.
| role | token today | meaning |
|---|---|---|
signal.human |
accent (amber) |
the human acting / "where you are" / the active step / "awaiting you" |
signal.agent |
agent (cyan) |
the agent class β squircle + agent status only |
state.caution |
accent (amber) |
a recoverable risk (first-time recipient, slippage) β via caution_line |
state.danger |
error (red) |
irreversible / loss-bearing (public+permanent, unlimited approval, over-cap) |
state.success |
success (green) |
verified read, confirmed tx |
focus.ring |
border.focus = accent |
the 1px focus-visible ring |
armed.confirm |
accent (amber) |
the ββ΅ key-caps once armed |
overlay.scrim |
bg.overlay |
the dimming layer under a modal/palette |
Rule: a view names the role, never re-derives the hex. state.danger may equal error today,
but a screen asks for "danger," so a future palette change moves one token, not fifty call sites.
-
UI / display: Schibsted Grotesk (bundled, OFL-1.1). Sentence case; no ALL-CAPS except tiny section labels. Hierarchy from weight + size + color, in that order (the old build leaned on color alone, which read flat).
-
Money / numbers / addresses / hashes: JetBrains Mono (bundled), tabular figures, full precision, never abbreviated in a ledger.
-
Type scale (named tokens β one value each, no ranges):
token px weight use text-hero64 500 balance hero (wallet home) text-tx-hero44 500 transaction hero β the oversized amount on a clear-signing confirm (Send / Shield / Approve). Was a 40px send hero text-h120 600 screen title text-section14 600 section heading text-body13 400 body / values text-label10 500 tiny uppercase group label, tracking-labelThe old spec gave ranges (hero 64-72, confirm 44, H1 20-22, label 10-11); v3 collapses each to one value so a screen carries a token, not a judgement call. The balance hero (
text-hero, home) and the transaction hero (text-tx-hero, confirm) are the only two display sizes and are distinct on purpose β a confirm is not the home. In code:text-hero/text-tx-hero/text-body/text-labelaretokens::TEXT_*consts (gpui has no utility at those sizes);text-h1andtext-sectionare gpui's.text_xl(20) and.text_sm(14), a compact 12px size is.text_xs, and a swap compose amount (a step below the confirm hero) is.text_3xl(30). -
Leading:
leading-tight1.15 (hero + headings) Β·leading-normal1.4 (body + labels) Β·leading-mono1.0 (money / addresses / hashes β tabular figures set tight so columns align). -
Tracking:
tracking-label+0.07em is the only non-zero tracking (the tiny uppercase label). Everything else is 0 β Schibsted Grotesk is drawn for editorial text at its natural spacing. -
Fallback stacks (GPUI silently drops to the system font if a family isn't registered, so name the fallback explicitly): UI =
Schibsted Grotesk, system-ui, sans-serif; mono =JetBrains Mono, ui-monospace, monospace. -
Weights: 400 / 500 / 600. Never heavier than 600.
-
Mono-for-money rules (all via
money.rs): dimmed decimals (integerprimary, decimals+tickermuted, color only, no size step); every USD figure carries$or a⦠USDcolumn header; one precision/abbreviation rule per context; zero renders$0, never$0.0k; reserve a fixed sign slot so decimals align across signed/unsigned.
Everything here is a named value, and a view carries the name β never a raw px(...) that
duplicates one. Two naming vehicles, by design:
- gpui's own utilities already name two of the scales, so they stay the idiom: spacing is
.gap_N/.p_N/.m_N(gpui's 4px grid βNunits =NΓ4px), and the h1 / section / small type steps are.text_xl(20) /.text_sm(14) /.text_xs(12). - the
tokensmodule (crates/deckard-app/src/tokens.rs, see Β§Build notes) carries aPixels/Durationconst for everything gpui can't name β the display type sizes, 13px body, 10px label, the exact radii, the object-size ladder, the chrome dimensions, and the arm-delay.
A raw text_size(px(..)) is always a review failure (the no_raw_text_size_px test fails the
build); a raw px(..) elsewhere that duplicates a named value below is one too. Bespoke one-off
layout dimensions with no token (a specific column width) stay a literal β they name nothing.
space-2 2 Β· space-4 4 Β· space-8 8 Β· space-12 12 Β· space-16 16 Β· space-20 20 Β·
space-24 24 Β· space-32 32 Β· space-48 48. space-2 is the sole sub-grid half-step (for
hairline-adjacent gaps); every other value is a 4px multiple. Off-scale spacing is a bug: 34 β 32.
Grouping still comes from whitespace + a tiny section label, not a hairline between every row
(see Β§Visual language) β the scale is what that whitespace is made of.
Object sizes are tuned to glyph legibility and chrome ergonomics, not to the spacing grid, so they get their own named ladder. This is the fix for the old "polices 34 but bakes in 30" tension: object sizes are allowed off the grid, on purpose, and named β the grid governs space between things, this ladder governs the size of things.
| token | px | use |
|---|---|---|
mark-sm |
16 | inline identity mark (rows, chips) |
mark-md |
20 | sidebar / breadcrumb mark |
mark-lg |
30 | page-header mark |
track |
4 | budget / utilization gauge thickness |
sidebar-w |
248 | the sidebar column |
breadcrumb-h |
44 | the top breadcrumb bar |
status-h |
25 | the bottom status strip |
content-max-w |
760 | the reading column on a main surface |
confirm-w |
460 | the centered clear-signing / confirm card |
(Control heights β button / input / row β are not yet one token; standardizing them is a code-side
follow-up. Until then compose from the spacing scale, e.g. space-8 vertical padding on text-body.)
radius-input 4 (inputs, buttons, key-caps) Β· radius-row 6 (rows, marks, chips β never a
fully-rounded pill) Β· radius-modal 10 (confirm buttons, modals, palette, slide-over) Β·
radius-full (the round human identicon only). Borders are always a 6-12% step from their
background; never harsh.
stroke-hairline 1 (dividers, input outlines, the focus ring) Β· stroke-track 4 (the gauge). There
are no other stroke widths.
The editorial language rejects shadow-stacked cards, so elevation is not a shadow scale β it is
(1) background brightness (bg.base < bg.hover < bg.raise < bg.raise2) and (2) exactly one
dimming bg.overlay scrim under a floating surface. Stacking order, low β high:
canvas β content β popover/menu β βK palette β slide-over β modal (+ scrim) β toast. A surface
higher in this order dims what's below it with the scrim; it does not cast a shadow onto it.
One alpha ladder, so tints stop being one-off rgba literals:
alpha-hairline .06 (faint fills) Β· alpha-tint .12 (the standard signal tint β cyan, red) Β·
alpha-tint-warm .14 (amber only β it reads weaker at equal alpha, so it gets a hair more) Β·
alpha-scrim .5 dark / .45 light (bg.overlay) Β· alpha-disabled .4 (a disabled control where the
text.muted step-down alone isn't enough).
Lucide, hairline-weight, monochrome, inheriting currentColor β never a second accent, never a
filled/duotone style. Sizes: icon-sm 14 (inline in a caution_line) Β· icon-md 16 (status glyphs,
row leads) Β· icon-lg 20 (page-header glyph).
motion-fast 120ms Β· motion-base 160ms Β· motion-slow 220ms; ease-out on enter, ease-in on exit.
Two fixed timings beyond those: arm-delay 450ms (the confirm's inert window, formerly "400-600ms")
and pulse 1600ms (the one ambient motion β a slow breathing pulse on an agent currently
acting, formerly "~1.6s"). No spinners, no skeleton confetti, no celebratory animation on money.
Text tokens meet WCAG AA on their background: text.primary / text.secondary and text.muted
β₯ 4.5:1 (already noted for light text.muted on base). The signal colors are graphical objects
(marks, rings, glyphs), held to β₯ 3:1 against their background β amber and cyan clear this on both
themes. Never drop a text token below AA to fit a layout.
Every interactive component defines rest / hover / focus-visible / selected / disabled, and data
components define empty / loading / error. Defaults: hover = ~5% lift (bg.hover);
selected/active = a brightness lift (bg.raise), never a colored keyline; focus-visible = 1px
amber ring; disabled = a step below base, text.muted, no hover.
- Sidebar tree β
PROJECTSheader. Project row: chevron + identity mark + name + hoverβ’β’β’++. Children: Wallets (mark + name + mono address + right balance) and Agents (squircle + name + spend magnitude + status). Current selection gets thebg.raiselift. All rows through onenav_rowwith a single resting color. - Breadcrumb top bar β
[identity mark] Project βΊ current, wherecurrentnames the selected entity (the wallet/agent name, never the literal word "Wallet"). Right: network pill, βK, theme, mask toggle. Network name appears once (not also restated in the status strip). - Page header β one anatomy via
page_header: identity mark/glyph + H1 (text.primary,text-h120/600) + a muted one-line subtitle. - Balance hero + allocation β big mono balance (dimmed decimals) + a meta line
(USD Β· synced Β· verified). Below it a thin tonal allocation bar (no amber) + a small legend.
Loading shows a money-shaped skeleton bar, never a bare
β. - Holdings ledger β hairline rows, hover lift, clickable β Swap. Columns: Asset (desaturated
token mark + name + dimmed ticker) Β· Balance Β· 24h Β· Value, all USD with
$, Stripe-aligned so columns scan vertically. Empty = one muted line ("No assets yet"), no box. - Activity row + status glyphs β one schema:
[glyph] [subject verb object Β· context] β¦β¦ [signed amount] [status glyph] [time]. Subject is "You" (amber) or the agent name. State is a small circularstatus_glyph(filled check = confirmed, amber clock-ring = pending, red x-ring = failed/declined). Day-grouped bands. - Amount input (
amount_field) β mono amount, asset chip right, dimmed USD-equiv, a Max link, and a live over-balance error border that disables the forward action. One component for Send and Swap.
Hold-to-confirm is an anti-pattern; we do not use it. Confirmation is keyboard-first with a visible key-cap affordance (like Linear/Raycast "Create β΅"), made spam-proof by tiering and an arm-delay:
- Routine forward steps (Continue, Review, Next): the primary shows
β΅; Enter advances. - Irreversible money moves (Send, Swap, Shield, Approve an agent proposal, Revoke): the primary
shows the
ββ΅chord (a chord can't be fat-fingered like Enter). The key-caps render amber when armed. - Arm-delay: the confirm is inert for
arm-delay(450ms) after the screen opens (key-caps dimmed), so a queued or held keypress from the previous screen can't carry through and fire. A one-line note states this: "Press ββ΅ to send. It arms a moment after this screen opens, so a stray keypress can't approve it." - Highest-risk (fresh address over a threshold, unlimited approval, agent over-cap): the first
ββ΅arms ("Press ββ΅ again to send") and the second sends. No modal, no checkbox. - Mouse users get an equivalent click on the same button; the button is never a hold target.
There is exactly one review surface, rendered for every origin: self-send, swap, shield, a dapp request, or an agent proposal. Only the origin header rail (who is asking) changes; the body is identical. Two review layouts would mean two ways to be fooled. It is a statement, not a form:
- Origin header rail: the requester's identity β
You are sending(amber),Kyoto proposes(cyan agent handle), orapp.uniswap.org requests(a dapp: neutral favicon + domain). A dapp or agent is identity + a state-color trust badge, never a third signal color. - The transaction is the hero: a tiny
SENDING/SWAPPINGlabel, then the amount big in mono, then the USD-equiv, thenTOwith a prominent identicon + ENS + full-ish address (viatruncated_address). Say the amount once β do not repeat it as a "you pay / you receive" balance-diff for a simple swap (a Rabby-style "what changes" block is only for txs whose net effects aren't obvious from the hero). - Danger first, in red (
caution_linedanger): "This can't be undone." (v4: was the wordier "Public on Ethereum and can't be undone" β plain and declarative, no textbook blockchain explainer.) Then amber caution lines (first-time recipient, slippage). Never a gray box. No speculative site-reputation copy β we don't have it. - Quiet supporting facts demoted below a hairline: From, network fee, route/slippage, and the
Allowed by authority line β the rule that permitted it + the cap after this move
(
Swap rule Β· $0.14 of $0.20 daily left after this). State each fact once. - The
ββ΅confirm button (via a platform-aware key-cap widget ββon macOS,Ctrlon Linux)- an Edit link. No prose arm-delay explainer beside it.
See Β§The agent interaction model. The budget_gauge and the Pause/Revoke/Rotate/Adjust controls are
mandatory there.
The universal control plane. Every user-facing action registers a Command
(CLAUDE.md Β§Command palette reachability). Fuzzy + frecency, matched chars lift by weight not hue,
selected row is a brightness lift, shortcuts shown right-aligned inside the rows. Shortcuts live
here and in menus, never as an on-canvas hint-chip row (that was slop; it is deleted).
Disabled primary when input is invalid; input error (amount > balance) with border + helper
replacing Max; pending/failed in Activity; loading = skeleton rows (never a spinner, never a bare
β); empty = one muted line (no box, no illustration).
The product's reason to exist, and the part to get right and lean.
Three authority models, kept distinct (docs/agent-authorization-map.md):
- Key-less proposer β ALL of v1. The agent proposes typed intents via the MCP sidecar; the human approves; the daemon signs with the human's key only after the policy gate. Limits are software-enforced, not chain-enforced (never claim "cannot exceed"). STOP zeroizes the key.
- Agent wallet / session keys β a distinct agent-controlled address, EIP-7702. Deferred (ADR-0002).
- Per-origin dapp grant β a different principal. Deferred (ADR-0001).
So a v1 agent is its policy + its activity, nothing more.
An agent is rendered entirely from its policy data and its activity feed. Adding a capability is a new field on the fence + a new verb in the feed, never a redesign. Multiple agents = more rows + more surfaces. The deferred model-2 (agent wallet/session keys) becomes new fence fields + a second identity on the agent surface. The feed already attributes by actor, so multi-agent works for free. Build for one agent (Atlas) today; change nothing structural to reach N.
- First-class and standalone in the sidebar under the project, beside Wallets.
- The agent surface (select an agent): identity (squircle + name
text.primary+ live status with the acting-pulse) + a one-line plain-language autonomy statement ("Atlas acts on its own under $0.20 per move and asks you above that. It can shield ETH only. It never holds your key, and it cannot send to a new address.") + editable Limits (per-tx cap, daily budget + thebudget_gauge) and Scope (allowed actions, allowed assets, session-key expiry) edited in-app, not policy.json + the controls Pause / Rotate key / Adjust limits / Revoke (and STOP), kill switch always one deliberate action away + what this agent did (its slice of the feed). This surface owns the policy; the wallet home does not. - The wallet home shows only a compact agent presence: one
nav_row-style row per agent (squircle + name + status + a thinbudget_gauge+ a chevron) that links to the agent surface. The home answers "is an agent running and how close to its budget" at a glance; detail is one click away. The old read-only policy dump on the home is removed.
User headspace: "My money is on autopilot. What just happened, is anything waiting on me, can I stop it right now?" Vigilance plus reassurance.
- An audit log: append-only, newest first, day-grouped, status glyphs, what the agent and you did, attributed by actor. Calm zero-state ("All clear, nothing needs you").
- STOP: always top-right; arms to
ββ΅; revokes + zeroizes the key. - Live streaming as the agent loop runs; the acting-pulse on the agent glyph.
A triage inbox ("Needs you") stacked above the log: pending approvals as actionable rows (open β
the clear-signing review β Approve ββ΅ / Deny / Inspect), keyboard nav (j/k, Enter), drill-in
receipts (tx hash, the cited limit, the policy check, explorer link), and filter by actor / status /
wallet, with repetitive within-cap actions collapsing into a group so exceptions float up. The
no-blind-approve invariant holds (approve resolves only the still-pending reviewed record).
- Addresses always mono, middle-truncated first-6 + last-4 (one rule, via
short_addr), paired with identicon + ENS (viatruncated_address), one-click copy with inline "Copied β", and a warning if a recipient resolves to the active wallet. - Clear-signing as above: plain language, exact mono figures, danger early in red, caution as an
amber icon inline (no box), the
ββ΅confirm. - Seed reveal β blurred, hold-to-reveal, auto-hides after a few seconds, a "make sure nobody is watching" caution, never auto-copied, Copy demoted below reveal, index numbers legible. (Hold-to-reveal is fine; it is hold-to-see, not hold-to-approve.)
- Network warning on Receive β one amber
caution_line, the risk word emphasized. - Kill switch β Pause / Revoke / Rotate always one deliberate action away on the agent surface; a master "Pause all agents" in Settings. STOP on Activity zeroizes the key.
Verified reads are mainnet-only (embedded Helios). Every other chain reads from a trusted RPC. We show those chains in the one wallet and never let an unverified number wear the verified look.
- Tier 1 β mainnet: Helios-checked.
Verifiedon a fresh head,Unsyncedotherwise. - Tier 2 β verified L2 (future): OP-stack via helios-opstack is sequencer-trusted, renders
Degraded, neverVerified(#77). - Tier 3 β raw-RPC (Arbitrum, Tempo, most L2s): no light client, every read is
Unsynced/ "NOT VERIFIED". Reuse that affordance; a Tier-3 balance never gets theVerifiedrow treatment. - Never fake it: a non-mainnet read never reaches
Verified. The downgrade is loud, not hidden. - No native asset (Tempo): gas in a stablecoin, no native balance. Deferred until the portfolio can show "no native asset" instead of a placeholder dressed as money.
- Guardrail is per-chain: the human-approval brake fires on every real-value chain (#76).
- A transport/RPC failure shows a calm humanized line ("Couldn't reach the network. Retrying."), never the raw provider string in the status strip.
A stepped, calm, full-bleed flow: Welcome (lead with the promise, not "Welcome to Deckard") β Secure (passphrase + a live strength meter + the consequence: "if you forget it, no one can reset it"; algorithm names demoted to a details affordance) β Back up (recovery phrase, hold-to-reveal, nobody-watching, never-auto-copied) β Verify (a separate step; the grid is hidden, confirm by position, the primary disabled until correct) β Ready (a real screen). Amber only on the active step, the caution, and the focus ring; primary CTAs neutral.
- SAFE (category baseline): dark-first near-black + cool neutrals, one accent discipline, grotesk + mono pairing, minimal motion, keyboard-first + βK, clear-signing, status-as-glyph, the Conductor/Splits sidebar tree.
- RISK (Deckard's face): the two-signal axis (amber human / cyan agent); monospace for ALL
money + addresses; the agent layer as first-class (standalone in the sidebar, its own surface,
rendered from policy); Portfolio as control + composition, not a price-chart casino; the editorial
type-driven, card-free composition; the breathing "currently acting" pulse as the one ambient
motion; the
ββ΅key-cap confirm as the signature trust gesture.
The token names above map to Rust consts, not prose:
- Color lives in
theme.rsβrefine()overrides the gpui-componentThemeConfigslots (light + dark). The two signal colors + their tints and the identity/shield neutrals resolve through the theme (amber/agent/amber_tint/agent_tint/identity_square/shield), so a widget reads them fromcx.theme()rather than taking them as arguments. - Spacing / type / radii / stroke / motion live in a
tokensmodule asconst Pixels/Duration(space_8,text_body,radius_modal,motion_base, β¦). A view uses the const; a rawpx(...)that duplicates a named token is a review failure and the magic-number lint flags it. - Fonts bundle via GPUI assets (
add_fontsinmain.rs); no web-font CDN. Name the fallback stacks (Β§Typography) β GPUI silently drops to the system font if a family isn't registered. div()isdisplay:blockin GPUI, so a child'sflex_1/justify_centeris inert unless the parent isv_flex/h_flex. GPUI'sStyledhas no letter-spacing setter, sotracking-labelis approximated with size + uppercase on the tiny label (the only tracked text).
A GUI change is not done until ALL hold (paste screenshots as evidence):
- Renders in Schibsted Grotesk + JetBrains Mono (fonts actually bundled), not the system font.
- Money + addresses are mono, dimmed-decimals, via
money.rs; addresses viashort_addr(6+4) + identicon. - No raw hex colors in the view; only
theme.*+ the theme'samber/agent. - No magic-number
px()for a value the token layer names β spacing viaspace-*, type/object sizes via their tokens, radii viaradius-*, motion viamotion-*(the lint flags raw dupes). - No card unless purposeful; grouping is whitespace + hairlines + section labels.
- No
βemoji anywhere; caution/danger viacaution_line(icon, no box). - Confirm is the
ββ΅key-cap (orβ΅for routine), never hold-to-confirm; arm-delay present. - Section labels via
section_label(10-11px uppercase tracked); no divergent copies. - Rows clamp (
min_w_0+ truncate left,flex-noneright); nothing overflows the pane. - Empty/loading/error/pending/failed/disabled states present; loading is a skeleton, not
β. - Amber β€1% of pixels and only on human/caution/where-you-are/focus/armed-confirm.
- Every new action has a βK
Command. - No leftover starter slop (keyboard-hint rows, "Welcome to Deckard", dead settings, leaked
build-flag or provider strings, orphan
β). - Identity is named (v4): wallets/agents show a real name/handle; the literal word "Wallet" appears nowhere as a label; the breadcrumb names the entity (never a project prefix, never "Wallet").
- Money keeps its context (v4): holdings carry a mono
$value column + 24h, decimal-point aligned; the balance hero carries a USD + synced/verified meta line (honest fallbacks off mainnet). - One shared Review (v4): send/swap/shield/dapp/agent all render the single
transaction-as-hero review; origin is a header rail + identity + a state-color trust badge, never
a third signal color; the
Allowed byline shows the cap after this move; no unenforced cap is ever shown as enforced. - Key-caps via the platform-aware
key_capwidget (βmacOS /CtrlLinux), not a hardcoded glyph. (v4) - Matches the golden-reference HTML:
designs/deckard-v4.htmlfor IA/layout/flow;designs/deckard-editorial-v3.html+designs/deckard-agent-v4.htmlfor the editorial atoms + the confirm styling.
| Date | Decision | Rationale |
|---|---|---|
| 2026-06-05 | Initial v0 system | /design-consultation. Amber-on-near-black, General Sans + JetBrains Mono, mono-for-money, clear-signing. |
| 2026-06-05 | Human-sovereign / agent-delegated; memorable thing = "autopilot you can see and stop" | The product is a wallet for agents that doesn't suck; the human stays principal. |
| 2026-06-05 | Two-signal axis: amber=human, cyan=agent | Makes "who acted" instant; the Blade-Runner duality as core semantic. |
| 2026-06-05 | IA: Conductor sidebar tree (Projects β Wallets + Agents), 2-pane, contextual views | Grounded in real Conductor + Splits shots. |
| 2026-06-05 | Re-grounded in REAL Linear/Conductor/Splits screenshots | First drafts were rumor-mill slop. |
| 2026-06-20 | Multi-chain = one wallet with a loud honest downgrade; mainnet stays the only Verified tier |
Verified-or-honestly-labeled reads are the moat. Scope: docs/research/multichain-scope.md. |
| 2026-06-20 | v2 foundation overhaul. Made the foundation enforceable: bundle the fonts (they were never embedded, the app ran in the system font), one shared widgets.rs vocabulary, a visual definition of done. |
An 8-agent source audit found the bones disciplined but the leaf widgets hand-rolled per file (3-4 divergent copies of every helper) and fonts unbundled. Prose could not fix per-file copy-paste. |
| 2026-06-20 | Editorial visual language; reject card-default. Hierarchy from type + whitespace + hairlines; surfaces/cards are rare and purposeful; oversized mono hero; cockpit row layout. | Chose Editorial over Instrument/Vault against real references. Defaulting to elevated cards is the AI-coded-design tell; Linear/Superhuman don't do it. |
| 2026-06-20 | ββ΅ key-cap confirm replaces hold-to-confirm. Tiered (β΅ routine / ββ΅ irreversible / double-ββ΅ highest-risk) + arm-delay. |
Hold-to-confirm is an anti-pattern; the key-cap is keyboard-first and the chord + arm-delay make it spam-proof. |
| 2026-06-20 | Transaction-as-hero clear-signing. Amount + recipient are the heroes; details demoted; danger red first; caution amber inline (no box). | The old flat key/value form read amateurish; what can lose money must dominate. |
| 2026-06-20 | Agent interaction model. Agents first-class standalone in the sidebar; a dedicated agent surface owns editable policy + controls + its own activity; the home shows a compact agent presence; expandability contract (agent = policy + activity, UX renders from data). | The read-only policy dump on the home was awkward; an agent that spends your money deserves a first-class surface; lean for one agent, expandable to N and to model-2 without redesign. |
| 2026-06-20 | Activity lean now = audit log + STOP; triage inbox / keyboard nav / drill-in receipts / filtering deferred and documented. | Build the see-and-stop log first; layer the inbox interactions once the loop is real. |
| 2026-06-20 | UI/display face: General Sans β Schibsted Grotesk (JetBrains Mono unchanged). | General Sans is Fontshare/ITF proprietary β its EULA forbids redistributing the raw files / public-server hosting, which the public repo violated once #114 committed them. Schibsted Grotesk is OFL-1.1, a structural drop-in (same 400/500/600 weights), and built for editorial publishing β it fits the locked Editorial direction. Chosen over Hanken Grotesk (safer/quieter) and IBM Plex Sans (more recognizable) via /design-consultation. |
| 2026-07-01 | v3 token layer + versioned references. Named every scale (spacing, object sizes, type, radii, stroke, elevation, opacity, icon, motion) as a token β Rust const; gave the semantic roles their own names over the single warm light (deliberate overload, not accidental); promoted light to a full token table; added elevation/opacity/AA sub-specs; unified the transaction hero to one text-tx-hero (was 40/32); declared object sizes a separate ladder (resolving the 30px-off-grid tension); checked the golden HTML references into designs/. |
A design-system audit found the doctrine strong but enforcement partial: color + fonts were centralized, but everything below (127 raw px() across ~43 values, 11 scattered text_size(px()), duplicated leaf widgets) was hand-rolled, and "matches golden reference" pointed at an out-of-repo, unversioned, stale file. Prose can't enforce a spacing grid; a const the compiler + a lint check can. |
| 2026-07-02 | v4 request-origin IA. Kept the visual system; reworked structure: a three-pane always-on right rail; the request-origin model (you / dapp / agent β one shared clear-signing Review, one origin-attributed feed, origin as identity + a state-color trust badge, never a third signal); dropped the Projects layer; auto-assigned renamable agent handles (retired the "Atlas" placeholder); a Transaction detail view; identity masthead + the no-"Wallet"-label rule; holdings with a mono $ column + decimal alignment + a hero USD/verified meta line; the honest cap ledger; and a platform-aware key-cap widget. Golden ref: designs/deckard-v4.html. |
A grounded flow audit (docs/research/11-ia-flows-audit.md) found the bones strong but four load-bearing gaps (identity, two clear-signing treatments, a read-only agent surface, an unenforced cap). A /design-consultation (docs/research/12-design-direction-v4.md; Linear/Raycast/Superhuman/Rabby references + two outside voices) landed the direction: the product is the requestβreviewβapprove/stop loop generalized across who is asking, and the agent is one origin among several β not the whole product. Deep agent internals + dapp-connection editing deferred. |