Skip to content

feat: add a collapse assistant steps chat preference - #29115

Draft
ibetitsmike wants to merge 14 commits into
mainfrom
mike/collapse-assistant-steps
Draft

feat: add a collapse assistant steps chat preference#29115
ibetitsmike wants to merge 14 commits into
mainfrom
mike/collapse-assistant-steps

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Adds a per-user Collapse assistant steps preference (Agents > Settings, off by default) that folds each turn's consecutive tool-step rows into one disclosure: "Worked for 2m 14s (7 steps)" once the turn completes, or a ticking "Working for 12s" while it is still running.

How grouping works

A step row is assistant output whose last visible block is a tool call or reasoning; narration that precedes a tool call folds with it. Answers, user prompts, system notices, and rows containing ask_user_question, propose_plan, or compaction markers are never folded. A completed reasoning-only row (for example after an interrupt) stays visible on its own rather than folding as "0 steps". Failed steps stay inside the block but the summary shows a "1 failed step" count so a failure is never hidden without a trace. Expansion is user-controlled: an expanded block stays expanded while new steps stream in and after the turn completes.

A turn counts as working while the chat status is running or interrupting. A chat parked in requires_action is waiting on a client, so the preceding steps read as finished work and the pending client-executed tool stays visible outside the fold.

Durations are the wall-clock span of part timestamps (tool calls, results, reasoning) taken from the raw messages, so hidden tool-result messages count, overlapping parallel tools are not summed, and the idle gap before the next prompt is excluded. Message created_at is not used because it is shared across an insert batch. Blocks whose parts carry no timestamps (older transcripts) show "Completed N steps" instead of a guessed duration.

Live turn

With the preference on, the live assistant row stays inside the fold for the whole working phase. Before the first reasoning or tool call the plain Thinking indicator shows as before. From the first reasoning delta on, the live row is a step row and folds as "Working for Ns"; between persisted steps, when the stream has reopened but nothing has arrived yet, the empty live row joins the block it follows instead of rendering a separate Thinking row under the fold. The live clock starts at the earliest streamed part timestamp (reasoning included), so the label does not jump when a step persists. The final answer streams outside the fold, with its own collapsed thinking disclosure above the text.

Two visible effects are accepted rather than fixed: narration that precedes a tool call is briefly visible outside the fold until its tool call streams (the client cannot know a tool call is coming), and the completed label can be up to about 2s shorter than the last live tick because the completed span uses the persisted part timestamps while the live clock also counts the model's pre-answer latency.

useSmoothText now resumes its reveal loop when its effect re-runs. Under StrictMode (dev and Storybook) the simulated unmount disposed the loop, so a reasoning block mounted inside a just-expanded fold stayed frozen at the bare "Thinking" title; production builds were unaffected.

Pagination

Each block is its own MessageScroller.Item. Completed blocks key off their newest row, which prepending older pages never changes, so a block that gains earlier rows keeps the same element. Expansion is recorded on the block's member rows plus a deterministic live key, so a choice made mid-turn survives completion and later prepends even when the turn's prompt is on an unloaded page. While older history is still unloaded, the oldest block is marked partial and reports lower bounds ("Worked for at least 8s (1 step or more)", "Working for at least 12s", "1 failed step or more").

Older rows that join an expanded partial block land inside one scroller item, where the scroller cannot anchor them and browsers skip scroll anchoring at the top, so the disclosure scrolls the viewport by its own growth to keep the reading position. It only does so for a real prepend (the block's previous first row is still a member), not when the live row hands off to its persisted step. When the same page also prepends a new scroller item (the turn's prompt row on the last page), MessageScroller re-anchors that item's top edge from its own MutationObserver callback, which would cancel a synchronous adjustment, so the block applies its growth from a microtask after that callback and before paint. A Vitest test covers the prepend and the live handoff; the story exercises both pages against the real scroller and asserts on element geometry because scroll position is the behavior under test.

Loading

The chat page waits for the preference in its existing loading view (the same skeleton it shows while the chat and messages load), so a saved "collapse" setting never paints rows unfolded and then folds them. If the preference request fails, the transcript renders unfolded, matching how the other display preferences on the page degrade; the settings switch surfaces the load error.

Backend

collapse_assistant_steps follows the existing task_notification_alert_dismissed boolean pattern: a user_configs key with dbauthz wrappers, no migration.

Dogfood UAT

