Skip to content

feat(mobile): active-now tray live sync, overflow expander, and screen-reader labels#4644

Merged
iscekic merged 11 commits into
mainfrom
feat/active-now-tray-followups
Jul 21, 2026
Merged

feat(mobile): active-now tray live sync, overflow expander, and screen-reader labels#4644
iscekic merged 11 commits into
mainfrom
feat/active-now-tray-followups

Conversation

@iscekic

@iscekic iscekic commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Implements the three approved follow-ups from #4626 (stacked on feat/active-now-pinned-section; retarget to main when #4626 merges).

1. Live-sync the Active-now tray over the user web socket

  • session-ingest: UserConnectionDO now broadcasts sessions.heartbeat to every one of the user's web sockets (removed sessions are detectable by absence, so the subscriber special-case is gone) and the aggregate snapshot is owner-unique, so a session transferring between CLI connections never appears twice.
  • cloud-agent-sdk: UserWebConnection exposes a connection-state API (isConnected() / onConnectionChange()), with wrapper-owned state so full-release/destroy transitions are reported correctly.
  • Mobile: a single app-level ActiveSessionsLiveSync owner retains the shared WS and applies heartbeats/snapshots/CLI-connect/disconnect events to the activeSessions.list cache through a serialized cancel-then-write pipeline (a stalled tRPC refetch can never clobber newer WS state). Explicit refreshes (reconnect, cli-connected, cli-disconnected, enrichment) are coalesced with durable pending reasons; enrichment fields (createdOnPlatform/createdAt/updatedAt) are preserved across WS payloads, and enrichment refetches are rate-limited to one per 10s regardless of outcome. All work is fenced per attachment epoch, so nothing publishes after unmount/remount. While the WS is connected the 10s tray poll is disabled; it resumes automatically when the socket drops.

2. Cap the tray with a +N more expander

Collapsed tray shows at most 3 rows (ACTIVE_NOW_TRAY_CAP); more pinned sessions render a +N more control that expands to all rows (Show less to collapse). The expander disappears entirely at ≤3 sessions, and reduced-motion disables the Reanimated layout/fade transitions while keeping the behavior.

3. Screen-reader labels for session rows

Both stored and remote rows now expose "<title>[, needs input], <badge>[, <spoken meta>]" — the visible eyebrow content in render order. Relative times are spoken as words (28 minutes ago, never 28M AGO letter-forms); the needs-input variant omits meta exactly as the visible row does. The expander is labeled N more active sessions / Show fewer active sessions.

Verification

  • Unit: 1183 mobile vitest tests (incl. new suites for the merge helpers, live-sync pipeline/races/reconnect/enrichment, tray window math, and a11y label composition), 487 session-ingest, 986 cloud-agent-sdk jest tests. format, typecheck (tsgo), lint (oxlint), check:unused green in every changed package.
  • Independent review loop: three fresh review rounds; findings (detach fencing, enrichment retry rate-limit on failure, cli.connected payload validation, epoch-based fence for remount races) fixed in dedicated commits; final round returned no findings.
  • Device E2E (iPhone 17 Pro sim, local stack, fake CLI WS client driving real heartbeats): live add/rename/remove/empty tray updates within ~2s of the heartbeat without any manual refresh or poll; enrichment preserved after updates; expander verified collapsed/expanded/shrink-while-expanded (3 rows + +2 MORE at 5 sessions); accessibility labels verified via the view hierarchy for tray rows, history rows, and the expander.
  • Not exercised on device (documented, covered by unit tests): WS-down 10s poll fallback and needs-input rows — both would have required disturbing shared services to reproduce.

@iscekic iscekic self-assigned this Jul 20, 2026
Comment thread apps/web/src/lib/cloud-agent-sdk/user-web-connection.ts
Comment thread apps/mobile/src/lib/active-sessions-live-sync.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The only change since the last review is a whitespace/formatting adjustment in apps/web/src/lib/cloud-agent-sdk/user-web-connection.test.ts (re-wrapped lines, no logic change); no new issues were found.

Files Reviewed (1 file)
  • apps/web/src/lib/cloud-agent-sdk/user-web-connection.test.ts
