Skip to content

perf(chat): place transcript rows through a gap-aware index - #32837

Merged
teamleaderleo merged 4 commits into
feat-cmux-nextfrom
leo/transcript-gap-index
Nov 3, 2026
Merged

teamleaderleo merged 4 commits into
feat-cmux-nextfrom
leo/transcript-gap-index

Conversation

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Carries #26833's purpose onto base's current placement: "Drawn transcript heights now update the reported row indices in an immutable prefix index. VirtualTranscript, visible-range queries, anchors, folds, and both minimaps read that geometry directly, without rebuilding or materializing full tops/heights arrays after a measured-height change or range movement." #26833 can't be merged as it stands: base's placement now sets each row's gap from its neighbours, and a plain prefix sum of heights can't express that (#26833 comment 6591297220). This PR redesigns the index so the gaps are part of it.

Part of manaflow-ai/cmuxterm-hq#4740. Replaces #26833.

What changes

  • placedHeightIndex.ts: the immutable index (leaves of 64 rows, path copying) is now gap-aware. Each segment stores its row count, its sum including the gaps between its own drawn rows, how many rows it draws (capped at 2), its first and last two drawn marks, the first drawn row's own gap, and its last row's height. Joining two segments adds transcriptGap(left.lastMark, right.firstMark) * scale only when both sides draw. So a row that starts or stops drawing changes the next drawn row's gap without touching that row's leaf, and zero-height rows are skipped the same way placeRows skips them. New: fromRows, splice (a new tail, or a truncate, in O(log rows + 64 + new rows)), update(index, height, mark?, gap?), gapAt, markAt, scan (an ordered walk), and lastMarks for the tail metrics. The plain fromHeights/update/append API and its tests stay.
  • model.ts: placeRowsIndexed(estimate, heightAt, previous?, origin) returns the same tops, heights, total, tailGap and restBand as placeRows for the same input. A reused placement keeps its index and reads only the changed rows and the rows from from on. Another scale or origin, or no index, places every row again, as placeRows does. placedWithOrigin moves the origin in O(1); withOrigin copied every top. PlacedLayout holds geometry and no tops/heights arrays. Estimates (layoutConversation) still use arrays; layoutGeometry reads either kind in O(1).
  • App.tsx: the measured memo places through placeRowsIndexed. It passes the rows kept by object as from, and the indices of rows whose drawn height changed as changed. Every tops/heights reader in App moved to layout.geometry: the resting and reported anchors, keyboard row focus, mounted spans, opening at an anchor, both fold inputs, Find and count reveals, and each row's top/gapBelow.
  • Downstream readers moved to geometry: scrollAnchor (upper bound), followGlide (heightAt), the chat minimap (rowPlaces: one ordered walk per placement, cached per geometry), the thread rail (RowTops, reading a few turns at a time) and mapMarks (one scan).

Equivalence

placeRowsIndexed.test.ts compares placeRowsIndexed with base's placeRows on every top, height, gap, total, tail gap, rest band, origin and visible range:

  • random rows over 40 seeds: a third of them zero-height, every mark kind, own gaps (no-marks mode), origin 0 and non-zero;
  • the gap rules by hand: a prompt, then hidden rows, then a message takes the turn gap; a trailing empty footer gives the block tail and the rest band; a row starting to draw moves its neighbour's gap;
  • the tail with no drawn row, one drawn row, and a footer after it;
  • 12 seeds of 30 incremental passes, each chained from the last as App chains them: a token (last row grows or a row arrives), an insert mid-list, three rows mid-list redrawn (one may start or stop drawing), and a removal mid-list;
  • an origin change places every row again; placedWithOrigin matches withOrigin; an unplaced estimate reads like its arrays;
  • read counts: a resize reads exactly 2 rows at both 9,000 and 90,000 rows, and a streamed token reads 1 row and leaves the old snapshot unchanged.

