Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/web-ui/src/flow_chat/components/FlowTextBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ export const FlowTextBlock = React.memo<FlowTextBlockProps>(({
// A live message stays one selection/reveal owner. Only its changing suffix
// enters ReactMarkdown; sealed semantic prefixes keep memoized renderers.
const sections = useMemo(() => markdownSection || parsedContent.hasProtocol || !textItem.isMarkdown
? [{ start: 0, end: displayContent.length }] : markdownSections(displayContent),
[displayContent, markdownSection, parsedContent.hasProtocol, textItem.isMarkdown]);
? [{ start: 0, end: displayContent.length }] : markdownSections(displayContent, { streaming: isVisuallyStreaming }),
[displayContent, isVisuallyStreaming, markdownSection, parsedContent.hasProtocol, textItem.isMarkdown]);

const renderMarkdown = (markdownContent: string, key?: React.Key, streaming = markdownStreaming, continuation = false) => (
<MarkdownRenderer
Expand Down
4 changes: 4 additions & 0 deletions src/web-ui/src/flow_chat/components/modern/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ before reporting a defect as new.
- Opening readiness follows the viewport's settled layout signal. The user
bubble may legitimately be virtualized away at a long reply's end; do not
reintroduce user-message polling or a fixed opening delay.
- A session switch remounts the transcript. Only derived, rebuildable state
survives it — the timeline projection and measured row sizes by stable item
key, in `timeline/retainedViewMemory.ts`, bounded to the three most recently
left views. Follow, anchor, opening and viewport ownership stay per mount.
- Viewport snapshot callbacks stay current through a ref. Capture synchronously
only on actual session unmount, not in effect cleanup on every history-window
change: mutation-phase geometry can see half-replaced virtual padding.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,9 @@ content such as history state and `RuntimeStatusSlot`.
invisible where the platform draws overlay scrollbars that take no layout
width — WebKit-backed builds, where `scrollbar-gutter: stable` reserves
nothing either. There the drag can still fight the frame loop while output
streams. Closing it means either a signal that does not depend on the bar
streams. A supplied-geometry fixture shows the same fight after the Turn has
settled, for as long as follow-output still holds the tail: a scroll with no
intent event is eased back by up to the whole step each frame. Closing it means either a signal that does not depend on the bar
having a box, or a scrollbar of our own.
- A collapse larger than `tailHoldMaxGapPx` still moves the viewport, by the
excess only.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ each missing what the other had.
| `../../store/modernFlowChatStore.test.ts`, `../../grouping/selectors.test.ts`, `../../grouping/activityGrouping.test.ts`, `../../../shared/utils/resourceBudget.test.ts` | cached/full projection equivalence across repeated thinking/tool-parameter streams, interior updates, rollback, completion and legacy retry/recovery; alternating live/history/side projections, completed-row reuse on append, same-store clear/reopen invalidation, warm equal-ID source isolation, group alias restoration and group-position independence; interrupted-prefix and shared-reference byte accounting |
| `useFlowChatVirtualizer.initial-window.test.tsx`, `useFlowChatSearch.test.ts` | delayed observer delivery after live prepend / history tail insertion resizes the surviving key; suspended mounts are observed on resume with or without origin handoff; closing one search releases its match cache without affecting another view |
| `../../perf/longRoundPerfHarness.test.tsx` | deterministic settled-row/block identity reuse, including interleaved history and side projections, plus diagnostic stage timings; supplied DOM fixtures, not native frame-rate or live remote evidence |
| `../../../infrastructure/markdown/markdownSections.test.ts`, `../../timeline/document.test.ts` | exact source reconstruction and equivalent Markdown ASTs across LF/CRLF/CR, lists/fences/tables/quotes/math, conservative reference/HTML fallback, canonical identities, Unicode search counts, bounded section estimates, no live completion repartition, and display-only narrative ID handoff with canonical bookmark/navigation IDs, retry/source isolation and alias-collision recovery; native single-attempt metadata omission and restored blank-line section coordinates without modifying host text |
| `../../../infrastructure/markdown/markdownSections.test.ts`, `../../timeline/document.test.ts` | exact source reconstruction and equivalent Markdown ASTs across LF/CRLF/CR, lists/fences/tables/quotes/math, conservative reference/HTML fallback, canonical identities, Unicode search counts, bounded section estimates, no live completion repartition, and display-only narrative ID handoff with canonical bookmark/navigation IDs, retry/source isolation and alias-collision recovery; native single-attempt metadata omission and restored blank-line section coordinates without modifying host text; running groups list only in-flight tool calls (serial, parallel, between steps, folded approval, retained narrative) and keep that under reveal/reader holds, while explicit expansion lists all and settled groups fold |
| `../FlowTextBlock.test.tsx`, `../../../infrastructure/markdown/MarkdownCodeBody.test.tsx` | sealed live-prefix DOM/parser reuse, one shared decoration observer, selected-node retention, source-only oversized fences and cleanup; DOM fixtures, not native paint evidence |
| `../../trajectory` | bounded long-session rail, stable range identities, keyboard preview vs selection, searchable summary pagination, legacy-peer gating, stale device/query rejection, canonical peer body navigation, shared detail components and bounded output; local DOM/transport fixtures, not native rendering or live remote evidence |
| `../FlowChatWidthBoundary.test.tsx`, `../../../infrastructure/appearance/preferences/flowChatWidthStore.test.ts`, `../../../app/scenes/settings/pages/application/FlowChatWidthSettingsSection.test.tsx` | symmetric pointer/keyboard column resizing, token-derived bounds, cancellation/cleanup, narrow-pane restoration, persisted preference compatibility and settings/reset synchronization; supplied DOM geometry, not visual or live remote evidence |
Expand Down Expand Up @@ -61,8 +61,9 @@ each missing what the other had.
| `useFlowChatViewportAnchor.test.tsx` | capture, restore, carry, the settle window |
| `useFlowChatLeadingExtent.test.tsx` | shared event geometry without repeated rectangle reads; fresh layout reads after resize and collection |
| `VirtualMessageList.session-boundary.test.tsx` | prepend compensation, consecutive paging with/without queued input, passive scroll suppression, navigation-target current Turn with gesture/follow/session handoff, search placement only outside the readable viewport, and real viewport-register acknowledgement before trajectory details/highlights commit for user/search/model/folded-tool/history targets |
| `VirtualMessageList.resize.test.tsx` | first-frame node and screen-position retention during repeated CSS-only width previews, release, fractional row geometry, native clamping, both row/container observer orders and delayed scroll delivery; same-size viewport recovery observes suspended mounts without further output, manual re-render or scrolling in live/history layouts; completed follow and reader-owned positions on local/simulated peer surfaces, not native or live-transport evidence |
| `VirtualMessageList.resize.test.tsx` | one viewport ResizeObserver instance kept through streamed flushes on local/simulated peer surfaces; visible rows held still while wheeling into never-measured rows with estimate misses both ways, including scrolls delivered after the gesture window; first-frame node and screen-position retention during repeated CSS-only width previews, release, fractional row geometry, native clamping, both row/container observer orders and delayed scroll delivery; same-size viewport recovery observes suspended mounts without further output, manual re-render or scrolling in live/history layouts; completed follow and reader-owned positions on local/simulated peer surfaces, not native or live-transport evidence |
| `VirtualMessageList.history-prepend.test.tsx` | full list with real virtualization/follow/ownership: stable row identity and screen offset through short/long completed answers, consecutive/dense prepends, delayed measurement/native scroll and reader-chosen positions on local/simulated peer surfaces; supplied row/DOM geometry, not native or live-transport evidence |
| `VirtualMessageList.session-switch.test.tsx`, `useFlowChatVirtualizer.retained-sizes.test.tsx`, `useConversationTimeline.retention.test.tsx` | reopening one of the three most recently left sessions reuses its projection rows and measured row sizes: zero size corrections, fewer commits and row mounts, no dropped rows in the native-history layout, the same settled placement, eviction of older sessions, a single correction for a row that changed while hidden, and no viewport shift when a partly visible retained row grows; supplied DOM geometry on the local surface, not native switching time, paint or remote evidence |
| `VirtualMessageList.timeline-prepend.test.tsx`, `FlowChatPrependSnapshot.test.tsx` | production timeline whole/compound/single-Turn pages; native bottom-origin continuity before observer delivery and zero passive scroll writes, stable hydration lifetime, fractional measurements, bounded long histories, empty-projection navigation, chronological navigation, legacy whole-text and section bookmark coordinates, live handoff and reader-selected block retention; settled host ID replacement retains measured row nodes/positions on every frame and uses canonical bookmarks across reopen; supplied DOM geometry on local/simulated Peer, not native paint or live transport evidence |
| `flowChatScrollbarMask.test.ts`, `VirtualMessageList.session-boundary.test.tsx` | idle gutter masking with symmetric, one-sided and overlay scrollbars; overflow restoration and content/viewport resizing without scroll writes; supplied DOM geometry, not native rendering or live remote evidence |
| `VirtualMessageList.edgeFade.test.ts`, `VirtualMessageList.session-boundary.test.tsx` | repeated scroll-away/back restores the opaque top edge even when an earlier native listener synchronously commits a virtual window; fractional/top-overscroll offsets, one follow notification per event, current gesture callbacks and old-session cleanup; DOM/CSS contracts, not native repaint or live remote evidence |
Expand Down Expand Up @@ -433,6 +434,12 @@ forced colors. Automated DOM/CSS checks do not establish this paint behavior.
6. From that reading position, switch to Settings and back. The session scene
remains mounted with usable geometry while inactive, and the viewport must
not enter host suspension or move to the tail.
7. Alternate between two long sessions several times. Each reopen must paint
its rows at their final heights, with no settling movement or scrollbar-thumb
jump. Repeat after resizing the pane while the other session is shown. Then
visit three other sessions and return: that reopen may settle as a first
open does. Pending: native switching time has not been measured with
retention.

### Submitting and revealing

Expand Down
101 changes: 100 additions & 1 deletion src/web-ui/src/flow_chat/components/modern/FLOWCHAT_VIRTUALIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,65 @@ summaries. Catalog corrections, partial-tail offsets, provisional usage reports
and explicit history-window offsets keep their existing precedence. No Session,
Turn body, viewport position or editor state is retained by this cache.

### Session switch retention

`VirtualMessageList` keys its session subtree by session id, so a switch
remounts the virtualizer, its key cache, every row and the timeline projection.
Before retention, reopening a session cost exactly what its first open did: rows
reserved estimates again, the opening reveal corrected each mounted row, and
rows selected from those estimates mounted only to be dropped. In the
supplied-geometry fixture (24 Turns, real heights far from estimates) a
native-history open spent 7 size corrections, 6 commits, 7 row mounts and 3
dropped rows; a live open 6 / 6 / 6 / 2.

The remount itself stays. Follow, anchor, opening reveal and viewport
ownership start fresh on every mount, and scroll restoration still comes only
from the saved viewport snapshot. What survives is derived and rebuildable:

- `retainedTimelineProjections` (`useConversationTimeline.ts`) keeps the
`ConversationDocumentProjection`, so a reopened view resumes its rows and its
view-local key aliases instead of rebuilding them.
- `retainedTranscriptRowSizes` (`timeline/retainedViewMemory.ts`) keeps the
measured row sizes by stable item key, newest first and at most 4096 per
view. `useFlowChatVirtualizer` seeds them as the rows' reservations on mount
(`retainedItemSizes`) and hands them back through `snapshotItemSizes()`. The
live tail seed sums reservations, so it also starts from retained sizes.

Both are keyed by the view's reader store, which is already scoped by surface
epoch, viewport and session, so device sources never share an entry. A mounted
view claims its entry and returns it on unmount; only the three most recently
left views are held. A retained size is a reservation like any other: a
mounted row is still measured and still replaces it, and a width or font change
leaves it in place under the same rule as a live reservation. It is also a
measurement: the next resize of a retained row is judged as a resize of content
the reader may be looking at, never as a first measurement, so a partly visible
retained row that grows does not shift the viewport. That holds only at the
content width the sizes were measured at, which the snapshot records: reopened at
another width, retained sizes still reserve space but their rows owe the
first-measurement repair again. A retained key that leaves the projection and
returns starts from an estimate rather than handing one back as a measurement.
The snapshot reads no geometry, so the unmount-phase DOM cannot skew it. A retained
projection holds its rows strongly, so up to three inactive views keep their
derived rows until they are reopened or evicted.

With retention the same fixture reopens a native-history session with 0
corrections, 5 commits, 3 mounts and 0 dropped rows, at the same placement. A
live reopen makes 0 corrections and 5 mounts at an unchanged 6 commits; it
still drops 2 rows, because the
one-time tail seed starts at the last item's start before follow takes the end.
A row that changed while the session was hidden costs one correction.
`VirtualMessageList.session-switch.test.tsx`,
`useFlowChatVirtualizer.retained-sizes.test.tsx` and
`useConversationTimeline.retention.test.tsx` cover this with supplied geometry
on the local surface. Native switching time, paint and remote surfaces are not
measured by them.

The Turn rail no longer rebuilds per flush. `ModernFlowChatContainer` keeps the
previous `dialogTurns` input while every Turn's id, user-message content and
composer presentation are unchanged, parses capsule previews once per stored
presentation object, and passes `FlowChatTurnRail` (now `React.memo`) a stable
navigation callback and empty visible-Turn list.

### Settled record identity

Live text/thinking items can have client-generated IDs, while the host's settled
Expand Down Expand Up @@ -194,6 +253,15 @@ changes. Native evidence and remaining coverage limits are in

### Mounted-row lifetime and opening readiness

The list's viewport ResizeObserver lives for the scroller's lifetime and reads
its committed callbacks through a ref. Streaming replaced several of those
callbacks on every flush, and rebinding the observer delivered a fresh initial
observation that re-measured every mounted row once per streamed chunk. The
Turn-boundary list (`userMessageItems`) also keeps its previous array while its
user-message items and indexes are unchanged. Streaming Markdown section
boundaries use a small separate cache, so intermediate prefixes do not evict
settled replies' sections before their session reopens.

Row refs attach without a state update. Search focus and excerpt highlights
resolve the ref in the row's layout phase, so first-mount behavior stays intact
without rendering every newly windowed row twice. Overscan follows its pixel
Expand Down Expand Up @@ -419,6 +487,31 @@ not trigger this readback. All viewport writes remain with the existing owner.
scroll-end delivery using the real virtualizer and supplied geometry; it does
not establish browser performance.

## Running tool group presentation

A running group's automatic disclosure lists only its in-flight tool calls below a
collapsed header that keeps the group summary. Settled thinking folds with the
finished calls; the assistant's text and any thinking still streaming stay visible,
and a thought that just finished stays while nothing follows it so its reveal is
not cut off. A serial run therefore shows one call, and a
parallel batch shows the calls still running. Between two serial calls the most
recent call stays, so the group does not empty and refill for each step. A member
awaiting approval stays visible even in a group the reader folded. The reader's
explicit expansion, an expanded member capsule and a failed file revision still
list every member; a settled group folds to its header as before.

A member row holding focus, a selection or a live instance stays in a live group,
so a settling call never takes the reader's caret or highlighted text with it.
A hold (a revealing text member, or member rows on screen while the reader is not
following) never widens a live group: a group that settles under a hold keeps its
live presentation until the hold releases, and a held folded group still opens.
Treating every hold as expansion flipped a running group between its live call and
every finished call on each streamed reveal, which moved the transcript across the
viewport edge and toggled the scrollbar thumb. This is a timeline projection choice (`readerMemberPresentation` in
`timeline/document.ts`, `liveFlowGroupMembers` in `grouping/lifecycle.ts`). It
does not change group membership, lifecycle, persisted records or the embedded
inline renderer.

## Collapsed thinking content lifetime

Thinking cards mount their Markdown body only while expanded or finishing a
Expand Down Expand Up @@ -536,7 +629,13 @@ the items have not moved yet.
and asks the viewport owner to apply a real measured delta only when the whole
item precedes the viewport in its internal coordinate direction: above the
reader for live layout, below the reader for bottom-origin history. A partly visible row is left alone because its changed content is
inside what the reader is looking at. TanStack's adjustment is always refused:
inside what the reader is looking at. A live row's *first* measurement is the
exception: it replaces a reservation, not content anyone saw, so a row that
starts above the viewport owes its whole delta even when its estimated end
reached into it. Judging that row by its estimated end let rows that mounted
while the reader wheeled up push or pull the visible transcript by the estimate
error (measured in a supplied-geometry fixture: 104px, 166px and 441px single-frame
jumps; zero with the rule). Measured rows that resize keep the visible-row rule. TanStack's adjustment is always refused:
it applies its delta to `scrollOffset`, the library's copy refreshed only from
scroll events. Every continuous writer here assigns `scrollTop` directly and
the matching scroll event lands a frame later, so that base can be stale. The
Expand Down
Loading
Loading