docs GitHub

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:

  1. Glance at what needs you, and answer it (approve, deny, reply, pick a choice).
  2. Chat with a session or an agent, and read what it did.
  3. Find anything: a session, a file, a memory, a command to run.
  4. 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.

RoleDarkPaperUse
--bg#0E0D0C#F4F1EAPage ground
--panel#161513#FBFAF6Cards, sheets, the Capsule
--hover#1E1C1A#EEEAE2Agent tiles, pressed rows, the Deny reveal, secondary buttons
--rule#2B2926#DCD7CCHairlines between rows
--rule-strong#3A3733#C9C3B7Card and input borders, outline buttons
--text#F1EEE6#141311Primary text
--text-2#B3AEA4#4A463FSecondary text
--label#8C877D#6B665DLabels, meta, placeholders. Smallest text colour allowed
--primary-bg / --primary-ink#F1EEE6 / #0E0D0C#141311 / #F4F1EAThe one primary button per view; also the count on a --beacon-dot badge
--primary-hover#FFFFFF#4A463FThe primary while pressed
--focus#F1EEE6#141311Focus ring
--signal-washrgba(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 / #5B3FC4Attention: needs you. A dot, a label or a count badge, nothing else
--del-washrgba(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#F0EDE5Command blocks, the live console
--mark-wire / --mark-dot#F1EEE6 / #F1EEE6#141311 / #141311The 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, a failed Label 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 --text whose 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-bg with --primary-ink (ink on bone in dark, paper on ink in paper). One per view.
  • Secondary: --text on --hover, --rule-strong border.
  • 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.

RoleFamilySize / lineWeightTrackingUse
Sheet titleSans28 / 34600-0.015emThe detail sheet's title
PageSans22 / 28600-0.01emThe page labels in the header (Now, Chats, Agents)
Row titleSans17 / 246000Rows in cards, section headers on Now, group headers, the chat nav title, agent names
LeadSans17 / 244000Chat messages, a question's text, the agent's reason, fact rows
InputSans17 / 244000Search and composer text (never under 16: no iOS zoom)
ButtonSans17 / 246000Every button label, sentence case (13 / 18 on the swipe reveals and chips)
SecondarySans13 / 184000Row second lines, facts under a card, --text-2
LabelSans12 / 166000Small labels ("Why kit wants to", "From memory"), sentence case, --label
MetaSans12 / 164000"kit · Harlow Legal", times, hints, tags
CodeMono13 / 184000Commands in blocks and rows, paths, counts
LogMono12 / 164000Live 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 --rule between rows; --rule-strong on 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), paper 0 24px 48px -24px rgba(20,19,17,0.28). In React Native: boxShadow with the same string (new architecture), and elevation: 12 as the Android fallback; drop the inset line where boxShadow is 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 (hitSlop makes 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, --panel fill, --rule-strong border, 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, --hover fill, --rule-strong border, 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-2 for 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-bg with --primary-ink: a check glyph (24) over "Approve" (asks) or "Send" (drafts) in 13/600, the Face ID glyph instead when presence.required is true and covered false (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, --hover fill with --text label 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 (--rule track, --text fill, 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-2 on the left; a 30 px round close button (--hover, --rule border, x glyph) on the right.
  • Title in Sheet title (28/34, 600, -0.015em), 12 above.
  • Row 3: a 7 px --beacon-dot and "Held 4 min" in Label --beacon-ink on 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, --rule border, 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-wash with --text-2 text).
  • 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-strong border, 52 tall, the choice in Row title and its note in Meta). Tap a choice to select it (--signal-wash fill, --focus border); 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 always required: false). Under it, two secondary buttons stacked, 46 tall, radius 12: "Always in <project>" (secondary: approves and writes the rule), then "Deny" (a ghost). Where ask.always_project is 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-2 line 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 from presence.since (the proof's time, which the box sends with covered); without it the line is left out. If the session lapsed between render and tap, the box answers 403 error.code: "presence_required": the sheet runs Face ID in place without closing and retries the same approve with x-vyre-presence-keep: 1, which starts a fresh session. A 403 denied is 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 --text dot before the agent's name; one with an open ask shows the --beacon-dot and 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, --hover fill, --rule border, 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, --rule border, 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 --text caret 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-strong border, 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): --panel fill, --rule-strong border, no wash, radius 12, 14 padding, 10 between parts. A 7 px --beacon-dot and "<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-2 line of facts ("3 commits · 6 files · harlow-legal/reports"). Then Deny (a ghost) and Approve (primary, with the Face ID glyph only when presence.required is true and covered false) 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-2 and 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, --hover fill, --rule border, 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, --hover track with a --rule border, 2 inset; the selected segment --bg with a --rule-strong border and 13/18 600 text, the others 13/18 --text-2.

Results as the user types:

  1. 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.
  2. 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 --label below.
  3. From memory: neutral rows with the memory stack icon, the fact with matches on --match, source and date in Meta --text-2 below.
  4. Chats: header in Row title, then a card of sessions: name (Row title), the snippet (Secondary, one line, matches on --match), "<project> · <date>" (Meta).
  5. 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 --text dot with a 3 px --rule ring and "Working on <session>" in Secondary --text-2; elapsed time in Code --label on the right.
    • The live console: --code-bg, --rule border, 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-2 and 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-strong border, --text-2.
  • 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

WhatHow
Hover-like press feedbackRow fills --hover for the press, 120 ms ease-out out
Row swipeFollows the finger 1:1; release springs (iOS default: response 0.35, damping 1)
Row collapse after an answerHeight to 0 over 180 ms ease-out, rows below move up
Sheet open and closeiOS sheet presentation; page behind scales to 0.94
Expand a tool row or diff180 ms ease-out
Page swipeFollows the finger; settles in 220 ms, cubic-bezier(0.25, 0.1, 0.25, 1); label colour crossfades
Long press on a row450 ms still (6 of slop) opens the row menu (Open, Archive, Rename), with a selection haptic
StreamingTokens 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 arrivalOne 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)