placedHeightIndex.test.ts adds tests of tops, gaps and upper bounds against the sequential walk across leaf boundaries (1 to 700 rows), of rows starting and stopping drawing, and of splice in every shape.

Sums are joined in tree order, so a top can differ from the left-to-right pass by float rounding only. The tests allow 1e-6 px. Their heights are dyadic, so they actually match exactly.

One case where App's reuse hint differs from base: when a drawn height changes above rows kept by object, base reads every row from the changed one on, and this reads only the changed rows. Both give the same result unless a kept row's own inputs changed without its drawn height changing. Base doesn't pick that row up either when nothing above it changed, so this adds no new case.

Performance

Model-only, the same harness run once at this head: bench/height-index.bench.ts. It runs base's placeRows as "before" (it is still in model.ts, unchanged) against placeRowsIndexed as "after", fed the same estimate, marks and drawn heights. Each pass chains from the last one, as App does.

Bench run 41561212681 (8-vCPU Linux, Bun). Each row is ms per placement pass, p50 / p95, over 300 passes at 9,000 rows and 100 passes at 90,000 rows. Both sides gave the same tops and total on every workload.

Metric Before (placeRows) After (placeRowsIndexed) Delta Harness
9,000 rows, token (last row grows), p50 / p95 0.0091 / 0.0527 ms 0.0042 / 0.0256 ms 3.9x total ROWS=9000 PASSES=300 bun bench/height-index.bench.ts, run
9,000 rows, mid-list resize, p50 / p95 0.0554 / 0.0703 ms 0.0038 / 0.0110 ms 12.1x total same
9,000 rows, resize near the top, p50 / p95 0.0640 / 0.0951 ms 0.0030 / 0.0083 ms 21.4x total same
90,000 rows, token, p50 / p95 0.0397 / 0.4927 ms 0.0064 / 0.0396 ms 10.7x total ROWS=90000 PASSES=100, same run
90,000 rows, mid-list resize, p50 / p95 0.3287 / 0.9604 ms 0.0064 / 0.0216 ms 50.6x total same
90,000 rows, resize near the top, p50 / p95 0.5202 / 1.3969 ms 0.0063 / 0.0204 ms 72.8x total same
Rows read for a resize of two rows, 9,000 and 90,000 rows every row from the first change on 2 placeRowsIndexed.test.ts, run

An earlier run, 41551700215, gave the same shape: at 9,000 rows a 4.5x / 12.0x / 13.9x total speedup, at 90,000 rows 9.8x / 34.6x / 59.0x.

The absolute win is small at today's chat sizes: at 9,000 rows base's pass is already about 0.06 ms, because placeRows keeps the unchanged prefix. The gain matters for long chats (about 1 ms p95 to about 0.02 ms at 90,000 rows) and for measures near the top.

What this does not change: layoutConversation (the estimate) still allocates and copies its four O(rows) arrays whenever the rows change, so with that in place a streamed token is still O(rows) memcpy end to end. Moving the estimate onto the index is the follow-up. The chat minimap's and rail's full-row passes stay O(rows) per paint, as on base; the minimap doesn't repaint while a reply streams. No browser frame claim.

Timers and observers

None added. The minimap's place cache is a WeakMap keyed by immutable geometry, so it is freed with its placement.

Size

