commit dddf12aa6730b4ade3df437f0caf65b3796e34cd
parent e5c350351903b6bc3850a328192de62b62731bf2
Author: William Casarin <jb55@jb55.com>
Date: Fri, 27 Feb 2026 10:34:59 -0800
fix: auto-steal focus retries across frames via AutoStealState enum
The transition-based auto-steal from 53d5c67 could miss steal
opportunities when suppressed (e.g. user was typing) during the
one-shot transition frame. Replace the two bools (auto_steal_focus,
pending_auto_steal) with an AutoStealState enum (Disabled / Idle /
Pending). Pending persists across frames until the steal logic
actually executes unsuppressed, while manual session switching still
works because Pending is only entered on new queue transitions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
2 files changed, 63 insertions(+), 25 deletions(-)
diff --git a/crates/notedeck_dave/src/focus_queue.rs b/crates/notedeck_dave/src/focus_queue.rs
@@ -50,6 +50,26 @@ pub struct FocusQueueUpdate {
pub changed: bool,
}
+/// Auto-steal focus state machine.
+///
+/// - `Disabled`: auto-steal is off, user controls focus manually.
+/// - `Idle`: auto-steal is on but no pending work.
+/// - `Pending`: auto-steal is on and a focus-queue transition was
+/// detected that hasn't been acted on yet (retries across frames
+/// if temporarily suppressed, e.g. user is typing).
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum AutoStealState {
+ Disabled,
+ Idle,
+ Pending,
+}
+
+impl AutoStealState {
+ pub fn is_enabled(self) -> bool {
+ matches!(self, Self::Idle | Self::Pending)
+ }
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QueueEntry {
pub session_id: SessionId,
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -149,8 +149,9 @@ pub struct Dave {
interrupt_pending_since: Option<Instant>,
/// Focus queue for agents needing attention
focus_queue: FocusQueue,
- /// Auto-steal focus mode: automatically cycle through focus queue items
- auto_steal_focus: bool,
+ /// Auto-steal focus state: Disabled, Idle (enabled, nothing pending),
+ /// or Pending (enabled, waiting to fire / retrying).
+ auto_steal: focus_queue::AutoStealState,
/// The session ID to return to after processing all NeedsInput items
home_session: Option<SessionId>,
/// Directory picker for selecting working directory when creating sessions
@@ -492,7 +493,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
show_scene: false, // Default to list view
interrupt_pending_since: None,
focus_queue: FocusQueue::new(),
- auto_steal_focus: false,
+ auto_steal: focus_queue::AutoStealState::Disabled,
home_session: None,
directory_picker,
session_picker: SessionPicker::new(),
@@ -916,7 +917,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
&mut self.focus_queue,
&self.model_config,
is_interrupt_pending,
- self.auto_steal_focus,
+ self.auto_steal.is_enabled(),
app_ctx,
ui,
);
@@ -953,7 +954,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
&self.focus_queue,
&self.model_config,
is_interrupt_pending,
- self.auto_steal_focus,
+ self.auto_steal.is_enabled(),
app_ctx,
ui,
);
@@ -998,7 +999,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
&self.focus_queue,
&self.model_config,
is_interrupt_pending,
- self.auto_steal_focus,
+ self.auto_steal.is_enabled(),
self.show_session_list,
app_ctx,
ui,
@@ -2213,7 +2214,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
&mut self.focus_queue,
get_backend(&self.backends, bt),
self.show_scene,
- self.auto_steal_focus,
+ self.auto_steal.is_enabled(),
&mut self.home_session,
egui_ctx,
) {
@@ -2233,7 +2234,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
self.delete_session(id);
}
KeyActionResult::SetAutoSteal(new_state) => {
- self.auto_steal_focus = new_state;
+ self.auto_steal = if new_state {
+ focus_queue::AutoStealState::Pending
+ } else {
+ focus_queue::AutoStealState::Disabled
+ };
}
KeyActionResult::PublishPermissionResponse(publish) => {
self.pending_perm_responses.push(publish);
@@ -2311,10 +2316,14 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
&mut self.session_manager,
&mut self.scene,
self.show_scene,
- self.auto_steal_focus,
+ self.auto_steal.is_enabled(),
&mut self.home_session,
);
- self.auto_steal_focus = new_state;
+ self.auto_steal = if new_state {
+ focus_queue::AutoStealState::Pending
+ } else {
+ focus_queue::AutoStealState::Disabled
+ };
None
}
UiActionResult::NewChat => {
@@ -2876,27 +2885,36 @@ impl notedeck::App for Dave {
notedeck::platform::try_vibrate();
}
- // Only auto-steal on queue transitions, not every frame.
- // This lets the user manually switch sessions between transitions.
- if queue_update.changed {
- // Suppress auto-steal while the user is typing (non-empty input)
+ // Transition to Pending on queue changes so auto-steal retries
+ // across frames if temporarily suppressed (e.g. user is typing).
+ if queue_update.changed && self.auto_steal.is_enabled() {
+ self.auto_steal = focus_queue::AutoStealState::Pending;
+ }
+
+ // Run auto-steal when pending. Transitions back to Idle once
+ // the steal logic executes (even if no switch was needed).
+ // Stays Pending while the user is typing so it retries next frame.
+ if self.auto_steal == focus_queue::AutoStealState::Pending {
let user_is_typing = self
.session_manager
.get_active()
.is_some_and(|s| !s.input.is_empty());
- let stole_focus = update::process_auto_steal_focus(
- &mut self.session_manager,
- &mut self.focus_queue,
- &mut self.scene,
- self.show_scene,
- self.auto_steal_focus && !user_is_typing,
- &mut self.home_session,
- );
+ if !user_is_typing {
+ let stole_focus = update::process_auto_steal_focus(
+ &mut self.session_manager,
+ &mut self.focus_queue,
+ &mut self.scene,
+ self.show_scene,
+ true,
+ &mut self.home_session,
+ );
+
+ if stole_focus {
+ activate_app(egui_ctx);
+ }
- // Raise the OS window when auto-steal switches to a NeedsInput session
- if stole_focus {
- activate_app(egui_ctx);
+ self.auto_steal = focus_queue::AutoStealState::Idle;
}
}