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:
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 {