notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 3edbf1b4f979a5f5c46d9e09a7c3fcffff51c9b0
parent dc3d2756716c610909df663b481a880369efd14d
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 26 Jan 2026 18:23:54 -0800

dave: use [1] Yes / [2] No for permission buttons

Changes keybindings from Y/A/D to 1/2 for permission responses,
making room for future multi-option prompts (1,2,3...). Keys 1/2
are intercepted for permissions when a prompt is pending, otherwise
they switch agents as before.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_dave/src/lib.rs | 40++++++++++++++++++++++++++++------------
Mcrates/notedeck_dave/src/session.rs | 3+++
Mcrates/notedeck_dave/src/ui/dave.rs | 27++++++++++++++++++++-------
Mcrates/notedeck_dave/src/ui/keybindings.rs | 22++++++++++++----------
4 files changed, 63 insertions(+), 29 deletions(-)

diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -375,6 +375,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr self.model_config.trial, &session.chat, &mut session.input, + &mut session.focus_requested, ) .compact(true) .is_working(is_working) @@ -464,11 +465,16 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr if let Some(session) = self.session_manager.get_active_mut() { let is_working = session.status() == crate::agent_status::AgentStatus::Working; let has_pending_permission = !session.pending_permissions.is_empty(); - DaveUi::new(self.model_config.trial, &session.chat, &mut session.input) - .is_working(is_working) - .interrupt_pending(interrupt_pending) - .has_pending_permission(has_pending_permission) - .ui(app_ctx, ui) + DaveUi::new( + self.model_config.trial, + &session.chat, + &mut session.input, + &mut session.focus_requested, + ) + .is_working(is_working) + .interrupt_pending(interrupt_pending) + .has_pending_permission(has_pending_permission) + .ui(app_ctx, ui) } else { DaveResponse::default() } @@ -522,11 +528,16 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr if let Some(session) = self.session_manager.get_active_mut() { let is_working = session.status() == crate::agent_status::AgentStatus::Working; let has_pending_permission = !session.pending_permissions.is_empty(); - DaveUi::new(self.model_config.trial, &session.chat, &mut session.input) - .is_working(is_working) - .interrupt_pending(interrupt_pending) - .has_pending_permission(has_pending_permission) - .ui(app_ctx, ui) + DaveUi::new( + self.model_config.trial, + &session.chat, + &mut session.input, + &mut session.focus_requested, + ) + .is_working(is_working) + .interrupt_pending(interrupt_pending) + .has_pending_permission(has_pending_permission) + .ui(app_ctx, ui) } else { DaveResponse::default() } @@ -534,7 +545,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr } fn handle_new_chat(&mut self) { - self.session_manager.new_session(); + let id = self.session_manager.new_session(); + // Request focus on the new session's input + if let Some(session) = self.session_manager.get_mut(id) { + session.focus_requested = true; + } } /// Delete a session and clean up backend resources @@ -764,7 +779,8 @@ impl notedeck::App for Dave { } // Handle global keybindings (when no text input has focus) - if let Some(key_action) = check_keybindings(ui.ctx()) { + let has_pending_permission = self.first_pending_permission().is_some(); + if let Some(key_action) = check_keybindings(ui.ctx(), has_pending_permission) { match key_action { KeyAction::AcceptPermission => { if let Some(request_id) = self.first_pending_permission() { diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs @@ -25,6 +25,8 @@ pub struct ChatSession { pub scene_position: egui::Vec2, /// Cached status for the agent (derived from session state) cached_status: AgentStatus, + /// Whether this session's input should be focused on the next frame + pub focus_requested: bool, } impl Drop for ChatSession { @@ -53,6 +55,7 @@ impl ChatSession { task_handle: None, scene_position: egui::Vec2::new(x, y), cached_status: AgentStatus::Idle, + focus_requested: false, } } diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -24,6 +24,7 @@ pub struct DaveUi<'a> { is_working: bool, interrupt_pending: bool, has_pending_permission: bool, + focus_requested: &'a mut bool, } /// The response the app generates. The response contains an optional @@ -86,7 +87,12 @@ pub enum DaveAction { } impl<'a> DaveUi<'a> { - pub fn new(trial: bool, chat: &'a [Message], input: &'a mut String) -> Self { + pub fn new( + trial: bool, + chat: &'a [Message], + input: &'a mut String, + focus_requested: &'a mut bool, + ) -> Self { DaveUi { trial, chat, @@ -95,6 +101,7 @@ impl<'a> DaveUi<'a> { is_working: false, interrupt_pending: false, has_pending_permission: false, + focus_requested, } } @@ -389,16 +396,16 @@ impl<'a> DaveUi<'a> { action: &mut Option<DaveAction>, ) { ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - // Deny button (red) with [D] hint + // Deny button (red) with [2] hint if ui .add( egui::Button::new( - egui::RichText::new("Deny [D]") + egui::RichText::new("[2] No") .color(ui.visuals().widgets.active.fg_stroke.color), ) .fill(egui::Color32::from_rgb(178, 34, 34)), ) - .on_hover_text("Press D to deny") + .on_hover_text("Press 2 to deny") .clicked() { *action = Some(DaveAction::PermissionResponse { @@ -409,16 +416,16 @@ impl<'a> DaveUi<'a> { }); } - // Allow button (green) with [Y] hint + // Allow button (green) with [1] hint if ui .add( egui::Button::new( - egui::RichText::new("Allow [Y]") + egui::RichText::new("[1] Yes") .color(ui.visuals().widgets.active.fg_stroke.color), ) .fill(egui::Color32::from_rgb(34, 139, 34)), ) - .on_hover_text("Press Y or A to allow") + .on_hover_text("Press 1 to allow") .clicked() { *action = Some(DaveAction::PermissionResponse { @@ -623,6 +630,12 @@ impl<'a> DaveUi<'a> { ); notedeck_ui::include_input(ui, &r); + // Request focus if flagged (e.g., after spawning a new agent) + if *self.focus_requested { + r.request_focus(); + *self.focus_requested = false; + } + // Unfocus text input when there's a pending permission request // so keyboard shortcuts (Y/A/N/D) can be used to respond if self.has_pending_permission { diff --git a/crates/notedeck_dave/src/ui/keybindings.rs b/crates/notedeck_dave/src/ui/keybindings.rs @@ -20,7 +20,9 @@ pub enum KeyAction { } /// Check for keybinding actions when no text input has focus -pub fn check_keybindings(ctx: &egui::Context) -> Option<KeyAction> { +/// If `has_pending_permission` is true, keys 1/2 are used for permission responses +/// instead of agent switching. +pub fn check_keybindings(ctx: &egui::Context, has_pending_permission: bool) -> Option<KeyAction> { // Escape works even when text input has focus (to interrupt AI) if ctx.input(|i| i.key_pressed(Key::Escape)) { return Some(KeyAction::Interrupt); @@ -32,17 +34,17 @@ pub fn check_keybindings(ctx: &egui::Context) -> Option<KeyAction> { } ctx.input(|i| { - // Permission response keys: Y/A for accept, N/D for deny - if i.key_pressed(Key::Y) || i.key_pressed(Key::A) { - return Some(KeyAction::AcceptPermission); - } - // Note: N is already used for new agent in scene view, so we use D for deny - // or N only when there's a pending permission - if i.key_pressed(Key::D) { - return Some(KeyAction::DenyPermission); + // When there's a pending permission, 1 = accept, 2 = deny + if has_pending_permission { + if i.key_pressed(Key::Num1) { + return Some(KeyAction::AcceptPermission); + } + if i.key_pressed(Key::Num2) { + return Some(KeyAction::DenyPermission); + } } - // Number keys 1-9 for switching agents + // Number keys 1-9 for switching agents (when no pending permission) for (idx, key) in [ Key::Num1, Key::Num2,