aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
authorMichelle Tilley <michelle@michelletilley.net>2026-04-15 16:39:38 -0700
committerGitHub <noreply@github.com>2026-04-15 16:39:38 -0700
commit7301d887c14376e4b1d9263d434da0e72d880372 (patch)
tree828152af4889cd07b3f08b966ce054fa1e26d7c6 /crates
parentUpdate release skill (diff)
downloadatuin-7301d887c14376e4b1d9263d434da0e72d880372.zip
fix: Enter runs suggested command when selecting permissions (#3418)
Diffstat (limited to 'crates')
-rw-r--r--crates/atuin-ai/Cargo.toml2
-rw-r--r--crates/atuin-ai/src/tools/descriptor.rs2
-rw-r--r--crates/atuin-ai/src/tui/components/atuin_ai.rs68
3 files changed, 44 insertions, 28 deletions
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,
}
});