fix(app): capture problems speak on the live page and the Activity bell, never on Home

Merged
GI
10d ago
+8923-1124

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. CaptureRecoveryBanner is 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. visiblePrompt walked 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/main merge, 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-attention captured the before/after renders below. These are headless widget captures with synthetic local state, not a device run.
  • flutter analyze on the touched files: no new issues. The one existing info, at conversation_capturing/page.dart:530, predates this PR.
  • python3 .github/scripts/check_mobile_ux_contract.py --report on 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 path FC-capture-recovery-dead-end names) 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 stalledHome beforeHome after
Live page, link stalledLive beforeLive after
Activity → Needs attentionActivity beforeActivity after

Scoped cleanups

  • Drops an unused package:omi/ui/ui.dart import from app/test/widgets/home_ask_anything_button_test.dart. Since #21393, origin/main fails 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