Momentexpo-haptics
Swipe crosses the commit pointimpactAsync(Medium)
Approval or send succeedsnotificationAsync(Success)
Deny, discardnotificationAsync(Warning)
Page label snaps, long-press menu opensselectionAsync()
Hold-to-talk starts and stopsimpactAsync(Soft)
A new item needs you while the app is opennotificationAsync(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, a failed Label 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: --label is the smallest text colour (5.4:1 dark, 5.0:1 paper; --text-2 on 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:

  1. Shell: header with pages, the page swipe (react-native-gesture-handler and reanimated), the Capsule.
  2. Now: the Needs you list with swipe and Undo, the Working card, From memory; setup lives in onboarding plus the one-row reminder.
  3. 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).
  4. 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).
  5. Find as the Capsule sheet: search field, segmented scope, Ask, Run, From memory, Chats, Files.
  6. 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 (and threads.get().asks) items carry anchor: { tool_use_id, event }; gate.held items carry anchor: { tool_use_id, event, thread, at }. Chat scrolls to anchor.tool_use_id when there is one, else to anchor.event, else to the first item at or after at in thread. A model's MCP call has no tool_use_id yet, so gate items land by event or time.
  • Questions as Needs items. threads.asks returns kind: "question" items with questions, agent and thread_name. Answer with threads.answer { ask, decision: "allow", answers: { [question]: "label" | "a, b" | "typed text" } }, or decision: "deny" for Later/decline. The ask id is the capability.
  • Always in <project>. Show the button only when ask.always_project is a name (it can be null for a moment after ask.raised; the sheet adds the button when it arrives, never reflowing under the user's thumb mid-tap). It sends threads.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.answered carries scope: "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 in gate.held gets changes: [{ file, added, removed }] and totals: { files, added, removed } (from git diff --numstat of the pushed range). The sheet's Changes row reads the totals ("6 files +412 -38", or sums changes when there are no totals) and expands to the per-file list. With neither, the row is left out.