3.8 KiB
History Render Cache Bridge
This document captures the current bridge between the new HistoryState data
model and the legacy per-cell layout caches that still live inside a few
history cells (ExecCell, streaming/assistant cells, and diff cells). The
bridge allows ChatWidget to render the unified history vector without
re-computing expensive layouts every frame while we complete the Step 6 renderer
work.
Rendering pipeline overview
- The draw loop in
ChatWidget::render_historyassembles a list ofRenderRequestvalues. Each request contains theHistoryIdof the record being rendered, the existingHistoryCellcache if one exists, and a set of "fallback" lines derived on demand from the semanticHistoryRecord. HistoryRenderState::visible_cells()receives those requests along with the currentRenderSettings(width, theme epoch, and reasoning visibility toggle). The render state stores cached layouts in aHashMap<CacheKey, CachedLayout>, whereCacheKeyis(HistoryId, width, theme_epoch, reasoning_visible).- When a cache entry exists,
visible_cells()returns the layout immediately. Otherwise it invokes the builder closure supplied in the request. The closure uses theHistoryCellif one exists, and falls back to the semantic lines so the renderer can still make forward progress even when we no longer materialize a trait object. - The resulting
VisibleCellbundles the resolved layout (plus the assistant streaming plan, if applicable).ChatWidgetuses theseVisibleCellsnapshots to copy pre-rendered buffer rows into the terminal buffer.
Cache invalidation
HistoryRenderState exposes several targeted invalidation helpers:
invalidate_history_id(HistoryId)removes cached layouts for a single record. We call this whenever a record’s semantic state changes (for example when streaming assistant output arrives).handle_width_change(width)prunes cache entries whose width no longer matches the viewport. Width changes also clear prefix sums so we rebuild the height map.invalidate_all()clears the layout cache and prefix sums. We use this when restoring snapshots or performing operations that massively reshuffle history.
The prefix sum cache (prefix_sums, last_prefix_* fields) is separate from
the layout cache. It stores cumulative heights so that scrolling computations
are O(1) per frame.
Fallback behaviors
RenderRequest::fallback_linesare derived from the semanticHistoryRecord. This ensures that even if a legacyHistoryCellcache is missing (for example, after deserializing a snapshot from disk) we still have content to render.ChatWidget::draw_historykeeps the oldHistoryCellobjects around only as an optimization; any cache miss rebuilds the layout from semantic state and stores the result in the shared cache.- Cells that still own per-width caches (
ExecCell, diff/assistant streaming) remain responsible for invalidating their internal caches. The history render cache treats those cells as opaque; once Step 6 lands we can remove the per-cell caches and rely exclusively onHistoryRenderState.
Removal roadmap
The interim bridge exists solely to prevent regressions while we finish Step 6 of the plan. Once all cells render from structured state and the shared cache proves stable, we can:
- Delete the per-cell layout caches (
ExecCell::cached_layout, assistant streaming wrappers, diff caches). - Convert all render paths to supply semantic state only, removing
HistoryCelltrait objects from the steady-state draw loop. - Simplify
ChatWidget::draw_historyso it only iterates overHistoryState.records.
Tracking this work in the plan keeps the team aligned on why the bridge exists and what remains before it can be removed.