Skip to content

feat(desktop): agent activity focus view - #6911

Open
baxen wants to merge 14 commits into
mainfrom
ss-dev-00/focus-view-consolidated
Open

feat(desktop): agent activity focus view#6911
baxen wants to merge 14 commits into
mainfrom
ss-dev-00/focus-view-consolidated

Conversation

@baxen

@baxen baxen commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Agent activity focus view

Turns agent activity into a full-cover, read-only conversation view for one agent in the current channel, with the detail and hierarchy needed to inspect a complete turn without leaving Buzz.

This supersedes #6542, #6538, #6720 and #6536 — per baxen's call to ship the stack as one PR. Those four are left open; ss-core-02 handles their disposition after baxen retests.

Major changes

  • Expands agent activity into a full-cover focus drawer, scoped to one agent + channel and intentionally read-only.
  • Restyles the raw activity stream as a conversation transcript with clearer prompts, authored answers, timestamps, and turn boundaries.
  • Groups thinking, tool calls, plans, interim notes, and failures into whole-turn work blocks that fold for scanning and expand for inspection.
  • Gives human messages and agent buzz messages send output distinct bubble treatments, including Open in chat navigation where a source Buzz message exists.
  • Coordinates the focus surface with the underlying drawer, including Escape handling and edit-state guards so closing/navigation does not fight active editing.
  • Adds a calmer, purpose-built code-block treatment for focused reading.
  • Removes repeated per-answer agent identity; the persistent header now owns attribution, avoiding visual collision with agent-authored Buzz message bubbles.

Commits

Commit Origin
6c9b0e2fe cover the channel with the agent activity drawer slice A, #6542 @ 105a3654d
cd2037e05 conversation transcript variant for focus-mode sessions slice B, #6538 @ 5e3faf443
29412d6ad bring the conversation variant closer to berd's recipes restyle, #6720 @ fd2e01799
90f526be6 fold focus-mode agent work into one transcript block slice C, #6536 @ 91a5754ad
403de2621 pin the conversation transcript variant to the cover drawer was preview-only glue; rewritten as real product code
8cea5e15e extract the channel idle auxiliary surface new here — see below
ef60f0cc190213ba6a (6) tho's polish, cherry-picked with -x from tho/agent-activity-focus-view @ dad000df4
23244d317 let a cover drawer replacement overlap on mount new here — fixes reconciliation 3 below

Every file tho's polish touches is byte-identical to his tip — verified per file. His branch is based on an older main, so folding it in as a merge would have reverted main's advances; the six polish commits are cherry-picked instead, leaving the rest of his branch (a rebase of this same stack) out.

Three semantic reconciliations against main

Slice A's base has moved, so these needed real resolution rather than a mechanical apply. Each is commented in the code so a reviewer doesn't have to re-derive it:

  1. ChannelPane.tsx — two cover-drawer owners. Kept slice A's single-covered-slot resolvers (resolveChannelAuxiliarySurface / resolveChannelCoverDrawer / useCoverDrawerPresence) and main's caller-owned idle-auxiliary cover drawer. Exactly one cover drawer stays possible: the idle panel sits outside auxiliarySurface, is not in the last-opened-wins set, and shouldUseFocusIdleDrawer requires every other candidate absent. useCoverDrawerPresence now takes coverDrawer !== null || useFocusIdleDrawer, with a four-way close-handler chain preserving main's priorityIdleAuxiliary precedence.
  2. CoverDrawer inherits main's outline-hidden. The drawer is tabIndex={-1} and focuses itself only to land the keyboard inside, so a ring around the whole surface is noise.
  3. Dropped main's mode="wait" on AnimatePresence (23244d317). I first kept it and that was wrong: overlapping mount is load-bearing for a cover drawer. Replacing one cover surface with another has to stay dismissable with a single Escape, and the guards that achieve that — CoverDrawer's capture-phase claim standing down while exiting, and the exiting panel declining to preventDefault — only have something to coordinate while both surfaces are mounted at once. Under mode="wait" the outgoing drawer unmounts before the incoming one enters, so the press lands on nothing and a second Escape is needed. agent-activity-cover.spec.ts:349 asserts the overlap directly and failed 3/3 in CI until the attribute came off. feat(projects): add agent and CLI project-home support #6590 added it to serialize projects-panel replacements; verified no regression there (111 drawer/focus-mode/thread e2e + 13 projects e2e green), and tho's visually-approved tree never had it — his base predates feat(projects): add agent and CLI project-home support #6590.

Also took main's side where it had advanced independently: the useRoutedMessageEdit extraction (slice A never touched routed-edit logic), ChannelGlyph replacing bare Hash, and #6900's token removal. Dropped focusedThreadCloseRequest and useFocusDrawerPresence (renamed useCoverDrawerPresence) after confirming no remaining callers in the tree.

ChannelIdleAuxiliarySurface — why there's a refactor commit

Landing the cover drawer on current main put ChannelPane.tsx at 1003 lines against the 1000-line cap. This is a genuine ratchet violation: the file was already over on slice A's original base (1010, so shrinking was permitted), but main has since brought it down to 982, so it may not grow at all. Extracting the idle auxiliary surface — the same split ChannelAgentSessionSurface already uses — brings it to 996.

One trap worth flagging for review: moving that JSX into a component initially moved the React key off AnimatePresence's direct child. Main deliberately uses different keys per presentation ("idle-auxiliary-surface" for the cover drawer vs. "idle-auxiliary-panel" for the split pane), so a single key on the wrapper would have changed exit/enter identity and broken the drawer's travel direction on a viewport change. The keys stay at the ChannelPane call site, matching the existing THREAD_SURFACE_KEY / AGENT_SESSION_SURFACE_KEY precedent.

Verification

At 23244d3171577be12abe2f4469c1f1a4cc2551f0, read in the same shell as git rev-parse HEAD, tree clean:

  • Full desktop suite (not scoped): 5800 passing / 0 failing / 83 suites, ~99s. Bar was ≥5526.
  • tsc --noEmit clean; pnpm check (biome + px-text + pubkey-truncation) exit 0; file-size ratchet clean.
  • All pre-push lanes green: check-push-org, push-head-scope, branch-skew, file-size-check, desktop-check, desktop-typecheck, desktop-test.
  • tho reported one useRetainedProjectGitViews.test.mjs failure on his tip; it did not reproduce in either full run here.

Deliberately excluded

ss-dev-00 and others added 12 commits August 26, 2026 15:25
Slice A of the focus-mode agent conversation. Agent activity gets the same
full-cover presentation threads already had in focus mode, because its
transcript is tool calls, diffs, and command output — content a 380px side
pane cannot show usefully. It deliberately offers no split/focus toggle: at
any viewport wide enough for two panes it always covers.

The drawer surface is extracted from `FocusThreadDrawer` into a
presentation-only `CoverDrawer` that knows nothing about what it covers, so
the thread drawer and the activity drawer are one surface with two contents.
Motion, scrim, Escape ownership, and focus capture/restore all live there.

"Which panel" and "which presentation" now resolve together in
`channelAuxiliarySurface`, which is what makes the two cover drawers mutually
exclusive: there is one covered slot and the resolved surface owns it.

Two ordering hazards the surface has to handle, both covered by tests:

- Escape during a replacement. `AnimatePresence` keeps the outgoing drawer
  mounted through its exit, capture-phase listeners fire in registration
  order, and the outgoing one registered first — so it would swallow the press
  via `stopImmediatePropagation` and the user would have to press Escape
  twice. The drawer stands down for its whole exit, gated on presence rather
  than the focus slot: a successor whose content takes focus never claims the
  slot, which would leave the outgoing claim current for exactly the drawer
  that should stand down.
- `StrictMode` replaying the focus-capture effect. By the replayed setup the
  drawer has already focused itself, so an unconditional
  `document.activeElement` read records the drawer as its own opener and a
  real close focuses a detached node, stranding the keyboard on `<body>`. A
  one-shot ref captures the opener exactly once.

