From 7301d887c14376e4b1d9263d434da0e72d880372 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Wed, 15 Apr 2026 16:39:38 -0700 Subject: fix: Enter runs suggested command when selecting permissions (#3418) --- Cargo.lock | 8 +-- crates/atuin-ai/Cargo.toml | 2 +- crates/atuin-ai/src/tools/descriptor.rs | 2 +- crates/atuin-ai/src/tui/components/atuin_ai.rs | 68 ++++++++++++++++---------- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19308bb4..f4e23bec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1532,9 +1532,9 @@ dependencies = [ [[package]] name = "eye_declare" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd705fa26778c4cd8cd93f08b76986495601e5fc7039ff0f80499d0f1398ca62" +checksum = "fa99f9efa03c7fae32abd0b2e77d22dac5c2a137d1267c3496b44a711d139bd6" dependencies = [ "crossterm", "eye_declare_macros", @@ -1548,9 +1548,9 @@ dependencies = [ [[package]] name = "eye_declare_macros" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae446305ea9f3f4679bd632a43e69eed48ba5484d5d692882a4c43e4666fe25d" +checksum = "a285ad61e123dad4c505e2d96818b0378d9facecdee074a02c525719a1db2f9b" dependencies = [ "proc-macro2", "quote", diff --git a/crates/atuin-ai/Cargo.toml b/crates/atuin-ai/Cargo.toml index 3bdd45d2..3be127de 100644 --- a/crates/atuin-ai/Cargo.toml +++ b/crates/atuin-ai/Cargo.toml @@ -45,7 +45,7 @@ async-stream = "0.3" uuid = { workspace = true } tui-textarea-2 = "0.10.2" unicode-width = "0.2" -eye_declare = "0.4.2" +eye_declare = "0.4.3" ratatui-core = "0.1" ratatui-widgets = "0.3" thiserror = { workspace = true } diff --git a/crates/atuin-ai/src/tools/descriptor.rs b/crates/atuin-ai/src/tools/descriptor.rs index 3b2b7ebf..fc44ec10 100644 --- a/crates/atuin-ai/src/tools/descriptor.rs +++ b/crates/atuin-ai/src/tools/descriptor.rs @@ -23,7 +23,7 @@ pub(crate) struct ToolDescriptor { pub(crate) const READ: &ToolDescriptor = &ToolDescriptor { canonical_names: &["read_file"], - capability: Some("client_v1_read"), + capability: Some("client_v1_read_file"), display_verb: "read", progressive_verb: "Reading file...", past_verb: "Read file", diff --git a/crates/atuin-ai/src/tui/components/atuin_ai.rs b/crates/atuin-ai/src/tui/components/atuin_ai.rs index c04ac722..848a001a 100644 --- a/crates/atuin-ai/src/tui/components/atuin_ai.rs +++ b/crates/atuin-ai/src/tui/components/atuin_ai.rs @@ -1,8 +1,9 @@ //! Top-level AtuinAi component that translates key events into AiTuiEvents. //! -//! This component wraps the entire view and handles key events that bubble up -//! from child components (or aren't consumed by them). It maps raw key events -//! to semantic `AiTuiEvent` variants based on the current `AppMode`. +//! Global shortcuts (Ctrl+C, Esc) are handled in the capture phase so they +//! fire regardless of which child is focused. Contextual shortcuts (Enter, +//! Tab) are handled in the bubble phase so child components like the +//! permission Select can consume them first. use std::sync::mpsc; @@ -41,6 +42,7 @@ fn atuin_ai( state.tx = tx.cloned(); }); + // Capture phase: global shortcuts that must fire regardless of child focus. hooks.use_event_capture(move |event, props, state| { let Event::Key(KeyEvent { code, @@ -66,28 +68,53 @@ fn atuin_ai( return EventResult::Consumed; } - match props.mode { - AppMode::Input => match code { - KeyCode::Esc => { + // Esc — always handled at the top level + if *code == KeyCode::Esc { + match props.mode { + AppMode::Input => { if props.has_executing_preview { let _ = tx.send(AiTuiEvent::InterruptToolExecution); - return EventResult::Consumed; - } - - if props.pending_confirmation { + } else if props.pending_confirmation { let _ = tx.send(AiTuiEvent::CancelConfirmation); - return EventResult::Consumed; + } else { + let _ = tx.send(AiTuiEvent::Exit); } - + } + AppMode::Generating | AppMode::Streaming => { + let _ = tx.send(AiTuiEvent::CancelGeneration); + } + AppMode::Error => { let _ = tx.send(AiTuiEvent::Exit); - EventResult::Consumed } + } + return EventResult::Consumed; + } + + EventResult::Ignored + }); + + // Bubble phase: contextual shortcuts that children (e.g. Select) may handle first. + hooks.use_event(move |event, props, state| { + let Event::Key(KeyEvent { + code, + kind: KeyEventKind::Press, + .. + }) = event + else { + return EventResult::Ignored; + }; + + let Some(ref tx) = state.read().tx else { + return EventResult::Ignored; + }; + + match props.mode { + AppMode::Input => match code { KeyCode::Tab => { if props.has_command && props.is_input_blank { let _ = tx.send(AiTuiEvent::InsertCommand); return EventResult::Consumed; } - EventResult::Ignored } KeyCode::Enter => { @@ -95,29 +122,18 @@ fn atuin_ai( let _ = tx.send(AiTuiEvent::ExecuteCommand); return EventResult::Consumed; } - EventResult::Ignored } _ => EventResult::Ignored, }, - AppMode::Generating | AppMode::Streaming => match code { - KeyCode::Esc => { - let _ = tx.send(AiTuiEvent::CancelGeneration); - EventResult::Consumed - } - _ => EventResult::Ignored, - }, AppMode::Error => match code { - KeyCode::Esc => { - let _ = tx.send(AiTuiEvent::Exit); - EventResult::Consumed - } KeyCode::Enter | KeyCode::Char('r') => { let _ = tx.send(AiTuiEvent::Retry); EventResult::Consumed } _ => EventResult::Ignored, }, + _ => EventResult::Ignored, } }); -- cgit v1.3.1