Skip to content

feat: live in-flight run progress notification (A1) - #111

Merged
adebnar merged 9 commits into
devfrom
feature/live-run-progress
Jul 20, 2026
Merged

feat: live in-flight run progress notification (A1)#111
adebnar merged 9 commits into
devfrom
feature/live-run-progress

Conversation

@adebnar

@adebnar adebnar commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Roadmap item A1 from docs/ideas/2026-07-16-competitive-refresh.md (P0).

Until now the app notified only when a run finished and showed nothing during the minutes a run was executing — the only in-flight signal was a spinner inside an open chat. This adds a live ongoing notification so an agent run is glanceable while the app is backgrounded.

What it does

  • Indeterminate by defaultacme · agent running / Calling tool: web_search.
  • Determinate when available — upgrades to an N/M bar whenever the agent's todo tool reports counts (the same signal the desktop client uses for its Tasks chip).
  • API 36+ uses the platform Notification.ProgressStyle so the system can promote it to a status-bar Live Update; API 26–35 falls back to NotificationCompat.setProgress.
  • New run_progress channel at IMPORTANCE_LOW, separate from the MIN service channel (which stays untouched) — MIN would have suppressed the status-bar presence this feature exists to provide.
  • Toggleable in Settings → Notifications, defaulting on.

Scope note

The roadmap proposed a true step 3/5. That is not achievable client-only: the only real iteration counter (api_call_count/max_iterations) is wired into gateway/run.py and the ACP adapter, never into tui_gateway/server.py, which is the path this app's WebSocket uses. The todo list is the one client-reachable proxy. A generic determinate bar would need a gateway change — deliberately out of scope here.

Architecture

Gateway events → a pure reducer (RunProgress.reduce) → a pure mapper (toSpec) → HermesNotifier, driven from GatewayConnectionService, which already owned the single event collector. Run state deliberately does not live in ChatUiState — that is scoped to an open chat and dies when backgrounded, which is exactly when this must survive. Keeping the reducer and mapper pure means all decision logic is unit-testable without Android (this repo has no Robolectric).

Also wires up session.info.running, which the client never parsed before, as the authoritative busy/idle backstop — message.complete alone misses interrupted and compacted turns, which would otherwise strand a permanent "running" notification.

Review findings fixed along the way

  • Tenant isolation: the notification's tenant was read live at post time while its route was latched at message.start. A mid-run ProfileManager.switchTo() — which SessionsViewModel.prepareOpen triggers when opening a session from the cross-profile list — re-posted an acme run as globex · agent running with globex's accent, still routed to acme's session. Tenant is now latched into the run itself, so name, accent, and route come from one snapshot.
  • Stranded notification: scope.cancel() is cooperative, so an in-flight collector iteration could re-post after onDestroy()'s cancel, leaving an unswipeable ongoing notification. The cancel now hangs off job.invokeOnCompletion, firing only once every child has terminated.
  • session.info{running:true} with a differing sessionId now starts a fresh run instead of keeping the stale one; the reserved-id guard now covers 1003 as well as 1001.

Verification

351 unit tests green (30 new across the reducer and mapper), compileDebugKotlin and assembleBeta green, gitleaks clean. On device: foreground service and the run_progress channel confirmed on the emulator; the full end-to-end live-run observation was not performed (no gateway pairing credentials available) and is recorded as skipped rather than assumed.

Known follow-ups (non-blocking)

  • The notification accent uses the hashed default hue rather than a user's custom per-profile override — the override is a Compose CompositionLocal and the notifier has no composition, so honouring it needs a repository-backed read.
  • After a headless START_STICKY restart with no UI, ProfileManager.active is null, so the title degrades to a generic "Agent running" (never a wrong tenant).

adebnar added 9 commits July 20, 2026 11:18
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.
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.
@gemini-code-assist

Copy link
Copy Markdown

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@adebnar
adebnar merged commit 60df8e5 into dev Jul 20, 2026
3 checks passed
@adebnar
adebnar deleted the feature/live-run-progress branch July 20, 2026 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant