Repository navigation
fix(site): split Working blocks at assistant text and scroll to their end on expand - #30656
ibetitsmike wants to merge 19 commits into
Conversation
|
@codex review |
Codex Review SummaryThis 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: 715f799a8e
ℹ️ 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".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c7f98a67d9
ℹ️ 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".
|
@codex review |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
|
@codex review |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
Expanding a Working block now brings the block's end into view. At the end of the transcript it resumes following, so a live block keeps its newest steps on screen. Other tool rows and every collapse keep holding the view in place. Moves the jsdom MessageScroller layout harness into a shared test helper.
…e Working block anchored handoff and collapse paths
userLayoutIntent now takes a required MessageScrollerUserLayoutIntentOptions, the comments describe the last-row hand-off to following precisely, and the revealEndOnExpand doc sits on its field. The layout test helper exposes currentVisibleRows() and flushLayout() instead of a mutable export, and BlockRow reuses ControlledDisclosure.
…on a last-row reveal
…he view moves away
bde29b9 to
ab2fdee
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. What shall we delve into next? Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
…king blocks at assistant text Text the assistant writes between steps streamed below the live working block and then folded into it once the next tool call arrived, because text before a row's last tool counted as work. Every assistant text now closes the open block, and the next work opens a new block below it, so a row splits into alternating work and output pieces.
…rontend review findings Parsing now records each content part's newest block index in ParsedMessageContent.partBlockIndices, so grouping no longer parses every message a second time. The grouping helpers are simpler, and their names say what they hold: RowSlice.pieces is now pieceRange, and the work piece ordinals are workOrdinal and liveWorkOrdinal. Tests and stories keep to the behavior each one names and reuse the working fixtures, and comments that restated the code are gone.
…ing blocks open across attached files Files the agent attached closed the open working block like text does. The reasoning before the final answer then started a block of its own, which has no steps and is dropped, so the thinking rendered loose under the attachments. Only text closes a block now. Attached files render below the block they were attached in, and the work after them, including the answer's reasoning, stays in it. A block also stays live while only attachments follow its last step.
…ess review of the attached-file fix The block field that records what closed a block is now closedByRowType, since it holds a row type, and the comments around the attached-file rule are shorter and state why files leave the block open. The grouping tests build the attach_file step with one attachStep helper, and the call helper accepts tool arguments, so the attach_file call carries a path as the backend requires. Both working-block story files share MockAttachedFileMessages, and the new AttachedFilesRenderBelowLiveBlock story shows a live block that keeps working while an attached file follows its last step.
… transcript rows Splitting Working blocks at assistant text leaves one-line block rows and narration rows next to each other, and 8px apart they run together. Space transcript rows 12px apart, the gap the chat loading skeleton already uses. Steps inside an expanded Working block keep their 8px gap, so they still read as one group.
jaaydenh
left a comment
There was a problem hiding this comment.
Two performance notes on the reveal chase in the vendored scroller, from reading the code (not measured in a browser).
A revealed row's growth kept scrolling its end back into view until the reader scrolled, so a resize re-wrap minutes later pulled the view down once per frame. The chase now ends 2s after its last scroll, and when the row changes width. The toggle observer ran the anchor hold and then the reveal, each a full scroll pass, and read the row's height after the scroll's writes. The reveal now runs first and the resize handler only runs when it did not scroll, and the row's rect is read once, before the scroll.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 29a3bcb14b
ℹ️ 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".
…rtial Working block above the files that open a page A history page that started with a row of attached files, followed by more of the block's work, rendered the files above a new block and reported that block as complete. Loading the older page then moved the files below the block and raised its step count and duration. Until assistant text or a row that never folds loads, grouping now treats the loaded rows as a possible continuation of a block from older history. The first block opened in that state is partial, and it renders above the leading file rows, where it stays once the older page loads. The PaginatedFromAttachedFile story renders that page.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ffcae5a6b7
ℹ️ 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".
…lify the hidden-entry fallback in Working block grouping The fallback that assigns a hidden entry, such as a search result that rendered no row, to a block combined a ternary, a nullish fallback, and a conditional expression in one assignment. It now starts from the block open after the previous row, and an explicit if assigns a search to the block that the next row's first work joins, which is that same block when one is open. Also drops a doc comment that only restated DraftSlice.workOrdinal.
|
@codex review |
💡 Codex ReviewWhen a live-only block starts after earlier reasoning or text in the same stream, its AGENTS.md reference: site/AGENTS.md:L11-L11 ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
…ve block that starts after text mounted across paging When an older page, a live-only block's persisted step, and the cleared stream arrive in one render, the block keeps its scroller item key only if its persisted start matches the start recorded while it was live. That check used the start of the whole stream, but since text closes a block, a block that starts after text in the same stream starts at its own tool call. The block remounted, collapsed, and lost focus. The live block identity now records the block's own start.
…s attached files While a turn is anchored, expanding its last block in view lets the anchor hold hand off to following once the reply fills the view. That applied only when the block was the scroller's last item, but attached files now render below the block, so in a turn that attached files the view never handed off and the answer streamed below the fold. The check now applies to any row that expands in view inside the anchored turn.
|
Re the Codex finding Preserve later live blocks across simultaneous paging handoff: confirmed and fixed in 8bb9e94. A test reproduced it: a stream of reasoning, text, and then a tool call, with the live block expanded, followed by one render that brings the prompt page, the persisted step, and no stream. The block remounted and lost focus. The live block identity now records the block's own start, and the fallback compares the persisted block's start with it instead of with the stream's start. The new
|
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
jaaydenh
left a comment
There was a problem hiding this comment.
Accessibility pass on the reveal-on-expand and block-splitting changes. Three inline comments below, roughly in priority order: the first two are worth fixing before merge, the third is worth considering.
One more, smaller point with no single line to anchor it: splitting at every assistant text produces many more Working disclosures with near-identical accessible names (several Worked for 3s (1 step) buttons in one turn). Screen reader users who navigate by the button list cannot tell them apart. That pattern existed before, but this PR makes it much more common; adding some context to the name (for example a short step summary) would help.
The scroll findings follow from how browsers handle focus navigation and the CSSOM behavior option; I did not reproduce them in Chromium.
| return true | ||
| } | ||
|
|
||
| if (!endsBelow) { |
There was a problem hiding this comment.
When the expanded block is taller than the view, scrollToItemEnd aligns its end to the bottom and pushes the header button above the top while it keeps keyboard focus (the review notes call this out as intended). For keyboard users that has two costs:
- The focus indicator leaves the screen right after Enter or Space.
- The next Tab moves focus to the first step inside the block, at its top. The browser scrolls that step into view, the view jumps back up, and the upward scroll clears
revealedItemRef. So for keyboard users the reveal is undone by their next keystroke.
Suggestion: never scroll past the header, i.e. scrollTop = min(endAlignedTop, headerTop - margin), which amounts to align: "start" when the block is taller than the view. Short blocks still show their end, and both the focused header and the next Tab target stay visible.
| // recorded after the scroll. | ||
| const scrollToItemEnd = React.useCallback( | ||
| (item: HTMLElement, rect: DOMRect) => { | ||
| scrollToElement(item, { align: "end", behavior: "smooth" }) |
There was a problem hiding this comment.
behavior: "smooth" passed from JS overrides the scroll-behavior: auto !important rule under prefers-reduced-motion in index.css, so reduced-motion users still get the animation. The existing commands do the same, but this one differs: it is not a single user-requested scroll. The chase in handleResize calls it again on every growth during the 2s window, and each call renews the window, so a few images loading one after another produce repeated motion the user did not start (WCAG 2.3.3).
A one-line fix:
const behavior = window.matchMedia("(prefers-reduced-motion: reduce)").matches
? "auto"
: "smooth"The same helper could cover the existing scrollToMessage callers in a follow-up.
| getStepCount(draft) > 0 || | ||
| (draft.containsLiveRow && | ||
| options.isTurnActive && | ||
| draft.closedByRowType === undefined), |
There was a problem hiding this comment.
Since text now closes the open block and a closed block with no steps is dropped here, a model that writes text, then reasons, then writes text (OpenAI narration, or reasoning at the start of a turn) shows a live, expandable Working button that unmounts when the text arrives. Before, that reasoning joined the already open block. If the user has focused or expanded that button, focus falls back to <body> and the content they opened collapses into an inline reasoning row. The remount case mentioned in the review notes (a block that starts after text and streams reasoning before its first tool call) loses focus the same way.
Options: keep the block while it is focused or expanded, or move focus to the reasoning row that replaces it.
…ocks and list them under Attachments (#30788) In `/agents` chats with **Collapse assistant steps** on, files the agent attaches (`attach_file`, computer-use screenshots, the `propose_plan` plan file) rendered as the answer after a Working block: the block closed at the attachment, and a turn that kept working split into several blocks. They now fold into the Working block of the step that made them and show when it is expanded. Rows outside every block (an uncollapsible `propose_plan` or `ask_user_question` row, a parked running tool) hide their files, and an agent row left with nothing else is not rendered. With the preference off, the transcript is unchanged, except that download cards get the focus ring the text-preview button beside them already has. To keep those files reachable, the right panel's Summary tab gets a collapsible **Attachments (N)** section, open by default. It lists the files the agent attached in the loaded messages, newest first, as the transcript's download cards (expired tile included); user uploads stay in their messages. While older history is unloaded, the section says so and offers **Load earlier messages**. Until the first message page reaches the chat store, the header shows no count, so a chat with files never flashes "(0)", and live messages do not make the list blink. Frontend only: production code changes 366 lines; the rest is tests, stories, and fixtures. This overlaps #30656, which renders attached files below the Working block; this PR keeps them inside the block, as the newer request asks, so whichever lands second reconciles `workingBlockGrouping.ts` and `ConversationTimeline.tsx`. **Collapse on.** The turn's files stay in the collapsed Working block, and the Summary panel lists them under Attachments (3), newest first.  **Block expanded.** Each file follows the step that attached it.  **Expired file.** After `old.zip` expires, the panel shows the expired tile in its place.  Follow-ups, out of scope: split `ConversationTimeline.tsx` (795 lines before, 805 now); keep focus on load-more buttons while they fetch (`HistorySidebar`, `InboxPopover`, this one); show the fetch error in both message pagers; share probe-learned expiry between the transcript and the panel; flag evicted files newer than the newest linked one, such as an evicted newest file (`deriveEvictedFileIds` cannot yet tell them from files the chat has not linked yet); name the file on the expired tile. ## Testing - `pnpm lint`, the AgentsPage unit tests (122 files, 2133 tests), and the story tests for the touched area (13 files, 261 tests) pass on 30007ac. - New unit tests cover a step's attachment staying in its block, rows outside blocks dropping files, `isAgentAttachmentOnlyRow`, `getAgentAttachments` (agent files only, newest first), the loading gate (a live message does not blank the list), and the load button. Each fails when its behavior breaks. - New stories: `ChatAttachments` (12 states), four `ChatSummaryPanel` states, and three working-block states. - Remote UAT round 4: **PASS** on 3e47d30, on a dev deployment built from the branch. It checked expired files on both surfaces, mouse and Enter downloads against the source files' sha256, no "(0)" on reload or chat switch, one count change per live attachment, and the focus ring. 30007ac changes only a doc comment after it, and the screenshots come from this round. Rounds 1 to 3 passed on earlier heads; round 3 covered Load earlier messages, rows outside blocks, and 18 real-model runs on four models. <details> <summary>Review record</summary> - 11 rounds of `coder-frontend-review` (8 global roles), `frontend-deep-review` (8 project roles), and `frontend-review` (FE1 to FE10), read-only. Round 9 ran all 17 roles on b53897c; rounds 10 and 11 re-checked the comment-only fixes on 3e47d30 and 30007ac. - Final verdicts: every checkpoint PASS except FE5 and FE6 (justified below). RP6's PASS requires the `ConversationTimeline.tsx` split follow-up listed above. - Test audit: PASS through b53897c. Every finding not listed below was fixed. - Codex: four rounds on 30007ac. Each of its five P2 findings was declined in its thread with evidence: a spinner for the one-commit hydration gap, the file name on the shared expired tile, the copy action for a text-and-file message that chatd never stores, gating the load button on hydration, and the `ChatAttachments` doc. Round 4 found no major issues. Justified FAILs: - FE5, no error text for a failed page: the transcript's pager shows the same retry-only state for the same query; follow-up. - FE6 and SA6, the button disables while fetching and loses focus: the repo's load-more pattern (`InboxPopover`, `HistorySidebar`); follow-up for all three. Dropped findings (finding: reason): - RO6 stack split: the component is 94 lines, and the hiding needs the panel, so the halves are not independent. - RP6 and RO5, split `ConversationTimeline.tsx` now: the PR adds call sites, a prop, and two flags; follow-up, and #30656 edits it. - RO1 `SidebarSectionHeader`: a sidebar h2 with no aria-controls; the section uses the Radix Collapsible in `SectionHeader`. - RO1 `isFileRenderBlock`: inline `block.type` checks are the grouping module's idiom. - RO2 shared retry button: the two share only variant, size, and label. - RO3 `every` helper: a one-line idiom with different predicates in two modules. - RO3 shared guard with `getMemberRow`: two conditions, and #30656 rewrites `getMemberRow`. - RO3 `Omit` over `Pick`: the `Pick` reuses the exported `EarlierMessagesProps` types. - RO3 page-level spread: `AgentChatPageView` passes every prop explicitly. - RO4 move `isAgentAttachmentOnlyRow`: it takes the grouping module's row type and sits beside `getRowBlocks`. - RO5 transcript focus ring: intended; it matches the text-preview button. - SD1 inline single-use story constants, and repeated fixture values: FE9 asks for named spread constants. - SD2 `{ message }` wrapper: retyping moves the map into `ConversationTimeline.tsx`, which #30656 edits. - SA2 `cva` for `fullWidth`: one boolean in a pre-existing component. - TN4 inline `type` imports: the repo's style for mixed imports. - TN4 `ChatStoreHandle`: `ChatStore` is the exported type; the handle is a file-local alias. - TN4 named `FileCardProps`: the file types its components inline. - TN4 `GetRowBlocksOptions`: named like `GroupWorkingBlocksOptions`. - TN5 `AgentAttachment` beside `readAgentAttachmentText`: that one reads user uploads, in another module. - TN6 copy: the repo's paged empty state (`SessionTimeline.tsx`) and the transcript's retry label. - CD1 `getAgentAttachments` doc: it states the ordering and the upload exclusion. - CD1 trim exported docs: AGENTS.md requires docs on exported symbols. - CD4 `isHydratingMessages` comment: pre-existing and consistent with its source. - CD5 consumers in the `EarlierMessagesProps` doc: the doc states the contract. - `splitRowBlocks` doc: the base text stays; a comment at the file condition gives the reason. - Hydration comment placement: it sits on the operand it explains. - Deleted `NotVisible` comment: this PR made it false. - TS1 and TS5 `NotVisible` assertions: pre-existing. - TS7 panel story with `hasMoreMessages`: tsc enforces the forward; `WithEarlierMessages` is the visual. - TS7 streamed-file story: live rows share the `getRowBlocks` call. - TS7 text-plus-file case and `ChatError` story: the grouping test fails on the same break; no new state. - TS7 raster image fixture: grouping treats every file part alike. - TS7 text and a file in one message: chatd persists a file as its own message; the unit test covers the branch. - Play-function assertions and a collapse test: plays drive state, and Radix owns collapsing. - `useChatMessages` unit test: a moved derivation with no new logic. - Drop `LoadingMessages` and `LiveMessageArriving`: each catches a call-site break (test audit). - A store per story, built in render: as `ChatPageContent.stories` does. - `NotVisible` capture shows the section: it renders whatever `isVisible` is. - RP4 hoist the `.map` callback: a single-use wrapper (site/AGENTS.md). - RP5 `useChatMessages` as a function: it owns two subscriptions that three components share. - RP8 pre-existing render helpers: unchanged; the split follow-up covers them. - Per-render derivations and a second subscription: pure derivations; only the queries are gated. - `useFileProbes()` before the branches: hooks cannot be conditional, and the default context is safe. - DF5 share probes with the panel: FileCard never probes; both read `chat.files`, polled every 5 s while the chat runs. - DF5 every loaded file evicted: a pre-existing `deriveEvictedFileIds` gap; follow-up. - DF5 empty before query data: the page shows its loading view then. - Spinner before the store fills: a one-commit window, so it would flash. - Previous chat after a switch: the page remounts per chat with its own store. - `void onFetchMoreMessages()`: `fetchNextPage` reports errors in its result, and the transcript calls it the same way. - Later parts first within a message: later parts are newer. - Up chevron when open: as `ChatSectionHeader` does. - `messageHelpers.ts` imports `chatAttachments.ts`: the page already loads it; dpdm finds no cycle. - Live status on a live row whose file is hidden: intended. - `m-0` on the empty state: `index.css` sets paragraph margins. - FileCard's name omits "Download file", and the expired tile names no file: pre-existing; naming the file is a follow-up. UAT observations, not failures: - A file in a message that also holds text after the tool call is listed only under Attachments, as for any row outside a block. Round 3 first reported it as a FAIL; only DB-seeded rows had that shape, and 18 real-model runs never did. - An evicted newest file keeps its link until a newer linked file exists (follow-up above). - The panel scroller clips the bottom of a card's focus ring at its bottom edge. - The load button has no aria-busy, and after a failure two buttons read "Retry loading earlier messages". - One parallel step with two files renders both tool rows before both cards. - The transcript's expired tile is 112px wide beside 384px cards, and its tooltip is mouse-only (pre-existing). </details> > Xum opened this PR on behalf of @ibetitsmike.
Expanding a collapsed Working block in the Agents chat kept the view where it was (#29354), so a block that expanded near the bottom of the view opened its steps below the fold, and the reader had to scroll to reach the latest one.
Expanding a Working block now brings its end into view. If the expanded block ends below the viewport, the transcript scrolls smoothly until the block's end sits at the bottom of the view; if it already ends in view, nothing moves. If a step then grows and pushes the end back out of view (an image loading), the view scrolls to the new end, until the reader scrolls or toggles a row, 2 seconds pass after its last scroll starts, or the block changes width because the view resized. When the block is the last row of the transcript, expanding it jumps to the end and resumes following new output. While a turn is anchored at the top of the view and one of its blocks expands in view, the view stays put and the anchor hold hands off to following once the reply fills the view, even with attached files below the block; if the expansion already used up the hold's tail spacer, it hands off at once. Collapsing is unchanged.
ToolCall.Rootgets an opt-inrevealEndOnExpandprop, set only byWorkingBlockDisclosure, so other tool rows keep their behavior. It passes{ revealEnd }to the vendored message scroller'suserLayoutIntent, whose toggle-commitMutationObservercalls the newrevealItemEnd. When the reveal scrolls, it places the row and the tail spacer itself, so the existing resize pass (the anchor hold) runs only when the reveal does not scroll. The reveal records the row's size and an expiry, and the scroller's resize handler scrolls to the row's new end while that jump is still settling and the expiry has not passed. A change in the row's width ends the reveal. The jsdomMessageScrollerlayout harness moves fromToolCall.test.tsxtosite/src/testHelpers/messageScrollerLayout.tsxsoWorkingBlockDisclosure.test.tsxcan reuse it.Working blocks also split at assistant text now. Text the assistant wrote between steps streamed below the live block, then folded into it when the next tool call arrived. Every assistant text now closes the open block, and the next step opens a new block below it. That includes OpenAI narration and the lead-in before a search, which #30529 folded into the block. Files the agent attaches with
attach_filedo not close a block: they render below the block they were attached in, and the work after them stays in it. Before, attached files closed the block, so the reasoning before the final answer formed a block with no steps, which is dropped, and the reasoning showed on its own between the files and the answer.groupWorkingBlockssplits each message row into alternating work and output pieces. Parsing records each content part's block index (ParsedMessageContent.partBlockIndices), so grouping can place each part without parsing the message again.Splitting blocks at text puts one-line block rows and narration rows next to each other, so transcript rows now sit 12px apart instead of 8px, the gap the chat loading skeleton already uses. Steps inside an expanded block keep their 8px gap.
Review notes
Deliberate choices from self-review (
frontend-reviewFE1 to FE10 andcoder-frontend-review):behavior: "smooth"bypasses the reduced-motion rule insite/src/index.css(scroll-behavior: auto), as the existing chat scroll commands already do (submitChatTurn.ts,ConversationTimeline.tsx).main. Remote UAT hit this at 1440x560; at 1440x700 the view handed off. Handing off in that case would mean jumping to the end at the expand, which moves the block the reader just opened.scrollTop.mainjumps the same way from the same scroll position; the collapse path is unchanged here.requestAnimationFramewithsetTimeout. jsdom runs frames on the globalsetInterval, and the existing live-label test fakes timers while amotioncomponent requests a frame, which stalls every later frame in the file.BlockRowstays separate fromToolCall.test.tsx'sToolRow: sharing them would add a render-prop abstraction for two callers.Working block grouping:
LabeledNarrationFoldsandLeadInBeforeSearchFoldsare nowLabeledNarrationStaysVisibleandLeadInBeforeSearchStaysVisible, so Pixel sees them as new stories.frontend-reviewFE9 asks for story variants in named constants, whilecoder-frontend-reviewTS3 and TS4 ask for overrides at the call site and no single-use story constants, and that skill lets its checkpoints win. The two new single-use message variants inConversationTimelineWorkingBlocks.stories.tsxstay inline, like the file's existing ones.attach_fileresult carries onlyname, the one fieldAttachFileLabelreads.mainalready shows for a turn with no tools. While that reasoning streams, a live Working block shows and dissolves when the text arrives.Row spacing:
ChatMessageScroller(theConversationTimeline,ChatPageContent,AgentChatPageView, andAgentChatPagestories). TheWorkingBlocksandLiveWorkingBlocksstories render a bareMessageScroller.Contentwith no gap, so they do not change.getFlexGapingeometry.ts), so its spacer math follows the class.Delivery record
ccbb338f5cb. Round 1: FAIL on the collapse clamp above, which reproduces onmainfrom the same scroll position; that scenario was reworded to record the clamp as an observation. Round 2 on the same SHA: PASS. That run did not test a turn split across history pages or non-desktop viewports; the later run below does. Between runs, unit tests and a Chromium probe story (not committed) that records everyscrollTocall and the scroll position each frame cover the changes.WorkingBlockDisclosurescroller tests (11 cases). Removing the reveal call, the prop, the!expandedguard, the in-view check, the anchored hand-off, the whole anchored branch, the spacer check, the growth chase, itssettling-jumpcheck, its in-view check, either height record, the clear on another jump, or the clear on a scroll up each fails at least one of them. The chase's growth check is only visible in a browser, where jsdom's instantscrollTohides it: in the Chromium probe, removing it re-issued the smooth scroll on every resize during the glide (3 calls instead of 1 when a row below the block grew mid-glide).focus()moved the view up to an earlier row, a later growth left the view there.715f799a8ed: two P2 findings (a cut-short smooth reveal at the last row; no hand-off when the expansion used up the anchor spacer). Both confirmed by execution and fixed inc7f98a67d99.c7f98a67d99: one P2 finding (content that grows after the reveal, such as a lazy image, pushed the end back out of view). Confirmed with a failing test and fixed in990cb3542af. Self-review then found that a scroll with no gesture left that chase armed; fixed inb928354858dandd7053a2de50.d7053a2de50: no findings.mainat99afdc3b0fbto pick up fix: fold web searches, answer reasoning, and OpenAI narration into working blocks #30529, which folds an answer's reasoning into the Working block it ends. The first commit now also exportsisViewportfromtestHelpers/messageScrollerLayout.tsx, because theToolCall.Scrollabletests from refactor(site/src/pages/AgentsPage/components/ChatElements/tools): add ToolCall.Scrollable #30648 use it and this PR moved it there. Commit SHAs above are from before the rebase; each rebased commit keeps its subject. Codex round 4 on the pre-rebase mergebde29b9a1cc: no findings. Codex round 5 onab2fdeec307: no findings.coder-frontend-review(eight roles) andfrontend-review(FE1 to FE10) over five rounds, the last on the Codex fixes, re-running each failing role after its fix until every role passed ond7053a2de50; findings fixed or justified above.f97f2a911d3to5f87b148d88: written and reviewed on a separate branch, then cherry-picked ontoab2fdeec307with no conflicts and the same patch (git patch-id).coder-frontend-reviewran 13 rounds until all eight roles passed, andfrontend-reviewFE1 to FE10 passed apart from the FE9 note above. Red-green: letting attached files close the block fails both new attached-file tests, and the old live rule fails the live one.5f87b148d88: tsc, biome, oxlint (including the design-system config), knip, the React Compiler check, and dpdm pass, and so do the unit tests insrc/pages/AgentsPage,src/vendor, andsrc/testHelpers(2128 passed, 2 skipped) and the ChatConversation story tests (118).aac53217c4a: compared 8px, 12px, and 16px on the same read-only dogfood chat in Chromium at 1280 and 390 px widths. The story tests that renderChatMessageScrollerpass (7 files, 215 tests), andcoder-frontend-review(eight roles) andfrontend-reviewFE1 to FE10 pass.aac53217c4a, fixed in29a3bcb14b6: the growth chase had no end besides user input, and an expand forced up to four full transcript layouts. The chase now expires and ends on a width change. The reveal runs before the anchor hold, which it replaces when it scrolls, and the row's rect is read once, before the scroll's writes. Two new cases (the window passes, the block re-wraps at a new width) each fail with their check removed. The reorder changes no test outcome (29 pass either way); in a throwaway jsdom probe, an anchored expand made 21getBoundingClientRectcalls instead of 69. Not measured in a browser.coder-frontend-reviewpassed after four rounds; the size check reusesSCROLL_POSITION_EPSILON, which upstream also applies to row positions. Site checks pass on the new head (unit 2130 passed, 2 skipped; ChatConversation stories 118).29a3bcb14b6: two P2 findings. When the newest loaded page opened with files the agent attached and later rows continued their block, the files rendered above a block labeled complete, and loading the older page moved the block above the files. Confirmed with a failing test and fixed inffcae5a6b7e: the block on that page is partial and renders above the files from the start (storyPaginatedFromAttachedFile). Declined: running the scroller tests in a real layout engine. They assert the rows the scroller publishes as visible, with the jsdom modelmainalready uses, and the Chromium probe and remote UAT cover browser behavior.ffcae5a6b7e: five style and structure findings. Fixed inacbd47dcc88: a hidden-entry assignment that nested a ternary, a nullish fallback, and a conditional, and a doc comment that restated a field name. Declined: extracting the row renderer into a component (the closure is onmain, and the file goes from 795 to 798 lines), a named props type for theTranscripttest helper (moved unchanged fromToolCall.test.tsx), and splitting a streamed row once rather than once per slice (the live row holds only the current step; in jsdom, a streamed update cost 0.04 ms for 3 sections and 1.8 ms for 50).acbd47dcc88: one P2 finding. A live-only block that starts after text in the same stream remounted, collapsed, and lost focus when its prompt page, its persisted step, and a cleared stream arrived in one render. Confirmed with a failing test and fixed in8bb9e94b76b. Codex round 9 onda079c0b460: no findings.29a3bcb14b6: the reveal, the last row, the anchored turn, growth follow, grouping with real models, the row gap, collapse, and the mobile viewport pass, and the collapse clamp matchesmain. It also found gaps thatmaindoes not handle either: an anchored expand of a block with attached files below it never handed off to following (da079c0b460fixes it while some tail spacer is left), an expand within about 30 ms of an older page starting to load loses the reveal, and growth after an expand that did not scroll is not followed. Part 2 onacbd47dcc88: with the page boundary at the attached files, the partial block renders above the files before and after the older page loads (the control,29a3bcb14b6, reorders them); the reveal, growth follow, and real-model grouping pass, and the grouping refactor renders 4 web-search chats the same asffcae5a6b7e. Part 3 onda079c0b460, withacbd47dcc88as the control: an anchored expand of the live block with attached files below it hands off to following at 1440x700 (2 of 2 runs, control 0 of 1), and so does an expand of an earlier block of the turn (2 of 2, control 0 of 2); the reveal below the fold and the anchored expand of the last block pass. At 1440x560 the files used up the tail spacer before the expand, so the hold stayed, as on the control andmain(see the review notes). Verdict onda079c0b460: PASS. Not tested: an OpenAI lead-in before a web search, OpenAI narration across three or more tool calls, and a real image that loads late inside a block (the growth checks injected it).da079c0b460: tsc, biome, oxlint (including the design-system config), knip, the React Compiler check, and dpdm pass, and so do the unit tests insrc/pages/AgentsPage,src/vendor, andsrc/testHelpers(2134 passed, 2 skipped) and the ChatConversation story tests (119).coder-frontend-review(eight roles) andfrontend-reviewFE1 to FE10 pass on each fix, with the failing roles re-run after their fixes.da079c0b460: the required checks pass. The Storybook (Pixel) job fails on one story this PR does not touch: theUserEngagementChartLoaded story's play function cannot find its "User engagement" group. fix(site/src): replace role="application" on recharts charts #30375 added that check tomainwith the wrong case, and fix: match chart accessible name in story #30907 fixed it onmainafter this run started. A re-run reuses this run's merge commit, so the job stays red until the next push.