Files
orca/tests/e2e
Brennan BensonandMerge Sim 1798786d4e perf(native-chat): mount only the transcript rows near the viewport (#19869)
* refactor(native-chat): share one row-content derivation between row and list

Windowing needs the list and the row to agree on which messages draw
nothing: a row the list counts but the row declines to render would
reserve estimated height for an empty slot.

Extracts the block derivation out of NativeChatMessageRow into a module
cached on the block array, so a streaming turn pays for it once per
revision rather than once per consumer.

* refactor(native-chat): keep an opened tool run open past its row's lifetime

A tool run, tool line or diff card the reader opened is state they created, but
it lives in the component's own `useState`. That is fine while every row is
mounted forever. It stops being fine the moment rows can be unmounted: the run
silently re-collapses behind the reader's back.

Rows now read their disclosure from a transcript-level map when one is provided
and fall back to their own state when they are rendered standalone. The controls
that re-sync a run — the toolbar's expand-all, a turn's disclosure, a diff
reveal — are folded into the key the choice is remembered under, so a control
flip reads as "nothing recorded yet" and the new default stands without a
mid-render write to a map an ancestor owns.

`ToolLine` moves to its own file; the run was over the line cap with it.

* perf(native-chat): mount only the transcript rows near the viewport

A settled transcript mounts every row it has ever loaded, so the cost of opening
a conversation grows with its length even though only a screenful is legible.
Rows near the viewport are now the only ones in the document; the rest are
reserved as estimated height and measured when they arrive.

Four things had to change for that to be safe:

- `zoom` moves from the transcript column onto the scroll container. Item
  measurements are in the zoomed content's pixels while `scrollTop` is not, so
  with the two split across the boundary the window's arithmetic was off by
  exactly the font scale — correct at the top of a transcript and blank deep
  inside it. The column's padding moves to a new inner element to keep the
  layout it had. This does mean the scrollbar itself zooms with the text.
- The three siblings that made up a row — the message, the turn status, the
  turn's diff rollup — move into one wrapper that carries the spacing they used
  to take from the column. The spacing between rows is the window's `gap`, never
  the height estimate, which would otherwise be counted twice.
- Messages that draw nothing no longer take a slot. Counted but undrawn, each
  one would reserve estimated height for a row that never appears.
- Paging in older history is driven by scroll events alone. Every row that
  resolves its real height moves the content and re-fires the size observers, so
  the old "am I near the top?" test would have asked for another page once per
  measurement. It now also requires the view to have moved upwards and requires
  new items since the last request.

Anchoring is the virtualizer's: `anchorTo: 'end'` re-resolves the row at the
current offset across a count change, which replaces the hand-rolled prepend
anchor, and `followOnAppend` keeps a reader at the bottom pinned there. The
document-level bottom pin stays, because the typing indicator, the activity line
and the column's end padding all live past the last row.

Revealing a diff from a turn rollup can target a row that isn't mounted, so that
row is pinned into the window and the card still reports its own position — a
turn that touched four files lands on the one that was asked for.

* fix(native-chat): let a pinned row reach the mounted window

Two faults the windowing tests turned up, plus the handles they needed.

The virtualizer memoizes its mounted index list on the range extractor's
identity. Holding that identity stable — which is right for the measurement
memo, and was the reason it was written that way — meant a row pinned after the
fact was never picked up: revealing a diff in a row the window had left behind
pointed at a row that stayed unmounted. The extractor now changes identity with
the pinned set, which is not a dependency of the measurement memo, so nothing
expensive is rebuilt.

The offset a row sits at is read off the `offsetParent` chain, with a rect-based
fallback for the case where there is none. Using that fallback for the window's
own scroll margin was wrong in kind: with no layout to measure, it returns the
scroll position itself, so the margin tracked the offset and the window sat at
the top of the transcript wherever the reader scrolled. The margin now takes the
offset chain or nothing; the fallback stays where it belongs, on the reveal.

The scroll root and the window's spacer are named, so measurement can find the
scroll root without depending on which utility class makes it scroll, and so a
test can tell a window from a whole transcript.

* test(native-chat): cover the windowed transcript, and prove the window engaged

The integration harness stubs `offsetHeight` — on the scroll root and on every
row — because that is what the virtualizer measures with, and a DOM without
layout answers zero to all of it. Rows report the height their own estimate
predicted, which keeps the reserved totals exact no matter which rows have been
mounted long enough to be measured.

Every case reads the window through one helper that refuses to pass when there
is no window. Without that, raising the usability gate would send all of them
down the whole-transcript path, where "fewer rows mounted than messages" is
false but every other assertion still holds — and they would go on reporting
green while covering nothing. Reserved height is asserted as an exact total
rather than "greater than zero", which a degenerate empty window also satisfies,
and the mounted range is asserted to bracket the offset rather than merely to be
smaller than the transcript.

Covered: the window mounts a subset and moves with the reader; the newest row
and a reveal's target stay mounted from outside it; an opened tool run is still
open when its row comes back; a message that draws nothing takes no slot; and
the scroll root with no usable height still renders every row as a direct child
of the transcript column.

What the environment cannot show is stated where it matters rather than faked:
its ResizeObserver never fires and a scroll assignment emits no event, so
measurement settling, the bottom pin under a streaming turn, prepend anchoring
and smooth scrolling are covered as pure decisions — height estimation, the
pinned set, range extraction, and whether a position should page in older
history — and left to a real renderer as behaviour.

* docs(native-chat): say that one offset path does read rects

* test(native-chat): pin the window against a row that grows in place

Whole-message appends were covered; a row being replaced by a taller
version of itself — what a streaming reply is — was not. The existing
windowing harness gains two things it needs to see that: a scroll root
with a real document (a height, a viewport, and a scrollTop that clamps),
and a resize observer that delivers when a target's height actually
changed, since happy-dom's never fires and nothing re-measures without it.

Frame by frame, while one row grows from 24px to 6358px: the view stays
0px from the bottom, the row stays mounted, and the reserved total tracks
the measurement rather than the estimate. A reader who scrolls up mid
growth keeps the exact offset they chose for the rest of it.

* test(native-chat): guard history prepend anchoring

* test(native-chat): strengthen prepend anchor contract

* fix(native-chat): preserve provider tool call identity

* fix(native-chat): harden transcript windowing lifecycle

* test(native-chat): install virtualizer viewport for turn timing

* fix(native-chat): reject blank tool call identities

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-10 21:52:21 -07:00
..
2026-06-10 17:23:12 -07:00