Skip to content

feat: experimental history browser for interactive history management#38

Merged
eitsupi merged 24 commits intomainfrom
feat/experimental/history-browser
Feb 3, 2026
Merged

feat: experimental history browser for interactive history management#38
eitsupi merged 24 commits intomainfrom
feat/experimental/history-browser

Conversation

@eitsupi
Copy link
Copy Markdown
Owner

@eitsupi eitsupi commented Feb 2, 2026

Add a terminal-based history browser UI similar to the help browser with :history browse [r|shell].

Features

Browse mode

  • Navigation: /k/Ctrl+P up, /j/Ctrl+N down, PageUp/Ctrl+B, PageDown/Ctrl+F, Home/g top, End/G bottom
  • Selection: Space toggle + move down, Tab toggle, Ctrl+A select all visible, Ctrl+U unselect all
  • Actions: Enter copy to clipboard and exit, y copy to clipboard and stay, d batch delete selected (with confirmation)
  • Filter: / to enter filter mode
  • Mouse: scroll wheel for navigation
  • Exit: q, Esc, Ctrl+C, Ctrl+D

Filter mode

  • Filter syntax: host:<name> hostname, cwd:<path> working directory prefix, exit:<N> exit status, free text for fuzzy command match (space-separated = AND)
  • Text editing: character input, Backspace, Delete, Left/Right cursor movement, Home/End move to start/end of input
  • List navigation: /Ctrl+P, /Ctrl+N, PageUp, PageDown, Alt+Home top, Alt+End bottom
  • Selection: Tab toggle, Ctrl+A select all visible, Ctrl+U unselect all
  • Confirm/Cancel: Enter confirm filter and return to browse mode, Esc clear filter and return to browse mode
  • Exit: Ctrl+C, Ctrl+D

Display

  • Multiline commands shown as single line with markers
  • Text scroll animation for long commands that overflow the display width
  • Header shows mode (R/Shell), entry count, and selected count
  • Filter syntax help and keybinding hints in footer

Technical details

  • Use rusqlite directly with SQLITE_OPEN_READ_ONLY and busy_timeout to avoid WAL conflicts during browsing
  • Delete operations use direct rusqlite for write access
  • Cache selected_count to avoid O(n) scan per render frame
  • UTF-8 safe string truncation and cursor manipulation using char_indices()
  • Filter uses in-place reparse() to avoid redundant cloning
  • Use contains() for bitflag modifier matching (Alt, Ctrl)
  • Tab completion for :history browse subcommand
  • Integration tests with polling-based waits (no fixed sleeps)

Files changed

  • history_browser.rs — Main browser implementation (~1340 lines)
  • meta_command.rs:history browse command registration and dispatch
  • completer.rs — Tab completion for the subcommand
  • integration_tests.rs — PTY-based integration tests

Add a terminal-based history browser UI similar to the help browser:
- Browse R or shell history with `:history browse [r|shell]`
- Filter by text (fuzzy), hostname, cwd prefix, or exit status
- Select items with Space, batch delete with d
- Copy commands with y (stay) or Enter (copy and exit)
- Text scroll animation for long commands

Technical changes:
- Use rusqlite directly with SQLITE_OPEN_READ_ONLY to avoid WAL conflicts
- Delete operations still use SqliteBackedHistory for write access
- Add tab completion for `:history browse` subcommand

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an interactive terminal-based history browser UI for viewing and managing R and shell command history, following a similar pattern to the existing help browser.

Changes:

  • Added a new history browser module with fuzzy filtering, text scrolling, and batch deletion capabilities
  • Integrated :history browse [r|shell] meta-command with tab completion
  • Used rusqlite directly with read-only connections to prevent WAL database conflicts

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
crates/arf-console/src/pager/history_browser.rs New 1068-line module implementing the interactive history browser with filtering, navigation, deletion, and clipboard operations
crates/arf-console/src/repl/meta_command.rs Added process_history_browse function and integrated :history browse subcommand with help text
crates/arf-console/src/pager/mod.rs Exported new run_history_browser and HistoryDbMode types, updated module documentation
crates/arf-console/src/completion/completer.rs Added tab completion for :history browse subcommand with r/shell target suggestions
crates/arf-console/tests/integration_tests.rs Added two integration tests verifying cross-session persistence and WAL corruption fix
crates/arf-console/Cargo.toml Added rusqlite workspace dependency
Cargo.toml Added rusqlite 0.37.0 with bundled feature to workspace dependencies
Cargo.lock Updated with rusqlite dependency entry

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

eitsupi and others added 15 commits February 2, 2026 18:00
- Add a second footer line showing supported filter syntax:
  host:<name>, cwd:<path>, exit:<N>, and fuzzy text search
- Adjust visible rows calculation to account for the new line

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Change selection toggle from Space to Tab for consistency with
  help browser and to allow spaces in filter input
- Update filter syntax help to clarify "space = AND" for combining
  multiple filter conditions (e.g., "host:server git push")
- Unify terminology: use "exit" instead of "quit" to match other pagers

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…e for deletion

