notedeck

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

commit b49690db04e161518ba456ca5521d0d7b54315ed
parent a4f6f25aca3bd3e24f77c30c6b9fbb6dd48d028f
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 24 Feb 2026 15:09:50 -0800

dave: refactor overlay into state machine, fix backend picker ordering

Replace scattered pending_backend_cwd / pending_backend_type Option
fields with data-carrying enum variants:

  BackendPicker { cwd }    — directory chosen, picking backend
  SessionPicker { backend } — backend chosen, picking session

The overlay match in ui() now uses std::mem::take so the owned data
flows naturally through the session-creation pipeline.

Also fix the ordering bug where selecting a directory with resumable
sessions would skip the backend picker entirely. The flow is now:
directory → backend picker → (optionally session picker) → session.

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

Diffstat:
Mcrates/notedeck_dave/src/lib.rs | 109++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mcrates/notedeck_dave/src/ui/mod.rs | 10+---------
2 files changed, 64 insertions(+), 55 deletions(-)

diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -91,15 +91,23 @@ fn secret_key_bytes(keypair: KeypairUnowned<'_>) -> Option<[u8; 32]> { }) } -/// Represents which full-screen overlay (if any) is currently active -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +/// Represents which full-screen overlay (if any) is currently active. +/// Data-carrying variants hold the state needed for that step in the +/// session-creation flow, replacing scattered `pending_*` fields. +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub enum DaveOverlay { #[default] None, Settings, DirectoryPicker, - SessionPicker, - BackendPicker, + /// Backend has been chosen; showing resumable-session list. + SessionPicker { + backend: BackendType, + }, + /// Directory chosen; waiting for user to pick a backend. + BackendPicker { + cwd: PathBuf, + }, } pub struct Dave { @@ -143,8 +151,6 @@ pub struct Dave { active_overlay: DaveOverlay, /// IPC listener for external spawn-agent commands ipc_listener: Option<ipc::IpcListener>, - /// CWD waiting for backend selection (set when backend picker is shown) - pending_backend_cwd: Option<PathBuf>, /// Pending archive conversion: (jsonl_path, dave_session_id, claude_session_id). /// Set when resuming a session; processed in update() where AppContext is available. pending_archive_convert: Option<(std::path::PathBuf, SessionId, String)>, @@ -468,7 +474,6 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr session_picker: SessionPicker::new(), active_overlay, ipc_listener, - pending_backend_cwd: None, pending_archive_convert: None, pending_message_load: None, pending_relay_events: Vec::new(), @@ -975,19 +980,20 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr } fn ui(&mut self, app_ctx: &mut AppContext, ui: &mut egui::Ui) -> DaveResponse { - // Check overlays first - they take over the entire UI - match self.active_overlay { + // Check overlays first — take ownership so we can call &mut self + // methods freely. Put the variant back if the overlay stays open. + let overlay = std::mem::take(&mut self.active_overlay); + match overlay { DaveOverlay::Settings => { match ui::settings_overlay_ui(&mut self.settings_panel, &self.settings, ui) { OverlayResult::ApplySettings(new_settings) => { self.apply_settings(new_settings.clone()); - self.active_overlay = DaveOverlay::None; return DaveResponse::new(DaveAction::UpdateSettings(new_settings)); } - OverlayResult::Close => { - self.active_overlay = DaveOverlay::None; + OverlayResult::Close => {} + _ => { + self.active_overlay = DaveOverlay::Settings; } - _ => {} } return DaveResponse::default(); } @@ -996,25 +1002,17 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr match ui::directory_picker_overlay_ui(&mut self.directory_picker, has_sessions, ui) { OverlayResult::DirectorySelected(path) => { - tracing::info!("directory selected (no resumable sessions): {:?}", path); + tracing::info!("directory selected: {:?}", path); self.create_or_pick_backend(path); } - OverlayResult::ShowSessionPicker(path) => { - tracing::info!( - "directory has resumable sessions, showing session picker: {:?}", - path - ); - self.session_picker.open(path); - self.active_overlay = DaveOverlay::SessionPicker; - } - OverlayResult::Close => { - self.active_overlay = DaveOverlay::None; + OverlayResult::Close => {} + _ => { + self.active_overlay = DaveOverlay::DirectoryPicker; } - _ => {} } return DaveResponse::default(); } - DaveOverlay::SessionPicker => { + DaveOverlay::SessionPicker { backend } => { match ui::session_picker_overlay_ui(&mut self.session_picker, ui) { OverlayResult::ResumeSession { cwd, @@ -1032,32 +1030,32 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr ); self.pending_archive_convert = Some((file_path, sid, claude_session_id)); self.session_picker.close(); - self.active_overlay = DaveOverlay::None; } OverlayResult::NewSession { cwd } => { - tracing::info!("new session from session picker: {:?}", cwd); + tracing::info!( + "new session from session picker: {:?} (backend: {:?})", + cwd, + backend + ); self.session_picker.close(); - self.create_or_pick_backend(cwd); + self.create_session_with_cwd(cwd, backend); } OverlayResult::BackToDirectoryPicker => { self.session_picker.close(); self.active_overlay = DaveOverlay::DirectoryPicker; } - _ => {} + _ => { + self.active_overlay = DaveOverlay::SessionPicker { backend }; + } } return DaveResponse::default(); } - DaveOverlay::BackendPicker => { - tracing::info!( - "rendering backend picker: {} backends available", - self.available_backends.len() - ); + DaveOverlay::BackendPicker { cwd } => { if let Some(bt) = ui::backend_picker_overlay_ui(&self.available_backends, ui) { tracing::info!("backend selected: {:?}", bt); - if let Some(cwd) = self.pending_backend_cwd.take() { - self.create_session_with_cwd(cwd, bt); - } - self.active_overlay = DaveOverlay::None; + self.create_or_resume_session(cwd, bt); + } else { + self.active_overlay = DaveOverlay::BackendPicker { cwd }; } return DaveResponse::default(); } @@ -1277,7 +1275,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 { + if matches!(self.active_overlay, DaveOverlay::DirectoryPicker) { self.active_overlay = DaveOverlay::None; } @@ -2170,22 +2168,41 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr ); if let Some(bt) = self.single_agentic_backend() { tracing::info!("single backend detected, skipping picker: {:?}", bt); - self.create_session_with_cwd(cwd, bt); - self.active_overlay = DaveOverlay::None; + self.create_or_resume_session(cwd, bt); } else if self.available_backends.is_empty() { // No agentic backends — fall back to configured backend - self.create_session_with_cwd(cwd, self.model_config.backend); - self.active_overlay = DaveOverlay::None; + self.create_or_resume_session(cwd, self.model_config.backend); } else { tracing::info!( "multiple backends available, showing backend picker: {:?}", self.available_backends ); - self.pending_backend_cwd = Some(cwd); - self.active_overlay = DaveOverlay::BackendPicker; + self.active_overlay = DaveOverlay::BackendPicker { cwd }; } } + /// After a backend is determined, either create a session directly or + /// show the session picker if there are resumable sessions for this backend. + fn create_or_resume_session(&mut self, cwd: PathBuf, backend_type: BackendType) { + // Only Claude has discoverable resumable sessions (from ~/.claude/) + if backend_type == BackendType::Claude { + let resumable = discover_sessions(&cwd); + if !resumable.is_empty() { + tracing::info!( + "found {} resumable sessions, showing session picker", + resumable.len() + ); + self.session_picker.open(cwd); + self.active_overlay = DaveOverlay::SessionPicker { + backend: backend_type, + }; + return; + } + } + self.create_session_with_cwd(cwd, backend_type); + self.active_overlay = DaveOverlay::None; + } + /// Get the first pending permission request ID for the active session fn first_pending_permission(&self) -> Option<uuid::Uuid> { update::first_pending_permission(&self.session_manager) diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs @@ -35,7 +35,6 @@ use crate::config::{AiMode, DaveSettings, ModelConfig}; use crate::focus_queue::FocusQueue; use crate::messages::PermissionResponse; use crate::session::{ChatSession, PermissionMessageState, SessionId, SessionManager}; -use crate::session_discovery::discover_sessions; use crate::update; use crate::DaveOverlay; use egui::include_image; @@ -109,8 +108,6 @@ pub enum OverlayResult { Close, /// Directory was selected (no resumable sessions) DirectorySelected(std::path::PathBuf), - /// Show session picker for the given directory - ShowSessionPicker(std::path::PathBuf), /// Resume a session ResumeSession { cwd: std::path::PathBuf, @@ -155,12 +152,7 @@ pub fn directory_picker_overlay_ui( if let Some(action) = directory_picker.overlay_ui(ui, has_sessions) { match action { DirectoryPickerAction::DirectorySelected(path) => { - let resumable_sessions = discover_sessions(&path); - if resumable_sessions.is_empty() { - return OverlayResult::DirectorySelected(path); - } else { - return OverlayResult::ShowSessionPicker(path); - } + return OverlayResult::DirectorySelected(path); } DirectoryPickerAction::Cancelled => { if has_sessions {