Skip to content

fix(agent-activity): align the stopped note and show it after the agent's reply - #2441

Merged
tulsi-builder merged 8 commits into
mainfrom
basil/stopped-notice-alignment
Nov 4, 2026
Merged

tulsi-builder merged 8 commits into
mainfrom
basil/stopped-notice-alignment

Conversation

@tulsi-builder

@tulsi-builder tulsi-builder commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

What

When you stop an agent, the "Agent · Stopped" note that replaces its activity bubble:

  1. was drawn at the far left edge of the timeline, outside the message column;
  2. sat above the agent's reply, even when the agent had already replied before being stopped.

Change

  • Alignment: the note renders in the incoming message frame (.message, which supplies the centered measure). It has no gutter, so its mini avatar starts at the incoming avatars' left edge. The stopped-note rules in Messages.module.css no longer matched anything since Unify agent activity with incoming message previews #1729 and are removed.
  • Placement: activityPresentation now also reports, for each request, the agents that posted a public (non-audience: "agents") reply (repliedAgents), and each agent's latest public reply row (replyRequests).
    • The request's accessory skips the stopped note for agents that replied.
    • The agent's latest reply row gets the request as stoppedRequest and renders that agent's stopped note after itself.
    • If the agent never replied, the note stays after the request, as before.
  • Threads: only the root request uses this, and only top-level replies can hold the note, because collapsed branches don't mount their rows. Requests nested inside a thread keep the previous placement. A moved note takes its visibility from the reply's thread scope, not from the row's layout, so it still shows when the reply uses continuation layout.

Validation

  • Desktop dev app (human): the note lines up with incoming avatars, and after stopping an agent that had already replied, it sits below the reply.
  • vitest run src/features/messages src/features/conversation src/bundled/agent-activity: 113 files / 2,766 tests pass. tsc --noEmit and Biome are clean.
  • New tests:
    • the note renders inside the message frame;
    • the note moves from the request to the agent's reply, while live work stays on the request;
    • the presentation maps each request to its latest public replies;
    • a thread root's note stays on a top-level reply when a branch is collapsed or expanded. This test fails without the thread repair.
    • a thread root's note shows on both a thread-layout and a continuation-layout reply. The continuation case fails without 6b7fa2b.
  • Agent review: Crack Catcher found that a collapsed branch could hide the note. Fixed in 63f7391; no remaining blockers.
  • Review feedback: the continuation-layout scope bug is fixed in 6b7fa2b. The message, conversation and agent-activity suites now pass at 2,768 tests.
  • Earlier full vitest run on a heavily loaded machine had unrelated flakes that passed on rerun. agent-harness/kit.test.ts › "refuses a mention from anyone else…" also fails on unchanged origin/main.

Not checked in the app: the thread-root placement, the collapsed-branch case, dark theme, narrow widths and enlarged text.

🤖 Generated with Claude Code

Since activity moved into incoming message previews, the "Agent · Stopped"
note rendered as a bare row beside the message, so it hugged the left edge
of the timeline instead of the centered reply column. Wrap it in the same
incoming message frame the live bubble uses.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
@tulsi-builder
tulsi-builder marked this pull request as ready for review November 4, 2026 16:47
@tulsi-builder
tulsi-builder requested a review from a team as a code owner November 4, 2026 16:47
…ent's reply

Start the note at the incoming avatars' edge instead of the bubble's inset,
and drop the stopped-note rules that no longer match. When the stopped agent
already replied publicly to the request, show its note after that agent's
latest reply instead of above it; otherwise it stays after the request.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 One non-blocking regression-test coverage gap; no production defect established.

// The note keeps the incoming message column instead of the list's edge.
expect(
screen.getByText("Blossom · Stopped").closest("[data-layout]"),
).toHaveAttribute("data-layout", "timeline");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P3] Protect the explicit thread layout as well as the default. This test omits messageLayout, so it only exercises the timeline fallback. Replacing the production data-layout expression with a hard-coded timeline still passes all 62 agent-activity tests, although thread layout has different spacing. Add a case with messageLayout="thread" and assert data-layout="thread" to catch loss of the pass-through. The existing assertion remains useful: it fails against the previous implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Added in 042a1f2: ActivityAccessory.test.tsx › "notes a stopped request in the response lane" now runs for both the default (asserts data-layout="timeline") and an explicit messageLayout="thread" (asserts data-layout="thread"). Verified the thread case fails when the production expression is hard-coded to timeline.

