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:
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);