notedeck

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

commit 5dfa8a1f35a34168bdffed2272b8058400d6f8eb
parent a621b0b7b7d1ec17c31a71dfd56c485acf7558cc
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 19 Feb 2026 10:17:44 -0800

dave: abbreviate home dir with ~ and add session header to chat view

Consolidate title/hostname/cwd into SessionDetails struct on
ChatSession. Use abbreviate_path() in session list, scene view, and
the new chat header so paths like /home/jb55/dev/foo show as ~/dev/foo.

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

Diffstat:
Mcrates/notedeck_dave/src/lib.rs | 12++++++------
Mcrates/notedeck_dave/src/session.rs | 30++++++++++++++++++++++--------
Mcrates/notedeck_dave/src/ui/dave.rs | 44++++++++++++++++++++++++++++++++++++++++++--
Mcrates/notedeck_dave/src/ui/mod.rs | 5+++--
Mcrates/notedeck_dave/src/ui/scene.rs | 4++--
Mcrates/notedeck_dave/src/ui/session_list.rs | 8++++----
Mcrates/notedeck_dave/src/update.rs | 4++--
7 files changed, 81 insertions(+), 26 deletions(-)

diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -347,7 +347,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr // Create a default session with current directory let sid = manager.new_session(std::env::current_dir().unwrap_or_default(), ai_mode); if let Some(session) = manager.get_mut(sid) { - session.hostname = hostname.clone(); + session.details.hostname = hostname.clone(); } (manager, DaveOverlay::None) } @@ -1094,7 +1094,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr // Focus on new session if let Some(session) = self.session_manager.get_mut(id) { - session.hostname = self.hostname.clone(); + session.details.hostname = self.hostname.clone(); session.focus_requested = true; if self.show_scene { self.scene.select(id); @@ -1257,7 +1257,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr queue_built_event( session_events::build_session_state_event( &claude_sid, - &session.title, + &session.details.title, &cwd, status, &self.hostname, @@ -1412,7 +1412,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr // Local sessions use the current machine's hostname; // remote sessions use what was stored in the event. - session.hostname = if is_remote { + session.details.hostname = if is_remote { state.hostname.clone() } else { self.hostname.clone() @@ -1585,7 +1585,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr let loaded = session_loader::load_session_messages(ctx.ndb, &txn, claude_sid); if let Some(session) = self.session_manager.get_mut(dave_sid) { - session.hostname = hostname; + session.details.hostname = hostname; if !loaded.messages.is_empty() { tracing::info!( "loaded {} messages for discovered session", @@ -1828,7 +1828,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr if let Some(claude_sid) = agentic.event_session_id() { self.pending_deletions.push(DeletedSessionInfo { claude_session_id: claude_sid.to_string(), - title: session.title.clone(), + title: session.details.title.clone(), cwd: agentic.cwd.to_string_lossy().to_string(), }); } diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs @@ -27,6 +27,13 @@ pub enum SessionSource { Remote, } +/// Session metadata for display in chat headers +pub struct SessionDetails { + pub title: String, + pub hostname: String, + pub cwd: Option<PathBuf>, +} + /// State for permission response with message #[derive(Default, Clone, Copy, PartialEq)] pub enum PermissionMessageState { @@ -263,7 +270,6 @@ impl AgenticSessionData { /// A single chat session with Dave pub struct ChatSession { pub id: SessionId, - pub title: String, pub chat: Vec<Message>, pub input: String, pub incoming_tokens: Option<Receiver<DaveApiResponse>>, @@ -282,8 +288,8 @@ pub struct ChatSession { pub agentic: Option<AgenticSessionData>, /// Whether this session is local (has a Claude process) or remote (relay-only). pub source: SessionSource, - /// Hostname of the machine where this session originated. - pub hostname: String, + /// Session metadata for display (title, hostname, cwd) + pub details: SessionDetails, } impl Drop for ChatSession { @@ -296,6 +302,11 @@ impl Drop for ChatSession { impl ChatSession { pub fn new(id: SessionId, cwd: PathBuf, ai_mode: AiMode) -> Self { + let details_cwd = if ai_mode == AiMode::Agentic { + Some(cwd.clone()) + } else { + None + }; let agentic = match ai_mode { AiMode::Agentic => Some(AgenticSessionData::new(id, cwd)), AiMode::Chat => None, @@ -303,7 +314,6 @@ impl ChatSession { ChatSession { id, - title: "New Chat".to_string(), chat: vec![], input: String::new(), incoming_tokens: None, @@ -314,7 +324,11 @@ impl ChatSession { ai_mode, agentic, source: SessionSource::Local, - hostname: String::new(), + details: SessionDetails { + title: "New Chat".to_string(), + hostname: String::new(), + cwd: details_cwd, + }, } } @@ -330,7 +344,7 @@ impl ChatSession { if let Some(ref mut agentic) = session.agentic { agentic.resume_session_id = Some(resume_session_id); } - session.title = title; + session.details.title = title; session } @@ -430,8 +444,8 @@ impl ChatSession { } else { title }; - if new_title != self.title { - self.title = new_title; + if new_title != self.details.title { + self.details.title = new_title; self.state_dirty = true; } break; diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -13,7 +13,7 @@ use crate::{ PermissionResponse, PermissionResponseType, QuestionAnswer, SubagentInfo, SubagentStatus, ToolResult, }, - session::{PermissionMessageState, SessionId}, + session::{PermissionMessageState, SessionDetails, SessionId}, tools::{PresentNotesCall, ToolCall, ToolCalls, ToolResponse}, }; use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers}; @@ -52,6 +52,8 @@ pub struct DaveUi<'a> { git_status: Option<&'a mut GitStatusCache>, /// Whether this is a remote session (no local Claude process) is_remote: bool, + /// Session details for header display + details: Option<&'a SessionDetails>, } /// The response the app generates. The response contains an optional @@ -159,9 +161,15 @@ impl<'a> DaveUi<'a> { ai_mode, git_status: None, is_remote: false, + details: None, } } + pub fn details(mut self, details: &'a SessionDetails) -> Self { + self.details = Some(details); + self + } + pub fn permission_message_state(mut self, state: PermissionMessageState) -> Self { self.permission_message_state = state; self @@ -324,7 +332,13 @@ impl<'a> DaveUi<'a> { .show(ui, |ui| { self.chat_frame(ui.ctx()) .show(ui, |ui| { - ui.vertical(|ui| self.render_chat(app_ctx, ui)).inner + ui.vertical(|ui| { + if let Some(details) = self.details { + session_header_ui(ui, details); + } + self.render_chat(app_ctx, ui) + }) + .inner }) .inner }) @@ -1303,3 +1317,29 @@ fn toggle_badges_ui( action } + +fn session_header_ui(ui: &mut egui::Ui, details: &SessionDetails) { + ui.horizontal(|ui| { + ui.heading(&details.title); + }); + + if let Some(cwd) = &details.cwd { + let cwd_display = super::path_utils::abbreviate_path(cwd); + 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(11.0) + .weak(), + ) + .wrap_mode(egui::TextWrapMode::Truncate), + ); + } + + ui.separator(); +} diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs @@ -64,7 +64,8 @@ fn build_dave_ui<'a>( .has_pending_permission(has_pending_permission) .plan_mode_active(plan_mode_active) .auto_steal_focus(auto_steal_focus) - .is_remote(is_remote); + .is_remote(is_remote) + .details(&session.details); if let Some(agentic) = &mut session.agentic { ui_builder = ui_builder @@ -263,7 +264,7 @@ pub fn scene_ui( .show(ui, |ui| { if let Some(selected_id) = scene.primary_selection() { if let Some(session) = session_manager.get_mut(selected_id) { - ui.heading(&session.title); + ui.heading(&session.details.title); ui.separator(); let response = build_dave_ui( diff --git a/crates/notedeck_dave/src/ui/scene.rs b/crates/notedeck_dave/src/ui/scene.rs @@ -165,7 +165,7 @@ impl AgentScene { let keybind_number = keybind_idx + 1; // 1-indexed for display let position = agentic.scene_position; let status = session.status(); - let title = &session.title; + let title = &session.details.title; let is_selected = selected_ids.contains(&id); let queue_priority = focus_queue.get_session_priority(id); @@ -411,7 +411,7 @@ impl AgentScene { ); // Cwd label (monospace, weak+small) - let cwd_text = cwd.to_string_lossy(); + let cwd_text = super::path_utils::abbreviate_path(cwd); let cwd_pos = center + Vec2::new(0.0, agent_radius + 38.0); painter.text( cwd_pos, diff --git a/crates/notedeck_dave/src/ui/session_list.rs b/crates/notedeck_dave/src/ui/session_list.rs @@ -156,9 +156,9 @@ impl<'a> SessionListUi<'a> { let response = self.session_item_ui( ui, - &session.title, + &session.details.title, cwd, - &session.hostname, + &session.details.hostname, is_active, shortcut_hint, session.status(), @@ -298,9 +298,9 @@ impl<'a> SessionListUi<'a> { /// Draw cwd text (monospace, weak+small) with clipping. /// Shows "hostname:cwd" when hostname is non-empty. fn cwd_ui(ui: &mut egui::Ui, cwd_path: &Path, hostname: &str, pos: egui::Pos2, max_width: f32) { - let cwd_str = cwd_path.to_string_lossy(); + let cwd_str = super::path_utils::abbreviate_path(cwd_path); let display_text = if hostname.is_empty() { - cwd_str.to_string() + cwd_str } else { format!("{}:{}", hostname, cwd_str) }; diff --git a/crates/notedeck_dave/src/update.rs b/crates/notedeck_dave/src/update.rs @@ -868,7 +868,7 @@ pub fn create_session_with_cwd( let id = session_manager.new_session(cwd, ai_mode); if let Some(session) = session_manager.get_mut(id) { - session.hostname = hostname.to_string(); + session.details.hostname = hostname.to_string(); session.focus_requested = true; if show_scene { scene.select(id); @@ -897,7 +897,7 @@ pub fn create_resumed_session_with_cwd( let id = session_manager.new_resumed_session(cwd, resume_session_id, title, ai_mode); if let Some(session) = session_manager.get_mut(id) { - session.hostname = hostname.to_string(); + session.details.hostname = hostname.to_string(); session.focus_requested = true; if show_scene { scene.select(id);