commit 9dbf828b2ca9b0a7b642e64818c3735e8331588e
parent 23c27ffb988062c1e6b9e01db9ea55a96f3b89af
Author: William Casarin <jb55@jb55.com>
Date: Tue, 17 Feb 2026 15:46:01 -0800
add source hostname to session state events and session list UI
Sessions now include a hostname tag in kind-31988 events, parsed on
restore, and displayed as hostname:cwd in the session list sidebar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
8 files changed, 74 insertions(+), 10 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -2417,6 +2417,16 @@ dependencies = [
]
[[package]]
+name = "gethostname"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
+dependencies = [
+ "rustix 1.0.7",
+ "windows-link 0.2.1",
+]
+
+[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4163,6 +4173,7 @@ dependencies = [
"egui_extras",
"enostr",
"futures",
+ "gethostname 1.1.0",
"hex",
"md-stream",
"nostrdb",
@@ -8397,7 +8408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
dependencies = [
"as-raw-xcb-connection",
- "gethostname",
+ "gethostname 0.4.3",
"libc",
"libloading",
"once_cell",
diff --git a/crates/notedeck_dave/Cargo.toml b/crates/notedeck_dave/Cargo.toml
@@ -30,6 +30,7 @@ egui_extras = { workspace = true }
md-stream = { workspace = true }
similar = "2"
dirs = "5"
+gethostname = "1"
[target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies]
rfd = { workspace = true }
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -141,6 +141,8 @@ pub struct Dave {
/// Sessions pending deletion state event publication.
/// Populated in delete_session(), drained in the update loop where AppContext is available.
pending_deletions: Vec<DeletedSessionInfo>,
+ /// Local machine hostname, included in session state events.
+ hostname: String,
}
/// A permission response queued for relay publishing.
@@ -306,13 +308,20 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
// Create IPC listener for external spawn-agent commands
let ipc_listener = ipc::create_listener(ctx);
+ let hostname = gethostname::gethostname()
+ .to_string_lossy()
+ .into_owned();
+
// In Chat mode, create a default session immediately and skip directory picker
// In Agentic mode, show directory picker on startup
let (session_manager, active_overlay) = match ai_mode {
AiMode::Chat => {
let mut manager = SessionManager::new();
// Create a default session with current directory
- manager.new_session(std::env::current_dir().unwrap_or_default(), ai_mode);
+ 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();
+ }
(manager, DaveOverlay::None)
}
AiMode::Agentic => (SessionManager::new(), DaveOverlay::DirectoryPicker),
@@ -346,6 +355,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
session_state_sub: None,
pending_perm_responses: Vec::new(),
pending_deletions: Vec::new(),
+ hostname,
}
}
@@ -894,6 +904,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
self.show_scene,
self.ai_mode,
cwd,
+ &self.hostname,
);
}
@@ -913,6 +924,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
cwd,
resume_session_id,
title,
+ &self.hostname,
)
}
@@ -924,6 +936,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
&mut self.scene,
self.show_scene,
self.ai_mode,
+ &self.hostname,
);
}
@@ -943,6 +956,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.focus_requested = true;
if self.show_scene {
self.scene.select(id);
@@ -1107,6 +1121,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
&session.title,
&cwd,
status,
+ &self.hostname,
&sk,
) {
Ok(evt) => {
@@ -1140,6 +1155,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
&info.title,
&info.cwd,
"deleted",
+ &self.hostname,
&sk,
) {
Ok(evt) => {
@@ -1265,6 +1281,14 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
let is_remote = session.is_remote();
+ // Local sessions use the current machine's hostname;
+ // remote sessions use what was stored in the event.
+ session.hostname = if is_remote {
+ state.hostname.clone()
+ } else {
+ self.hostname.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);
@@ -1395,11 +1419,15 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
.to_string();
let cwd_str = session_events::get_tag_value(¬e, "cwd").unwrap_or("");
let cwd = std::path::PathBuf::from(cwd_str);
+ let hostname = session_events::get_tag_value(¬e, "hostname")
+ .unwrap_or("")
+ .to_string();
tracing::info!(
- "discovered new session from relay: '{}' ({})",
+ "discovered new session from relay: '{}' ({}) on {}",
title,
- claude_sid
+ claude_sid,
+ hostname,
);
existing_ids.insert(claude_sid.to_string());
@@ -1415,6 +1443,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;
if !loaded.messages.is_empty() {
tracing::info!(
"loaded {} messages for discovered session",
diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs
@@ -184,6 +184,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,
}
impl Drop for ChatSession {
@@ -214,6 +216,7 @@ impl ChatSession {
ai_mode,
agentic,
source: SessionSource::Local,
+ hostname: String::new(),
}
}
diff --git a/crates/notedeck_dave/src/session_events.rs b/crates/notedeck_dave/src/session_events.rs
@@ -621,6 +621,7 @@ pub fn build_session_state_event(
title: &str,
cwd: &str,
status: &str,
+ hostname: &str,
secret_key: &[u8; 32],
) -> Result<BuiltEvent, EventBuildError> {
let mut builder = init_note_builder(AI_SESSION_STATE_KIND, "", Some(now_secs()));
@@ -632,6 +633,7 @@ pub fn build_session_state_event(
builder = builder.start_tag().tag_str("title").tag_str(title);
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);
// Discoverability
builder = builder.start_tag().tag_str("t").tag_str("ai-session-state");
@@ -1081,6 +1083,7 @@ mod tests {
"Fix the login bug",
"/tmp/project",
"working",
+ "my-laptop",
&sk,
)
.unwrap();
@@ -1099,6 +1102,7 @@ mod tests {
assert!(json.contains("Fix the login bug"));
assert!(json.contains("working"));
assert!(json.contains("/tmp/project"));
+ assert!(json.contains(r#""hostname","my-laptop"#));
}
#[test]
diff --git a/crates/notedeck_dave/src/session_loader.rs b/crates/notedeck_dave/src/session_loader.rs
@@ -234,6 +234,7 @@ pub struct SessionState {
pub title: String,
pub cwd: String,
pub status: String,
+ pub hostname: String,
}
/// Load all session states from kind-31988 events in ndb.
@@ -279,6 +280,7 @@ pub fn load_session_states(ndb: &Ndb, txn: &Transaction) -> Vec<SessionState> {
.to_string(),
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(),
});
}
diff --git a/crates/notedeck_dave/src/ui/session_list.rs b/crates/notedeck_dave/src/ui/session_list.rs
@@ -158,6 +158,7 @@ impl<'a> SessionListUi<'a> {
ui,
&session.title,
cwd,
+ &session.hostname,
is_active,
shortcut_hint,
session.status(),
@@ -184,6 +185,7 @@ impl<'a> SessionListUi<'a> {
ui: &mut egui::Ui,
title: &str,
cwd: &Path,
+ hostname: &str,
is_active: bool,
shortcut_hint: Option<usize>,
status: AgentStatus,
@@ -286,22 +288,28 @@ 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, cwd_pos, max_text_width);
+ cwd_ui(ui, cwd, hostname, cwd_pos, max_text_width);
}
response
}
}
-/// Draw cwd text (monospace, weak+small) with clipping
-fn cwd_ui(ui: &mut egui::Ui, cwd_path: &Path, pos: egui::Pos2, max_width: f32) {
- let cwd_text = cwd_path.to_string_lossy();
+/// 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 display_text = if hostname.is_empty() {
+ cwd_str.to_string()
+ } else {
+ format!("{}:{}", hostname, cwd_str)
+ };
let cwd_font = egui::FontId::monospace(10.0);
let cwd_color = ui.visuals().weak_text_color();
let cwd_galley = ui
.painter()
- .layout_no_wrap(cwd_text.to_string(), cwd_font.clone(), cwd_color);
+ .layout_no_wrap(display_text.clone(), cwd_font.clone(), cwd_color);
if cwd_galley.size().x > max_width {
let clip_rect = egui::Rect::from_min_size(
@@ -317,7 +325,7 @@ fn cwd_ui(ui: &mut egui::Ui, cwd_path: &Path, pos: egui::Pos2, max_width: f32) {
ui.painter().text(
pos,
egui::Align2::LEFT_CENTER,
- &cwd_text,
+ &display_text,
cwd_font,
cwd_color,
);
diff --git a/crates/notedeck_dave/src/update.rs b/crates/notedeck_dave/src/update.rs
@@ -895,11 +895,13 @@ pub fn create_session_with_cwd(
show_scene: bool,
ai_mode: AiMode,
cwd: PathBuf,
+ hostname: &str,
) -> SessionId {
directory_picker.add_recent(cwd.clone());
let id = session_manager.new_session(cwd, ai_mode);
if let Some(session) = session_manager.get_mut(id) {
+ session.hostname = hostname.to_string();
session.focus_requested = true;
if show_scene {
scene.select(id);
@@ -922,11 +924,13 @@ pub fn create_resumed_session_with_cwd(
cwd: PathBuf,
resume_session_id: String,
title: String,
+ hostname: &str,
) -> SessionId {
directory_picker.add_recent(cwd.clone());
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.focus_requested = true;
if show_scene {
scene.select(id);
@@ -945,6 +949,7 @@ pub fn clone_active_agent(
scene: &mut AgentScene,
show_scene: bool,
ai_mode: AiMode,
+ hostname: &str,
) -> Option<SessionId> {
let cwd = session_manager
.get_active()
@@ -956,6 +961,7 @@ pub fn clone_active_agent(
show_scene,
ai_mode,
cwd,
+ hostname,
))
}