Skip to content

feat: render slash-command autocomplete popup in TUI#135

Draft
yamaceay wants to merge 6 commits into
stippi:mainfrom
yamaceay:feat/autocomplete-popup-render
Draft

feat: render slash-command autocomplete popup in TUI#135
yamaceay wants to merge 6 commits into
stippi:mainfrom
yamaceay:feat/autocomplete-popup-render

Conversation

@yamaceay

@yamaceay yamaceay commented Jun 10, 2026

Copy link
Copy Markdown

Context

PR 5 of 5 — slash-command autocomplete series. This is the visual payoff.

Dependency / merge order:

#136 → #131 → #132 → #133 → #134 → #135 (this PR)

After this PR the feature is complete: typing / in the composer shows a
floating command list; arrow keys move the highlight; Tab accepts; Escape
dismisses.


Why

PR #134 wired the state and input handling for the autocomplete popup, but
nothing was visible yet. This PR adds the Ratatui rendering that makes the
popup appear on screen.

What

renderer.rs

New state:

Field Purpose
autocomplete_items: Vec<(&'static str, &'static str)> Pre-filtered (name, description) pairs for the current query
autocomplete_selected: usize Highlighted row index

New public API:

  • set_autocomplete(items, selected) — called from app.rs whenever the
    filtered list or selection changes.
  • clear_autocomplete() — hides the popup.

render_autocomplete_popup — static helper, called from paint() after
composer.render() so the popup overlays the input area without disturbing
the scroll-back content:

  • Position: directly above input_area, indented 2 columns to align
    visually under the "› " composer prefix. X is clamped so the popup never
    overflows the terminal width.
  • Size: height = min(items.len(), 8), width = longest_entry + padding,
    capped at terminal width.
  • Scrolling window: keeps the selected item in view when the list is taller
    than 8 rows.
  • Row format: /name — description
  • Styling:
    • Normal row: gray text, transparent background — low visual weight.
    • Selected row: DarkGray background, Cyan /name, White description.
  • Uses the same Paragraph + Span + Line pattern as the rest of the
    renderer's status area, so it inherits consistent font metrics.

app.rs

The three autocomplete match arms (UpdateAutocomplete, MoveAutocomplete,
SelectAutocomplete) and the paste-event handler now all call
renderer.set_autocomplete() / renderer.clear_autocomplete() in addition to
updating AppState, so the renderer is always in sync.

Scope

renderer.rs (new fields + 2 setters + render_autocomplete_popup method) and
app.rs (renderer calls added to 4 existing match arms).

Test plan

  • cargo build --no-default-features passes.
  • Type / → popup appears listing all 7 commands.
  • Type /m → popup filters to /model only.
  • Type /cl → popup filters to /clear only.
  • Press Down → selection highlight moves down.
  • Press Up → selection wraps from top to bottom.
  • Press Tab → textarea becomes /model (or whichever is selected); popup closes.
  • Press Escape → popup closes; cursor stays in textarea; no session cancellation.
  • Type normal text → popup never appears.
  • List longer than 8 (add test commands): popup scrolls to keep selected row visible.
  • Resize terminal → popup re-positions correctly on next frame.
  • Selected row is visually distinct (DarkGray background, Cyan name).

yamaceay added 6 commits June 11, 2026 10:40
The GPUI desktop UI depends on Metal shader compilation, which requires
Xcode developer tools to be installed (xcrun must find the `metal` binary).
On machines without Xcode this hard-blocked every `cargo build`, even when
only the terminal TUI was needed.

Introduce a `gpui-ui` Cargo feature that gates all GPUI-related code:

- `Cargo.toml`: `gpui`, `gpui_platform`, and `gpui-component` are now
  optional dependencies, activated only by `--features gpui-ui`.
  The gpui entry in `[dev-dependencies]` is removed (it was unused in
  the unit-test suite).

- `src/app/mod.rs`, `src/ui/mod.rs`: `pub mod gpui` gated behind
  `#[cfg(feature = "gpui-ui")]`.

- `src/ui/backend.rs`: The `GpuiTerminalCommandExecutor` import and its
  single usage site are gated; the TUI-only path falls back to
  `DefaultCommandExecutor` (which `GpuiTerminalCommandExecutor` itself
  delegates to internally).

- `src/main.rs`: The `--ui` code paths (logging setup, model resolution,
  `app::gpui::run()`) are gated.  Passing `--ui` without the feature
  produces a clear error message rather than a linker crash.

- `src/cli.rs`: The `UiSettings::load()` call that reads the GPUI-
  persisted default model is gated; TUI-only builds fall through to the
  alphabetical-first-model fallback.

Build commands:
  # TUI only (no Xcode required):
  cargo build --no-default-features

  # With GPUI desktop UI (requires Xcode / Metal):
  cargo build --features gpui-ui
Introduce a `SlashCommand` struct (name, aliases, description) and an
`all_commands()` function that returns a static slice of all registered
commands.  This gives the rest of the codebase — in particular the
upcoming autocomplete popup — a single, authoritative place to discover
what slash commands exist, without needing to duplicate the match arms
in `process_command`.

The help text is now derived from the registry instead of being a hard-
coded string, so adding a new command in one place is sufficient.

No behavior change: all existing commands keep the same names, aliases,
and runtime behaviour.
Adds two new slash commands to the terminal TUI:

- `/clear`   — wipes the conversation message history (message nodes,
               active path, plan) for the current session, then clears
               the UI transcript.  The session itself stays alive so the
               user can continue without re-starting.
- `/compact` — placeholder that shows an informational error until a
               proper summarisation implementation lands.

The data path is:
  CommandProcessor (commands.rs)
    → KeyEventResult::{ClearContext,CompactContext}  (input.rs)
    → BackendEvent::{ClearContext,CompactContext}     (app.rs)
    → handler in handle_backend_events               (backend.rs)
    → UiEvent::ClearMessages / UiEvent::DisplayError (terminal UI)
Add a public `current_line() -> &str` method that returns the text of the
logical line the cursor is currently on (i.e. the content between the two
surrounding newlines, or the start/end of the buffer).

This is a thin wrapper over the two existing private helpers
`beginning_of_current_line()` and `end_of_current_line()`; no logic
is duplicated and no behavior changes.

The method is the entry point for the upcoming slash-command autocomplete
(PR 4), which needs to inspect what the user has typed on the current line
without re-parsing the entire textarea buffer.

Four unit tests cover: single-line content, cursor on first/last line of
multi-line content, and the empty-textarea edge case.
Adds the data-flow layer for the slash-command autocomplete popup.
No visual rendering yet — that lands in the next PR.

## state.rs
- Three new fields on `AppState`: `autocomplete_active`, `autocomplete_query`
  (text after the `/`), `autocomplete_selected` (highlighted row index).
- Three new helpers: `open_autocomplete(query)`, `close_autocomplete()`,
  `move_autocomplete_selection(delta, count)` (wraps around with rem_euclid).

## input.rs
- `autocomplete_active: bool` added to `InputManager` so Up/Down/Tab can be
  intercepted when the popup is open without checking `AppState` from inside
  the input layer.
- New `KeyEventResult` variants:
  - `UpdateAutocomplete { query: Option<String> }` — emitted after every
    keystroke; `None` means close the popup, `Some(q)` means open/update.
  - `MoveAutocomplete(i32)` — Up/Down while popup open.
  - `SelectAutocomplete` — Tab (or Enter via popup) while popup open.
- The `_ =>` fallback arm now returns `UpdateAutocomplete` instead of
  `Continue`; the rest of the code-paths (Shift+Enter) do likewise.
- Escape while `autocomplete_active` closes the popup instead of bubbling.
- `slash_prefix()` is now public so `app.rs` can call it after paste events.
- Tests updated for the new return types; new tests cover slash detection,
  arrow-key interception, Tab selection, and Escape dismissal.

## app.rs
- Imports `all_commands()` for filtering.
- Three new match arms in the event loop:
  - `UpdateAutocomplete` — filters `all_commands()` by prefix, calls
    `state.open_autocomplete` / `state.close_autocomplete`, syncs
    `input_manager.autocomplete_active`.
  - `MoveAutocomplete` — delegates to `state.move_autocomplete_selection`.
  - `SelectAutocomplete` — computes the selected command name, replaces
    `"/<query>"` on the current line with `"/<name> "`, closes popup.
- Paste handling now also checks `slash_prefix()` to keep autocomplete in
  sync after bracketed-paste events.
Adds the visual layer for the autocomplete popup.  Typing '/' in the
composer now shows a floating list of matching commands above the input
area; arrow keys move the highlight and Tab accepts.

## renderer.rs
- Two new fields on `TerminalRenderer`: `autocomplete_items` (pre-filtered
  list of (name, description) pairs) and `autocomplete_selected` (highlighted
  row index).
- `set_autocomplete(items, selected)` and `clear_autocomplete()` public setters.
- `render_autocomplete_popup(f, input_area, items, selected)` — static helper
  that draws the popup directly over the Ratatui buffer:
  - Height: min(items.len(), 8 rows), no border.
  - Horizontally aligned under the "›" prefix of the composer.
  - Each row: "  /name  —  description"; selected row gets a DarkGray
    background with Cyan name and White description.
  - Scrolls to keep the selected item in view.
- Called from `paint()` after `composer.render()` so the popup overlays
  the input area.

## app.rs
- `UpdateAutocomplete`, `MoveAutocomplete`, and `SelectAutocomplete` arms
  now also call `renderer.set_autocomplete()` / `renderer.clear_autocomplete()`
  so the renderer is always in sync with `AppState`.
- Paste event handling likewise drives the renderer.
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.

1 participant