commit 170b7c43df570b8ffd52de96b77abe4705b19146
parent abe852a6d5092566f91c68e0ef4cd9875420c272
Author: William Casarin <jb55@jb55.com>
Date: Fri, 20 Feb 2026 13:30:27 -0800
dave: group agent sessions by hostname in session list
Replace the flat "Agents" section with hostname-based groups so
sessions from different machines are visually separated. Hostname
groups are cached in SessionManager and rebuilt only when sessions
change, avoiding per-frame allocations. CWD display no longer
includes the hostname prefix since it's now a section header.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
4 files changed, 89 insertions(+), 43 deletions(-)
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -372,6 +372,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
if let Some(session) = manager.get_mut(sid) {
session.details.hostname = hostname.clone();
}
+ manager.rebuild_host_groups();
(manager, DaveOverlay::None)
}
AiMode::Agentic => (SessionManager::new(), DaveOverlay::DirectoryPicker),
@@ -1133,6 +1134,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
}
}
+ self.session_manager.rebuild_host_groups();
// Close directory picker if open
if self.active_overlay == DaveOverlay::DirectoryPicker {
@@ -1497,6 +1499,8 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
}
+ self.session_manager.rebuild_host_groups();
+
// Skip the directory picker since we restored sessions
self.active_overlay = DaveOverlay::None;
}
@@ -1684,6 +1688,8 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
}
+ self.session_manager.rebuild_host_groups();
+
// If we were showing the directory picker, switch to showing sessions
if matches!(self.active_overlay, DaveOverlay::DirectoryPicker) {
self.active_overlay = DaveOverlay::None;
diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs
@@ -548,6 +548,12 @@ pub struct SessionManager {
next_id: SessionId,
/// Pending external editor job (only one at a time)
pub pending_editor: Option<EditorJob>,
+ /// Cached agent grouping by hostname. Each entry is (hostname, session IDs
+ /// in recency order). Rebuilt via `rebuild_host_groups()` when sessions or
+ /// hostnames change.
+ host_groups: Vec<(String, Vec<SessionId>)>,
+ /// Cached chat session IDs in recency order. Rebuilt alongside host_groups.
+ chat_ids: Vec<SessionId>,
}
impl Default for SessionManager {
@@ -564,6 +570,8 @@ impl SessionManager {
active: None,
next_id: 1,
pending_editor: None,
+ host_groups: Vec::new(),
+ chat_ids: Vec::new(),
}
}
@@ -576,6 +584,7 @@ impl SessionManager {
self.sessions.insert(id, session);
self.order.insert(0, id); // Most recent first
self.active = Some(id);
+ self.rebuild_host_groups();
id
}
@@ -595,6 +604,7 @@ impl SessionManager {
self.sessions.insert(id, session);
self.order.insert(0, id); // Most recent first
self.active = Some(id);
+ self.rebuild_host_groups();
id
}
@@ -636,6 +646,7 @@ impl SessionManager {
if self.active == Some(id) {
self.active = self.order.first().copied();
}
+ self.rebuild_host_groups();
true
} else {
false
@@ -709,6 +720,49 @@ impl SessionManager {
pub fn session_ids(&self) -> Vec<SessionId> {
self.order.clone()
}
+
+ /// Get cached agent session groups by hostname.
+ /// Each entry is (hostname, session IDs in recency order).
+ pub fn host_groups(&self) -> &[(String, Vec<SessionId>)] {
+ &self.host_groups
+ }
+
+ /// Get cached chat session IDs in recency order.
+ pub fn chat_ids(&self) -> &[SessionId] {
+ &self.chat_ids
+ }
+
+ /// Get a session's index in the recency-ordered list (for keyboard shortcuts).
+ pub fn session_index(&self, id: SessionId) -> Option<usize> {
+ self.order.iter().position(|&oid| oid == id)
+ }
+
+ /// Rebuild the cached hostname groups from current sessions and order.
+ /// Call after adding/removing sessions or changing a session's hostname.
+ pub fn rebuild_host_groups(&mut self) {
+ self.host_groups.clear();
+ self.chat_ids.clear();
+
+ for &id in &self.order {
+ if let Some(session) = self.sessions.get(&id) {
+ if session.ai_mode != AiMode::Agentic {
+ if session.ai_mode == AiMode::Chat {
+ self.chat_ids.push(id);
+ }
+ continue;
+ }
+ let hostname = &session.details.hostname;
+ if let Some(group) = self.host_groups.iter_mut().find(|(h, _)| h == hostname) {
+ group.1.push(id);
+ } else {
+ self.host_groups.push((hostname.clone(), vec![id]));
+ }
+ }
+ }
+
+ // Sort groups by hostname for stable ordering
+ self.host_groups.sort_by(|a, b| a.0.cmp(&b.0));
+ }
}
impl ChatSession {
diff --git a/crates/notedeck_dave/src/ui/session_list.rs b/crates/notedeck_dave/src/ui/session_list.rs
@@ -89,47 +89,46 @@ impl<'a> SessionListUi<'a> {
fn sessions_list_ui(&self, ui: &mut egui::Ui) -> Option<SessionListAction> {
let mut action = None;
let active_id = self.session_manager.active_id();
- let sessions = self.session_manager.sessions_ordered();
-
- // Split into agents and chats
- let agents: Vec<_> = sessions
- .iter()
- .enumerate()
- .filter(|(_, s)| s.ai_mode == AiMode::Agentic)
- .collect();
- let chats: Vec<_> = sessions
- .iter()
- .enumerate()
- .filter(|(_, s)| s.ai_mode == AiMode::Chat)
- .collect();
-
- // Agents section
- if !agents.is_empty() {
+
+ // Agents grouped by hostname (pre-computed, no per-frame allocation)
+ for (hostname, ids) in self.session_manager.host_groups() {
+ let label = if hostname.is_empty() {
+ "Local"
+ } else {
+ hostname
+ };
ui.label(
- egui::RichText::new("Agents")
+ egui::RichText::new(label)
.size(12.0)
.color(ui.visuals().weak_text_color()),
);
ui.add_space(4.0);
- for (index, session) in &agents {
- if let Some(a) = self.render_session_item(ui, session, *index, active_id) {
- action = Some(a);
+ for &id in ids {
+ if let Some(session) = self.session_manager.get(id) {
+ let index = self.session_manager.session_index(id).unwrap_or(0);
+ if let Some(a) = self.render_session_item(ui, session, index, active_id) {
+ action = Some(a);
+ }
}
}
ui.add_space(8.0);
}
- // Chats section
- if !chats.is_empty() {
+ // Chats section (pre-computed IDs)
+ let chat_ids = self.session_manager.chat_ids();
+ if !chat_ids.is_empty() {
ui.label(
egui::RichText::new("Chats")
.size(12.0)
.color(ui.visuals().weak_text_color()),
);
ui.add_space(4.0);
- for (index, session) in &chats {
- if let Some(a) = self.render_session_item(ui, session, *index, active_id) {
- action = Some(a);
+ for &id in chat_ids {
+ if let Some(session) = self.session_manager.get(id) {
+ let index = self.session_manager.session_index(id).unwrap_or(0);
+ if let Some(a) = self.render_session_item(ui, session, index, active_id) {
+ action = Some(a);
+ }
}
}
}
@@ -158,7 +157,6 @@ impl<'a> SessionListUi<'a> {
ui,
&session.details.title,
cwd,
- &session.details.hostname,
&session.details.home_dir,
is_active,
shortcut_hint,
@@ -186,7 +184,6 @@ impl<'a> SessionListUi<'a> {
ui: &mut egui::Ui,
title: &str,
cwd: &Path,
- hostname: &str,
home_dir: &str,
is_active: bool,
shortcut_hint: Option<usize>,
@@ -290,7 +287,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, home_dir, cwd_pos, max_text_width);
+ cwd_ui(ui, cwd, home_dir, cwd_pos, max_text_width);
}
response
@@ -298,25 +295,12 @@ 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,
- home_dir: &str,
- pos: egui::Pos2,
- max_width: f32,
-) {
- let cwd_str = if home_dir.is_empty() {
+fn cwd_ui(ui: &mut egui::Ui, cwd_path: &Path, home_dir: &str, pos: egui::Pos2, max_width: f32) {
+ let display_text = 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 {
- format!("{}:{}", hostname, cwd_str)
- };
let cwd_font = egui::FontId::monospace(10.0);
let cwd_color = ui.visuals().weak_text_color();
diff --git a/crates/notedeck_dave/src/update.rs b/crates/notedeck_dave/src/update.rs
@@ -908,6 +908,7 @@ pub fn create_session_with_cwd(
}
}
}
+ session_manager.rebuild_host_groups();
id
}
@@ -937,6 +938,7 @@ pub fn create_resumed_session_with_cwd(
}
}
}
+ session_manager.rebuild_host_groups();
id
}