Skip to content

surface live agent activity in composer status pills - #2646

Draft
tellaho wants to merge 13 commits into
mainfrom
tho/composer-live-activity-resurrection
Draft

surface live agent activity in composer status pills#2646
tellaho wants to merge 13 commits into
mainfrom
tho/composer-live-activity-resurrection

Conversation

@tellaho

@tellaho tellaho commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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

  1. Run the desktop app and open a channel where one observer-backed agent is working.
  2. Confirm a fixed-height pill appears with the agent avatar and latest meaningful action.
  3. Let activity pause and confirm the label decays to the agent-named working state; emit another action and confirm the label transitions in place.
  4. Hover the pill and confirm its position and width remain stable while the compact live transcript card is open.
  5. Click the pill and confirm the full agent session opens in the auxiliary panel.
  6. Start multiple observer-backed agents and confirm the most recently active agent moves left only when the strip is not hovered.
  7. Narrow the composer and confirm multiple pills scroll horizontally with directional edge fades; confirm one pill instead shrinks and truncates without a fade.
  8. Start a typing-only agent and human typer and confirm they appear together in one typing group rather than as transcript-backed pills.
  9. Enable reduced motion and confirm label and pill transitions become immediate.

Screenshots / demos

Single agent — current action
Single activity pill

Multiple agents — most-recent-first ordering
Multiple ordered activity pills

Hover — compact live transcript preview
Live activity hover preview

Typing-only agent — grouped with human typers
Combined typing group

Narrow multi-agent strip — horizontal overflow and edge fade
Narrow multi-agent overflow

Narrow single-agent strip — shrinks without fades
Narrow lone activity pill

npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w and others added 9 commits July 26, 2026 23:59
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
tellaho force-pushed the tho/composer-live-activity-resurrection branch from c3079e0 to 538223d Compare July 27, 2026 07:06
…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>
@tellaho tellaho changed the title add selected-agent live activity to the composer show live agent activity in composer status pills Jul 27, 2026
…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>
@tellaho tellaho changed the title show live agent activity in composer status pills surface live agent activity in composer status pills Jul 27, 2026
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w and others added 2 commits July 27, 2026 02:09
…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
tellaho marked this pull request as ready for review July 27, 2026 09:21
@tellaho
tellaho marked this pull request as draft July 27, 2026 16:42
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