commit 53d5c67cf3ab67aa2879f094c673aec5f2d83c41
parent c72a82c576e8fbbf72dc39e54d4dfc340bb2b2f0
Author: William Casarin <jb55@jb55.com>
Date: Thu, 26 Feb 2026 19:49:30 -0800
dave: make auto-steal focus transition-based instead of per-frame
Auto-steal previously ran every frame and immediately snapped focus
back to the indicator session, making manual session switching
(Ctrl+1-9, Ctrl+Tab) impossible while indicators were active.
Now auto-steal only fires when the focus queue actually changes (new
indicator appears or one clears), letting users freely switch between
sessions in the meantime.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
2 files changed, 52 insertions(+), 27 deletions(-)
diff --git a/crates/notedeck_dave/src/focus_queue.rs b/crates/notedeck_dave/src/focus_queue.rs
@@ -45,6 +45,11 @@ impl FocusPriority {
}
}
+pub struct FocusQueueUpdate {
+ pub new_needs_input: bool,
+ pub changed: bool,
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QueueEntry {
pub session_id: SessionId,
@@ -302,19 +307,20 @@ impl FocusQueue {
}
/// Update focus queue based on session indicator fields.
- /// Returns true if any session transitioned to NeedsInput.
pub fn update_from_indicators(
&mut self,
sessions: impl Iterator<Item = (SessionId, Option<FocusPriority>)>,
- ) -> bool {
- let mut has_new_needs_input = false;
+ ) -> FocusQueueUpdate {
+ let mut new_needs_input = false;
+ let mut changed = false;
for (session_id, indicator) in sessions {
let prev = self.previous_indicators.get(&session_id).copied();
if prev != Some(indicator) {
+ changed = true;
if let Some(priority) = indicator {
self.enqueue(session_id, priority);
if priority == FocusPriority::NeedsInput {
- has_new_needs_input = true;
+ new_needs_input = true;
}
} else {
self.dequeue(session_id);
@@ -322,7 +328,10 @@ impl FocusQueue {
}
self.previous_indicators.insert(session_id, indicator);
}
- has_new_needs_input
+ FocusQueueUpdate {
+ new_needs_input,
+ changed,
+ }
}
pub fn get_session_priority(&self, session_id: SessionId) -> Option<FocusPriority> {
@@ -569,17 +578,30 @@ mod tests {
(session(2), Some(FocusPriority::NeedsInput)),
(session(3), None), // No indicator = no dot
];
- queue.update_from_indicators(indicators.into_iter());
+ let update = queue.update_from_indicators(indicators.into_iter());
+ assert!(update.changed);
+ assert!(update.new_needs_input);
assert_eq!(queue.len(), 2);
// Verify NeedsInput is first in priority order
assert_eq!(queue.entries[0].session_id, session(2));
assert_eq!(queue.entries[0].priority, FocusPriority::NeedsInput);
+ // No-op: same indicators again → no change
+ let indicators = vec![
+ (session(1), Some(FocusPriority::Done)),
+ (session(2), Some(FocusPriority::NeedsInput)),
+ ];
+ let update = queue.update_from_indicators(indicators.into_iter());
+ assert!(!update.changed);
+ assert!(!update.new_needs_input);
+
// Update: session 2 indicator cleared (should be removed from queue)
let indicators = vec![(session(2), None)];
- queue.update_from_indicators(indicators.into_iter());
+ let update = queue.update_from_indicators(indicators.into_iter());
+ assert!(update.changed);
+ assert!(!update.new_needs_input);
assert_eq!(queue.len(), 1);
assert_eq!(queue.current().unwrap().session_id, session(1));
}
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -2816,32 +2816,35 @@ impl notedeck::App for Dave {
// Update focus queue from persisted indicator field
let indicator_iter = self.session_manager.iter().map(|s| (s.id, s.indicator));
- let new_needs_input = self.focus_queue.update_from_indicators(indicator_iter);
+ let queue_update = self.focus_queue.update_from_indicators(indicator_iter);
// Vibrate on Android whenever a session transitions to NeedsInput
- if new_needs_input {
+ if queue_update.new_needs_input {
notedeck::platform::try_vibrate();
}
- // Suppress auto-steal while the user is typing (non-empty input)
- let user_is_typing = self
- .session_manager
- .get_active()
- .is_some_and(|s| !s.input.is_empty());
-
- // Process auto-steal focus mode
- 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,
- );
+ // 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)
+ 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,
+ );
- // Raise the OS window when auto-steal switches to a NeedsInput session
- if stole_focus {
- activate_app(ui.ctx());
+ // Raise the OS window when auto-steal switches to a NeedsInput session
+ if stole_focus {
+ activate_app(ui.ctx());
+ }
}
// Render UI and handle actions