commit ca439afa578b06042a654fb3abb0ca5311edaf25
parent ed4c3747c17d3e0a80ac9829b3ca92b624eca0d5
Author: William Casarin <jb55@jb55.com>
Date: Tue, 27 Jan 2026 13:22:09 -0800
dave: move focus queue indicator to chat listing dots
Move the 1/2 queue position badge from the input box to indicator dots
on the chat listing sidebar. Sessions in the focus queue now show a
colored dot on the far right side of their listing entry, using the
same priority colors (yellow for NeedsInput, red for Error, blue for
Done).
This provides better visibility of which agents need attention without
cluttering the input area.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
4 files changed, 71 insertions(+), 70 deletions(-)
diff --git a/crates/notedeck_dave/src/focus_queue.rs b/crates/notedeck_dave/src/focus_queue.rs
@@ -330,4 +330,9 @@ impl FocusQueue {
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)
+ }
}
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -368,7 +368,6 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
.show(ui, |ui| {
if let Some(selected_id) = self.scene.primary_selection() {
let interrupt_pending = self.is_interrupt_pending();
- let queue_info = self.focus_queue.ui_info();
if let Some(session) = self.session_manager.get_mut(selected_id) {
// Show title
ui.heading(&session.title);
@@ -382,7 +381,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
!session.pending_permissions.is_empty();
let plan_mode_active =
session.permission_mode == PermissionMode::Plan;
- let mut dave_ui = DaveUi::new(
+ let response = DaveUi::new(
self.model_config.trial,
&session.chat,
&mut session.input,
@@ -393,13 +392,8 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
.interrupt_pending(interrupt_pending)
.has_pending_permission(has_pending_permission)
.plan_mode_active(plan_mode_active)
- .permission_message_state(session.permission_message_state);
-
- if let Some((pos, total, priority)) = queue_info {
- dave_ui = dave_ui.focus_queue_info(pos, total, priority);
- }
-
- let response = dave_ui.ui(app_ctx, ui);
+ .permission_message_state(session.permission_message_state)
+ .ui(app_ctx, ui);
if response.action.is_some() {
dave_response = response;
@@ -485,7 +479,8 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
});
ui.separator();
- SessionListUi::new(&self.session_manager, ctrl_held).ui(ui)
+ SessionListUi::new(&self.session_manager, &self.focus_queue, ctrl_held)
+ .ui(ui)
})
.inner
})
@@ -493,14 +488,13 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
// Now we can mutably borrow for chat
let interrupt_pending = self.is_interrupt_pending();
- let queue_info = self.focus_queue.ui_info();
let chat_response = ui
.allocate_new_ui(egui::UiBuilder::new().max_rect(chat_rect), |ui| {
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();
let plan_mode_active = session.permission_mode == PermissionMode::Plan;
- let mut dave_ui = DaveUi::new(
+ DaveUi::new(
self.model_config.trial,
&session.chat,
&mut session.input,
@@ -510,13 +504,8 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
.interrupt_pending(interrupt_pending)
.has_pending_permission(has_pending_permission)
.plan_mode_active(plan_mode_active)
- .permission_message_state(session.permission_message_state);
-
- if let Some((pos, total, priority)) = queue_info {
- dave_ui = dave_ui.focus_queue_info(pos, total, priority);
- }
-
- dave_ui.ui(app_ctx, ui)
+ .permission_message_state(session.permission_message_state)
+ .ui(app_ctx, ui)
} else {
DaveResponse::default()
}
@@ -548,7 +537,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
.fill(ui.visuals().faint_bg_color)
.inner_margin(egui::Margin::symmetric(8, 12))
.show(ui, |ui| {
- SessionListUi::new(&self.session_manager, ctrl_held).ui(ui)
+ SessionListUi::new(&self.session_manager, &self.focus_queue, ctrl_held).ui(ui)
})
.inner;
if let Some(action) = session_action {
@@ -570,12 +559,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
} else {
// Show chat
let interrupt_pending = self.is_interrupt_pending();
- let queue_info = self.focus_queue.ui_info();
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();
let plan_mode_active = session.permission_mode == PermissionMode::Plan;
- let mut dave_ui = DaveUi::new(
+ DaveUi::new(
self.model_config.trial,
&session.chat,
&mut session.input,
@@ -585,13 +573,8 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
.interrupt_pending(interrupt_pending)
.has_pending_permission(has_pending_permission)
.plan_mode_active(plan_mode_active)
- .permission_message_state(session.permission_message_state);
-
- if let Some((pos, total, priority)) = queue_info {
- dave_ui = dave_ui.focus_queue_info(pos, total, priority);
- }
-
- dave_ui.ui(app_ctx, ui)
+ .permission_message_state(session.permission_message_state)
+ .ui(app_ctx, ui)
} else {
DaveResponse::default()
}
diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs
@@ -2,7 +2,6 @@ use super::diff;
use crate::{
config::DaveSettings,
file_update::FileUpdate,
- focus_queue::FocusPriority,
messages::{
Message, PermissionRequest, PermissionResponse, PermissionResponseType, ToolResult,
},
@@ -30,8 +29,6 @@ pub struct DaveUi<'a> {
plan_mode_active: bool,
/// State for tentative permission response (waiting for message)
permission_message_state: PermissionMessageState,
- /// Focus queue info: (current_position, total, priority)
- focus_queue_info: Option<(usize, usize, FocusPriority)>,
}
/// The response the app generates. The response contains an optional
@@ -115,7 +112,6 @@ impl<'a> DaveUi<'a> {
focus_requested,
plan_mode_active: false,
permission_message_state: PermissionMessageState::None,
- focus_queue_info: None,
}
}
@@ -149,16 +145,6 @@ impl<'a> DaveUi<'a> {
self
}
- pub fn focus_queue_info(
- mut self,
- position: usize,
- total: usize,
- priority: FocusPriority,
- ) -> Self {
- self.focus_queue_info = Some((position, total, priority));
- self
- }
-
fn chat_margin(&self, ctx: &egui::Context) -> i8 {
if self.compact || notedeck::ui::is_narrow(ctx) {
20
@@ -711,24 +697,6 @@ impl<'a> DaveUi<'a> {
}
badge.show(ui).on_hover_text("Ctrl+M to toggle plan mode");
- // Show focus queue indicator if there are items in the queue
- if let Some((position, total, priority)) = self.focus_queue_info {
- let variant = match priority {
- FocusPriority::NeedsInput => super::badge::BadgeVariant::Warning,
- FocusPriority::Error => super::badge::BadgeVariant::Destructive,
- FocusPriority::Done => super::badge::BadgeVariant::Info,
- };
- let queue_text = format!("{}/{}", position, total);
- let mut queue_badge =
- super::badge::StatusBadge::new(&queue_text).variant(variant);
- if ctrl_held {
- queue_badge = queue_badge.keybind("N/P/D");
- }
- queue_badge
- .show(ui)
- .on_hover_text("Focus Queue: Ctrl+N next, Ctrl+P prev, Ctrl+D dismiss");
- }
-
let r = ui.add(
egui::TextEdit::multiline(self.input)
.desired_width(f32::INFINITY)
diff --git a/crates/notedeck_dave/src/ui/session_list.rs b/crates/notedeck_dave/src/ui/session_list.rs
@@ -2,6 +2,7 @@ use egui::{Align, Color32, Layout, Sense};
use notedeck_ui::app_images;
use crate::agent_status::AgentStatus;
+use crate::focus_queue::{FocusPriority, FocusQueue};
use crate::session::{SessionId, SessionManager};
use crate::ui::keybind_hint::paint_keybind_hint;
@@ -16,13 +17,19 @@ pub enum SessionListAction {
/// UI component for displaying the session list sidebar
pub struct SessionListUi<'a> {
session_manager: &'a SessionManager,
+ focus_queue: &'a FocusQueue,
ctrl_held: bool,
}
impl<'a> SessionListUi<'a> {
- pub fn new(session_manager: &'a SessionManager, ctrl_held: bool) -> Self {
+ pub fn new(
+ session_manager: &'a SessionManager,
+ focus_queue: &'a FocusQueue,
+ ctrl_held: bool,
+ ) -> Self {
SessionListUi {
session_manager,
+ focus_queue,
ctrl_held,
}
}
@@ -89,12 +96,16 @@ impl<'a> SessionListUi<'a> {
None
};
+ // Check if this session is in the focus queue
+ let queue_priority = self.focus_queue.get_session_priority(session.id);
+
let response = self.session_item_ui(
ui,
&session.title,
is_active,
shortcut_hint,
session.status(),
+ queue_priority,
);
if response.clicked() {
@@ -120,6 +131,7 @@ impl<'a> SessionListUi<'a> {
is_active: bool,
shortcut_hint: Option<usize>,
status: AgentStatus,
+ queue_priority: Option<FocusPriority>,
) -> egui::Response {
let desired_size = egui::vec2(ui.available_width(), 36.0);
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click());
@@ -159,15 +171,48 @@ impl<'a> SessionListUi<'a> {
12.0 // Leave room for status bar
};
- // Draw title text
+ // Draw focus queue indicator dot on the far right if in queue
+ let text_end_x = if let Some(priority) = queue_priority {
+ let dot_radius = 5.0;
+ let dot_center = rect.right_center() - egui::vec2(12.0, 0.0);
+ ui.painter()
+ .circle_filled(dot_center, dot_radius, priority.color());
+ 24.0 // Space reserved for the dot
+ } else {
+ 8.0 // Normal right padding
+ };
+
+ // Draw title text (with clipping to avoid overlapping the dot)
let text_pos = rect.left_center() + egui::vec2(text_start_x, 0.0);
- ui.painter().text(
- text_pos,
- egui::Align2::LEFT_CENTER,
- title,
- egui::FontId::proportional(14.0),
- ui.visuals().text_color(),
- );
+ let max_text_width = rect.width() - text_start_x - text_end_x;
+
+ // Clip title if needed
+ let font_id = egui::FontId::proportional(14.0);
+ let text_color = ui.visuals().text_color();
+ let galley = ui
+ .painter()
+ .layout_no_wrap(title.to_string(), font_id.clone(), text_color);
+
+ if galley.size().x > max_text_width {
+ // Text is too long, use ellipsis
+ let clip_rect = egui::Rect::from_min_size(
+ text_pos - egui::vec2(0.0, galley.size().y / 2.0),
+ egui::vec2(max_text_width, galley.size().y),
+ );
+ ui.painter().with_clip_rect(clip_rect).galley(
+ text_pos - egui::vec2(0.0, galley.size().y / 2.0),
+ galley,
+ text_color,
+ );
+ } else {
+ ui.painter().text(
+ text_pos,
+ egui::Align2::LEFT_CENTER,
+ title,
+ font_id,
+ text_color,
+ );
+ }
response
}