notedeck

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

commit f578630fa5ab399807b047d99737bf066bf37f18
parent 083b2dfd2763f44864ed3800d05c79f324cf3909
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 27 Feb 2026 13:59:17 -0800

dave: add focus queue NEXT badge for mobile navigation

Adds a ▶ badge to the right of AUTO in the status bar when in narrow
mode, allowing mobile users to tap through the focus queue without
keyboard shortcuts (Ctrl+N).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_dave/src/lib.rs | 9+++++++++
Mcrates/notedeck_dave/src/ui/dave.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/notedeck_dave/src/ui/mod.rs | 5+++++
3 files changed, 63 insertions(+), 2 deletions(-)

diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -2333,6 +2333,15 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr self.handle_new_chat(); None } + UiActionResult::FocusQueueNext => { + crate::update::focus_queue_next( + &mut self.session_manager, + &mut self.focus_queue, + &mut self.scene, + self.show_scene, + ); + None + } UiActionResult::Compact => { if let Some(session) = self.session_manager.get_active() { let session_id = format!("dave-session-{}", session.id); diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -8,6 +8,7 @@ use crate::{ backend::BackendType, config::{AiMode, DaveSettings}, file_update::FileUpdate, + focus_queue::FocusPriority, git_status::GitStatusCache, messages::{ AskUserQuestionInput, AssistantMessage, CompactionInfo, ExecutedTool, Message, @@ -76,6 +77,8 @@ pub struct DaveUi<'a> { permission_mode: PermissionMode, /// When the last AI response token was received last_activity: Option<std::time::Instant>, + /// Focus queue info for mobile NEXT badge: (position, total, priority) + focus_queue_info: Option<(usize, usize, FocusPriority)>, } /// The response the app generates. The response contains an optional @@ -165,6 +168,8 @@ pub enum DaveAction { ToggleAutoSteal, /// Trigger manual context compaction Compact, + /// Navigate to the next focus queue item (mobile) + FocusQueueNext, } impl<'a> DaveUi<'a> { @@ -200,6 +205,7 @@ impl<'a> DaveUi<'a> { backend_type: BackendType::Remote, permission_mode: PermissionMode::Default, last_activity: None, + focus_queue_info: None, } } @@ -290,6 +296,11 @@ impl<'a> DaveUi<'a> { self } + pub fn focus_queue_info(mut self, info: Option<(usize, usize, FocusPriority)>) -> Self { + self.focus_queue_info = info; + self + } + pub fn usage(mut self, usage: &'a crate::messages::UsageInfo, model: Option<&str>) -> Self { self.usage = Some(usage); self.context_window = crate::messages::context_window_for_model(model); @@ -396,6 +407,7 @@ impl<'a> DaveUi<'a> { is_agentic, permission_mode, auto_steal_focus, + self.focus_queue_info, self.usage, self.context_window, self.last_activity, @@ -1465,6 +1477,7 @@ fn status_bar_ui( is_agentic: bool, permission_mode: PermissionMode, auto_steal_focus: bool, + focus_queue_info: Option<(usize, usize, FocusPriority)>, usage: Option<&crate::messages::UsageInfo>, context_window: u64, last_activity: Option<std::time::Instant>, @@ -1485,7 +1498,12 @@ fn status_bar_ui( // Right-aligned section: usage bar, badges, then refresh ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let badge_action = if is_agentic { - toggle_badges_ui(ui, permission_mode, auto_steal_focus) + toggle_badges_ui( + ui, + permission_mode, + auto_steal_focus, + focus_queue_info, + ) } else { None }; @@ -1505,7 +1523,12 @@ fn status_bar_ui( } else if is_agentic { // No git status (remote session) - just show badges and usage ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let badge_action = toggle_badges_ui(ui, permission_mode, auto_steal_focus); + let badge_action = toggle_badges_ui( + ui, + permission_mode, + auto_steal_focus, + focus_queue_info, + ); if let Some(instant) = last_activity { ui.label( egui::RichText::new(format_relative_time(instant)) @@ -1609,10 +1632,34 @@ fn toggle_badges_ui( ui: &mut egui::Ui, permission_mode: PermissionMode, auto_steal_focus: bool, + focus_queue_info: Option<(usize, usize, FocusPriority)>, ) -> Option<DaveAction> { let ctrl_held = ui.input(|i| i.modifiers.ctrl); + let is_narrow = notedeck::ui::is_narrow(ui.ctx()); let mut action = None; + // NEXT badge for focus queue navigation (narrow/mobile only, rendered first = rightmost) + if is_narrow { + if let Some((_pos, _total, priority)) = 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 mut next_badge = super::badge::StatusBadge::new("\u{25b6}").variant(variant); + if ctrl_held { + next_badge = next_badge.keybind("N"); + } + if next_badge + .show(ui) + .on_hover_text("Next in focus queue (Ctrl+N)") + .clicked() + { + action = Some(DaveAction::FocusQueueNext); + } + } + } + // AUTO badge (rendered first in right-to-left, so it appears rightmost) let mut auto_badge = super::badge::StatusBadge::new("AUTO").variant(if auto_steal_focus { super::badge::BadgeVariant::Info diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs @@ -559,6 +559,7 @@ pub fn narrow_ui( (DaveResponse::default(), session_action) } else if let Some(session) = session_manager.get_active_mut() { let dot_color = focus_queue.current().map(|e| e.priority.color()); + let fq_info = focus_queue.ui_info(); let response = build_dave_ui( session, model_config, @@ -566,6 +567,7 @@ pub fn narrow_ui( auto_steal_focus, ) .status_dot_color(dot_color) + .focus_queue_info(fq_info) .ui(app_ctx, ui); (response, None) } else { @@ -826,6 +828,8 @@ pub enum UiActionResult { Compact, /// Permission mode command needs relay publishing (observer → host). PublishModeCommand(update::ModeCommandPublish), + /// Navigate to next focus queue item (mobile) + FocusQueueNext, } /// Handle a UI action from DaveUi. @@ -906,6 +910,7 @@ pub fn handle_ui_action( } } DaveAction::ToggleAutoSteal => UiActionResult::ToggleAutoSteal, + DaveAction::FocusQueueNext => UiActionResult::FocusQueueNext, DaveAction::ExitPlanMode { request_id, approved,