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:
lzt404 committed 2026-08-30 18:48:01 +08:00
1 parent 270ad2869c
commit 79b50b7a2b
10 files changed
+288 -26

No files matched your search

+3
View File
@@ -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',
+3
View File
@@ -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': '每次询问',
+47 -1
View File
@@ -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">