Collapsed branches do not mount their rows, so a stopped note anchored to a
nested reply would disappear. Anchor the root's note only to top-level replies.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
@tulsi-builder tulsi-builder changed the title fix(agent-activity): keep the stopped note in the reply column fix(agent-activity): align the stopped note and show it after the agent's reply Nov 4, 2026

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 A stopped note can disappear when its thread reply uses continuation layout.

repliedAgents={undefined}
stoppedRequest={undefined}
stoppedAgent={author}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P2] Preserve thread scope when relocating a stopped note. If an agent posts two consecutive top-level public replies in a thread, the latest reply uses continuation layout. Stopping work on the root then hides its note through repliedAgents, while this relocated MessageActivity receives the root message and the reply’s continuation layout. visible() consequently applies the channel-only filter and removes thread-scoped work, leaving no stopped note in either location. A targeted render reproduction passes with the reply layout set to thread and fails with continuation (missing Blossom · Stopped). Derive visibility from the request/thread scope independently of the host row geometry, and add a rendered continuation-reply regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Good catch, thanks. Fixed in 6b7fa2b. The relocated note now gets its visibility from the reply's thread scope (threadScoped), not from the row's layout. I added a rendered regression, keeps a thread request's stopped note on a %s reply, covering both thread and continuation reply layouts. The continuation case fails without the fix. CI is green on 6b7fa2b.

…thread

A thread root's note moved onto a continuation-layout reply took the reply's
layout as its scope, so thread work was filtered out and the note vanished.
Derive the scope from the reply's thread instead of its geometry.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 The continuation-scope regression is fixed. One remaining regression-coverage gap is noted inline.

