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]>
244 lines
11 KiB
Markdown
244 lines
11 KiB
Markdown
# 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 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** 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 <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.
|