From 8e0f2140f7de378660f42f69bd4262a3c9c8760d Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 6 Nov 2026 12:33:06 -0500 Subject: [PATCH 1/6] Show one working indicator while a message's activity bubble is live Since #2454 the conversation's working indicator stayed up for an agent even when the live activity bubble under the triggering message already showed that agent working. Solo chats and DMs showed two indicators for one turn: the bubble with its step, and bare dots at the end. A replacement registration can now say the mounted row is the agent's working indicator (presence), not only its step details. The accessory's live bubble claims presence; recorded update history does not. The conversation indicator and the timeline's live-agent list drop agents a live bubble presents, and keep their dots beside recorded history, as #2454 intended. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson --- docs/agents.md | 5 ++- docs/buzz-help.md | 3 +- .../agent-activity/ActivityAccessory.test.tsx | 12 +++++-- .../agent-activity/ActivityAccessory.tsx | 11 +++++-- .../conversation/typing-presentation.test.tsx | 32 ++++++++++++++++++- .../conversation/typing-presentation.tsx | 26 ++++++++++++++- src/features/messages/TypingIndicator.tsx | 19 ++++++++--- 7 files changed, 96 insertions(+), 12 deletions(-) diff --git a/docs/agents.md b/docs/agents.md index 6c2171f31b..98a3a4761b 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -1294,7 +1294,10 @@ tasks unknown. The live conversation indicator remains visible while agents work, even when recorded activity is collapsed or expanded. It keeps dots beside current tool labels; reduced motion leaves the dots static. Activity text may shimmer too, -but does not replace that persistent working cue. +but does not replace that persistent working cue. While an agent's live activity +bubble shows on the triggering message, that bubble is its working indicator and +the conversation does not add a second one; the conversation cue returns once +recorded history owns the work. When local request evidence is available, old activity does not inherit the working/thinking state of a different request in the same thread. diff --git a/docs/buzz-help.md b/docs/buzz-help.md index ad45f58bb1..436ac91e19 100644 --- a/docs/buzz-help.md +++ b/docs/buzz-help.md @@ -721,7 +721,8 @@ available rather than silently discarding it. its speaker. Thread work appears inside its thread; the channel’s reply summary still shows when an agent is working there. Dots remain at the end of the conversation while agents work, alongside current step details when available. - With Reduce Motion enabled, the dots stay still. Open the live indicator or + While the activity under your message shows an agent working, it is that + agent's only working indicator. With Reduce Motion enabled, the dots stay still. Open the live indicator or expand recorded work to view the agent’s activity or **Stop** the running request. The live indicator keeps its controls while recorded updates are visible. Stop availability depends on the agent type. diff --git a/src/bundled/agent-activity/ActivityAccessory.test.tsx b/src/bundled/agent-activity/ActivityAccessory.test.tsx index 16d292fcf4..b0275fe22f 100644 --- a/src/bundled/agent-activity/ActivityAccessory.test.tsx +++ b/src/bundled/agent-activity/ActivityAccessory.test.tsx @@ -1688,9 +1688,10 @@ it.each([ name: `View agent activity: Blossom, ${label}`, }), ).toBeVisible(); + // The message bubble is the agent's one working indicator here. expect( - screen.getByRole("button", { name: "Activity: Blossom working" }), - ).toBeVisible(); + screen.queryByRole("button", { name: /Activity: .*working/ }), + ).toBeNull(); expect(screen.getAllByText(label)).toHaveLength(1); view.rerender(ui([A])); expect( @@ -1698,6 +1699,9 @@ it.each([ name: `View agent activity: Blossom, ${label}`, }), ).toBeVisible(); + expect( + screen.queryByRole("button", { name: /Activity: .*working/ }), + ).toBeNull(); expect( screen.queryByRole("button", { name: "Reading notes.txt" }), ).toBeNull(); @@ -1867,6 +1871,10 @@ it("shows thinking once across the live bubble, recorded history and conversatio const view = render(ui(false)); try { expect(screen.getAllByText("Thinking")).toHaveLength(1); + // Until history owns the work, the message bubble is its one indicator. + expect( + screen.queryByRole("button", { name: /Activity: .*working/ }), + ).toBeNull(); view.rerender(ui(true)); expect( screen.queryByLabelText("Agent activity on this message"), diff --git a/src/bundled/agent-activity/ActivityAccessory.tsx b/src/bundled/agent-activity/ActivityAccessory.tsx index 1a06e241a5..3461e060ff 100644 --- a/src/bundled/agent-activity/ActivityAccessory.tsx +++ b/src/bundled/agent-activity/ActivityAccessory.tsx @@ -416,6 +416,7 @@ function MessageActivity( {shown.map((entry) => ( row.agent === entry.agent)} key={entry.agent} {...props} agent={entry.agent} @@ -878,9 +879,15 @@ function TypingReplacement({ threadRootId, agent, shared = false, -}: ComposerAccessoryProps & { agent: string; shared?: boolean }) { + presence = false, +}: ComposerAccessoryProps & { + agent: string; + shared?: boolean; + /** This bubble shows the agent working, so the conversation shows no second cue. */ + presence?: boolean; +}) { useActivityTypingReplacement( - { session, channelId, threadRootId, pubkey: agent }, + { session, channelId, threadRootId, pubkey: agent, presence }, shared, ); useTypingReplacement( diff --git a/src/features/conversation/typing-presentation.test.tsx b/src/features/conversation/typing-presentation.test.tsx index c2210d3b92..f0898de89a 100644 --- a/src/features/conversation/typing-presentation.test.tsx +++ b/src/features/conversation/typing-presentation.test.tsx @@ -5,6 +5,7 @@ import type { RelaySession } from "../relay/session"; import { ActivityIndicatorPresentation, useActivityTypingReplacement, + usePresentedAgents, useReplacedTypers, } from "./typing-presentation"; @@ -15,12 +16,14 @@ const session = {} as RelaySession; function Replacement({ channelId, participants, + presence, }: { channelId: string; participants: readonly string[]; + presence?: boolean; }) { useActivityTypingReplacement( - { session, channelId, threadRootId: undefined, pubkey: "" }, + { session, channelId, threadRootId: undefined, pubkey: "", presence }, true, participants, ); @@ -50,3 +53,30 @@ it("re-renders a replaced-typers reader only when its conversation's keys change rerender(view(["one", "four"], ["two", "three"])); expect([...replaced]).toEqual(["one", "four"]); }); + +it("separates rows that show an agent working from rows that own only its details", () => { + let replaced: ReadonlySet = new Set(); + let presented: ReadonlySet = new Set(); + function Reader() { + replaced = useReplacedTypers(session, "alpha", undefined); + presented = usePresentedAgents(session, "alpha", undefined); + return null; + } + const view = (presence: boolean) => ( + + + + + + + ); + const { rerender } = render(view(true)); + expect([...replaced].sort()).toEqual(["bubble", "history"]); + expect([...presented]).toEqual(["bubble"]); + rerender(view(false)); + expect([...presented]).toEqual([]); +}); diff --git a/src/features/conversation/typing-presentation.tsx b/src/features/conversation/typing-presentation.tsx index afd5a96c59..9378ec97e3 100644 --- a/src/features/conversation/typing-presentation.tsx +++ b/src/features/conversation/typing-presentation.tsx @@ -16,6 +16,9 @@ type Replacement = Readonly<{ threadRootId: string | undefined; pubkey: string; requestId?: string | undefined; + /** This mounted row is the agent's live working indicator, not only its + * step details: the conversation must not show a second one. */ + presence?: boolean | undefined; }>; function createPresentation() { const entries = new Map(); @@ -117,7 +120,8 @@ function useReplacement( ) { const store = context?.store; const active = context?.active; - const { session, channelId, threadRootId, pubkey, requestId } = value ?? {}; + const { session, channelId, threadRootId, pubkey, requestId, presence } = + value ?? {}; const participantKeys = (participants ?? (pubkey ? [pubkey] : [])).join(":"); useLayoutEffect(() => { if (!active || !enabled || !session || !channelId || !participantKeys) @@ -129,6 +133,7 @@ function useReplacement( threadRootId, pubkey, requestId, + presence, })), ); }, [ @@ -140,12 +145,30 @@ function useReplacement( threadRootId, participantKeys, requestId, + presence, ]); } +/** Agents whose step details a mounted row shows in this conversation. */ export function useReplacedTypers( session: object | undefined, channelId: string, threadRootId: string | undefined, +) { + return useScopedReplacements(session, channelId, threadRootId, false); +} +/** Agents whose live working indicator a mounted row shows in this conversation. */ +export function usePresentedAgents( + session: object | undefined, + channelId: string, + threadRootId: string | undefined, +) { + return useScopedReplacements(session, channelId, threadRootId, true); +} +function useScopedReplacements( + session: object | undefined, + channelId: string, + threadRootId: string | undefined, + presenceOnly: boolean, ) { const context = useContext(Presentation); const activityStore = useContext(ActivityPresentation); @@ -173,6 +196,7 @@ export function useReplacedTypers( ], (entry) => !entry.requestId && + (!presenceOnly || !!entry.presence) && entry.session === session && entry.channelId === channelId && entry.threadRootId === threadRootId, diff --git a/src/features/messages/TypingIndicator.tsx b/src/features/messages/TypingIndicator.tsx index dd4ecd6ccb..40bf3e33cd 100644 --- a/src/features/messages/TypingIndicator.tsx +++ b/src/features/messages/TypingIndicator.tsx @@ -1,6 +1,9 @@ import { useActivityFocusHandoff } from "./use-activity-focus-handoff"; import messageStyles from "./Messages.module.css"; -import { useReplacedTypers } from "../conversation/typing-presentation"; +import { + usePresentedAgents, + useReplacedTypers, +} from "../conversation/typing-presentation"; import { useChannelIdentityNames } from "../identity-names/react"; import { useCallback, @@ -97,6 +100,7 @@ export function useLiveConversationAgents( threadRootId: string | undefined, includeThreads = true, ): readonly string[] { + const presented = usePresentedAgents(session, channelId, threadRootId); const watchChannel = !threadRootId && !includeThreads; const subscribe = useCallback( (listener: () => void) => { @@ -130,7 +134,10 @@ export function useLiveConversationAgents( includeThreads, threadRootsOf(channelRowsOf(session, channelId, watchChannel)), ); - return [...details.keys()].sort().join(","); + return [...details.keys()] + .filter((key) => !presented.has(key)) + .sort() + .join(","); }; const key = useSyncExternalStore(subscribe, getKey, getKey); return useMemo(() => (key ? key.split(",") : []), [key]); @@ -144,6 +151,7 @@ export function useConversationAgents( includeThreads = true, ) { const replaced = useReplacedTypers(session, channelId, threadRootId); + const presented = usePresentedAgents(session, channelId, threadRootId); const entries = useSyncExternalStore( session.typing.subscribe, session.typing.snapshot, @@ -196,8 +204,11 @@ export function useConversationAgents( getDetails, getDetails, ); - // A message may own the step details, but the conversation keeps its busy cue. - const agents = [...details.keys()].sort(); + // A message's live bubble is the agent's working indicator. Recorded history + // may own only the step details; then the conversation keeps its busy cue. + const agents = [...details.keys()] + .filter((key) => !presented.has(key)) + .sort(); return { activity: (session.agentActivity?.snapshot ?? noActivity)(), details, From 17065ee02fb66e1e1150b3db8645dfd8e52a1093 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 6 Nov 2026 12:36:16 -0500 Subject: [PATCH 2/6] Cover timeline stacking with a presented agent; lead docs with the rule Review follow-up: the timeline's live-agent list must drop a presented agent too, or the previous reply stacks onto a hidden indicator. Docs now state the bubble rule before the dots rule. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson --- docs/agents.md | 14 +++---- docs/buzz-help.md | 8 ++-- .../ChannelTimeline.viewport.test.tsx | 41 ++++++++++++++++--- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/docs/agents.md b/docs/agents.md index 98a3a4761b..803c70cc9b 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -1291,13 +1291,13 @@ tasks unknown. ### Activity updates in the conversation -The live conversation indicator remains visible while agents work, even when -recorded activity is collapsed or expanded. It keeps dots beside current tool -labels; reduced motion leaves the dots static. Activity text may shimmer too, -but does not replace that persistent working cue. While an agent's live activity -bubble shows on the triggering message, that bubble is its working indicator and -the conversation does not add a second one; the conversation cue returns once -recorded history owns the work. +When no live activity bubble on the triggering message shows an agent, the live +conversation indicator remains visible while it works, even when recorded +activity is collapsed or expanded. While that bubble shows, it is the agent's +working indicator and the conversation adds no second one; the conversation cue +returns once recorded history owns the work. The indicator keeps dots beside +current tool labels; reduced motion leaves the dots static. Activity text may +shimmer too, but does not replace that persistent working cue. When local request evidence is available, old activity does not inherit the working/thinking state of a different request in the same thread. diff --git a/docs/buzz-help.md b/docs/buzz-help.md index 436ac91e19..753b14063f 100644 --- a/docs/buzz-help.md +++ b/docs/buzz-help.md @@ -719,10 +719,10 @@ available rather than silently discarding it. not a guarantee that an agent is available or that a message was delivered. Expand grouped work updates to read each message; the collapsed excerpt names its speaker. Thread work appears inside its thread; the channel’s reply summary - still shows when an agent is working there. Dots remain at the end of the - conversation while agents work, alongside current step details when available. - While the activity under your message shows an agent working, it is that - agent's only working indicator. With Reduce Motion enabled, the dots stay still. Open the live indicator or + still shows when an agent is working there. While the activity under your + message shows an agent working, it is that agent's only working indicator. + Otherwise dots remain at the end of the conversation while agents work, + alongside current step details when available. With Reduce Motion enabled, the dots stay still. Open the live indicator or expand recorded work to view the agent’s activity or **Stop** the running request. The live indicator keeps its controls while recorded updates are visible. Stop availability depends on the agent type. diff --git a/src/features/messages/ChannelTimeline.viewport.test.tsx b/src/features/messages/ChannelTimeline.viewport.test.tsx index 6d3260f17a..243b5c1dc0 100644 --- a/src/features/messages/ChannelTimeline.viewport.test.tsx +++ b/src/features/messages/ChannelTimeline.viewport.test.tsx @@ -25,9 +25,21 @@ import { } from "../conversation/typing-presentation"; import type { RelaySession } from "../relay/session"; -function RecordedTyping({ session }: { session: RelaySession }) { +function RecordedTyping({ + session, + presence = false, +}: { + session: RelaySession; + presence?: boolean; +}) { useTypingReplacement( - { session, channelId: "alpha", threadRootId: undefined, pubkey: "agent" }, + { + session, + channelId: "alpha", + threadRootId: undefined, + pubkey: "agent", + presence, + }, true, ); return null; @@ -334,9 +346,13 @@ it.each([1, 4])( }, ); -it.each([false, true])( - "keeps ongoing typing in the reply group after completed history (replaced=%s)", - (replaced) => { +it.each([ + [false, false], + [true, false], + [true, true], +])( + "keeps ongoing typing in the reply group after completed history (replaced=%s, presented=%s)", + (replaced, presented) => { const { owner, updates, content } = activityFixture(); try { const history = updates(2); @@ -367,7 +383,9 @@ it.each([false, true])( }; const view = render( - {replaced && } + {replaced && ( + + )} {content( [...history, { ...reply, id: "reply", content: "Main reply" }], undefined, @@ -377,6 +395,17 @@ it.each([false, true])( , ); expect(screen.queryByText("2 completed steps")).toBeNull(); + if (presented) { + // A live bubble elsewhere shows the agent: no conversation cue, and + // the reply does not join one. + expect( + screen.queryByRole("button", { name: /Activity: .*working/ }), + ).toBeNull(); + expect( + screen.getByText("Main reply").closest("li"), + ).not.toHaveAttribute("data-stack-next", "true"); + return; + } expect( screen.getByRole("button", { name: /Activity: .*working/ }), ).toBeVisible(); From 9b38f871169591aca69d83f4cde29cb906bbdd2f Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 6 Nov 2026 12:37:43 -0500 Subject: [PATCH 3/6] Register a thread root's live bubble under that thread MessageRow gives a thread's root row no thread root, so its live bubble registered under the channel scope while it shows in the thread panel. The thread's indicator then kept a second cue, and the channel's could lose its own. Register the shared presentation under the conversation the bubble shows in. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson --- .../agent-activity/ActivityAccessory.test.tsx | 52 +++++++++++++++++++ .../agent-activity/ActivityAccessory.tsx | 8 ++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/bundled/agent-activity/ActivityAccessory.test.tsx b/src/bundled/agent-activity/ActivityAccessory.test.tsx index b0275fe22f..c029e69202 100644 --- a/src/bundled/agent-activity/ActivityAccessory.test.tsx +++ b/src/bundled/agent-activity/ActivityAccessory.test.tsx @@ -1778,6 +1778,58 @@ it.each([false, true])( }, ); +it("lets a thread root's live bubble replace that thread's indicator", () => { + const local = createLocalAgentActivity(); + const f = fixture(local); + const typing = [{ pubkey: A, channelId: "c", threadRootId: root }]; + const session = { + ...f.props.session, + typing: { snapshot: () => typing, subscribe: () => () => {} }, + } as unknown as RelaySession; + // MessageRow passes the row's own thread root: none for a thread's root row. + const props = { ...f.props, session, threadRootId: undefined }; + local.begin({ + scope: "test", + agent: A, + channelId: "c", + messageId: root, + threadRootId: root, + thread: true, + }); + const ui = (layout: "thread" | "timeline") => ( + + + + + ); + const view = render(ui("thread")); + try { + expect( + screen.getByLabelText("Agent activity on this message"), + ).toBeVisible(); + expect( + screen.queryByRole("button", { name: /Activity: .*working/ }), + ).toBeNull(); + // In the channel timeline the root shows no thread bubble, so the thread + // keeps its own indicator. + view.rerender(ui("timeline")); + expect( + screen.queryByLabelText("Agent activity on this message"), + ).toBeNull(); + expect( + screen.getByRole("button", { name: /Activity: .*working/ }), + ).toBeVisible(); + } finally { + view.unmount(); + f.dispose(); + } +}); + it("shows thinking once across the live bubble, recorded history and conversation indicator", () => { const local = createLocalAgentActivity(); const f = fixture(local); diff --git a/src/bundled/agent-activity/ActivityAccessory.tsx b/src/bundled/agent-activity/ActivityAccessory.tsx index 3461e060ff..e06f233a21 100644 --- a/src/bundled/agent-activity/ActivityAccessory.tsx +++ b/src/bundled/agent-activity/ActivityAccessory.tsx @@ -877,6 +877,8 @@ function TypingReplacement({ session, channelId, threadRootId, + message, + messageLayout, agent, shared = false, presence = false, @@ -886,8 +888,12 @@ function TypingReplacement({ /** This bubble shows the agent working, so the conversation shows no second cue. */ presence?: boolean; }) { + // A thread's root row has no thread root of its own, but its bubble shows in + // that thread's conversation, so it owns that thread's indicator. + const shownIn = + threadRootId ?? (messageLayout === "thread" ? message?.id : undefined); useActivityTypingReplacement( - { session, channelId, threadRootId, pubkey: agent, presence }, + { session, channelId, threadRootId: shownIn, pubkey: agent, presence }, shared, ); useTypingReplacement( From 5826f1c120ca4210040d8c0db182aba99bf852dd Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 6 Nov 2026 12:40:18 -0500 Subject: [PATCH 4/6] Derive presence from the bubble's shared registration Review follow-up: every entry the live bubble renders is in its bubble, so the per-entry check was always true. The shared registration now implies presence. The stacking regression registers presence through the conversation's activity store, as production does. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson --- .../agent-activity/ActivityAccessory.tsx | 14 +++--- .../ChannelTimeline.viewport.test.tsx | 43 +++++++++++-------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/bundled/agent-activity/ActivityAccessory.tsx b/src/bundled/agent-activity/ActivityAccessory.tsx index e06f233a21..712a51c377 100644 --- a/src/bundled/agent-activity/ActivityAccessory.tsx +++ b/src/bundled/agent-activity/ActivityAccessory.tsx @@ -416,7 +416,6 @@ function MessageActivity( {shown.map((entry) => ( row.agent === entry.agent)} key={entry.agent} {...props} agent={entry.agent} @@ -881,19 +880,24 @@ function TypingReplacement({ messageLayout, agent, shared = false, - presence = false, }: ComposerAccessoryProps & { agent: string; + /** Only the live bubble shares its registration. It shows the agent working, + * so the conversation shows no second cue (presence). */ shared?: boolean; - /** This bubble shows the agent working, so the conversation shows no second cue. */ - presence?: boolean; }) { // A thread's root row has no thread root of its own, but its bubble shows in // that thread's conversation, so it owns that thread's indicator. const shownIn = threadRootId ?? (messageLayout === "thread" ? message?.id : undefined); useActivityTypingReplacement( - { session, channelId, threadRootId: shownIn, pubkey: agent, presence }, + { + session, + channelId, + threadRootId: shownIn, + pubkey: agent, + presence: shared, + }, shared, ); useTypingReplacement( diff --git a/src/features/messages/ChannelTimeline.viewport.test.tsx b/src/features/messages/ChannelTimeline.viewport.test.tsx index 243b5c1dc0..9f4a6a146a 100644 --- a/src/features/messages/ChannelTimeline.viewport.test.tsx +++ b/src/features/messages/ChannelTimeline.viewport.test.tsx @@ -20,25 +20,29 @@ import { foldMessages } from "../relay/fold"; import { keypair, message } from "../relay/testing"; import type { ChannelMessage, ChannelWindow } from "../relay/contracts"; import { + ActivityIndicatorPresentation, TypingPresentation, + useActivityTypingReplacement, useTypingReplacement, } from "../conversation/typing-presentation"; import type { RelaySession } from "../relay/session"; -function RecordedTyping({ - session, - presence = false, -}: { - session: RelaySession; - presence?: boolean; -}) { +function RecordedTyping({ session }: { session: RelaySession }) { useTypingReplacement( + { session, channelId: "alpha", threadRootId: undefined, pubkey: "agent" }, + true, + ); + return null; +} +/** A message's live bubble registers presence in the conversation's store. */ +function LiveBubble({ session }: { session: RelaySession }) { + useActivityTypingReplacement( { session, channelId: "alpha", threadRootId: undefined, pubkey: "agent", - presence, + presence: true, }, true, ); @@ -382,17 +386,18 @@ it.each([ }, }; const view = render( - - {replaced && ( - - )} - {content( - [...history, { ...reply, id: "reply", content: "Main reply" }], - undefined, - true, - queries, - )} - , + + + {replaced && !presented && } + {presented && } + {content( + [...history, { ...reply, id: "reply", content: "Main reply" }], + undefined, + true, + queries, + )} + + , ); expect(screen.queryByText("2 completed steps")).toBeNull(); if (presented) { From bb2259264116822854dce6b6974af19107be5b27 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 6 Nov 2026 13:07:25 -0500 Subject: [PATCH 5/6] Expect one working indicator in the local agent thread journey The journey asserted the thread tail's working dots alongside the message's live bubble, the duplicate this branch removes. Assert that the tail shows none and check reduced-motion dots on the bubble. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- tests/browser/local-agent-activity.spec.mjs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/browser/local-agent-activity.spec.mjs b/tests/browser/local-agent-activity.spec.mjs index e4abcd7a28..c58a701440 100644 --- a/tests/browser/local-agent-activity.spec.mjs +++ b/tests/browser/local-agent-activity.spec.mjs @@ -92,14 +92,15 @@ test("local agent receipt, work and failure agree across sidebar, thread and exa ).toHaveCount(0); await page.evaluate((id) => window.localAgentFixture.handover(id), second.id); await expect(waiting).toContainText("Reading notes.txt"); - // The real thread tail keeps dots when a message owns the tool details, - // including without shimmer. jsdom cannot prove CSS visibility. + // The message's live bubble is the only working indicator: the thread tail + // adds no second one. Its dots stay visible without motion; jsdom cannot + // prove CSS visibility. const liveStep = thread.getByRole("button", { name: "Activity: Local tester working", exact: true, }); - const dots = liveStep.locator('[data-state="working"] > span'); - await expect(liveStep).toBeVisible(); + await expect(liveStep).toHaveCount(0); + const dots = waiting.locator('[data-state="working"] > span'); await page.emulateMedia({ reducedMotion: "reduce" }); await expect(dots).toHaveCount(3); await expect(dots.first()).toBeVisible(); From 433182b244d2764c52a2dccbd0d29b14875292d8 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 6 Nov 2026 16:54:14 -0500 Subject: [PATCH 6/6] Rewrap the working indicator help paragraph Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 Signed-off-by: Logan Johnson --- docs/buzz-help.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/buzz-help.md b/docs/buzz-help.md index 753b14063f..1ef720fc47 100644 --- a/docs/buzz-help.md +++ b/docs/buzz-help.md @@ -722,10 +722,11 @@ available rather than silently discarding it. still shows when an agent is working there. While the activity under your message shows an agent working, it is that agent's only working indicator. Otherwise dots remain at the end of the conversation while agents work, - alongside current step details when available. With Reduce Motion enabled, the dots stay still. Open the live indicator or - expand recorded work to view the agent’s activity or **Stop** the running - request. The live indicator keeps its controls while recorded updates are - visible. Stop availability depends on the agent type. + alongside current step details when available. With Reduce Motion enabled, + the dots stay still. Open the live indicator or expand recorded work to view + the agent’s activity or **Stop** the running request. The live indicator + keeps its controls while recorded updates are visible. Stop availability + depends on the agent type. - Agent-to-agent messages stay in the conversation as collapsed history after work finishes or you reopen a thread. Expand them to read dispatches, handoffs, and results. The working indicator shows current activity separately.