Static review of all ~3.6k lines of packaging/ios-companion/ (24 Swift files),
cross-checked against the backend (src/web/server.ts, soul/mail/push/agents) and
the plans (PLAN_IOS_ONBOARDING_v1.0.md, IOS_COMPANION_PLAN.md). Findings are
code-grounded (file:line); a few are flagged uncertain (need runtime confirm).
Severities: Blocker (broken core flow) · High · Medium · Low/Nit.
Scope note: simulator/unsigned builds only exercise empty/placeholder states for widgets + Live Activity (App Group / ActivityKit are no-ops there); those layouts are reasoned from code, not observed.
The app is well-architected (clean Theme, tolerant Codable, sound SSE
reconnect, Keychain-only token, correct device-revoke gating). The problems
cluster in five places:
- Mutation/control actions fail silently —
LisaClient.fire()discards the HTTP status, so every Approve/Deny/Send/Cancel/Revoke/Dismiss/push-register that returns 401/403/404/409 looks like it worked. Highest-impact bug. - Chat is bare — swallows in-band
errorevents (turn hangs), no auto-scroll, renders raw text (no markdown/code), shows no tool activity. It's the most-used tab and the weakest. - Soul → Values/Opinions render "—" —
SoulItemfield mismatch vs/api/soul. - Live Activity / Widget freshness — the activity never updates/ends on-device (frozen forever, never auto-dismisses); the widget snapshot is only written while the Dispatch tab is open.
- Security: app-switcher snapshot leaks the token — the biometric lock arms on
.background, after iOS already snapshotted the.inactiveframe.
Plus pervasive accessibility gaps (no VoiceOver labels, color-only status, sub-44pt tap targets) and several incomplete-vs-plan items (§F).
| # | Sev | Area | File:line | Issue |
|---|---|---|---|---|
| A1 | Blocker | Dispatch/Net | LisaClient.swift:106-110,155-167 |
fire() only throws on transport errors; HTTP 403/404/409 return normally and are discarded. So managedApprove/Deny/Send/Cancel, ptySend/Cancel, managedStart, consentRevoke(All), advisorDismiss, setAutonomyState all fail silently. A remote phone with remoteControl:false gets 403 on every control tap with zero feedback. Fix: make fire() throw LisaError.http(code) on non-2xx (like decode() already does). |
| A2 | Blocker | Chat | ChatView.swift:41-46 |
/chat SSE emits in-band {type:"error"} on a 200 stream (server.ts:2053); the loop only handles text, so an errored/aborted turn leaves the transcript stuck at "…Lisa: " with no message and no spinner. Fix: handle error/done events in the loop. |
| A3 | Blocker | Glance | LiveActivityController.swift (whole) |
The app calls Activity.request but never activity.update/.end anywhere. Refresh is 100% remote APNs, which the project says has no Apple key yet → a pinned activity is frozen at its start state forever and never auto-dismisses. Fix: drive update/end locally from the existing SSE merge in RosterModel. |
| A4 | High | Inspect/Net | Models.swift:194-201 ↔ src/soul/types.ts:73-88 |
SoulItem.label = statement ?? what ?? text ?? summary ?? name ?? "—", but /api/soul Values are {slug,title,body} and Opinions {slug,stance,confidence} → every Value & Opinion renders "—" in InspectViews.SoulView:74. Only Desires (what) work. Fix: add title/body/stance/slug to the fallback chain. |
| A5 | High | Glance | RosterView.swift:10-65 |
The home-widget App-Group snapshot is written only from RosterModel, which lives only while the Dispatch tab is on screen. Stay on Chat / force-quit → widget is stale (or "Open Lisa Pocket"); its own 15-min refresh just re-reads the same stale value. Fix: publish the snapshot from a tab-independent place + on background. |
| A6 | High | Security | App.swift:36,46-47 + AppState.swift:236 |
Biometric lock re-arms on .background, but iOS snapshots the .inactive frame for the app switcher first → the multitasking thumbnail shows the last screen (Settings token SecureField, chat) unredacted. Fix: lock / show a privacy cover on .inactive too. |
| A7 | High | Push/Net | LisaClient.swift:183-186 |
pushRegister builds prefs omitting mail; server defaults mail:true, so turning "Mail digest + alerts" off is a silent no-op. Fix: add "mail": prefs.mail. |
| A8 | High | Dispatch | RosterView.swift:374-423 |
Control buttons render purely off session.controllable, never off ControlPolicy.remoteControl (fetched only in Settings, shown read-only). A policy-blocked remote device sees Approve/Deny/Send/Cancel/Adopt that all 403 (silently, per A1). Fix: fetch policy in Dispatch; disable/annotate when blocked. |
| A9 | High | Dispatch | RosterView.swift:385-399 |
"done"/"error" managed+PTY sessions still show live Send/Cancel/Adopt; acting → ok:false → 404 (invisible per A1). Fix: hide/disable controls on terminal states. |
| A10 | High | Onboarding | QRScannerView.swift:33,111 |
didScan latches true on the first decode and is never reset; stop() halts the session. After any scan (incl. a non-LISA QR) the viewfinder freezes permanently. Fix: only latch on a successful LISA parse, or expose a reset. |
| A11 | High | Onboarding | OnboardingFlow.swift:201-207 |
Bad scan does openManual() while still on .scan with the camera live under the sheet; a second decode can fire go(.connect) behind the sheet (race). Also yanks the user into a form for a momentary mis-aim (plan intends "stay & retry"). Fix: leave .scan/pause session before presenting; don't force-manual on a parse miss. |
| A12 | High | Chat | ChatView.swift:64-70 |
No ScrollViewReader/auto-scroll — a streaming reply grows past the viewport and the view doesn't follow; user must drag continuously. Fix: add a bottom anchor + scrollTo on transcript change. |
| A13 | High | Chat | ChatView.swift:65 |
Replies render as one concatenated raw Text — no markdown, code blocks unreadable (proportional font, no background), links not tappable. For a coding-assistant client this is a major fidelity gap. Fix: per-message markdown/bubbles. |
- B1
OnboardingFlow.swift:258—connectScreenverify runs in.task; on Back→re-scan→forward the re-fire depends on SwiftUI rebuilding the branch; if identity is kept, user is stuck on a stale error/no spinner. Make deterministic (.id(step)/ explicit trigger). (uncertain) - B2
OnboardingManualEntry:443-449— Apple-sign-in error mapping lumps 500 /.decodeinto "Couldn't reach that LISA Cloud URL" (server was reached). Distinguish reachable-but-erroring. - B3
OnboardingManualEntry:454— cloud paste path routes throughparsePairingwhich defaults bare URLs tohttp, silently downgrading a cloud connection. Default https in cloud mode. - B4
OnboardingFlow.swift:178-182— pair screen is method-agnostic: a user who chose Mac app is still led with thelisa pairterminal command as primary. Branch bymethod(lead with menu-bar "Pair iPhone…"). - B5
RosterView.swift:430-432— optimistic send clears the field before the send resolves; a silent 403/404 loses the typed text. Clear only on success / restore on failure. - B6
RosterView.swift:49-57— roster merge only upserts; no "removed" SSE event, sodonerows linger until the next fullload(). Reconcile against full loads. - B7
RosterView.swift:123-140— no first-load spinner: opening Dispatch on a populated Mac briefly shows "No agents". (DispatchLedgerViewdoes this right with aloadedflag.) - B8
RosterView.swift:391— PTY output is a manual one-shotptyOutputpull; the liveGET /api/agents/pty/<id>/streamSSE (server.ts:1386) is never wired. Monitoring is half-built vs backend. - B9
SettingsView.swift:96-99+AppState.swift:96— "Connect to" segmented switch only persistslisa.mode; the liveconfigstill points at the other plane (cloud https vs mac http) until re-pair → broken mixed state. Reconcile or warn on switch. - B10
SettingsView.swift:104-119— ntfy push toggles are live@Statethat do nothing until "Register push" is tapped; no "pending/unsaved" hint. Auto-register on change or label it. - B11
SettingsView.swift:107—Toggle("Reve notes", isOn: $prefs.idle)mislabels theidle("while you were away") pref; the separateadvisorpref is never surfaced (hardcoded). Clarify labels; exposeadvisor. - B12
ReveView.swift:98-113— every sub-fetch istry?thenerror = nilunconditionally → load errors never surface; an unreachable Mac looks like a quiet-but-healthy day. Set an error when the ping throws (SenseView does this right). - B13
SenseView.swift:84-89— a failed consent revoke is swallowed; user believes they revoked a privacy signal when they didn't. Surface failure. - B14
ChatView.swift:75-82— Send enables on whitespace-only input (input.isEmptyvs trimmed); fires a no-op that clears the field. Trim in the disabled check. - B15 Chat — tool-call events (
tool_start/tool_end,server.ts:1991) are dropped; a tool-running turn shows a silent pause. Append an inline "· running Bash…" marker. - B16
InspectViews.swift:8-30—AsyncContentloads once on appear with no retry / no pull-to-refresh; on failure the user must leave+re-enter. Add a Retry button. - B17
AppState.swift:71-94/push.ts:509— APNs is the only push transport wired; withoutLISA_APNS_*on the Mac the app says "Push registered" but nothing ever arrives. The server fully supportsntfy(no Apple infra) but the client never offers it. Expose ntfy or surface the APNs-needs-config truth. - B18
LisaClient.swift:197-224— SSE usesURLSession.shared(60s default timeout) with no keep-alive from the server on/events; a quiet stream can be torn down with no auto-reconnect at the client layer (RosterView reconnects; Chat mood reconnects; raw/eventsconsumers should too). Use a dedicated session with large timeouts. - B19
AppState.swift:187—verifyConnection/onboarding can hang up to 60s on an unreachable host (.shareddefault timeout). Use a short (~10s) probe timeout. - B20
RosterView.swift:392-397— PTY output viewer (vertical scroll,maxHeight:200) clips long unbroken lines horizontally inside a Form row;DispatchDetailViewusesScrollView(.horizontal)— inconsistent. Unify. - B21
RosterView.swift:364-366— control-actionstatusrenders at the bottom of the form, often off-screen below an expanded PTY section. Once A1 is fixed, errors need to land near the action. - B22 Glance — Dynamic Island expanded layout drops the turn count (compact + Lock Screen show it);
AgentLiveActivity.swift:23-35. Add it. - B23 Glance — Live Activity uses hardcoded
.yellow/.blue/.red/.green(AgentLiveActivity.swift:47) becauseThemeisn't compiled into the widget target (project.yml:71); the pinned dot mismatches the roster and.yellowis low-contrast on a white Lock Screen. Move status colors toShared/. - B24 Glance — widget time is
style:.time(absolute, widget-process TZ); for a "café, Mac at home" use case.relativeis far clearer and conveys staleness (AgentCountWidget.swift:71). - B25 Glance — no staleness guard:
snap.updatedAt > .distantPastonly checks configured, so an hours-old snapshot renders as confidently live. Dim / append "· 2h ago" past N minutes. - B26 Glance — pinning the same session twice spawns duplicate activities (no
[sessionId: Activity]registry, button has no disable-after-pin);LiveActivityController.swift:10. Keep a registry.
- C1
OnboardingFlow.swift:55-79, 233-257— Welcome & Connect screens areSpacer-based with no ScrollView; large Dynamic Type / SE can clip with no scroll escape. Wrap in ScrollView. - C2
OnboardingFlow.swift:141,166,190— install/start/pair CTAs sit outside the ScrollView with bare.padding(.bottom,24); use.safeAreaInset(.bottom)for the home indicator. - C3
OnboardingScaffold.swift:86-91—CopyCommandRowclips the longserveCommand(lineLimit(2)+minimumScaleFactor); user can't read what they copy. Allow wrap / horizontal scroll. - C4
OnboardingFlow.swift:75— "I already have LISA running" shortcut back-fills install/start dots as completed (misleading); also assumes Mac (a cloud user lands on Mac pairing). - C5
OnboardingFlow.swift:209-228— scan-step "Not now" / instructions over the live camera have no scrim → unreadable on bright scenes. Add a material/gradient behind the top bar. - C6
ChatView.swift:65— empty placeholder "Say hi to Lisa." usesTheme.text(looks like a real message); dim it. - C7
ChatView.swift:64-85— no.scrollDismissesKeyboard(.interactively), no submit-on-return, no focus mgmt. - C8
ReveView.swift:67/InspectViews.swift:86— monospaced recap/memory in List rows can clip long lines; notextSelection. Enable selection, verify wrap. - C9
SettingsView.swift:48-89,183— Save/Apply/Connect confirmation is a single sharedstatusfar down the form (off-screen). Inline confirmation / toast. - C10
AgentCountWidget.swift:47-50,122—accessoryInlineuses flat emoji▶/⏸(monochrome system tint kills the "stuck" alarm) and no leading SF Symbol; noaccessoryCircular/systemLargefamilies.systemSmalldropstotal+summary (dead space). - C11
AgentLiveActivity.swift:11-21— Lock Screen banner has nocontainerBackground/activityBackgroundTint→ risk of low-contrast/unstyled chrome on iOS 18. - C12
LiveActivityController.swift:25—staleDate: nil→ a dead activity never visually ages. Set ~30 min. - C13
AgentSnapshot.swift:20/RosterView.swift:69— widget "stuck" mergeserror+waiting(very different urgency) and "total" countsdone/idlerows, inflating the headline. Disambiguate / exclude terminal rows.
Confirmed: no accessibilityLabel/Hint/Value/element anywhere in the content
tabs, roster, onboarding scaffold, or widgets. Specific gaps:
- D1 Status conveyed by color only, no labels:
StatusDot(Theme.swift:69/RosterView.swift:196), Live Activity dot, mail importance‼/!(ReveView.swift:46), copied-check green. VoiceOver + color-blind users get nothing. - D2 Onboarding progress dots have no VoiceOver representation (
OnboardingScaffold.swift:18) — add "Step N of M". - D3 Icon-only buttons unlabeled: Send (
ChatView.swift:80), Copy (28×28,OnboardingScaffold.swift:96), LockView Unlock hint, shield "permission required" icon (RosterView.swift:210). - D4 Sub-44pt tap targets: Send button, Copy button,
OnboardingSecondaryButton. - D5
RosterRowreads as disjoint fragments — combine into one labeled element; label StatCell counts ("Needs you: 2"). - D6
MoodChip/portrait (ChatView.swift:129) and Soul moodProgressView(value:)(InspectViews.swift:51) have no a11y label/value. - D7 Scanner region (
OnboardingFlow.swift:209) — no description that a camera is live / where to point.
- E1 (=A6) App-switcher snapshot exposes the token — lock on
.inactive. - E2
LisaClient.swift:70-78— token rides in?token=query onAsyncImageasset URLs (logged by proxies/caches); in cloud edition that's the sharedLISA_WEB_TOKEN. Necessary workaround (AsyncImage can't set headers) — ensure the server doesn't log query strings. - E3
TokenStore.swift:20—kSecAttrAccessibleAfterFirstUnlockallows backup/iCloud-Keychain migration of a full-control token;…WhenUnlockedThisDeviceOnlyis tighter if no background read is needed. - E4
AppState.swift:55,242— biometric lock fail-opens if the device passcode is removed (auto-unlocks, token exposed). Intentional ("don't trap"), but warn in copy or keep locked. - E5
TokenStore.swift:10-22—save()ignoresSecItemAddstatus; a failed write looks like "unpaired" with no error.
- F1 Live Activity (M2 headline "agent 进度常驻锁屏") — only
startexists; no on-deviceupdate/end(=A3). The pin is a one-shot snapshot, not a living activity. - F2 Widget families — missing
accessoryCircular(the prime lock-screen slot) andsystemLarge; lock-screen coverage is inline+rectangular only. NoAppIntentConfigurationto choose which agent/project to pin (plan's North-Star pins a specific agent). - F3 PTY live monitoring — backend
/api/agents/pty/<id>/streamSSE unused; output is manual pull only (=B8). - F4 ntfy push — server-supported, never exposed in the client; APNs-only path silently no-ops without an Apple key (=B17).
- F5 Chat fidelity — no markdown/code rendering (=A13), no tool-call surfacing (=B15), no history pagination (
/api/historyexists, unused). - F6 Settings "Sign in with Apple (coming soon)" (
SettingsView.swift:91-94) is a hard-disabled dead button despite the full exchange path being implemented (AppState.connectCloudWithApple). Wire it or remove. - F7 Connection-mode switch is cosmetic (
AppState.swift:31"UX-only for now") — switching Mac↔Cloud changes nothing about scheme/port/config (=B9). - F8 Onboarding —
OnboardingScan.swiftnamed in the plan doesn't exist (scan inlined); install-screen "Help link" escape hatch absent. - F9
adoptedSessionIddecoded but never shown (Models.swift:25);DispatchView.startedAtdecoded but never shown. Minor informational gaps. - F10 Release entitlement —
aps-environment: development(project.yml:61); a TestFlight/App Store build needsproduction(the whole push + Live Activity path is dev-only today). (testflight.sh flips this for the archive — verify it covers the widget too.)
Endpoint/shape parity is otherwise excellent — ~30 endpoints + their JSON match
the server (verified). Tolerant Codable (lastMtime ISO-or-epoch, optional-heavy).
SSE reconnect-with-backoff + foreground resync (RosterView, Chat mood). Token in
Keychain only (never UserDefaults). Device-revoke correctly gated to localhost
(client never offers remote revoke). APNs subscription keyed separately from ntfy
(won't clobber). @MainActor discipline is clean (no data races found). Proactive
toggle has optimistic-rollback + availability gating. verifyConnection 401/404
mapping is correct. Apple token-exchange path fully wired (404/401/403 handled).
serveCommand correctly arms LISA_WEB_TOKEN.
- A1 (
fire()throws on non-2xx) — one change unlocks honest feedback for ~10 actions. - A2 + A12 + A13 — make Chat usable (error events, auto-scroll, markdown).
- A4 — Soul Values/Opinions (
SoulItemfields). - A6/E1 — lock on
.inactive(token leak). - A7 —
mailpref on the wire. - A3 + A5 — local Live Activity lifecycle + tab-independent widget snapshot.
- A8/A9 — gate Dispatch controls by policy + terminal state.
- A10/A11 — scanner re-arm + scan-sheet race.
- Accessibility pass (§D) — labels + tap targets, before submission.
Part 1 catalogs bugs. Part 2 is the design layer: navigation/IA, visual system, motion, feedback, state design, density — and a phased plan to make the app feel coherent and smooth. (Code-grounded; a screenshot-based visual-QA pass on a paired device is itself plan item P5.4 below.)
- H1 — 5 tabs, but two are low-frequency.
App.swift:19-30: Dispatch · Chat · Reve · Sense · Settings. Sense (privacy consent, revoke-only) and Reve ("while you were away" recap) are occasional surfaces sitting in prime bottom-bar real estate. Consensus iOS pattern: keep ≤3–4 primary tabs; demote the rest. Proposal: collapse to Chat · Dispatch · Lisa · Settings, where "Lisa" is a home/overview (mood, current desire, Reve recap, Soul/Memory peek) and Sense moves into Settings (it's a consent control). This also gives the app the home/overview the web UI has (Dashboard) and the iOS app lacks. - H2 — lands on Chat, but there's no glanceable "home". After pairing
(
AppState.finishOnboarding→ tab 1) the user is dropped into a bare chat. A home/overview that answers "what is Lisa doing / how is she" at a glance would orient better and showcase the product's soul (mood portrait, desire, recap). - H3 — Inspect (Soul/Memory/Skills/Tools) is buried in Settings.
SettingsViewhosts the most interesting content (who Lisa is) under a utilitarian tab. Surface it in the proposed home/overview. - H4 — tab labels/icons. "Reve" is an opaque label (French; the feature brand)
with
moon.stars; "Sense" with a radiowave sensor icon is also non-obvious. If kept as tabs, pair with clearer affordances; if demoted (H1), moot.
- I1 — Theme is good but not shared with the widget target.
Theme.swiftis centralized and clean, butproject.yml:71excludesSources/from the widget, so widgets hardcode.yellow/.blue/...(§B23) — status colors visibly mismatch the app. Fix: move the status palette + a few tokens intoShared/. - I2 — background helpers used inconsistently. Most lists use
consoleBackground()(Theme.swift:63);ChatView:86usesTheme.bgDeepdirectly. Cards/sections styling varies (onboarding cards vs SettingsFormvs roster rows). Define one card/section style and apply it everywhere. - I3 — no spacing/type scale. Padding values are ad-hoc (8/12/14/18/22/24/28/32
sprinkled across views); font sizes mix semantic (
.body,.caption) with fixed (.system(size: 72/52/17/11)). A small spacing scale (Theme.space.s/m/l) and a type ramp would remove visual jitter between screens and harden Dynamic Type. - I4 — monospace content handled three ways. Horizontal scroll
(
DispatchDetailView), clip-in-form (PTY output, recap, memory), and plain wrap. Pick one "code/log block" component (mono + selectable + horizontal scroll + copy) and reuse. - I5 — dark-only, contrast risks. The app forces
.dark(fine, on-brand), but some tokens are low-contrast (e.g.Theme.tertiarycaptions; widget.yellowon white Lock Screen). Audit contrast ratios for WCAG AA on the key labels.
- J1 — feedback is buried, not surfaced. Across Settings/Roster, action results
render as a shared
statusstring at the bottom of a form, often off-screen (§B21, §C9). There is no toast/snackbar and no consistent haptics (only onboarding usesHaptics). Fix: a lightweight top/bottom toast + success/ error haptics on every mutating action — the single biggest "feels responsive" upgrade once §A1 surfaces real outcomes. - J2 — inconsistent loading/empty/error states.
DispatchLedgershows a spinner before first load;Rosterflashes "No agents" (§B7);Reveswallows errors (§B12);Inspecthas no retry (§B16). Fix: oneAsyncStateView(loading → content → empty → error+retry) used by every data screen. - J3 — no list/content animation. The roster upserts rows with no
insert/remove/move animation (§B6) — agents pop in/out abruptly. Add
.animation/transitions keyed on the session id set. - J4 — no skeletons; abrupt content swaps. First loads jump from blank → full. Lightweight skeleton/shimmer placeholders (roster rows, chat, inspect) smooth perceived performance.
- J5 — transitions. Onboarding uses a tasteful
easeInOut(0.2); the rest is default. Tab/state changes and modal presents could use subtle, consistent motion. Keep it restrained (this is a utility app, not a toy).
- K1 — Chat is the #1 visual+interaction upgrade. Today a single concatenated
Text(§A13). Redesign: message bubbles (user right / Lisa left), **markdown- code blocks** (mono, bg, copy), auto-scroll (§A12), a typing indicator
- inline tool chips ("· running Bash") (§B15), and the mood portrait as a small persistent header. This alone transforms the app's feel.
- K2 — Roster rows are information-dense but flat.
RosterRowpacks project / agent / pill / subtitle / counts / shield; good data, weak hierarchy. Strengthen the primary line (agent + state color), demote metadata, give the "needs you" shield a clear treatment (it's the call-to-action). - K3 — Onboarding is the most polished area (cards, dots, haptics) but: Welcome/ Connect don't scroll under large type (§C1), the scan step has no scrim (§C5), the pair step isn't method-aware (§B4), dots back-fill on the shortcut (§C4).
- K4 — Settings is a dense
Formwith unclear labels ("Reve notes" →idle, §B11), buried confirmations (§C9), and a dead "Sign in with Apple (coming soon)" button (§F6). Tighten IA, fix labels, wire or cut the dead button. - K5 — Glance surfaces (§B22–B26, §C10–C13): expanded Island drops turns,
hardcoded colors, absolute time, missing
accessoryCircular, no staleness dim.
Each phase is independently shippable; ordered by value-per-effort. (Phase 0 = Part 1's correctness fixes, which several design items depend on.)
- P0 — Correctness (Part 1).
fire()non-2xx (A1) · chat error events (A2) · Soul fields (A4) · lock on.inactive(A6) · mail pref (A7) · Live Activity local lifecycle (A3) · widget snapshot off-tab (A5) · Dispatch control gating (A8/A9) · scanner re-arm + scan race (A10/A11). - P1 — Interaction smoothness.
AsyncStateView(loading/empty/error+retry) everywhere (J2) · toast + haptics feedback system (J1) · chat auto-scroll (A12) · roster list animations (J3) · short request timeouts (B19). - P2 — Chat redesign (K1). Bubbles · markdown/code · tool chips · typing
indicator · mood header · history pagination (
/api/history). - P3 — Visual consistency & polish. Share status palette to
Shared/(I1) · one card/section style (I2) · spacing scale + type ramp (I3) · one code/log block (I4) · onboarding scroll/scrim/method-aware (K3) · widget polish: relative time, staleness,accessoryCircular, Theme colors, expanded turns (K5). - P4 — IA / navigation. Collapse to 4 tabs + a home/overview (H1/H2) · surface Inspect (H3) · clearer labels (H4) · Settings IA + wire/cut Apple button (K4).
- P5 — Accessibility & QA. VoiceOver labels + values (§D) · ≥44pt tap targets · Dynamic Type audit (I3) · contrast audit (I5) · P5.4: screenshot-based visual QA on a paired device across tabs/states (the populated screens this static review couldn't observe).
Effort (rough): P0 ≈ 2–3 days · P1 ≈ 2 days · P2 ≈ 2–3 days · P3 ≈ 2–3 days · P4 ≈ 2 days · P5 ≈ 2 days. Total ≈ 2–3 weeks of focused work; P0+P1+P5 (≈1 week) is the realistic "ready for a credible TestFlight/submission" bar.
Two independent positions, each grounded in Parts 1–2.
- The UI is the product claim. The pitch is a "self-evolving AI with a soul," yet the soul (Soul/Memory) is "buried in Settings" (H3) and first run drops into a bare chat with no home (H2). Shipping that under-delivers on the central promise.
- Chat is the core loop and it's broken for a coding client (A13): code blocks are unreadable, no auto-scroll (A12), no tool feedback (B15). Plain is forgivable; illegible code on every turn is "this can't do its one job."
- Foundations are cheap now, expensive later: one
AsyncStateView(J2), one toast/haptics (J1), shared palette (I1), spacing/type ramp (I3) are horizontal primitives — 1 edit now vs N call-sites after the code grows and users anchor on the 5-tab model (H1). - A11y + unpolished states are App-Review + first-impression risk (D1–D5, B7, B12, B16). The plan itself gates a11y "before submission."
- Concedes: P0 is strictly first; for a closed TestFlight, P0+P1+P5 is a fair minimum; P4 (re-tab) shouldn't precede usage data.
- ~0 users, no signal. P0 fixes things "broken regardless of who shows up" (A1, A2, A4, A6, A7, A8–A11, F10 + a cheap a11y pass) — that's the credible bar, ~1 week per the doc's own estimate. P2–P4 are bets on guesses about nonexistent users.
- The IA re-tab (P4) is the riskiest, least-justified work. Its whole basis — "Sense and Reve are low-frequency" (H1) — is an assertion with no usage data. Re-tabbing around an untested guess introduces nav bugs, forces relearning, and gets thrown away if testers actually live in Reve/Sense. Textbook premature architecture.
- TestFlight is the signal mechanism. Ship functional, watch which tabs get used and what testers complain about, then invest. Each phase is "independently shippable," so deferring costs nothing structurally.
- Concedes: the genuine must-dos (A1 silent-failure, A6 token leak, A2 hung chat,
A4 Soul "—", scanner freeze,
aps-environmentF10) are non-negotiable; and a thin chat fix + a thin feedback path are worth pulling forward.
The two sides converge on the floor and disagree only on Chat's launch-tier and on P3/P4 timing. Synthesis:
🚩 Launch gate (do now — ~1–1.5 weeks):
- P0 correctness — A1 (
fire()non-2xx) · A2 (chat error events) · A4 (Soul fields) · A6 (lock on.inactive) · A7 (mail pref) · A8/A9 (Dispatch gating) · A10/A11 (scanner) · A3/A5 (Live Activity local lifecycle + off-tab snapshot) · F10 (aps-environmentproduction — verify the archive). - Thin chat — A2 + A12 auto-scroll + render replies via
Text(.init(markdown:))(gets bold/links/inline code for ~nothing). Defer the full K1 redesign. - Thin feedback — a toast + success/error haptics on mutating actions (so
A1's now-surfaced failures land visibly). Defer the full
AsyncStateViewrollout, but adopt it for the 2–3 screens touched anyway. - Minimal a11y — VoiceOver labels + values, color+label status, ≥44pt targets (D1–D5).
- One cheap P3 pull-forward — move the status palette to
Shared/(fixes the widget/app color mismatch, I1/B23) since it's a small horizontal change.
⏳ Fast-follow (after TestFlight signal):
- P2 full Chat redesign (bubbles, tool chips, typing indicator, history).
- P3 visual-system overhaul (spacing scale, type ramp, one card/log component).
- P4 IA / re-tab + home/overview — gated on actual usage data, not H1's assumption. (反方's strongest point; 正方 concedes.)
Always: P5.4 on-device screenshot QA once a paired build exists.
Rationale: 反方 wins on P4 (don't re-architect IA pre-signal) and on not gold-plating P3; 正方 wins on Chat (a coding client that can't render code is the product not working) and on a11y/feedback being correctness-of-experience, not taste — so the launch gate pulls a thin slice of P1/P2 forward and defers the heavy/ speculative phases to data.
Execution against the verdict's launch gate. ✅ Launch gate complete — 9 PRs merged (#183–#191). The deferred fast-follow (P2 full Chat redesign, P3 visual overhaul, P4 IA re-tab) stays held pending TestFlight usage signal, per the verdict.
| PR | Items |
|---|---|
| #183 | A1 silent action failures · A7 mail pref · A4 Soul fields |
| #184 | A2 chat error events · A12 auto-scroll · markdown (thin Chat) |
| #185 | A6 app-switcher token-leak privacy cover |
| #186 | A10/A11 scanner re-arm + scan-sheet race |
| #187 | A8/A9 Dispatch control gating (policy + terminal state) |
| #188 | A3 Live Activity local lifecycle · A5 off-tab widget snapshot |
| #189 | I1/B23 shared status palette · B22/B24/B25/C10 widget polish |
| #190 | J1 toast + haptics feedback · B13 Sense revoke failure |
| #191 | D1–D5 minimal accessibility pass |
Each verified with xcodegen + xcodebuild test (build + 25 tests green).
ActivityKit / App-Group / VoiceOver behaviors are only fully exercisable on a
signed device build — covered by P5.4 (on-device screenshot QA) once a paired
TestFlight build exists.