About 1,550 changed lines (+1,208 / -345). Kept as one PR: the index redesign changes the placement type that every reader in App.tsx and the minimaps reads, so landing the index alone would leave production on placeRows with an unused second geometry API (the state #26745 left base in). About 650 of the lines are the equivalence tests.

Changelog

Changed: Transcript rows are placed through a gap-aware index, so a streamed token or a measured row no longer copies every row's place.

No capture: no visual change; placement output is equal to base's by test.

Observed / Not run

  • Ran on the fleet at this head: the 49 tests of the placement index, its geometry, the anchor, follow, minimap paint and scroll map, 49 pass and 0 fail (41561212681). The full webview suite (41552476154) ended with 13 failing files. I re-ran those 13 at base 3aca90c and at this head (41560098877 base, 41560105019 head). The same 10 fail on both (chat-header-plain-icons, chat-header-tab-row-surface, check-system-blue, find-import-pressed, gallery-coverage, hairline-rims, theme-colors, tooltip-rules, ui-rules, agent-pane-queue-row-click). The other 3 pass on both (thread-minimap-default-wave, summary-scroll-steady, agent-pane-short-pane-composer). That full run was cut short by its deadline, so not every file ran at this head.
  • bun run check (lint, format, type check, check:pieces): green: format, lint, type check and every check:pieces step (41561212681); at the merged head's tree, bun run check exited 0 and 79 of 79 tests passed across 10 files (41562213494).
  • Not run: the browser anchor/drift oracle (perf(chat): update measured transcript geometry by row index #26833's 3,000-turn harness) and the WebKit scroll-frame bench.

🤖 Generated with Claude Code

Agent session: agent session

teamleaderleo and others added 4 commits November 3, 2026 00:15
The prefix index now folds base's gap rules into its leaves: each segment
keeps its first and last two drawn marks, so a drawn row's gap comes from the
nearest drawn row above it across leaves, and the tail gap and rest band read
from the root. placeRowsIndexed gives placeRows' output from it, reusing the
last placement's index and reading only the changed rows and the new tail.
App.tsx and the transcript's geometry readers (anchors, folds, follow, both
minimaps) read tops through it instead of tops/heights arrays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-627034dbf10a04318c5c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-627034dbf10a04318c5c
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review f3ed6b7: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr32837 -f ref=leo/transcript-gap-index -f platform=macos -f backend_mode=local

Gallery before/after for f3ed6b7: 16 states rendered, 0 images differ, 3051 more touched states not rendered. Images are in the run's artifact, not on this public PR; the independent review of this head reads them on its own, so the PR needs no hand-made screenshots of these states.

gh run download 41563308811 -R manaflow-ai/cmux -n gallery-advisory-32837-f3ed6b7ba3362a79507a8684093b254f63399576 (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.chat-minimap/streaming Apple System Colors Light unchanged 0% own ChatMinimap.tsx
agent-pane.chat-minimap/streaming Apple System Colors unchanged 0% own ChatMinimap.tsx
agent-pane.chat-minimap/at-rest Apple System Colors Light unchanged 0% own ChatMinimap.tsx
agent-pane.chat-minimap/at-rest Apple System Colors unchanged 0% own ChatMinimap.tsx
agent-pane.chat-width/standard Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/standard Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide Apple System Colors unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors Light unchanged 0% own App.tsx
agent-pane.chat-width/wide-blocks Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/short-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/short-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/wide-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/wide-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-edge/no-edge Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-edge/no-edge Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/scrolled-to-end Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-floats/composer-grown Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-40 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-40 Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-56 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-56 Apple System Colors unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-72 Apple System Colors Light unchanged 0% own App.tsx
agent-pane.composer-rest-gap/gap-72 Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/shipped Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/shipped Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/two-thirds Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/two-thirds Apple System Colors unchanged 0% own App.tsx
agent-pane.continuation-spacing/half-tucked Apple System Colors Light unchanged 0% own App.tsx
agent-pane.continuation-spacing/half-tucked Apple System Colors unchanged 0% own App.tsx

Pick other states with a Gallery: entry.id, entry.id/variant line in the PR body.

Shipped in:

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6.1-sol high, sweep, via the review router; full diff from merge base 7d1037f to this head): SHIP at f3ed6b7

No P1/P2 findings.

@teamleaderleo
teamleaderleo merged commit c8a72a9 into feat-cmux-next Nov 3, 2026
202 of 207 checks passed
@teamleaderleo
teamleaderleo deleted the leo/transcript-gap-index branch November 3, 2026 05:18

This branch was previously deployed

1 inactive deployment
ci-routing-agent — f3ed6b7b Deployed Nov 3, 2026 by teamleaderleo via cmux-tui tree preflight #103434
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant