Repository navigation
fix(agent-activity): align the stopped note and show it after the agent's reply - #2441
Conversation
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]>
…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
left a comment
There was a problem hiding this comment.
🤖 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"); |
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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]>
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 A stopped note can disappear when its thread reply uses continuation layout.
| repliedAgents={undefined} | ||
| stoppedRequest={undefined} | ||
| stoppedAgent={author} | ||
| /> |
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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
left a comment
There was a problem hiding this comment.
🤖 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), | ||
| )} |
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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} |
There was a problem hiding this comment.
🤖 [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.
There was a problem hiding this comment.
🤖 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.
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
left a comment
There was a problem hiding this comment.
🤖 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.
activityRunsfolds onlyaudience=agentsrows, 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:394has 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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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={ |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
…-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
* 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
…-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]>
What
When you stop an agent, the "Agent · Stopped" note that replaces its activity bubble:
Change
.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 inMessages.module.cssno longer matched anything since Unify agent activity with incoming message previews #1729 and are removed.activityPresentationnow 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).stoppedRequestand renders that agent's stopped note after itself.Validation
vitest run src/features/messages src/features/conversation src/bundled/agent-activity: 113 files / 2,766 tests pass.tsc --noEmitand Biome are clean.vitest runon 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 unchangedorigin/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