Skip to content

feat(tui): add reverse history search to composer#17550

Merged
fcoury-oai merged 11 commits intomainfrom
fcoury/history-search
Apr 12, 2026
Merged

feat(tui): add reverse history search to composer#17550
fcoury-oai merged 11 commits intomainfrom
fcoury/history-search

Conversation

@fcoury-oai
Copy link
Copy Markdown
Contributor

@fcoury-oai fcoury-oai commented Apr 12, 2026

Problem

The TUI had shell-style Up/Down history recall, but Ctrl+R did 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
image

Mental model

Ctrl+R opens 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, and Enter accepts that preview as an editable draft while Esc restores 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 ChatComposerHistory owns traversal, async fetch continuation, boundary clamping, and unique-result caching. Unique-result caching stores cloned HistoryEntry values 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

ChatComposer detects Ctrl+R, snapshots the current draft, switches the footer to FooterMode::HistorySearch, and routes search-mode keys before normal editing. Query edits call ChatComposerHistory::search with restart = true, which starts from the newest combined-history offset. Repeated Ctrl+R or Up searches older; Down searches newer through already discovered unique matches or continues the scan. Persistent history entries still arrive asynchronously through on_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, leaving chat_composer.rs responsible for routing and rendering integration instead of owning every search helper inline. codex-rs/tui/src/bottom_pane/chat_composer_history.rs remains 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_search tells whether the footer query is idle, searching, matched, or unmatched; ChatComposerHistory.search tracks 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-tui unit 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 used just fmt, cargo test -p codex-tui history_search, cargo test -p codex-tui, and just fix -p codex-tui.

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.
@fcoury-oai fcoury-oai marked this pull request as ready for review April 12, 2026 20:42
Copy link
Copy Markdown
Collaborator

@etraut-openai etraut-openai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@fcoury-oai fcoury-oai merged commit 0393a48 into main Apr 12, 2026
22 checks passed
@fcoury-oai fcoury-oai deleted the fcoury/history-search branch April 12, 2026 22:32
@etraut-openai
Copy link
Copy Markdown
Collaborator

This addresses #2622.

@github-actions github-actions bot locked and limited conversation to collaborators Apr 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants