notedeck

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

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:
Mcrates/notedeck_dave/src/focus_queue.rs | 36+++++++++++++++++++++++++++++-------
Mcrates/notedeck_dave/src/lib.rs | 43+++++++++++++++++++++++--------------------
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