feat(tui): add reverse history search to composer#17550
Merged
fcoury-oai merged 11 commits intomainfrom Apr 12, 2026
Merged
Conversation
Add a Ctrl-R history search mode that uses the footer as the query input and previews matching history in the composer once the user starts typing. The search restores the original draft on Esc, accepts the preview on Enter, and updates shortcut overlay snapshots for the new command.
Add render-only highlighting for Ctrl-R history search matches in the composer preview so accepted drafts keep their plain text. Highlight ranges are computed case-insensitively and cleared when search mode exits.
Improve the reverse history search footer so the active actions read more cleanly while staying within the existing TUI style system. The `enter` and `esc` hints now use cyan bold emphasis, the labels stay dim, and the footer snapshot and style assertions cover the new presentation.
Keep Ctrl-R history search stable when navigation reaches the first or last matching entry instead of treating the boundary as no match. Track exhausted search directions so repeated boundary keypresses do not keep scanning or fetching history before the user changes direction.
Track unique prompt text during Ctrl-R history search so repeated history entries do not appear as separate matches. Cache discovered unique matches so older/newer navigation stays bounded and reversible while skipping duplicate offsets.
Explain the Ctrl+R composer and history search state machines so reviewers can follow draft restoration, async fetches, and dedupe. Keep the documentation scoped to the existing implementation without changing runtime behavior.
Move the Ctrl-R composer search session, footer rendering, and match highlight helpers into a child module so chat_composer.rs owns less feature-specific state. Keep traversal and dedupe in chat_composer_history.rs while moving the focused search tests next to the extracted implementation.
Document the split between composer-owned Ctrl-R search UI state and history-owned traversal state so reviewers can follow the lifecycle and async response contracts. Add the reverse-search mental model to docs/tui-chat-composer.md without changing runtime behavior.
Clear the normal history cursor when Esc cancels a Ctrl-R search so a previewed match cannot leak into later Up/Down navigation. Add a regression test that cancels a matched search from an empty draft and verifies the next Up starts from the newest history entry.
Treat Ctrl+C as a search-mode cancellation before the bottom pane falls back to clearing drafts or triggering global interrupt handling. Restore the original draft through the shared history-search cancel path and cover both bottom-pane Ctrl+C routing and direct composer Ctrl+C key events.
Flush pending paste-burst input before Ctrl+R snapshots the composer draft for reverse history search. This keeps text typed or pasted just before search from being lost when search is canceled or accepted. Add regression coverage for both a held first character and an active buffered paste burst entering history search.
etraut-openai
approved these changes
Apr 12, 2026
Collaborator
etraut-openai
left a comment
There was a problem hiding this comment.
Nice! In addition to reviewing the code (at a high level, anyway), I also built from source and played with the feature. It worked well, and I didn't notice any bugs.
Collaborator
|
This addresses #2622. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The TUI had shell-style Up/Down history recall, but
Ctrl+Rdid not provide the reverse incremental search workflow users expect from shells. Users needed a way to search older prompts without immediately replacing the current draft, and the interaction needed to handle async persistent history, repeated navigation keys, duplicate prompt text, footer hints, and preview highlighting without making the main composer file even harder to review.history-search.mov
Mental model
Ctrl+Ropens a temporary search session owned by the composer. The footer line becomes the search input, the composer body previews the current match only after the query has text, andEnteraccepts that preview as an editable draft whileEscrestores the draft that existed before search started. The history layer provides a combined offset space over persistent and local history, but search navigation exposes unique prompt text rather than every physical history row.Non-goals
This change does not rewrite stored history, change normal Up/Down browsing semantics, add fuzzy matching, or add persistent metadata for attachments in cross-session history. Search deduplication is deliberately scoped to the active Ctrl+R search session and uses exact prompt text, so case, whitespace, punctuation, and attachment-only differences are not normalized.
Tradeoffs
The implementation keeps search state in the existing composer and history state machines instead of adding a new cross-module controller. That keeps ownership local and testable, but it means the composer still coordinates visible search status, draft restoration, footer rendering, cursor placement, and match highlighting while
ChatComposerHistoryowns traversal, async fetch continuation, boundary clamping, and unique-result caching. Unique-result caching stores clonedHistoryEntryvalues so known matches can be revisited without cache lookups; this is simple and robust for interactive search sizes, but it is not a global history index.Architecture
ChatComposerdetectsCtrl+R, snapshots the current draft, switches the footer toFooterMode::HistorySearch, and routes search-mode keys before normal editing. Query edits callChatComposerHistory::searchwithrestart = true, which starts from the newest combined-history offset. RepeatedCtrl+Ror Up searches older; Down searches newer through already discovered unique matches or continues the scan. Persistent history entries still arrive asynchronously throughon_entry_response, where a pending search either accepts the response, skips a duplicate, or requests the next offset.The composer-facing pieces now live in
codex-rs/tui/src/bottom_pane/chat_composer/history_search.rs, leavingchat_composer.rsresponsible for routing and rendering integration instead of owning every search helper inline.codex-rs/tui/src/bottom_pane/chat_composer_history.rsremains the owner of stored history, combined offsets, async fetch state, boundary semantics, and duplicate suppression. Match highlighting is computed from the current composer text while search is active and disappears when the match is accepted.Observability
There are no new logs or telemetry. The practical debug path is state inspection:
ChatComposer.history_searchtells whether the footer query is idle, searching, matched, or unmatched;ChatComposerHistory.searchtracks selected raw offsets, pending persistent fetches, exhausted directions, and unique match cache state. If a user reports skipped or repeated results, first inspect the exact stored prompt text, the selected offset, whether an async persistent response is still pending, and whether a query edit restarted the search session.Tests
The change is covered by focused
codex-tuiunit tests for opening search without previewing the latest entry, accepting and canceling search, no-match restoration, boundary clamping, footer hints, case-insensitive highlighting, local duplicate skipping, and persistent duplicate skipping through async responses. Snapshot coverage captures the footer-mode visual changes. Local verification usedjust fmt,cargo test -p codex-tui history_search,cargo test -p codex-tui, andjust fix -p codex-tui.