TUI history recall rewraps large prompts on each Up/Down navigation

Open 💬 0 comments Opened Jun 23, 2026 by ClSlaid

What issue are you seeing?

In the TUI composer, Up/Down input-history recall can become visibly sluggish when recent or persistent history contains large prompts.

The suspected hot path is that each recalled entry is restored as a whole-buffer text replacement, which invalidates the TextArea wrap cache. Rendering then repeats the same wrapping, grapheme segmentation, width calculation, and cell construction work for text that was just recalled at the same textarea width. Persistent history recall also returns only raw text, so the first render after a lookup does this work on the UI path.

This is most noticeable when stepping repeatedly through multi-KiB prompts, especially prompts with mixed CJK text, ASCII command/path-like tokens, combining marks, and emoji sequences.

What steps can reproduce the bug?

  1. Use the Codex TUI with input history enabled.
  2. Add many multi-KiB prompts to local or persistent history. A representative stress case is 100 entries of about 8 KiB each with mixed CJK, ASCII, combining marks, and emoji/ZWJ sequences.
  3. Focus the composer at a cursor position where Up/Down history navigation is active.
  4. Press Up/Down repeatedly, for example 12 Up recalls followed by 12 Down recalls.

What is the expected behavior?

History recall should remain responsive for recently navigated entries. Repeated Up/Down navigation through the same recent prompts should be able to reuse bounded, width-specific prepared layout data instead of recomputing the wrap/render metadata every time.

Additional information

Root-cause hypothesis:

  • TextArea currently keeps only the active wrap cache for the current text/width.
  • Whole-buffer history restore invalidates that cache.
  • Recalling the same large prompt again at the same width repeats wrapping and render-cell preparation.
  • Persistent history lookup is already asynchronous, but the expensive layout preparation still happens later during rendering.

Proposed fix direction:

  • Add a bounded recent wrap/render cache for TextArea, keyed by textarea width and text.
  • Reuse cached wrap ranges when a history entry is restored at the same width.
  • Materialize rendered cells lazily for visible rows on the UI path, instead of eagerly preparing every wrapped line during local-history recall.
  • Prewarm persistent history through the existing background history lookup path after the composer observes its current width, where heavier preparation can happen off the UI path.
  • Debounce resize-triggered prewarm rebuilds and ignore stale-width results.
  • Keep the cache bounded and skip very large entries so memory usage stays predictable.

Local A/B benchmark

A temporary TUI latency A/B benchmark on macOS compared main against the PR branch in a release-profile codex-tui lib test build. The benchmark uses 100 synthetic local history entries of about 8 KiB each, totaling 833,100 bytes. Entries intentionally mix CJK text, ASCII command/path-like tokens, emoji/ZWJ sequences, and combining marks.

Each sample drives ChatComposer through 12 Up recalls followed by 12 Down recalls in an 80x48 viewport, measuring desired_height, handle_key_event, and ratatui Buffer rendering. Each scenario used 9 samples, for 216 measured steps.

This includes composer history navigation, whole-buffer restore, textarea wrap/cache work, and TUI buffer rendering. It does not include real terminal keyboard input, frame scheduling, terminal diff/flush cost, async persistent-history lookup latency, or OS/terminal rendering effects.

| Scenario | main | PR branch | Delta |
| --- | ---: | ---: | ---: |
| Initial Up 12 + Down 12 | 37.827 ms total; p50 0.1741 ms, p95 0.2096 ms, max 0.3799 ms | 11.103 ms total; p50 0.0510 ms, p95 0.0819 ms, max 0.2757 ms | 3.4x faster total; p95 2.6x faster |
| Repeated Up 12 + Down 12 | 37.210 ms total; p50 0.1740 ms, p95 0.1992 ms, max 0.2115 ms | 5.074 ms total; p50 0.0203 ms, p95 0.0483 ms, max 0.0812 ms | 7.3x faster total; p95 4.1x faster |

The initial traversal is now faster than main; the earlier cold regression is fixed by leaving rendered-cell construction lazy on the UI path. Persistent history prewarm can still do the heavier preparation off the UI path.

I have a PR-ready branch that implements this direction and includes tests for cache reuse, resize debounce behavior, stale-width handling, lazy local-cache rendering, and CJK/emoji render equivalence:

https://github.com/ClSlaid/codex/pull/1

I read docs/contributing.md and understand that external code contributions are by invitation only. If this issue is high-priority enough and the proposed approach aligns with the Codex team's intended solution, could a maintainer please confirm the direction and invite a PR?

View original on GitHub ↗