notedeck

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

commit 341d84966c98cc16215d0e6d2ff5245d2e3c080e
parent e6a8450cfd521e2e085b933498e197248d33a520
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 19 Feb 2026 11:03:32 -0800

dave: add home_dir to session state events for remote cwd abbreviation

Include the publisher's home directory in nostr session state events so
remote clients can properly abbreviate paths with ~ instead of showing
full paths like /home/jb55/dev/foo on mobile devices.

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

Diffstat:
Mcrates/notedeck_dave/src/lib.rs | 16++++++++++++++++
Acrates/notedeck_dave/src/path_utils.rs | 19+++++++++++++++++++
Mcrates/notedeck_dave/src/session.rs | 6++++++
Mcrates/notedeck_dave/src/session_events.rs | 2++
Mcrates/notedeck_dave/src/session_loader.rs | 2++
Mcrates/notedeck_dave/src/ui/dave.rs | 6+++++-
Mcrates/notedeck_dave/src/ui/directory_picker.rs | 2+-
Mcrates/notedeck_dave/src/ui/mod.rs | 1-
Dcrates/notedeck_dave/src/ui/path_utils.rs | 11-----------
Mcrates/notedeck_dave/src/ui/scene.rs | 8+++++++-
Mcrates/notedeck_dave/src/ui/session_list.rs | 19++++++++++++++++---
Mcrates/notedeck_dave/src/ui/session_picker.rs | 2+-
12 files changed, 75 insertions(+), 19 deletions(-)

diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -10,6 +10,7 @@ pub mod ipc; pub(crate) mod mesh; mod messages; mod path_normalize; +pub(crate) mod path_utils; mod quaternion; pub mod session; pub mod session_converter; @@ -156,6 +157,7 @@ struct DeletedSessionInfo { claude_session_id: String, title: String, cwd: String, + home_dir: String, } /// Subscription waiting for ndb to index 1988 conversation events. @@ -1261,6 +1263,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr &cwd, status, &self.hostname, + &session.details.home_dir, &sk, ), &format!("publishing session state: {} -> {}", claude_sid, status), @@ -1292,6 +1295,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr &info.cwd, "deleted", &self.hostname, + &info.home_dir, &sk, ), &format!( @@ -1418,6 +1422,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr self.hostname.clone() }; + // Use home_dir from the event for remote abbreviation + if !state.home_dir.is_empty() { + session.details.home_dir = state.home_dir.clone(); + } + if let Some(agentic) = &mut session.agentic { if let (Some(root), Some(last)) = (loaded.root_note_id, loaded.last_note_id) { agentic.live_threading.seed(root, last, loaded.event_count); @@ -1564,6 +1573,9 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr let hostname = session_events::get_tag_value(&note, "hostname") .unwrap_or("") .to_string(); + let home_dir = session_events::get_tag_value(&note, "home_dir") + .unwrap_or("") + .to_string(); tracing::info!( "discovered new session from relay: '{}' ({}) on {}", @@ -1586,6 +1598,9 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr if let Some(session) = self.session_manager.get_mut(dave_sid) { session.details.hostname = hostname; + if !home_dir.is_empty() { + session.details.home_dir = home_dir; + } if !loaded.messages.is_empty() { tracing::info!( "loaded {} messages for discovered session", @@ -1830,6 +1845,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr claude_session_id: claude_sid.to_string(), title: session.details.title.clone(), cwd: agentic.cwd.to_string_lossy().to_string(), + home_dir: session.details.home_dir.clone(), }); } } diff --git a/crates/notedeck_dave/src/path_utils.rs b/crates/notedeck_dave/src/path_utils.rs @@ -0,0 +1,19 @@ +use std::path::Path; + +/// Abbreviate a path by replacing the given home directory prefix with ~ +pub fn abbreviate_with_home(path: &Path, home_dir: &str) -> String { + let home = Path::new(home_dir); + if let Ok(relative) = path.strip_prefix(home) { + return format!("~/{}", relative.display()); + } + path.display().to_string() +} + +/// Abbreviate a path using the local machine's home directory +pub fn abbreviate_path(path: &Path) -> String { + if let Some(home) = dirs::home_dir() { + abbreviate_with_home(path, &home.to_string_lossy()) + } else { + path.display().to_string() + } +} diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs @@ -32,6 +32,9 @@ pub struct SessionDetails { pub title: String, pub hostname: String, pub cwd: Option<PathBuf>, + /// Home directory of the machine where this session originated. + /// Used to abbreviate cwd paths for remote sessions. + pub home_dir: String, } /// State for permission response with message @@ -328,6 +331,9 @@ impl ChatSession { title: "New Chat".to_string(), hostname: String::new(), cwd: details_cwd, + home_dir: dirs::home_dir() + .map(|h| h.to_string_lossy().to_string()) + .unwrap_or_default(), }, } } diff --git a/crates/notedeck_dave/src/session_events.rs b/crates/notedeck_dave/src/session_events.rs @@ -655,6 +655,7 @@ pub fn build_session_state_event( cwd: &str, status: &str, hostname: &str, + home_dir: &str, secret_key: &[u8; 32], ) -> Result<BuiltEvent, EventBuildError> { let mut builder = init_note_builder(AI_SESSION_STATE_KIND, "", Some(now_secs())); @@ -667,6 +668,7 @@ pub fn build_session_state_event( builder = builder.start_tag().tag_str("cwd").tag_str(cwd); builder = builder.start_tag().tag_str("status").tag_str(status); builder = builder.start_tag().tag_str("hostname").tag_str(hostname); + builder = builder.start_tag().tag_str("home_dir").tag_str(home_dir); // Discoverability builder = builder.start_tag().tag_str("t").tag_str("ai-session-state"); diff --git a/crates/notedeck_dave/src/session_loader.rs b/crates/notedeck_dave/src/session_loader.rs @@ -230,6 +230,7 @@ pub struct SessionState { pub cwd: String, pub status: String, pub hostname: String, + pub home_dir: String, pub created_at: u64, } @@ -277,6 +278,7 @@ pub fn load_session_states(ndb: &Ndb, txn: &Transaction) -> Vec<SessionState> { cwd: get_tag_value(&note, "cwd").unwrap_or("").to_string(), status: get_tag_value(&note, "status").unwrap_or("idle").to_string(), hostname: get_tag_value(&note, "hostname").unwrap_or("").to_string(), + home_dir: get_tag_value(&note, "home_dir").unwrap_or("").to_string(), created_at: note.created_at(), }); } diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -1341,7 +1341,11 @@ fn session_header_ui(ui: &mut egui::Ui, details: &SessionDetails) { .wrap_mode(egui::TextWrapMode::Truncate), ); if let Some(cwd) = &details.cwd { - let cwd_display = super::path_utils::abbreviate_path(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 { diff --git a/crates/notedeck_dave/src/ui/directory_picker.rs b/crates/notedeck_dave/src/ui/directory_picker.rs @@ -1,5 +1,5 @@ +use crate::path_utils::abbreviate_path; use crate::ui::keybind_hint::paint_keybind_hint; -use crate::ui::path_utils::abbreviate_path; use egui::{RichText, Vec2}; use std::path::PathBuf; diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs @@ -7,7 +7,6 @@ mod git_status_ui; pub mod keybind_hint; pub mod keybindings; pub mod markdown_ui; -pub mod path_utils; mod pill; mod query_ui; pub mod scene; diff --git a/crates/notedeck_dave/src/ui/path_utils.rs b/crates/notedeck_dave/src/ui/path_utils.rs @@ -1,11 +0,0 @@ -use std::path::Path; - -/// Abbreviate a path for display (e.g., replace home dir with ~) -pub fn abbreviate_path(path: &Path) -> String { - if let Some(home) = dirs::home_dir() { - if let Ok(relative) = path.strip_prefix(&home) { - return format!("~/{}", relative.display()); - } - } - path.display().to_string() -} diff --git a/crates/notedeck_dave/src/ui/scene.rs b/crates/notedeck_dave/src/ui/scene.rs @@ -177,6 +177,7 @@ impl AgentScene { status, title, &agentic.cwd, + &session.details.home_dir, is_selected, ctrl_held, queue_priority, @@ -330,6 +331,7 @@ impl AgentScene { status: AgentStatus, title: &str, cwd: &Path, + home_dir: &str, is_selected: bool, show_keybinding: bool, queue_priority: Option<FocusPriority>, @@ -411,7 +413,11 @@ impl AgentScene { ); // Cwd label (monospace, weak+small) - let cwd_text = super::path_utils::abbreviate_path(cwd); + let cwd_text = if home_dir.is_empty() { + crate::path_utils::abbreviate_path(cwd) + } else { + crate::path_utils::abbreviate_with_home(cwd, home_dir) + }; 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 @@ -159,6 +159,7 @@ impl<'a> SessionListUi<'a> { &session.details.title, cwd, &session.details.hostname, + &session.details.home_dir, is_active, shortcut_hint, session.status(), @@ -186,6 +187,7 @@ impl<'a> SessionListUi<'a> { title: &str, cwd: &Path, hostname: &str, + home_dir: &str, is_active: bool, shortcut_hint: Option<usize>, status: AgentStatus, @@ -288,7 +290,7 @@ impl<'a> SessionListUi<'a> { // Draw cwd below title - only in Agentic mode if show_cwd { let cwd_pos = rect.left_center() + egui::vec2(text_start_x, 7.0); - cwd_ui(ui, cwd, hostname, cwd_pos, max_text_width); + cwd_ui(ui, cwd, hostname, home_dir, cwd_pos, max_text_width); } response @@ -297,8 +299,19 @@ 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 = super::path_utils::abbreviate_path(cwd_path); +fn cwd_ui( + ui: &mut egui::Ui, + cwd_path: &Path, + hostname: &str, + home_dir: &str, + pos: egui::Pos2, + max_width: f32, +) { + let cwd_str = if home_dir.is_empty() { + crate::path_utils::abbreviate_path(cwd_path) + } else { + crate::path_utils::abbreviate_with_home(cwd_path, home_dir) + }; let display_text = if hostname.is_empty() { cwd_str } else { diff --git a/crates/notedeck_dave/src/ui/session_picker.rs b/crates/notedeck_dave/src/ui/session_picker.rs @@ -1,8 +1,8 @@ //! UI component for selecting resumable Claude sessions. +use crate::path_utils::abbreviate_path; use crate::session_discovery::{discover_sessions, format_relative_time, ResumableSession}; use crate::ui::keybind_hint::paint_keybind_hint; -use crate::ui::path_utils::abbreviate_path; use egui::{RichText, Vec2}; use std::path::{Path, PathBuf};