feat(desktop): fold focus-mode agent work into one transcript block - #6536
feat(desktop): fold focus-mode agent work into one transcript block#6536baxen wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 643b310690
ℹ️ 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".
| function isToolStepRunning(item: ToolItem): boolean { | ||
| return item.status === "executing" || item.status === "pending"; | ||
| } |
There was a problem hiding this comment.
Gate running state on live session ownership
When an agent crashes or disconnects after emitting a tool start but before its terminal update, the archived item permanently retains executing or pending. This status-only check therefore marks the card as running whenever that history is reopened, even though AgentSessionTranscriptList already knows there is no matching active turn/live session; the card displays a spinner and ToolRunLiveElapsed keeps increasing from the original timestamp indefinitely. Gate this phase on current turn/session liveness, or terminalize orphaned steps so dead work is shown as stopped or timed out rather than actively running.
AGENTS.md reference: AGENTS.md:L13-L18
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Measured at 9c2900e9b rather than reasoned from the code, with a throwaway JSDOM probe against AgentSessionWorkBlockSegment (deleted; not in the tree). The question survives the rewrite, the answer is "spins forever", and it is worse in the work block than it was on the card.
agentSessionToolRunSummary.ts and ToolRunLiveElapsed are gone with the tool-chain card, so the climbing elapsed timer half of this comment no longer applies: the block reads duration through getToolDurationDisplay, which needs both startedAt and completedAt (or a duration in the result), so an orphan shows no duration at all — no ticking counter.
The running-state half is intact and amplified, because the block's status is now derived from the same unguarded check:
desktop/src/features/agents/ui/agentSessionWorkBlockGrouping.ts:267 toolEntryState() → status "executing"|"pending" ⇒ "running"
desktop/src/features/agents/ui/agentSessionWorkBlockGrouping.ts:332 summarizeWorkBlock → any "running" entry ⇒ isActive: true
isActive is not just a per-row glyph, it is the block's live-vs-finished policy. Reopening history that contains one orphaned executing step therefore produces, measured:
| observation | orphan present (no live turn) | all steps settled |
|---|---|---|
folded summary line (N steps) |
absent — isActive suppresses it |
present |
| rail rows after animations settle | 2 of 2, never folds | 0 (folded) |
data-step-state |
["settled","running"] |
["settled","settled"] |
.animate-pulse nodes |
1, indefinitely | 0 |
So reopened history renders as work in progress: no summary line, no fold, and a pulsing bullet. On a 7-step history with step 4 orphaned, the live window also engages — visible steps: 3 of 7, with 4 previous steps behind a nested disclosure, and it never settles. A reader looking at finished history is shown three steps and a spinner, with four hidden.
For contrast I rendered the same item through TranscriptActivityItem outside a block: default-variant pulse nodes: 0, text "Ran d / Waiting for tool details." — a plain muted row. The regression is specific to this PR, because the block is the first consumer that turns tool status into a presentation mode rather than a row label.
The evidence to gate on is already threaded into this component and currently discarded: buildConversationTurnMeta (agentSessionConversationMeta.ts:47) returns EMPTY_TRANSCRIPT_TURN_META for a non-live turn, so the block cannot distinguish "turn not live" from "live but nothing streaming" — both arrive as streamingItemId: null. The minimal correction is to carry isTurnLive in the turn meta and require it before a running entry may set isActive, leaving the glyph free to show a dead step as stopped rather than executing. Flagged to the tech lead for a C-or-follow-up call; not fixed in this push.
Confirmed: not stale. Leaving the thread open.
|
Latest revision and validation:
The PR description now explicitly records the intentional flat threshold change from 3 to 2 and that @ss-core-02 |
|
Validation complete at
The PR description now explicitly documents the intentional flat grouping threshold change from 3 to 2 and One independent P2 review thread remains open about orphaned executing/pending history after a crashed or disconnected session. That is a separate liveness-ownership policy question; I have left it visible for review rather than silently changing the scope of the card implementation. @ss-core-02 |
74b9015 to
e875102
Compare
e875102 to
a05347d
Compare
Revision: rebased onto #6720, and a bug the browser caughtHead: Relay posts are plain rail steps nowCaught by looking at the rendered preview, not by a unit test — the JSDOM suite was green while the rail was visibly wrong. A This is the same failure core-02 flagged for interim notes, reached by a different route: there the item is an assistant message, here it is a tool call that merely classifies as one. Fixed with an explicit presentation signal ( Before / after, same seeded turn:
Fold animation now asserted in a browserPer the quality list: the fold must animate, and a unit test can only see end states. The preview spec samples the panel height per frame while it closes and asserts at least one height strictly between full and zero — which a Mutation-checked: setting Full states
Plan sits as a sibling after the block; the failed step is inside it and named in the folded line; the answer's prose and fenced code are unaffected. Verification at
|
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 main's 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, which is the property worth having
here: no future edit to the block can regress the other two variants
without first moving this branch.
Behaviour, mirroring berd:
- Live: the block is open and the rail IS the status, so there is no
header line to restate it. The last three steps show in true arrival
order; older ones go behind an "N previous steps" disclosure at the top
of the rail, 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 do this — its content is either
laid out or not, with no intermediate height — so the collapse would
snap. 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, so the animation stays
meaningful instead of firing on every mount.
- Reader choice wins: once the reader toggles a block, policy stops
opening and closing it.
One deliberate departure from berd, per core-02: 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, per berd — failure is a glyph shape,
not a colour, so one bad step does not read as an alarm across the run.
## A closed entry type, so a row's kind has one answer
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 precisely how the headline and the
chain eligibility drifted apart on the abandoned tool-chain card, so both
render sites are exhaustive switches over the entry and neither asks
`item.type` again.
The projection is closed in the TYPE SYSTEM, not only by convention. An
earlier revision made the entry a product of independent fields
(`{ item, kind, state }`) with a catch-all `return "tool"`, which left two
things wrong that no test could see: a future `TranscriptItem` variant
would silently wear a wrench, and impossible pairs like
`{ kind: "note", item: <thought> }` stayed representable — so the body
switch still had to re-check `item.type` and render `""` on a mismatch it
could not otherwise handle. Meaning was therefore still derived in two
places.
Now:
- `WorkBlockItem` is the closed union of items a block admits, and
`isWorkItem` is a type guard, so the membership decision is made once
and every later stage receives the narrowed type. `admittedWorkItems`
returns the narrowed array rather than a boolean because an `.every()`
guard cannot narrow the array it tested.
- `WorkBlockEntry` is a discriminated union pairing each kind with its own
item type, and fixing `state: "settled"` on the prose kinds. Both classes
of impossible entry are now unrepresentable rather than defended against.
- `projectWorkBlockEntry` switches exhaustively over `WorkBlockItem` with
no default, so admitting a new item type without deciding how it renders
is a compile error (`TS2366: Function lacks ending return statement`),
not a wrench.
- The body switch takes the whole entry, so narrowing on `kind` narrows
`item` too. The `item.type === "thought" ? item.text : ""` fallbacks are
gone because there is no longer a mismatch to fall back from.
Verified by compiling three mutants, each of which now fails `tsc` where
before it type-checked: admitting `plan` to `WorkBlockItem` without a
projection case (TS2366), projecting a thought as a note (TS2322 on
`item`), and giving a thought `state: "failed"` (TS2322 on `state`). The
runtime kind/item pairing is also asserted in
`agentSessionWorkBlockGrouping.test.mjs`, because types are stripped at
runtime and swapping the two prose branches by hand is the easy mistake —
that mutant fails the test.
The projection also fixes an ordering bug the old split invited: a tool
carrying a stale `isError` from a retry while the new attempt executes
reads as `running`, not `failed`, so a live block cannot fold its own
count to "N steps · 1 failed" while the work is still in flight.
**Interim notes suppress the identity row.** #6720 gives every
conversation-variant assistant message a 20px avatar + name row, which is
right for the turn's answer. A rail note is the same item type, so
routing it through that presenter would render a fully attributed agent
turn nested inside a muted step row — the agent apparently replying twice,
once inside the work it was doing. Notes render through a dedicated rail
prose body instead, keeping markdown and the focus code-block recipe by
providing the same `CodeBlockVariantContext` value the presenter would.
Done on this side rather than by reaching into #6720, so that PR keeps one
rule for what a message looks like. Notes share the thought's speech
bubble, matching berd's `progress` entry: both are the agent talking.
## Two props that would have been dead code
- `useControlledDisclosure` is deleted rather than reused. Its entire
reason to exist was the `<details>` echo trap — `<details>` fires
`toggle` for programmatic `open` changes indistinguishably from clicks,
so a policy-driven open echoes back looking like reader intent. This
block's trigger is a `<button>`, where the only thing that can call the
handler is a real click. Keeping the guard would have been dead code
masquerading as load-bearing. Nothing else in the tree imported it.
- berd brightens rail prose with `usePrimaryText={open}`; here the
brightening is unconditional. A closed block unmounts its rows rather
than dimming them, so there is no state in which rail prose is on
screen and not in an open block — the flag's false branch would be
unreachable. A test records that reasoning so the divergence is not
mistaken for an oversight.
## The thought-duration machinery is deleted, not fixed
dev-01 flagged that `turnSegmentItems` does not know `work-block`, so a
thought followed only by block content would never settle its duration.
Tracing it: the meta is built from pre-transform display blocks, so no
`work-block` segment can reach that function — the reported bug cannot
fire. But the trace turned up something worse. `ConversationThought` was
the only reader of `thoughtDurationSecondsById` and
`formatThoughtDisclosureLabel`, and this commit deletes it, leaving ~100
lines of production code and ~200 lines of tests that only tested each
other. Fixing a bug in code with no reader would have preserved the
illusion that focus mode still shows "Thought for Ns" somewhere.
So the duration map, its label formatter and `elapsedSeconds` are gone.
`AgentSessionTranscriptTurnMeta` narrows to the one field that still has
a consumer: `streamingItemId`, which the work block needs because a
thought or note streaming in carries no status of its own. Its tests are
rewritten around what that hint must actually get right — the tail of a
live turn, skipping setup, expanding a summary segment to its last leaf,
and reporting nothing at all when the turn is idle.
Three further notes on translation rather than transcription:
- berd's bullet masks the spine with `bg-card` and its BOT-1599 note
warns off `bg-background`. The rule is "mask with the surface the
transcript is drawn on"; in Buzz that surface is the cover drawer,
which is literally `bg-background`. The two tokens are NOT
interchangeable here: `[data-buzz-content-surface]` locally overrides
`--background` to `--buzz-content-dark` while `--card` keeps the theme
value, so in Buzz Dark `bg-card` paints the bullet rgb(36,41,46) over an
rgb(26,26,26) drawer — the exact BOT-1599 failure. Light mode matches
under either class, so light-mode evidence alone would not have caught
it. Copying the class would have followed the letter of berd's note
against its point.
- Reduced motion is read via `matchMedia` — the way `TerminalSubstrate`
reads it — not motion's `useReducedMotion`, which resolves the query
once per process and caches it. That cache made the preference
untestable (the assertion turned on module load order, not on the
setting) and ignored a mid-session change.
- The memo takes the entry SPREAD into props, not the entry object, and
its boundary is the step BODY rather than the whole row. The projection
is rebuilt whenever the item array changes, so entry objects are fresh
on every append and a memo keyed on one would never hit; spread, the
compared props are `item` (reference-stable) plus two strings. Spreading
also keeps the union intact, so the body switch still narrows `item`
from `kind`. The row stays outside because the glyph depends on
`isLast`, which changes for the previous last row on every append.
`ConversationThought` is deleted here rather than in dev-01's restyle,
per core-02's sequencing ruling: this is the commit that replaces it, so
reasoning stays visible in focus mode at every commit. Its four
disclosure-keyed tests are deleted rather than adapted — they assert a
`<details>` that no longer exists on that path. `default`/
`compactPreview` thought rendering is untouched.
Non-vacuous by mutation testing, each mutant run in isolation:
- note falls through to the tool kind: 4 failures.
- note routed through the message presenter: 2. Note given the wrench: 1.
- `entryState` checking failure before running: 1.
- rail bullet tinted on failure: 1. Prose muted instead of primary: 1.
- memo keyed on the projected entry object: 2 — this is the bug the
projection actually introduced, caught by the pre-existing streaming
cost tests before it shipped.
- streaming hint forced to null: 4. Summary segments not expanded when
finding the tail: 1.
- windowing keyed off `open` instead of the reader's choice: 3. Policy
already holds live blocks open, so this switches windowing off in
exactly the case it exists for.
- reduced-motion preference forced false: 1.
- rail bubble suppression removed (the bug above): the relay-step test
stops passing.
An earlier mutant also caught a test lying: the block-level "echo" test
passed with the guard removed, because the trigger is a `<button>` and
nothing was listening for `toggle` at all. It now asserts the structural
reason the trap cannot apply plus a repeated fold→reopen→fold cycle,
which is what a recorded echo would actually have disabled.
Relay posts are plain rail steps. A `buzz messages send` step classifies
as `renderClass: "message"`, so it renders through `CompactMessageSummary`
— 28px avatar, bordered speech bubble, timestamp, delivery-receipt
button. Correct in the activity feed, where a posted message is a
destination to open; on a muted rail it makes the agent appear to reply
in the middle of its own work. This is the same failure the interim-note
case avoids, reached by a different route: there the item IS an assistant
message, here it is a tool call that merely classifies as one. Caught by
looking at the seeded browser preview, not by a unit test — the JSDOM
suite was green while the rendered rail was wrong.
Suppressed with an explicit presentation signal
(`useIsInsideWorkBlockRail`, default false) rather than by reading the
transcript variant, because `conversation` alone is not the condition:
the same relay step rendered OUTSIDE a block in that variant should keep
its bubble. Both halves of that branch are now pinned by tests, so
suppressing the bubble everywhere cannot pass. Defaulting to false keeps
`default`/`compactPreview` markup byte-identical.
Rebased onto #6720 at `e8709554a` per core-02's B → #6720 → C ordering.
Verified at this tree: full desktop suite 5472 passing / 0 failing,
`tsc --noEmit` clean, `pnpm check` findings identical to main's baseline
(2 warnings + 2 infos, all pre-existing), px-text/pubkey-truncation/
file-size gates clean.
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
a05347d to
9c2900e
Compare





