aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-ai/src/tui/app.rs
diff options
context:
space:
mode:
authorMichelle Tilley <michelle@michelletilley.net>2026-03-26 19:19:47 -0700
committerGitHub <noreply@github.com>2026-03-27 02:19:47 +0000
commitb649a7ab8de6488c1341e94c37d032c07d5b3f13 (patch)
treeca9aadc1175b8439dd85de135f3804681b755776 /crates/atuin-ai/src/tui/app.rs
parentfix: set WorkingDirectory in PowerShell Invoke-AtuinSearch (#3351) (diff)
downloadatuin-b649a7ab8de6488c1341e94c37d032c07d5b3f13.zip
feat: Use eye-declare for more performant and flexible AI TUI (#3343)
This PR replaces the mess of custom rendering code in Atuin AI with [eye-declare](https://github.com/BinaryMuse/eye-declare), and updates the TUI to feel more terminal-native: output appears inline and persists in scrollback, so you can scroll up and look at previous conversations for reference. The "review" state — which used to exist between the LLM generating a response and the user either executing or following up — has been removed; just start typing to follow up with the LLM, or press `enter` at the empty input box to execute the suggested command. <img width="1203" height="633" alt="image" src="https://github.com/user-attachments/assets/159ee447-9a2a-4edd-b56e-a79bf1aaaa94" />
Diffstat (limited to 'crates/atuin-ai/src/tui/app.rs')
-rw-r--r--crates/atuin-ai/src/tui/app.rs157
1 files changed, 0 insertions, 157 deletions
diff --git a/crates/atuin-ai/src/tui/app.rs b/crates/atuin-ai/src/tui/app.rs
deleted file mode 100644
index ecb1eb81..00000000
--- a/crates/atuin-ai/src/tui/app.rs
+++ /dev/null
@@ -1,157 +0,0 @@
-use super::state::{AppMode, AppState, ExitAction};
-use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
-use tui_textarea::{Input, Key};
-
-/// Thin wrapper around AppState for compatibility
-/// All state lives in AppState, this just provides the handle_key interface
-pub struct App {
- pub state: AppState,
-}
-
-impl App {
- pub fn new() -> Self {
- Self {
- state: AppState::new(),
- }
- }
-
- /// Handle a key event. Returns true if render is needed.
- pub fn handle_key(&mut self, key: KeyEvent) -> bool {
- match self.state.mode {
- AppMode::Input => self.handle_input_key(key),
- AppMode::Generating => self.handle_generating_key(key),
- AppMode::Streaming => self.handle_streaming_key(key),
- AppMode::Review => self.handle_review_key(key),
- AppMode::Error => self.handle_error_key(key),
- }
- }
-
- fn handle_input_key(&mut self, key: KeyEvent) -> bool {
- // Handle special keys ourselves
- match key.code {
- KeyCode::Esc => {
- self.state.exit(ExitAction::Cancel);
- return true;
- }
- KeyCode::Enter => {
- if self.state.input_is_empty() {
- self.state.exit(ExitAction::Cancel);
- } else {
- self.state.start_generating();
- }
- return true;
- }
- _ => {}
- }
-
- // Delegate all other keys to textarea
- // Manually convert crossterm KeyEvent to tui-textarea Input
- // (needed due to crossterm version mismatch)
- let tui_key = match key.code {
- KeyCode::Char(c) => Key::Char(c),
- KeyCode::Backspace => Key::Backspace,
- KeyCode::Delete => Key::Delete,
- KeyCode::Left => Key::Left,
- KeyCode::Right => Key::Right,
- KeyCode::Up => Key::Up,
- KeyCode::Down => Key::Down,
- KeyCode::Home => Key::Home,
- KeyCode::End => Key::End,
- KeyCode::PageUp => Key::PageUp,
- KeyCode::PageDown => Key::PageDown,
- KeyCode::Tab => Key::Tab,
- _ => Key::Null,
- };
-
- if tui_key != Key::Null {
- let input = Input {
- key: tui_key,
- ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
- alt: key.modifiers.contains(KeyModifiers::ALT),
- shift: key.modifiers.contains(KeyModifiers::SHIFT),
- };
- self.state.textarea.input(input);
- }
- true
- }
-
- fn handle_generating_key(&mut self, key: KeyEvent) -> bool {
- match key.code {
- KeyCode::Esc => {
- self.state.cancel_generation();
- true
- }
- _ => false, // Discard other keys during generation
- }
- }
-
- fn handle_streaming_key(&mut self, key: KeyEvent) -> bool {
- match key.code {
- KeyCode::Esc => {
- self.state.cancel_streaming();
- true
- }
- _ => false, // Ignore other keys during streaming
- }
- }
-
- fn handle_review_key(&mut self, key: KeyEvent) -> bool {
- match key.code {
- KeyCode::Esc => {
- self.state.confirmation_pending = false; // Clear confirmation state
- self.state.exit(ExitAction::Cancel);
- true
- }
- KeyCode::Enter => {
- let cmd = self.state.current_command().map(|c| c.to_string());
- if let Some(cmd) = cmd {
- if self.state.is_current_command_dangerous() && !self.state.confirmation_pending
- {
- // First Enter on dangerous command: enter confirmation mode
- self.state.confirmation_pending = true;
- } else {
- // Second Enter (confirmation), or non-dangerous command: execute
- self.state.confirmation_pending = false;
- self.state.exit(ExitAction::Execute(cmd));
- }
- }
- true
- }
- KeyCode::Tab => {
- let cmd = self.state.current_command().map(|c| c.to_string());
- if let Some(cmd) = cmd {
- self.state.confirmation_pending = false; // Clear on Tab too
- self.state.exit(ExitAction::Insert(cmd));
- }
- true
- }
- KeyCode::Char('f') => {
- // Changed from 'e' to 'f' for follow-up mode
- self.state.confirmation_pending = false; // Clear on follow-up
- self.state.start_edit_mode();
- true
- }
- _ => false,
- }
- }
-
- fn handle_error_key(&mut self, key: KeyEvent) -> bool {
- match key.code {
- KeyCode::Esc => {
- self.state.exit(ExitAction::Cancel);
- true
- }
- KeyCode::Enter | KeyCode::Char('r') => {
- self.state.retry();
- true
- }
- _ => false,
- }
- }
-}
-
-impl Default for App {
- fn default() -> Self {
- Self::new()
- }
-}