Skip to content

Commit 170b7c4

Browse files
jb55claude
andcommitted
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>
1 parent abe852a commit 170b7c4

File tree

4 files changed

+89
-43
lines changed

4 files changed

+89
-43
lines changed

crates/notedeck_dave/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
372372
if let Some(session) = manager.get_mut(sid) {
373373
session.details.hostname = hostname.clone();
374374
}
375+
manager.rebuild_host_groups();
375376
(manager, DaveOverlay::None)
376377
}
377378
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
11331134
}
11341135
}
11351136
}
1137+
self.session_manager.rebuild_host_groups();
11361138

11371139
// Close directory picker if open
11381140
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
14971499
}
14981500
}
14991501

1502+
self.session_manager.rebuild_host_groups();
1503+
15001504
// Skip the directory picker since we restored sessions
15011505
self.active_overlay = DaveOverlay::None;
15021506
}
@@ -1684,6 +1688,8 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
16841688
}
16851689
}
16861690

1691+
self.session_manager.rebuild_host_groups();
1692+
16871693
// If we were showing the directory picker, switch to showing sessions
16881694
if matches!(self.active_overlay, DaveOverlay::DirectoryPicker) {
16891695
self.active_overlay = DaveOverlay::None;

crates/notedeck_dave/src/session.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,12 @@ pub struct SessionManager {
548548
next_id: SessionId,
549549
/// Pending external editor job (only one at a time)
550550
pub pending_editor: Option<EditorJob>,
551+
/// Cached agent grouping by hostname. Each entry is (hostname, session IDs
552+
/// in recency order). Rebuilt via `rebuild_host_groups()` when sessions or
553+
/// hostnames change.
554+
host_groups: Vec<(String, Vec<SessionId>)>,
555+
/// Cached chat session IDs in recency order. Rebuilt alongside host_groups.
556+
chat_ids: Vec<SessionId>,
551557
}
552558

553559
impl Default for SessionManager {
@@ -564,6 +570,8 @@ impl SessionManager {
564570
active: None,
565571
next_id: 1,
566572
pending_editor: None,
573+
host_groups: Vec::new(),
574+
chat_ids: Vec::new(),
567575
}
568576
}
569577

@@ -576,6 +584,7 @@ impl SessionManager {
576584
self.sessions.insert(id, session);
577585
self.order.insert(0, id); // Most recent first
578586
self.active = Some(id);
587+
self.rebuild_host_groups();
579588

580589
id
581590
}
@@ -595,6 +604,7 @@ impl SessionManager {
595604
self.sessions.insert(id, session);
596605
self.order.insert(0, id); // Most recent first
597606
self.active = Some(id);
607+
self.rebuild_host_groups();
598608

599609
id
600610
}
@@ -636,6 +646,7 @@ impl SessionManager {
636646
if self.active == Some(id) {
637647
self.active = self.order.first().copied();
638648
}
649+
self.rebuild_host_groups();
639650
true
640651
} else {
641652
false
@@ -709,6 +720,49 @@ impl SessionManager {
709720
pub fn session_ids(&self) -> Vec<SessionId> {
710721
self.order.clone()
711722
}
723+
724+
/// Get cached agent session groups by hostname.
725+
/// Each entry is (hostname, session IDs in recency order).
726+
pub fn host_groups(&self) -> &[(String, Vec<SessionId>)] {
727+
&self.host_groups
728+
}
729+
730+
/// Get cached chat session IDs in recency order.
731+
pub fn chat_ids(&self) -> &[SessionId] {
732+
&self.chat_ids
733+
}
734+
735+
/// Get a session's index in the recency-ordered list (for keyboard shortcuts).
736+
pub fn session_index(&self, id: SessionId) -> Option<usize> {
737+
self.order.iter().position(|&oid| oid == id)
738+
}
739+
740+
/// Rebuild the cached hostname groups from current sessions and order.
741+
/// Call after adding/removing sessions or changing a session's hostname.
742+
pub fn rebuild_host_groups(&mut self) {
743+
self.host_groups.clear();
744+
self.chat_ids.clear();
745+
746+
for &id in &self.order {
747+
if let Some(session) = self.sessions.get(&id) {
748+
if session.ai_mode != AiMode::Agentic {
749+
if session.ai_mode == AiMode::Chat {
750+
self.chat_ids.push(id);
751+
}
752+
continue;
753+
}
754+
let hostname = &session.details.hostname;
755+
if let Some(group) = self.host_groups.iter_mut().find(|(h, _)| h == hostname) {
756+
group.1.push(id);
757+
} else {
758+
self.host_groups.push((hostname.clone(), vec![id]));
759+
}
760+
}
761+
}
762+
763+
// Sort groups by hostname for stable ordering
764+
self.host_groups.sort_by(|a, b| a.0.cmp(&b.0));
765+
}
712766
}
713767

714768
impl ChatSession {

crates/notedeck_dave/src/ui/session_list.rs

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -89,47 +89,46 @@ impl<'a> SessionListUi<'a> {
8989
fn sessions_list_ui(&self, ui: &mut egui::Ui) -> Option<SessionListAction> {
9090
let mut action = None;
9191
let active_id = self.session_manager.active_id();
92-
let sessions = self.session_manager.sessions_ordered();
93-
94-
// Split into agents and chats
95-
let agents: Vec<_> = sessions
96-
.iter()
97-
.enumerate()
98-
.filter(|(_, s)| s.ai_mode == AiMode::Agentic)
99-
.collect();
100-
let chats: Vec<_> = sessions
101-
.iter()
102-
.enumerate()
103-
.filter(|(_, s)| s.ai_mode == AiMode::Chat)
104-
.collect();
105-
106-
// Agents section
107-
if !agents.is_empty() {
92+
93+
// Agents grouped by hostname (pre-computed, no per-frame allocation)
94+
for (hostname, ids) in self.session_manager.host_groups() {
95+
let label = if hostname.is_empty() {
96+
"Local"
97+
} else {
98+
hostname
99+
};
108100
ui.label(
109-
egui::RichText::new("Agents")
101+
egui::RichText::new(label)
110102
.size(12.0)
111103
.color(ui.visuals().weak_text_color()),
112104
);
113105
ui.add_space(4.0);
114-
for (index, session) in &agents {
115-
if let Some(a) = self.render_session_item(ui, session, *index, active_id) {
116-
action = Some(a);
106+
for &id in ids {
107+
if let Some(session) = self.session_manager.get(id) {
108+
let index = self.session_manager.session_index(id).unwrap_or(0);
109+
if let Some(a) = self.render_session_item(ui, session, index, active_id) {
110+
action = Some(a);
111+
}
117112
}
118113
}
119114
ui.add_space(8.0);
120115
}
121116

122-
// Chats section
123-
if !chats.is_empty() {
117+
// Chats section (pre-computed IDs)
118+
let chat_ids = self.session_manager.chat_ids();
119+
if !chat_ids.is_empty() {
124120
ui.label(
125121
egui::RichText::new("Chats")
126122
.size(12.0)
127123
.color(ui.visuals().weak_text_color()),
128124
);
129125
ui.add_space(4.0);
130-
for (index, session) in &chats {
131-
if let Some(a) = self.render_session_item(ui, session, *index, active_id) {
132-
action = Some(a);
126+
for &id in chat_ids {
127+
if let Some(session) = self.session_manager.get(id) {
128+
let index = self.session_manager.session_index(id).unwrap_or(0);
129+
if let Some(a) = self.render_session_item(ui, session, index, active_id) {
130+
action = Some(a);
131+
}
133132
}
134133
}
135134
}
@@ -158,7 +157,6 @@ impl<'a> SessionListUi<'a> {
158157
ui,
159158
&session.details.title,
160159
cwd,
161-
&session.details.hostname,
162160
&session.details.home_dir,
163161
is_active,
164162
shortcut_hint,
@@ -186,7 +184,6 @@ impl<'a> SessionListUi<'a> {
186184
ui: &mut egui::Ui,
187185
title: &str,
188186
cwd: &Path,
189-
hostname: &str,
190187
home_dir: &str,
191188
is_active: bool,
192189
shortcut_hint: Option<usize>,
@@ -290,33 +287,20 @@ impl<'a> SessionListUi<'a> {
290287
// Draw cwd below title - only in Agentic mode
291288
if show_cwd {
292289
let cwd_pos = rect.left_center() + egui::vec2(text_start_x, 7.0);
293-
cwd_ui(ui, cwd, hostname, home_dir, cwd_pos, max_text_width);
290+
cwd_ui(ui, cwd, home_dir, cwd_pos, max_text_width);
294291
}
295292

296293
response
297294
}
298295
}
299296

300297
/// Draw cwd text (monospace, weak+small) with clipping.
301-
/// Shows "hostname:cwd" when hostname is non-empty.
302-
fn cwd_ui(
303-
ui: &mut egui::Ui,
304-
cwd_path: &Path,
305-
hostname: &str,
306-
home_dir: &str,
307-
pos: egui::Pos2,
308-
max_width: f32,
309-
) {
310-
let cwd_str = if home_dir.is_empty() {
298+
fn cwd_ui(ui: &mut egui::Ui, cwd_path: &Path, home_dir: &str, pos: egui::Pos2, max_width: f32) {
299+
let display_text = if home_dir.is_empty() {
311300
crate::path_utils::abbreviate_path(cwd_path)
312301
} else {
313302
crate::path_utils::abbreviate_with_home(cwd_path, home_dir)
314303
};
315-
let display_text = if hostname.is_empty() {
316-
cwd_str
317-
} else {
318-
format!("{}:{}", hostname, cwd_str)
319-
};
320304
let cwd_font = egui::FontId::monospace(10.0);
321305
let cwd_color = ui.visuals().weak_text_color();
322306

crates/notedeck_dave/src/update.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,7 @@ pub fn create_session_with_cwd(
908908
}
909909
}
910910
}
911+
session_manager.rebuild_host_groups();
911912
id
912913
}
913914

@@ -937,6 +938,7 @@ pub fn create_resumed_session_with_cwd(
937938
}
938939
}
939940
}
941+
session_manager.rebuild_host_groups();
940942
id
941943
}
942944

0 commit comments

Comments
 (0)