Files
nucleic/docs/UX_IOS.md
T

246 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 · 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 always` writes the same `always_rule` the Mac would (RUNTIME §5) at the chosen scope.
**Not offered on `destructive` requests** — each destructive action must be a deliberate,
one-off `Allow`, 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/`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 <Mac name>." 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 | `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 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.