fix(app): capture problems speak on the live page and the Activity bell, never on Home
What changed and why
Capture problems no longer appear on Home. They show up in two places: the live transcription page and the Activity bell. This follows David's review of the design mockups (live page option A, Activity option 1).
- Live transcription page. The amber recovery card is gone. During a pendant link stall, the header reads "Reconnecting… · Pendant" (including while a retry is in flight), and one line under it says "Omi isn't sending audio · Reconnect". Reconnect retries in place and never navigates. Tapping the message opens the details sheet. There is no dismiss: the line goes away on its own when audio returns. A storage episode shows "Phone storage full · Retry sync" and leaves the header live. An exhausted ingress recovery shows its message with no action and no saved-audio claim (INV-CAP-1).
- Home.
CaptureRecoveryBanneris no longer mounted. Instead, the Activity bell's amber dot now also lights for an acute link episode. - Activity → Needs attention. A new row, "Omi isn't sending audio", reconnects in place. Its subtitle "Still recording on your Omi" appears only when the pendant is off the link, not for a connected-but-silent pendant. The exhausted-ingress version of that row opens the explanation instead. The storage and upload rows are now plain settings-style rows with a chevron into Offline Sync. The Manage/Retry buttons and the amber washes are gone, and amber appears only in the row icons. The section header matches the day headers.
- Fix found by the renders.
visiblePromptwalked devices in map order. An open storage episode on the local WAL therefore hid a pendant stall: the Activity row and the bell dot went missing, and Reconnect retried the storage transfer. The acute episode now takes priority.
The widget moves to lib/pages/conversation_capturing/capture_recovery_status.dart. Removed strings: activityAttentionManage, captureBannerDismiss, captureBannerWhatsWrong, captureLinkStalledTranscribesLater. The live header is CaptureWedgeAppBar in the same file. Added string: activityAttentionLinkStalledSubtitle (49 locales via scripts/l10n.py).
Line-Count-Exception: backend/routers/sync.py | 2903 -> 2921 | growth comes from origin/main (d6447116ec, 0721d7f34d) merged into this stacked branch; no line in this PR adds to it
Product invariants affected
- INV-CAP-1: touches
app/lib/services/capture/capture_wedge_monitor.dart. Retention and admission behavior are unchanged. The monitor only exposes which episode is acute, prefers it as the visible prompt, and lights the bell for it. The ingress-failure surfaces still make no saved-audio claim, and tests cover this. - INV-MEM-4: carried by the earlier
origin/mainmerge, not by this PR's own commits.
How it was verified
app/scripts/visual_audit.sh --base 7745645b79 --head WORKTREE --only home-capture-link-stall,conversation-live-recovery-banner,activity-capture-attentioncaptured the before/after renders below. These are headless widget captures with synthetic local state, not a device run.flutter analyzeon the touched files: no new issues. The one existing info, atconversation_capturing/page.dart:530, predates this PR.python3 .github/scripts/check_mobile_ux_contract.py --reporton the three touched UI files: clean.python3 scripts/l10n.py check: ok, 49 locales.
Tests
flutter test test/widgets/capture_recovery_banner_test.dart test/widgets/needs_attention_capture_test.dart test/pages/conversations/ test/pages/activity/ test/widgets/pending_work_test.dart test/services/capture/ test/visual_audit/ plus the capturing-page tests → 678 passed.
capture_recovery_banner_test.dart(the pathFC-capture-recovery-dead-endnames) now covers the status line:- Reconnect retries in place, steps aside while the retry runs, and comes back if no audio returns.
- The message opens the details sheet; there is no dismiss; touch targets are at least 44pt.
- The ingress failure gets no retry.
- Storage gets Retry sync.
- Only acute episodes re-label the header.
- A stall outranks an open storage episode, and Reconnect retries the link rather than the transfer.
needs_attention_capture_test.dart:- The rows have chevrons and no buttons, and tapping the storage row opens Offline Sync once.
- The link row reconnects in place.
- The ingress row explains and makes no saved-audio claim.
- An open storage episode does not hide the stall (regression for the ordering bug).
- The bell dot lights for a stall until audio flows again.
home_no_capture_banner_test.dart: real Home shows nothing for an acute episode or a storage episode, and the bell flag is set for the acute one.
Review
A ZCode (GLM-5.3) review of a860adfad2 found no blockers. Two should-fixes are addressed in 7ca2a5ac39: the header now holds "Reconnecting" through an in-flight retry, and a connected-but-silent pendant makes no still-recording claim. Two nits are also fixed there: the audit now asserts the link row, and the header has widget tests. dismissVisibleEpisode is left in the monitor API, still covered by its tests.
UI evidence
| Before (7745645b79) | After | |
|---|---|---|
| Home, link stalled | ![]() | ![]() |
| Live page, link stalled | ![]() | ![]() |
| Activity → Needs attention | ![]() | ![]() |
Scoped cleanups
- Drops an unused
package:omi/ui/ui.dartimport fromapp/test/widgets/home_ask_anything_button_test.dart. Since #21393,origin/mainfails the analyzer ratchet on it (unused_import: 1 found, baseline 0), so every PR merged with main fails Dart Analyze & Tests.
Failure class (fixes)
Failure-Class: FC-capture-recovery-dead-end
Follow-up: the record's wording still mentions an explicit dismiss and the old banner path in scope_hints. Per the template, that registry change goes in its own registry-only PR.
🤖 Generated with Claude Code





