commit eacaa41b34c2eb19b2a617b53882c2860c1b69a9
parent 3ec5b203e903f6be11a06c26cb0677528750bc15
Author: William Casarin <jb55@jb55.com>
Date: Mon, 26 Jan 2026 12:16:13 -0800
dave: fix RTS scene bugs and improve multi-agent status updates
- Process events for all sessions, not just active one, so agent status
updates in real-time without needing to switch to each agent
- Remove grid from scene view (was rendering incorrectly)
- Add compact mode to DaveUi for better sidebar layout with reduced margins
- Guard keyboard shortcuts with wants_keyboard_input() to prevent
N/backspace from spawning/deleting agents while typing in chat
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
3 files changed, 141 insertions(+), 158 deletions(-)
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -155,111 +155,139 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
self.settings = settings;
}
- /// Process incoming tokens from the ai backend
+ /// Process incoming tokens from the ai backend for ALL sessions
fn process_events(&mut self, app_ctx: &AppContext) -> bool {
// Should we continue sending requests? Set this to true if
- // we have tool responses to send back to the ai
+ // we have tool responses to send back to the ai (only for active session)
let mut should_send = false;
+ let active_id = self.session_manager.active_id();
+
+ // Get all session IDs to process
+ let session_ids = self.session_manager.session_ids();
+
+ for session_id in session_ids {
+ // Take the receiver out to avoid borrow conflicts
+ let recvr = {
+ let Some(session) = self.session_manager.get_mut(session_id) else {
+ continue;
+ };
+ session.incoming_tokens.take()
+ };
- // Take the receiver out to avoid borrow conflicts
- let recvr = {
- let Some(session) = self.session_manager.get_active_mut() else {
- return should_send;
+ let Some(recvr) = recvr else {
+ continue;
};
- session.incoming_tokens.take()
- };
- let Some(recvr) = recvr else {
- return should_send;
- };
+ while let Ok(res) = recvr.try_recv() {
+ // Nudge avatar only for active session
+ if active_id == Some(session_id) {
+ if let Some(avatar) = &mut self.avatar {
+ avatar.random_nudge();
+ }
+ }
- while let Ok(res) = recvr.try_recv() {
- if let Some(avatar) = &mut self.avatar {
- avatar.random_nudge();
- }
+ let Some(session) = self.session_manager.get_mut(session_id) else {
+ break;
+ };
+
+ match res {
+ DaveApiResponse::Failed(err) => session.chat.push(Message::Error(err)),
+
+ DaveApiResponse::Token(token) => match session.chat.last_mut() {
+ Some(Message::Assistant(msg)) => *msg = msg.clone() + &token,
+ Some(_) => session.chat.push(Message::Assistant(token)),
+ None => {}
+ },
+
+ DaveApiResponse::ToolCalls(toolcalls) => {
+ tracing::info!("got tool calls: {:?}", toolcalls);
+ session.chat.push(Message::ToolCalls(toolcalls.clone()));
+
+ let txn = Transaction::new(app_ctx.ndb).unwrap();
+ for call in &toolcalls {
+ // execute toolcall
+ match call.calls() {
+ ToolCalls::PresentNotes(present) => {
+ session.chat.push(Message::ToolResponse(ToolResponse::new(
+ call.id().to_owned(),
+ ToolResponses::PresentNotes(present.note_ids.len() as i32),
+ )));
+
+ // Only send for active session
+ if active_id == Some(session_id) {
+ should_send = true;
+ }
+ }
- let Some(session) = self.session_manager.get_active_mut() else {
- break;
- };
+ ToolCalls::Invalid(invalid) => {
+ if active_id == Some(session_id) {
+ should_send = true;
+ }
- match res {
- DaveApiResponse::Failed(err) => session.chat.push(Message::Error(err)),
-
- DaveApiResponse::Token(token) => match session.chat.last_mut() {
- Some(Message::Assistant(msg)) => *msg = msg.clone() + &token,
- Some(_) => session.chat.push(Message::Assistant(token)),
- None => {}
- },
-
- DaveApiResponse::ToolCalls(toolcalls) => {
- tracing::info!("got tool calls: {:?}", toolcalls);
- session.chat.push(Message::ToolCalls(toolcalls.clone()));
-
- let txn = Transaction::new(app_ctx.ndb).unwrap();
- for call in &toolcalls {
- // execute toolcall
- match call.calls() {
- ToolCalls::PresentNotes(present) => {
- session.chat.push(Message::ToolResponse(ToolResponse::new(
- call.id().to_owned(),
- ToolResponses::PresentNotes(present.note_ids.len() as i32),
- )));
-
- should_send = true;
- }
+ session.chat.push(Message::tool_error(
+ call.id().to_string(),
+ invalid.error.clone(),
+ ));
+ }
- ToolCalls::Invalid(invalid) => {
- should_send = true;
+ ToolCalls::Query(search_call) => {
+ if active_id == Some(session_id) {
+ should_send = true;
+ }
- session.chat.push(Message::tool_error(
- call.id().to_string(),
- invalid.error.clone(),
- ));
+ let resp = search_call.execute(&txn, app_ctx.ndb);
+ session.chat.push(Message::ToolResponse(ToolResponse::new(
+ call.id().to_owned(),
+ ToolResponses::Query(resp),
+ )))
+ }
}
+ }
+ }
- ToolCalls::Query(search_call) => {
- should_send = true;
+ DaveApiResponse::PermissionRequest(pending) => {
+ tracing::info!(
+ "Permission request for tool '{}': {:?}",
+ pending.request.tool_name,
+ pending.request.tool_input
+ );
+
+ // Store the response sender for later
+ session
+ .pending_permissions
+ .insert(pending.request.id, pending.response_tx);
+
+ // Add the request to chat for UI display
+ session
+ .chat
+ .push(Message::PermissionRequest(pending.request));
+ }
- let resp = search_call.execute(&txn, app_ctx.ndb);
- session.chat.push(Message::ToolResponse(ToolResponse::new(
- call.id().to_owned(),
- ToolResponses::Query(resp),
- )))
- }
- }
+ DaveApiResponse::ToolResult(result) => {
+ tracing::debug!("Tool result: {} - {}", result.tool_name, result.summary);
+ session.chat.push(Message::ToolResult(result));
}
}
+ }
- DaveApiResponse::PermissionRequest(pending) => {
- tracing::info!(
- "Permission request for tool '{}': {:?}",
- pending.request.tool_name,
- pending.request.tool_input
- );
-
- // Store the response sender for later
- session
- .pending_permissions
- .insert(pending.request.id, pending.response_tx);
-
- // Add the request to chat for UI display
- session
- .chat
- .push(Message::PermissionRequest(pending.request));
+ // Check if channel is disconnected (stream ended)
+ match recvr.try_recv() {
+ Err(std::sync::mpsc::TryRecvError::Disconnected) => {
+ // Stream ended, clear task state
+ if let Some(session) = self.session_manager.get_mut(session_id) {
+ session.task_handle = None;
+ // Don't restore incoming_tokens - leave it None
+ }
}
-
- DaveApiResponse::ToolResult(result) => {
- tracing::debug!("Tool result: {} - {}", result.tool_name, result.summary);
- session.chat.push(Message::ToolResult(result));
+ _ => {
+ // Channel still open, put receiver back
+ if let Some(session) = self.session_manager.get_mut(session_id) {
+ session.incoming_tokens = Some(recvr);
+ }
}
}
}
- // Put the receiver back
- if let Some(session) = self.session_manager.get_active_mut() {
- session.incoming_tokens = Some(recvr);
- }
-
should_send
}
@@ -364,6 +392,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
&session.chat,
&mut session.input,
)
+ .compact(true)
.ui(app_ctx, ui);
if response.action.is_some() {
diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs
@@ -18,6 +18,7 @@ pub struct DaveUi<'a> {
chat: &'a [Message],
trial: bool,
input: &'a mut String,
+ compact: bool,
}
/// The response the app generates. The response contains an optional
@@ -79,19 +80,29 @@ pub enum DaveAction {
impl<'a> DaveUi<'a> {
pub fn new(trial: bool, chat: &'a [Message], input: &'a mut String) -> Self {
- DaveUi { trial, chat, input }
+ DaveUi {
+ trial,
+ chat,
+ input,
+ compact: false,
+ }
+ }
+
+ pub fn compact(mut self, compact: bool) -> Self {
+ self.compact = compact;
+ self
}
- fn chat_margin(ctx: &egui::Context) -> i8 {
- if notedeck::ui::is_narrow(ctx) {
+ fn chat_margin(&self, ctx: &egui::Context) -> i8 {
+ if self.compact || notedeck::ui::is_narrow(ctx) {
20
} else {
100
}
}
- fn chat_frame(ctx: &egui::Context) -> egui::Frame {
- let margin = Self::chat_margin(ctx);
+ fn chat_frame(&self, ctx: &egui::Context) -> egui::Frame {
+ let margin = self.chat_margin(ctx);
egui::Frame::new().inner_margin(egui::Margin {
left: margin,
right: margin,
@@ -107,7 +118,7 @@ impl<'a> DaveUi<'a> {
egui::Frame::NONE
.show(ui, |ui| {
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
- let margin = Self::chat_margin(ui.ctx());
+ let margin = self.chat_margin(ui.ctx());
let r = egui::Frame::new()
.outer_margin(egui::Margin {
@@ -126,7 +137,7 @@ impl<'a> DaveUi<'a> {
.stick_to_bottom(true)
.auto_shrink([false; 2])
.show(ui, |ui| {
- Self::chat_frame(ui.ctx())
+ self.chat_frame(ui.ctx())
.show(ui, |ui| {
ui.vertical(|ui| self.render_chat(app_ctx, ui)).inner
})
diff --git a/crates/notedeck_dave/src/ui/scene.rs b/crates/notedeck_dave/src/ui/scene.rs
@@ -175,9 +175,6 @@ impl AgentScene {
egui::Scene::new()
.zoom_range(0.1..=4.0)
.show(ui, &mut scene_rect, |ui| {
- // Draw background grid
- Self::draw_grid(ui);
-
// Draw agents and collect interaction responses
for session in session_manager.iter() {
let id = session.id;
@@ -259,16 +256,19 @@ impl AgentScene {
}
}
- // Handle keyboard input
- if ui.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)) {
- if !self.selected.is_empty() {
- response = SceneResponse::new(SceneAction::DeleteSelected);
+ // Handle keyboard input (only when no text input has focus)
+ if !ui.ctx().wants_keyboard_input() {
+ if ui.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace))
+ {
+ if !self.selected.is_empty() {
+ response = SceneResponse::new(SceneAction::DeleteSelected);
+ }
}
- }
- // Handle 'n' key to spawn new agent
- if ui.input(|i| i.key_pressed(egui::Key::N)) {
- response = SceneResponse::new(SceneAction::SpawnAgent);
+ // Handle 'n' key to spawn new agent
+ if ui.input(|i| i.key_pressed(egui::Key::N)) {
+ response = SceneResponse::new(SceneAction::SpawnAgent);
+ }
}
// Handle box selection completion
@@ -313,63 +313,6 @@ impl AgentScene {
response
}
- /// Draw the background grid
- fn draw_grid(ui: &mut egui::Ui) {
- let painter = ui.painter();
- let rect = ui.max_rect();
- let grid_spacing = 50.0;
- let grid_color = ui
- .visuals()
- .widgets
- .noninteractive
- .bg_stroke
- .color
- .gamma_multiply(0.3);
-
- // Vertical lines
- let start_x = (rect.min.x / grid_spacing).floor() * grid_spacing;
- let mut x = start_x;
- while x < rect.max.x {
- painter.line_segment(
- [Pos2::new(x, rect.min.y), Pos2::new(x, rect.max.y)],
- egui::Stroke::new(1.0, grid_color),
- );
- x += grid_spacing;
- }
-
- // Horizontal lines
- let start_y = (rect.min.y / grid_spacing).floor() * grid_spacing;
- let mut y = start_y;
- while y < rect.max.y {
- painter.line_segment(
- [Pos2::new(rect.min.x, y), Pos2::new(rect.max.x, y)],
- egui::Stroke::new(1.0, grid_color),
- );
- y += grid_spacing;
- }
-
- // Draw origin axes slightly brighter
- let axis_color = ui
- .visuals()
- .widgets
- .noninteractive
- .bg_stroke
- .color
- .gamma_multiply(0.6);
- if rect.min.x < 0.0 && rect.max.x > 0.0 {
- painter.line_segment(
- [Pos2::new(0.0, rect.min.y), Pos2::new(0.0, rect.max.y)],
- egui::Stroke::new(2.0, axis_color),
- );
- }
- if rect.min.y < 0.0 && rect.max.y > 0.0 {
- painter.line_segment(
- [Pos2::new(rect.min.x, 0.0), Pos2::new(rect.max.x, 0.0)],
- egui::Stroke::new(2.0, axis_color),
- );
- }
- }
-
/// Draw a single agent unit and return the interaction Response
fn draw_agent(
ui: &mut egui::Ui,