ContributingDraft
Vyre phone
The design for Vyre on a phone: one Expo app (React Native) for iOS, Android and the web, like Paseo's (the user's decision, 2026-09-27; mobile leads ADR 0027). It builds from this file. Colours and type are the Deck's reduced system (deck-design's second pass, docs/design/deck.md on work/deck-design e1428b1), pasted verbatim; this file adds only phone roles, sizes and behaviour. Desktop (the Deck) shares the same tokens and chat items, so a card looks the same on both.
UPDATE (2 Oct 2026, v2, team/0.2.2/ux-prototype.html, approved by the user): the phone now has a glass tab bar of five (Now, Chat, Projects, Agents, More); More is a sheet (Planner, Memory, Vault, Drive, Devices, Settings); the header carries the page title, search and the avatar; Lumen floats above the bar. Where this page says "no tab bar", the update wins.
Decision (2026-09-27, superseded for the tab bar): Direction B's shell (no tab bar, pages you swipe between, the floating Capsule with hold-to-talk) with Direction A's screens (Chat, Find, Agents, the approval sheet). Now is A's grouped "Needs you" list: every row swipes to approve or deny and opens a detail sheet with Open session. The screens feel native to iOS: sentence-case sans type, grouped cards, standard sheets. Mono is for commands, code and logs only.
1. What the phone is for
In order of how often it happens:
- Glance at what needs you, and answer it (approve, deny, reply, pick a choice).
- Chat with a session or an agent, and read what it did.
- Find anything: a session, a file, a memory, a command to run.
- Watch an agent work.
Everything else (setup, rules, vault, connections) lives in Settings, behind the avatar.
Principles:
- What needs you comes first on every screen, and it is always one tap from an answer.
- Calm. One primary action per view, and no motion that is not feedback.
- Native gestures and sheets. Nothing the user has to learn twice.
- Light (SPEC principle 8). Nothing polls faster than 60 s, and live output streams only while it is on screen.
2. Tokens
Colour roles
Role names are the Deck's (deck/css/deck.css, and the shared mockup sheet docs/design/deck-directions/vyre.css), so a phone view and a Deck view read the same variables.
| Role | Dark | Paper | Use |
|---|---|---|---|
--bg | #0E0D0C | #F4F1EA | Page ground |
--panel | #161513 | #FBFAF6 | Cards, sheets, the Capsule |
--hover | #1E1C1A | #EEEAE2 | Agent tiles, pressed rows, the Deny reveal, secondary buttons |
--rule | #2B2926 | #DCD7CC | Hairlines between rows |
--rule-strong | #3A3733 | #C9C3B7 | Card and input borders, outline buttons |
--text | #F1EEE6 | #141311 | Primary text |
--text-2 | #B3AEA4 | #4A463F | Secondary text |
--label | #8C877D | #6B665D | Labels, meta, placeholders. Smallest text colour allowed |
--primary-bg / --primary-ink | #F1EEE6 / #0E0D0C | #141311 / #F4F1EA | The one primary button per view; also the count on a --beacon-dot badge |
--primary-hover | #FFFFFF | #4A463F | The primary while pressed |
--focus | #F1EEE6 | #141311 | Focus ring |
--signal-wash | rgba(241,238,230,0.12) | rgba(20,19,17,0.10) | The Ask row in Find, added diff lines |
--match (phone) | rgba(241,238,230,0.20) | rgba(20,19,17,0.16) | Search match highlight, the Open session flash |
--beacon-ink / --beacon-dot | #B8A4FF / #B8A4FF | #5B3FC4 / #5B3FC4 | Attention: needs you. A dot, a label or a count badge, nothing else |
--del-wash | rgba(140,135,125,0.14) | rgba(107,102,93,0.10) | Deleted diff lines, with --text-2 text (--label is 4.49:1 there) |
--code-bg | #121110 | #F0EDE5 | Command blocks, the live console |
--mark-wire / --mark-dot | #F1EEE6 / #F1EEE6 | #141311 / #141311 | The mark |
--scrim (phone) | rgba(0,0,0,0.62) | rgba(20,19,17,0.34) | Behind a sheet |
The whole palette is neutrals, one accent (bone: primary actions, focus, running, added lines)
and one attention colour. No coral or red, no gold, no other hue (the user's rules). The
attention colour ("needs you") keeps the --beacon-* names and is violet; teal (#5FD4C4 dark,
#0B6E66 paper) is the only alternative (honey is out). Swapping it is one line per theme: change
the two --beacon-* values. There is no beacon wash or beacon rule. Every pair above passes
WCAG AA.
The mark's dot is Signal (dark) or Ink (paper) when nothing is waiting, and the attention colour
when anything needs you. A deleted line in a diff is --text-2 on --del-wash, never attention.
The attention colour is only ever a dot, a label ("kit is waiting on you", "Held 4 min") or a
count badge (--primary-ink on --beacon-dot: 9.1:1 dark, 6.3:1 paper), never a fill, border
or wash.
Held, ask and question cards are neutral: --panel with a --rule-strong border, no wash.
Errors and destructive actions carry no colour:
- A failure is a fact, not an alarm:
--text, a crossed-circle glyph, afailedLabel and the reason. It takes the attention colour only when the user has to act on it. - A destructive action (delete a project, forget a memory, remove a device) is slowed down by
words and a second step, never by colour. The sheet says what goes, with counts ("This removes
214 files, 41 memories and 3 sessions"). The destructive button is an outline in
--textwhose label carries the count ("Delete 214 files") and needs a 0.6 s hold (with a fill that tracks the hold, and a haptic at the end). The safe choice ("Keep it") is never the primary fill either. Deny and Discard on a held item are not destructive (the item can be held again, and Discard has Undo), so they are ordinary secondary buttons. A held Gate delete of outside data (mail, files, posts) uses the same outline button with the count ("Delete 12 emails"). Its second step is Face ID when the proof runs, or the 0.6 s hold when the device is covered, never both.
Buttons, the same three kinds as the Deck:
- Primary:
--primary-bgwith--primary-ink(ink on bone in dark, paper on ink in paper). One per view. - Secondary:
--texton--hover,--rule-strongborder. - Ghost (text-only, like "Details" or "Open session"): full
--text, 600, never--text-2. Cancel, Deny, Discard and Later are always ghosts.
Order in any action area: primary, then secondary, then the ghost. All labels sentence case, 600.
Text on tints: on paper, --label measures 4.48:1 on --signal-wash, just under AA, so meta
text on any wash steps up to --text-2. Input
placeholders are --label at full opacity (browsers default to a lighter grey that fails).
Theme follows the system (useColorScheme), with Dark, Paper and System in Settings.
Every token in this file is a plain value the Expo app takes as is: colours as hex or rgba()
strings, sizes, line heights, radii and spacing as unitless numbers (px here are dp/pt), weights
as the strings "400" and "600", tracking as a number of points (-0.015em at 22 is -0.33). The app
keeps them in one tokens.ts with dark and paper objects under the role names without the
dashes (--text-2 is text2); nothing in it is computed from a CSS variable at runtime.
Type
Two families, two weights. Instrument Sans for everything; JetBrains Mono only for commands, code, paths, IDs and keys, at 12 or 13. Weights 400 and 600, nothing else. No uppercase and no letter-spaced caps anywhere.
The scale is the Deck's five steps, 12/16, 13/18, 15/22, 20/26 and 28/34, with the phone's +2 on the 15 and 20 steps (for iOS body size and no zoom on focus). So the phone uses exactly five sizes: 12/16, 13/18, 17/24, 22/28, 28/34. 12, 13 and 28 are shared with the Deck. The app bundles both fonts (expo-font) and scales them with Dynamic Type (iOS) and font scale (Android); these are the default "Large" size.
| Role | Family | Size / line | Weight | Tracking | Use |
|---|---|---|---|---|---|
| Sheet title | Sans | 28 / 34 | 600 | -0.015em | The detail sheet's title |
| Page | Sans | 22 / 28 | 600 | -0.01em | The page labels in the header (Now, Chats, Agents) |
| Row title | Sans | 17 / 24 | 600 | 0 | Rows in cards, section headers on Now, group headers, the chat nav title, agent names |
| Lead | Sans | 17 / 24 | 400 | 0 | Chat messages, a question's text, the agent's reason, fact rows |
| Input | Sans | 17 / 24 | 400 | 0 | Search and composer text (never under 16: no iOS zoom) |
| Button | Sans | 17 / 24 | 600 | 0 | Every button label, sentence case (13 / 18 on the swipe reveals and chips) |
| Secondary | Sans | 13 / 18 | 400 | 0 | Row second lines, facts under a card, --text-2 |
| Label | Sans | 12 / 16 | 600 | 0 | Small labels ("Why kit wants to", "From memory"), sentence case, --label |
| Meta | Sans | 12 / 16 | 400 | 0 | "kit · Harlow Legal", times, hints, tags |
| Code | Mono | 13 / 18 | 400 | 0 | Commands in blocks and rows, paths, counts |
| Log | Mono | 12 / 16 | 400 | 0 | Live console, command output, diffs |
Buttons are sentence-case sans, the same as the Deck's second pass.
Space, shape, lines
- 4 px grid. Use 4, 8, 12, 16, 24, 32. Side gutter 16. Safe areas come from the device
(react-native-safe-area-context;
env(safe-area-inset-*)on the web); the mockups use 59 top and 34 bottom. - Radii: chip 4, filter chip 8, code block 6 (inline) or 8 (sheet), card 10, button 10 (44 tall) or 12 (46 and 54 tall), segmented control 9 (segments 7), sheet 14 (top corners), agent tile 8 (at 32 px) or 11 (at 40), chat bubble 18, the Capsule and round buttons fully round. Cards and sheets match the Deck; buttons are rounder, as on iOS.
- Hairline 1 px
--rulebetween rows;--rule-strongon card, input and outline-button borders. - One shadow, for things that float (the Capsule and sheets):
dark
inset 0 1px 0 rgba(241,238,230,0.06), 0 24px 48px -24px rgba(0,0,0,0.6), paper0 24px 48px -24px rgba(20,19,17,0.28). In React Native:boxShadowwith the same string (new architecture), andelevation: 12as the Android fallback; drop the inset line whereboxShadowis not available. - No gradients, glows, or blur behind content. The Capsule and sheets are opaque
--panel. - Icons: inline stroke SVG on a 24 grid, 1.5 stroke (1.7 at 26 px and above), round caps and
joins,
currentColor. Sizes 16, 20, 22, 26. The set the phone needs: back, more, plus, close, search, mic, send (arrow up), stop, check, face-id, terminal, eye, pause, clock, file, chat. - Touch targets are 44 x 44 at least, even where the drawing is smaller (
hitSlopmakes up the difference, 8 on every side for a 28 px control).
3. The shell
No tab bar. Three pages side by side: Now, Chats, Agents. Find is not a page; it is the Capsule, opened.
+--------------------------------------+
| (v) Now Chats Agents (a) | header, 48 tall under the safe area
| |
| page content, scrolls |
| |
| |
| ( v Ask juno, find, or run (mic) ) the Capsule, floating
+--------------------------------------+
- Header. The mark (22 px) on the left, then the three page labels in Page type, the current
one in
--text, the others in--label. The avatar (34 px circle, the owner's initial) on the right opens Settings as a sheet. On Agents the avatar's place holds "+" (new agent). The header does not collapse; it stays 48 tall. - Pages swipe left and right with the page label snapping under the finger. A swipe is a page swipe once it moves 15 horizontally and more horizontally than vertically (so a list still scrolls); it commits past a third of the width or at 500 pt/s, else springs back. Content that scrolls sideways (a code block, filter chips) keeps the swipe while it can scroll. On the web only a 32 px edge strip starts a page swipe, so text selection still works. Tapping a label jumps there. The app reopens on the page it was on, except that it opens on Now whenever something needs you.
- The Capsule floats 12 from each side, sitting on the bottom safe area, 56 tall,
--panelfill,--rule-strongborder, the float shadow. It holds the mark (20 px), the placeholder "Ask <assistant>, find, or run" (the assistant's name from onboarding), and a 40 px mic button.- Tap: Find opens as a full-height sheet with the keyboard up.
- Drag up: same, following the finger.
- Hold the mic: dictate. Release to put the words in Find; the words are never sent on their own.
- Pages leave 56 + 16 of bottom padding so the last row clears it.
- Pushed screens (a chat, an agent's live view, Settings pages) slide in from the right, have a back chevron, and support the edge swipe back. A pushed chat hides the Capsule and shows its own composer (section 6).
4. Now
Sections have sentence-case headers in Row title (17/24, 600) with the count on the right in
Meta --label, 24 above and 8 below. Cards are --panel, --rule border, radius 10, with
hairlines between rows. Any section with nothing in it is left out, except as noted.
Needs you
Header: an 8 px --beacon-dot, then "Needs you", count on the right. One card holds a row per
item, oldest first. Under the card, one Meta line in --label: "Swipe right to approve, left to
deny." Show the hint until the user has swiped once, then drop it.
The row:
[k] Send email to Dana 12m >
Q3 report, the short version
kit · Harlow Legal
- 12 x 14 padding. Tile 32 px, radius 8,
--hoverfill,--rule-strongborder, the agent's initial (15, 600). The tile is the agent that asked. - Line 1: the title in Row title, time since it was held on the right in Meta
--label. - Line 2: the command in Command mono (13/18)
--text-2for an ask; the subject for a draft; the question for a question, in Secondary--text-2. One line, truncated at the end. - Line 3: "<agent> · <project>" in Meta
--label. - Chevron 16 px in
--label. - Titles by kind: an ask is the action ("Push q3-report"); a draft is "Send email to Dana" (verb and person); a question is "<agent> has a question".
Swipe:
- Right reveals the primary action from the left edge, 100 wide, in
--primary-bgwith--primary-ink: a check glyph (24) over "Approve" (asks) or "Send" (drafts) in 13/600, the Face ID glyph instead whenpresence.requiredis true andcoveredfalse (section 5). Past 100 or a fast fling, it commits: the proof runs if one is needed, and on success the row collapses. Letting go short of 100 leaves the action showing; tapping it commits. - Left reveals "Deny" (asks) or "Discard" (drafts) from the right edge, 100 wide,
--hoverfill with--textlabel and an x glyph. Committing needs no presence check unless the tool demands one. The row collapses with an Undo toast for 4 s. - A question has no one-swipe answer: swiping right opens its sheet, swiping left offers "Later" (snooze 1 h).
- A failed approval springs the row back and shows the reason under line 3 in
--text, with a--label"failed". - Tap anywhere else on the row opens its detail sheet (section 5).
When nothing needs you, the section is one line in --label: "Nothing needs you." No card.
Working
Header "Working" and count. One card, a row per running session:
- Tile 32, the session name in Row title with the step count on the right in Meta ("3 of 5"),
the latest step in Secondary
--text-2("Rendering reports/q3.pdf"), then a 3 px progress bar (--ruletrack,--textfill, radius 2) 8 below. - A finished one reads "Done" on the right, no bar, and leaves after an hour.
- Tap opens the agent's live view. When nothing is running, the two most recent sessions stand in.
From memory
One neutral card row (--panel, --rule border, radius 10, 12 x 14 padding, no wash): the
memory stack icon (20, --text-2), what memory learned today or what is due soon in Lead
--text, and its source in Meta --text-2 ("From memory · your call with Dana, Tue"). Tap opens
the fact in Find with its source.
Setup and pairing
The passkey and "Set up this phone" cards leave Now. First run is an onboarding flow (install, notifications, passkey or device key). Later, anything missing is one row at the top of Now ("Add a passkey to approve from this phone", chevron) that opens that step as a sheet. A Mac asking to pair is a Needs you row like any other, with its own sheet.
5. The detail sheet
Every Needs you row, and "Details" on a chat's approval card, opens this sheet at the large
detent (the page behind scales to 0.94 with rounded top corners, over black), radius 14 top
corners, grabber (36 x 5, --rule-strong), --panel fill, the float shadow, 20 side padding.
Swipe down, the close button or the scrim closes it. Content scrolls; the action area stays
pinned above the bottom safe area.
Header (every kind):
- Row 1: a 22 px agent tile and "<agent> asks · <project>" in Meta
--text-2on the left; a 30 px round close button (--hover,--ruleborder, x glyph) on the right. - Title in Sheet title (28/34, 600, -0.015em), 12 above.
- Row 3: a 7 px
--beacon-dotand "Held 4 min" in Label--beacon-inkon the left; Open session (a ghost) with a chevron on the right, 17/600--text, 44 tall.
Open session opens the exact session this came from in Chat, scrolled to the moment it was
raised: the matching tool row or message is centred and flashes --match for 1.2 s. The sheet
closes first. If the session is on a Mac that is away, it still opens the read-only transcript,
and the chat header says the Mac is away.
Body by kind:
- Ask (a tool call). The command in a mono block (
--code-bg,--ruleborder, radius 8, 12 x 14 padding, Code 13/18,$in--label). Then "Why <agent> wants to" in Label and the agent's reason in Lead. Then fact rows between hairlines, label on the left in Lead--label, value on the right in Lead--text: Remote, Branch, Changes ("6 files +412 -38", counts in Code--text-2), Held by ("Your rule: pushes ask first"). Tap Changes to expand the files, each with its counts; tap a file for its diff (+ lines on--signal-wash, - lines on--del-washwith--text-2text). - Draft (held at the Gate). To, Subject and Body edit in place: tap into them. There is no Edit button, and the only actions are Send and Discard (the same editing as the Deck, js/editable.js). The sources it drew from, if any, as neutral rows with the memory stack icon. An edit changes the primary to "Send edited".
- Question. The question in Lead type, then the choices as full-width rows (radius 10,
--rule-strongborder, 52 tall, the choice in Row title and its note in Meta). Tap a choice to select it (--signal-washfill,--focusborder); the last row is a free-text field ("Something else").
Action area (8 between buttons, 34 bottom):
- Ask: the primary, full width, 54 tall, radius 12,
--primary-bg: "Approve", one tap (asks are alwaysrequired: false). Under it, two secondary buttons stacked, 46 tall, radius 12: "Always in <project>" (secondary: approves and writes the rule), then "Deny" (a ghost). Whereask.always_projectis null, only Deny sits under the primary. - Held item at the Gate (a send, spend or delete of outside data): the primary follows the three presence states below, "Send with Face ID" (22 px glyph; "with Touch ID", "with fingerprint", "with passkey" by device) only when not covered; "Discard" a ghost, never a proof. Drafts are held sends. Nothing else.
- Question: "Answer" primary, enabled once a choice is picked or text typed; "Later" a ghost. No proof.
- When Face ID shows (the no-nag rule): only for pairing a device, reading or using a vault
secret, outbound actions (messages, posts, emails, payments), and deleting outside data (mail,
files, posts). In this sheet that means a held send, spend or delete at the Gate. Asks
(Claude's permission asks and questions), edits, commands, git pushes and discards never
prompt. The phone reads the box's
presence: {required, covered}on every ask and held item and never guesses from the tool name. There are three states:required: false: no proof. "Approve" or "Send" with a check glyph, one tap.required: true, covered: true: this device proved presence recently (one proof lasts about 30 minutes). No prompt. "Send" (or "Delete") with a check glyph, and one Meta--text-2line under the button: "Confirmed with Face ID a moment ago" ("12 min ago").required: true, covered: false: "Send with Face ID" ("Delete with Face ID") with the glyph; the proof runs on tap and opens a new presence session. The "ago" comes frompresence.since(the proof's time, which the box sends withcovered); without it the line is left out. If the session lapsed between render and tap, the box answers 403error.code: "presence_required": the sheet runs Face ID in place without closing and retries the same approve withx-vyre-presence-keep: 1, which starts a fresh session. A 403deniedis a refusal, never a prompt: the sheet shows the reason and closes.
- A proof is the same box-verified check per ADR 0004 on every surface. The web build uses a WebAuthn passkey assertion. The iOS and Android builds use a device-key signature
after Face ID or the fingerprint (ADR 0018): a P-256 key in the Secure Enclave or StrongBox,
enrolled once through the Deck's passkey, signs the same message the Capsule signs, and the box
checks it as method
device. (A store app cannot assert passkeys for a self-hosted box's domain, so native never uses platform passkeys.) On success the sheet closes, the row collapses and the next item's row pulses once. On cancel nothing changes. - While the answer travels, the tapped button shows a spinner and both buttons are disabled.
If the box has not confirmed in 15 s, the sheet stays open with "Didn't reach your box. Try
again." in Meta
--text-2, and nothing is lost. - Verbs: the Deck's gate card says Allow once / Always in <project> / Deny. The phone says
"Approve" (the user's pick); both send the same decision,
allow.
6. Chats
The list (page)
Project filter chips across the top (All, then each project), 30 tall, radius 8, Button 13/18: the
selected one --text fill with --bg text, the others --rule-strong outline with --text-2
text. This replaces the old Projects tab. Below, one card of rows, newest first:
- Tile 32, session name (Row title), time on the right (Meta), last line (Secondary
--text-2, one line), "<agent> · <project>" (Meta--label). A running session shows a 7 px--textdot before the agent's name; one with an open ask shows the--beacon-dotand its count on the right instead of the time. - Tap opens the session. Swipe left: Archive.
A session (pushed)
Nav bar, 52 tall under the safe area, hairline below, three columns: "< Chats" (17) on the left;
the session name (Row title) centred with "<agent> · <project>" under it in Meta --label; a 44 px
more button on the right (rename, watch, stop, archive). The header does not show the page labels;
the edge swipe goes back.
The transcript, 16 side padding, 14 between items, a centred Meta --label time stamp at each
gap of more than an hour ("Today 12:01"):
- You: a bubble on the right, max 290 wide,
--hoverfill,--ruleborder, radius 18 18 6 18, 10 x 14 padding, Lead 17/24. - Agent: no bubble. A 24 px tile and the author in Label
--text-2(the assistant's name, or the agent's name; "Vyre" only when none is known), then the text in Lead 17/24. - Tool rows: grouped in one box,
--ruleborder, radius 10, hairlines between rows. One row each, 9 x 12 padding, Code 13/18: a 16 px check (done) or spinner (running) in--label, the action and target ("Edited reports/q3.tsx", "Ran npm test"), the result on the right in--label("+412 -38", "42 passed"). Tap expands the row in place: the full command, output (Log 12/16, 12 lines then "Show all"), or the diff. - Streaming: the reply grows in 17/24 with a 2 x 19
--textcaret at the end. The box sends text in bursts; the app reveals what it has over 150 ms at the display rate (at least one character a frame), so the reply flows instead of jumping, and never lags more than 250 ms. - Following: the transcript is an inverted list, so it opens at the newest item. It follows
new items while you are within 32 of the bottom; scrolling up more than 24 stops following,
and a 44 px round "Jump to latest" button (
--panel,--rule-strongborder, a down chevron) appears 12 above the composer on the right. Tapping it scrolls down and follows again. Older items load as you come within 96 of the top. - Tool runs: three or more tool rows in a row collapse to one row ("Edited 4 files, ran 2 commands") that expands in place. A single failed row never collapses.
- Approval card (an ask in this session):
--panelfill,--rule-strongborder, no wash, radius 12, 14 padding, 10 between parts. A 7 px--beacon-dotand "<agent> is waiting on you" in Label--beacon-ink, "Details" on the right, a ghost in Button 13/18--text(opens the detail sheet). The command in a mono block (--code-bg, radius 6, 10 x 12, Code 13/18). One Meta--text-2line of facts ("3 commits · 6 files · harlow-legal/reports"). Then Deny (a ghost) and Approve (primary, with the Face ID glyph only whenpresence.requiredis true andcoveredfalse) side by side, 44 tall, radius 10, Button 17/24. Answered, it shrinks to one Meta line: "Approved by you, 12:07". - Question card: the same card with the choices as rows inside it.
- Recalled: a neutral row when memory fed the reply: the memory stack icon, "From memory"
in Label
--text-2and the source in Meta, no wash.
Composer: a bar pinned above the keyboard or the bottom safe area, --bg fill, hairline above,
8 x 12 padding: a 36 px round attach button (--hover, plus glyph), the input (38 tall, radius
19, --rule-strong border, 17, placeholder "Message <agent>"), and a 36 px round button: send
(arrow up, --primary-bg) once there is text, stop (an 11 px square on --text) while a reply
streams. The Capsule is hidden in a pushed chat. The composer rides the keyboard by a transform (not a
relayout each frame), and the transcript gets the same bottom inset, so the last line stays in
view. On iOS keyboard heights under 120 (the prediction bar alone) are ignored. The input grows
to 6 lines, then scrolls. A fast flick down the transcript (over 1.5 pt/ms) closes the keyboard;
a slow drag to read does not. Sending resumes the session here (the lease moves
to this phone); if another surface holds it, one Meta line above the composer says who, and
sending takes it.
7. Find (the Capsule, opened)
A full-height sheet, 16 side padding, 12 between blocks:
- Top row, 52 tall: the search field (40 tall, radius 10,
--hoverfill,--ruleborder, magnifier in--label, 17 text, a clear button) and "Done" (17) on the right. - A segmented control: All, Chats, Files, Memory, Run. 32 tall, radius 9,
--hovertrack with a--ruleborder, 2 inset; the selected segment--bgwith a--rule-strongborder and 13/18 600 text, the others 13/18--text-2.
Results as the user types:
- Ask: a card row: the assistant's tile, "Ask <assistant>" in Row title, the typed words as
a question in Secondary
--text-2, a chevron. Tap runs it: a new lean thread with the assistant, pushed as a chat. - Run: a card of matching commands (the Deck's Find grammar:
@kit ...,tell <session> to ...,watch <session>, "New session on ..."): a terminal glyph, the plain-words action in Row title with matches on--match, the command it will run in Log 12/16--labelbelow. - From memory: neutral rows with the memory stack icon, the fact with matches on
--match, source and date in Meta--text-2below. - Chats: header in Row title, then a card of sessions: name (Row title), the snippet (Secondary,
one line, matches on
--match), "<project> · <date>" (Meta). - Files: from the paired Macs, when they are online: file glyph, path in Code 13/18, "<repo> · <Mac>" in Meta. One Meta line says so when the Macs are away.
Empty query: recent searches and the four most recent sessions. Search waits 150 ms after the last keystroke and cancels the previous request.
8. Agents
Under the header, one Meta --label line: "1 working, 1 idle".
- Working agent card (
--panel, radius 10, 14 padding, 12 between parts):- A 40 px tile (radius 11), the name in Row title, and under it a 7 px
--textdot with a 3 px--rulering and "Working on <session>" in Secondary--text-2; elapsed time in Code--labelon the right. - The live console:
--code-bg,--ruleborder, radius 8, 10 x 12, Log 12/16--text-2, the last 3 lines (the command in--label), a block caret on the last. - "Step 3 of 5" in Secondary
--text-2and a 3 px progress bar filling the rest of the row. - Watch (eye glyph) and Pause (pause glyph) secondary buttons side by side, 40 tall, radius 10, Button 17/24.
- The projects it may work in as chips: Meta 12/16, 3 x 8 padding, radius 4,
--rule-strongborder,--text-2.
- A 40 px tile (radius 11), the name in Row title, and under it a 7 px
- Idle agent: one card row, 40 px tile, the name in Row title with a role tag ("Assistant", Meta,
radius 4, outline) and "Idle · sees every project" in Secondary
--label, a chevron. Tap opens the agent. - Scheduled: header in Row title, then a card of rows: a clock glyph (22,
--label), the job in Lead and "<agent> · <cadence>" in Secondary--label, "in 14h" on the right in Meta--label. - Watch pushes the live view: the full console, scrollable, with the session's tool rows, and the same approval card when it asks.
9. Motion
| What | How |
|---|---|
| Hover-like press feedback | Row fills --hover for the press, 120 ms ease-out out |
| Row swipe | Follows the finger 1:1; release springs (iOS default: response 0.35, damping 1) |
| Row collapse after an answer | Height to 0 over 180 ms ease-out, rows below move up |
| Sheet open and close | iOS sheet presentation; page behind scales to 0.94 |
| Expand a tool row or diff | 180 ms ease-out |
| Page swipe | Follows the finger; settles in 220 ms, cubic-bezier(0.25, 0.1, 0.25, 1); label colour crossfades |
| Long press on a row | 450 ms still (6 of slop) opens the row menu (Open, Archive, Rename), with a selection haptic |
| Streaming | Tokens appear as they arrive, no fade; caret blinks at 1 s |
| Open session highlight | --match fill on the target row, fades out over 1.2 s |
| Attention dot on arrival | One pulse (scale 1 to 1.6 and back, 400 ms), then still |
Reduce Motion: no scale behind sheets, no pulse, swipes and pages crossfade instead of slide. Nothing animates in the background.
10. Haptics (iOS and Android through expo-haptics; the web uses none on iOS, and navigator.vibrate(10) on Android only for commits)
| Moment | expo-haptics |
|---|---|
| Swipe crosses the commit point | impactAsync(Medium) |
| Approval or send succeeds | notificationAsync(Success) |
| Deny, discard | notificationAsync(Warning) |
| Page label snaps, long-press menu opens | selectionAsync() |
| Hold-to-talk starts and stops | impactAsync(Soft) |
| A new item needs you while the app is open | notificationAsync(Warning), once, only if Now is on screen |
11. States
- Loading: layout skeleton (the real card shapes in
--hover, no shimmer) for the first load only; later refreshes keep the old content until the new arrives. - Offline (box unreachable): one line under the header,
--text-2, "Can't reach your box. Showing what it said at 12:04.", with Retry. Actions that need the box are disabled with the reason on press. The app keeps the last Now, chat list and open transcripts on the device, so it opens with content even offline. - Reconnecting: a small pill under the header ("Reconnecting", then "Updating") with a spinner, never a blocking screen, gone when the catch-up lands. Retries back off from 2 s to 60 s and stop while the app is in the background.
- Notifications: a push only when something needs you (an ask, a held item, a question) or a watched session finishes. None when you used Vyre on any surface in the last 3 minutes (the box shows it in the open surface instead), none for an item already on your screen, and none for errors. Tapping one opens the app on that item's detail sheet (also from a cold start). No action buttons in the notification: an answer always happens in the app, where the presence check can run. The one exception: planner alarms and timers always push and ring, even right after you used Vyre and through quiet hours. Planner reminders follow the rule above. The box-side rule is pwa's (core/push, ADR 0011).
- Mac away: its sessions stay, read-only, with a machine chip; Open session still works.
- Empty pages: one sentence and a way forward. Chats: "No sessions yet." plus the Capsule. Agents: "Only <assistant> so far." plus "New agent".
- Errors:
--text, a crossed-circle glyph, afailedLabel and the reason; the attention colour only when the user must act. Never red.
12. Accessibility
- Real buttons and links. Every swipe action is also in the row's accessibility actions (VoiceOver
custom actions, Android
AccessibilityAction) and in the detail sheet. - Labels: a Needs row reads "kit, Harlow Legal, wants to push q3-report, git push origin q3-report, 4 minutes ago. Actions: Approve, Deny, Open."
- Contrast:
--labelis the smallest text colour (5.4:1 dark, 5.0:1 paper;--text-2on tints). Attention text measures 9.1:1 (dark) and 6.3:1 (paper) on the ground. - The mockups were rendered to PNG in both themes and every text node, placeholder and icon
was measured against its composited background: 1,048 checks, all at AA (4.5:1 text, 3:1 icons
and 24 px text). Builds repeat the check on their real screens before shipping, and watch for
a CSS rule like
button { color: inherit }beating a button's own colour class (deck-design hit exactly this: unreadable buttons). - Dynamic Type up to AX3: rows grow, the header labels shrink to fit and then scroll horizontally, the Capsule grows to 64.
- Focus ring: 2 px
--focus, 2 px offset.
13. Light by default
- Needs, Working and agents come from events (gate.held, gate.released, ask.raised, ask.answered, thread.*). The fallback poll is 60 s, and none while hidden.
- The live console subscribes only while an agent panel or live view is on screen, and shows at most 4 new lines a second.
- Transcripts load the last 50 items and page backwards on scroll.
14. What the build does
One Expo app (mobile leads; ADR 0027) for iOS, Android and the web. The pwa team's Deck phone layout (deck/, under 600 px) stays until the Expo web build replaces it. Order:
- Shell: header with pages, the page swipe (react-native-gesture-handler and reanimated), the Capsule.
- Now: the Needs you list with swipe and Undo, the Working card, From memory; setup lives in onboarding plus the one-row reminder.
- Detail sheet for ask, draft and question, with Open session (@gorhom/bottom-sheet at a large snap point; the native sheet presentation on iOS where the router offers it).
- Chat: nav bar, bubbles and agent messages, the tool-row box, the approval card, the composer with stop (react-native-keyboard-controller for the keyboard).
- Find as the Capsule sheet: search field, segmented scope, Ask, Run, From memory, Chats, Files.
- Agents: the working-agent card with its console, idle rows, Scheduled, and the live view.
Throughout: the haptics in section 10, Dynamic Type and font scale, the bundled fonts (expo-font),
and the tokens from one tokens.ts (section 2).
15. Contracts
Landed on work/chat (10604b9):
- Anchors for Open session.
threads.asks(andthreads.get().asks) items carryanchor: { tool_use_id, event };gate.helditems carryanchor: { tool_use_id, event, thread, at }. Chat scrolls toanchor.tool_use_idwhen there is one, else toanchor.event, else to the first item at or afteratinthread. A model's MCP call has no tool_use_id yet, so gate items land by event or time. - Questions as Needs items.
threads.asksreturnskind: "question"items withquestions,agentandthread_name. Answer withthreads.answer { ask, decision: "allow", answers: { [question]: "label" | "a, b" | "typed text" } }, ordecision: "deny"for Later/decline. The ask id is the capability. - Always in <project>. Show the button only when
ask.always_projectis a name (it can be null for a moment afterask.raised; the sheet adds the button when it arrives, never reflowing under the user's thumb mid-tap). It sendsthreads.answer { ask, decision: "always", scope: "project" }with the same presence check; the rule is written to that project's own.claude/settings.local.json.ask.answeredcarriesscope: "project", and the approval card then reads "Always allowed in <project>, 12:07".
Queued with chat (next session; the sha follows when it lands):
- Diff summary. Permission asks for Edit, MultiEdit and Write get
detail.changes: [{ file, added, removed }]. A held git push ingate.heldgetschanges: [{ file, added, removed }]andtotals: { files, added, removed }(fromgit diff --numstatof the pushed range). The sheet's Changes row reads the totals ("6 files +412 -38", or sumschangeswhen there are no totals) and expands to the per-file list. With neither, the row is left out.