feat: finalize search UX and release hardening

This commit is contained in:
stoorps 2026-03-21 16:53:33 +00:00
parent c63b2917da
commit 34f9543a78
Signed by: stoorps
SSH key fingerprint: SHA256:AZlPfu9hTu042EGtZElmDQoy+KvMOeShLDan/fYLoNI
44 changed files with 4983 additions and 94 deletions

View file

@ -0,0 +1,137 @@
# Search Interactive TUI Design
## Summary
This change upgrades `aim search <QUERY>` from a plain text summary into an interactive terminal search flow when stdout and stdin are attached to a TTY. The interactive flow will use `ratatui` for a scrollable result browser, numeric multi-select inspired by `paru`, and an explicit confirmation step before install handoff.
The repository does not currently contain a checked-in config file, but the product already has a user-facing `config.toml` used for themes outside the repository tree. This design extends that existing config contract rather than introducing a second settings path.
## Goals
- Make `aim search <QUERY>` interactive by default on TTY.
- Render one result per row with clear columns for provider, repository, and install query.
- Support bottom-to-top list orientation by default, with config control.
- Support keyboard paging and numeric multi-select in the result browser.
- Show a confirmation step before install handoff unless disabled in config.
- Keep the search domain provider-extensible and avoid coupling core search ranking to terminal UI code.
## Non-Goals
- No `--json` output in this slice.
- No non-interactive rich formatting redesign beyond preserving a readable fallback.
- No general settings overhaul beyond the minimum config foundation needed to read existing theme settings and the new search keys together.
## Recommended Approach
### Option 1: Extend `dialoguer`
This keeps dependencies smaller, but it does not fit the requested behavior well. Paging, bottom-to-top layout, dense row rendering, and `paru`-style numeric selection would have to be simulated awkwardly across prompt screens.
### Option 2: Add a small config layer plus a dedicated `ratatui` search flow
This is the recommended approach. It creates a minimal, reusable settings boundary in `aim-cli`, keeps the current `aim-core` search contract intact, and gives the terminal UI enough control to implement the requested interaction model without twisting `dialoguer` into a pseudo-TUI.
### Option 3: Keep search plain text and launch a second prompt-only selection phase
This is simpler to ship, but it falls short of the requested UX and would likely be replaced immediately. It also duplicates state between the renderer and the selection prompt.
## Architecture
### Config
Add a lightweight config loader in `aim-cli` that reads the existing user `config.toml` location already used for theme settings. The loader should:
- tolerate a missing file by returning defaults
- ignore unknown keys
- treat malformed config as a CLI error with a clear path-aware message
- expose a typed `CliConfig` model for UI code
The search section should be:
```toml
[search]
bottom_to_top = true
skip_confirmation = false
```
This keeps search-specific settings namespaced and avoids a flat `skip_search_confirmation` key that will not scale once more search settings exist.
### Dispatch Flow
`aim search <QUERY>` should continue to build `SearchResults` through `aim-core`. `aim-cli` then chooses one of two render paths:
- TTY path: launch the interactive search browser
- non-TTY path: render the existing plain text summary
The interactive search browser should return one of three outcomes:
- cancelled
- confirmed selection set
- selection set that still requires explicit confirmation
Install execution is not part of this slice. The result of the search browser can remain a terminal-side selection artifact for now, but the code should be shaped so install handoff can be added without reworking the browser state machine.
### TUI Model
Add a dedicated module for interactive search state in `aim-cli`. It should own:
- the ordered result rows
- the highlighted cursor row
- the selected row indices
- the current page and viewport
- the row number buffer for `paru`-style typed numeric selection
- the config-driven orientation flag
- the confirmation mode state
Each visible row should stay one line tall. The row should include:
- numeric index
- provider label
- repository or package identity
- install-ready query
Warnings and installed matches should remain accessible, but the main browser should prioritize remote installable hits. If installed matches are shown in the interactive view, they should render in a distinct section or with a clear marker so they are not confused with remote install targets.
### Key Handling
The search browser should support:
- `j` / `k` and arrow keys for movement
- `Ctrl+d` / `Ctrl+u` or `PageDown` / `PageUp` for paging
- `g` / `G` for jump to top or bottom
- digit entry for numeric selection ranges and comma-separated values
- `Space` to toggle the highlighted row
- `Enter` to continue to confirmation
- `Esc` or `q` to cancel
Numeric selection should accept the same grammar throughout the session, for example `1`, `1,4,7`, and `3-6`. Invalid tokens should not panic; they should produce a small inline validation message and preserve the current selection.
### Confirmation
After the user leaves the browser with at least one selection, `aim-cli` should show a confirmation step by default. That step should summarise the chosen items and require an explicit yes/no confirmation.
If `[search].skip_confirmation = true`, the browser should return the chosen set immediately after selection finalization.
### Error Handling
- Missing or empty search results should not launch the browser; use the existing text renderer.
- Non-TTY stdin or stdout should not attempt `ratatui` initialization.
- Terminal initialization failure should fall back to plain text output rather than aborting the search command.
- Config parse failure should abort with a clear message because silent misconfiguration would be hard to debug.
## Testing Strategy
Follow TDD for each slice:
1. Add config parsing tests for defaults, valid search overrides, and malformed TOML.
2. Add state-machine tests for numeric selection parsing, paging, orientation, and confirmation transitions.
3. Add CLI tests covering TTY-gated fallback behavior and config-driven confirmation skipping.
4. Add a focused rendering test for one-line row formatting to keep the table stable.
Avoid end-to-end terminal snapshot tests that depend on terminal escape sequences unless state-level coverage proves insufficient.
## Delivery Notes
- Keep `aim-core` unchanged unless a search-install handoff type is clearly needed.
- Keep the existing plain text renderer for non-interactive contexts and future `--json` work.
- Preserve current theme behavior and make the new config loader the shared entry point for both theme settings and search settings.

