feat(ios): companion follow-ups — tabs, mood portrait, deep-links, Face ID, APNs reg, tests#130
Merged
Merged
Conversation
Settings → "Inspect Lisa" links to four GET-only screens (plan §G6,
Appendix B): Soul (identity / purpose / constitution + mood bars + values /
opinions / desires), Memory (user + memory text), Skills, Tools. A small
AsyncContent loader renders loading / error / content uniformly so each view
stays a thin wrapper over its fetch. Models mirror src/soul/types.ts and
/api/{soul,memory,skills,tools}, lenient (all-optional) so a missing field
never breaks decode.
Verified: ./build.sh -> BUILD SUCCEEDED.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New tab (plan Appendix B): a "while you were away" idle note + current desire (from /api/island/ping), an agent-activity recap over a 2h/8h/24h window (/api/agents/recap), and advisor suggestions (/api/advisor/latest) that can be dismissed — POST /api/advisor/dismiss feeds the server's down-weighting loop. Loader re-runs when the window changes or pairing flips on. Verified: ./build.sh -> BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New tab (plan §G6): per-signal consent state from /api/consent and the last ambient events from /api/sense/recent. Revoke-only by design — tightening consent is safe from anywhere, but granting a sensitive signal remotely would widen the surface, which the privacy floor (§7.2) keeps a Mac action; the footer points grants at the Mac. Revoke / revoke-all reload the list. Verified: ./build.sh -> BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A mood chip above the chat transcript, seeded from /api/island/ping and kept live via the `mood` SSE event. Honest scope: this wires the mood *data* (the unblocked part); it's a stand-in for the mood *portrait* — bundling the mac-client's portrait art is a follow-up (the iOS app has no asset catalog yet). Verified: ./build.sh -> BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings → "Paired devices" lists each per-device token (/api/devices) with platform + last-seen. Read-only on the phone: revoking is loopback-only on the server (a Mac-owner action), so the footer points there rather than offering a button that would 403. Verified: ./build.sh -> BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Dispatch tab's toolbar opens Lisa's own fire-and-forget dispatches (the ledger, distinct from the observed-agent roster): pid / task / alive from /api/dispatch/list, and a captured log tail per entry from /api/dispatch/status. The status endpoint is policy-gated, so a 403/404 just leaves the tail empty. Verified: ./build.sh -> BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The roster stream now reconnects on drop with exponential backoff (1→30s cap), doing a full /api/agents/sessions resync before each retry so transitions during the gap aren't lost; healthy traffic resets the backoff. On return to foreground (scenePhase → .active) it resyncs and reconnects, since iOS suspends SSE in the background. Previously a dropped stream just went silent until the next appear. Verified: ./build.sh -> BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AgentCountWidget now also supports the lock-screen accessory families (.accessoryInline "▶ N active · ⏸ M stuck", .accessoryRectangular) alongside systemSmall/Medium, and the whole widget is a tap target via .widgetURL(lisapocket://roster). Registers the `lisapocket://` URL scheme (CFBundleURLTypes) — the app's deep-link handler (next commit) routes it; the same scheme backs the ntfy push Click URL. Verified: ./build.sh -> BUILD SUCCEEDED; CFBundleURLTypes present in the built Info.plist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app now handles its URL scheme (.onOpenURL): lisapocket://roster selects the Dispatch tab (the Widget's tap target); lisapocket://session?agent=&id= (the ntfy push Click) selects Dispatch and opens that session. RosterView drives a NavigationStack path and resolves the pending session once it's in the roster, tolerating either arrival order (link before or after the roster loads). Verified: ./build.sh -> BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The device token grants full control of the Mac's agents, so Settings → Security can require biometric unlock (LocalAuthentication, .deviceOwnerAuth → Face ID/Touch ID with passcode fallback). When armed + paired, the app locks at launch and re-locks on backgrounding; a full-screen LockView auto-prompts and offers a manual retry. If no auth is enrolled, it doesn't trap the user. Adds NSFaceIDUsageDescription. Verified: ./build.sh -> BUILD SUCCEEDED; NSFaceIDUsageDescription in the built Info.plist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a hosted XCTest target (LisaPocketTests) covering the pure logic, and extracts that logic so it's testable in isolation: - rosterCounts(_:at:) — the Widget snapshot bucketing (pulled out of RosterModel.publishSnapshot). - AppState.parseDeepLink(_:) -> DeepLinkRoute — the lisapocket:// parse (pulled out of handleDeepLink), nonisolated so it runs off the main actor. Tests also cover sortRows ranking. build.sh gains a `test` action (`./build.sh test`) and project.yml a LisaPocket scheme wiring the test target. Verified: ./build.sh test -> Executed 8 tests, 0 failures (TEST SUCCEEDED) on the iOS 17 simulator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reve / Sense tabs, mood indicator, Inspect Lisa, paired devices, Face ID lock, dispatch ledger, SSE auto-reconnect, widget accessory families + deep-links, and `./build.sh test`. Notes the remaining follow-ups (APNs Live-Activity refresh; real mood portrait art). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upgrades the mood chip to Lisa's actual portrait — the server's own art at /assets/lisa/<slug>.png (the same set the web island uses), loaded over the existing connection via AsyncImage. A new LisaClient.assetURL carries the token as a query param (AsyncImage can't set headers). No bundling (115 PNGs stay server-side) and it's always in sync; the chip remains the caption + the fallback while loading or if a slug has no art. Turns out the art existed all along (src/web/assets/lisa/), so the earlier "needs an asset catalog" caveat was wrong. Verified: ./build.sh -> BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both tabs now re-fetch when the app returns to foreground (scenePhase → .active), matching the roster — so a backgrounded app shows fresh recap / consent / events on return, without each tab holding its own SSE connection. (They already load on open + pull-to-refresh.) Verified: ./build.sh -> BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings → "Push (APNs)" requests notification authorization and registers for
remote notifications; a small UIApplicationDelegateAdaptor captures the device
token and AppState POSTs it to /api/push/register {kind:"apns"}. Adds the
aps-environment entitlement. This is the client half — delivery needs the Mac's
APNs key (next commit, backend); on the Simulator there's no token, reported
honestly. ntfy push remains the zero-Apple-infra path.
Verified: ./build.sh -> BUILD SUCCEEDED; aps-environment present in the built
entitlements.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AppDelegate becomes the UNUserNotificationCenter delegate: foreground pushes show a banner, and tapping a push routes its `link` custom key (the lisapocket:// deep-link the server sets, mirroring ntfy's Click) through AppState.handleDeepLink — so an APNs tap lands on the right session, same as the widget/ntfy paths. README synced (real mood portrait; ntfy + APNs). Verified: ./build.sh -> BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… lock) - BLOCKER: live roster updates were silently dropped. The agent_session_update SSE sends lastMtime as epoch-ms (number) while the model declared String?, so decode threw typeMismatch and merge() never ran — the roster only updated on reconnect. AgentSession now decodes lastMtime from either a string (REST) or a number (SSE → normalized to ISO). +3 regression decode tests. - Mood SSE now reconnects with backoff (was: stopped permanently after the first drop, leaving the portrait stale). - Roster foreground resync is skipped while the Face ID lock is up (don't fetch behind the gate). - MoodPortrait: percent-encode the slug in the asset path; show ProgressView while loading (was the failure placeholder). Verified: ./build.sh test -> 11 tests, 0 failures (TEST SUCCEEDED). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The next wave of iOS companion work — pushing the "blocked" list as far as it goes without an Apple key or a device. Stacked on #128; pairs with the backend APNs sender in #129. Build-verified at every commit.
What
/assets/lisa/<slug>.png(the set the web island uses) via AsyncImage, with the token in the query (it can't set headers). No bundling — turns out the art existed insrc/web/assets/lisa/, so the earlier "needs an asset catalog" caveat was wrong. Chip stays the caption/fallback.UIApplicationDelegateAdaptorcaptures the token), and POSTs it to/api/push/register {kind:"apns"}. Adds theaps-environmententitlement.linkcustom key (set by the backend, mirroring ntfy's Click) throughhandleDeepLink, so it lands on the right session; foreground pushes show a banner.scenePhase → .active(matching the roster), without holding their own SSE connections.Verification
./build.sh→ BUILD SUCCEEDED after each commit;aps-environmentconfirmed in the built entitlements.Honest limits
APNs delivery still needs a real Apple push key + device (external — see #129 for the sender). Live Live-Activity remote refresh remains a follow-up (also key-gated). The App Group's runtime data still requires a signed build (compile-verified on the Simulator).
🤖 Generated with Claude Code
Note (merge bookkeeping): the original stacked iOS PRs #124 (QR + Widget) and #128 (Reve/Sense/Inspect/mood/devices/deep-links/Face ID/widget polish/tests) were merged/closed when their base branches were deleted during the stack merge. #124's commits are already on
main; #128's commits ride in this PR (this branch was stacked on #128), so retargeting #130 tomainbrings the full iOS follow-up set in one merge. All commits are preserved individually. Pre-merge review fixes are included (thelastMtimeSSE-decode blocker, mood-SSE reconnect, lock-gated resync, slug encoding).