11 KiB
Nucleic — iPhone UX (v0)
The phone is a thin remote client to the Mac host (SYNC_PROTOCOL): no local git or CLI,
holds no canonical state, scope approve in v1. Its whole job: tell you which session
needs you and let you answer — from a notification, a glance, or in the app — wherever you
are. Everything here is a projection of the same authority the Mac UX renders (UX_MACOS §8).
Status: design draft.
1. Invariants
- Notification-first. The primary entry point is a push, not app-launch. The app must be great when opened cold from a notification straight to one approval.
- Projection, not authority. Every screen is a
subscribeover the sync protocol; every action is an intent (approvalRespond,sendInput). No state is invented on-device. - Glanceable. "What needs me, and how many" must be answerable from the lock screen / a Live Activity / a widget — without unlocking into the app.
- Honest about connectivity. The phone is often disconnected or on a flaky link; stale data is always labeled, and actions disable rather than silently fail.
- Risk-gated. Destructive/network approvals require an extra deliberate step (biometric), matching the Mac's "open detail" friction (UX_MACOS §4).
2. Capabilities in v1 (scope = approve)
| Can | Cannot (scope = control, later) |
|---|---|
| View session list + status across projects | Start a new session |
| Read transcript (read-friendly) | Merge / rebase / squash |
| Read diff | Discard a session |
| Answer approvals (the point) | Manage projects / pairing-of-others |
Send a follow-up prompt (unblock awaitingInput) |
Interrupt a run |
sendInput is allowed at approve (SYNC §5.1) — typing the next step to unblock a paused
agent is a core mobile use; structural git actions stay on the Mac until scope=control ships.
3. Navigation
A 2-tab TabView (a phone doesn't need the Mac's three columns):
┌─────────────────────────┐ Tab 1: Sessions (attention-first home)
│ Sessions │ Tab 2: Settings (host, device, connection)
│ ──────────────────── │
│ ⚠ NEEDS YOU (2) │ ← pinned section, top
│ ⚠ auth-refactor ProjA │
│ ◔ payment-flow ProjC │
│ ─ RUNNING (4) ─────── │
│ ◐ flaky-tests +312/−40 │
│ ◐ graphql +88/−12 │
│ ─ DONE (3) ────────── │
│ ✓ docs-pass │
│ ┌─────────────────────┐ │
│ │ ● Connected · LAN │ │ ← persistent connection chip
│ └─────────────────────┘ │
│ [ Sessions ] [ ⚙ ] │
└─────────────────────────┘
Same status glyphs/semantics as the Mac (UX_MACOS §1) so the two devices read identically.
NEEDS YOU is pinned and badge-counted (= app icon badge). Tapping a row pushes detail.
4. Session detail
NavigationStack push. A segmented control swaps three read views; the bottom is the action
area.
┌─────────────────────────┐
│ ‹ auth-refactor ProjA │
│ Claude · ⚠ awaiting │
│ [ Transcript | Diff ] │ (Log is power-user; behind ⋯)
│ ─────────────────────── │
│ 🤖 I'll update the auth │
│ middleware… │
│ ▸ Bash git status │ tap to expand input/result
│ ▸ Edit auth/mw.ts │ → jumps to Diff
│ … │
│ ─────────────────────── │
│ ⚠ Permission requested │ ← action area (§5)
│ Bash · destructive │
│ $ rm -rf build/ │
│ [Deny] [Allow ▾] │
└─────────────────────────┘
- Transcript: streamed markdown + collapsible tool rows; verbosity defaults to
.coalescedon cellular,.fullon Wi‑Fi/foreground (SYNC §5.5) to save battery/data. - Diff: read-only, unified (narrow screen), file picker, syntax highlight. Sourced from the
host snapshot's
diffStat+ on-demand diff fetch. - Action area is status-driven:
awaitingApproval→approval card;awaitingInput/finished→a compact prompt composer (sendInput);running→live activity line (no interrupt at this scope).
5. The approval interaction (the defining feature)
Three entry paths, one resolution model (first-responder-wins, SYNC §5.4 — "Approved on Mac" collapses the card if someone beats you).
5.1 From a notification (primary path)
Lock screen / banner:
┌─────────────────────────────────┐
│ Nucleic · auth-refactor │
│ ⚠ Bash wants to run: rm -rf build│
│ [ Deny ] [ Allow ] │ ← actionable (low/medium risk only)
└─────────────────────────────────┘
- Low/medium risk (
readOnly/write/execute): inline notification actions resolve directly via a background task — no unlock needed beyond the OS's own auth on the action. - High risk (
destructive/network): the notification has no inline Allow. It opens the app to the approval card, which requires Face ID/Touch ID beforeAllowis enabled. Deliberate friction mirrors the Mac. - Payload is minimal (title + reason, no code/diff — SYNC §6); the app pulls the real
ApprovalRequestover the E2EE channel on open.
5.2 In-app approval card
┌─ ⚠ Permission requested ──────┐
│ Bash · execute · network │
│ $ npm publish │ tap → full input sheet
│ │
│ [ Deny ] [ Allow ] │
│ Allow always ▾ │
│ ├ this command, this session │
│ ├ any Bash, this session │
│ └ Bash matching `npm *` │
│ 🔒 Face ID required │
└───────────────────────────────┘
Allow alwayswrites the samealways_rulethe Mac would (RUNTIME §5) at the chosen scope. Not offered ondestructiverequests — each destructive action must be a deliberate, one-offAllow, mirroring the Mac.- Modify-and-allow appears only if
BackendCapabilities.canModifyToolInput(Claude) — the card reads capabilities just like the Mac, so Codex hides it.
5.3 Live Activity / Dynamic Island
For a session that's running or has a pending approval, a Live Activity keeps it on the lock
screen / Island:
Dynamic Island (expanded):
◐ auth-refactor · ⚠ 1 approval waiting [ Open ]
Glance = status + pending-approval count; tap routes straight to the card. (Apple Watch approve-from-wrist is a natural later add; same intent.)
6. Connectivity states (must be unambiguous)
The connection chip (and detail action area) reflect transport truth:
| State | Chip | Behavior |
|---|---|---|
| Connected · Direct (LAN) | ● green "Direct (LAN)" | full interactivity, .full verbosity allowed |
| Connected · Relay | ● green "Relay" | full interactivity, default .coalesced |
| Reconnecting | ◐ amber | last snapshot shown, stamped "as of HH:MM", actions disabled |
| Disconnected | ○ gray | read-only stale view; actions disabled with "reconnect to act" |
| Host asleep / unreachable | ○ "Mac offline" | explains the Mac must be awake (LAN) / relay needed |
- No optimistic actions. An approval/
sendInputrequires a live channel; if it drops mid-send, the UI shows it failed and the request is still pending on the host (idempotent re-send on reconnect, deduped on the host). - Reconnect is automatic with
sinceSeqcatch-up (SYNC §7); the user sees a brief "catching up…" then live state.
7. Pairing & security UX
- Pair: Settings → "Add this iPhone" shows instructions; the Mac displays a QR, the phone scans it (SYNC §4.2). Success → "Paired with ." Identity keys live in the Secure Enclave.
- Biometric gate: Face ID/Touch ID required for high-risk approvals (§5.1) and, optionally (setting), for all approvals and for opening the app.
- Revoke: either side can unpair; the phone shows "This device was removed on the Mac" and drops to the pairing screen.
- Privacy posture surfaced: a short note that code/diffs are end-to-end encrypted and the relay can't read them — trust is part of the UX for a tool touching proprietary source.
8. Ambient surfaces
- App icon badge / Dock-equivalent: count of
NEEDS YOUsessions. - Home/Lock-screen widget: attention count + the top waiting session; tap → that approval.
- Focus/notification tuning: per-project notification levels (e.g. only
destructiveapprovals break through a Focus) — set on the phone, scoped to this device.
9. Mapping to sync + runtime
| iOS surface | Sync message | Notes |
|---|---|---|
| Sessions home | listSessions → sessionList, live sessionUpdated |
attention sort client-side |
| Open a session | subscribe(sessionID, sinceSeq, verbosity) → snapshot + events |
verbosity by transport/foreground |
| Approval card / notification | push wake → pull approvalRequested; approvalRespond(id, Decision) |
first-responder-wins; biometric on high-risk |
| Prompt composer | sendInput(sessionID, AgentInput) |
scope ≥ approve |
| Diff view | snapshot diffStat + on-demand diff fetch |
read-only |
| Connection chip | SecureChannel transport state |
drives action enablement |
The phone never holds canonical state — identical to the Mac (UX_MACOS §8). Two renderers, one host authority.
10. Differences from the Mac (by design)
- No create / merge / discard / interrupt (scope
approve; those arecontrol, later). - Notification-first, not window-first; Live Activity + widget replace the menu-bar item.
- Verbosity defaults down (battery/data) and gates up only when foregrounded on a fast link.
- Biometric on risky approvals; the Mac uses open-detail friction instead.
11. Open questions
- High-risk on mobile — biometric-gated in-app
Allow(proposed) vs. no mobile approve fordestructiveat all (force it to the Mac)? Trades convenience against blast radius. - Follow-up input scope — keep
sendInputatapprove, or is "type a new prompt" really a control action that belongs behindscope=control? (Leaning: keep it; it's the natural mobile unblock.) - Multi-host — v1 assumes one Mac; when a user pairs two Macs, does the phone show a host switcher or a merged cross-host list?
- Live Activity budget — one Activity per running session vs. a single aggregate Activity ("3 running, 1 waiting") to respect iOS limits.
- Offline approval queueing — confirm we never queue an approval to auto-send on reconnect (stale, possibly dangerous); current design refuses and requires a live channel.