View file

@ -0,0 +1,166 @@
# Search Interactive TUI Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add config-backed interactive search to `aim search <QUERY>` with a `ratatui` browser, numeric multi-select, paging, and an optional confirmation skip.
**Architecture:** Keep `aim-core` responsible for search retrieval and ranking. Add a small config loader plus a `ratatui`-backed state machine in `aim-cli`, and gate the interactive path on TTY availability with a safe plain-text fallback.
**Tech Stack:** Rust, clap, serde, toml, ratatui, crossterm, assert_cmd
---
### Task 1: Add CLI config loading for search settings
**Files:**
- Create: `crates/aim-cli/src/config.rs`
- Modify: `crates/aim-cli/src/lib.rs`
- Modify: `crates/aim-cli/src/main.rs`
- Test: `crates/aim-cli/tests/config_loading.rs`
**Step 1: Write the failing test**
Add tests covering:
- missing config returns defaults
- valid `[search]` config overrides defaults
- malformed TOML returns a path-aware error
**Step 2: Run test to verify it fails**
Run: `cargo test --package aim-cli --test config_loading`
Expected: FAIL because `config.rs` and the config loader do not exist.
**Step 3: Write minimal implementation**
Implement a typed `CliConfig` with nested `SearchConfig` and the minimum loader API needed by `aim-cli`.
Defaults:
```rust
SearchConfig {
bottom_to_top: true,
skip_confirmation: false,
}
```
The loader must tolerate a missing file and reject malformed TOML with the resolved path in the error.
**Step 4: Run test to verify it passes**
Run: `cargo test --package aim-cli --test config_loading`
Expected: PASS
### Task 2: Add a search browser state machine
**Files:**
- Create: `crates/aim-cli/src/ui/search_browser.rs`
- Modify: `crates/aim-cli/src/ui/mod.rs`
- Test: `crates/aim-cli/tests/search_browser.rs`
**Step 1: Write the failing test**
Add state-level tests for:
- bottom-to-top ordering default
- cursor movement and page movement
- single index selection
- comma-separated and range numeric selection
- invalid numeric input preserving current selection
- confirmation state transitions
**Step 2: Run test to verify it fails**
Run: `cargo test --package aim-cli --test search_browser`
Expected: FAIL because the browser state module does not exist.
**Step 3: Write minimal implementation**
Build a pure Rust state model that does not require a live terminal to test. Keep terminal drawing and key-event adaptation separate from selection and pagination logic.
**Step 4: Run test to verify it passes**
Run: `cargo test --package aim-cli --test search_browser`
Expected: PASS
### Task 3: Wire `ratatui` and TTY-gated interactive search dispatch
**Files:**
- Modify: `crates/aim-cli/Cargo.toml`
- Modify: `crates/aim-cli/src/lib.rs`
- Modify: `crates/aim-cli/src/ui/render.rs`
- Modify: `crates/aim-cli/src/main.rs`
- Modify: `crates/aim-cli/src/ui/prompt.rs`
- Test: `crates/aim-cli/tests/search_cli.rs`
**Step 1: Write the failing test**
Add CLI coverage for:
- non-TTY search stays plain text
- config skip confirmation changes the post-selection path
- empty result sets do not launch the browser
Use deterministic seams rather than a full escape-sequence snapshot test.
**Step 2: Run test to verify it fails**
Run: `cargo test --package aim-cli --test search_cli`
Expected: FAIL because interactive search dispatch is not implemented.
**Step 3: Write minimal implementation**
Add `ratatui` and `crossterm`, initialize the browser only when stdin and stdout are terminals, and fall back cleanly to the existing renderer if terminal setup fails or the result set is empty.
**Step 4: Run test to verify it passes**
Run: `cargo test --package aim-cli --test search_cli`
Expected: PASS
### Task 4: Add row rendering and confirmation summary coverage
**Files:**
- Modify: `crates/aim-cli/src/ui/search_browser.rs`
- Test: `crates/aim-cli/tests/ui_summary.rs`
**Step 1: Write the failing test**
Add focused tests for:
- one-line row formatting
- provider and query columns remaining visible
- confirmation summary content for multi-select
**Step 2: Run test to verify it fails**
Run: `cargo test --package aim-cli --test ui_summary`
Expected: FAIL because the browser summaries are not rendered yet.
**Step 3: Write minimal implementation**
Implement the smallest formatting helpers needed to keep rows stable and the confirmation screen legible.
**Step 4: Run test to verify it passes**
Run: `cargo test --package aim-cli --test ui_summary`
Expected: PASS
### Task 5: Final verification
**Files:**
- Modify as required by prior tasks only
**Step 1: Run focused CLI tests**
Run: `cargo test --package aim-cli --test config_loading --test search_browser --test search_cli --test ui_summary`
Expected: PASS
**Step 2: Run workspace formatting**
Run: `cargo fmt --all`
Expected: PASS
**Step 3: Run workspace linting and regression tests**
Run: `cargo test --workspace && cargo clippy --workspace --all-targets --all-features -- -D warnings`
Expected: PASS