Independent remote UAT (dogfood workspace, real Claude Haiku 4.5 via the AI Gateway, real browser input) ran in seven rounds. Round 1 (605272e) failed on the between-steps timer freeze, round 2 (63d99ee) confirmed that fix and found the in-block reading-position jump, round 3 verified c932002, round 4 (8ddc43a) verified the live-turn fold with a 100ms DOM sampler, round 5 confirmed it on the main merge c822df1, round 6 (a70cc67) found the final-page reading-position jump fixed above, and round 7 verified a83e850 (each proven by git rev-parse HEAD and /api/v2/buildinfo on the remote). The only later commit adds a story and changes no runtime code.

Scenario Result Evidence
Live turn: no Thinking row or reasoning disclosure outside the fold from the first fold to the answer PASS Rounds 4 to 7: 0 escapes over 104 to 206 samples per turn; only pre-tool narration and the answer itself appeared outside
Live label monotonic across step persists; final "Worked for Ns (k steps)" with k = tool calls PASS "Working for 0s" to "18s" with no reset, then "Worked for 17s (3 steps)" (rounds 6 and 7); the completed label ends 1s to 2s below the last live tick
Live timer ticks between steps; completes to a fixed label; expansion kept PASS sleep 25: "Working for 1s to 8s", then "Worked for 25s (1 step)", DONE outside, aria-expanded retained (round 3)
Reload mid-tool restores elapsed time from the call part, not the reload; live state still folded after reload PASS Restored "Working for 13s" against 12.82s since the persisted call part (round 3); reload mid-turn kept the fold (round 4)
Cold load with the preference on: skeleton until the preference resolves, first frame already folded PASS Preference GET held 3s; skeleton in the recording, zero ungrouped tool rows across 47 frames (round 3)
Older pages prepended into an expanded partial fold keep the reading position, including the last page that also loads the prompt row PASS Round 7, wheel to the top before each page: in-block page 24 to 49 steps, anchor top 192 to 192 (0px), scrollTop 0 to 800; final page 49 to 65 steps plus the prompt row, anchor 192 to 192 (0px), scrollTop 0 to 781 (602 block growth + 171 prompt), same block element, still expanded. Round 6 measured the final page at +602px before the fix
Partial fold keeps its element and expansion across prepends; lower-bound labels PASS Same disclosure through 24, 49, 65 steps: "at least 41s (24 steps or more)" to "Worked for 2m 19s (65 steps)" with "1 failed step" (rounds 3, 6, 7)
Command then ask_user_question in one turn PASS Preceding steps read "Worked for 29s (5 steps)", question outside the fold, answered, later command folded (round 3)
Preference GET 500 PASS Red "Failed to load your collapse assistant steps preference." with the switch disabled (round 3)
Regressions: no-tool answers, single step, failed-step badge, keyboard Enter/Space, preference off PASS 63d99ee and round 3 smoke
Interim answer splitting a turn NOT TESTED The model never produced a message-ending interim answer; covered by unit tests and stories only
Live partial fold label ("Working for at least"); partial failed badge ("1 failed step or more") NOT TESTED live Completed lower-bound labels verified; the partial badge needs an is_error tool result inside the newest loaded page, which shell exit codes do not produce; covered by stories and a Vitest test
Cross-tab preference change applied to an open chat ACCEPTED Requires a reload; refetchOnWindowFocus is off app-wide, same as the other display preferences
Client-executed tool parked in requires_action NOT REACHABLE from the web UI Covered by a unit test and a story

Sanitized evidence (screenshots, recordings, sampler JSON, raw API responses, harnesses) is attached to the dogfood chat https://dogfood.cdr.dev/agents/385f856b-7742-4d75-af8c-f94e2a0df558 as r4-uat-archive.tar.gz, r3-uat-archive.tar.gz and r2-final-evidence-archive.tar.gz; rounds 5 to 7 wrote their measurements to the UAT workspace (/home/coder/uat-r5 to uat-r7) because the chat's attachment slots are exhausted.

This PR was prepared by Xum, an AI coding agent, on behalf of @ibetitsmike.

Add a "Collapse assistant steps" switch to Agents > Settings. When it is
on, each turn's consecutive tool-step rows fold into one disclosure
labeled "Worked for <duration> (<n> steps)" once complete, or
"Working for <elapsed>" while the turn is still running.

Grouping and identity rules:
- A step row is assistant output whose last visible block is a tool
  call or reasoning. Answers, user prompts, system notices, and rows
  containing ask_user_question, propose_plan, or compaction markers are
  never folded.
