Repository navigation
Conversation
Live agent work had several places it could show: a bubble under the request message, the end of the conversation, and the shimmer and Stop control on recorded coordination history. Mounted components negotiated which one showed through a presence registry. Mounted is not visible (the timeline keeps rows within 1600px mounted), so the same work moved as you scrolled, sometimes showed nowhere, briefly showed twice, and got stranded above the agent's own later replies. Now live work has one place: the end of the conversation it happens in, where its reply will land. The request message shows only what happened to it (failed, unconfirmed, stopped). Coordination history is history: it never animates and has no Stop. One rule decides which conversation work belongs to: a top-level request is its own conversation until it has replies, then it is a thread. The channel's end, the thread reply summary, the end indicator's Stop controls and the outcome caption all apply it. The presence registry (typing-presentation), useActivityIndicator, the live bubble and the history's live status projection are deleted. The history projection no longer reads live activity at all. Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz> Signed-off-by: Bradley Axen <baxen@squareup.com>
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3585388345
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| inThread(turn.threadRootId) || | ||
| turns.some( | ||
| (raw) => raw.turnId === turn.turnId && inThread(raw.threadRootId), |
There was a problem hiding this comment.
Use request roots when placing outcomes
When older harness telemetry omits threadRootEventId and lacks captured prompt context, entry.turns can still identify the conversation through its requests, but this predicate examines only turn.threadRootId and the transcript-derived root. After a top-level request gains a reply, its failed or unconfirmed outcome therefore remains visible on the channel copy instead of moving exclusively into the thread. Apply the same requestedRoot(turn) fallback already used by currentActivity and conversationWorkStops before calling inThread.
Useful? React with 👍 / 👎.
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One additional activity-placement regression.
| for (const evidence of working.filter((turn) => turn.agent === agent)) { | ||
| const turn = transcript.find((turn) => turn.turnId === evidence.turnId); | ||
| const root = turn?.threadRootId ?? requestedRoot(evidence); | ||
| const unthreaded = |
There was a problem hiding this comment.
🤖 [P2] Preserve hosted thread ownership when its root is outside the loaded window
When a hosted turn belongs to an older thread whose root is absent from channelThreadRoots, this classification treats it as channel work. A focused comparison using a working turn with threadRootEventId = oldRoot, a request {messageId: reply, threadRootId: oldRoot}, and a roots set containing only another root returned {label: "Working", roots: [null]} at this head; the base excluded it from the channel-only indicator. The corresponding TypingIndicator filter labels its Stop control as channel-scoped and sends the conversation link to the channel rather than the thread.
Absence from the loaded window is not evidence of a top-level request without replies. Require positive evidence for channel ownership, preserve known reply/thread ownership, and apply the same predicate to the Stop filter. Add regression coverage for roots outside the loaded window.
|
🤖 Drip design review at The intent is to give current agent work one predictable home, with its transcript and Stop at the conversation’s end. This fits the activity bug-fix route. Must fix — identify which request Stop affects. With two live top-level requests to the same hosted agent and neither having replies, the Working now popup produces two rows with the same agent, aggregate status and “Stop” label, but different stop targets. The new channel filtering removes their roots from presentation, and the row/control rendering cannot distinguish them. Keep a short request preview beside each distinct control and in its accessible name, independently of channel/thread classification. Cover two requests for one agent with identical current steps. Design preference (blocks merge) — work visibility while reading history. A running request now has no receipt, and its live row/Stop scroll out of view. The existing Jump to latest only reports new messages. @mahanti: choose whether that tail-only experience is acceptable, or whether the existing Jump control should indicate running work and lead to its controls. I prefer the latter; it preserves one live row without making readers guess whether a request is being handled. Limits: source review only; no supplied screenshots, app or tests run. Rendered themes, widths, enlarged text, keyboard/focus, reduced motion and real local/remote lifecycle behavior remain unverified. |
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account, integrating Rocket, Mantis and Groot.
One P2 correctness finding is inline: after raw activity eviction, a still-working hosted turn can lose its only conversation Stop control. The simplification otherwise keeps the existing conversation, history and outcome owners.
Optional follow-ups:
- Cover retirement while keyboard focus is inside the end indicator’s Stop popup. The new browser case covers the trigger and composer;
useActivityFocusHandoff(trigger)does not include the portalled popup. Browser behavior in that case remains unverified. - Align the outcome-scope documentation with legacy turns that lack a recorded root:
MessageOutcome.visible()does not userequestedRoot, although the new docs say outcomes follow that fallback rule. This behavior predates this PR.
Reviewed head 35853883457e0004ac95c196384608b077e240ff, target b6289eb5990e7cfe3f0d4f76b23ab5a68180a288, merge-base c036cb080dd3011de251397fa61eb83e8a7881f9. Source-only: no tests, builds or app workflows were run; the author’s reported validation was not independently verified. Public description inspected; it has no attached images. This is a COMMENT review, not approval.
| typeof turn.threadRootId === "string" && | ||
| (!threadRootId || turn.threadRootId === threadRootId) && | ||
| snapshot.turns.some( | ||
| turns.flatMap((turn) => { |
There was a problem hiding this comment.
🤖 [P2] Preserve Stop for retained hosted turns after raw-buffer eviction
This iterates only turns reconstructed from snapshot.records. That buffer is globally capped at 200 records / 2 MiB (activity.ts:13–14,427–433), while snapshot.turns deliberately retains fresh lifecycle and request identity separately. If other activity evicts all raw frames for a still-working hosted turn, currentActivity can still show it via requestedRoot(evidence), but this loop never visits it and returns no Stop control. The request/history controls removed here previously used requestWork → requestActivity, which merges retained turns and survived the same eviction, so moving all controls to the end makes this a user-visible loss.
Build the hosted controls from working retained turns, augmenting them with any raw transcript, and resolve the root from the retained threadRootId or requestedRoot when raw scope is absent. Preserve terminal/capability checks. Add coverage for a fresh working turn whose raw frames have been fully evicted but whose retained scope remains known.
Summary
Live agent work now has exactly one place: the end of the conversation it happens in, where the agent's reply will land. The message that started the work shows only what happened to it.
Before this change, an agent's work could show in three places:
Mounted components decided which one showed by registering with each other (
typing-presentation.tsx). But mounted isn't visible: the timeline keeps rows within 1600px mounted. So the same work moved as you scrolled, sometimes showed nowhere, briefly showed twice, and got stranded above the agent's own later replies with a second avatar and name. Recent fixes (#2454, #2613, #2851) each patched the handoff between two of those places. This PR removes the handoff instead.The rules
Live work is shown only at the conversation's end. The timeline's end shows the channel's work and a thread's end shows that thread's. The end shows dots, the current step, the transcript link and Stop, and stacks under the agent's latest reply. Nothing negotiates where it goes, and nothing depends on what is mounted or scrolled into view.
A message shows only what happened to its request: Couldn't finish (with Retry or dismiss), a result Buzz couldn't confirm, or Stopped. Once the agent has replied, that note follows its latest reply (unchanged).
Coordination history is history. It never animates and has no Stop. It keeps its transcript links.
One rule decides which conversation work belongs to: a top-level request is its own conversation until it has replies, and then it is a thread. These all apply the same rule:
currentActivity, channel-only view)threadWorkingAgents)ActivityAccessory)Before, a top-level request's turn, which harnesses root at the request itself, counted as "thread" work for some of these and as "channel" work for others.
What changed
Production diff is roughly +340/−1590.
Deleted:
typing-presentation.tsx: the presence and replacement registry, plus its providers inConversation,MessageRowandMessageComposeruse-activity-indicator.tsActivityAccessory, with its leave animation, typing replacement and Stoptyping-presentationcurrent-activity.tsandactivity-presentation.ts, which only the bubble usedactivity-presentation.tsis now layout only. It no longer reads the activity snapshot:retired,working,liveParticipants,ownsLiveandactiveare gone. History no longer re-renders on activity updates.ActivityGroupActionskeeps only the transcript links. Its Stop control is removed.TypingIndicator:presented/replacedfiltering.currentActivity/conversationWorkStops: when a turn's records name no thread root, they fall back to the root of the turn's triggering messages (requestedRoot). Older harnesses omitthreadRootEventId, and the bubble used to cover that case by message ID.Docs:
docs/agents.md,docs/buzz-help.md, and the plugin's README and description.Tests
ActivityAccessory.test.tsxis reorganized around the rule.main(c036cb0) and pass here.vitest run: 1028 files pass.lintanddesign:checkare clean.local-agent-activity.spec.mjs: 7 journeysagent-activity-overhaul.spec.mjs: the hosted-thread, avatar-stack and placement journeysthread-resize.spec.mjs: bottom-follow and reading-position checks as work arrives at the endnested-replies.spec.mjs: branch hooksNot in this PR (follow-ups)
Running a command) is coarser than the bubble's (Run command · cd …). Local agents are unchanged.ComposerAccessoryProps.representedAgentsis unused now, butcontracts.tsis FOUNDATION, so it is left in place.Checklist