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:
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,