notedeck

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

commit 25646da388f7bc208aa5a0a2079b11c0f2b7bbb6
parent 29b0a37a0f5132fda3bd51033892f50d3e04c1cc
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 24 Feb 2026 12:49:52 -0800

dave: per-session backend selection with backend picker UI

Allow choosing between Claude Code and Codex backends during session
creation. Available backends are detected by checking for binaries on
PATH. When multiple agentic backends are found, a picker overlay is
shown after directory selection. Single-backend setups skip the picker.

Each ChatSession now stores its own BackendType, and the Dave struct
holds a HashMap of all available backends instead of a single one.

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

Diffstat:
Mcrates/notedeck_dave/src/backend/claude.rs | 5+----
Mcrates/notedeck_dave/src/backend/traits.rs | 17++++++++++++++++-
Mcrates/notedeck_dave/src/config.rs | 14+++++++++++++-
Mcrates/notedeck_dave/src/lib.rs | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mcrates/notedeck_dave/src/session.rs | 29+++++++++++++++++++++++------
Mcrates/notedeck_dave/src/ui/mod.rs | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck_dave/src/update.rs | 16++++++++++------
7 files changed, 285 insertions(+), 57 deletions(-)

diff --git a/crates/notedeck_dave/src/backend/claude.rs b/crates/notedeck_dave/src/backend/claude.rs @@ -60,16 +60,13 @@ struct SessionHandle { } pub struct ClaudeBackend { - #[allow(dead_code)] // May be used in the future for API key validation - api_key: String, /// Registry of active sessions (using dashmap for lock-free access) sessions: DashMap<String, SessionHandle>, } impl ClaudeBackend { - pub fn new(api_key: String) -> Self { + pub fn new() -> Self { Self { - api_key, sessions: DashMap::new(), } } diff --git a/crates/notedeck_dave/src/backend/traits.rs b/crates/notedeck_dave/src/backend/traits.rs @@ -7,7 +7,7 @@ use std::sync::mpsc; use std::sync::Arc; /// Backend type selection -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum BackendType { OpenAI, Claude, @@ -16,6 +16,21 @@ pub enum BackendType { Remote, } +impl BackendType { + pub fn display_name(&self) -> &'static str { + match self { + BackendType::OpenAI => "OpenAI", + BackendType::Claude => "Claude Code", + BackendType::Codex => "Codex", + BackendType::Remote => "Remote", + } + } + + pub fn is_agentic(&self) -> bool { + matches!(self, BackendType::Claude | BackendType::Codex) + } +} + /// Trait for AI backend implementations pub trait AiBackend: Send + Sync { /// Stream a request to the AI backend diff --git a/crates/notedeck_dave/src/config.rs b/crates/notedeck_dave/src/config.rs @@ -4,12 +4,24 @@ use serde::{Deserialize, Serialize}; use std::env; /// Check if a binary exists on the system PATH. -fn has_binary_on_path(binary: &str) -> bool { +pub fn has_binary_on_path(binary: &str) -> bool { env::var_os("PATH") .map(|paths| env::split_paths(&paths).any(|dir| dir.join(binary).is_file())) .unwrap_or(false) } +/// Detect which agentic backends are available based on binaries in PATH. +pub fn available_agentic_backends() -> Vec<BackendType> { + let mut backends = Vec::new(); + if has_binary_on_path("claude") { + backends.push(BackendType::Claude); + } + if has_binary_on_path("codex") { + backends.push(BackendType::Codex); + } + backends +} + /// AI interaction mode - determines UI complexity and feature set #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AiMode { diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -99,6 +99,7 @@ pub enum DaveOverlay { Settings, DirectoryPicker, SessionPicker, + BackendPicker, } pub struct Dave { @@ -110,8 +111,10 @@ pub struct Dave { avatar: Option<DaveAvatar>, /// Shared tools available to all sessions tools: Arc<HashMap<String, Tool>>, - /// AI backend (OpenAI, Claude, etc.) - backend: Box<dyn AiBackend>, + /// AI backends keyed by type — multiple may be available simultaneously + backends: HashMap<BackendType, Box<dyn AiBackend>>, + /// Which agentic backends are available (detected from PATH at startup) + available_backends: Vec<BackendType>, /// Model configuration model_config: ModelConfig, /// Whether to show session list on mobile @@ -140,6 +143,8 @@ 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)>, @@ -285,6 +290,18 @@ fn ingest_live_event( } /// Calculate an anonymous user_id from a keypair +/// Look up a backend by type from the map, falling back to Remote. +fn get_backend( + backends: &HashMap<BackendType, Box<dyn AiBackend>>, + bt: BackendType, +) -> &dyn AiBackend { + backends + .get(&bt) + .or_else(|| backends.get(&BackendType::Remote)) + .unwrap() + .as_ref() +} + fn calculate_user_id(keypair: KeypairUnowned) -> String { use sha2::{Digest, Sha256}; // pubkeys have degraded privacy, don't do that @@ -349,25 +366,41 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr // Determine AI mode from backend type let ai_mode = model_config.ai_mode(); - // Create backend based on configuration - let backend: Box<dyn AiBackend> = match model_config.backend { - BackendType::OpenAI => { - use async_openai::Client; - let client = Client::with_config(model_config.to_api()); - Box::new(OpenAiBackend::new(client, ndb.clone())) - } - BackendType::Claude => { - let api_key = model_config - .anthropic_api_key - .as_ref() - .expect("Claude backend requires ANTHROPIC_API_KEY or CLAUDE_API_KEY"); - Box::new(ClaudeBackend::new(api_key.clone())) + // Detect available agentic backends from PATH + let available_backends = config::available_agentic_backends(); + + // Create backends for all available agentic CLIs + the configured primary + let mut backends: HashMap<BackendType, Box<dyn AiBackend>> = HashMap::new(); + + for &bt in &available_backends { + match bt { + BackendType::Claude => { + backends.insert(BackendType::Claude, Box::new(ClaudeBackend::new())); + } + BackendType::Codex => { + backends.insert( + BackendType::Codex, + Box::new(CodexBackend::new( + std::env::var("CODEX_BINARY").unwrap_or_else(|_| "codex".to_string()), + )), + ); + } + _ => {} } - BackendType::Codex => Box::new(CodexBackend::new( - std::env::var("CODEX_BINARY").unwrap_or_else(|_| "codex".to_string()), - )), - BackendType::Remote => Box::new(RemoteOnlyBackend), - }; + } + + // If the configured backend is OpenAI and not yet created, add it + if model_config.backend == BackendType::OpenAI { + use async_openai::Client; + let client = Client::with_config(model_config.to_api()); + backends.insert( + BackendType::OpenAI, + Box::new(OpenAiBackend::new(client, ndb.clone())), + ); + } + + // Remote backend is always available for discovered sessions + backends.insert(BackendType::Remote, Box::new(RemoteOnlyBackend)); let avatar = render_state.map(DaveAvatar::new); let mut tools: HashMap<String, Tool> = HashMap::new(); @@ -395,7 +428,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr AiMode::Chat => { let mut manager = SessionManager::new(); // Create a default session with current directory - let sid = manager.new_session(std::env::current_dir().unwrap_or_default(), ai_mode); + let sid = manager.new_session( + std::env::current_dir().unwrap_or_default(), + ai_mode, + model_config.backend, + ); if let Some(session) = manager.get_mut(sid) { session.details.hostname = hostname.clone(); } @@ -407,7 +444,8 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr Dave { ai_mode, - backend, + backends, + available_backends, avatar, session_manager, tools: Arc::new(tools), @@ -425,6 +463,7 @@ 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(), @@ -515,6 +554,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr AiMode::Chat, cwd, &self.hostname, + self.model_config.backend, ); if let Some(session) = self.session_manager.get_mut(id) { @@ -956,8 +996,7 @@ 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) => { - self.create_session_with_cwd(path); - self.active_overlay = DaveOverlay::None; + self.create_or_pick_backend(path); } OverlayResult::ShowSessionPicker(path) => { self.session_picker.open(path); @@ -978,16 +1017,21 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr title, file_path, } => { + // Resumed sessions are always Claude (discovered from JSONL) let claude_session_id = session_id.clone(); - let sid = self.create_resumed_session_with_cwd(cwd, session_id, title); + let sid = self.create_resumed_session_with_cwd( + cwd, + session_id, + title, + BackendType::Claude, + ); self.pending_archive_convert = Some((file_path, sid, claude_session_id)); self.session_picker.close(); self.active_overlay = DaveOverlay::None; } OverlayResult::NewSession { cwd } => { - self.create_session_with_cwd(cwd); self.session_picker.close(); - self.active_overlay = DaveOverlay::None; + self.create_or_pick_backend(cwd); } OverlayResult::BackToDirectoryPicker => { self.session_picker.close(); @@ -997,6 +1041,15 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr } return DaveResponse::default(); } + DaveOverlay::BackendPicker => { + if let Some(bt) = ui::backend_picker_overlay_ui(&self.available_backends, ui) { + if let Some(cwd) = self.pending_backend_cwd.take() { + self.create_session_with_cwd(cwd, bt); + } + self.active_overlay = DaveOverlay::None; + } + return DaveResponse::default(); + } DaveOverlay::None => {} } @@ -1126,7 +1179,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr AiMode::Chat => { // In chat mode, create a session directly without the directory picker let cwd = std::env::current_dir().unwrap_or_default(); - self.create_session_with_cwd(cwd); + self.create_session_with_cwd(cwd, self.model_config.backend); } AiMode::Agentic => { // In agentic mode, show the directory picker to select a working directory @@ -1136,7 +1189,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr } /// Create a new session with the given cwd (called after directory picker selection) - fn create_session_with_cwd(&mut self, cwd: PathBuf) { + fn create_session_with_cwd(&mut self, cwd: PathBuf, backend_type: BackendType) { update::create_session_with_cwd( &mut self.session_manager, &mut self.directory_picker, @@ -1145,6 +1198,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr self.ai_mode, cwd, &self.hostname, + backend_type, ); } @@ -1154,6 +1208,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr cwd: PathBuf, resume_session_id: String, title: String, + backend_type: BackendType, ) -> SessionId { update::create_resumed_session_with_cwd( &mut self.session_manager, @@ -1165,6 +1220,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr resume_session_id, title, &self.hostname, + backend_type, ) } @@ -1189,9 +1245,11 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr // Drain all pending connections (non-blocking) while let Some(mut pending) = listener.try_recv() { // Create the session and get its ID - let id = self - .session_manager - .new_session(pending.cwd.clone(), self.ai_mode); + let id = self.session_manager.new_session( + pending.cwd.clone(), + self.ai_mode, + self.model_config.backend, + ); self.directory_picker.add_recent(pending.cwd); // Focus on new session @@ -1496,6 +1554,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr state.claude_session_id.clone(), state.title.clone(), AiMode::Agentic, + BackendType::Claude, ); // Load conversation history from kind-1988 events @@ -1640,10 +1699,15 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr .map(|s| s.id) .collect(); for id in to_delete { + let bt = self + .session_manager + .get(id) + .map(|s| s.backend_type) + .unwrap_or(BackendType::Remote); update::delete_session( &mut self.session_manager, &mut self.focus_queue, - self.backend.as_ref(), + get_backend(&self.backends, bt), &mut self.directory_picker, id, ); @@ -1706,6 +1770,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr claude_sid.to_string(), state.title.clone(), AiMode::Agentic, + BackendType::Remote, ); // Load any conversation history that arrived with it @@ -2034,10 +2099,15 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr } } + let bt = self + .session_manager + .get(id) + .map(|s| s.backend_type) + .unwrap_or(BackendType::Remote); update::delete_session( &mut self.session_manager, &mut self.focus_queue, - self.backend.as_ref(), + get_backend(&self.backends, bt), &mut self.directory_picker, id, ); @@ -2045,9 +2115,14 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr /// Handle an interrupt request - requires double-Escape to confirm fn handle_interrupt_request(&mut self, ctx: &egui::Context) { + let bt = self + .session_manager + .get_active() + .map(|s| s.backend_type) + .unwrap_or(BackendType::Remote); self.interrupt_pending_since = update::handle_interrupt_request( &self.session_manager, - self.backend.as_ref(), + get_backend(&self.backends, bt), self.interrupt_pending_since, ctx, ); @@ -2064,6 +2139,32 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr self.interrupt_pending_since.is_some() } + /// If only one agentic backend is available, return it. Otherwise None + /// (meaning we need to show the backend picker). + fn single_agentic_backend(&self) -> Option<BackendType> { + if self.available_backends.len() == 1 { + Some(self.available_backends[0]) + } else { + None + } + } + + /// Create a session with the given cwd, or show the backend picker if + /// multiple agentic backends are available. + fn create_or_pick_backend(&mut self, cwd: PathBuf) { + if let Some(bt) = self.single_agentic_backend() { + self.create_session_with_cwd(cwd, bt); + self.active_overlay = DaveOverlay::None; + } 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; + } else { + self.pending_backend_cwd = Some(cwd); + self.active_overlay = DaveOverlay::BackendPicker; + } + } + /// 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) @@ -2076,12 +2177,17 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr /// Handle a keybinding action fn handle_key_action(&mut self, key_action: KeyAction, ui: &egui::Ui) { + let bt = self + .session_manager + .get_active() + .map(|s| s.backend_type) + .unwrap_or(BackendType::Remote); match ui::handle_key_action( key_action, &mut self.session_manager, &mut self.scene, &mut self.focus_queue, - self.backend.as_ref(), + get_backend(&self.backends, bt), self.show_scene, self.auto_steal_focus, &mut self.home_session, @@ -2112,7 +2218,16 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr /// Handle the Send action, including tentative permission states fn handle_send_action(&mut self, ctx: &AppContext, ui: &egui::Ui) { - match ui::handle_send_action(&mut self.session_manager, self.backend.as_ref(), ui.ctx()) { + let bt = self + .session_manager + .get_active() + .map(|s| s.backend_type) + .unwrap_or(BackendType::Remote); + match ui::handle_send_action( + &mut self.session_manager, + get_backend(&self.backends, bt), + ui.ctx(), + ) { SendActionResult::SendMessage => { self.handle_user_send(ctx, ui); } @@ -2136,10 +2251,15 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr return None; } + let bt = self + .session_manager + .get_active() + .map(|s| s.backend_type) + .unwrap_or(BackendType::Remote); match ui::handle_ui_action( action, &mut self.session_manager, - self.backend.as_ref(), + get_backend(&self.backends, bt), &mut self.active_overlay, &mut self.show_session_list, ui.ctx(), @@ -2239,12 +2359,13 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr .agentic .as_ref() .and_then(|a| a.resume_session_id.clone()); + let backend_type = session.backend_type; let tools = self.tools.clone(); let model_name = self.model_config.model().to_owned(); let ctx = ctx.clone(); // Use backend to stream request - let (rx, task_handle) = self.backend.stream_request( + let (rx, task_handle) = get_backend(&self.backends, backend_type).stream_request( messages, tools, model_name, diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use std::sync::mpsc::Receiver; use crate::agent_status::AgentStatus; +use crate::backend::BackendType; use crate::config::AiMode; use crate::git_status::GitStatusCache; use crate::messages::{ @@ -333,6 +334,8 @@ pub struct ChatSession { pub source: SessionSource, /// Session metadata for display (title, hostname, cwd) pub details: SessionDetails, + /// Which backend this session uses (Claude, Codex, etc.) + pub backend_type: BackendType, } impl Drop for ChatSession { @@ -344,7 +347,7 @@ impl Drop for ChatSession { } impl ChatSession { - pub fn new(id: SessionId, cwd: PathBuf, ai_mode: AiMode) -> Self { + pub fn new(id: SessionId, cwd: PathBuf, ai_mode: AiMode, backend_type: BackendType) -> Self { let details_cwd = if ai_mode == AiMode::Agentic { Some(cwd.clone()) } else { @@ -376,6 +379,7 @@ impl ChatSession { .map(|h| h.to_string_lossy().to_string()) .unwrap_or_default(), }, + backend_type, } } @@ -386,8 +390,9 @@ impl ChatSession { resume_session_id: String, title: String, ai_mode: AiMode, + backend_type: BackendType, ) -> Self { - let mut session = Self::new(id, cwd, ai_mode); + let mut session = Self::new(id, cwd, ai_mode, backend_type); if let Some(ref mut agentic) = session.agentic { agentic.resume_session_id = Some(resume_session_id); } @@ -609,11 +614,16 @@ impl SessionManager { } /// Create a new session with the given cwd and make it active - pub fn new_session(&mut self, cwd: PathBuf, ai_mode: AiMode) -> SessionId { + pub fn new_session( + &mut self, + cwd: PathBuf, + ai_mode: AiMode, + backend_type: BackendType, + ) -> SessionId { let id = self.next_id; self.next_id += 1; - let session = ChatSession::new(id, cwd, ai_mode); + let session = ChatSession::new(id, cwd, ai_mode, backend_type); self.sessions.insert(id, session); self.order.insert(0, id); // Most recent first self.active = Some(id); @@ -629,11 +639,13 @@ impl SessionManager { resume_session_id: String, title: String, ai_mode: AiMode, + backend_type: BackendType, ) -> SessionId { let id = self.next_id; self.next_id += 1; - let session = ChatSession::new_resumed(id, cwd, resume_session_id, title, ai_mode); + let session = + ChatSession::new_resumed(id, cwd, resume_session_id, title, ai_mode, backend_type); self.sessions.insert(id, session); self.order.insert(0, id); // Most recent first self.active = Some(id); @@ -855,7 +867,12 @@ mod tests { use std::sync::mpsc; fn test_session() -> ChatSession { - ChatSession::new(1, PathBuf::from("/tmp"), AiMode::Agentic) + ChatSession::new( + 1, + PathBuf::from("/tmp"), + AiMode::Agentic, + BackendType::Claude, + ) } #[test] diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs @@ -30,6 +30,7 @@ pub use settings::{DaveSettingsPanel, SettingsPanelAction}; // ============================================================================= use crate::agent_status::AgentStatus; +use crate::backend::BackendType; use crate::config::{AiMode, DaveSettings, ModelConfig}; use crate::focus_queue::FocusQueue; use crate::messages::PermissionResponse; @@ -200,6 +201,67 @@ pub fn session_picker_overlay_ui( OverlayResult::None } +/// Render the backend picker overlay UI. +/// Returns Some(BackendType) when the user has selected a backend. +pub fn backend_picker_overlay_ui( + available_backends: &[BackendType], + ui: &mut egui::Ui, +) -> Option<BackendType> { + let mut selected = None; + + // Handle keyboard shortcuts: 1-9 for quick selection + for (idx, &bt) in available_backends.iter().enumerate().take(9) { + let key = match idx { + 0 => egui::Key::Num1, + 1 => egui::Key::Num2, + 2 => egui::Key::Num3, + 3 => egui::Key::Num4, + 4 => egui::Key::Num5, + _ => continue, + }; + if ui.input(|i| i.key_pressed(key)) { + return Some(bt); + } + } + + let is_narrow = notedeck::ui::is_narrow(ui.ctx()); + + egui::Frame::new() + .fill(ui.visuals().panel_fill) + .inner_margin(egui::Margin::symmetric(if is_narrow { 16 } else { 40 }, 20)) + .show(ui, |ui| { + ui.heading("Select Backend"); + ui.add_space(8.0); + ui.label("Choose which AI backend to use for this session:"); + ui.add_space(16.0); + + let max_width = if is_narrow { + ui.available_width() + } else { + 400.0 + }; + + ui.allocate_ui_with_layout( + egui::vec2(max_width, ui.available_height()), + egui::Layout::top_down(egui::Align::LEFT), + |ui| { + for (idx, &bt) in available_backends.iter().enumerate() { + let label = format!("[{}] {}", idx + 1, bt.display_name()); + let button = egui::Button::new(egui::RichText::new(&label).size(16.0)) + .min_size(egui::vec2(max_width, 40.0)); + + if ui.add(button).clicked() { + selected = Some(bt); + } + ui.add_space(4.0); + } + }, + ); + }); + + selected +} + /// Scene view action returned after rendering pub enum SceneViewAction { None, diff --git a/crates/notedeck_dave/src/update.rs b/crates/notedeck_dave/src/update.rs @@ -3,7 +3,7 @@ //! These are standalone functions with explicit inputs to reduce the complexity //! of the main Dave struct and make the code more testable and reusable. -use crate::backend::AiBackend; +use crate::backend::{AiBackend, BackendType}; use crate::config::AiMode; use crate::focus_queue::{FocusPriority, FocusQueue}; use crate::messages::{ @@ -907,10 +907,11 @@ pub fn create_session_with_cwd( ai_mode: AiMode, cwd: PathBuf, hostname: &str, + backend_type: BackendType, ) -> SessionId { directory_picker.add_recent(cwd.clone()); - let id = session_manager.new_session(cwd, ai_mode); + let id = session_manager.new_session(cwd, ai_mode, backend_type); if let Some(session) = session_manager.get_mut(id) { session.details.hostname = hostname.to_string(); session.focus_requested = true; @@ -937,10 +938,12 @@ pub fn create_resumed_session_with_cwd( resume_session_id: String, title: String, hostname: &str, + backend_type: BackendType, ) -> SessionId { directory_picker.add_recent(cwd.clone()); - let id = session_manager.new_resumed_session(cwd, resume_session_id, title, ai_mode); + let id = + session_manager.new_resumed_session(cwd, resume_session_id, title, ai_mode, backend_type); if let Some(session) = session_manager.get_mut(id) { session.details.hostname = hostname.to_string(); session.focus_requested = true; @@ -964,9 +967,9 @@ pub fn clone_active_agent( ai_mode: AiMode, hostname: &str, ) -> Option<SessionId> { - let cwd = session_manager - .get_active() - .and_then(|s| s.cwd().cloned())?; + let active = session_manager.get_active()?; + let cwd = active.cwd().cloned()?; + let backend_type = active.backend_type; Some(create_session_with_cwd( session_manager, directory_picker, @@ -975,6 +978,7 @@ pub fn clone_active_agent( ai_mode, cwd, hostname, + backend_type, )) }