diff --git a/src-tauri/src/acp/connection.rs b/src-tauri/src/acp/connection.rs index 150f2d74c..1864cb6fe 100644 --- a/src-tauri/src/acp/connection.rs +++ b/src-tauri/src/acp/connection.rs @@ -36,7 +36,9 @@ use crate::acp::file_system_runtime::{FileSystemRuntime, FileSystemRuntimeError, use crate::acp::registry::{self, AgentDistribution}; use crate::acp::session_state::SessionState; use crate::acp::stderr_tail::{summarize_parser_error, StderrTail, TailScope}; -use crate::acp::terminal_runtime::{TerminalRuntime, TerminalRuntimeError}; +use crate::acp::terminal_runtime::{ + TerminalRuntime, TerminalRuntimeError, TerminalShellRuntimeConfig, +}; use crate::acp::types::{ AcpEvent, AvailableCommandInfo, ConnectionInfo, ConnectionStatus, GrokEffortSpec, PermissionOptionInfo, PlanEntryInfo, PromptCapabilitiesInfo, PromptInputBlock, @@ -1154,6 +1156,7 @@ pub async fn spawn_agent_connection( preferred_mode_id: Option, preferred_config_values: BTreeMap, delegation_injection: Option, + terminal_shell_config: TerminalShellRuntimeConfig, ) -> Result, AcpError> { // Create the authoritative session state up front. Subsequent emit_with_state // calls write through this state and increment its seq counter so the first @@ -1317,6 +1320,7 @@ pub async fn spawn_agent_connection( emitter_clone.clone(), Arc::clone(&state_clone), terminal_base_env, + terminal_shell_config, preferred_mode_id, preferred_config_values, delegation_injection, @@ -3170,6 +3174,7 @@ async fn run_connection( emitter: EventEmitter, state: Arc>, terminal_base_env: BTreeMap, + terminal_shell_config: TerminalShellRuntimeConfig, preferred_mode_id: Option, preferred_config_values: BTreeMap, delegation_injection: Option, @@ -3188,7 +3193,9 @@ async fn run_connection( // `terminal/create` without a `cwd` (e.g. CodeBuddy) runs in the folder the // conversation runs in rather than codeg's own process cwd. let terminal_runtime = Arc::new( - TerminalRuntime::with_base_env(terminal_base_env).with_default_cwd(Some(cwd.clone())), + TerminalRuntime::with_base_env(terminal_base_env) + .with_default_cwd(Some(cwd.clone())) + .with_default_shell_config(terminal_shell_config), ); let cwd_string = cwd.to_string_lossy().to_string(); tracing::info!("[ACP] fs policy {}", fs_policy.describe()); diff --git a/src-tauri/src/acp/manager.rs b/src-tauri/src/acp/manager.rs index 59d6549ee..1ed437f1a 100644 --- a/src-tauri/src/acp/manager.rs +++ b/src-tauri/src/acp/manager.rs @@ -25,6 +25,7 @@ use crate::acp::question::{ build_outcome, QuestionAnswer, QuestionOutcome, QuestionSpec, RegisteredQuestion, SessionQuestionAccess, }; +use crate::acp::terminal_runtime::TerminalShellRuntimeConfig; use crate::acp::types::{ AcpEvent, AgentOptionsSnapshot, ConfigStaleKind, ConnectionInfo, ConnectionStatus, ForkResultInfo, PromptCapabilitiesInfo, PromptInputBlock, @@ -201,6 +202,10 @@ pub struct ConnectionManager { /// tests; in production initialized from env via /// `spawn_handshake_timeout_from_env`. spawn_handshake_timeout: Duration, + /// Shared General Settings shell used by ACP terminal fallbacks. Cloned + /// into each connection runtime so a setting update applies to existing + /// model sessions as well as newly spawned ones. + terminal_shell_config: TerminalShellRuntimeConfig, /// Delegation broker + token registry + UDS path installed during app /// bootstrap (`install_delegation`). When present, `spawn_agent` propagates /// the injection to `spawn_agent_connection`, which makes @@ -262,6 +267,7 @@ impl ConnectionManager { connections: Arc::new(Mutex::new(HashMap::new())), spawn_locks: Arc::new(Mutex::new(HashMap::new())), spawn_handshake_timeout: spawn_handshake_timeout_from_env(), + terminal_shell_config: TerminalShellRuntimeConfig::new(), delegation_injection: Arc::new(std::sync::OnceLock::new()), probe_locks: Arc::new(Mutex::new(HashMap::new())), pending_questions: Arc::new(Mutex::new(HashMap::new())), @@ -275,6 +281,7 @@ impl ConnectionManager { connections: self.connections.clone(), spawn_locks: self.spawn_locks.clone(), spawn_handshake_timeout: self.spawn_handshake_timeout, + terminal_shell_config: self.terminal_shell_config.clone(), delegation_injection: self.delegation_injection.clone(), probe_locks: self.probe_locks.clone(), pending_questions: self.pending_questions.clone(), @@ -293,6 +300,13 @@ impl ConnectionManager { self.delegation_injection.get().cloned() } + /// Returns the shared terminal-shell setting consumed by ACP terminal + /// runtimes. Keeping the handle shared makes saves apply immediately to + /// connections that are already running. + pub fn terminal_shell_config(&self) -> TerminalShellRuntimeConfig { + self.terminal_shell_config.clone() + } + /// Test-only constructor that overrides the spawn-handshake timeout. /// Production code should use `new()`. #[cfg(test)] @@ -301,6 +315,7 @@ impl ConnectionManager { connections: Arc::new(Mutex::new(HashMap::new())), spawn_locks: Arc::new(Mutex::new(HashMap::new())), spawn_handshake_timeout: timeout, + terminal_shell_config: TerminalShellRuntimeConfig::new(), delegation_injection: Arc::new(std::sync::OnceLock::new()), probe_locks: Arc::new(Mutex::new(HashMap::new())), pending_questions: Arc::new(Mutex::new(HashMap::new())), @@ -478,6 +493,7 @@ impl ConnectionManager { preferred_mode_id, preferred_config_values, self.delegation_snapshot(), + self.terminal_shell_config.clone(), ) .await?; diff --git a/src-tauri/src/acp/terminal_runtime.rs b/src-tauri/src/acp/terminal_runtime.rs index 70c5b51cc..bfa1097c3 100644 --- a/src-tauri/src/acp/terminal_runtime.rs +++ b/src-tauri/src/acp/terminal_runtime.rs @@ -1,5 +1,5 @@ use std::collections::{BTreeMap, HashMap, HashSet}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::Stdio; use std::sync::Arc; use std::time::Duration; @@ -10,9 +10,11 @@ use sacp::schema::{ TerminalOutputResponse, WaitForTerminalExitRequest, WaitForTerminalExitResponse, }; use tokio::io::{AsyncRead, AsyncReadExt}; -use tokio::sync::{watch, Mutex, Notify}; +use tokio::sync::{watch, Mutex, Notify, RwLock}; use tokio::task::JoinHandle; +use crate::terminal::manager::resolve_shell; + type TerminalMap = HashMap>; const DEFAULT_OUTPUT_BYTE_LIMIT: u64 = 1_000_000; /// After the child process exits, wait up to this long for the stdout/stderr @@ -390,6 +392,31 @@ async fn own_terminal_process(terminal: Arc, mut child: tokio: .send_replace(TerminalCompletion::Exited(exit_status)); } +/// Shared, hot-swappable default shell for ACP terminal requests. +/// +/// The setting is owned by [`crate::acp::manager::ConnectionManager`] and +/// cloned into every connection runtime. Reading it when a terminal is created +/// means a change in General Settings also affects already-running model +/// sessions. +#[derive(Clone, Default)] +pub struct TerminalShellRuntimeConfig { + inner: Arc>>, +} + +impl TerminalShellRuntimeConfig { + pub fn new() -> Self { + Self::default() + } + + pub async fn snapshot(&self) -> Option { + self.inner.read().await.clone() + } + + pub async fn set(&self, default_shell: Option) { + *self.inner.write().await = default_shell; + } +} + pub struct TerminalRuntime { terminals: Mutex, /// Base environment merged into every spawned terminal command before @@ -407,6 +434,10 @@ pub struct TerminalRuntime { /// process cwd (often "/" on desktop, the dev crate dir in development). /// `None` leaves the process cwd inherited (legacy behavior). default_cwd: Option, + /// The current General Settings default shell. Structured ACP requests + /// still direct-exec real programs, while shell command lines and shell + /// builtins use this selected shell as their fallback. + default_shell: TerminalShellRuntimeConfig, } #[derive(Debug, Clone)] @@ -428,6 +459,7 @@ impl TerminalRuntime { terminals: Mutex::new(HashMap::new()), base_env, default_cwd: None, + default_shell: TerminalShellRuntimeConfig::new(), } } @@ -438,6 +470,17 @@ impl TerminalRuntime { self } + /// Use a shared General Settings shell value for ACP terminal fallbacks. + /// The config is read at command creation time so existing connections pick + /// up setting changes without being restarted. + pub fn with_default_shell_config( + mut self, + default_shell: TerminalShellRuntimeConfig, + ) -> Self { + self.default_shell = default_shell; + self + } + /// Apply stdio, working directory, and environment to a freshly built /// terminal command. Shared by the direct-exec and shell-fallback spawn /// paths in `create_terminal` so both honor the same cwd precedence and @@ -514,14 +557,15 @@ impl TerminalRuntime { // as unrunnable (`NotFound`, or `InvalidFilename` when the whole line is // longer than the OS path limit — grok crams ` -lc "