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:
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,