repliedAgents={stableActivity.agents(
`replied:${row.id}`,
presentation.repliedAgents.get(row.id),
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P3] Add coverage for the timeline's paired relocation wiring. Setting repliedAgents hides the note on the request, so the latest public reply must also receive the matching stoppedRequest. The accessory tests construct both instances directly, the presentation test checks only maps, and the browser no-reply case does not exercise this host wiring. Dropping or mis-keying this lookup would therefore silently remove the note without failing those tests. Add a ChannelTimeline regression asserting both the request's replied agent and the latest public reply's request ID, analogous to the ThreadPanel nesting test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Added in c936280: moves a request's stopped note to its agent's latest public reply in ChannelTimeline.viewport.test.tsx. It renders ChannelTimeline with a request, an earlier agent reply, an activity step and a latest agent reply. It then checks that the request gets repliedAgents=[agent], the latest reply gets stoppedRequest=request, and the earlier reply gets nothing. Dropping the stoppedRequest lookup makes the test fail.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
placement="response"
representedAgents={representedAgents}
repliedAgents={repliedAgents}
stoppedRequest={stoppedRequest}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [P3] Cover the response-accessory forwarding hop. The ChannelTimeline and ThreadPanel tests mock MessageRow, while accessory tests supply props directly. Removing this stoppedRequest forwarding line still passes all 2769 tests under messages, agent-activity and conversation. If this forwarding regresses, repliedAgents still suppresses the request note while the latest reply receives no stopped note. Add one MessageRow.test.tsx case using the response-accessory registry harness around lines 1753–1773: capture accessory props and assert that both repliedAgents and stoppedRequest arrive. This is a regression-coverage gap; the current forwarding is present.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Added in dfaaf99: MessageRow.test.tsx › "forwards replied agents and the stopped request to response accessories" registers a response-placement accessory, captures its props, and asserts both repliedAgents and stoppedRequest arrive. Verified it fails when either forwarding line in MessageRow.tsx is removed.

tulsi-builder and others added 3 commits November 4, 2026 13:45
Assert MessageRow hands repliedAgents and stoppedRequest to response
accessories, so dropping either forwarding prop fails a test.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>
…alignment

Signed-off-by: tulsi <[email protected]>

# Conflicts:
#	src/features/messages/MessageRow.test.tsx
Run the stopped-note layout assertion for both the timeline default and
an explicit thread layout, so a hard-coded data-layout fails a test.

Co-authored-by: BasilBot <399581b6165ab01343e3a1324774b1024d76942aea563468f93c975e5ee5193c@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: tulsi <[email protected]>

@loganj loganj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Larry and Auto: an independent adversarial review of 042a1f2. Source review only. Auto also ran vitest run src/features/messages src/features/conversation src/bundled/agent-activity at this head: 113 files, 2782 tests passed. kalvinnchau's earlier P2 (continuation scope) and three P3 coverage threads are fixed at this head, so we do not repeat them.

Verdict: approve. One convention item needs a human (inline on contracts.ts). This approval is a source review; it is not the FOUNDATION sign-off.

What we traced and found sound:

  • A public reply row always mounts with response accessories. activityRuns folds only audience=agents rows, and in ThreadPanel a top-level row with collapsed children still mounts.
  • The request side (repliedAgents) and the reply side (replyRequests) come from the same pass in each host. So the note cannot be skipped on one side and missed on the other, and only the agent's latest reply holds it.
  • The moved note's thread scope matches the request's own in both hosts: false in the timeline, true in the thread panel.
  • Other hosts (MediaReviewViewer, HuddleDiscussionView, conversation.ui.Message) pass neither prop and keep the old placement.
  • Both removed [data-activity-stopped] CSS rules were dead. local-agent-activity.spec.mjs:394 has no public reply, so its note stays on the request.

Not-inline note:

  • question/advice (attribution): top-level timeline replies have no reply link, so they count toward the latest human message (activity-presentation.ts ~179-186). Example: A asks R, B posts M, then agent X replies P. P counts toward M, so X's stopped note for R stays above M and P. Nothing is lost or doubled. In a busy channel, "after the reply" just does not happen. Worth a line under the PR's limitations.

representedAgents?: readonly string[] | undefined;
/** Agents with a later public reply to this request; their stopped notes
* follow that reply instead of this message. */
repliedAgents?: readonly string[] | undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convention, must-fix unless a human already approved it: this file is marked // FOUNDATION (line 1). The repo AGENTS.md requires explicit human guidance before editing FOUNDATION files. This PR adds repliedAgents and stoppedRequest to the plugin-facing ComposerAccessoryProps, and the description does not record that guidance. Please get it and note it in the description, or keep these props host-internal.

<>
<MessageActivity {...props} />
{request && author && (
<MessageActivity

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (UX): the stopped agent can show twice for a moment. When an agent that already replied is stopped, the request keeps its leaving bubble for the leave animation (leaving/retained, ~lines 244-262). The reply's moved note mounts at once, so the bubble and "X · Stopped" overlap briefly. Before, the note waited for the bubble to leave (bubble.length ? [] : …). This is cosmetic. Is it acceptable?

advice (perf): each agent's latest reply row also mounts a second MessageActivity. That instance subscribes to session.agentActivity and runs messageWork on every store change, plus the recorded-agents, names and profile hooks. In agent-heavy channels, this roughly doubles the activity work per agent row. Suggestion: mount the inner component only when the request has stopped work for that agent, or derive the stopped set once in the host presentation.

stoppedRequest={undefined}
stoppedAgent={author}
// Visibility follows the request's scope, not the reply row's layout.
threadScoped={

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (simplicity): this repeats the messageLayout === "thread" || threadRootId rule that visible() uses as its default (~lines 171-173), but evaluates it against the reply. A short comment, or computing threadScoped once in the outer component for both calls, removes the duplicated rule.

const rootActivity = rootId ? rootPresentation.byRow.get(rootId) : undefined;
// A stopped note may follow only a reply that is always shown: collapsed
// branches do not mount their rows.
const rootReplies = useMemo(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

advice (simplicity/perf): rootReplies repeats the computation that renderReplies(undefined) already runs (~line 1467): same rows, agents and rootId. The only difference is pinnedActivity, which affects only retained. It also runs the full activityRuns + activityStatus/requestWork pass just to read two maps. Suggestion: memoize the top-level presentation once and use it in both places, or extract only the public-reply map.

@tulsi-builder
tulsi-builder merged commit 97c959b into main Nov 4, 2026
41 checks passed
@tulsi-builder
tulsi-builder deleted the basil/stopped-notice-alignment branch November 4, 2026 20:14
wpfleger96 added a commit that referenced this pull request Nov 4, 2026
…-nip-fi

* origin/main: (23 commits)
  fix(inbox): show Solo responses under All, read live replies in the open conversation, describe toolbar badges (#2275)
  AIDA-2434: Show saved teams in mentions (#2339)
  AIDA-2455: Restore channel scrollbar dragging beside threads (#2436)
  feat(agent-protection): show the owner why a protection provider denied input (#2105)
  fix(search): show slow lookups beside their section headings (#2442)
  Align Bestie DM layout and unread state; speed up thinking shimmer (#2418)
  Send Claude, Pi and Goose history as exact records (#2088)
  fix(agent-activity): align the stopped note and show it after the agent's reply (#2441)
  fix(agents): stop orphaned Attention commands and prevent duplicate runs (#2448)
  feat(mentions): quieter mention menu that marks agents and their owners (#2328)
  feat: add a Canvas View tab, History diffs and a 256 KiB limit (#2318)
  feat(plugins): let local agents write and reload plugins (#2435)
  feat(onboarding): offer Create a community after public Builderlab sign-in (#2397)
  Open any Inbox filter view by route (#2394)
  fix(bestie): deleting an agent saves no Bestie record where there was none (#2420)
  fix(unread): never treat agents-audience messages as unread (#1552)
  feat(focus): add an optional Focus plugin that quiets everything except Solo chats (#2277)
  [5/5] Add protection to the Goose harness plugin (#839)
  feat(agents): identify plugins in native hook output (#2299)
  [4/5] Add protection to the Codex harness plugin (#1879)
  ...

# Conflicts:
#	src/features/identity/builderlab-hosted.test.ts
#	src/features/identity/builderlab-hosted.ts
gilgtm added a commit that referenced this pull request Nov 4, 2026
* origin/main:
  fix: exclude muted channels from sidebar unread scroll indicators (#2335)
  fix(claude-code): open long Claude transcripts a page at a time (#2428)
  feat(solo): share chats as view only (#2375)
  fix(inbox): show Solo responses under All, read live replies in the open conversation, describe toolbar badges (#2275)
  AIDA-2434: Show saved teams in mentions (#2339)
  AIDA-2455: Restore channel scrollbar dragging beside threads (#2436)
  feat(agent-protection): show the owner why a protection provider denied input (#2105)
  fix(search): show slow lookups beside their section headings (#2442)
  Align Bestie DM layout and unread state; speed up thinking shimmer (#2418)
  Send Claude, Pi and Goose history as exact records (#2088)
  fix(agent-activity): align the stopped note and show it after the agent's reply (#2441)
  fix(agents): stop orphaned Attention commands and prevent duplicate runs (#2448)

Signed-off-by: Gil Birman <[email protected]>
Co-authored-by: Claude <[email protected]>

# Conflicts:
#	src/app/shell/PageNavigation.tsx
damienrj added a commit to damienrj/buzz-app that referenced this pull request Nov 4, 2026
…-clone-auth-ab8b97

* origin/main: (38 commits)
  feat(identity): decide a missing backup choice from community membership (block#2378)
  Offer Builderlab decisions in FI builds (block#2227)
  feat(inbox): preview recent Inbox activity when hovering the sidebar row (block#2347)
  Add collapsible Solo and Team sidebar sections (block#2296)
  fix(codex): open long Codex transcripts a page of items at a time (block#2259)
  Keep Solo model and effort choices per chat (block#2342)
  feat(sidebar): smooth channel reordering with bounded motion (block#2108)
  Connect legacy agents from DMs and channel mentions (block#2423)
  fix: exclude muted channels from sidebar unread scroll indicators (block#2335)
  fix(claude-code): open long Claude transcripts a page at a time (block#2428)
  feat(solo): share chats as view only (block#2375)
  fix(inbox): show Solo responses under All, read live replies in the open conversation, describe toolbar badges (block#2275)
  AIDA-2434: Show saved teams in mentions (block#2339)
  AIDA-2455: Restore channel scrollbar dragging beside threads (block#2436)
  feat(agent-protection): show the owner why a protection provider denied input (block#2105)
  fix(search): show slow lookups beside their section headings (block#2442)
  Align Bestie DM layout and unread state; speed up thinking shimmer (block#2418)
  Send Claude, Pi and Goose history as exact records (block#2088)
  fix(agent-activity): align the stopped note and show it after the agent's reply (block#2441)
  fix(agents): stop orphaned Attention commands and prevent duplicate runs (block#2448)
  ...

Signed-off-by: damienrj <[email protected]>
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.

3 participants