Retargeted: the work block, not the tool-chain card
This PR previously carried Slice C's tool-chain card presentation. Per ss-core-02's direction change (2026-08-24), that presentation is superseded by berd's shipping transcript model, and this same PR is retargeted rather than replaced.
ToolChainCards.tsx, the derived verb/object headline, and the card chrome are gone. Carried forward: the grouping module, step memoization, and their tests. (TheuseControlledDisclosurehook was carried at first, then deleted — see below.)Head:
9c2900e9b, one commit on top of #6720's current heade8709554a. Rebased ontoss-dev-01/berd-restyle(#6720) per core-02's revised ordering: B (#6538) → #6720 → C (this PR). Both must merge first.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. Now, within a turn, everything between the prompt and the final 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 stepsline when the work is done.Behaviour
N previous stepsdisclosure at the top.6 steps · 1 failed), chevron right, rotating down on open. Clicking expands the whole rail. The fold animates: the block mounts open and settles closed after a paint, matching berd. Native<details>cannot animate height, so this usesmotion(already a dep).window.matchMedia("(prefers-reduced-motion: reduce)")with achangelistener, followingTerminalSubstrate.tsx. Note for reviewers:motion's ownuseReducedMotioncaches the media query at import andMotionConfig reducedMotiondoes not override it; verified empirically with throwaway probes before choosing this route.size-5glyph bullets that mask the spine. Thought and interim-note rows get a speech-bubble glyph, tool rows a wrench, running steps pulse. Failure is a glyph shape, not a colour, so one failed step does not read as an alarm across the whole run; the tinted output block carries the red when expanded.Grouping is variant-aware AT THE LIST BOUNDARY
TranscriptDisplayBlockViewruns the additive work-block transform only forconversation:defaultandcompactPreviewtherefore walk main's segments on the identical code path they always did, which makes Slice B's byte-for-byte baseline fixture hold by construction rather than by assertion. This is cleaner than Slice C's approach of special-casing the preview inside the component.One projection per leaf: a closed entry type
Every admitted leaf is projected once into a rail entry, and the glyph, the body, and the folded line's counts all read that projection instead of re-asking
item.type === ...at each render site. That re-derivation is what made an interim agent note fall through to the tool branch and pick up a wrench (ss-quality-00's finding 1).The model is a discriminated union, not a
{ item, kind, state }product:Three things stop compiling as a result, each verified with a throwaway mutant rather than asserted:
TS2366—projectWorkBlockEntrylacks an ending return statementnoteTS2322—rolemissing (WorkBlockNoteItemisExtract<TranscriptItem, {type:"message"}> & { role: "assistant" })failedTS2322—"failed"is not assignable to"settled"Supporting changes that make the union reach the render sites:
isWorkItemis a type guard, andadmittedWorkItems(segment, finalAnswerId)returnsWorkBlockItem[] | nullinstead of a boolean. An.every()predicate cannot narrow the array it just tested, so the old boolean form lost the type at the admission boundary.projectWorkBlockEntryis an exhaustive switch with nodefault. The repo has precedent forconst x: never = ...exhaustiveness checks (sound.ts,SettingsPanels.tsx), but for a returning function the missing-return error is stronger and needs no extra code.toolEntryStatechecks running before failed, so a staleisErrorfrom a retry cannot fold a live block toN steps · 1 failedwhile the work is still in flight.WorkBlockStepBody's props rather than passed as anentryobject. The projection is rebuilt on every append, so entry objects are fresh each time and a memo keyed on one would never hit; spread, the compared props are the reference-stableitemplus two strings.kindnarrowsitem, which deleted the previousitem.type === "thought" ? item.text : ""re-checks. Those silent empty-body branches are now unrepresentable rather than merely unreached.TypeScript is stripped at runtime in this repo's
.mjstests, so the compile-time half is enforced by production code plustscand the runtime half by an explicit[kind, item.type]pairing assertion inagentSessionWorkBlockGrouping.test.mjs. Non-vacuity checked by swapping the two prose projection branches — the test fails; restored, it passes.Decisions worth reviewing as decisions
Block id derives from the FIRST item (
work-block:${first.id}). Keying on the last item would remount the block on every streamed append and discard the reader's disclosure choice.Maximal runs of consecutive work items, not "everything between prompt and answer." When a non-work row (permission gate, error, mid-turn plan update) lands inside the work, the span reading would have to lift that row out of position. Splitting keeps every row where it happened — which matters most for exactly those rows.
Summary segments are expanded back to leaf tool rows inside the block, so the reader never faces two collapsed layers. The block is the one grouping in this variant.
The final answer is identified positionally (last assistant message), not by liveness, so block membership does not reshuffle at turn completion.
formatWorkBlockSummaryLabeldeparts from berd by appending· N failed. A bare step count is the one thing that leaves a reader unable to tell a clean run from a broken one.isActiveaccepts two evidence sources — a step reporting itself running, and the list'sstreamingItemIdhint. Either alone leaves a gap: a streaming thought carries no tool status, and a tool left executing after an observer-stream drop would pin the block open forever.bg-background, not berd's literalbg-card, for the bullet mask. Same rule, different surface: in berd the transcript sits on a card, in Buzz it sits on the drawer'sbg-background. The two tokens are not interchangeable here — in Buzz Dark the drawer sits inside[data-buzz-content-surface], which locally overrides--backgroundto--buzz-content-darkwhile--cardkeeps the theme value. Measured in a seeded browser,bg-cardpaints the bulletrgb(36,41,46)over argb(26,26,26)drawer: a visible disc of the wrong shade, which is exactly the BOT-1599 failure berd's note warns about.Interim notes and relay posts are suppressed on THIS side, via a dedicated prose body and an explicit
useIsInsideWorkBlockRailsignal, rather than by reaching into the message presenter — so style(desktop): bring the conversation variant closer to berd's recipes #6720 keeps one rule for what a message looks like. Two different routes reach the same wrong result: an interim note is an assistant message (style(desktop): bring the conversation variant closer to berd's recipes #6720 would give it a 20px avatar + name identity row), and a relaymessages sendstep is a tool call that merely classifies asrenderClass: "message"(which routes it to a 28px avatar + speech bubble + delivery receipt). Either one nested in a muted rail step reads as the agent replying inside its own work. The signal defaults tofalse, so the other two variants cannot observe it.The signal is presentation, not variant.
conversationalone is not the condition — the same relay step rendered outside a block in that variant should keep its bubble, and a test pins that half of the branch so suppressing it everywhere cannot pass.ConversationThoughtremovalPer ss-core-02's sequencing ruling (b) and ss-dev-01's handoff, the
ConversationThoughtbranch ofactivityRenderClasses/ThoughtActivity.tsxis deleted in this PR, because this is the commit where the rail starts rendering thinking as a row — so no commit ever leaves focus mode with reasoning invisible. Thedefault/compactPreviewthought path is untouched. The four conversation-variant thought tests keyed to the old<details>are deleted rather than adapted, since the element they assert no longer exists on that path.Files
Added:
agentSessionWorkBlockGrouping.ts—groupConversationWorkBlocks,conversationSegmentsForBlock,projectWorkBlockEntries,summarizeWorkBlock,formatWorkBlockSummaryLabel,formatPreviousStepsLabel,windowWorkBlockEntries,WORK_BLOCK_LIVE_WINDOW_SIZE = 3, and theWorkBlockItem/WorkBlockEntrytypesAgentSessionWorkBlock.tsx— the rail UI +AgentSessionWorkBlockSegmentAgentSessionWorkBlock.test.mjs,agentSessionWorkBlockGrouping.test.mjsModified:
AgentSessionTranscriptList.tsx(variant branch,work-blocksegment kind),ThoughtActivity.tsx,AgentSessionTranscriptList.conversation.test.mjs,agentSessionTranscriptContext.ts(the rail presentation signal),AgentSessionToolItem/ToolItem.tsx(honours it),agentSessionConversationMeta.ts.Deleted as dead code, each with a comment or test recording why:
shared/hooks/useControlledDisclosure.ts+ test — the block's trigger is a<button>, so there is no browsertoggleecho to guard against and the hook had no remaining consumer.thoughtDurationSecondsById,formatThoughtDisclosureLabel,elapsedSeconds+ ~200 lines of tests that only tested themselves.ConversationThoughtwas their only reader, and this PR deletes it. A bug had been reported in that code; fixing dead code would have been worse than removing it.Verification
Gates at
9c2900e9b: desktop suite 5472 passing / 0 failing,tsc --noEmitclean,pnpm checkat main's exact 4-finding baseline (2 warnings + 2 infos, all pre-existing, checked against main rather than assumed), px-text / pubkey-truncation / file-size gates clean. Focused suites: grouping 21/21,AgentSessionWorkBlock.test.mjs25/25, conversation list 17/17.Eleven runtime mutants, each run in isolation, all caught: interim note falling through to the tool kind (4 failures),
toolEntryStateordering failed-before-running (1), note through the message presenter (2), note given a wrench (1), bullet tinted red (1), prose muted (1), memo keyed on the freshly-projected entry object (2 — the actual bug I hit), streaming hint forced null (4), summary segments not expanded when finding the tail (1), rail bubble suppression removed (the relay-step test stops passing), and swapping the two prose projection branches (the[kind, item.type]pairing assertion fails). Plus the three compile-time mutants in the entry-type table above. Tree restored and re-verified afterward.The fold animation is asserted in a real browser, not just at its end states — the preview spec samples the panel height per frame while it closes and requires at least one height strictly between full and zero. Re-run at
9c2900e9b:fold heights: 245.5 -> 0 via 41 samples, passing. Non-vacuity re-confirmed at this head by settingCOLLAPSE_TRANSITION.durationto0: the run fails with "the fold must pass through intermediate heights — a details element would jump straight to 0". A<details>element cannot animate height and fails the same way. The reduced-motion endpoint is covered in the same spec.One honest note on coverage: mutating the echo guard revealed that a block-level echo test I had written was vacuous — the block's disclosure is a
<button>, not<details>, so there is no programmatic toggle to echo. The guard and its hook were deleted as dead code rather than kept with a passing-but-empty test.Screenshots
Seeded through the real
__BUZZ_E2E_SEED_OBSERVER_EVENTS__observer-frame path. No production caller passesvariant="conversation"yet — the cover drawer that pins it is ss-dev-00's separate slice — so the variant was pinned in a throwaway worktree with Slice A cherry-picked to capture these. The work block is not reachable in a build until that slice lands alongside this one. The preview spec is not committed to this branch.7 steps · 1 failed)The rail at review size — every step reads the same way, including the relay post (
Sent Confirmed the plural/singular mismatch…), which earlier rendered as a speech bubble with an avatar and delivery receipt:Measured bullet/surface colours, both themes:
rgb(255,255,255)rgb(255,255,255)rgb(229,229,230)rgb(26,26,26)rgb(26,26,26)rgb(64,69,74)Exact match in both, and asserted (
expect(bullet).toBe(drawer)) rather than eyeballed, so the BOT-1599 masking contract holds. Re-run at9c2900e9bin buzz-dark:{"bullet":"rgb(26, 26, 26)","drawer":"rgb(26, 26, 26)","spine":"rgb(64, 69, 74)"}, passing. Non-vacuity re-confirmed at this head by swapping the bullet tobg-card:Expected: "rgb(26, 26, 26)" / Received: "rgb(36, 41, 46)"— exactly the wrong-shade disc berd's note warns about.Screenshot hosting: these are
raw.githubusercontent.comURLs fromscripts/post-screenshots.sh. The previousbuzz.block.builderlab.xyz/media/...links returned 401 to GitHub's anonymous camo proxy and rendered broken for anyone reading on GitHub.