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