From 48da5ccbfae19091e7f963dc2bb744c8a865de2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Thu, 23 Apr 2026 11:06:32 +0800 Subject: [PATCH] Reduce Settings complexity by removing the embedded shell surface The Settings terminal added a full xterm plus Tauri PTY stack for a job that is better handled by dedicated install and configuration flows. This change removes the Settings tab, frontend terminal wiring, Tauri terminal commands, and the terminal-only dependencies so the desktop settings surface stays narrower and less fragile. Constraint: The worktree already contains unrelated desktop icon and UI changes, so this commit stages only the terminal-removal slice Rejected: Keep a hidden or runtime-only terminal stub | it would still preserve the heavy cross-layer maintenance surface Rejected: Remove only the Settings tab and leave the Tauri PTY backend | that would leave dead code and unused dependencies behind Confidence: high Scope-risk: narrow Reversibility: clean Directive: If future install workflows need more power, prefer Settings-native actions and runtime refresh over reintroducing a general shell tab Tested: bun x vitest run src/__tests__/generalSettings.test.tsx src/__tests__/skillsSettings.test.tsx src/__tests__/pluginsSettings.test.tsx src/__tests__/agentsSettings.test.tsx src/__tests__/mcpSettings.test.tsx src/components/settings/InstallCenter.test.tsx; bun run lint; bun run build; cargo check --manifest-path desktop/src-tauri/Cargo.toml Not-tested: Manual packaged desktop app click-through after removing the Settings terminal tab --- desktop/bun.lock | 6 - desktop/package.json | 2 - desktop/src-tauri/Cargo.lock | 101 +--- desktop/src-tauri/Cargo.toml | 1 - desktop/src-tauri/src/lib.rs | 31 +- desktop/src-tauri/src/terminal.rs | 522 ------------------ desktop/src/__tests__/agentsSettings.test.tsx | 3 + .../src/__tests__/generalSettings.test.tsx | 9 + .../src/__tests__/pluginsSettings.test.tsx | 3 + desktop/src/__tests__/skillsSettings.test.tsx | 3 + desktop/src/__tests__/terminalPanel.test.tsx | 27 - .../settings/TerminalPanel.restart.test.tsx | 127 ----- .../src/components/settings/TerminalPanel.tsx | 422 -------------- desktop/src/i18n/locales/en.ts | 24 +- desktop/src/i18n/locales/zh.ts | 24 +- desktop/src/lib/settingsTerminal.ts | 56 -- desktop/src/pages/Settings.tsx | 3 - 17 files changed, 28 insertions(+), 1336 deletions(-) delete mode 100644 desktop/src-tauri/src/terminal.rs delete mode 100644 desktop/src/__tests__/terminalPanel.test.tsx delete mode 100644 desktop/src/components/settings/TerminalPanel.restart.test.tsx delete mode 100644 desktop/src/components/settings/TerminalPanel.tsx delete mode 100644 desktop/src/lib/settingsTerminal.ts diff --git a/desktop/bun.lock b/desktop/bun.lock index 13c4092d..52d20da9 100644 --- a/desktop/bun.lock +++ b/desktop/bun.lock @@ -7,8 +7,6 @@ "dependencies": { "@tailwindcss/typography": "^0.5.19", "@types/dompurify": "^3.2.0", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", "dompurify": "^3.3.3", "lucide-react": "^0.469.0", "marked": "^15.0.7", @@ -472,10 +470,6 @@ "@vitest/utils": ["@vitest/utils@3.2.4", "https://registry.npmmirror.com/@vitest/utils/-/utils-3.2.4.tgz", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "https://registry.npmmirror.com/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="], - - "@xterm/xterm": ["@xterm/xterm@6.0.0", "https://registry.npmmirror.com/@xterm/xterm/-/xterm-6.0.0.tgz", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="], - "acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], diff --git a/desktop/package.json b/desktop/package.json index 7cd33e83..b43d7818 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -18,8 +18,6 @@ "dependencies": { "@tailwindcss/typography": "^0.5.19", "@types/dompurify": "^3.2.0", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", "dompurify": "^3.3.3", "lucide-react": "^0.469.0", "marked": "^15.0.7", diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 8933a60e..addd41eb 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -309,12 +309,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "chrono" version = "0.4.44" @@ -332,7 +326,6 @@ name = "claude-code-desktop" version = "0.1.5" dependencies = [ "anyhow", - "portable-pty", "serde", "serde_json", "tauri", @@ -683,12 +676,6 @@ dependencies = [ "tendril 0.5.0", ] -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - [[package]] name = "dpi" version = "0.1.2" @@ -736,7 +723,7 @@ dependencies = [ "rustc_version", "toml 0.9.12+spec-1.1.0", "vswhom", - "winreg 0.55.0", + "winreg", ] [[package]] @@ -806,17 +793,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] - [[package]] name = "filetime" version = "0.2.27" @@ -1796,12 +1772,6 @@ dependencies = [ "selectors 0.24.0", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -2040,18 +2010,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nodrop" version = "0.1.14" @@ -2576,27 +2534,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "portable-pty" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "downcast-rs", - "filedescriptor", - "lazy_static", - "libc", - "log", - "nix", - "serial2", - "shared_library", - "shell-words", - "winapi", - "winreg 0.10.1", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -3355,17 +3292,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "serial2" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdbc46aa3882ec3d48ec2b5abcb4f0d863a13d7599265f3faa6d851f23c12f3" -dependencies = [ - "cfg-if", - "libc", - "winapi", -] - [[package]] name = "serialize-to-javascript" version = "0.1.2" @@ -3429,22 +3355,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "shared_library" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" -dependencies = [ - "lazy_static", - "libc", -] - -[[package]] -name = "shell-words" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" - [[package]] name = "shlex" version = "1.3.0" @@ -5333,15 +5243,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "winreg" version = "0.55.0" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index c1feff6a..19eee268 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -18,5 +18,4 @@ tauri-plugin-process = "2" tauri-plugin-updater = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -portable-pty = "0.9.0" anyhow = "1.0.102" diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index cdfa5a5e..7619ed43 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -16,13 +16,6 @@ use tauri_plugin_shell::{ ShellExt, }; -mod terminal; - -use terminal::{ - terminal_close, terminal_resize, terminal_start_session, terminal_write, stop_all_sessions, - TerminalState, -}; - #[derive(Default)] struct ServerState(Mutex); @@ -121,8 +114,7 @@ fn resolve_app_root(_app: &AppHandle) -> Result { // 我们直接用当前可执行文件所在目录作为 app_root: // Dev: desktop/src-tauri/target// (rust 跑出来的 binary 那一层) // Prod: .app/Contents/MacOS/ (sidecar 二进制的同级目录) - let exe = std::env::current_exe() - .map_err(|err| format!("resolve current exe path: {err}"))?; + let exe = std::env::current_exe().map_err(|err| format!("resolve current exe path: {err}"))?; let dir = exe .parent() .ok_or_else(|| "current exe has no parent dir".to_string())? @@ -305,26 +297,18 @@ pub fn run() { let builder = tauri::Builder::default() .manage(ServerState::default()) .manage(AdapterState::default()) - .manage(TerminalState::default()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_updater::Builder::new().build()) - .invoke_handler(tauri::generate_handler![ - get_server_url, - restart_adapters_sidecar, - terminal_start_session, - terminal_write, - terminal_resize, - terminal_close - ]); + .invoke_handler(tauri::generate_handler![get_server_url, restart_adapters_sidecar]); // macOS: native menu bar (traffic-light overlay style) #[cfg(target_os = "macos")] let builder = builder .menu(|app| { - let about_item = MenuItemBuilder::with_id("nav_about", "关于 Claude Code Haha") - .build(app)?; + let about_item = + MenuItemBuilder::with_id("nav_about", "关于 Claude Code Haha").build(app)?; let settings_item = MenuItemBuilder::with_id("nav_settings", "设置...") .accelerator("CmdOrCtrl+,") .build(app)?; @@ -353,9 +337,7 @@ pub fn run() { .select_all() .build()?; - let view_submenu = SubmenuBuilder::new(app, "View") - .fullscreen() - .build()?; + let view_submenu = SubmenuBuilder::new(app, "View").fullscreen().build()?; let window_submenu = SubmenuBuilder::new(app, "Window") .minimize() @@ -415,9 +397,6 @@ pub fn run() { if matches!(event, RunEvent::Exit | RunEvent::ExitRequested { .. }) { stop_server_sidecar(app_handle); stop_adapters_sidecar(app_handle); - if let Some(state) = app_handle.try_state::() { - stop_all_sessions(&state); - } } }); } diff --git a/desktop/src-tauri/src/terminal.rs b/desktop/src-tauri/src/terminal.rs deleted file mode 100644 index af01943b..00000000 --- a/desktop/src-tauri/src/terminal.rs +++ /dev/null @@ -1,522 +0,0 @@ -use std::{ - collections::HashMap, - fs, - io::{Read, Write}, - path::{Path, PathBuf}, - sync::Mutex, - thread, - time::{SystemTime, UNIX_EPOCH}, -}; - -use anyhow::{anyhow, Context, Result}; -use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize}; -use serde::Serialize; -use tauri::{AppHandle, Emitter, State}; - -const TERMINAL_EVENT: &str = "desktop-terminal-event"; - -pub struct TerminalSession { - master: Box, - writer: Box, - child: Box, - cleanup_paths: Vec, -} - -#[derive(Default)] -pub struct TerminalState(pub Mutex>); - -#[derive(Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct TerminalStartResponse { - pub session_id: String, - pub shell: String, - pub cwd: String, - pub explicit_command_name: String, - pub docs_command_name: String, -} - -#[derive(Clone, Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum TerminalEventPayload { - Output { session_id: String, data: String }, - Exit { session_id: String, exit_code: Option }, -} - -#[tauri::command] -pub fn terminal_start_session( - app: AppHandle, - state: State<'_, TerminalState>, - cwd: Option, -) -> Result { - start_session(&app, &state, cwd).map_err(|err| err.to_string()) -} - -#[tauri::command] -pub fn terminal_write( - state: State<'_, TerminalState>, - session_id: String, - data: String, -) -> Result<(), String> { - let mut guard = state - .0 - .lock() - .map_err(|_| "terminal state is unavailable".to_string())?; - let session = guard - .get_mut(&session_id) - .ok_or_else(|| format!("terminal session not found: {session_id}"))?; - - session - .writer - .write_all(data.as_bytes()) - .and_then(|_| session.writer.flush()) - .map_err(|err| format!("write terminal input: {err}")) -} - -#[tauri::command] -pub fn terminal_resize( - state: State<'_, TerminalState>, - session_id: String, - cols: u16, - rows: u16, -) -> Result<(), String> { - if cols == 0 || rows == 0 { - return Ok(()); - } - - let mut guard = state - .0 - .lock() - .map_err(|_| "terminal state is unavailable".to_string())?; - let session = guard - .get_mut(&session_id) - .ok_or_else(|| format!("terminal session not found: {session_id}"))?; - - session - .master - .resize(PtySize { - rows, - cols, - pixel_width: 0, - pixel_height: 0, - }) - .map_err(|err| format!("resize terminal: {err}")) -} - -#[tauri::command] -pub fn terminal_close( - state: State<'_, TerminalState>, - session_id: String, -) -> Result<(), String> { - let session = { - let mut guard = state - .0 - .lock() - .map_err(|_| "terminal state is unavailable".to_string())?; - guard.remove(&session_id) - }; - - if let Some(session) = session { - thread::spawn(move || terminate_terminal_session(session)); - } - - Ok(()) -} - -pub fn stop_all_sessions(state: &State<'_, TerminalState>) { - let sessions = if let Ok(mut guard) = state.0.lock() { - guard.drain().map(|(_, session)| session).collect::>() - } else { - Vec::new() - }; - - for session in sessions { - terminate_terminal_session(session); - } -} - -fn cleanup_terminal_session(session: &mut TerminalSession) { - for path in session.cleanup_paths.drain(..) { - let _ = fs::remove_file(path); - } -} - -fn terminate_terminal_session(mut session: TerminalSession) { - let _ = session.child.kill(); - let _ = session.child.wait(); - cleanup_terminal_session(&mut session); -} - -fn start_session( - app: &AppHandle, - state: &State<'_, TerminalState>, - cwd: Option, -) -> Result { - let requested_cwd = resolve_terminal_cwd(cwd)?; - let session_id = build_terminal_session_id(); - let cli_binary = resolve_sidecar_binary_path(app)?; - let app_root = resolve_app_root()?; - - let pty_system = native_pty_system(); - let pair = pty_system - .openpty(PtySize { - rows: 24, - cols: 100, - pixel_width: 0, - pixel_height: 0, - }) - .context("open pty")?; - - let shell_bootstrap = build_shell_bootstrap( - &session_id, - &requested_cwd, - &cli_binary, - &app_root, - )?; - - let mut cmd = CommandBuilder::new(&shell_bootstrap.program); - cmd.cwd(&requested_cwd); - for arg in &shell_bootstrap.args { - cmd.arg(arg); - } - for (key, value) in &shell_bootstrap.env { - cmd.env(key, value); - } - - let reader = pair - .master - .try_clone_reader() - .context("clone pty reader")?; - let writer = pair.master.take_writer().context("take pty writer")?; - let child = pair - .slave - .spawn_command(cmd) - .context("spawn shell in pty")?; - - let output_session_id = session_id.clone(); - let output_app = app.clone(); - thread::spawn(move || stream_terminal_output(output_app, output_session_id, reader)); - - let mut guard = state - .0 - .lock() - .map_err(|_| anyhow!("terminal state is unavailable"))?; - guard.insert( - session_id.clone(), - TerminalSession { - master: pair.master, - writer, - child, - cleanup_paths: shell_bootstrap.cleanup_paths, - }, - ); - - Ok(TerminalStartResponse { - session_id, - shell: shell_bootstrap.display_name, - cwd: requested_cwd.to_string_lossy().to_string(), - explicit_command_name: "claude-haha".to_string(), - docs_command_name: "claude".to_string(), - }) -} - -fn stream_terminal_output( - app: AppHandle, - session_id: String, - mut reader: Box, -) { - let mut buffer = [0_u8; 8192]; - - loop { - match reader.read(&mut buffer) { - Ok(0) => { - let _ = app.emit( - TERMINAL_EVENT, - TerminalEventPayload::Exit { - session_id: session_id.clone(), - exit_code: None, - }, - ); - break; - } - Ok(read_bytes) => { - let data = String::from_utf8_lossy(&buffer[..read_bytes]).to_string(); - let _ = app.emit( - TERMINAL_EVENT, - TerminalEventPayload::Output { - session_id: session_id.clone(), - data, - }, - ); - } - Err(_) => { - let _ = app.emit( - TERMINAL_EVENT, - TerminalEventPayload::Exit { - session_id: session_id.clone(), - exit_code: None, - }, - ); - break; - } - } - } -} - -struct ShellBootstrap { - program: String, - args: Vec, - env: Vec<(String, String)>, - cleanup_paths: Vec, - display_name: String, -} - -fn build_shell_bootstrap( - session_id: &str, - cwd: &Path, - cli_binary: &Path, - app_root: &Path, -) -> Result { - #[cfg(target_os = "windows")] - { - let bootstrap = build_powershell_bootstrap(cwd, cli_binary, app_root); - return Ok(ShellBootstrap { - program: "powershell.exe".to_string(), - args: vec![ - "-NoLogo".to_string(), - "-NoExit".to_string(), - "-NoProfile".to_string(), - "-Command".to_string(), - bootstrap, - ], - env: vec![ - ("TERM".to_string(), "xterm-256color".to_string()), - ("CLAUDE_APP_ROOT".to_string(), app_root.to_string_lossy().to_string()), - ], - cleanup_paths: Vec::new(), - display_name: "PowerShell".to_string(), - }); - } - - #[cfg(not(target_os = "windows"))] - { - let shell_path = resolve_unix_shell()?; - let rc_path = write_bash_rc_file(session_id, cwd, cli_binary, app_root)?; - return Ok(ShellBootstrap { - program: shell_path.to_string_lossy().to_string(), - args: vec![ - "--noprofile".to_string(), - "--rcfile".to_string(), - rc_path.to_string_lossy().to_string(), - "-i".to_string(), - ], - env: vec![ - ("TERM".to_string(), "xterm-256color".to_string()), - ("COLORTERM".to_string(), "truecolor".to_string()), - ("CLAUDE_APP_ROOT".to_string(), app_root.to_string_lossy().to_string()), - ], - cleanup_paths: vec![rc_path], - display_name: "bash".to_string(), - }) - } -} - -#[cfg(not(target_os = "windows"))] -fn resolve_unix_shell() -> Result { - for candidate in ["/bin/bash", "/usr/bin/bash"] { - let path = PathBuf::from(candidate); - if path.exists() { - return Ok(path); - } - } - - Err(anyhow!("bash is not available on this machine")) -} - -#[cfg(not(target_os = "windows"))] -fn write_bash_rc_file( - session_id: &str, - cwd: &Path, - cli_binary: &Path, - app_root: &Path, -) -> Result { - let rc_path = std::env::temp_dir().join(format!("cc-haha-terminal-{session_id}.bashrc")); - let home_rc = std::env::var("HOME") - .ok() - .map(PathBuf::from) - .map(|home| home.join(".bashrc")); - - let source_user_rc = if let Some(path) = home_rc.filter(|candidate| candidate.exists()) { - format!("source {} >/dev/null 2>&1 || true\n", quote_for_bash(&path.to_string_lossy())) - } else { - String::new() - }; - - let cli_path = quote_for_bash(&cli_binary.to_string_lossy()); - let app_root_path = quote_for_bash(&app_root.to_string_lossy()); - let cwd_path = quote_for_bash(&cwd.to_string_lossy()); - - let rc_content = format!( - "{source_user_rc}export CLAUDE_APP_ROOT={app_root_path}\n\ -export PS1='\\w $ '\n\ -function claude-haha() {{\n {cli_path} cli --app-root \"$CLAUDE_APP_ROOT\" \"$@\"\n}}\n\ -function claude() {{\n claude-haha \"$@\"\n}}\n\ -cd {cwd_path}\n" - ); - - fs::write(&rc_path, rc_content).context("write terminal rc file")?; - Ok(rc_path) -} - -#[cfg(target_os = "windows")] -fn build_powershell_bootstrap(cwd: &Path, cli_binary: &Path, app_root: &Path) -> String { - let cli = quote_for_powershell(&cli_binary.to_string_lossy()); - let root = quote_for_powershell(&app_root.to_string_lossy()); - let cwd_value = quote_for_powershell(&cwd.to_string_lossy()); - - format!( - "$env:CLAUDE_APP_ROOT = '{root}'; \ -function global:claude-haha {{ & '{cli}' cli --app-root $env:CLAUDE_APP_ROOT @Args }}; \ -function global:claude {{ claude-haha @Args }}; \ -Set-Location -LiteralPath '{cwd_value}'" - ) -} - -fn resolve_terminal_cwd(requested: Option) -> Result { - let path = requested - .filter(|value| !value.trim().is_empty()) - .map(PathBuf::from) - .unwrap_or_else(default_home_dir); - - if !path.exists() { - return Err(anyhow!( - "terminal working directory does not exist: {}", - path.to_string_lossy() - )); - } - - if !path.is_dir() { - return Err(anyhow!( - "terminal working directory is not a directory: {}", - path.to_string_lossy() - )); - } - - Ok(path) -} - -fn default_home_dir() -> PathBuf { - #[cfg(target_os = "windows")] - { - std::env::var_os("USERPROFILE") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("C:\\")) - } - - #[cfg(not(target_os = "windows"))] - { - std::env::var_os("HOME") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("/")) - } -} - -fn resolve_app_root() -> Result { - let exe = std::env::current_exe().context("resolve current exe path")?; - let dir = exe - .parent() - .ok_or_else(|| anyhow!("current exe has no parent dir"))?; - Ok(dir.to_path_buf()) -} - -fn resolve_sidecar_binary_path(_app: &AppHandle) -> Result { - let current_exe = std::env::current_exe().context("resolve current exe path")?; - let current_dir = current_exe - .parent() - .ok_or_else(|| anyhow!("current exe has no parent dir"))?; - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let binaries_dir = manifest_dir.join("binaries"); - - for candidate in sidecar_name_candidates() { - let sibling = current_dir.join(&candidate); - if sibling.exists() { - return Ok(sibling); - } - - let dev_binary = binaries_dir.join(&candidate); - if dev_binary.exists() { - return Ok(dev_binary); - } - } - - Err(anyhow!( - "could not locate bundled sidecar near {} or {}", - current_dir.to_string_lossy(), - binaries_dir.to_string_lossy() - )) -} - -fn sidecar_name_candidates() -> Vec { - let mut candidates = vec!["claude-sidecar".to_string()]; - let triple_name = format!("claude-sidecar-{}", current_target_triple()); - candidates.push(triple_name); - - #[cfg(target_os = "windows")] - { - let with_exe = candidates - .iter() - .map(|name| format!("{name}.exe")) - .collect::>(); - candidates.extend(with_exe); - } - - candidates -} - -fn current_target_triple() -> &'static str { - #[cfg(all(target_os = "macos", target_arch = "aarch64"))] - { - "aarch64-apple-darwin" - } - - #[cfg(all(target_os = "macos", target_arch = "x86_64"))] - { - "x86_64-apple-darwin" - } - - #[cfg(all(target_os = "windows", target_arch = "x86_64"))] - { - "x86_64-pc-windows-msvc" - } - - #[cfg(all(target_os = "windows", target_arch = "aarch64"))] - { - "aarch64-pc-windows-msvc" - } - - #[cfg(all(target_os = "linux", target_arch = "x86_64"))] - { - "x86_64-unknown-linux-gnu" - } - - #[cfg(all(target_os = "linux", target_arch = "aarch64"))] - { - "aarch64-unknown-linux-gnu" - } -} - -fn build_terminal_session_id() -> String { - let millis = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis()) - .unwrap_or(0); - format!("terminal-{millis}") -} - -fn quote_for_bash(value: &str) -> String { - format!("'{}'", value.replace('\'', "'\"'\"'")) -} - -#[cfg(target_os = "windows")] -fn quote_for_powershell(value: &str) -> String { - value.replace('\'', "''") -} diff --git a/desktop/src/__tests__/agentsSettings.test.tsx b/desktop/src/__tests__/agentsSettings.test.tsx index e124362d..c7c850ee 100644 --- a/desktop/src/__tests__/agentsSettings.test.tsx +++ b/desktop/src/__tests__/agentsSettings.test.tsx @@ -22,8 +22,11 @@ vi.mock('../stores/providerStore', () => ({ useProviderStore: () => ({ providers: [], activeId: null, + presets: [], isLoading: false, + isPresetsLoading: false, fetchProviders: vi.fn(), + fetchPresets: vi.fn(), deleteProvider: vi.fn(), activateProvider: vi.fn(), activateOfficial: vi.fn(), diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx index a101cbcb..d426f98f 100644 --- a/desktop/src/__tests__/generalSettings.test.tsx +++ b/desktop/src/__tests__/generalSettings.test.tsx @@ -131,6 +131,15 @@ describe('Settings > General tab', () => { expect(useSettingsStore.getState().setSkipWebFetchPreflight).toHaveBeenCalledWith(false) }) + + it('keeps install and extension tabs available after removing the embedded terminal tab', () => { + render() + + expect(screen.getByText('Install')).toBeInTheDocument() + expect(screen.getByText('MCP')).toBeInTheDocument() + expect(screen.getByText('Plugins')).toBeInTheDocument() + expect(screen.queryByText('Terminal')).not.toBeInTheDocument() + }) }) describe('Settings > Providers tab', () => { diff --git a/desktop/src/__tests__/pluginsSettings.test.tsx b/desktop/src/__tests__/pluginsSettings.test.tsx index 3e8848e8..3a1e9d7a 100644 --- a/desktop/src/__tests__/pluginsSettings.test.tsx +++ b/desktop/src/__tests__/pluginsSettings.test.tsx @@ -23,8 +23,11 @@ vi.mock('../stores/providerStore', () => ({ useProviderStore: () => ({ providers: [], activeId: null, + presets: [], isLoading: false, + isPresetsLoading: false, fetchProviders: vi.fn(), + fetchPresets: vi.fn(), deleteProvider: vi.fn(), activateProvider: vi.fn(), activateOfficial: vi.fn(), diff --git a/desktop/src/__tests__/skillsSettings.test.tsx b/desktop/src/__tests__/skillsSettings.test.tsx index 8280b647..d13a996d 100644 --- a/desktop/src/__tests__/skillsSettings.test.tsx +++ b/desktop/src/__tests__/skillsSettings.test.tsx @@ -19,8 +19,11 @@ vi.mock('../stores/providerStore', () => ({ useProviderStore: () => ({ providers: [], activeId: null, + presets: [], isLoading: false, + isPresetsLoading: false, fetchProviders: vi.fn(), + fetchPresets: vi.fn(), deleteProvider: vi.fn(), activateProvider: vi.fn(), activateOfficial: vi.fn(), diff --git a/desktop/src/__tests__/terminalPanel.test.tsx b/desktop/src/__tests__/terminalPanel.test.tsx deleted file mode 100644 index 622ca2b9..00000000 --- a/desktop/src/__tests__/terminalPanel.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest' -import { render, screen } from '@testing-library/react' -import '@testing-library/jest-dom' - -import { TerminalPanel } from '../components/settings/TerminalPanel' -import { useSettingsStore } from '../stores/settingsStore' - -describe('TerminalPanel', () => { - beforeEach(() => { - useSettingsStore.setState({ locale: 'en' }) - }) - - it('shows bundled CLI guidance and browser-runtime fallback text', () => { - render() - - expect(screen.getByText('Setup Terminal')).toBeInTheDocument() - expect( - screen.getByText(/if you already have the official Claude Code installed/i), - ).toBeInTheDocument() - expect( - screen.getByText(/available only inside the packaged Tauri desktop runtime/i), - ).toBeInTheDocument() - expect(screen.getByText('Copy `claude-haha`')).toBeInTheDocument() - expect(screen.queryByText('Working directory')).not.toBeInTheDocument() - expect(screen.queryByText('Restart shell')).not.toBeInTheDocument() - }) -}) diff --git a/desktop/src/components/settings/TerminalPanel.restart.test.tsx b/desktop/src/components/settings/TerminalPanel.restart.test.tsx deleted file mode 100644 index 2c23736c..00000000 --- a/desktop/src/components/settings/TerminalPanel.restart.test.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import '@testing-library/jest-dom' - -import { useSettingsStore } from '../../stores/settingsStore' - -const terminalMocks = vi.hoisted(() => ({ - startDesktopTerminal: vi.fn(), - closeDesktopTerminal: vi.fn(), - resizeDesktopTerminal: vi.fn().mockResolvedValue(undefined), - writeDesktopTerminal: vi.fn().mockResolvedValue(undefined), - listenDesktopTerminalEvents: vi.fn().mockResolvedValue(() => {}), -})) - -vi.mock('../../lib/desktopRuntime', () => ({ - isTauriRuntime: () => true, -})) - -vi.mock('../../lib/settingsTerminal', () => ({ - startDesktopTerminal: terminalMocks.startDesktopTerminal, - closeDesktopTerminal: terminalMocks.closeDesktopTerminal, - resizeDesktopTerminal: terminalMocks.resizeDesktopTerminal, - writeDesktopTerminal: terminalMocks.writeDesktopTerminal, - listenDesktopTerminalEvents: terminalMocks.listenDesktopTerminalEvents, -})) - -class MockTerminal { - cols = 100 - rows = 24 - - loadAddon() {} - - open() {} - - focus() {} - - clear() {} - - write() {} - - onData() { - return { dispose() {} } - } - - dispose() {} -} - -class MockFitAddon { - fit() {} -} - -vi.mock('@xterm/xterm', () => ({ - Terminal: MockTerminal, -})) - -vi.mock('@xterm/addon-fit', () => ({ - FitAddon: MockFitAddon, -})) - -import { TerminalPanel } from './TerminalPanel' - -function createDeferred() { - let resolve!: (value: T | PromiseLike) => void - const promise = new Promise((innerResolve) => { - resolve = innerResolve - }) - return { promise, resolve } -} - -describe('TerminalPanel restart flow', () => { - beforeEach(() => { - useSettingsStore.setState({ locale: 'en' }) - - vi.stubGlobal( - 'ResizeObserver', - class { - observe() {} - disconnect() {} - }, - ) - - terminalMocks.startDesktopTerminal.mockReset() - terminalMocks.closeDesktopTerminal.mockReset() - terminalMocks.resizeDesktopTerminal.mockClear() - terminalMocks.writeDesktopTerminal.mockClear() - terminalMocks.listenDesktopTerminalEvents.mockClear() - - terminalMocks.startDesktopTerminal - .mockResolvedValueOnce({ - sessionId: 'session-1', - shell: 'bash', - cwd: '/Users/nanmi', - explicitCommandName: 'claude-haha', - docsCommandName: 'claude', - }) - .mockResolvedValueOnce({ - sessionId: 'session-2', - shell: 'bash', - cwd: '/tmp/restarted', - explicitCommandName: 'claude-haha', - docsCommandName: 'claude', - }) - }) - - it('switches to the new session without waiting for old close to finish', async () => { - const oldClose = createDeferred() - terminalMocks.closeDesktopTerminal.mockImplementation((sessionId: string) => { - if (sessionId === 'session-1') return oldClose.promise - return Promise.resolve() - }) - - render() - - await screen.findByText('Shell: bash · Working directory: /Users/nanmi') - - fireEvent.click(screen.getByRole('button', { name: 'Restart shell' })) - - await waitFor(() => - expect( - screen.getByText('Shell: bash · Working directory: /tmp/restarted'), - ).toBeInTheDocument(), - ) - expect(terminalMocks.closeDesktopTerminal).toHaveBeenCalledWith('session-1') - - oldClose.resolve() - }) -}) diff --git a/desktop/src/components/settings/TerminalPanel.tsx b/desktop/src/components/settings/TerminalPanel.tsx deleted file mode 100644 index d582476f..00000000 --- a/desktop/src/components/settings/TerminalPanel.tsx +++ /dev/null @@ -1,422 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from 'react' -import '@xterm/xterm/css/xterm.css' -import { Button } from '../shared/Button' -import { useTranslation } from '../../i18n' -import { useUIStore } from '../../stores/uiStore' -import { isTauriRuntime } from '../../lib/desktopRuntime' -import { - closeDesktopTerminal, - listenDesktopTerminalEvents, - resizeDesktopTerminal, - startDesktopTerminal, - writeDesktopTerminal, - type DesktopTerminalStart, -} from '../../lib/settingsTerminal' - -type TerminalLike = { - clear(): void - dispose(): void - focus(): void - write(data: string): void - onData(listener: (data: string) => void): { dispose(): void } - cols: number - rows: number -} - -type FitAddonLike = { - fit(): void -} - -function readThemeVar(name: string, fallback: string) { - if (typeof window === 'undefined') return fallback - const value = getComputedStyle(document.documentElement) - .getPropertyValue(name) - .trim() - return value || fallback -} - -export function TerminalPanel() { - const t = useTranslation() - const addToast = useUIStore((s) => s.addToast) - const [session, setSession] = useState(null) - const [launchError, setLaunchError] = useState(null) - const [isLaunching, setIsLaunching] = useState(false) - const [exitCode, setExitCode] = useState(null) - const [terminalReady, setTerminalReady] = useState(false) - const [eventsReady, setEventsReady] = useState(false) - const hostRef = useRef(null) - const terminalRef = useRef(null) - const fitRef = useRef(null) - const sessionIdRef = useRef(null) - const pendingOutputRef = useRef([]) - const autoStartedRef = useRef(false) - const isDesktop = isTauriRuntime() - - const focusTerminal = () => { - terminalRef.current?.focus() - } - - const closeSessionInBackground = (sessionId: string | null) => { - if (!sessionId) return - void closeDesktopTerminal(sessionId).catch((error) => { - setLaunchError(error instanceof Error ? error.message : String(error)) - }) - } - - const flushBufferedOutput = () => { - if (!terminalRef.current || pendingOutputRef.current.length === 0) return - for (const chunk of pendingOutputRef.current) { - terminalRef.current.write(chunk) - } - pendingOutputRef.current = [] - } - - useEffect(() => { - if (!isDesktop || !hostRef.current || terminalRef.current) return - - let disposed = false - let cleanup = () => {} - - void (async () => { - const [{ Terminal }, { FitAddon }] = await Promise.all([ - import('@xterm/xterm'), - import('@xterm/addon-fit'), - ]) - if (disposed || !hostRef.current) return - - const terminal = new Terminal({ - cursorBlink: true, - convertEol: true, - fontFamily: - '"SF Mono", "JetBrains Mono", "Fira Code", "Cascadia Mono", ui-monospace, monospace', - fontSize: 12.5, - lineHeight: 1.35, - scrollback: 5000, - theme: { - background: readThemeVar('--color-terminal-bg', '#121212'), - foreground: readThemeVar('--color-terminal-fg', '#d4d4d4'), - cursor: readThemeVar('--color-brand', '#d97757'), - cursorAccent: readThemeVar('--color-surface', '#ffffff'), - selectionBackground: 'rgba(217, 119, 87, 0.22)', - black: '#1f1f1f', - red: '#ff6d67', - green: '#7ef18a', - yellow: '#f8c55f', - blue: '#7aa2f7', - magenta: '#c792ea', - cyan: '#7fd1b9', - white: '#d7d2d0', - brightBlack: '#6c6763', - brightRed: '#ff8e88', - brightGreen: '#96f5a0', - brightYellow: '#ffd67c', - brightBlue: '#98b8ff', - brightMagenta: '#ddb2ff', - brightCyan: '#9be7d1', - brightWhite: '#f7f2ef', - }, - }) - const fitAddon = new FitAddon() - terminal.loadAddon(fitAddon) - terminal.open(hostRef.current) - terminal.focus() - terminalRef.current = terminal - fitRef.current = fitAddon - setTerminalReady(true) - flushBufferedOutput() - - const scheduleResize = () => { - if (!fitRef.current || !terminalRef.current || !sessionIdRef.current) return - fitRef.current.fit() - const cols = terminalRef.current.cols - const rows = terminalRef.current.rows - if (cols > 0 && rows > 0) { - void resizeDesktopTerminal(sessionIdRef.current, cols, rows).catch(() => null) - } - } - - const dataDisposable = terminal.onData((data) => { - const currentSessionId = sessionIdRef.current - if (!currentSessionId) return - void writeDesktopTerminal(currentSessionId, data).catch((error) => { - setLaunchError( - error instanceof Error ? error.message : String(error), - ) - }) - }) - - const resizeObserver = - typeof ResizeObserver !== 'undefined' - ? new ResizeObserver(() => scheduleResize()) - : null - resizeObserver?.observe(hostRef.current) - hostRef.current.addEventListener('mousedown', focusTerminal) - - setTimeout(scheduleResize, 0) - - cleanup = () => { - dataDisposable.dispose() - resizeObserver?.disconnect() - hostRef.current?.removeEventListener('mousedown', focusTerminal) - terminal.dispose() - terminalRef.current = null - fitRef.current = null - setTerminalReady(false) - } - })() - - return () => { - disposed = true - cleanup() - } - }, [isDesktop]) - - useEffect(() => { - if (!isDesktop) return - - let cancelled = false - let unlisten: (() => void) | undefined - - void listenDesktopTerminalEvents((event) => { - if (cancelled || event.sessionId !== sessionIdRef.current) return - if (event.type === 'output') { - if (terminalRef.current) { - terminalRef.current.write(event.data) - } else { - pendingOutputRef.current.push(event.data) - } - return - } - - setExitCode(event.exitCode ?? null) - if (event.exitCode !== null && event.exitCode !== undefined) { - terminalRef.current?.write( - `\r\n${t('settings.terminal.exitPrefix')} ${event.exitCode}\r\n`, - ) - } - }).then((dispose) => { - unlisten = dispose - setEventsReady(true) - }) - - return () => { - cancelled = true - setEventsReady(false) - unlisten?.() - } - }, [isDesktop, t]) - - const startSession = async () => { - if (!isDesktop) return - - setIsLaunching(true) - setLaunchError(null) - setExitCode(null) - - const previousSessionId = sessionIdRef.current - - try { - const started = await startDesktopTerminal() - sessionIdRef.current = started.sessionId - setSession(started) - terminalRef.current?.clear() - terminalRef.current?.focus() - fitRef.current?.fit() - if (terminalRef.current?.cols && terminalRef.current?.rows) { - await resizeDesktopTerminal( - started.sessionId, - terminalRef.current.cols, - terminalRef.current.rows, - ).catch(() => null) - } - if (previousSessionId && previousSessionId !== started.sessionId) { - closeSessionInBackground(previousSessionId) - } - } catch (error) { - setLaunchError(error instanceof Error ? error.message : String(error)) - } finally { - setIsLaunching(false) - } - } - - useEffect(() => { - if (!isDesktop || !terminalReady || !eventsReady || autoStartedRef.current) return - autoStartedRef.current = true - void startSession() - - return () => { - autoStartedRef.current = false - const existingSessionId = sessionIdRef.current - sessionIdRef.current = null - if (existingSessionId) { - closeSessionInBackground(existingSessionId) - } - } - }, [eventsReady, isDesktop, terminalReady]) - - const explicitCommand = session?.explicitCommandName || 'claude-haha' - const docsCommand = session?.docsCommandName || 'claude' - const exampleCommands = useMemo( - () => [ - `${docsCommand} plugin install skill-creator@claude-plugins-official --scope user`, - `${explicitCommand} mcp add docs --transport http https://example.com/mcp`, - ], - [docsCommand, explicitCommand], - ) - - const handleCopy = async (value: string) => { - try { - await navigator.clipboard.writeText(value) - addToast({ - type: 'success', - message: t('settings.terminal.copied'), - }) - } catch (error) { - addToast({ - type: 'error', - message: - error instanceof Error ? error.message : t('settings.terminal.copyFailed'), - }) - } - } - - return ( -
-
-
-
-
- {t('settings.terminal.eyebrow')} -
-

- {t('settings.terminal.title')} -

-

- {t('settings.terminal.description')} -

-
- - {isDesktop ? ( -
- - -
- ) : null} -
- -
-
-
-
- {t('settings.terminal.docsTitle')} -
-

- {t('settings.terminal.docsBody', { - docsCommand, - explicitCommand, - })} -

-
- -
- - -
-
- -
- {exampleCommands.map((command) => ( - - ))} -
-
-
- -
- {!isDesktop ? ( -
- {t('settings.terminal.runtimeOnly')} -
- ) : ( -
-
-
-
- {t('settings.terminal.sessionTitle')} -
-
- {session - ? t('settings.terminal.sessionMeta', { - shell: session.shell, - cwd: session.cwd, - }) - : t('settings.terminal.sessionPending')} -
-
- {exitCode !== null ? ( - - {t('settings.terminal.exitBadge', { code: String(exitCode) })} - - ) : null} -
- - {launchError ? ( -
- {launchError} -
- ) : null} - -
-
-
-
-
- - {session?.cwd || t('settings.terminal.sessionPending')} - -
-
-
-
- )} -
-
- ) -} diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index e4eb4615..027c8ce4 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -51,7 +51,6 @@ export const en = { 'settings.tab.providers': 'Providers', 'settings.tab.permissions': 'Permissions', 'settings.tab.general': 'General', - 'settings.tab.terminal': 'Terminal', 'settings.tab.install': 'Install', 'settings.tab.skills': 'Skills', 'settings.tab.mcp': 'MCP', @@ -307,6 +306,7 @@ export const en = { 'settings.install.stop': 'Stop', 'settings.install.refresh': 'Refresh state', 'settings.install.newConversation': 'New install chat', + 'settings.install.clearConversation': 'Clear chat', 'settings.install.contextDefault': 'If no directory is selected, the installer session starts from your home directory.', 'settings.install.contextUsing': 'Installer session directory: {path}', 'settings.install.contextTitle': 'Execution directory', @@ -321,27 +321,7 @@ export const en = { 'settings.install.createFailed': 'Failed to create installer session', 'settings.install.refreshDone': 'Plugin, MCP, and skill state refreshed', 'settings.install.newConversationReady': 'A fresh installer conversation is ready; your next request will start a new install context.', - 'settings.terminal.eyebrow': 'Bundled CLI Terminal', - 'settings.terminal.title': 'Setup Terminal', - 'settings.terminal.description': 'Open a real shell inside Settings for plugin, MCP, and skill setup. This terminal wires the bundled desktop CLI into the shell so install commands can run without requiring a separate global Claude CLI.', - 'settings.terminal.restart': 'Restart shell', - 'settings.terminal.clear': 'Clear screen', - 'settings.terminal.docsTitle': 'Docs command compatibility', - 'settings.terminal.docsBody': 'If you already have the official Claude Code installed, you can keep using `{docsCommand} ...` commands. If not, use the bundled CLI here instead: `{explicitCommand} ...`.', - 'settings.terminal.copyDocsCommand': 'Copy `{name}`', - 'settings.terminal.copyExplicitCommand': 'Copy `{name}`', - 'settings.terminal.copied': 'Command copied', - 'settings.terminal.copyFailed': 'Failed to copy command', - 'settings.terminal.sessionTitle': 'Interactive shell', - 'settings.terminal.sessionMeta': 'Shell: {shell} · Working directory: {cwd}', - 'settings.terminal.sessionPending': 'Launching shell...', - 'settings.terminal.exitPrefix': 'Shell exited with code', - 'settings.terminal.exitBadge': 'Exit {code}', - 'settings.terminal.runtimeOnly': 'The embedded terminal is available only inside the packaged Tauri desktop runtime.', - 'settings.terminal.contextTitle': 'Working directory', - 'settings.terminal.contextAuto': 'Default context', - 'settings.terminal.contextHint': 'Choose where the shell should start before restarting it. This is useful when an install command needs project-local files or a local MCP config target.', - 'settings.terminal.contextActive': 'Current shell directory: {path}', + 'settings.install.clearConversationReady': 'Installer chat cleared. Your next request will start a fresh install context.', // Settings > Skills 'settings.skills.title': 'Installed Skills', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index 8fbe6fc0..65ecc6e7 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -53,7 +53,6 @@ export const zh: Record = { 'settings.tab.providers': '服务商', 'settings.tab.permissions': '权限', 'settings.tab.general': '通用', - 'settings.tab.terminal': '终端', 'settings.tab.install': '安装中心', 'settings.tab.skills': '技能', 'settings.tab.mcp': 'MCP', @@ -309,6 +308,7 @@ export const zh: Record = { 'settings.install.stop': '中断执行', 'settings.install.refresh': '刷新安装状态', 'settings.install.newConversation': '新建安装会话', + 'settings.install.clearConversation': '清除对话', 'settings.install.contextDefault': '未指定目录时默认使用 Home 目录启动安装会话。', 'settings.install.contextUsing': '当前安装会话目录:{path}', 'settings.install.contextTitle': '执行目录', @@ -323,27 +323,7 @@ export const zh: Record = { 'settings.install.createFailed': '创建安装会话失败', 'settings.install.refreshDone': '已刷新插件、MCP 和技能状态', 'settings.install.newConversationReady': '已准备新的安装会话;下一条请求会启动新的安装上下文。', - 'settings.terminal.eyebrow': '内置 CLI 终端', - 'settings.terminal.title': '安装与配置终端', - 'settings.terminal.description': '在设置页里直接打开一个真实 shell,用来安装 plugin、MCP 和 skill。这个终端会把桌面端内置 CLI 注入进去,不依赖用户额外安装全局 Claude CLI。', - 'settings.terminal.restart': '重启 shell', - 'settings.terminal.clear': '清空屏幕', - 'settings.terminal.docsTitle': '安装命令兼容提示', - 'settings.terminal.docsBody': '如果你已经安装了官方 Claude Code,可以继续使用 `{docsCommand} ...` 命令;如果没有安装,就使用我们打包的 CLI,也就是 `{explicitCommand} ...`。', - 'settings.terminal.copyDocsCommand': '复制 `{name}`', - 'settings.terminal.copyExplicitCommand': '复制 `{name}`', - 'settings.terminal.copied': '命令已复制', - 'settings.terminal.copyFailed': '复制命令失败', - 'settings.terminal.sessionTitle': '交互式 shell', - 'settings.terminal.sessionMeta': 'Shell:{shell} · 工作目录:{cwd}', - 'settings.terminal.sessionPending': '正在启动 shell...', - 'settings.terminal.exitPrefix': 'Shell 退出,退出码', - 'settings.terminal.exitBadge': '退出 {code}', - 'settings.terminal.runtimeOnly': '这个内嵌终端只会在打包后的 Tauri 桌面运行时里可用。', - 'settings.terminal.contextTitle': '启动目录', - 'settings.terminal.contextAuto': '默认上下文', - 'settings.terminal.contextHint': '如果安装命令需要项目本地文件,或者要写 local/project 级 MCP 配置,可以先在这里切目录,再重启 shell。', - 'settings.terminal.contextActive': '当前 shell 目录:{path}', + 'settings.install.clearConversationReady': '已清除当前安装对话;下一条请求会启动新的安装上下文。', // Settings > Skills 'settings.skills.title': '已安装技能', diff --git a/desktop/src/lib/settingsTerminal.ts b/desktop/src/lib/settingsTerminal.ts deleted file mode 100644 index 05dce042..00000000 --- a/desktop/src/lib/settingsTerminal.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { isTauriRuntime } from './desktopRuntime' - -export type DesktopTerminalStart = { - sessionId: string - shell: string - cwd: string - explicitCommandName: string - docsCommandName: string -} - -export type DesktopTerminalEvent = - | { type: 'output'; sessionId: string; data: string } - | { type: 'exit'; sessionId: string; exitCode?: number | null } - -export async function startDesktopTerminal(cwd?: string) { - if (!isTauriRuntime()) { - throw new Error('Desktop terminal is only available in the Tauri runtime.') - } - - const { invoke } = await import(/* @vite-ignore */ '@tauri-apps/api/core') - return invoke('terminal_start_session', { - cwd: cwd?.trim() || null, - }) -} - -export async function writeDesktopTerminal(sessionId: string, data: string) { - const { invoke } = await import(/* @vite-ignore */ '@tauri-apps/api/core') - return invoke('terminal_write', { sessionId, data }) -} - -export async function resizeDesktopTerminal( - sessionId: string, - cols: number, - rows: number, -) { - const { invoke } = await import(/* @vite-ignore */ '@tauri-apps/api/core') - return invoke('terminal_resize', { - sessionId, - cols, - rows, - }) -} - -export async function closeDesktopTerminal(sessionId: string) { - const { invoke } = await import(/* @vite-ignore */ '@tauri-apps/api/core') - return invoke('terminal_close', { sessionId }) -} - -export async function listenDesktopTerminalEvents( - handler: (event: DesktopTerminalEvent) => void, -) { - const { listen } = await import(/* @vite-ignore */ '@tauri-apps/api/event') - return listen('desktop-terminal-event', (event) => { - handler(event.payload) - }) -} diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx index dea53f1b..e70e7b30 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -28,7 +28,6 @@ import { ClaudeOfficialLogin } from '../components/settings/ClaudeOfficialLogin' import { useUpdateStore } from '../stores/updateStore' import { formatBytes } from '../lib/formatBytes' import { InstallCenter } from '../components/settings/InstallCenter' -import { TerminalPanel } from '../components/settings/TerminalPanel' export function Settings() { const [activeTab, setActiveTab] = useState('providers') @@ -51,7 +50,6 @@ export function Settings() { setActiveTab('permissions')} /> setActiveTab('general')} /> setActiveTab('adapters')} /> - setActiveTab('terminal')} /> setActiveTab('install')} /> setActiveTab('mcp')} /> setActiveTab('agents')} /> @@ -70,7 +68,6 @@ export function Settings() { {activeTab === 'permissions' && } {activeTab === 'general' && } {activeTab === 'adapters' && } - {activeTab === 'terminal' && } {activeTab === 'install' && } {activeTab === 'mcp' && } {activeTab === 'agents' && }