- Failed steps stay inside the block but are counted on the summary.
- Durations are the wall-clock span of part timestamps (tool calls,
  results, reasoning), never a sum of overlapping tools and never the
  idle gap before the next prompt. Blocks with no part timestamps show
  "Completed <n> steps".
- Completed blocks key off their newest row so prepending older pages
  keeps the same element; the live block keys off its turn so appended
  steps never remount it, and both forms share a turnKey so a mid-turn
  expansion survives completion.
- The oldest loaded block is marked partial while older history exists
  and reports lower bounds ("Worked for at least ...").
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T19:47:17.215737Z d13ebe6 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: 605272e338

ℹ️ 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 thread site/src/pages/AgentsPage/components/CollapseAssistantStepsSettings.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatConversation/workingBlockGrouping.ts Outdated
- Only fold runs that contain tool activity, so a reasoning-only row is
  never hidden behind a "0 steps" summary.
- Show an explicit error when the preference fails to load instead of an
  unexplained disabled switch.
- Index message spans once and binary-search per block so grouping stays
  linear on long transcripts.
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 4ae82b9c9e

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

The stream state is cleared when a step's durable message commits, so
between steps (for example while a long tool runs) the live status reads
idle even though the chat is still running. Deriving turn activity from
it made the block flip to "Worked for" mid-turn. Use the server chat
status instead, which stays running, requires_action, or interrupting
until the turn actually ends.
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 97fb5a26d5

ℹ️ 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 thread site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx Outdated
- Wait for the preference query before rendering rows, so a saved
  "collapse" setting never paints the transcript ungrouped and then
  folds it (which also moved the viewport on long chats).
- Treat requires_action as the agent waiting on the user rather than
  working, so the fold reads "Worked for" instead of ticking until the
  action deadline.
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: bb6a8cce11

ℹ️ 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 thread site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatConversation/WorkingBlockDisclosure.tsx Outdated
- Track expansion on a block's durable member rows plus a deterministic
  live key, so it survives the live-to-complete handoff and prepends even
  when the turn's prompt is not loaded.
- Do not let the pager treat the preference-pending blank as the start of
  history.
- Label a partial live block as a lower bound ("Working for at least").
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 63d99ee926

ℹ️ 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 thread site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx Outdated
Wait for the display preference in the chat page's existing loading
view so a cold load shows the skeleton instead of a blank transcript,
and drop the timeline-level null return plus the pager guard it needed.

A client-executed tool that parks the chat in requires_action keeps its
running call outside the fold instead of hiding it behind a completed
summary.
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: ce271957e7

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

The watch-event stories mount the real AgentChatPage, which now waits
for the display preference before leaving its loading view.
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@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: 8a6988ac9d

ℹ️ 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 thread site/src/pages/AgentsPage/AgentChatPage.tsx
Older pages prepend rows inside an expanded partial block rather than as
new scroller items, so the scroller cannot anchor them and browsers skip
scroll anchoring at the top. The block now scrolls the viewport by its
own growth so the row being read stays in place.
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: c93200249e

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

With steps collapsed, the live row kept escaping the fold: the stream
reopens with no output before every step (phase starting, or streaming
with no blocks yet), so a Thinking shimmer popped up under "Working for"
and vanished when the next tool call arrived; a turn's opening reasoning
rendered as a thinking disclosure and was then swallowed by the fold once
its first tool call streamed. The summary row already shimmers, so the
turn now reads as one block from its first reasoning until the answer.

- An idle live row extends the block it follows but never starts one, so
  the turn's very first moments keep the plain Thinking indicator.
- A live reasoning-only run folds; completed reasoning-only rows still
  stay visible.
- The stream records its earliest part timestamp so the live clock starts
  when reasoning starts, matching the persisted span.
- useSmoothStreamingText re-arms its reveal loop on effect setup: under
  StrictMode the simulated unmount disposed it, freezing a block that
  mounted mid-stream (visible when expanding the fold in dev/Storybook).
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

@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: c822df18dc

ℹ️ 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 thread site/src/pages/AgentsPage/AgentSettingsGeneralPageView.stories.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatConversation/WorkingBlockDisclosure.tsx Outdated
Comment thread site/src/pages/AgentsPage/components/ChatConversation/WorkingBlockDisclosure.tsx Outdated
The expanded working block only shifts the scroller when its previous
first row is still a member, so the live row handing off to its
persisted step no longer moves the reading position.