Previous Review Summaries (3 snapshots, latest commit f64bb06)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit f64bb06)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Both previously flagged WARNINGs (missing destroyed guard in onConnectionChange, and maybeScheduleEnrichmentRefresh stalling for non-enrichment fetch paths) are fixed correctly in this incremental diff; no new issues were found in the changed code.

Files Reviewed (4 files)
  • apps/mobile/src/lib/active-sessions-live-sync.ts - previous issue fixed
  • apps/mobile/src/lib/active-sessions-live-sync.pending.test.ts
  • apps/web/src/lib/cloud-agent-sdk/user-web-connection.ts - previous issue fixed
  • apps/web/src/lib/cloud-agent-sdk/user-web-connection.test.ts

Previous review (commit 737dd94)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Both previously flagged WARNINGs (missing destroyed guard in onConnectionChange, and maybeScheduleEnrichmentRefresh stalling for non-enrichment fetch paths) are fixed correctly in this incremental diff; no new issues were found in the changed code.

Files Reviewed (4 files)
  • apps/mobile/src/lib/active-sessions-live-sync.ts - previous issue fixed
  • apps/mobile/src/lib/active-sessions-live-sync.pending.test.ts
  • apps/web/src/lib/cloud-agent-sdk/user-web-connection.ts - previous issue fixed
  • apps/web/src/lib/cloud-agent-sdk/user-web-connection.test.ts

Previous review (commit bab70a8)

Status: 2 Issues Found | Recommendation: Address before merge

Executive Summary

The highest-risk issue is a functional gap in apps/mobile/src/lib/active-sessions-live-sync.ts where enrichment refresh can silently stall for sessions surfaced by non-enrichment fetches (cli-connected/cli-disconnected/reconnect).

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/cloud-agent-sdk/user-web-connection.ts 600 onConnectionChange lacks the destroyed guard used by sibling subscription methods, so its "safe on a destroyed connection" contract isn't enforced by code
apps/mobile/src/lib/active-sessions-live-sync.ts 387 Enrichment refresh (maybeScheduleEnrichmentRefresh) is only triggered from the WS write path, not after successful cli-connected/cli-disconnected/reconnect fetches, so newly-unenriched sessions from those paths can stall until an unrelated WS payload arrives
Files Reviewed (30 files)
  • apps/mobile/src/app/(app)/_layout.tsx
  • apps/mobile/src/components/agents/active-now-section.test.ts
  • apps/mobile/src/components/agents/active-now-section.tsx
  • apps/mobile/src/components/agents/active-now-window.ts
  • apps/mobile/src/components/agents/mobile-session-manager.test.ts
  • apps/mobile/src/components/agents/session-row-accessibility-label.test.ts
  • apps/mobile/src/components/agents/session-row-accessibility-label.ts
  • apps/mobile/src/components/agents/session-row.tsx
  • apps/mobile/src/lib/active-sessions-live-sync-mount.tsx
  • apps/mobile/src/lib/active-sessions-live-sync.departure.test.ts
  • apps/mobile/src/lib/active-sessions-live-sync.enrichment.test.ts
  • apps/mobile/src/lib/active-sessions-live-sync.pending.test.ts
  • apps/mobile/src/lib/active-sessions-live-sync.race.test.ts
  • apps/mobile/src/lib/active-sessions-live-sync.reconnect.test.ts
  • apps/mobile/src/lib/active-sessions-live-sync.test-helpers.ts
  • apps/mobile/src/lib/active-sessions-live-sync.test.ts
  • apps/mobile/src/lib/active-sessions-live-sync.ts - 1 issue
  • apps/mobile/src/lib/active-sessions-live.merge.test.ts
  • apps/mobile/src/lib/active-sessions-live.test.ts
  • apps/mobile/src/lib/active-sessions-live.ts
  • apps/mobile/src/lib/hooks/use-agent-sessions.ts
  • apps/mobile/src/lib/hooks/use-user-web-connection-state.ts
  • apps/mobile/vitest.config.ts
  • apps/web/src/lib/cloud-agent-sdk/session-routing.test.ts
  • apps/web/src/lib/cloud-agent-sdk/session-transport.test.ts
  • apps/web/src/lib/cloud-agent-sdk/session.test.ts
  • apps/web/src/lib/cloud-agent-sdk/user-web-connection.test.ts
  • apps/web/src/lib/cloud-agent-sdk/user-web-connection.ts - 1 issue
  • services/session-ingest/src/dos/UserConnectionDO.test.ts
  • services/session-ingest/src/dos/UserConnectionDO.ts

