Files
nucleic/docs/UX_IOS.md
T
abkslmandClaude Opus 4.8 0e47298720 Add Nucleic design plan: Swift/SwiftUI Claude Code/Codex wrapper
Comprehensive design for a Conductor-style macOS app (host) + iPhone
(thin remote client) that runs parallel Claude Code / Codex sessions in
isolated git worktrees, with interactive per-session approvals.

PLAN.md is the hub; docs/ over-specifies each layer:
- BACKEND_PROTOCOL: normalized AgentEvent model, capabilities, approvals
- ADAPTERS: Claude MCP approval server + Codex app-server JSON-RPC,
  with wire contracts confirmed from primary sources
- SYNC_PROTOCOL: LAN/relay E2EE sync, seq-cursor catch-up
- WORKTREE_MANAGER: worktree lifecycle, diff, integrate, reconcile
- RUNTIME_ARCHITECTURE: single-writer pipeline, GRDB schema, concurrency
- UX_MACOS / UX_IOS: information architecture and approval flows
- OBSERVABILITY_AND_TESTING: redaction-aware observability + fixture harness

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-11 23:49:19 -07:00

11 KiB
Raw Blame History

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

  1. 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.
  2. Projection, not authority. Every screen is a subscribe over the sync protocol; every action is an intent (approvalRespond, sendInput). No state is invented on-device.
  3. Glanceable. "What needs me, and how many" must be answerable from the lock screen / a Live Activity / a widget — without unlocking into the app.
  4. 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.
  5. 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 .coalesced on cellular, .full on WiFi/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 before Allow is enabled. Deliberate friction mirrors the Mac.
  • Payload is minimal (title + reason, no code/diff — SYNC §6); the app pulls the real ApprovalRequest over the E2EE channel on open.

5.2 In-app approval card

┌─ ⚠ Permission requested ──────┐
│ Bash · execute · destructive  │
│   $ rm -rf build/ && npm pub… │   tap → full input sheet
│                               │
│ [   Deny   ] [   Allow   ]    │
│ Allow always ▾                │
│  ├ this command, this session │
│  ├ any Bash, this session     │
│  └ Bash matching `npm *`      │
│ 🔒 Face ID required (destruct.)│
└───────────────────────────────┘
  • Allow always writes the same always_rule the Mac would (RUNTIME §5) at the chosen scope.
  • 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 · LAN ● green "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/sendInput requires 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 sinceSeq catch-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 YOU sessions.
  • Home/Lock-screen widget: attention count + the top waiting session; tap → that approval.
  • Focus/notification tuning: per-project notification levels (e.g. only destructive approvals break through a Focus) — set on the phone, scoped to this device.

9. Mapping to sync + runtime

iOS surface Sync message Notes
Sessions home listSessionssessionList, 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 are control, 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

  1. High-risk on mobile — biometric-gated in-app Allow (proposed) vs. no mobile approve for destructive at all (force it to the Mac)? Trades convenience against blast radius.
  2. Follow-up input scope — keep sendInput at approve, or is "type a new prompt" really a control action that belongs behind scope=control? (Leaning: keep it; it's the natural mobile unblock.)
  3. 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?
  4. Live Activity budget — one Activity per running session vs. a single aggregate Activity ("3 running, 1 waiting") to respect iOS limits.
  5. 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.