notedeck

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

commit a4f6f25aca3bd3e24f77c30c6b9fbb6dd48d028f
parent e7d8acadcd326e5470c8012af9ade044b9eaf9b4
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 24 Feb 2026 13:28:45 -0800

dave: add backend brand icons and tracing for picker flow

Add Claude (terracotta) and Codex (green) SVG brand icons shown in
three places: session list items, session header, and backend picker
overlay. Icons are tinted with brand colors via backend_color().

Also add tracing throughout the backend picker flow (directory
selection → create_or_pick_backend → overlay rendering → selection)
to aid debugging overlay visibility issues.

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

Diffstat:
Aassets/icons/claude-code.svg | 3+++
Aassets/icons/codex.svg | 3+++
Mcrates/notedeck_dave/src/lib.rs | 26++++++++++++++++++++++++++
Mcrates/notedeck_dave/src/ui/dave.rs | 71++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mcrates/notedeck_dave/src/ui/mod.rs | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mcrates/notedeck_dave/src/ui/session_list.rs | 17++++++++++++++++-
Mcrates/notedeck_dave/src/update.rs | 1+
7 files changed, 152 insertions(+), 33 deletions(-)

diff --git a/assets/icons/claude-code.svg b/assets/icons/claude-code.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z" fill="white"/> +</svg> diff --git a/assets/icons/codex.svg b/assets/icons/codex.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M14.949 6.547a3.94 3.94 0 0 0-.348-3.273 4.11 4.11 0 0 0-4.4-1.934 4.1 4.1 0 0 0-1.778-.14 4.15 4.15 0 0 0-2.118.114 4.1 4.1 0 0 0-1.891.948 4.04 4.04 0 0 0-1.158 1.753 4.1 4.1 0 0 0-1.563.679 4 4 0 0 0-1.139 1.254 3.99 3.99 0 0 0 .502 4.731 3.94 3.94 0 0 0 .346 3.274 4.11 4.11 0 0 0 4.402 1.933c.382.425.852.764 1.377.995.526.231 1.095.35 1.67.346 1.78.002 3.358-1.132 3.901-2.804a4.1 4.1 0 0 0 1.563-.68 4 4 0 0 0 1.14-1.253 3.99 3.99 0 0 0-.506-4.716m-6.097 8.406a3.05 3.05 0 0 1-1.945-.694l.096-.054 3.23-1.838a.53.53 0 0 0 .265-.455v-4.49l1.366.778q.02.011.025.035v3.722c-.003 1.653-1.361 2.992-3.037 2.996m-6.53-2.75a2.95 2.95 0 0 1-.36-2.01l.095.057L5.29 12.09a.53.53 0 0 0 .527 0l3.949-2.246v1.555a.05.05 0 0 1-.022.041L6.473 13.3c-1.454.826-3.311.335-4.15-1.098m-.85-6.94A3.02 3.02 0 0 1 3.07 3.949v3.785a.51.51 0 0 0 .262.451l3.93 2.237-1.366.779a.05.05 0 0 1-.048 0L2.585 9.342a2.98 2.98 0 0 1-1.113-4.094zm11.216 2.571L8.747 5.576l1.362-.776a.05.05 0 0 1 .048 0l3.265 1.86a3 3 0 0 1 1.173 1.207 2.96 2.96 0 0 1-.27 3.2 3.05 3.05 0 0 1-1.36.997V8.279a.52.52 0 0 0-.276-.445m1.36-2.015-.097-.057-3.226-1.855a.53.53 0 0 0-.53 0L6.249 6.153V4.598a.04.04 0 0 1 .019-.04L9.533 2.7a3.07 3.07 0 0 1 3.257.139c.474.325.843.778 1.066 1.303.223.526.289 1.103.191 1.664zM5.503 8.575 4.139 7.8a.05.05 0 0 1-.026-.037V4.049c0-.57.166-1.127.476-1.607s.752-.864 1.275-1.105a3.08 3.08 0 0 1 3.234.41l-.096.054-3.23 1.838a.53.53 0 0 0-.265.455zm.742-1.577 1.758-1 1.762 1v2l-1.755 1-1.762-1z" fill="white"/> +</svg> diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -368,6 +368,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr // Detect available agentic backends from PATH let available_backends = config::available_agentic_backends(); + tracing::info!( + "detected {} agentic backends: {:?}", + available_backends.len(), + available_backends + ); // Create backends for all available agentic CLIs + the configured primary let mut backends: HashMap<BackendType, Box<dyn AiBackend>> = HashMap::new(); @@ -991,9 +996,14 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr match ui::directory_picker_overlay_ui(&mut self.directory_picker, has_sessions, ui) { OverlayResult::DirectorySelected(path) => { + tracing::info!("directory selected (no resumable sessions): {:?}", path); self.create_or_pick_backend(path); } OverlayResult::ShowSessionPicker(path) => { + tracing::info!( + "directory has resumable sessions, showing session picker: {:?}", + path + ); self.session_picker.open(path); self.active_overlay = DaveOverlay::SessionPicker; } @@ -1025,6 +1035,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr self.active_overlay = DaveOverlay::None; } OverlayResult::NewSession { cwd } => { + tracing::info!("new session from session picker: {:?}", cwd); self.session_picker.close(); self.create_or_pick_backend(cwd); } @@ -1037,7 +1048,12 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr return DaveResponse::default(); } DaveOverlay::BackendPicker => { + tracing::info!( + "rendering backend picker: {} backends available", + self.available_backends.len() + ); if let Some(bt) = ui::backend_picker_overlay_ui(&self.available_backends, ui) { + tracing::info!("backend selected: {:?}", bt); if let Some(cwd) = self.pending_backend_cwd.take() { self.create_session_with_cwd(cwd, bt); } @@ -2147,7 +2163,13 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr /// Create a session with the given cwd, or show the backend picker if /// multiple agentic backends are available. fn create_or_pick_backend(&mut self, cwd: PathBuf) { + tracing::info!( + "create_or_pick_backend: {} available backends: {:?}", + self.available_backends.len(), + self.available_backends + ); if let Some(bt) = self.single_agentic_backend() { + tracing::info!("single backend detected, skipping picker: {:?}", bt); self.create_session_with_cwd(cwd, bt); self.active_overlay = DaveOverlay::None; } else if self.available_backends.is_empty() { @@ -2155,6 +2177,10 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr self.create_session_with_cwd(cwd, self.model_config.backend); self.active_overlay = DaveOverlay::None; } else { + tracing::info!( + "multiple backends available, showing backend picker: {:?}", + self.available_backends + ); self.pending_backend_cwd = Some(cwd); self.active_overlay = DaveOverlay::BackendPicker; } diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -5,6 +5,7 @@ use super::markdown_ui; use super::query_ui::query_call_ui; use super::top_buttons::top_buttons_ui; use crate::{ + backend::BackendType, config::{AiMode, DaveSettings}, file_update::FileUpdate, git_status::GitStatusCache, @@ -70,6 +71,8 @@ pub struct DaveUi<'a> { /// Number of trailing user messages dispatched in the current stream. /// Used by the queued indicator to skip dispatched messages. dispatched_user_count: usize, + /// Which backend this session uses + backend_type: BackendType, } /// The response the app generates. The response contains an optional @@ -183,9 +186,15 @@ impl<'a> DaveUi<'a> { usage: None, context_window: crate::messages::context_window_for_model(None), dispatched_user_count: 0, + backend_type: BackendType::Remote, } } + pub fn backend_type(mut self, bt: BackendType) -> Self { + self.backend_type = bt; + self + } + pub fn details(mut self, details: &'a SessionDetails) -> Self { self.details = Some(details); self @@ -311,7 +320,7 @@ impl<'a> DaveUi<'a> { ); ui.allocate_new_ui(egui::UiBuilder::new().max_rect(details_rect), |ui| { ui.set_clip_rect(details_rect); - session_header_ui(ui, details); + session_header_ui(ui, details, self.backend_type); }); } } @@ -1526,33 +1535,41 @@ fn toggle_badges_ui( action } -fn session_header_ui(ui: &mut egui::Ui, details: &SessionDetails) { - ui.vertical(|ui| { - ui.spacing_mut().item_spacing.y = 1.0; - ui.add( - egui::Label::new(egui::RichText::new(details.display_title()).size(13.0)) - .wrap_mode(egui::TextWrapMode::Truncate), - ); - if let Some(cwd) = &details.cwd { - let cwd_display = if details.home_dir.is_empty() { - crate::path_utils::abbreviate_path(cwd) - } else { - crate::path_utils::abbreviate_with_home(cwd, &details.home_dir) - }; - let display_text = if details.hostname.is_empty() { - cwd_display - } else { - format!("{}:{}", details.hostname, cwd_display) - }; +fn session_header_ui(ui: &mut egui::Ui, details: &SessionDetails, backend_type: BackendType) { + ui.horizontal(|ui| { + // Backend icon + if backend_type.is_agentic() { + let icon = crate::ui::backend_icon(backend_type).max_height(16.0); + ui.add(icon); + } + + ui.vertical(|ui| { + ui.spacing_mut().item_spacing.y = 1.0; ui.add( - egui::Label::new( - egui::RichText::new(display_text) - .monospace() - .size(10.0) - .weak(), - ) - .wrap_mode(egui::TextWrapMode::Truncate), + egui::Label::new(egui::RichText::new(details.display_title()).size(13.0)) + .wrap_mode(egui::TextWrapMode::Truncate), ); - } + if let Some(cwd) = &details.cwd { + let cwd_display = if details.home_dir.is_empty() { + crate::path_utils::abbreviate_path(cwd) + } else { + crate::path_utils::abbreviate_with_home(cwd, &details.home_dir) + }; + let display_text = if details.hostname.is_empty() { + cwd_display + } else { + format!("{}:{}", details.hostname, cwd_display) + }; + ui.add( + egui::Label::new( + egui::RichText::new(display_text) + .monospace() + .size(10.0) + .weak(), + ) + .wrap_mode(egui::TextWrapMode::Truncate), + ); + } + }); }); } diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs @@ -38,6 +38,7 @@ use crate::session::{ChatSession, PermissionMessageState, SessionId, SessionMana use crate::session_discovery::discover_sessions; use crate::update; use crate::DaveOverlay; +use egui::include_image; /// Build a DaveUi from a session, wiring up all the common builder fields. fn build_dave_ui<'a>( @@ -66,7 +67,8 @@ fn build_dave_ui<'a>( .auto_steal_focus(auto_steal_focus) .is_remote(is_remote) .dispatched_user_count(session.dispatched_user_count) - .details(&session.details); + .details(&session.details) + .backend_type(session.backend_type); if let Some(agentic) = &mut session.agentic { let model = agentic @@ -202,6 +204,29 @@ pub fn session_picker_overlay_ui( OverlayResult::None } +/// Brand color for a backend type. +pub fn backend_color(bt: BackendType) -> egui::Color32 { + match bt { + BackendType::Claude => egui::Color32::from_rgb(0xD9, 0x77, 0x57), // Anthropic terracotta + BackendType::Codex => egui::Color32::from_rgb(0x10, 0xA3, 0x7F), // OpenAI green + _ => egui::Color32::WHITE, + } +} + +/// Get an icon image for a backend type, tinted with its brand color. +pub fn backend_icon(bt: BackendType) -> egui::Image<'static> { + let img = match bt { + BackendType::Claude => { + egui::Image::new(include_image!("../../../../assets/icons/claude-code.svg")) + } + BackendType::Codex => { + egui::Image::new(include_image!("../../../../assets/icons/codex.svg")) + } + _ => egui::Image::new(include_image!("../../../../assets/icons/sparkle.svg")), + }; + img.tint(backend_color(bt)) +} + /// Render the backend picker overlay UI. /// Returns Some(BackendType) when the user has selected a backend. pub fn backend_picker_overlay_ui( @@ -247,11 +272,40 @@ pub fn backend_picker_overlay_ui( egui::Layout::top_down(egui::Align::LEFT), |ui| { for (idx, &bt) in available_backends.iter().enumerate() { + let desired = egui::vec2(max_width, 44.0); + let (rect, response) = + ui.allocate_exact_size(desired, egui::Sense::click()); + let response = response.on_hover_cursor(egui::CursorIcon::PointingHand); + + // Background + let fill = if response.hovered() { + ui.visuals().widgets.hovered.weak_bg_fill + } else { + ui.visuals().widgets.inactive.weak_bg_fill + }; + ui.painter().rect_filled(rect, 8.0, fill); + + // Icon + let icon_size = 20.0; + let icon_x = rect.left() + 12.0; + let icon_rect = egui::Rect::from_center_size( + egui::pos2(icon_x + icon_size / 2.0, rect.center().y), + egui::vec2(icon_size, icon_size), + ); + backend_icon(bt).paint_at(ui, icon_rect); + + // Label let label = format!("[{}] {}", idx + 1, bt.display_name()); - let button = egui::Button::new(egui::RichText::new(&label).size(16.0)) - .min_size(egui::vec2(max_width, 40.0)); - - if ui.add(button).clicked() { + let text_pos = egui::pos2(icon_x + icon_size + 10.0, rect.center().y); + ui.painter().text( + text_pos, + egui::Align2::LEFT_CENTER, + &label, + egui::FontId::proportional(16.0), + ui.visuals().text_color(), + ); + + if response.clicked() { selected = Some(bt); } ui.add_space(4.0); diff --git a/crates/notedeck_dave/src/ui/session_list.rs b/crates/notedeck_dave/src/ui/session_list.rs @@ -4,6 +4,7 @@ use egui::{Align, Color32, Layout, Sense}; use notedeck_ui::app_images; use crate::agent_status::AgentStatus; +use crate::backend::BackendType; use crate::config::AiMode; use crate::focus_queue::{FocusPriority, FocusQueue}; use crate::session::{SessionId, SessionManager}; @@ -177,6 +178,7 @@ impl<'a> SessionListUi<'a> { session.status(), queue_priority, session.ai_mode, + session.backend_type, ); let mut action = None; @@ -251,6 +253,7 @@ impl<'a> SessionListUi<'a> { status: AgentStatus, queue_priority: Option<FocusPriority>, session_ai_mode: AiMode, + backend_type: BackendType, ) -> egui::Response { // Per-session: Chat sessions get shorter height (no CWD), no status bar // Agentic sessions get taller height with CWD and status bar @@ -278,7 +281,7 @@ impl<'a> SessionListUi<'a> { ui.painter().rect_filled(rect, corner_radius, fill); // Status color indicator (left edge vertical bar) - only in Agentic mode - let text_start_x = if show_status_bar { + let mut text_start_x = if show_status_bar { let status_color = status.color(); let status_bar_rect = egui::Rect::from_min_size( rect.left_top() + egui::vec2(2.0, 4.0), @@ -290,6 +293,18 @@ impl<'a> SessionListUi<'a> { 8.0 // Smaller padding in Chat mode (no status bar) }; + // Backend icon (only for agentic backends) + if backend_type.is_agentic() { + let icon_size = 14.0; + let icon_rect = egui::Rect::from_center_size( + rect.left_center() + egui::vec2(text_start_x + icon_size / 2.0, 0.0), + egui::vec2(icon_size, icon_size), + ); + let icon = crate::ui::backend_icon(backend_type); + icon.paint_at(ui, icon_rect); + text_start_x += icon_size + 4.0; + } + // Draw shortcut hint at the far right let mut right_offset = 8.0; // Start with normal right padding diff --git a/crates/notedeck_dave/src/update.rs b/crates/notedeck_dave/src/update.rs @@ -899,6 +899,7 @@ pub fn poll_editor_job(session_manager: &mut SessionManager) { // ============================================================================= /// Create a new session with the given cwd. +#[allow(clippy::too_many_arguments)] pub fn create_session_with_cwd( session_manager: &mut SessionManager, directory_picker: &mut DirectoryPicker,