notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit d3bf1273a5ebc2b5e3110c98712e2f290ea46fb9
parent ae2b22ebf90dd9ecb2dcee8833f37b63b428ec75
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 18 Feb 2026 11:36:51 -0800

fix remote messages not received until local message sent

After restarting Dave, restored local sessions had no
live_conversation_sub because the subscription was only created for
remote sessions. This meant remote user messages (e.g. from phone)
were ignored until a local message triggered the backend, which
created the subscription via SessionInfo. Subscribe to conversation
events for all restored sessions regardless of local/remote status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_dave/src/lib.rs | 66++++++++++++++++++++++++++++++++++++++++++------------------------
Mcrates/notedeck_dave/src/session.rs | 6+++++-
Mcrates/notedeck_dave/src/session_loader.rs | 5+----
Mcrates/notedeck_dave/src/ui/diff.rs | 7+++----
Mcrates/notedeck_dave/src/ui/mod.rs | 23+++++++++++++++++------
5 files changed, 68 insertions(+), 39 deletions(-)

diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -329,9 +329,7 @@ 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(); + 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 @@ -436,9 +434,15 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr match res { DaveApiResponse::Failed(ref err) => { if let Some(sk) = &secret_key { - if let Some(evt) = - ingest_live_event(session, app_ctx.ndb, sk, err, "error", None, None) - { + if let Some(evt) = ingest_live_event( + session, + app_ctx.ndb, + sk, + err, + "error", + None, + None, + ) { events_to_publish.push(evt); } } @@ -1317,14 +1321,19 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr agentic.live_threading.seed(root, last, loaded.event_count); } // Load permission state and dedup set from events - agentic.permissions.merge_loaded(loaded.permissions.responded, loaded.permissions.request_note_ids); + agentic.permissions.merge_loaded( + loaded.permissions.responded, + loaded.permissions.request_note_ids, + ); agentic.seen_note_ids = loaded.note_ids; // Set remote status from state event agentic.remote_status = AgentStatus::from_status_str(&state.status); agentic.remote_status_ts = state.created_at; - // Set up live conversation subscription for remote sessions - if is_remote && agentic.live_conversation_sub.is_none() { + // Set up live conversation subscription so we can + // receive messages from remote clients (e.g. phone) + // even before the local backend is started. + if agentic.live_conversation_sub.is_none() { let conv_filter = nostrdb::Filter::new() .kinds([session_events::AI_CONVERSATION_KIND as u64]) .tags([state.claude_session_id.as_str()], 'd') @@ -1333,7 +1342,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr Ok(sub) => { agentic.live_conversation_sub = Some(sub); tracing::info!( - "subscribed for live conversation events for remote session '{}'", + "subscribed for live conversation events for session '{}'", state.title, ); } @@ -1405,8 +1414,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr .iter() .filter(|s| { s.agentic.as_ref().is_some_and(|a| { - a.event_session_id() == Some(claude_sid) - && ts > a.remote_status_ts + a.event_session_id() == Some(claude_sid) && ts > a.remote_status_ts }) }) .map(|s| s.id) @@ -1489,21 +1497,25 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr if !cwd_path.exists() { session.source = session::SessionSource::Remote; } - let is_remote = session.is_remote(); 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); } // Load permission state and dedup set - agentic.permissions.merge_loaded(loaded.permissions.responded, loaded.permissions.request_note_ids); + agentic.permissions.merge_loaded( + loaded.permissions.responded, + loaded.permissions.request_note_ids, + ); agentic.seen_note_ids = loaded.note_ids; // Set remote status agentic.remote_status = AgentStatus::from_status_str(status_str); agentic.remote_status_ts = note.created_at(); - // Set up live conversation subscription for remote sessions - if is_remote && agentic.live_conversation_sub.is_none() { + // Set up live conversation subscription so we can + // receive messages from remote clients (e.g. phone) + // even before the local backend is started. + if agentic.live_conversation_sub.is_none() { let conv_filter = nostrdb::Filter::new() .kinds([session_events::AI_CONVERSATION_KIND as u64]) .tags([claude_sid], 'd') @@ -1512,7 +1524,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr Ok(sub) => { agentic.live_conversation_sub = Some(sub); tracing::info!( - "subscribed for live conversation events for remote session '{}'", + "subscribed for live conversation events for session '{}'", &title, ); } @@ -1541,10 +1553,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr /// /// For local sessions: only process `role=user` messages arriving from /// remote clients (phone), collecting them for backend dispatch. - fn poll_remote_conversation_events( - &mut self, - ndb: &nostrdb::Ndb, - ) -> Vec<(SessionId, String)> { + fn poll_remote_conversation_events(&mut self, ndb: &nostrdb::Ndb) -> Vec<(SessionId, String)> { let mut remote_user_messages: Vec<(SessionId, String)> = Vec::new(); let session_ids = self.session_manager.session_ids(); for session_id in session_ids { @@ -1554,7 +1563,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr let is_remote = session.is_remote(); // Get sub without holding agentic borrow - let sub = match session.agentic.as_ref().and_then(|a| a.live_conversation_sub) { + let sub = match session + .agentic + .as_ref() + .and_then(|a| a.live_conversation_sub) + { Some(s) => s, None => continue, }; @@ -1660,7 +1673,10 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr }; // Store the note ID for linking responses - agentic.permissions.request_note_ids.insert(perm_id, *note.id()); + agentic + .permissions + .request_note_ids + .insert(perm_id, *note.id()); session.chat.push(Message::PermissionRequest( crate::messages::PermissionRequest { @@ -1927,7 +1943,9 @@ impl notedeck::App for Dave { if let enostr::RelayEvent::Opened = (&ev.event).into() { if ev.relay == PNS_RELAY_URL { if let Some(sub_id) = &pns_sub_id { - if let Some(sk) = app_ctx.accounts.get_selected_account().keypair().secret_key { + if let Some(sk) = + app_ctx.accounts.get_selected_account().keypair().secret_key + { let pns_keys = enostr::pns::derive_pns_keys(&sk.secret_bytes()); let pns_filter = nostrdb::Filter::new() .kinds([enostr::pns::PNS_KIND as u64]) diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs @@ -109,7 +109,11 @@ impl PermissionTracker { } /// Merge loaded permission state from restored events. - pub fn merge_loaded(&mut self, responded: HashSet<Uuid>, request_note_ids: HashMap<Uuid, [u8; 32]>) { + pub fn merge_loaded( + &mut self, + responded: HashSet<Uuid>, + request_note_ids: HashMap<Uuid, [u8; 32]>, + ) { self.responded = responded; self.request_note_ids.extend(request_note_ids); } diff --git a/crates/notedeck_dave/src/session_loader.rs b/crates/notedeck_dave/src/session_loader.rs @@ -67,10 +67,7 @@ pub fn query_replaceable_filtered( ); match best { - Ok(map) => map - .into_values() - .filter_map(|(_, key)| key) - .collect(), + Ok(map) => map.into_values().filter_map(|(_, key)| key).collect(), Err(_) => vec![], } } diff --git a/crates/notedeck_dave/src/ui/diff.rs b/crates/notedeck_dave/src/ui/diff.rs @@ -13,10 +13,9 @@ pub fn file_update_ui(update: &FileUpdate, ui: &mut Ui) { .inner_margin(8.0) .corner_radius(4.0) .show(ui, |ui| { - egui::ScrollArea::horizontal() - .show(ui, |ui| { - render_diff_lines(update.diff_lines(), &update.update_type, ui); - }); + egui::ScrollArea::horizontal().show(ui, |ui| { + render_diff_lines(update.diff_lines(), &update.update_type, ui); + }); }); } diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs @@ -327,7 +327,11 @@ pub fn desktop_ui( ui: &mut egui::Ui, ) -> (DaveResponse, Option<SessionListAction>, bool) { let available = ui.available_rect_before_wrap(); - let sidebar_width = if available.width() < 830.0 { 200.0 } else { 280.0 }; + let sidebar_width = if available.width() < 830.0 { + 200.0 + } else { + 280.0 + }; let ctrl_held = ui.input(|i| i.modifiers.ctrl); let mut toggle_scene = false; @@ -677,8 +681,10 @@ pub fn handle_ui_action( DaveAction::PermissionResponse { request_id, response, - } => update::handle_permission_response(session_manager, request_id, response) - .map_or(UiActionResult::Handled, UiActionResult::PublishPermissionResponse), + } => update::handle_permission_response(session_manager, request_id, response).map_or( + UiActionResult::Handled, + UiActionResult::PublishPermissionResponse, + ), DaveAction::Interrupt => { update::execute_interrupt(session_manager, backend, ctx); UiActionResult::Handled @@ -694,8 +700,10 @@ pub fn handle_ui_action( DaveAction::QuestionResponse { request_id, answers, - } => update::handle_question_response(session_manager, request_id, answers) - .map_or(UiActionResult::Handled, UiActionResult::PublishPermissionResponse), + } => update::handle_question_response(session_manager, request_id, answers).map_or( + UiActionResult::Handled, + UiActionResult::PublishPermissionResponse, + ), DaveAction::ExitPlanMode { request_id, approved, @@ -716,7 +724,10 @@ pub fn handle_ui_action( }, ) }; - result.map_or(UiActionResult::Handled, UiActionResult::PublishPermissionResponse) + result.map_or( + UiActionResult::Handled, + UiActionResult::PublishPermissionResponse, + ) } } }