surface live agent activity in composer status pills - #2646
Draft
tellaho wants to merge 13 commits into
Draft
Conversation
Rebuild of the composer live-activity experiment (old draft PR #1575) on current main primitives. Behind the new off-by-default composerLiveActivity preview feature, the agents-working popover shows one selected working agent's channel-scoped feed at a time: - ComposerLiveActivityFeed reuses ManagedAgentSessionPanel with the compactPreview transcript variant (same primitive as the profile activity embed), so live/archive merging, projection, anchored scrolling, and idle handling stay owned by that surface. - Agent switching via pill tabs (role=tablist) under the preview; selection resolves explicit tab > open session pane > first working agent, and falls through when the selected agent stops working. - Whole preview is one click target with a Last-live recency pill; transcript rows underneath are inert, avoiding nested interactive controls inside a clickable shell. - formatLastLiveLabel extracted from UserProfilePanelTabs into profile/lib/lastLiveLabel so both surfaces share the recency label. With the experiment off, the popover renders the existing agent list unchanged. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…nd AT Wrap the ManagedAgentSessionPanel subtree in a native inert container so compact transcript rows (which can render keyboard-focusable message links) are removed from pointer, tab-order, and assistive-tech interaction — CSS pointer-events-none only neutralized the mouse. The overlay open button and Last-live pill stay accessible. Also extract deriveLastLiveAt and make it archive-aware: the Last-live pill now takes the newest across the live transcript window, archived channel events, and the active-turn anchor, so it can no longer disagree with archived rows visible in the preview. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Dev-only floating pane that simulates up to two synthetic agents working and/or streaming progress in the active channel, so the composer agents-working bar and the composerLiveActivity popover feed can be exercised without real agents or a relay. Verified end to end in the dev app (mock bridge): working toggle lights the composer bar with resolved agent names/initials, liveness keeps the turn alive, progress frames render in the live-activity feed, End all turns clears cleanly. - Add desktop/src/features/agents/debug/debugAgentHarness.ts: module-level store driving the real pipeline seams — turn_started/turn_liveness (10s heartbeat, under the 25s prune bound) into activeAgentTurnsStore, and scripted ACP session/update frames (thought, tool call start/finish, three-chunk streamed assistant message, usage tick) injected as acp_read events into observerRelayStore every 2.5s; fixed synthetic 64-hex pubkeys with distinct avatar initials (Alpha Debug / Beta Debug); also exports the useDebugHarnessRelayAgents roster hook - Add desktop/src/features/agents/debug/AgentActivityDebugPanel.tsx: collapsible bottom-right pane with per-agent Working/Progress switches, one-shot "Emit once", and "End all turns" (emits turn_completed, exercising the terminal-tombstone/resurrection-guard path) - Add desktop/src/features/agents/debug/DebugHarnessMount.tsx: self-gating one-line mount (lazy + import.meta.env.DEV, so no production chunk), keeping the AppShell call site tiny - useChannelActivityTyping.ts: marked block appending the synthetic agents to the relay-agent roster (scoped to the viewed channel so they pass getChannelAgentSessionAgents) and reporting the active channel; placed here rather than ChannelScreen.tsx to stay under that file's pinned 1000-line-guard ceiling - AppShell.tsx: marked import + one-line <DebugHarnessMount /> render - Add desktop/src/features/agents/debug/README.md with removal steps TEMPORARY — HOW TO REMOVE: grep for "DEBUG HARNESS"; delete desktop/src/features/agents/debug/, revert the relayAgentsWithDebug block in useChannelActivityTyping.ts (pass relayAgents straight through again), and drop the DebugHarnessMount import + render in AppShell.tsx. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…gent turns - Extract the composer status strip into ChannelComposerActivityRow.tsx (new component hosting the inline bot-activity trigger and the typing indicator, plus the useChannelWorkingAgentPubkeys subscription). Also brings ChannelPane.tsx back under the 1000-line file-size ceiling (1006 -> 969). - Change the row from min-h-8 to a fixed h-8.5 (34px). The inline bot-activity trigger is h-7 (28px), which plus the row's pb-1.5 (6px) made the active row 34px — 2px taller than the 32px idle minimum — so the bottom-anchored composer visibly bumped up whenever an agent turn or bot typing mounted the trigger, and dropped back when it cleared. 34px matches the previous active-state height exactly: the working state renders identically and the idle state now reserves the same space. (Typing indicators alone are 20px of content, under the old minimum, which is why only the agent-activity trigger caused the shift.) - channels.spec.ts: add "composer does not shift when the activity row mounts and clears" e2e test asserting the composer's y-position stays stable across trigger mount and clear. Verified via negative control: with min-h-8 restored the test fails with exactly the 2px delta. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- BotActivityBar.tsx: replace the combined "agents working" trigger with one pill per working agent (avatar + latest action summary in a rounded-full chip, max-w-30 with truncation so pill widths stay uniform); extract BotActivityAgentPill with per-pill hover popover state (useHoverPopover) and remove the tab strip, tab selection state, and the 5-headline rotation interval - Pill label shows the agent's single latest action headline and decays to a generic "Working…" state once activity goes quiet (15s staleness window, 5s useNow tick); deliberately no rotation between recent actions - Hovering a pill shows the live activity feed as the popover surface itself (w-80 p-0, edge-to-edge, no inset bordered box); clicking the pill opens the agent's full runtime in the auxiliary panel (preventDefault stops Radix's composed trigger toggle from racing the hover/focus open state, which made click-to-open flaky) - composerLiveActivity.ts: add deriveActivityPillLabel pure helper (channel-scoped spine-first scan + staleness decay, ACTIVITY_PILL_STALE_MS = 15s) and remove the now-unused resolveSelectedActivityAgent - composerLiveActivity.test.mjs: replace resolveSelectedActivityAgent tests with 7 deriveActivityPillLabel cases (fresh headline wins, staleness decay, custom window, channel scoping, spine-over-metadata, metadata fallback, empty transcript) - ComposerLiveActivityFeed.tsx: overlay button rounding rounded-lg → rounded-[inherit] so the hover tint follows the popover's rounded-xl clip when the feed renders edge-to-edge - ChannelComposerActivityRow.tsx / ChannelPane.tsx: drop the removed variant prop (the toolbar variant was unused; both call sites rendered inline) - channels.spec.ts: activity-indicator test now hovers the pill for the legacy preview popover and clicks the pill for direct aux-panel open - Flag-off (composerLiveActivity disabled) hover popover keeps the legacy "View activity" item; pills stay h-7 inside the fixed h-8.5 activity row so the composer-shift guard keeps passing Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
- BotActivityBar.tsx: extract AnimatedPillSlot around each agent pill —
reorders animate with a 0.9s spring (bounce 0.15, subtler overshoot) and
an opacity dip via [1, 0.35, 1] keyframes running the same duration with
linear easing, so the fade lands together with the layout switch instead
of trailing it
- Label updates render as a clipped push-up ticker (h-3.5 overflow-hidden
container; the new text pushes the old text up and out, 0.28s tween,
instant under reduced motion); swaps are deferred while the pill's slot
is mid layout animation (AnimatedPillSlot passes isMoving via render
prop) and play once the spring settles, otherwise immediately
- Ticker keys on the backing transcript item id, not the label string: a
genuinely new action animates, while streamed chunks that grow the same
item (message first line, thought text) update text in place — fixes
phantom re-animations of visually identical truncated text
- composerLiveActivity.ts: deriveActivityPillLabel now returns
ActivityPillHeadline {id, label} and skips usage_update /
available_commands_update lifecycle meta frames, which previously
headlined the pill as bare "Usage"/"Commands" between real actions
- animations.css: .buzz-shimmer.truncate::before ellipsizes the glow
overlay copy the same way as the base text, so the shimmer sweeps the
ellipsis instead of revealing the clipped rightmost characters
- Pill max width raised from max-w-30 (120px) to max-w-36 (144px)
- channels.spec.ts: activity-indicator test now asserts the flag-ON
reality (E2E seeds every preview feature, so composerLiveActivity always
renders): hovering the pill opens the live-activity feed popover and
clicking opens the agent session in the aux panel — replaces the legacy
"View activity" item assertions, which only ever passed via a popover
toggle race in the pre-pill trigger
- composerLiveActivity.test.mjs: object-shape assertions plus new cases —
meta frames never headline, meta-only transcripts stay generic, and the
headline id stays stable while a message streams (19 tests passing)
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…ster label decay
Composer activity strip (BotActivityBar.tsx):
- Centralize hover popover state at the strip level (useStripHoverPopover):
ONE active pill and ONE timer for the whole strip, replacing per-pill
timers that raced each other (double-open on pill-to-pill travel, stuck
cards when the cursor clipped an open card)
- Freeze pill order, membership, layout animation, and pill widths while
the cursor is over the bar or a hover card is showing; queued reorders
apply (animated) after the hold releases
- Pin the hovered pill's rendered width so label swaps can't shift
neighboring pills under the cursor
- Open on keyboard focus only for :focus-visible, and preventDefault on
the card's close auto-focus, fixing the re-open loop when Radix
returned focus to the trigger
- Typing-fallback-only agents (working source "typing", no observer turn)
now render a passive status pill: borderless, no max-width cap, no hover
card, no click-through, not focusable — there is no transcript or
session behind the typing signal to open
Pill label decay (composerLiveActivity.ts):
- Shorten ACTIVITY_PILL_STALE_MS 15s → 6s so pauses in the event stream
cycle the pill back to the generic label promptly; tick 5s → 1s
- Generic label is now agent-named ("<name> is working…") instead of the
bare "Working…"
Debug harness (features/agents/debug):
- Add per-agent "Typing" toggle simulating the basic "is typing"
situation: a synthetic channel-scoped typing entry with no observer
turn, exercising the typing-fallback path (useChannelActivityTyping →
reportChannelBotTyping → passive pill); independent of Working/Progress
so observer-over-typing precedence is testable; "End all turns" also
clears typing
- useDebugHarnessTypingEntries appends the synthetic entries in
useChannelActivityTyping (dev-only, reference-stable when idle)
Tests:
- channels.spec.ts: e2e coverage for hover-freeze — card freezes pill
order with queued reorder applying on close, and bar-hover freeze
without an open card
- composerLiveActivity.test.mjs: freshness fixtures updated for the 6s
stale window
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…is working…" - BotActivityBar.tsx: the generic decay label on a typing-fallback-only pill (working source "typing", no observer turn) now reads "<name> is typing…", matching the human typing indicator's vocabulary; observer-backed pills keep "<name> is working…" - A fresh action headline still wins over both generic labels - Both generic labels share the same ticker id, so a typing → observer source upgrade updates the text in place instead of playing the push-up swap Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…group - ChannelComposerActivityRow.tsx: partition the channel's working set by signal source — observer-backed agents keep the interactive activity pills, while typing-fallback-only agents (no observer turn, nothing to hover or open) are diverted out of the pill strip and merged with the human typers into ONE combined TypingIndicatorRow group with an overlapping avatar set; channel-agent roster names are overlaid onto the profile lookup so roster-only agents never fall back to truncated pubkeys - Fix row layout: the pill strip wrapper and typing row were both flex-1, splitting the row 50/50 and stranding the typing group at the halfway mark — the strip now sizes to content (still shrinkable) and the typing group takes the remainder; row gap set to gap-3, and the typing row's base px-4/sm:px-6 padding is zeroed next to pills (composer-edge alignment padding only applies when the group leads the row) - TypingIndicatorRow.tsx: 4+ typers label now reads "X, Y, and N more are typing..." per spec (was "N others"); new optional labelClassName prop so the composer row can match the pills' font-semibold weight without changing the thread panel's indicator - BotActivityBar.tsx: revert the passive typing pill variant and "is typing…" label — typing-only agents never reach the pill anymore; pill avatar bumped 18px → 20px with a uniform 3px surround (pl-0.75, h-7 minus 1px borders) and asymmetric pr-2 for label breathing room - debug/README.md: document that the harness Typing toggle now renders through the combined typing indicator group Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
tellaho
force-pushed
the
tho/composer-live-activity-resurrection
branch
from
July 27, 2026 07:06
c3079e0 to
538223d
Compare
…composer The thread footer's activity/typing row was centered into an mx-auto max-w-4xl column while the thread composer above it spans the full pane width, so in a wide thread pane the typing indicator floated toward the pane's center instead of hugging the composer's left gutter. - MessageThreadPanel.tsx: drop the mx-auto/max-w-4xl wrapper so the row spans the same width as the composer above it - Remove flex-1 from the toolbarExtraActions (agent pill strip) wrapper — it previously ate half the row and pushed the typing group to the midpoint whenever it mounted; the strip now sizes to its content, mirroring ChannelComposerActivityRow - Make the typing indicator's leading padding conditional like the channel row: composer-edge padding only when the typing group leads the row, pl-0 when it follows pills so the flex gap is the whole spacing Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…ip when narrow
Remove the composerLiveActivity preview flag so the live-activity hover
preview ships for everyone, and land the pill strip overflow behavior
for narrow containers.
Flag removal (ships by default):
- preview-features.json: drop the composerLiveActivity entry — absence
from the manifest means stable/always-on; E2E preview seeding and the
Settings > Experiments card both derive from the manifest, so they
pick this up automatically
- BotActivityBar.tsx: remove the useFeatureEnabled gate and the legacy
flag-off "View activity" popover item; the pill hover popover now
always renders ComposerLiveActivityFeed
- Remove the now-dead openAgentSessionPubkey plumbing through
BotActivityAgentPill / BotActivityComposerAction /
ChannelComposerActivityRow / ChannelPane — it only backed the legacy
item's active-session highlight
- Update stale flag references in ComposerLiveActivityFeed's doc
comment, the agents debug harness README, and a channels.spec.ts
comment
Pill strip overflow (BotActivityBar.tsx, ChannelComposerActivityRow.tsx):
- With several pills in a narrow container (thread panel toolbars
especially), pills keep their natural label-truncated width and the
strip scrolls horizontally — scrollbar hidden, edge gradient fades
signalling clipped pills — instead of compressing every pill into an
unreadable sliver
- New useStripOverflowFades hook tracks scroll position plus
scroller/content resizes (ResizeObserver) so fades appear and
disappear without a re-render triggering event
- A lone pill shrinks to fit (min-w-0) instead of scrolling — an edge
fade over a single pill reads as a cut-off bug, not an affordance
- layoutScroll on the scroller keeps motion's slot layout animations
measured relative to the scroll offset
- e2e: add "narrow strip scrolls horizontally with edge fades instead
of compressing pills" and "lone pill shrinks to fit a narrow
container without scroll fades"
Verified: tsc, biome check, desktop unit tests (3651 pass), and the
pill/strip/live-activity e2e subset. Two pre-existing channels.spec.ts
failures ("composer does not shift when the activity row mounts and
clears", "hovering the bar itself freezes pill order without opening a
card") reproduce identically on the parent commit with a clean tree —
unrelated to this change.
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…ation sleeps The composer pill strip only renders observer-backed agents — typing-only agents fold into the combined typing indicator group by design — but the activity-indicator and no-shift e2e tests still summoned the pill via mock typing events, so the pill never mounted and both tests failed at the first toBeVisible. - Rewrite 'shows and clears activity indicators' on __BUZZ_E2E_SEED_ACTIVE_TURNS__: assert the typing-only fold-in first, then the observer-turn promotion (typing indicator clears as the pill mounts), the channel-scoped aux panel with the live transcript, turn_completed clearing the pill, and a fresh turn re-mounting it. - Rewrite 'composer does not shift' on observer-turn seeds and extend it: the fixed-row contract now covers pill mount, pill clear, AND the combined typing group mounting — all within ±0.5px of the idle composer top, which would catch the reported 2px drift. - Make the bar-hover freeze test deterministic: the strip sizes to its content, so 'the bar's empty right side' could land on the trailing pill and open a hover card. Park the cursor in the inter-pill gap instead and wait for the hold's width-pin style before seeding the reorder. - Replace both 1200ms animation sleeps (0.9s spring + 180ms grace left only ~120ms margin on CI) with state-based assertions: the pill label committing the seeded message text proves the store update reached the strip before the frozen-order assertions run. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Rip out the dev-only debug harness added in 4818d8b once the composer live-activity screenshot matrix is captured. Deletes the whole features/agents/debug/ folder (harness, mount, panel, README) and restores the two consumers to their pre-harness shape: - AppShell.tsx: drop the DebugHarnessMount import and its JSX mount. - useChannelActivityTyping.ts: drop the useDebugHarnessRelayAgents / useDebugHarnessTypingEntries hooks and pass relayAgents / typingEntries straight through again (restoring the memo deps). The file is now byte-identical to its pre-harness state. No product behavior changes. Verified the harness residue (Alpha/Beta Debug constants, debug store) is gone from the production dist bundle. Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
tellaho
marked this pull request as ready for review
July 27, 2026 09:21
tellaho
marked this pull request as draft
July 27, 2026 16:42
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Category: improvement
User Impact: Working agents now appear as compact composer status pills that show their latest action, preview live activity on hover, and open the full session on click.
Problem: The combined “agents working” control hid which agent was doing what, shifted the composer when it mounted, and required an extra interaction to understand recent activity. Human typing and agent activity also competed for space without a clear signal hierarchy, while crowded thread composers could compress activity labels until they were unreadable.
Solution: Replace the combined control with a fixed-height, most-recently-active pill strip for observer-backed agents, while grouping typing-only agents with human typers. Each pill shows the latest meaningful channel-scoped action, freezes safely while hovered, previews the shared compact activity feed, and opens the full agent session on click. Narrow multi-agent strips scroll horizontally with edge fades; a lone pill shrinks and truncates to fit.
File changes
desktop/src/features/channels/ui/BotActivityBar.tsx
Replaces the combined working-agent trigger with animated per-agent status pills, shared hover-card control, hover-safe ordering, activity-label transitions, live transcript previews, direct session click-through, and responsive horizontal overflow affordances.
desktop/src/features/channels/ui/ChannelComposerActivityRow.tsx
Extracts the fixed-height composer activity row, separates observer-backed agents from typing-only agents, and combines typing-only agents with human typers.
desktop/src/features/channels/ui/ChannelPane.tsx
Uses the shared activity row in the main composer and the pill strip in thread composers.
desktop/src/features/channels/ui/ComposerLiveActivityFeed.tsx
Provides the inert, channel-scoped compact transcript preview used by each pill’s hover card.
desktop/src/features/channels/ui/composerLiveActivity.ts
Defines pure helpers for meaningful action labels, freshness decay, most-recent activity ordering, and archive-aware recency.
desktop/src/features/channels/ui/composerLiveActivity.test.mjs
Covers label selection, metadata filtering, streaming stability, freshness decay, ordering, and recency behavior.
desktop/src/features/channels/ui/useChannelActivityTyping.ts
Separates observer-backed agent activity from typing-only fallback signals for the composer row.
desktop/src/features/messages/ui/TypingIndicatorRow.tsx
Supports composer-specific label styling and changes larger groups to “N more are typing.”
desktop/src/features/profile/lib/lastLiveLabel.ts
Extracts the shared Last-live formatter used by compact activity surfaces.
desktop/src/features/profile/lib/lastLiveLabel.test.mjs
Covers shared Last-live labels.
desktop/src/features/profile/ui/UserProfilePanelTabs.tsx
Uses the shared Last-live formatter without changing profile behavior.
desktop/src/shared/styles/globals/animations.css
Keeps shimmer overlays aligned with truncated pill labels.
desktop/tests/e2e/channels.spec.ts
Covers composer geometry, pill interaction, activity updates, ordering, hover freezing, click-through, and narrow-strip overflow behavior.
Reproduction steps
Screenshots / demos
Single agent — current action

Multiple agents — most-recent-first ordering

Hover — compact live transcript preview

Typing-only agent — grouped with human typers

Narrow multi-agent strip — horizontal overflow and edge fade

Narrow single-agent strip — shrinks without fades
