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:
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(¬e, "hostname")
.unwrap_or("")
.to_string();
+ let home_dir = session_events::get_tag_value(¬e, "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(¬e, "cwd").unwrap_or("").to_string(),
status: get_tag_value(¬e, "status").unwrap_or("idle").to_string(),
hostname: get_tag_value(¬e, "hostname").unwrap_or("").to_string(),
+ home_dir: get_tag_value(¬e, "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};