commit 45e793c11a5a4f5e85bc5f10a74356b83f2db93d
parent 0314273c6e7fb2556f45e638d7ba6af82727e4ed
Author: William Casarin <jb55@jb55.com>
Date: Tue, 27 Jan 2026 15:51:15 -0800
dave: simplify focus_queue
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
1 file changed, 89 insertions(+), 231 deletions(-)
diff --git a/crates/notedeck_dave/src/focus_queue.rs b/crates/notedeck_dave/src/focus_queue.rs
@@ -2,59 +2,41 @@ use crate::agent_status::AgentStatus;
use crate::session::SessionId;
use std::collections::HashMap;
-/// Priority levels for the focus queue (higher = more urgent)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FocusPriority {
- /// Low priority - agent completed work (may need follow-up)
Done = 0,
- /// Medium priority - agent encountered an error
Error = 1,
- /// High priority - agent needs user input (permission request)
NeedsInput = 2,
}
impl FocusPriority {
- /// Convert from AgentStatus, returns None for non-queue-worthy statuses
pub fn from_status(status: AgentStatus) -> Option<Self> {
match status {
- AgentStatus::NeedsInput => Some(FocusPriority::NeedsInput),
- AgentStatus::Error => Some(FocusPriority::Error),
- AgentStatus::Done => Some(FocusPriority::Done),
+ AgentStatus::NeedsInput => Some(Self::NeedsInput),
+ AgentStatus::Error => Some(Self::Error),
+ AgentStatus::Done => Some(Self::Done),
AgentStatus::Idle | AgentStatus::Working => None,
}
}
- /// Get the color associated with this priority
pub fn color(&self) -> egui::Color32 {
match self {
- FocusPriority::NeedsInput => egui::Color32::from_rgb(255, 200, 0), // Yellow/amber
- FocusPriority::Error => egui::Color32::from_rgb(220, 60, 60), // Red
- FocusPriority::Done => egui::Color32::from_rgb(70, 130, 220), // Blue
+ Self::NeedsInput => egui::Color32::from_rgb(255, 200, 0),
+ Self::Error => egui::Color32::from_rgb(220, 60, 60),
+ Self::Done => egui::Color32::from_rgb(70, 130, 220),
}
}
}
-/// An entry in the focus queue
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QueueEntry {
pub session_id: SessionId,
pub priority: FocusPriority,
}
-/// Priority queue for agents needing attention.
-/// Uses separate vectors for each priority level for simpler management.
-/// Navigation goes through high priority first, then medium, then low.
pub struct FocusQueue {
- /// High priority - needs input (permission requests)
- needs_input: Vec<SessionId>,
- /// Medium priority - errors
- errors: Vec<SessionId>,
- /// Low priority - done
- done: Vec<SessionId>,
- /// Current navigation position as (priority_level, index within that level)
- /// priority_level: 0 = needs_input, 1 = errors, 2 = done
- cursor: Option<(usize, usize)>,
- /// Track previous status to detect transitions
+ entries: Vec<QueueEntry>, // kept sorted: NeedsInput -> Error -> Done
+ cursor: Option<usize>, // index into entries
previous_statuses: HashMap<SessionId, AgentStatus>,
}
@@ -67,272 +49,148 @@ impl Default for FocusQueue {
impl FocusQueue {
pub fn new() -> Self {
Self {
- needs_input: Vec::new(),
- errors: Vec::new(),
- done: Vec::new(),
+ entries: Vec::new(),
cursor: None,
previous_statuses: HashMap::new(),
}
}
- fn get_vec(&self, priority: FocusPriority) -> &Vec<SessionId> {
- match priority {
- FocusPriority::NeedsInput => &self.needs_input,
- FocusPriority::Error => &self.errors,
- FocusPriority::Done => &self.done,
- }
+ pub fn len(&self) -> usize {
+ self.entries.len()
}
- fn get_vec_mut(&mut self, priority: FocusPriority) -> &mut Vec<SessionId> {
- match priority {
- FocusPriority::NeedsInput => &mut self.needs_input,
- FocusPriority::Error => &mut self.errors,
- FocusPriority::Done => &mut self.done,
- }
+ pub fn is_empty(&self) -> bool {
+ self.entries.is_empty()
}
- fn get_vec_by_level(&self, level: usize) -> &Vec<SessionId> {
- match level {
- 0 => &self.needs_input,
- 1 => &self.errors,
- _ => &self.done,
- }
+ fn sort_key(p: FocusPriority) -> i32 {
+ // want NeedsInput first, then Error, then Done
+ -(p as i32)
}
- /// Find which vec contains a session and its index
- fn find_session(&self, session_id: SessionId) -> Option<(FocusPriority, usize)> {
- if let Some(idx) = self.needs_input.iter().position(|&id| id == session_id) {
- return Some((FocusPriority::NeedsInput, idx));
- }
- if let Some(idx) = self.errors.iter().position(|&id| id == session_id) {
- return Some((FocusPriority::Error, idx));
- }
- if let Some(idx) = self.done.iter().position(|&id| id == session_id) {
- return Some((FocusPriority::Done, idx));
- }
- None
+ fn find(&self, session_id: SessionId) -> Option<usize> {
+ self.entries.iter().position(|e| e.session_id == session_id)
}
- /// Enqueue a session with the given priority.
- /// If already in queue at different priority, moves it.
- pub fn enqueue(&mut self, session_id: SessionId, priority: FocusPriority) {
- // Remove from any existing position first
- if let Some((old_priority, _)) = self.find_session(session_id) {
- if old_priority == priority {
- return; // Already in correct queue
+ fn normalize_cursor_after_remove(&mut self, removed_idx: usize) {
+ match self.cursor {
+ None => {}
+ Some(_cur) if self.entries.is_empty() => self.cursor = None,
+ Some(cur) if removed_idx < cur => self.cursor = Some(cur - 1),
+ Some(cur) if removed_idx == cur => {
+ // keep cursor pointing at a valid item (same index if possible, else last)
+ let new_cur = cur.min(self.entries.len().saturating_sub(1));
+ self.cursor = Some(new_cur);
}
- self.get_vec_mut(old_priority)
- .retain(|&id| id != session_id);
- }
-
- // Add to appropriate queue
- self.get_vec_mut(priority).push(session_id);
-
- // Initialize cursor if this is the first item
- if self.cursor.is_none() && self.len() == 1 {
- let level = match priority {
- FocusPriority::NeedsInput => 0,
- FocusPriority::Error => 1,
- FocusPriority::Done => 2,
- };
- self.cursor = Some((level, 0));
- }
- }
-
- /// Remove a session from the queue
- pub fn dequeue(&mut self, session_id: SessionId) {
- if let Some((priority, idx)) = self.find_session(session_id) {
- let vec = self.get_vec_mut(priority);
- vec.remove(idx);
-
- // Adjust cursor if needed
- if let Some((level, cursor_idx)) = self.cursor {
- let priority_level = match priority {
- FocusPriority::NeedsInput => 0,
- FocusPriority::Error => 1,
- FocusPriority::Done => 2,
- };
-
- if self.is_empty() {
- self.cursor = None;
- } else if level == priority_level {
- // Removed from current level
- let vec_len = self.get_vec(priority).len();
- if vec_len == 0 {
- // Level is now empty, move to next non-empty level
- self.cursor = self.find_next_valid_cursor(level, 0);
- } else if idx <= cursor_idx {
- // Adjust index within level
- let new_idx = if idx < cursor_idx {
- cursor_idx - 1
- } else {
- cursor_idx.min(vec_len - 1)
- };
- self.cursor = Some((level, new_idx));
- }
- }
+ Some(_) => {}
+ }
+ }
+
+ /// Insert entry in priority order (stable within same priority).
+ fn insert_sorted(&mut self, entry: QueueEntry) {
+ let key = Self::sort_key(entry.priority);
+ let pos = self
+ .entries
+ .iter()
+ .position(|e| Self::sort_key(e.priority) > key)
+ .unwrap_or(self.entries.len());
+ self.entries.insert(pos, entry);
+
+ // initialize cursor if this is the first item
+ if self.cursor.is_none() && self.entries.len() == 1 {
+ self.cursor = Some(0);
+ } else if let Some(cur) = self.cursor {
+ // if we inserted before the cursor, shift cursor right
+ if pos <= cur {
+ self.cursor = Some(cur + 1);
}
}
}
- /// Find the next valid cursor position starting from given level
- fn find_next_valid_cursor(
- &self,
- start_level: usize,
- _start_idx: usize,
- ) -> Option<(usize, usize)> {
- // Try levels in order: needs_input (0), errors (1), done (2)
- for level in 0..3 {
- let check_level = (start_level + level) % 3;
- let vec = self.get_vec_by_level(check_level);
- if !vec.is_empty() {
- return Some((check_level, 0));
+ pub fn enqueue(&mut self, session_id: SessionId, priority: FocusPriority) {
+ if let Some(i) = self.find(session_id) {
+ if self.entries[i].priority == priority {
+ return;
}
+ // remove old entry, then reinsert at correct spot
+ self.entries.remove(i);
+ self.normalize_cursor_after_remove(i);
}
- None
- }
-
- /// Total number of items across all queues
- pub fn len(&self) -> usize {
- self.needs_input.len() + self.errors.len() + self.done.len()
+ self.insert_sorted(QueueEntry {
+ session_id,
+ priority,
+ });
}
- /// Check if all queues are empty
- pub fn is_empty(&self) -> bool {
- self.needs_input.is_empty() && self.errors.is_empty() && self.done.is_empty()
+ pub fn dequeue(&mut self, session_id: SessionId) {
+ if let Some(i) = self.find(session_id) {
+ self.entries.remove(i);
+ self.normalize_cursor_after_remove(i);
+ }
}
- /// Navigate to next item in queue (wraps around)
- /// Order: all needs_input -> all errors -> all done -> wrap to needs_input
pub fn next(&mut self) -> Option<SessionId> {
- if self.is_empty() {
+ if self.entries.is_empty() {
+ self.cursor = None;
return None;
}
-
- let (level, idx) = self.cursor.unwrap_or((0, 0));
-
- // Try next in current level
- let vec_len = self.get_vec_by_level(level).len();
- if idx + 1 < vec_len {
- let session_id = self.get_vec_by_level(level)[idx + 1];
- self.cursor = Some((level, idx + 1));
- return Some(session_id);
- }
-
- // Move to next non-empty level
- for offset in 1..=3 {
- let next_level = (level + offset) % 3;
- let next_vec = self.get_vec_by_level(next_level);
- if !next_vec.is_empty() {
- let session_id = next_vec[0];
- self.cursor = Some((next_level, 0));
- return Some(session_id);
- }
- }
-
- // Shouldn't reach here if not empty
- None
+ let cur = self.cursor.unwrap_or(0);
+ let next = (cur + 1) % self.entries.len();
+ self.cursor = Some(next);
+ Some(self.entries[next].session_id)
}
- /// Navigate to previous item in queue (wraps around)
pub fn prev(&mut self) -> Option<SessionId> {
- if self.is_empty() {
+ if self.entries.is_empty() {
+ self.cursor = None;
return None;
}
-
- let (level, idx) = self.cursor.unwrap_or((0, 0));
-
- // Try previous in current level
- if idx > 0 {
- let session_id = self.get_vec_by_level(level)[idx - 1];
- self.cursor = Some((level, idx - 1));
- return Some(session_id);
- }
-
- // Move to previous non-empty level (going backwards)
- for offset in 1..=3 {
- let prev_level = (level + 3 - offset) % 3;
- let prev_vec = self.get_vec_by_level(prev_level);
- if !prev_vec.is_empty() {
- let last_idx = prev_vec.len() - 1;
- let session_id = prev_vec[last_idx];
- self.cursor = Some((prev_level, last_idx));
- return Some(session_id);
- }
- }
-
- None
+ let cur = self.cursor.unwrap_or(0);
+ let prev = (cur + self.entries.len() - 1) % self.entries.len();
+ self.cursor = Some(prev);
+ Some(self.entries[prev].session_id)
}
- /// Get the current queue entry without changing position
pub fn current(&self) -> Option<QueueEntry> {
- let (level, idx) = self.cursor?;
- let vec = self.get_vec_by_level(level);
- let session_id = *vec.get(idx)?;
- let priority = match level {
- 0 => FocusPriority::NeedsInput,
- 1 => FocusPriority::Error,
- _ => FocusPriority::Done,
- };
- Some(QueueEntry {
- session_id,
- priority,
- })
+ let i = self.cursor?;
+ self.entries.get(i).copied()
}
- /// Get current position (1-indexed for display) across all queues
pub fn current_position(&self) -> Option<usize> {
- let (level, idx) = self.cursor?;
- let mut pos = idx + 1; // 1-indexed
-
- // Add counts from higher priority levels
- for l in 0..level {
- pos += self.get_vec_by_level(l).len();
- }
-
- Some(pos)
+ Some(self.cursor? + 1) // 1-indexed
}
- /// Get queue info for UI display: (position, total, priority)
- /// Returns None if queue is empty
pub fn ui_info(&self) -> Option<(usize, usize, FocusPriority)> {
let entry = self.current()?;
- let position = self.current_position()?;
- Some((position, self.len(), entry.priority))
+ Some((self.current_position()?, self.len(), entry.priority))
}
- /// Update queue based on status changes.
- /// Call this after updating all session statuses.
pub fn update_from_statuses(
&mut self,
sessions: impl Iterator<Item = (SessionId, AgentStatus)>,
) {
for (session_id, status) in sessions {
- let prev_status = self.previous_statuses.get(&session_id).copied();
-
- // Detect transition to a queue-worthy state
- if prev_status != Some(status) {
+ let prev = self.previous_statuses.get(&session_id).copied();
+ if prev != Some(status) {
if let Some(priority) = FocusPriority::from_status(status) {
- // State transitioned to NeedsInput, Error, or Done
self.enqueue(session_id, priority);
} else {
- // State transitioned away from queue-worthy (e.g., back to Working)
self.dequeue(session_id);
}
}
-
self.previous_statuses.insert(session_id, status);
}
}
- /// Remove tracking for a deleted session
+ pub fn get_session_priority(&self, session_id: SessionId) -> Option<FocusPriority> {
+ self.entries
+ .iter()
+ .find(|e| e.session_id == session_id)
+ .map(|e| e.priority)
+ }
+
pub fn remove_session(&mut self, session_id: SessionId) {
self.dequeue(session_id);
self.previous_statuses.remove(&session_id);
}
-
- /// Check if a session is in the queue and return its priority if so
- pub fn get_session_priority(&self, session_id: SessionId) -> Option<FocusPriority> {
- self.find_session(session_id).map(|(priority, _)| priority)
- }
}