Conversation
* docs: spec for live in-flight run progress (A1) * docs: implementation plan for live in-flight run progress * feat: pure run-progress reducer over gateway run events * feat: map run progress to a platform-independent notification spec * feat: persist and expose the live run-progress preference * feat: render the live run-progress notification with API 36 progress style * feat: drive the live run-progress notification from the gateway event stream * Fix stranded run-progress notification race and split profile read onDestroy() cancelled the collector scope then immediately called notifier.cancelRunProgress(), but scope.cancel() is cooperative and does not preempt a collector iteration already executing synchronously. An in-flight iteration could still call notifier.postRunProgress(...) after the cancel, with no ordering guarantee, permanently stranding an ongoing notification. Hang the final cancel off the Job's actual completion (invokeOnCompletion) instead of off the cancel() call, so it fires only after every child coroutine has genuinely finished. Also collapse the two separate profiles.active.value reads in updateRunProgress into one, so a profile switch landing between them can no longer split a notification's title/route from its accent colour across two different tenants. * Latch tenant into run progress to fix accent/route drift A run's tenant is now latched into RunProgress at message.start / session.info time instead of being re-read from ProfileManager.active at notification-post time. Opening a cross-profile session (SessionsViewModel.prepareOpen) switches the active profile without resetting run state, so the two could drift and post a notification with one tenant's accent/title while its route still pointed at another tenant's session. Also: a differing sessionId on a running session.info now starts a fresh run instead of silently keeping the stale one, and the second reserved notification id (1003, run-progress) is now guarded against id collisions the same way the first (1001) already was.
There was a problem hiding this comment.
Code Review
This pull request implements a live, in-flight run progress notification feature that tracks agent runs using gateway WebSocket events. It introduces a pure RunProgress reducer, maps state to a platform-independent RunProgressSpec, adds a settings toggle to persist user preferences, and renders notifications using API 36+ ProgressStyle with a backward-compatible fallback. The feedback highlights critical improvements: ensuring ProfileManager is refreshed during cold starts to avoid uninitialized state, isolating API 36+ classes in a nested object to prevent class verification errors on older Android versions, and defensively filtering out non-object elements in the todos array to ensure accurate progress counts.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
- todoCounts(): skip non-object todos array elements entirely instead of letting a bare string/number/null inflate the total denominator - HermesNotifier: isolate the API-36 ProgressStyle construction in its own class so no API-36 type is named in HermesNotifier's own method bodies (defensive class-verification hardening, no behaviour change) - GatewayConnectionService.onCreate(): seed ProfileManager's active profile on a headless START_STICKY restart so the run-progress notification title still names the tenant
Kotlin's ::class.java compiles to a reflection call rather than a type literal, so Intent(Context, Class) is not recognised as setting a component and the resulting PendingIntents read as implicit. Use setClassName() with the class's own name instead: identical at runtime, rename-safe, and unambiguously explicit. Matters most for the direct-reply intent, which must be FLAG_MUTABLE — mutable plus implicit is the genuinely exploitable combination.
Static analysis constant-folds a flag literal at the call site but not a value returned from a helper, so FLAG_IMMUTABLE was unprovable and the PendingIntents read as mutable. Drops the pendingFlags() helper in favour of the literal; no runtime change.
Mutations inside a Kotlin apply { } block are made on the lambda receiver,
which static analysis does not reliably attribute back to the variable, so
the component set there was invisible and the intents read as implicit.
Assign to the variable directly instead. No runtime change.
Promotes
devto production as v0.1.52.Beta soak: v0.1.52-beta.1.
What ships
A1 — live in-flight run progress (#111). The app previously notified only when a run finished and showed nothing during the minutes one was executing. It now posts a live ongoing notification: indeterminate by default (
acme · agent running/Calling tool: web_search), upgrading to a determinateN/Mbar whenever the agent'stodotool reports counts. On API 36+ it uses the platformNotification.ProgressStyleso the system can promote it to a status-bar Live Update; API 26–35 falls back toNotificationCompat.setProgress. Newrun_progresschannel atIMPORTANCE_LOW, separate from the MINservicechannel. Toggleable in Settings → Notifications, on by default.Also wires up
session.info.running— which the client never parsed — as the authoritative busy/idle backstop, sincemessage.completealone misses interrupted and compacted turns.Notable fixes made during review
scope.cancel()is cooperative, so an in-flight event could re-post afteronDestroy()'s cancel, leaving an unswipeable ongoing notification. The cancel now hangs offjob.invokeOnCompletion.Verification
351 unit tests green,
assembleBetagreen, gitleaks clean, 0 open HIGH/CRITICAL Dependabot alerts.versionCode 56/versionName 0.1.52.Known non-blocking follow-ups: the notification accent uses the default hashed hue rather than a custom per-profile override; after a headless
START_STICKYrestart the title degrades to a generic "Agent running" (never a wrong tenant).