Skip to content

Reuse cached parser size when two split panes show one session - #658

Merged
edwin-zvs merged 1 commit into
mainfrom
fix-split-program-lag-2
Jul 1, 2026
Merged

edwin-zvs merged 1 commit into
mainfrom
fix-split-program-lag-2

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #650 — that fix stopped the accumulation of stale Program state, but the user found the TUI still goes "extremely laggy, unusable" from a single occurrence of this concrete sequence:

  1. Create a session (Add in-app text selection for TUI copy #1), open its Program view, edit and save.
  2. Create session docs(agents): require branch + worktree + PR for all changes #2, positioned just above Add in-app text selection for TUI copy #1 in the session list. Split the window and switch the new pane to show docs(agents): require branch + worktree + PR for all changes #2.
  3. Delete (or archive) docs(agents): require branch + worktree + PR for all changes #2.

Deleting/archiving #2 triggers focus_neighbor_of, which reassigns #2's pane to its list neighbor — session #1. Both panes now show session #1.

Root cause

ItemHistory (the per-session PTY/tool-block replay cache) holds a single cached parser sized to whichever width it was last asked to replay at. Two panes showing the same session at two different widths call history.replay(width, ...) with alternating widths every single frame — each call invalidates and rebuilds the cache the other pane just set.

  • For a plain PTY session (replay_cached), the rebuild is a bounded tail re-feed — cheap.
  • For a tool-block session (smith/claude/codex — anything that ever emitted a ToolBlock), replay_full's cols-change path rebuilds from item 0 on every call. Measured: a couple thousand accumulated lines with alternating cols took multiple seconds per frame; a 50k-line real-world history didn't finish in over a minute. Stable cols, by contrast, is near-instant (existing replay_full_scales_with_history_size test).

This is the same failure family the existing pin_tile_reuses_cached_size_to_avoid_split_thrash test already guards against for the split+pin-strip case — it was just never guarded for two ordinary split panes converging on one session, which wasn't reachable before this delete/archive-neighbor-reassignment behavior existed.

Fix

render_terminal_for_window now tracks which sessions have already been replayed this frame (terminal_replayed_sessions_this_frame, cleared every frame). The first pane to render a session uses its own natural width as before. Any later pane rendering the same session this frame reuses the first pane's cached_dims() instead of forcing its own — so the cache never actually alternates. Trade-off: the duplicate pane's content may not perfectly fill its own width, which is a much better outcome than a multi-second-per-frame freeze.

Test plan

  • New regression test two_split_panes_same_session_reuses_cached_size_instead_of_thrashing: two split panes at deliberately unequal widths show one tool-block session; asserts 30 frames stay under 1s. Verified it actually catches the regression (fails at ~2.7s with the fix disabled, passes at ~0.08s with it enabled).
  • New integration test two_windows_converging_on_one_session_with_program_open: drives the real open_program_popup / select_session / split_active_window / on_session_deleted methods end-to-end and checks Program-popup bookkeeping stays consistent through the convergence.
  • cargo test -p agentd-cli — 519 passed, 0 failed (including the pre-existing pin_tile_reuses_cached_size_to_avoid_split_thrash and replay_full_scales_with_history_size).
  • cargo build / cargo clippy -p agentd-cli --no-deps — no new warnings.

A stale window-selection reassignment — the neighbor a deleted or
archived session's pane falls back to (focus_neighbor_of) — can leave
two split panes showing the same session. ItemHistory caches a single
parser sized to whichever width it was last replayed at; two panes at
two different widths alternate that width every frame.

For a plain PTY session this is a bounded-tail rebuild (cheap). For a
tool-block session (smith/claude/codex — anything with a ToolBlock
item), replay_full's cols-change path rebuilds from item 0 on every
call: measured multiple seconds for just a couple thousand
accumulated lines once cols alternate every call, versus near-instant
when cols stay stable. Same failure family as
pin_tile_reuses_cached_size_to_avoid_split_thrash, which already
guards the split+pin-strip case — just never guarded two ordinary
split panes converging on one session.

render_terminal_for_window now reuses the first pane's cached parser
size for any later pane rendering an already-replayed-this-frame
session, trading a slightly-off-size duplicate render for avoiding
the rebuild.
@edwin-zvs
edwin-zvs merged commit 366a79f into main Jul 1, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the fix-split-program-lag-2 branch July 1, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant