From 442e08b4e0c6492da9182a67428b446dfab52c90 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Sun, 26 Jul 2026 23:55:58 -0700 Subject: [PATCH] feat(composer): collapse typing agents and humans into one indicator group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Signed-off-by: Taylor Ho --- .../features/channels/ui/BotActivityBar.tsx | 37 ++----- .../ui/ChannelComposerActivityRow.tsx | 104 ++++++++++++++++-- .../messages/ui/TypingIndicatorRow.tsx | 6 +- 3 files changed, 105 insertions(+), 42 deletions(-) diff --git a/desktop/src/features/channels/ui/BotActivityBar.tsx b/desktop/src/features/channels/ui/BotActivityBar.tsx index 238624cf7..dd116c29b 100644 --- a/desktop/src/features/channels/ui/BotActivityBar.tsx +++ b/desktop/src/features/channels/ui/BotActivityBar.tsx @@ -2,7 +2,6 @@ import * as React from "react"; import { Loader2 } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; -import { useAgentWorking } from "@/features/agents/agentWorkingSignal"; import { getAgentTranscript, subscribeAgentObserverStore, @@ -163,10 +162,9 @@ function useStripHoverPopover(): StripHoverPopover { * `composerLiveActivity` preview flag off, the hover popover keeps the * legacy "View activity" item instead. * - * Typing-fallback-only agents (working source "typing", no observer turn) - * render a passive, borderless, width-uncapped status instead: no hover - * card, no click-through, not focusable — there is no transcript or session - * behind the signal to open. + * Only observer-backed agents reach this pill: typing-fallback-only agents + * are diverted into the combined typing indicator group by + * ChannelComposerActivityRow before the strip renders. */ function BotActivityAgentPill({ agent, @@ -197,10 +195,6 @@ function BotActivityAgentPill({ const pillKey = agent.pubkey.toLowerCase(); const open = hover.activePubkey === pillKey; const shouldReduceMotion = useReducedMotion(); - // Typing-fallback-only agents (no observer turn) render borderless and - // uncapped — the basic "is typing" presentation. - const typingOnly = - useAgentWorking(agent.pubkey, channelId).source === "typing"; const transcript = useAgentTranscript(true, agent.pubkey); const now = useNow(PILL_LABEL_TICK_MS); const headline = React.useMemo( @@ -209,11 +203,8 @@ function BotActivityAgentPill({ ); const activeId = headline?.id ?? GENERIC_LABEL_ID; // No fresh action headline (see deriveActivityPillLabel) — decay to the - // agent-named generic label. Typing-fallback-only agents read - // "is typing…" (matching the human indicator's vocabulary); observer-backed - // agents read "is working…". - const activeLabel = - headline?.label ?? `${agent.name} is ${typingOnly ? "typing" : "working"}…`; + // agent-named generic working label. + const activeLabel = headline?.label ?? `${agent.name} is working…`; // The rendered label lags the derived one while the pill is moving: the // push-up ticker plays after the slot settles (or immediately when idle). // Keyed by transcript item id, not label text — a NEW action swaps, while @@ -261,7 +252,7 @@ function BotActivityAgentPill({ <> @@ -288,20 +279,6 @@ function BotActivityAgentPill({ ); - // Typing-fallback-only: a passive status readout — no hover card, no - // click-through, not focusable. There is no observer turn (and so no - // transcript or session) behind it to open. - if (typingOnly) { - return ( -
- {pillContent} -
- ); - } - return ( { @@ -316,7 +293,7 @@ function BotActivityAgentPill({