Fix these issues in Kilo Cloud


Reviewed by claude-sonnet-5 · Input: 26 · Output: 4.6K · Cached: 522.5K

Review guidance: REVIEW.md from base branch main

Base automatically changed from feat/active-now-pinned-section to main July 21, 2026 13:13
iscekic added 10 commits July 21, 2026 15:17
…nique

Deliver sessions.heartbeat to every one of the user's web sockets via
broadcastToWeb, mirroring the cli.connected/cli.disconnected pattern,
and drop the subscriber-set special-case (a removed session id is
detectable from its absence in the payload).

Make aggregateSessions emit each session only under its current owner
(sessionOwners.get(id) === connectionId), closing a latent duplicate-id
hole after an ownership transfer with both CLIs still connected.
Add isConnected() and onConnectionChange() to UserWebConnection, driven
by base-connection callbacks for live transitions and by the wrapper's
own stopConnection()/destroy() for the final disconnected state (base
destroy() emits no callback). Make retain() a required member of the
type and update the implementers/doubles accordingly.

Covers release-then-retain, no-callback-on-destroy, and a
pong-timeout-triggered reconnect transition.
Add a single app-level ActiveSessionsLiveSync owner mounted once in the
authenticated layout. It retains the shared user web connection and
merges sessions.list / sessions.heartbeat / cli.connected /
cli.disconnected into the activeSessions.list query cache through one
serialized, non-blocking pipeline (cancelQueries + setQueryData). Cache
writes never await a fetch; every write cancels in-flight fetches, so a
late fetch can never overwrite a newer web-socket write.

Refreshes run via fetchQuery({ staleTime: 0 }) with durable per-reason
pending state, a rate-limited (>=10s) self-healing enrichment retry, and
exactly one refresh per disconnect->connect rising edge. The merge
preserves only createdOnPlatform/createdAt/updatedAt for known ids while
taking connectionId and liveness from the latest payload, is id-unique
latest-payload-wins, and drops parentSessionId rows. The list poll is
replaced with refetchInterval that is disabled only while the socket is
connected, via a useSyncExternalStore binding over the SDK connection
state.
Cap the collapsed tray at 3 rows and add an animated, reduced-motion-
aware expander. selectTrayWindow returns the visible rows and hidden
count; collapsed shows the first cap rows with a +N more control,
expanded shows all rows with Show less. The section header keeps the
total pinned count. The expander has a 44pt touch target and a
screen-reader label, and layout transitions are suppressed under
reduced motion while remaining functional.
Add a pure sessionRowAccessibilityLabel composer that mirrors the
visible eyebrow content: title, then needs input when the needs-input
eyebrow renders (meta omitted), then badge, then a speech-friendly meta
that expands abbreviated time-ago units so VoiceOver reads 5 minutes ago
rather than 5M AGO. Apply it to both the stored and remote session rows
so tray and history rows read consistently. No visual changes.
Review follow-ups: detach() now fences queued writes, in-flight fetches,
and trailing re-kicks so a disposed owner can no longer write the shared
cache; enrichment refreshes are rate-limited after failed attempts too;
cli.connected payloads are Zod-validated like every other system event.
A boolean detached flag reopens the fence when the same instance
re-attaches (StrictMode remount): work queued under the previous
attachment could resume and publish stale payloads. Replace it with a
monotonically increasing attachment epoch captured at work creation.
…cheduling

- onConnectionChange now no-ops on a destroyed connection, matching
  subscribeToCliSession, so late listeners cannot leak.
- Enrichment pending-reason reconciliation is extracted into a
  non-kicking helper; processFetchQueue re-evaluates it after a
  successful fetch (so cli-connected/reconnect fetches resolving with
  unenriched rows schedule enrichment) while keeping a single kick
  source and the 10s retry rate-limit even after failed fetches.
@iscekic
iscekic force-pushed the feat/active-now-tray-followups branch from 737dd94 to f64bb06 Compare July 21, 2026 13:20
@iscekic
iscekic merged commit be580e1 into main Jul 21, 2026
20 checks passed
@iscekic
iscekic deleted the feat/active-now-tray-followups branch July 21, 2026 13:57
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.

2 participants