Reconciled against main, where the idle auxiliary panel gained its own
full-cover presentation (#6590):

- `FocusThreadDrawer` keeps main's `label` prop and passes it through as
  `CoverDrawer`'s `ariaLabel`; the idle panel reuses the presentation with its
  own name, which is why the label is a prop and not the thread's constant.
- `outline-hidden` carries onto the extracted surface with the reason it
  exists recorded — the drawer is `tabIndex={-1}` and focuses itself, so the
  ring is noise.
- `useFocusIdleDrawer` sits outside `auxiliarySurface` on purpose: the idle
  panel is caller-owned, is not in the last-opened-wins set, and
  `idleAuxiliaryOverridesThread` lets it render ahead of a thread. It still
  cannot contend for the covered slot, because `shouldUseFocusIdleDrawer`
  requires every other candidate to be absent.
- main's `mode="wait"` on the presence boundary is kept. It closes the window
  where two drawers are mounted together, but the drawer's presence and
  focus-slot guards stay: they protect the surface, not this host's choice of
  mode.

Squashed from ss-dev-00/agent-activity-cover (#6542) at 105a365.

Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
…nt sessions

Slice B. A third, purely additive `conversation` transcript variant for the
full-cover reading surface: the human's prompt as a right-aligned bubble, the
agent's reply as unboxed prose at a comfortable measure, reasoning behind a
"Thinking… / Thought for Ns" disclosure, the plan as a live checklist, and
session/status noise receded to quiet dividers.

The transcript previously had two presentations — the dense `default` activity
feed and `compactPreview` — both tuned for a 380px panel. Read at full width
they are a scan surface, not something you read a turn in.

Threaded through `ManagedAgentSessionPanel` and `AgentSessionThreadPanel` as
an optional prop. Tool items deliberately route through the same
`ToolActivity` presenter as `default`, which is what lets the tool-chain card
work in slice C land on top without conflict.

The byte-for-byte guarantee for the existing two variants is enforced by a
captured-markup fixture rather than by inspection: the baseline was produced
by mounting the same transcript on unmodified code, so the test fails on any
drift in `default` or `compactPreview` output.

Three defects found and fixed while building it, each with a
non-vacuous test:

- The author row resolved its name through `resolveUserLabel`'s
  `fallbackName: item.title`, whose value describes the *trigger* that started
  the turn ("@Mention", "Prompt", a title-cased Buzz event kind) and is never
  an identity. Harmless where it only seeded avatar initials; the conversation
  author row promotes it to displayed text, so an unresolved sender rendered
  as if "@Mention" were their name. Identity now stops at the profile —
  display name, NIP-05 handle, truncated pubkey — and the title stays as
  trigger chrome in the footer. The existing test was asserting the bug.
- The captured-markup fixture embeds formatted timestamps, so it was
  zone-sensitive; pinning `TZ` then exposed that it is locale-sensitive too.
  Assigning `process.env.LANG` at runtime does not fix it — node resolves its
  default locale once at startup, unlike `TZ`, which `Date` reads lazily. Both
  formatting surfaces the render path reaches are overridden, including
  `Date.prototype.toLocale*`, which does not route through
  `Intl.DateTimeFormat`. `Intl.DateTimeFormat` must be replaced with a plain
  function, not an arrow: the render path calls it with `new`.

Squashed from ss-dev-01/conversation-variant (#6538) at 5e3faf4.

Co-authored-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
A design-delta note measured the conversation variant against berd's shipping
chat surface and named three divergences that made the focus view read as "not
berd" regardless of how the rest was arranged. This takes all three literally,
on Buzz tokens rather than berd's.

**Agent identity row.** The largest gap: agent prose carried no attribution at
all, so a reply read as unowned body text in a full-cover view. berd labels
every agent turn with a 20px round avatar plus the name at `text-xs`. Added for
the `conversation` variant only — a test asserts the row does not leak into the
other variants, so a failure names the cause instead of printing a markup diff.

**Prompt bubble.** berd's user turn is a soft fill with no border, `px-4 py-2`,
12px radius. berd's `rounded-sm` is 12px on its own scale, not Tailwind's stock
2px, so Buzz's `rounded-xl` is the exact equivalent. The cap moves from
`max-w-[85%]` to a fixed 640px measure mirroring `--chat-user-message-max-width`:
a percentage cap re-wraps the prompt on every resize of the cover, a fixed
measure holds one stable line length.

**Fenced code.** The language moves into a real header row above the frame with
the copy action opposite it, framed at Buzz's `rounded-lg` (`--radius: 0.625rem`,
exactly berd's value).

The markdown renderer is shared with channel messages, so the code recipe is
opt-in through `CodeBlockVariantContext` read at render time. A prop would have
to thread through `createMarkdownComponents`, whose component map must stay
module-stable and whose parsed-node cache keys on a variant string — the same
reason `VideoReviewMarkdownContext` already exists. A context provider renders
no DOM, so `default`/`compactPreview` markup stays byte-identical.

Three defects fixed on top of the restyle, each found by review or a bug pass:

- The code-recipe provider was mounted inside `MessageActivity`, which only
  handles assistant items — `UserMessageBubble` returns before it, so a fence
  inside a human prompt kept the legacy 16px muted frame nested inside the new
  12px bubble. The recipe is a property of the *surface*, not of a role, so the
  provider moves up to `AgentSessionTranscriptList`.
- The identity row read the raw `agentAvatarUrl`/`agentName` props, and the
  primary channel-opened flow passes `agentAvatarUrl: null` because
  `ChannelAgentSessionAgent` carries no avatar field — so every channel-opened
  focus session showed initials in the identity row while the header directly
  above it showed the real profile avatar. Now resolved profile-first out of the
  `profiles` lookup the panel already hands down, with the props kept as
  fallback: a locally managed agent can hold an avatar its relay profile never
  published.
- `UserAvatar` names itself (`<img alt="${displayName} avatar">` or its
  fallback initials) and the identity row puts the same name in visible text
  right after it, so assistive tech read the identity twice for every agent
  turn. The avatar is wrapped `aria-hidden`, verified in Chromium's real
  accessibility tree over CDP (`ignoredReasons: ["ariaHiddenSubtree"]`), not
  from DOM attributes. Marked at this call site rather than teaching the shared
  `UserAvatar` a decorative mode, which would affect all 45 of its call sites.

The conversation test file had reached 1253 lines, past the 1000-line ceiling.
It slipped the `file-size-check` gate because that script only scans
`.ts`/`.tsx` under `src/features` — the rule in AGENTS.md plainly covers it.
Split into a shared non-test harness module (following the existing
`observedUnreadTestHarness.mjs` precedent, because duplicating the
order-sensitive TZ/locale pins into two files is exactly how they drift) plus
two test files, bodies moved verbatim.

AGENTS.md gains the `assert.ok(x === null)` rule earned here: the
`assert.equal(el, null)` form serializes the matched element and its entire
subtree to build a diff when it fails, which on a real transcript exhausts
memory and kills the runner with SIGKILL after ~100s — a genuine regression
becomes unreadable and looks like an OOM in unrelated code.

Squashed from ss-dev-01/berd-restyle (#6720) at fd2e017.

Co-authored-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
Slice C. Focus mode gave every thought and tool step its own row and its own
disclosure, so a turn that did real work scrolled its answer off screen behind
a stack of chrome. This adopts berd's shipping transcript model: within a turn,
everything between the prompt and the answer that is thinking, a tool step, or
an interim agent note becomes one "work block" — a thin rail of small glyph
bullets that folds to a single "N steps" line when the work is done.

Grouping is variant-aware **at the list boundary** rather than inside the
grouping module: `TranscriptDisplayBlockView` runs the additive work-block
transform only for `conversation`, so `default` and `compactPreview` walk the
same segments on the identical code path they always did. That makes slice B's
byte-for-byte baseline fixture hold by construction rather than by assertion —
no future edit to the block can regress the other two variants without first
moving that branch.

Behaviour, mirroring berd:

- Live: the block is open and the rail IS the status, so there is no header
  line restating it. The last three steps show in arrival order; older ones go
  behind an "N previous steps" disclosure, so a long run cannot push the answer
  out of view.
- Finished: folds to "N steps" with a chevron that rotates on open.
- The fold animates. `<details>` cannot — its content is either laid out or
  not, with no intermediate height — so a block that was live when it mounted
  renders open for a paint and then settles closed, giving the height tween a
  start state that was actually painted. A block already finished on mount
  (scrollback) never had a rail on screen and closes immediately.
- Reader choice wins: once the reader toggles a block, policy stops opening and
  closing it.

One deliberate departure from berd: a finished block holding a failure folds to
"N steps · 1 failed". A bare count is the one thing a reader cannot distinguish
a clean run from a broken one by, and a fold that hides a failure behind a
neutral number invites them not to open it. The rail bullet itself stays muted —
failure is a glyph shape, not a colour, so one bad step does not read as an
alarm across the run.

Items are projected once into rail entries, and the glyph, the body, and the
folded line's counts all read that projection; two independent classifications
of the same item is how a headline and chain eligibility drifted apart on an
earlier attempt. The projection is closed in the type system rather than by
convention, so a future `TranscriptItem` variant cannot silently wear a wrench
and impossible pairs are not representable.

Four defects fixed on top, each mutation-checked in isolation:

- **A reader's open block was folded shut by the agent posting again.** Work
  blocks are derived and ided after their first step, and only the LAST
  assistant message is exempt from the block — so a second answer demotes the
  first, the runs on either side merge, the old block unmounts, and the
  `useState` holding the reader's expansion goes with it. The choice now lives
  above the block, keyed by the STEP ids it was taken on, since the steps
  survive regrouping. An open choice beats a folded one (hiding steps a reader
  asked for loses what they were reading; showing extra costs a scroll), and
  the choice is recorded against every step in the block — recording only the
  first leaves the absorbed block's stale `open` entry behind and, since open
  wins, the merged block could never be folded again.
- **A finished block went live again between turns.** `lastTurnId` walked the
  display blocks, but a turn that has only emitted setup lifecycle rows
  classifies to zero segments and produces no block — so for the whole gap
  between `turn_started` and the next turn's first renderable item, "newest
  turn with a block" was the turn that had already ended, and its trailing item
  became the streaming item: a settled 6-step block re-opened, dropped to its
  last three steps, then folded back. Every turn, not an edge case. Liveness
  now comes from the newest turn id in the item stream, and a trailing item is
  only reported as streaming when the live turn owns it.
- **The running step's pulse ignored reduced motion.** `WorkBlockRailGlyph`
  used bare `animate-pulse`, which no `prefers-reduced-motion` block in the app
  matches — all 20 are scoped to `buzz-*`/`motion-*`/`t-skel-*`. Now
  `motion-safe:animate-pulse`. That swap also repaired three assertions it
  would otherwise have disarmed: they matched `.animate-pulse` as a substring
  and, worse, all three ran on a folded rail with no rows, so "nothing pulses"
  held against a build where abandoned steps pulse forever.
- **A work block whose running step had no live session never settled.**

Two test-hygiene fixes worth keeping: a gap test was pinning a sequence whose
`session/new` card moved `streamingItemId` on its own, so those frames passed
against the pre-fix code and proved nothing (the plain no-restart path now
leads); and the orphaned suite inherited React Query's default 300000ms
`gcTime`, arming a five-minute timer at teardown that node:test waited out —
`gcTime: 0` takes the full desktop suite from 307s to 102s, verified against a
negative control rather than on the passing tree alone.

Squashed from ss-dev-02/tool-chain-cards (#6536) at 91a5754.

Co-authored-by: ss-dev-02 <9e916f802f7932c38e630ac6b4726f5db7ca326c0d98e9bab495309eef13fe5a@buzz.block.builderlab.xyz>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
…rawer

The last wire of the feature, and what makes the four slices above one thing:
the cover drawer now renders the conversation transcript, so opening agent
activity in a channel lands in the reading view rather than the dense activity
feed. Resolves the `TODO(#6538)` slice A landed with.

The variant is chosen by presentation, not inferred from panel width. Width was
only ever a proxy, and a breakable one — the split pane can be dragged wide and
a narrow overlay can be tall. The presentation that already decided to cover is
the thing that knows this is a reading surface, so it decides the variant too.

Non-cover hosts return `undefined` rather than `"default"`: the panel already
defaults, and naming it here would imply this function decides the non-cover
variant when the profile panel picks `compactPreview` at its own call site.

Asserted end-to-end on the transcript's `data-transcript-variant` marker rather
than on the prop, so the test proves the value survives the whole path from the
resolver through `ChannelAgentSessionSurface` and the panel to the transcript.
It lives in the screenshot spec because the marker only renders once the
transcript has content, and that is the spec that seeds a turn. Verified
discriminating: reverting the pinned value alone fails the assertion.

Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
Landing the cover drawer on current main pushed `ChannelPane.tsx` to 1003
lines, three over the 1000-line cap. The file was already over the cap on
the cover drawer's original base (1010 lines, so shrinking was permitted),
but main has since brought it down to 982, so it may not grow at all.

Move the caller-owned idle auxiliary surface into its own component, the
same split `ChannelAgentSessionSurface` already uses, which brings the pane
back to 996. Behaviour-preserving: the panel and both of its presentations
are byte-equivalent to the inline version, and the per-presentation React
keys stay on `AnimatePresence`'s direct child rather than moving inside the
new component, so the cover drawer and the split pane keep the distinct
enter/exit identities main gave them.

Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>

Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
(cherry picked from commit 62b9603)
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>

Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
(cherry picked from commit 07344db)
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>

Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
(cherry picked from commit 0ec8f16)
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
(cherry picked from commit 20f32f5)
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
(cherry picked from commit a0da52a)
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
(cherry picked from commit dad000d)
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90213ba6af

ℹ️ 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".

Comment on lines +29 to +31
for (const line of text.split(/\r?\n/)) {
const match = line.match(CHECKLIST_LINE);
if (!match) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve non-checklist plan lines

When a free-form plan mixes checklist items with explanatory text, or an ACP checklist entry contains a multiline description, every continuation/non-checklist line is discarded here. Because ConversationPlanCard switches to the parsed list as soon as any entry exists, those lines never reach its Markdown fallback, so the wide agent-activity view silently truncates valid plan details. Preserve unmatched/continuation text, or only use the structured rendering when the entire plan can be parsed without loss.

AGENTS.md reference: AGENTS.md:L11-L18

Useful? React with 👍 / 👎.

ss-dev-00 and others added 2 commits August 26, 2026 16:22
Reconciling slice A against current main took main's `mode="wait"` on the
`AnimatePresence` that hosts the auxiliary surfaces, added by #6590 to
serialize replacements. That was the wrong side to take: overlapping mount is
load-bearing for the cover drawer.

Replacing one cover surface with another must stay dismissable with a single
Escape, and the guards that achieve it — `CoverDrawer`'s capture-phase claim
standing down while exiting, and the exiting panel declining to
`preventDefault` — only have something to coordinate while both surfaces are
mounted at once. Under `mode="wait"` the outgoing drawer unmounts before the
incoming one enters, the press lands on nothing, and the user needs a second
Escape.

`agent-activity-cover.spec.ts:349` asserts the overlap directly and failed
deterministically (3/3 CI retries, reproduced locally); removing the attribute
turns it green. Verified no regression in the behaviour #6590 was protecting:
the drawer, focus-mode, thread and projects e2e specs pass (124 tests), and
the comment now records why the attribute must not come back.

Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
… variant

`observer-feed-screenshots` 09 asserted the `available_commands_update`
lifecycle row as `getByText("Commands", { exact: true })`. That located the
dense variant's separate title span. The observer feed ("View activity") is
hosted by the agent activity cover drawer, so once the pin commit made the
drawer render the `conversation` variant, a plain lifecycle status recedes to a
`ConversationDivider` whose label is the joined `title · text` — and the exact
text node stopped existing.

The row is NOT dropped: the failure screenshot shows "Commands · Commands
available: 3" rendered as the centered divider. So this is a stale assertion
against the new presentation, not lost content, and the pin is correctly scoped
— the drawer IS the reading surface here.

Replaces the bare text node with three assertions that are strictly stronger:
the lifecycle row exists, it carries `data-variant="conversation-divider"`, and
its full text is the joined label. A variant flip and a dropped detail half now
each fail, where the old assertion could only see one span. Tests 08/10 in the
same spec never caught this because their `getByText` calls are non-exact and
match the joined label by substring.

Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
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.

1 participant