feat(settings): add configurable default terminal for CLI agents
- Add default terminal selector under Advanced Settings > Software Settings - Detect available terminals across macOS, Windows, and Linux - Persist and migrate the default-terminal configuration - Apply the selected terminal when launching CLI agents - Add localized labels and regression tests Fixes #179
This commit is contained in:
1 parent
270ad2869c
commit
79b50b7a2b
10 files changed
+288
-26
No files matched your search
+208
-25
@@ -1,5 +1,111 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AgentTerminalOption {
|
||||
pub(crate) id: String,
|
||||
pub(crate) label: String,
|
||||
}
|
||||
|
||||
fn terminal_option(id: &str, label: &str) -> AgentTerminalOption {
|
||||
AgentTerminalOption {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn program_on_path(program: &str) -> Option<PathBuf> {
|
||||
let path = env::var_os("PATH")?;
|
||||
env::split_paths(&path).find_map(|directory| {
|
||||
let candidate = directory.join(program);
|
||||
candidate.is_file().then_some(candidate)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn macos_iterm2_installed() -> bool {
|
||||
Path::new("/Applications/iTerm.app").is_dir()
|
||||
|| env::var_os("HOME")
|
||||
.map(PathBuf::from)
|
||||
.is_some_and(|home| home.join("Applications/iTerm.app").is_dir())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux_terminal_definitions() -> &'static [(
|
||||
&'static str,
|
||||
&'static str,
|
||||
&'static [&'static str],
|
||||
&'static str,
|
||||
)] {
|
||||
&[
|
||||
(
|
||||
"x-terminal-emulator",
|
||||
"x-terminal-emulator",
|
||||
&["-e"],
|
||||
"System terminal",
|
||||
),
|
||||
(
|
||||
"gnome-terminal",
|
||||
"gnome-terminal",
|
||||
&["--"],
|
||||
"GNOME Terminal",
|
||||
),
|
||||
("konsole", "konsole", &["-e"], "Konsole"),
|
||||
("xfce4-terminal", "xfce4-terminal", &["-e"], "Xfce Terminal"),
|
||||
("mate-terminal", "mate-terminal", &["--"], "MATE Terminal"),
|
||||
("kitty", "kitty", &["-e"], "Kitty"),
|
||||
("alacritty", "alacritty", &["-e"], "Alacritty"),
|
||||
("ghostty", "ghostty", &["-e"], "Ghostty"),
|
||||
("xterm", "xterm", &["-e"], "XTerm"),
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn available_agent_terminals() -> Vec<AgentTerminalOption> {
|
||||
let mut options = vec![terminal_option("auto", "Automatic")];
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
options.push(terminal_option("terminal", "Terminal"));
|
||||
if macos_iterm2_installed() {
|
||||
options.push(terminal_option("iterm2", "iTerm2"));
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if program_on_path("wt.exe").is_some() {
|
||||
options.push(terminal_option("windows-terminal", "Windows Terminal"));
|
||||
}
|
||||
let powershell = windows_powershell_executable();
|
||||
if powershell.is_file() {
|
||||
options.push(terminal_option("powershell", "PowerShell"));
|
||||
}
|
||||
let command_prompt = windows_command_processor();
|
||||
if command_prompt.is_file() {
|
||||
options.push(terminal_option("cmd", "Command Prompt"));
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
for (id, program, _, label) in linux_terminal_definitions() {
|
||||
if program_on_path(program).is_some() {
|
||||
options.push(terminal_option(id, label));
|
||||
}
|
||||
}
|
||||
}
|
||||
options
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_agent_terminal(value: &str) -> String {
|
||||
let value = value.trim();
|
||||
if available_agent_terminals()
|
||||
.iter()
|
||||
.any(|option| option.id == value)
|
||||
{
|
||||
value.to_string()
|
||||
} else {
|
||||
DEFAULT_AGENT_TERMINAL.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn launch_agent(
|
||||
app: tauri::AppHandle,
|
||||
@@ -13,6 +119,7 @@ pub(crate) fn launch_agent(
|
||||
.home_dir()
|
||||
.map_err(|error| format!("无法获取用户目录: {error}"))?;
|
||||
let config = gui_config_state.snapshot()?;
|
||||
let terminal = normalize_agent_terminal(&config.default_terminal);
|
||||
let requested_target = target
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
@@ -30,7 +137,13 @@ pub(crate) fn launch_agent(
|
||||
let executable =
|
||||
find_pi_executable(&home).ok_or_else(|| "未找到 Pi CLI 可执行文件".to_string())?;
|
||||
let launch_directory = resolve_launch_directory(working_directory.as_deref(), &home)?;
|
||||
return launch_cli_agent(&executable, PI_AGENT_NAME, &launch_directory, &[]);
|
||||
return launch_cli_agent(
|
||||
&executable,
|
||||
PI_AGENT_NAME,
|
||||
&launch_directory,
|
||||
&[],
|
||||
&terminal,
|
||||
);
|
||||
}
|
||||
|
||||
let client = AgentClient::parse(&client)?;
|
||||
@@ -73,6 +186,7 @@ pub(crate) fn launch_agent(
|
||||
client.name(),
|
||||
&launch_directory,
|
||||
environment_to_remove,
|
||||
&terminal,
|
||||
)
|
||||
}
|
||||
(_, "app") => Err(format!("{} 不支持桌面 App 启动方式", client.name())),
|
||||
@@ -602,6 +716,7 @@ fn launch_cli_agent(
|
||||
label: &str,
|
||||
working_directory: &Path,
|
||||
environment_to_remove: &[&str],
|
||||
terminal: &str,
|
||||
) -> Result<(), String> {
|
||||
let removals = environment_to_remove
|
||||
.iter()
|
||||
@@ -614,10 +729,19 @@ fn launch_cli_agent(
|
||||
removals,
|
||||
shell_single_quote(&path_to_string(executable)),
|
||||
);
|
||||
let script = format!(
|
||||
"tell application \"Terminal\"\nactivate\ndo script \"{}\"\nend tell",
|
||||
command_line.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
);
|
||||
let script = if terminal == "iterm2" {
|
||||
format!(
|
||||
"tell application \"iTerm2\"\nactivate\nset newWindow to (create window with default profile)\ntell current session of newWindow\nwrite text \"{}\"\nend tell\nend tell",
|
||||
command_line.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
)
|
||||
} else if matches!(terminal, "auto" | "terminal") {
|
||||
format!(
|
||||
"tell application \"Terminal\"\nactivate\ndo script \"{}\"\nend tell",
|
||||
command_line.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
)
|
||||
} else {
|
||||
return Err(format!("启动 {label} 失败:不支持所选终端"));
|
||||
};
|
||||
let output = Command::new("osascript")
|
||||
.arg("-e")
|
||||
.arg(script)
|
||||
@@ -637,27 +761,21 @@ fn launch_cli_agent(
|
||||
label: &str,
|
||||
working_directory: &Path,
|
||||
environment_to_remove: &[&str],
|
||||
terminal: &str,
|
||||
) -> Result<(), String> {
|
||||
let terminals: &[(&str, &[&str])] = &[
|
||||
("x-terminal-emulator", &["-e"]),
|
||||
("gnome-terminal", &["--"]),
|
||||
("konsole", &["-e"]),
|
||||
("xfce4-terminal", &["-e"]),
|
||||
("mate-terminal", &["--"]),
|
||||
("kitty", &["-e"]),
|
||||
("alacritty", &["-e"]),
|
||||
("ghostty", &["-e"]),
|
||||
("xterm", &["-e"]),
|
||||
];
|
||||
let path = env::var_os("PATH").unwrap_or_default();
|
||||
let definitions = linux_terminal_definitions();
|
||||
let mut last_error = None;
|
||||
for (terminal, arguments) in terminals {
|
||||
if !env::split_paths(&path).any(|directory| directory.join(terminal).is_file()) {
|
||||
for definition in definitions {
|
||||
let (id, program, arguments, _) = *definition;
|
||||
if terminal != "auto" && id != terminal {
|
||||
continue;
|
||||
}
|
||||
let mut command = Command::new(terminal);
|
||||
if program_on_path(program).is_none() {
|
||||
continue;
|
||||
}
|
||||
let mut command = Command::new(program);
|
||||
command
|
||||
.args(*arguments)
|
||||
.args(arguments)
|
||||
.arg(executable)
|
||||
.current_dir(working_directory)
|
||||
.stdin(Stdio::null())
|
||||
@@ -671,6 +789,9 @@ fn launch_cli_agent(
|
||||
Err(error) => last_error = Some(error.to_string()),
|
||||
}
|
||||
}
|
||||
if terminal != "auto" && last_error.is_none() {
|
||||
return Err(format!("启动 {label} 失败:未找到所选终端"));
|
||||
}
|
||||
Err(match last_error {
|
||||
Some(error) => format!("启动 {label} 失败: {error}"),
|
||||
None => format!("启动 {label} 失败:未找到可用的终端程序"),
|
||||
@@ -683,14 +804,66 @@ fn launch_cli_agent(
|
||||
label: &str,
|
||||
working_directory: &Path,
|
||||
environment_to_remove: &[&str],
|
||||
terminal: &str,
|
||||
) -> Result<(), String> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
const CREATE_NEW_CONSOLE: u32 = 0x0000_0010;
|
||||
let mut command = windows_command_for_executable(executable, true);
|
||||
command
|
||||
.current_dir(working_directory)
|
||||
.creation_flags(CREATE_NEW_CONSOLE);
|
||||
let is_batch_script = executable
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.is_some_and(|extension| {
|
||||
extension.eq_ignore_ascii_case("cmd") || extension.eq_ignore_ascii_case("bat")
|
||||
});
|
||||
|
||||
let mut command = match terminal {
|
||||
"auto" => {
|
||||
let mut command = windows_command_for_executable(executable, true);
|
||||
command
|
||||
.current_dir(working_directory)
|
||||
.creation_flags(CREATE_NEW_CONSOLE);
|
||||
command
|
||||
}
|
||||
"windows-terminal" => {
|
||||
let terminal_executable = program_on_path("wt.exe")
|
||||
.ok_or_else(|| format!("启动 {label} 失败:未找到 Windows Terminal"))?;
|
||||
let directory = path_to_string(working_directory);
|
||||
let mut command = Command::new(terminal_executable);
|
||||
command.args(["-d", &directory, "--"]);
|
||||
if is_batch_script {
|
||||
command
|
||||
.arg(windows_command_processor())
|
||||
.args(["/D", "/K", "call"])
|
||||
.arg(windows_batch_executable_argument(executable));
|
||||
} else {
|
||||
command.arg(executable);
|
||||
}
|
||||
command
|
||||
}
|
||||
"powershell" => {
|
||||
let mut command = Command::new(windows_powershell_executable());
|
||||
let script = format!(
|
||||
"Set-Location -LiteralPath {}; & {}",
|
||||
windows_powershell_single_quoted_literal(&path_to_string(working_directory)),
|
||||
windows_powershell_single_quoted_literal(&path_to_string(executable)),
|
||||
);
|
||||
command.args(["-NoLogo", "-NoProfile", "-NoExit", "-Command", &script]);
|
||||
command.creation_flags(CREATE_NEW_CONSOLE);
|
||||
command
|
||||
}
|
||||
"cmd" => {
|
||||
let mut command = Command::new(windows_command_processor());
|
||||
let command_line = format!(
|
||||
"cd /d \"{}\" && call \"{}\"",
|
||||
path_to_string(working_directory).replace('"', "\"\""),
|
||||
path_to_string(executable).replace('"', "\"\"")
|
||||
);
|
||||
command.args(["/D", "/K", &command_line]);
|
||||
command.creation_flags(CREATE_NEW_CONSOLE);
|
||||
command
|
||||
}
|
||||
_ => return Err(format!("启动 {label} 失败:不支持所选终端")),
|
||||
};
|
||||
for key in environment_to_remove {
|
||||
command.env_remove(key);
|
||||
}
|
||||
@@ -706,6 +879,7 @@ fn launch_cli_agent(
|
||||
label: &str,
|
||||
_working_directory: &Path,
|
||||
_environment_to_remove: &[&str],
|
||||
_terminal: &str,
|
||||
) -> Result<(), String> {
|
||||
Err(format!("当前平台不支持启动 {label}"))
|
||||
}
|
||||
@@ -714,6 +888,15 @@ fn launch_cli_agent(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unknown_terminal_defaults_to_automatic() {
|
||||
assert_eq!(
|
||||
normalize_agent_terminal("missing-terminal"),
|
||||
DEFAULT_AGENT_TERMINAL
|
||||
);
|
||||
assert_eq!(normalize_agent_terminal(" auto "), "auto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_launch_directory_is_rejected() {
|
||||
let error = resolve_launch_directory(Some("relative/project"), Path::new("/fallback"))
|
||||
|
||||
@@ -180,6 +180,8 @@ pub(crate) fn software_settings(
|
||||
autostart_enabled: app_autostart_enabled(app)?,
|
||||
start_core_on_launch: config.start_core_on_launch,
|
||||
silent_start_enabled: config.silent_start,
|
||||
default_terminal: normalize_agent_terminal(&config.default_terminal),
|
||||
available_terminals: available_agent_terminals(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -201,6 +203,7 @@ pub(crate) fn save_software_settings(
|
||||
let previous_config = gui_config_state.snapshot()?;
|
||||
let previous_autostart_enabled = app_autostart_enabled(&app)?;
|
||||
let autostart_changed = previous_autostart_enabled != settings.autostart_enabled;
|
||||
let default_terminal = normalize_agent_terminal(&settings.default_terminal);
|
||||
|
||||
if autostart_changed {
|
||||
set_app_autostart_enabled(&app, settings.autostart_enabled)?;
|
||||
@@ -209,6 +212,7 @@ pub(crate) fn save_software_settings(
|
||||
let config = if previous_config.close_behavior == settings.close_behavior
|
||||
&& previous_config.start_core_on_launch == settings.start_core_on_launch
|
||||
&& previous_config.silent_start == settings.silent_start_enabled
|
||||
&& previous_config.default_terminal == default_terminal
|
||||
{
|
||||
previous_config
|
||||
} else {
|
||||
@@ -216,6 +220,7 @@ pub(crate) fn save_software_settings(
|
||||
settings.close_behavior,
|
||||
settings.start_core_on_launch,
|
||||
settings.silent_start_enabled,
|
||||
default_terminal.clone(),
|
||||
) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
|
||||
@@ -1237,6 +1237,9 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
|
||||
if presence.silent_start.is_none() {
|
||||
changed = true;
|
||||
}
|
||||
if presence.default_terminal.is_none() {
|
||||
changed = true;
|
||||
}
|
||||
if presence.download_source.is_none() {
|
||||
config.download_source = if config.prefer_gitcode_downloads {
|
||||
VersionDownloadSource::Gitcode
|
||||
@@ -1493,6 +1496,11 @@ pub(crate) fn sanitize_gui_config(config: &mut GuiConfigFile) -> Result<bool, St
|
||||
config.locale = normalized_locale.to_string();
|
||||
changed = true;
|
||||
}
|
||||
let default_terminal = normalize_agent_terminal(&config.default_terminal);
|
||||
if config.default_terminal != default_terminal {
|
||||
config.default_terminal = default_terminal;
|
||||
changed = true;
|
||||
}
|
||||
let host = config.host.trim();
|
||||
let host = if host.is_empty() {
|
||||
if config.allow_lan {
|
||||
@@ -1642,6 +1650,7 @@ pub(crate) fn write_gui_config_to_path(
|
||||
("start-core-on-launch", value(config.start_core_on_launch)),
|
||||
("silent-start", value(config.silent_start)),
|
||||
("close-behavior", value(config.close_behavior.as_str())),
|
||||
("default-terminal", value(config.default_terminal.as_str())),
|
||||
("auth-dir", value(config.auth_dir.as_str())),
|
||||
(
|
||||
"management-secret-key",
|
||||
|
||||
@@ -131,6 +131,7 @@ const LEGACY_DEFAULT_MAIN_WINDOW_HEIGHT: u32 = 891;
|
||||
const OAUTH_DIR_NAME: &str = "oauth";
|
||||
const DEFAULT_AUTH_DIR: &str = "../oauth";
|
||||
const DEFAULT_API_KEY: &str = "123456";
|
||||
const DEFAULT_AGENT_TERMINAL: &str = "auto";
|
||||
const DEFAULT_API_KEY_INITIAL_REMARK: &str = "默认密钥";
|
||||
const DEFAULT_REQUEST_RETRY: u32 = 3;
|
||||
const DEFAULT_MAX_RETRY_CREDENTIALS: u32 = 0;
|
||||
@@ -584,6 +585,7 @@ struct GuiConfigFile {
|
||||
start_core_on_launch: bool,
|
||||
silent_start: bool,
|
||||
close_behavior: WindowsCloseBehavior,
|
||||
default_terminal: String,
|
||||
window_width: Option<u32>,
|
||||
window_height: Option<u32>,
|
||||
auth_dir: String,
|
||||
@@ -868,6 +870,7 @@ impl Default for GuiConfigFile {
|
||||
start_core_on_launch: true,
|
||||
silent_start: false,
|
||||
close_behavior: WindowsCloseBehavior::Ask,
|
||||
default_terminal: DEFAULT_AGENT_TERMINAL.to_string(),
|
||||
window_width: Some(DEFAULT_MAIN_WINDOW_WIDTH),
|
||||
window_height: Some(DEFAULT_MAIN_WINDOW_HEIGHT),
|
||||
auth_dir: DEFAULT_AUTH_DIR.to_string(),
|
||||
@@ -906,6 +909,7 @@ struct GuiConfigPresence {
|
||||
api_access_remarks: Option<Vec<GuiApiAccessRemark>>,
|
||||
management_secret_key: Option<String>,
|
||||
close_behavior: Option<WindowsCloseBehavior>,
|
||||
default_terminal: Option<String>,
|
||||
start_core_on_launch: Option<bool>,
|
||||
silent_start: Option<bool>,
|
||||
usage_statistics_enabled: Option<bool>,
|
||||
@@ -949,6 +953,8 @@ struct SoftwareSettings {
|
||||
autostart_enabled: bool,
|
||||
start_core_on_launch: bool,
|
||||
silent_start_enabled: bool,
|
||||
default_terminal: String,
|
||||
available_terminals: Vec<AgentTerminalOption>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -958,6 +964,7 @@ struct SoftwareSettingsInput {
|
||||
autostart_enabled: bool,
|
||||
start_core_on_launch: bool,
|
||||
silent_start_enabled: bool,
|
||||
default_terminal: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
@@ -1911,11 +1918,13 @@ impl GuiConfigState {
|
||||
close_behavior: WindowsCloseBehavior,
|
||||
start_core_on_launch: bool,
|
||||
silent_start: bool,
|
||||
default_terminal: String,
|
||||
) -> Result<GuiConfigFile, String> {
|
||||
self.update(|config| {
|
||||
config.close_behavior = close_behavior;
|
||||
config.start_core_on_launch = start_core_on_launch;
|
||||
config.silent_start = silent_start;
|
||||
config.default_terminal = normalize_agent_terminal(&default_terminal);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ fn gui_config_defaults_are_stable() {
|
||||
assert!(content.contains("start-core-on-launch = true"));
|
||||
assert!(content.contains("silent-start = false"));
|
||||
assert!(content.contains("close-behavior = \"ask\""));
|
||||
assert!(content.contains("default-terminal = \"auto\""));
|
||||
assert_eq!(config.default_terminal, DEFAULT_AGENT_TERMINAL);
|
||||
assert!(content.contains("window-width = 1280"));
|
||||
assert!(content.contains("window-height = 800"));
|
||||
assert!(content.contains("auth-dir = \"../oauth\""));
|
||||
|
||||
@@ -1023,6 +1023,7 @@ fn startup_preserves_all_user_owned_yaml_and_only_applies_gui_managed_values() {
|
||||
start_core_on_launch: true,
|
||||
silent_start: false,
|
||||
close_behavior: WindowsCloseBehavior::Ask,
|
||||
default_terminal: DEFAULT_AGENT_TERMINAL.to_string(),
|
||||
window_width: None,
|
||||
window_height: None,
|
||||
auth_dir: path_to_string(&fixed_oauth_dir().unwrap()),
|
||||
|
||||
@@ -3763,6 +3763,7 @@ mod tests {
|
||||
start_core_on_launch: true,
|
||||
silent_start: false,
|
||||
close_behavior: crate::WindowsCloseBehavior::Ask,
|
||||
default_terminal: crate::DEFAULT_AGENT_TERMINAL.to_string(),
|
||||
window_width: None,
|
||||
window_height: None,
|
||||
auth_dir: String::new(),
|
||||
|
||||
@@ -372,6 +372,9 @@ export const en: Record<MessageKey, string> = {
|
||||
'config.software.startCoreOnLaunchDescription': 'Automatically start the CPA core whenever EasyCLIProxyAPI launches',
|
||||
'config.software.silentStart': 'Silent Launch',
|
||||
'config.software.silentStartDescription': 'Start without showing the main window and run directly in the system tray',
|
||||
'config.software.defaultTerminal': 'Default terminal',
|
||||
'config.software.defaultTerminalDescription': 'Terminal program used when launching CLI agents',
|
||||
'config.software.terminal.auto': 'Automatic',
|
||||
'config.software.closeBehavior': 'Close behavior',
|
||||
'config.software.closeBehaviorDescription': 'Choose whether closing the window exits the app or keeps it running in the system tray',
|
||||
'config.software.behavior.ask': 'Ask every time',
|
||||
|
||||
@@ -370,6 +370,9 @@ export const zhCN = {
|
||||
'config.software.startCoreOnLaunchDescription': '每次启动 EasyCLIProxyAPI 时自动启动 CPA 内核',
|
||||
'config.software.silentStart': '静默启动',
|
||||
'config.software.silentStartDescription': '启动应用时不显示主窗口,直接在系统托盘运行',
|
||||
'config.software.defaultTerminal': '默认终端',
|
||||
'config.software.defaultTerminalDescription': '启动 CLI 智能体时使用的终端程序',
|
||||
'config.software.terminal.auto': '自动选择',
|
||||
'config.software.closeBehavior': '关闭行为',
|
||||
'config.software.closeBehaviorDescription': '决定关闭窗口时是退出程序还是继续在系统托盘运行',
|
||||
'config.software.behavior.ask': '每次询问',
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
Power,
|
||||
Terminal,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
@@ -79,11 +80,18 @@ type DraftRefreshMode = 'replace' | 'preserve';
|
||||
|
||||
type NetworkDraftDirty = Record<NetworkDraftField, boolean>;
|
||||
|
||||
type AgentTerminalOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
type SoftwareSettings = {
|
||||
closeBehavior: CloseBehavior;
|
||||
autostartEnabled: boolean;
|
||||
startCoreOnLaunch: boolean;
|
||||
silentStartEnabled: boolean;
|
||||
defaultTerminal: string;
|
||||
availableTerminals: AgentTerminalOption[];
|
||||
};
|
||||
|
||||
type CoreTlsSettings = {
|
||||
@@ -119,6 +127,7 @@ export function ConfigPanelPage() {
|
||||
const [softwareAutostartDraft, setSoftwareAutostartDraft] = useState(false);
|
||||
const [softwareStartCoreDraft, setSoftwareStartCoreDraft] = useState(true);
|
||||
const [softwareSilentStartDraft, setSoftwareSilentStartDraft] = useState(false);
|
||||
const [softwareDefaultTerminalDraft, setSoftwareDefaultTerminalDraft] = useState('auto');
|
||||
const [softwareSavedStatusVisible, setSoftwareSavedStatusVisible] = useState(false);
|
||||
const [tlsSettings, setTlsSettings] = useState<CoreTlsSettings | null>(null);
|
||||
const [tlsSettingsLoading, setTlsSettingsLoading] = useState(true);
|
||||
@@ -264,6 +273,7 @@ export function ConfigPanelPage() {
|
||||
setSoftwareAutostartDraft(result.autostartEnabled);
|
||||
setSoftwareStartCoreDraft(result.startCoreOnLaunch);
|
||||
setSoftwareSilentStartDraft(result.silentStartEnabled);
|
||||
setSoftwareDefaultTerminalDraft(result.defaultTerminal);
|
||||
} catch (error) {
|
||||
setSoftwareSettings(null);
|
||||
showNotice(t('config.error.saveFailed', { error: String(error) }), 'error');
|
||||
@@ -690,6 +700,7 @@ export function ConfigPanelPage() {
|
||||
&& softwareAutostartDraft === softwareSettings.autostartEnabled
|
||||
&& softwareStartCoreDraft === softwareSettings.startCoreOnLaunch
|
||||
&& softwareSilentStartDraft === softwareSettings.silentStartEnabled
|
||||
&& softwareDefaultTerminalDraft === softwareSettings.defaultTerminal
|
||||
) return;
|
||||
|
||||
setBusyAction('software');
|
||||
@@ -700,6 +711,7 @@ export function ConfigPanelPage() {
|
||||
autostartEnabled: softwareAutostartDraft,
|
||||
startCoreOnLaunch: softwareStartCoreDraft,
|
||||
silentStartEnabled: softwareSilentStartDraft,
|
||||
defaultTerminal: softwareDefaultTerminalDraft,
|
||||
},
|
||||
});
|
||||
setSoftwareSettings(result);
|
||||
@@ -707,6 +719,7 @@ export function ConfigPanelPage() {
|
||||
setSoftwareAutostartDraft(result.autostartEnabled);
|
||||
setSoftwareStartCoreDraft(result.startCoreOnLaunch);
|
||||
setSoftwareSilentStartDraft(result.silentStartEnabled);
|
||||
setSoftwareDefaultTerminalDraft(result.defaultTerminal);
|
||||
setSoftwareSavedStatusVisible(true);
|
||||
showNotice(t('config.notice.softwareUpdated'), 'success');
|
||||
} catch (error) {
|
||||
@@ -714,6 +727,7 @@ export function ConfigPanelPage() {
|
||||
setSoftwareAutostartDraft(softwareSettings.autostartEnabled);
|
||||
setSoftwareStartCoreDraft(softwareSettings.startCoreOnLaunch);
|
||||
setSoftwareSilentStartDraft(softwareSettings.silentStartEnabled);
|
||||
setSoftwareDefaultTerminalDraft(softwareSettings.defaultTerminal);
|
||||
setSoftwareSavedStatusVisible(false);
|
||||
showNotice(t('config.error.saveFailed', { error: String(error) }), 'error');
|
||||
void loadSoftwareSettings();
|
||||
@@ -746,10 +760,13 @@ export function ConfigPanelPage() {
|
||||
&& softwareSilentStartDraft !== softwareSettings.silentStartEnabled;
|
||||
const softwareStartCoreDirty = softwareSettings !== null
|
||||
&& softwareStartCoreDraft !== softwareSettings.startCoreOnLaunch;
|
||||
const softwareDefaultTerminalDirty = softwareSettings !== null
|
||||
&& softwareDefaultTerminalDraft !== softwareSettings.defaultTerminal;
|
||||
const softwareSettingsDirty = softwareCloseBehaviorDirty
|
||||
|| softwareAutostartDirty
|
||||
|| softwareStartCoreDirty
|
||||
|| softwareSilentStartDirty;
|
||||
|| softwareSilentStartDirty
|
||||
|| softwareDefaultTerminalDirty;
|
||||
const softwareStatusLabel = softwareSettingsLoading
|
||||
? t('common.loading')
|
||||
: softwareSettings === null
|
||||
@@ -1660,6 +1677,35 @@ export function ConfigPanelPage() {
|
||||
<span className="switch-track" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="config-software-setting-row config-software-close-row">
|
||||
<div className="config-software-setting-copy">
|
||||
<span className="config-software-setting-icon" aria-hidden="true">
|
||||
<Terminal size={18} />
|
||||
</span>
|
||||
<div>
|
||||
<strong>{t('config.software.defaultTerminal')}</strong>
|
||||
<small>{t('config.software.defaultTerminalDescription')}</small>
|
||||
</div>
|
||||
</div>
|
||||
<label className="config-software-select">
|
||||
<span className="sr-only">{t('config.software.defaultTerminal')}</span>
|
||||
<select
|
||||
className="config-network-input"
|
||||
value={softwareDefaultTerminalDraft}
|
||||
disabled={softwareSettingsLoading || softwareSettings === null || busyAction !== null}
|
||||
onChange={(event) => {
|
||||
setSoftwareSavedStatusVisible(false);
|
||||
setSoftwareDefaultTerminalDraft(event.currentTarget.value);
|
||||
}}
|
||||
>
|
||||
{(softwareSettings?.availableTerminals ?? []).map((terminal) => (
|
||||
<option value={terminal.id} key={terminal.id}>
|
||||
{terminal.id === 'auto' ? t('config.software.terminal.auto') : terminal.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="config-software-setting-row config-software-close-row">
|
||||
<div className="config-software-setting-copy">
|
||||
<span className="config-software-setting-icon" aria-hidden="true">
|
||||
|
||||
Reference in new issue
Block a user