Also from the Codex review on c822df1:
- A partial block's failed-step badge reads as a lower bound.
- The collapse preference switch behavior moves from story play
  assertions into a Vitest test; the stories only arrange the state.
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

Head a70cc67 addresses the five threads from the previous round (Vitest coverage for the preference switch, partial failed-step badge, prepend-only scroll compensation; the partial-timestamp thread is answered with evidence).

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

@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: a70cc67574

ℹ️ 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 +334 to +336
failedCount: tools.filter(
(tool) => tool.isError || tool.status === "error",
).length,

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 Include renderer-detected failures in the collapsed badge

When process_signal returns an ordinary {success: false} result, its renderer promotes that response to an error even though is_error and status remain non-error (Tool.tsx:1113-1126). This predicate therefore leaves the failed tool out of failedCount, so with collapsing enabled the failure is hidden behind a normal Completed N steps summary with no warning badge. Compute the count using the same soft-failure classification used by the tool renderers.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Leaving this for the PR owner to decide. The badge counts protocol-level failures (is_error or status === "error"), which is also what the raw tool rows report; the soft-failure promotion is per renderer and differs by tool (process_signal checks success === false, start_workspace and the renderer at Tool.tsx ~414 check result.error, the subagent renderer has its own state), so counting them here means extracting a shared classifier from ~6 renderer sites in Tool.tsx. That is a refactor of the tool renderers rather than a fold change; if wanted it is better done as a follow-up so both the rows and the badge use one rule.

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

Comment on lines +60 to +62
await expect(
canvas.getByRole("button", { name: "Worked for 12s (2 steps)" }),
).toHaveAttribute("aria-expanded", "false");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move working-block assertions out of story plays

The new working-block story suites use play functions as assertion-heavy behavior tests, starting with this rendered-state check and continuing through keyboard, callback, expansion, and pagination assertions in both working-block story files. Pixel already captures the arranged visual state, while valuable behavior checks belong in Vitest; leave these plays setup-only and move the interaction assertions to React Testing Library tests.

AGENTS.md reference: site/AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Leaving this for the PR owner to decide. These plays were written before #29016 reworded FE1 on main, and the same pattern still exists on main (for example TogglesSendShortcut in this file). The two settings stories were migrated in a70cc67; the working-block suites cover keyboard, expansion, pagination and reading-position behavior that partly depends on real layout (the scroll story cannot move to jsdom), so migrating them is a separate mechanical pass rather than a fix for this PR's behavior.

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

Comment on lines +37 to +40
() => contentHeight,
);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid DOM-geometry assertions in the scroll test

This test globally replaces offsetHeight and later asserts an exact scrollTop, so it verifies a hand-authored jsdom geometry model rather than observable browser behavior and can pass even when real scroller layout or anchoring is broken. Extract and unit-test the prepend decision as pure logic, or cover the actual scroll preservation in a browser-level test instead of mocking layout measurements.

AGENTS.md reference: site/AGENTS.md:L75-L75

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Partly agreed. The observable behavior is covered against the real scroller and layout by the PrependIntoExpandedBlockKeepsReadingPosition story, which now exercises both the in-block page and the final page that also prepends the prompt row (it was red on the a70cc code and is green on a83e850). This unit test only pins the decision that a live row handing off to its persisted step must not scroll, which has no equivalent browser scenario yet; the mocked height is there because jsdom has no layout, not to model geometry. Leaving the thread open for the PR owner: either keep it as the guard's unit test or replace it with a browser story for the handoff.

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

When the last history page both grows an expanded working block and
prepends the prompt row as a new scroller item, MessageScroller restores
the block's top edge from its MutationObserver callback, which ran after
the block's synchronous scroll adjustment and cancelled it, so the
reading position jumped by the block's growth. The block now applies its
growth from a microtask, after that callback and before paint.

The story loads the final page from the top the way paging does and
keeps its control after the rows so the scroller sees the prepend.
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

Head a83e850 adds one fix on top of a70cc67: the expanded working block now applies its prepend growth to the scroller from a microtask, after MessageScroller's own re-anchor of a prepended item, so the final history page (which also prepends the prompt row) no longer jumps the reading position by the block's growth. Covered by the extended PrependIntoExpandedBlockKeepsReadingPosition story.

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: a83e850b05

ℹ️ 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 noted the rejected update branch had no visual coverage; the story
rejects the PUT and leaves the error text arranged for Pixel.
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

Head d13ebe6 only adds the CollapseAssistantStepsSaveError story on top of a83e850; no runtime code changed.

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

@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: d13ebe6b6a

ℹ️ 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 +1623 to +1624
expect(canvas.queryByTestId("conversation-timeline")).toBeNull();
expect(canvas.queryByText("Inspect the workspace")).toBeNull();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move cold-load behavior assertions into Vitest

These checks assert the intermediate loading state inside a Storybook play function, while the later checks also verify the loading-to-folded transition after an artificial timer. Pixel only captures the settled final state, so keep only the wait needed to arrange that screenshot here and cover the transition behavior with React Testing Library and Vitest instead.

AGENTS.md reference: site/AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Same question as the working-block story thread above: these plays predate the FE1 rewording in #29016. Leaving it for the PR owner to decide whether the story suites get migrated in this PR or in a follow-up pass.

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

export const CollapseAssistantStepsSettings: FC = () => {
const queryClient = useQueryClient();
const query = useQuery(preferenceSettings());
const mutation = useMutation(updatePreferenceSettings(queryClient));

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 Retain the successful preference value when refetch fails

When the preference PUT succeeds but the invalidation refetch fails, updatePreferenceSettings discards the successful response and leaves the old cached preference in place, so this controlled switch snaps back to its previous value even though the server saved the change. Unlike the already-handled initial-load failure, this post-save failure misrepresents a completed mutation; seed the cache from the mutation response before invalidating or otherwise preserve the successful value.

AGENTS.md reference: site/AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This uses the shared updatePreferenceSettings mutation from #/api/queries/users (mutate, then invalidate preferenceSettingsKey), the same as the other preference switches on this page, so a failed refetch after a successful PUT behaves identically for all of them. Changing the shared mutation to seed the cache from the response is a page-wide change rather than part of this preference; leaving it for the PR owner.

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

if (
!content ||
!previous ||
previous.firstRowKey === firstRowKey ||

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 Compensate for stable-key read-file prepends

When pagination prepends more read_file calls into the first merged row of an expanded partial block, getDisplayMessageKey deliberately preserves that row's key even though its content grows above the reader. This equality check therefore skips the height correction and lets the current content jump downward. The fresh case beyond the earlier scroll fix is that a genuine prepend can grow a stable-key merged row, so prepend detection also needs to account for changes within that row.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed this is a real gap, and a narrow one: it needs a read_file group at the block's first row to be split by a page boundary. getDisplayMessageKey keeps the group key stable on purpose, so the key comparison here cannot see the growth. The fix would be to identify the block by its member message ids instead of row keys (a prepend is then "the first id got smaller and the previous first id is still a member", and the live handoff has no previous id), which also changes the prop, the unit test and the story. Leaving it for the PR owner to decide whether it lands here or as a follow-up; note the same top-level group already grows without compensation on main when it is a scroller item of its own.

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

current.rowIndices.push(index);
current.containsLiveRow ||= row.type === "live";
for (const tool of content.visibleTools) {
current.tools.set(tool.id, tool);

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 Scope fallback tool IDs to their message rows

When persisted tool-call parts omit tool_call_id, parseMessageContent generates IDs only from the part index, so two consecutive single-tool assistant messages both expose tool-call-0. This block-wide map then overwrites the first tool, causing the disclosure to report one step instead of two and potentially discard one row's failure state; scope fallback identities to the message or row while retaining deduplication for genuine protocol IDs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The tool-call-<index> fallback only exists for persisted parts without a tool_call_id; chatd always writes ids for tool calls and results, and the map is keyed by id precisely so a call and its result from different messages merge into one step. Scoping fallback ids to their message would only change behavior for malformed transcripts. Leaving it for the PR owner; I would not change it here.

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

Comment on lines +485 to +486
isTurnActive:
chatStatus === "running" || chatStatus === "interrupting",

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 Stop the working timer during interruption

When a turn with persisted steps enters interrupting, the interrupt live row stays outside the fold, but this condition still marks the latest historical block live and keeps its Working for ... timer ticking beside the stopping indicator. The fresh state not covered by the requires-action fix is interrupting: deriveLiveStatus explicitly says this phase must not claim the agent is still producing output while the worker drains, so complete or pause the disclosure timer during interruption.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Deliberate: the PR treats interrupting as still working because the worker is draining the turn and the steps are not finished, and the completed label then uses the persisted timestamps, so the tick during the (short) interruption does not leak into the final duration. If the preference is to freeze the timer as soon as the interrupt is requested, that is a one-line change to isTurnActive; leaving the call to the PR owner.

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

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