Use Connection::open() directly instead of SqliteBackedHistory::with_file()
for the delete operation. The previous approach created a competing WAL
connection alongside the main REPL's active history connection, risking
cache inconsistency and database corruption.

Also changes one-by-one DELETE to a single batch DELETE with params_from_iter
for atomicity and performance.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Address roborev findings for commit 432ebf9:
- Add busy_timeout(5s) to prevent SQLITE_BUSY on concurrent access
- Fix misleading "transaction" comment (single statement uses autocommit)
- Add schema drift warning to test helper
- Move deleted_count closer to its usage

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Compute the feedback message directly from ids_to_delete.len() before
the vector goes out of scope, eliminating the redundant intermediate
binding.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Remove redundant thread::sleep calls before expect() (which already
polls), and replace post-browser-exit sleeps with clear_and_expect
for prompt detection. This eliminates flaky test risk under CI load.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The load_history() function runs in a single-threaded UI context with
no concurrent access to the connection. NO_MUTEX disables SQLite's
internal mutex, which is unnecessary here and could mask threading
bugs if the code were ever called from multiple threads.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
session_id was selected at index 3 but discarded (mapped to None),
making subsequent column indices fragile. Remove it from the SELECT
and shift indices 4-7 down to 3-6.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…ersion

Replace io::Error::other(e.to_string()) with io::Error::other(e) to
retain the original rusqlite::Error as the error source chain. This
preserves type information for debugging and error inspection.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add vim-style `/` key to enter filter mode, where all character input
goes to the filter text. In normal mode, single-char keybindings
(q, d, y, j, k, g, etc.) work as navigation/commands without conflicting
with filter input. Esc clears filter and returns to normal mode,
Enter confirms filter and returns to normal mode.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…o filter mode

Extract move_cursor_up/down/page_up/page_down/to_top/to_bottom helpers
to eliminate duplicated navigation logic between filter mode, normal mode,
and mouse scroll handlers. Also add PageUp/Down, Home/End, Tab, Ctrl+A,
Ctrl+U support to filter mode so users can navigate and select without
leaving filter input.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…+B/F conflict in filter mode

Add early return in move_page_up/move_page_down when filtered list is
empty to prevent cursor from pointing to a non-existent index. Also
remove Ctrl+B/Ctrl+F bindings from filter mode's PageUp/PageDown since
they conflict with Emacs-style cursor movement (back/forward) which
users expect in a text input context.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Replace `|e| io::Error::other(e)` closures with `io::Error::other` function
references, and apply rustfmt formatting fixes.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
1. Replace manual selected_count field with derived selected_count()
   method to eliminate desync risk
2. Use safe i64→u64 conversion for duration_ms (try_from instead of as)
3. Show "Copied: ..." feedback after Enter-copy in :history browse
4. Change mouse scroll to move viewport only (not cursor), matching
   standard pager behavior; cursor stays visible via clamping
5. Extract common complete_targets() helper in completer to deduplicate
   browse/clear target completion logic

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…ard scroll underflow

1. Use chars().take() instead of byte slice for command display
   truncation to prevent panic on multi-byte UTF-8 characters
2. Cache selected_count() result in local variable during render
   to avoid redundant O(n) iterations
3. Guard mouse scroll cursor clamping against visible_rows == 0
   to prevent usize underflow on extremely small terminals

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@eitsupi eitsupi requested a review from Copilot February 3, 2026 12:25
@eitsupi eitsupi marked this pull request as ready for review February 3, 2026 12:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

eitsupi and others added 7 commits February 3, 2026 12:35
Space toggles selection and moves cursor down for convenient multi-select.
Tab remains as toggle-only (no cursor movement). Filter mode is unaffected.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
In filter mode, plain Home/End now moves the cursor within the filter
input field. Alt+Home/End retains the list top/bottom navigation.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…ter mode

KeyModifiers is a bitflag type, so exact equality (KeyModifiers::ALT)
would miss cases like Alt+Shift. Use contains() guard to match any
modifier combination that includes Alt.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…der frame

Introduce cached_selected_count field maintained by toggle_selection,
select_all_visible, unselect_all, and delete_selected instead of
scanning all entries on every render call.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…rse()

Remove the redundant clone+reassign pattern where callers modified
raw_query in-place then passed a clone to update(). The new reparse()
method reads raw_query directly, eliminating the unnecessary allocation.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
… Backspace/Delete

Backspace and Delete handlers used unwrap_or(0) / unwrap_or(len) when
resolving char_indices, which could panic on a multi-byte boundary if
cursor_pos were ever inconsistent. Use if-let to skip the operation
when the char index is not found.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…crement

Prevent potential usize underflow panic if the cache were ever
desynchronized, using saturating_sub(1) instead of plain subtraction.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@eitsupi eitsupi merged commit 5a4b08a into main Feb 3, 2026
10 checks passed
@eitsupi eitsupi deleted the feat/experimental/history-browser branch February 3, 2026 13:07
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.

2 participants