Skip to content

fix(web): start a new turn fold at each user message - #1

Draft
dpatou wants to merge 1 commit into
mainfrom
fix/fold-split-visual-response-at-user-message
Draft

dpatou wants to merge 1 commit into
mainfrom
fix/fold-split-visual-response-at-user-message

Conversation

@dpatou

@dpatou dpatou commented Sep 9, 2026 •

Copy link
Copy Markdown

Status: draft, on hold. Review found that this approach does not cover the main reported scenario and splits the "Worked for …" duration in a misleading way. See Known limitations. The likely next step is to replace it with the approach of pingdotgg#8903 (never fold a non-blank assistant message), pending a decision.

What Changed

deriveTerminalAssistantMessageIds and deriveTurnFolds in apps/web/src/components/chat/MessagesTimeline.logic.ts segment a turn by the user messages preceding its entries instead of grouping by turnId alone. Each user message starts its own "Worked for …" fold with its own terminal assistant message.

  • TurnFold and the turn-fold row carry a foldId. It equals turnId for the common single-segment turn, so existing DOM ids are unchanged, and becomes turnId:index when a steer split the turn. The row's DOM id and expand/collapse state key on it. Citation jumps keep expanding by turnId.
  • Fold duration is computed per segment. latestTurn.startedAt/completedAt is only used when the turn has a single segment; the "You stopped after …" label stays on the last segment of an interrupted turn.
  • Active-turn behavior is unchanged: a running turn stays fully unfolded.

One new test in MessagesTimeline.logic.test.ts covers a steered turn: both responses stay visible, two folds share the turn id but expand independently, and only the last one takes the stopped label.

Why

Sending a prompt while a turn is running is a steer for every provider adapter (Claude, Cursor, Grok, OpenCode, Antigravity): the follow-up response reuses the running turn's id. The fold logic grouped by turnId, so once the turn settled the response to the first prompt was treated as superseded mid-turn narration and hidden behind the fold anchored on the second response. The user only saw the answer to the queued prompt.

Related: pingdotgg#7529 and pingdotgg#7518 (closed by pingdotgg#7723, which keeps the opening message visible), pingdotgg#8879 and pingdotgg#10991 (open, same fold swallowing a substantive answer). pingdotgg#8903 proposes never folding a non-blank assistant message.

Known limitations

Timeline entries are ordered by createdAt, and a steer's user message is persisted at send time. The segment boundary therefore falls at the moment the user pressed send, not at the moment the provider actually picked the prompt up.

  1. Does not cover the main scenario. With Claude Code, a prompt sent mid-turn is delivered only after the current response finishes. The long answer to the first prompt is therefore written after the steer's timestamp, lands in the second segment, and is folded again under the short acknowledgement of the steer. This PR only helps when the first answer had already started streaming before the steer was sent.
  2. Misleading durations. The first fold measures "from prompt to steer", the second "from steer to end", which absorbs all remaining work on the first prompt. A long prompt steered after 17s shows "Worked for 17s" then "Worked for 189s" on the steer, although nearly all of the 189s served the first prompt. Nothing in the entries attributes tool work to a prompt, so no client-side split is honest.

Alternatives considered:

UI Changes

Before: after a steer, a settled turn shows one "Worked for …" row followed only by the response to the second prompt. After: user message, fold, response, user message, fold, response. Screenshots deferred while the approach is under review.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes (not applicable, no motion change)

Verification: vp test run --project unit src/components/chat/MessagesTimeline.logic.test.ts src/components/chat/MessagesTimeline.test.tsx (148 passing), tsc --noEmit in apps/web (clean).

Model and harness: Claude Opus 5 (implementation) and Claude Fable 5.1 (review) via Claude Code in T3 Code.

A steer reuses its turn's id for the follow-up response, and both
deriveTerminalAssistantMessageIds and deriveTurnFolds grouped by turnId
alone. Once the turn settled, the pre-steer response was treated as
superseded narration and stayed hidden behind the later "Worked for X" fold.

Both now segment a turn by the user messages preceding its entries, so each
user message gets its own fold and terminal assistant message. Folds carry a
foldId (turnId, or `turnId:index` when steered) used for the row's DOM id and
its independent expand state, and duration is computed per segment instead of
borrowing latestTurn's span.
@dpatou
dpatou force-pushed the fix/fold-split-visual-response-at-user-message branch from d676b9b to b73a480 Compare September 10, 2026 00:14
@dpatou
dpatou marked this pull request as draft September 10, 2026 00:43
@github-actions github-actions Bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Oct 4, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant