diff --git a/.github/scripts/windows-installer.iss b/.github/scripts/windows-installer.iss index 6c94843b..777020bf 100644 --- a/.github/scripts/windows-installer.iss +++ b/.github/scripts/windows-installer.iss @@ -56,6 +56,12 @@ RestartApplications=no [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +; Writing shell verbs is an install-time decision, the way VS Code and Git for +; Windows treat theirs — not a runtime preference, so tty7 has no setting for +; it. Off by default: the registry is user-visible system state. The keys land +; under HKCU even for an all-users install, so this only ever affects whoever +; ran the installer. Inno restores the previous choice when upgrading. +Name: "explorermenu"; Description: "Add ""Open in tty7"" to the folder context menu"; GroupDescription: "Shell integration:"; Flags: unchecked ; Builds before the tty7/tty7-app split installed the GUI as tty7.exe. Upgrading ; only *adds* tty7-app.exe, so the old binary would stay on disk — and a taskbar @@ -107,6 +113,11 @@ Name: "{autoprograms}\tty7"; Filename: "{app}\tty7-app.exe"; AppUserModelID: "co Name: "{autodesktop}\tty7"; Filename: "{app}\tty7-app.exe"; Tasks: desktopicon; AppUserModelID: "com.github.tty7" [Run] +; The registry shape lives in core::explorer_context_menu, not here: the app +; reads those same keys to decide whether an existing registration still points +; at this install, and two hand-kept copies of the layout would drift. Runs +; before the launch entry below so a first start already sees the final state. +Filename: "{app}\tty7-app.exe"; Parameters: "--register-explorer-menu"; Tasks: explorermenu; Flags: runhidden waituntilterminated Filename: "{app}\tty7-app.exe"; Description: "{cm:LaunchProgram,tty7}"; Flags: nowait postinstall skipifsilent [UninstallRun] @@ -117,6 +128,11 @@ Filename: "{app}\tty7-app.exe"; Description: "{cm:LaunchProgram,tty7}"; Flags: n ; the call returns without opening a window. RunOnceId keys the entry so a repeated ; uninstall doesn't run it twice. Filename: "{app}\tty7-app.exe"; Parameters: "--stop-daemon"; Flags: runhidden waituntilterminated; RunOnceId: "StopDaemon" +; Unconditional, and deliberately not gated on the task: an install that had the +; menu registered and was later upgraded without the box ticked still holds the +; keys, and verbs pointing at a deleted exe are worse than a no-op. Removing keys +; that were never written succeeds silently. +Filename: "{app}\tty7-app.exe"; Parameters: "--unregister-explorer-menu"; Flags: runhidden waituntilterminated; RunOnceId: "UnregisterExplorerMenu" [Code] (* Gracefully stop the persistent daemon before we overwrite tty7-app.exe. We can't diff --git a/docs/features.md b/docs/features.md index bc58f1b8..215cecf6 100644 --- a/docs/features.md +++ b/docs/features.md @@ -23,6 +23,7 @@ - **Sync with system** — Settings → Appearance; pick separate light and dark themes and tty7 follows the OS appearance live (`theme_follow_system`, `theme_preset_light` / `theme_preset_dark` in `config.json`) - **Window opacity & blur** — Settings → Appearance → Window; applies to every theme, *Follow theme* returns to the theme's own `opacity` / `blur` - **CJK / IME input** +- **Windows Explorer menu** — the installer offers *Add “Open in tty7” to the folder context menu* as a setup task, off by default, and the uninstaller always takes it back out. Writing shell verbs is an install-time decision, so there is no runtime setting; a portable-zip install can do it itself with `tty7-app.exe --register-explorer-menu` (or `--unregister-explorer-menu`). Either way the keys land under `HKCU`, so only your own Windows account is affected ## Fonts @@ -66,7 +67,7 @@ it never wraps or replaces the agent. - **Tray icon** — a system tray / menu bar item that flips to an attention state the moment any agent needs your input; its menu lists every agent pane (brand avatar + status dot, click to reveal), switches the notification policy, and offers *Quit and Stop Daemon* alongside the plain session-keeping quit (`show_tray_icon`, on by default) - **`tty7 wait`** — the CLI's orchestration primitive: block until a pane's agent needs input or finishes its turn (`tty7 wait %3 --until waiting,done --changed --timeout 600`, exit 124 on timeout), so one agent can sleep until its peer blocks on a permission prompt instead of screen-scraping — then `tty7 capture %3 --plain` to read the result. The agent status is a level, not an event, so `--changed` ignores the state the pane was already in when the wait began; without it, the JSON's `stale` flag says whether the answer might belong to the previous turn - **Orchestration skill** — a switch (Settings → Agents) that installs a Claude Code skill (`~/.claude/skills/tty7-orchestration`) teaching a *primary* agent the delegation loop — spawn a worker pane, send it a bounded task, `wait` on it, capture the result. A skill rather than a global instruction on purpose: only its one-line description rides in context until explicitly invoked, and worker agents never inherit orchestration authority -- **`tty7` on PATH** — the CLI ships inside every installer and is put on PATH at launch, so a script or a coding agent can drive tty7 from any terminal. Inside a tty7 pane it works regardless, since panes inherit the app's environment. On Unix it is a symlink into whichever of `/opt/homebrew/bin`, `/usr/local/bin`, `~/.local/bin`, `~/bin`, `~/.cargo/bin` your PATH already covers; on Windows the install directory is appended to your user PATH, and the uninstaller takes it back out. A `tty7` you installed yourself is left alone, never replaced. Off via Settings → About or `install_cli_on_path: false` in `config.json` +- **`tty7` on PATH** — the CLI ships inside every installer and is put on PATH at launch, so a script or a coding agent can drive tty7 from any terminal. Inside a tty7 pane it works regardless, since panes inherit the app's environment. On Unix it is a symlink into whichever of `/opt/homebrew/bin`, `/usr/local/bin`, `~/.local/bin`, `~/bin`, `~/.cargo/bin` your PATH already covers; on Windows the install directory is appended to your user PATH, and the uninstaller takes it back out. A `tty7` you installed yourself is left alone, never replaced. Off via Settings → Agents or `install_cli_on_path: false` in `config.json` ## SSH diff --git a/docs/features.zh-CN.md b/docs/features.zh-CN.md index 62ee5ee5..564ae3f7 100644 --- a/docs/features.zh-CN.md +++ b/docs/features.zh-CN.md @@ -16,13 +16,14 @@ - **标签页与分屏** —— 永远开在当前目录 - **侧栏按仓库分组** —— 左侧标签栏按 git 仓库分组、每组一个标题行,不在仓库里的标签归入末尾的 *Scratch* 组;切分支、仓库内 `cd` 都不会挪动行(`config.json` 的 `sidebar_grouping`:默认 `repo`,`none` 恢复扁平列表) -- **命令面板** ⌘ P · 回滚搜索 ⌘ F +- **命令面板** ⌘ P · scrollback 搜索 ⌘ F - **⌘ 点击打开链接** · 桌面通知 · 划选即复制(可选,设置 → 终端 → 剪贴板) - **智能双击选中** —— 双击直接选中整条 URL、文件路径、括号/引号对,中文按词典分词出词;Shift 点击扩展选区(设置 → 终端 → 鼠标可开关;分隔符用 `config.json` 的 `word_separators` 配置) - **9 套主题,也能自定义** — YAML 种子主题,背景支持纯色、渐变或图片;可导入 iTerm2 `.itermcolors`;应用内颜色编辑器带背景图选择 - **跟随系统外观** — 设置 → Appearance;分别选好浅色和深色主题,tty7 随系统深浅模式实时切换(`config.json` 中的 `theme_follow_system`、`theme_preset_light` / `theme_preset_dark`) - **窗口透明与模糊** — 设置 → Appearance → Window;对所有主题生效,*Follow theme* 恢复主题自带的 `opacity` / `blur` - **CJK / 输入法输入** +- **Windows 资源管理器右键菜单** —— 安装程序提供 *Add “Open in tty7” to the folder context menu* 这个安装任务,默认不勾选,卸载时一律移除。写 shell verb 是安装期的决定,所以没有运行时开关;用 portable zip 的话可以自己执行 `tty7-app.exe --register-explorer-menu`(或 `--unregister-explorer-menu`)。两种方式写入的键都在 `HKCU` 下,只影响你自己的 Windows 账户 ## 字体 @@ -63,7 +64,7 @@ Aider、Amp、OpenCode 等约 17 个)并在其外围加功能 —— 绝不包 - **托盘图标** —— 系统托盘 / 菜单栏常驻图标,任何 agent 等你输入时立即切换为提醒态;菜单列出所有 agent pane(品牌头像 + 状态点,点击直达)、可切换通知策略,并在保留会话的普通退出之外提供 *Quit and Stop Daemon*(`show_tray_icon`,默认开启) - **`tty7 wait`** —— CLI 的编排原语:阻塞到某个 pane 的 agent 等待输入或完成一轮(`tty7 wait %3 --until waiting,done --changed --timeout 600`,超时退出码 124),让一个 agent 睡到同伴卡在权限确认的那一刻,而不是抓屏猜——然后 `tty7 capture %3 --plain` 收结果。agent 状态是电平不是边沿,所以 `--changed` 会忽略 wait 开始时 pane 本来就处在的那个状态;不加它的话,JSON 里的 `stale` 标记会告诉你这个答案是不是上一轮留下的 - **Orchestration skill** —— 一个开关(设置 → Agents),安装一个 Claude Code skill(`~/.claude/skills/tty7-orchestration`),教 *primary* agent 完整的委派循环——开 worker pane、发一个边界清晰的任务、`wait` 等待、收结果。特意做成 skill 而非全局指令:平时只有一行描述占上下文,显式调用才加载全文,worker agent 也不会继承编排权限 -- **`tty7` 上 PATH** —— CLI 随每个安装包一起发布,启动时自动放到 PATH 上,脚本和 coding agent 在任何终端里都能驱动 tty7。tty7 自己的 pane 里则一定可用,因为 pane 继承 app 的环境。Unix 上是往 `/opt/homebrew/bin`、`/usr/local/bin`、`~/.local/bin`、`~/bin`、`~/.cargo/bin` 中你 PATH 已经覆盖的那个目录里放一个软链;Windows 上是把安装目录追加到用户 PATH,卸载时再摘掉。你自己装的 `tty7` 一律保持原样,不会被覆盖。关掉:设置 → About,或 `config.json` 里 `install_cli_on_path: false` +- **`tty7` 上 PATH** —— CLI 随每个安装包一起发布,启动时自动放到 PATH 上,脚本和 coding agent 在任何终端里都能驱动 tty7。tty7 自己的 pane 里则一定可用,因为 pane 继承 app 的环境。Unix 上是往 `/opt/homebrew/bin`、`/usr/local/bin`、`~/.local/bin`、`~/bin`、`~/.cargo/bin` 中你 PATH 已经覆盖的那个目录里放一个软链;Windows 上是把安装目录追加到用户 PATH,卸载时再摘掉。你自己装的 `tty7` 一律保持原样,不会被覆盖。关掉:设置 → Agents,或 `config.json` 里 `install_cli_on_path: false` ## SSH @@ -96,9 +97,9 @@ Aider、Amp、OpenCode 等约 17 个)并在其外围加功能 —— 绝不包 | ⌘ ] · ⌘ [ | 下一个窗格 · 上一个窗格 | | ⌘ ⌥ ←→↑↓ | 按方向切换焦点窗格 | | ⌘ ⏎ · ⌘ ⇧ ⏎ | 切换全屏 · 最大化 / 还原窗格 | -| ⌘ K | 清屏并清空回滚缓冲区 | +| ⌘ K | 清屏并清空 scrollback | | ⌘ P | 命令面板 | -| ⌘ F | 搜索回滚缓冲区 | +| ⌘ F | 搜索 scrollback | | ⌃ R | 模糊搜索 shell 历史 | | ⌘ + · ⌘ − · ⌘ 0 | 字号增大 · 减小 · 重置 | diff --git a/src/core/explorer_context_menu.rs b/src/core/explorer_context_menu.rs index becd4765..4b5384e3 100644 --- a/src/core/explorer_context_menu.rs +++ b/src/core/explorer_context_menu.rs @@ -1,13 +1,18 @@ //! Optional Windows Explorer context-menu integration. //! -//! tty7 deliberately does not register shell verbs during installation or -//! startup. The registry is user-visible system state, so only the explicit -//! buttons in Settings call [`register`] or [`unregister`]. Both verbs invoke -//! the GUI-subsystem `tty7-app.exe` directly so Explorer never allocates a -//! transient console. The app first offers the path to an already running GUI -//! through `GuiOpen`, then continues normal startup when no GUI receives it. +//! The registry is user-visible system state, so tty7 never writes these verbs +//! on its own: the Windows installer offers a task checkbox and invokes +//! [`register`], and its uninstaller always invokes [`unregister`]. There is no +//! runtime setting — the same install-time-only treatment VS Code and Git for +//! Windows give their shell entries. Keeping the key layout here rather than in +//! the .iss keeps one description of it in the tree. +//! +//! Both verbs invoke the GUI-subsystem `tty7-app.exe` directly so Explorer never +//! allocates a transient console. The app first offers the path to an already +//! running GUI through `GuiOpen`, then continues normal startup when no GUI +//! receives it. -use std::ffi::{OsStr, OsString}; +use std::ffi::OsString; use std::path::{Path, PathBuf}; use anyhow::{Context as _, Result}; @@ -15,19 +20,6 @@ use anyhow::{Context as _, Result}; const DIRECTORY_KEY: &str = r"Software\Classes\Directory\shell\tty7"; const BACKGROUND_KEY: &str = r"Software\Classes\Directory\Background\shell\tty7"; -/// The state shown in Settings. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Status { - /// Neither tty7 verb exists for this user. - NotRegistered, - /// Both verbs exactly describe the currently running tty7 installation. - Registered, - /// At least one verb exists, but the pair is incomplete or points elsewhere. - NeedsUpdate, - /// Explorer shell verbs are unavailable on this operating system. - Unsupported, -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Location { Directory, @@ -66,23 +58,6 @@ struct Registration { command: OsString, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct RegistryShape { - values: u32, - subkeys: u32, -} - -fn registration_tree_is_exact(root: RegistryShape, command: RegistryShape) -> bool { - root == (RegistryShape { - values: 2, - subkeys: 1, - }) && command - == (RegistryShape { - values: 1, - subkeys: 0, - }) -} - fn replace_entry_with( registration: &Registration, delete: impl FnOnce(&str) -> Result<()>, @@ -105,11 +80,6 @@ impl Registration { } } -/// Return the live per-user registration state. -pub fn status() -> Result { - platform_status() -} - /// Register both Explorer verbs for the current user. pub fn register() -> Result<()> { platform_register() @@ -151,6 +121,7 @@ fn registrations(app: &Path) -> [Registration; 2] { #[cfg(windows)] mod windows { use super::*; + use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt as _; use windows_sys::Win32::Foundation::{ @@ -175,21 +146,10 @@ mod windows { } } - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - enum EntryState { - Missing, - Matching, - Different, - } - fn wide(value: &OsStr) -> Vec { value.encode_wide().chain(std::iter::once(0)).collect() } - fn units(value: &OsStr) -> Vec { - value.encode_wide().collect() - } - fn io_error(action: &str, code: u32) -> anyhow::Error { anyhow::anyhow!( "{action}: {}", @@ -197,20 +157,6 @@ mod windows { ) } - fn open_key(path: &str) -> Result> { - let path = wide(OsStr::new(path)); - let mut key: HKEY = std::ptr::null_mut(); - // SAFETY: `path` is NUL-terminated and alive for the call; `key` is a - // valid out-parameter and is wrapped only when the call succeeds. - let code = - unsafe { RegOpenKeyExW(HKEY_CURRENT_USER, path.as_ptr(), 0, KEY_READ, &mut key) }; - match code { - ERROR_SUCCESS => Ok(Some(RegistryKey(key))), - ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND => Ok(None), - other => Err(io_error("opening the tty7 Explorer registry key", other)), - } - } - fn create_key(path: &str) -> Result { let path = wide(OsStr::new(path)); let mut key: HKEY = std::ptr::null_mut(); @@ -235,56 +181,6 @@ mod windows { Ok(RegistryKey(key)) } - fn query_string(key: &RegistryKey, name: Option<&OsStr>) -> Result>> { - let name = name.map(wide); - let name_ptr = name.as_ref().map_or(std::ptr::null(), |name| name.as_ptr()); - let mut kind = 0u32; - let mut bytes = 0u32; - // SAFETY: the key is live, the optional value-name pointer is either - // null or NUL-terminated, and the size/type out-parameters are valid. - let code = unsafe { - RegQueryValueExW( - key.0, - name_ptr, - std::ptr::null(), - &mut kind, - std::ptr::null_mut(), - &mut bytes, - ) - }; - if matches!(code, ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND) { - return Ok(None); - } - if code != ERROR_SUCCESS { - return Err(io_error("reading a tty7 Explorer registry value", code)); - } - if kind != REG_SZ || !bytes.is_multiple_of(2) { - return Ok(None); - } - - let mut value = vec![0u16; (bytes as usize).div_ceil(2)]; - // SAFETY: `value` is sized from the preceding query and remains live; - // Win32 receives its capacity in bytes through `bytes`. - let code = unsafe { - RegQueryValueExW( - key.0, - name_ptr, - std::ptr::null(), - &mut kind, - value.as_mut_ptr().cast(), - &mut bytes, - ) - }; - if code != ERROR_SUCCESS { - return Err(io_error("reading a tty7 Explorer registry value", code)); - } - value.truncate(bytes as usize / 2); - while value.last() == Some(&0) { - value.pop(); - } - Ok(Some(value)) - } - fn set_string(key: &RegistryKey, name: Option<&OsStr>, value: &OsStr) -> Result<()> { let name = name.map(wide); let name_ptr = name.as_ref().map_or(std::ptr::null(), |name| name.as_ptr()); @@ -302,59 +198,6 @@ mod windows { } } - fn key_shape(key: &RegistryKey) -> Result { - let mut subkeys = 0u32; - let mut values = 0u32; - // SAFETY: `key` is live and the two count pointers reference writable - // locals. Every optional output that is not needed is passed as null, - // which `RegQueryInfoKeyW` explicitly permits. - let code = unsafe { - RegQueryInfoKeyW( - key.0, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null(), - &mut subkeys, - std::ptr::null_mut(), - std::ptr::null_mut(), - &mut values, - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - std::ptr::null_mut(), - ) - }; - if code == ERROR_SUCCESS { - Ok(RegistryShape { values, subkeys }) - } else { - Err(io_error("inspecting the tty7 Explorer registry key", code)) - } - } - - fn entry_state(registration: &Registration) -> Result { - let Some(root) = open_key(registration.location.key())? else { - return Ok(EntryState::Missing); - }; - let label = query_string(&root, None)?; - let icon = query_string(&root, Some(OsStr::new("Icon")))?; - - let command_path = format!(r"{}\command", registration.location.key()); - let command_key = match open_key(&command_path)? { - Some(key) => key, - None => return Ok(EntryState::Different), - }; - let command = query_string(&command_key, None)?; - let matches = label.as_deref() == Some(&units(OsStr::new(registration.location.label()))) - && icon.as_deref() == Some(&units(®istration.icon)) - && command.as_deref() == Some(&units(®istration.command)) - && registration_tree_is_exact(key_shape(&root)?, key_shape(&command_key)?); - Ok(if matches { - EntryState::Matching - } else { - EntryState::Different - }) - } - fn write_entry_contents(registration: &Registration) -> Result<()> { let root = create_key(registration.location.key())?; set_string(&root, None, OsStr::new(registration.location.label()))?; @@ -389,18 +232,6 @@ mod windows { } } - pub(super) fn status() -> Result { - let app = application_path()?; - let states = registrations(&app).map(|entry| entry_state(&entry)); - let [directory, background] = states; - let (directory, background) = (directory?, background?); - Ok(match (directory, background) { - (EntryState::Missing, EntryState::Missing) => Status::NotRegistered, - (EntryState::Matching, EntryState::Matching) => Status::Registered, - _ => Status::NeedsUpdate, - }) - } - pub(super) fn register() -> Result<()> { let app = application_path()?; for registration in registrations(&app) { @@ -422,11 +253,6 @@ mod windows { } } -#[cfg(windows)] -fn platform_status() -> Result { - windows::status() -} - #[cfg(windows)] fn platform_register() -> Result<()> { windows::register() @@ -437,11 +263,6 @@ fn platform_unregister() -> Result<()> { windows::unregister() } -#[cfg(not(windows))] -fn platform_status() -> Result { - Ok(Status::Unsupported) -} - #[cfg(not(windows))] fn platform_register() -> Result<()> { anyhow::bail!("Windows Explorer integration is only available on Windows") @@ -486,50 +307,6 @@ mod tests { ); } - #[test] - fn registration_shape_rejects_every_extra_value_or_subkey() { - assert!(registration_tree_is_exact( - RegistryShape { - values: 2, - subkeys: 1, - }, - RegistryShape { - values: 1, - subkeys: 0, - }, - )); - assert!(!registration_tree_is_exact( - RegistryShape { - values: 3, - subkeys: 1, - }, - RegistryShape { - values: 1, - subkeys: 0, - }, - )); - assert!(!registration_tree_is_exact( - RegistryShape { - values: 2, - subkeys: 1, - }, - RegistryShape { - values: 2, - subkeys: 0, - }, - )); - assert!(!registration_tree_is_exact( - RegistryShape { - values: 2, - subkeys: 2, - }, - RegistryShape { - values: 1, - subkeys: 0, - }, - )); - } - #[test] fn registration_replaces_the_owned_tree_before_writing() { let registration = Registration::new( diff --git a/src/main.rs b/src/main.rs index 9fd0d134..0d1712cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -143,6 +143,15 @@ fn open_path_from( None } +/// `Some(true)` to register the Explorer verbs, `Some(false)` to remove them. +fn explorer_menu_action_from(args: &[std::ffi::OsString]) -> Option { + args.iter().find_map(|arg| match arg.as_os_str() { + a if a == std::ffi::OsStr::new("--register-explorer-menu") => Some(true), + a if a == std::ffi::OsStr::new("--unregister-explorer-menu") => Some(false), + _ => None, + }) +} + /// Offers an explicit launch path to an already running local GUI. /// /// The dispatcher is injected so the startup decision can be tested without @@ -303,6 +312,26 @@ fn main() { crate::core::crash::install(role); crate::core::logfile::install(role); + // The Windows installer owns the Explorer context menu: a task checkbox + // runs these, and the uninstaller always runs the unregister half. Keeping + // the registry shape in `explorer_context_menu` rather than in the .iss + // means the installer and the running app can never disagree about it. + // Handled after the log file is open, because a GUI-subsystem process has + // no console to report a failure on and Inno does not surface exit codes: + // the log is the only place the reason can survive. + if let Some(register) = explorer_menu_action_from(&args) { + let result = if register { + crate::core::explorer_context_menu::register() + } else { + crate::core::explorer_context_menu::unregister() + }; + if let Err(error) = result { + log::error!("the Explorer context-menu update failed: {error}"); + std::process::exit(1); + } + return; + } + if daemon { if let Err(e) = crate::daemon::server::run_daemon() { log::error!("daemon exited with error: {e}"); @@ -405,11 +434,33 @@ mod tests { #[cfg(test)] mod argument_tests { - use super::{config_dir_from, forward_open_path_with, open_path_from}; + use super::{ + config_dir_from, explorer_menu_action_from, forward_open_path_with, open_path_from, + }; use std::ffi::OsString; use std::path::PathBuf; use tty7_core::daemon::control::ReplyOk; + /// The installer passes exactly one of these; every other launch — above + /// all a plain `--open-path` from the very menu they register — must fall + /// through to normal startup instead of rewriting the registry and exiting. + #[test] + fn explorer_menu_flags_are_distinguished_and_otherwise_absent() { + assert_eq!( + explorer_menu_action_from(&[OsString::from("--register-explorer-menu")]), + Some(true) + ); + assert_eq!( + explorer_menu_action_from(&[OsString::from("--unregister-explorer-menu")]), + Some(false) + ); + assert_eq!( + explorer_menu_action_from(&[OsString::from("--open-path"), OsString::from("/work")]), + None + ); + assert_eq!(explorer_menu_action_from(&[]), None); + } + #[test] fn open_path_accepts_separate_and_equals_forms() { let separate = open_path_from( diff --git a/src/ui/app.rs b/src/ui/app.rs index b1e4669b..ef5863bc 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -34,8 +34,7 @@ use crate::ui::palette::{ use crate::ui::pane::{CloseOutcome, Dir, Pane, PaneSlot}; use crate::ui::presets::Fill; use crate::ui::settings::{ - ExplorerContextMenuNote, Recording, SettingsSection, SettingsState, ThemeEditor, - humanize_action, + Recording, SettingsSection, SettingsState, ThemeEditor, humanize_action, }; use crate::ui::theme::{apply_theme, set_menus, window_background}; @@ -1945,38 +1944,6 @@ impl Tty7App { self.update_config(cx, |cfg| cfg.install_cli_on_path = on); } - pub(crate) fn register_explorer_context_menu(&mut self, cx: &mut Context) { - self.run_explorer_context_menu_action(true, cx); - } - - pub(crate) fn unregister_explorer_context_menu(&mut self, cx: &mut Context) { - self.run_explorer_context_menu_action(false, cx); - } - - fn run_explorer_context_menu_action(&mut self, register: bool, cx: &mut Context) { - // Registry operations are tiny and synchronous. Keeping the action on - // the UI thread also makes the displayed status correspond to the - // completed write, without a task racing a closed Settings window. - let result = if register { - crate::core::explorer_context_menu::register() - } else { - crate::core::explorer_context_menu::unregister() - }; - let note = match result { - Ok(()) if register => ExplorerContextMenuNote::Registered, - Ok(()) => ExplorerContextMenuNote::Unregistered, - Err(error) if register => ExplorerContextMenuNote::RegisterFailed(error.to_string()), - Err(error) => ExplorerContextMenuNote::UnregisterFailed(error.to_string()), - }; - let status = - crate::core::explorer_context_menu::status().map_err(|error| error.to_string()); - if let Some(settings) = self.settings.as_mut() { - settings.explorer_context_menu_status = status; - settings.explorer_context_menu_note = Some(note); - } - cx.notify(); - } - pub(crate) fn set_dim_inactive_panes(&mut self, on: bool, cx: &mut Context) { self.update_config(cx, |cfg| cfg.dim_inactive_panes = on); } @@ -3618,9 +3585,6 @@ impl Tty7App { theme_search, recording: None, rebinding_note: None, - explorer_context_menu_status: crate::core::explorer_context_menu::status() - .map_err(|error| error.to_string()), - explorer_context_menu_note: None, ssh_form: None, ssh_detail: crate::ui::settings::SshDetail::None, ssh_filter, diff --git a/src/ui/i18n.rs b/src/ui/i18n.rs index 485e196f..319f799b 100644 --- a/src/ui/i18n.rs +++ b/src/ui/i18n.rs @@ -379,21 +379,6 @@ pub enum L10nKey { SettingsCommandLine, SettingsCommandLineDesc, SettingsInstallCliOnPath, - SettingsExplorerContextMenu, - SettingsExplorerContextMenuDesc, - SettingsExplorerNotRegistered, - SettingsExplorerRegistered, - SettingsExplorerNeedsUpdate, - SettingsExplorerUnavailable, - SettingsExplorerStatusUnavailable, - SettingsExplorerRegister, - SettingsExplorerUpdate, - SettingsExplorerUnregister, - SettingsExplorerRegisteredNote, - SettingsExplorerUnregisteredNote, - SettingsExplorerRegisterFailed, - SettingsExplorerUnregisterFailed, - SettingsExplorerWindows11Note, SettingsServer, SettingsServerDesc, SettingsRestartServer, @@ -421,7 +406,6 @@ pub enum L10nKey { SettingsSearchDetectUrlsKeywords, SettingsSearchDiffPreviewFromCountsKeywords, SettingsSearchDimInactivePanesKeywords, - SettingsSearchExplorerContextMenuKeywords, SettingsSearchFocusFollowsMouseKeywords, SettingsSearchFontFamilyKeywords, SettingsSearchFontLigaturesKeywords, @@ -1098,7 +1082,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::SettingsNavTerminal => ("Terminal", "终端"), L10nKey::SettingsNavInput => ("Input", "输入"), L10nKey::SettingsNavSsh => ("SSH", "SSH"), - L10nKey::SettingsNavAgents => ("Agents", "智能体"), + L10nKey::SettingsNavAgents => ("Agents", "Agents"), L10nKey::SettingsNavWindowTabs => ("Window & Tabs", "窗口与标签页"), L10nKey::SettingsNavKeybindings => ("Keybindings", "按键绑定"), L10nKey::SettingsNavAbout => ("About", "关于"), @@ -1439,7 +1423,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { "仅适用于没有可继承目录的 shell,例如窗口的第一个标签页。新标签页和分屏仍会继承活动窗格的目录,已经打开的 shell 会继续运行。", ), L10nKey::SettingsScrolling => ("Scrolling", "滚动"), - L10nKey::SettingsScrollback => ("Scrollback", "回滚缓冲"), + L10nKey::SettingsScrollback => ("Scrollback", "Scrollback"), L10nKey::SettingsScrollbackDesc => ( "Lines of history kept per pane. Applies to new panes.", "每个窗格保留的历史行数。仅适用于新窗格。", @@ -1529,14 +1513,14 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { "⌥+key sends the escape chord shells expect (⌥B = back one word) instead of typing a special character (∫).", "⌥+按键 发送 shell 期望的转义组合键(⌥B = 后退一个词),而不是输入特殊字符(∫)。", ), - L10nKey::SettingsAgentsIntro => ("Agents", "智能体"), + L10nKey::SettingsAgentsIntro => ("Agents", "Agents"), L10nKey::SettingsAgentsIntroDesc => ( "Hook integrations give panes running these agents live session status (working / waiting / done) in the tab bar. Only active inside tty7.", - "钩子集成让标签栏中的窗格实时显示这些智能体的会话状态(进行中 / 等待中 / 已完成)。仅在 tty7 内生效。", + "hook 集成让标签栏中的窗格实时显示这些 agent 的会话状态(进行中 / 等待中 / 已完成)。仅在 tty7 内生效。", ), L10nKey::SettingsReadingAgentConfig => ( "Reading this machine's agent config…", - "正在读取这台机器的智能体配置…", + "正在读取这台机器的 agent 配置…", ), L10nKey::SettingsStatusNotInstalled => ("Not installed", "未安装"), L10nKey::SettingsStatusInstalled => ("Installed", "已安装"), @@ -1547,7 +1531,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::SettingsUninstall => ("Uninstall", "卸载"), L10nKey::SettingsOfflineMachines => ( "{count} more saved machines are not connected — open a workspace on one to install its hooks there.", - "还有 {count} 台已保存的机器未连接——在其中一台上打开工作区,即可在那台机器上安装钩子。", + "还有 {count} 台已保存的机器未连接——在其中一台上打开工作区,即可在那台机器上安装 hook。", ), L10nKey::SettingsSyncWithSystem => ("Sync with system", "跟随系统"), L10nKey::SettingsSyncWithSystemDesc => ( @@ -1598,7 +1582,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::SettingsShowTrayIcon => ("Show tray icon", "显示托盘图标"), L10nKey::SettingsShowTrayIconDesc => ( "Keep a status item in the system tray / menu bar: it signals when a coding agent needs your input, and its menu jumps to agent panes.", - "在系统托盘/菜单栏保留状态项:当编码智能体需要输入时发出提示,其菜单可跳转到智能体窗格。", + "在系统托盘/菜单栏保留状态项:当编码 agent 需要输入时发出提示,其菜单可跳转到该 agent 的窗格。", ), L10nKey::SettingsTabs => ("Tabs", "标签页"), L10nKey::SettingsNewTabPosition => ("New tab position", "新标签页位置"), @@ -1618,11 +1602,11 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::SettingsDiffPreviewFromCounts => ( "Open diff preview from sidebar counts", - "从侧栏计数打开差异预览", + "从侧栏计数打开 diff 预览", ), L10nKey::SettingsDiffPreviewFromCountsDesc => ( "Click a row's +N −N to open the working-tree diff in an overlay. Off keeps the branch and the counts on the row and just stops them being clickable.", - "点击行上的 +N −N 可在浮层中打开工作树差异。关闭时行上仍显示分支和计数,但不再可点击。", + "点击行上的 +N −N 可在浮层中打开 worktree diff。关闭时行上仍显示分支和计数,但不再可点击。", ), L10nKey::SettingsNotifications => ("Notifications", "通知"), L10nKey::SettingsNotifyOnCommandFinish => ("Notify on command finish", "命令完成时通知"), @@ -1667,11 +1651,11 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::SettingsRestoreAllDefaults => ("Restore all defaults", "恢复全部默认值"), L10nKey::SettingsAboutDesc1 => ( "A terminal workbench: shells, workspaces, SSH, coding agents.", - "一个终端工作台:shell、工作区、SSH、编码智能体。", + "一个终端工作台:shell、工作区、SSH、编码 agent。", ), L10nKey::SettingsAboutDesc2 => ( "Editor-grade input in every shell, shells that outlive quits and reboots without tmux, a native SSH stack with profiles and port forwarding, and live status for panes running coding agents.", - "每个 shell 都具备编辑器级输入;无需 tmux 也能让 shell 在退出和重启后继续运行;原生的 SSH 栈支持主机配置和端口转发;为运行编码智能体的窗格提供实时状态。", + "每个 shell 都具备编辑器级输入;无需 tmux 也能让 shell 在退出和重启后继续运行;原生的 SSH 栈支持主机配置和端口转发;为运行编码 agent 的窗格提供实时状态。", ), L10nKey::SettingsAboutTech => ( "Pure Rust · GPU rendering on Zed's gpui · VT core from Alacritty", @@ -1741,43 +1725,12 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::SettingsCommandLine => ("Command line", "命令行"), L10nKey::SettingsCommandLineDesc => ( "Put the bundled `tty7` command on your PATH at launch, so scripts and coding agents can drive tty7 from any terminal. Inside a tty7 pane it works either way. Turn this off if you keep your own `tty7` — one you built or installed yourself — and do not want it shadowed. Takes effect at next launch.", - "启动时将自带的 `tty7` 命令加入 PATH,让脚本和编码智能体可在任意终端驱动 tty7。在 tty7 窗格内两种情况都可用。如果你自己构建或安装了 `tty7` 且不希望被遮蔽,请关闭此选项。下次启动时生效。", + "启动时将自带的 `tty7` 命令加入 PATH,让脚本和编码 agent 可在任意终端驱动 tty7。在 tty7 窗格内两种情况都可用。如果你自己构建或安装了 `tty7` 且不希望被遮蔽,请关闭此选项。下次启动时生效。", ), L10nKey::SettingsInstallCliOnPath => ( "Install the `tty7` command on PATH", "将 `tty7` 命令安装到 PATH", ), - L10nKey::SettingsExplorerContextMenu => ("Windows Explorer", "Windows 文件资源管理器"), - L10nKey::SettingsExplorerContextMenuDesc => ( - "Add “Open in tty7” when you right-click a folder and “Open tty7 here” when you right-click a folder background. This is off by default and is registered only for your Windows account.", - "右键单击文件夹时添加“Open in tty7”,右键单击文件夹背景时添加“Open tty7 here”。此功能默认关闭,且只为当前 Windows 账户注册。", - ), - L10nKey::SettingsExplorerNotRegistered => ("Not registered", "未注册"), - L10nKey::SettingsExplorerRegistered => ("Registered", "已注册"), - L10nKey::SettingsExplorerNeedsUpdate => ("Needs update", "需要更新"), - L10nKey::SettingsExplorerUnavailable => ("Unavailable", "不可用"), - L10nKey::SettingsExplorerStatusUnavailable => ("Status unavailable", "无法获取状态"), - L10nKey::SettingsExplorerRegister => ("Register", "注册"), - L10nKey::SettingsExplorerUpdate => ("Update", "更新"), - L10nKey::SettingsExplorerUnregister => ("Unregister", "取消注册"), - L10nKey::SettingsExplorerRegisteredNote => ( - "Registered. Right-click a folder or folder background in Explorer to open it in tty7.", - "已注册。现在可以在文件资源管理器中右键单击文件夹或文件夹背景,以在 tty7 中打开。", - ), - L10nKey::SettingsExplorerUnregisteredNote => ( - "Unregistered from Windows Explorer.", - "已从 Windows 文件资源管理器中取消注册。", - ), - L10nKey::SettingsExplorerRegisterFailed => { - ("Could not register: {error}", "无法注册:{error}") - } - L10nKey::SettingsExplorerUnregisterFailed => { - ("Could not unregister: {error}", "无法取消注册:{error}") - } - L10nKey::SettingsExplorerWindows11Note => ( - "On Windows 11, classic shell entries may appear under “Show more options”.", - "在 Windows 11 上,经典右键菜单项可能显示在“显示更多选项”中。", - ), L10nKey::SettingsServer => ("Server", "服务器"), L10nKey::SettingsServerDesc => ( "Restart the server on this computer to pick up a newly granted macOS permission, recover if it stops responding, or start from a clean slate. This ends all running shells here; your tabs and layout reopen with fresh shells. A remote machine's server is restarted from its own menu in the workspace switcher.", @@ -1812,11 +1765,11 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::SettingsSearchClaudeCodeKeywords => ( "agent integration hooks install uninstall status rich session working waiting tab bar sidebar badge claude", - "Claude Code 智能体 集成 钩子 安装 卸载 状态 会话 claude agent integration hooks install", + "Claude Code agent 集成 hook 安装 卸载 状态 会话 claude agent integration hooks install", ), L10nKey::SettingsSearchCodexKeywords => ( "agent integration hooks install openai codex", - "Codex 智能体 集成 钩子 安装 OpenAI codex agent integration hooks install", + "Codex agent 集成 hook 安装 OpenAI codex agent integration hooks install", ), L10nKey::SettingsSearchCommandLineToolKeywords => ( "cli tty7 path shell command install symlink terminal iterm agent script", @@ -1829,7 +1782,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::SettingsSearchCopilotCliKeywords => ( "agent integration hooks install github copilot", - "Copilot CLI 智能体 集成 钩子 安装 GitHub copilot agent integration hooks install", + "Copilot CLI agent 集成 hook 安装 GitHub copilot agent integration hooks install", ), L10nKey::SettingsSearchCopyOnSelectKeywords => ( "clipboard selection yank mouse", @@ -1853,16 +1806,12 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::SettingsSearchDiffPreviewFromCountsKeywords => ( "diff overlay preview sidebar counts git changes click branch lines", - "从侧栏计数打开差异预览 差异 预览 侧栏 git diff preview sidebar counts git changes", + "从侧栏计数打开 diff 预览 diff 预览 侧栏 git diff preview sidebar counts git changes", ), L10nKey::SettingsSearchDimInactivePanesKeywords => ( "fade unfocused inactive split pane focus opacity highlight active dimming", "调暗 非活动窗格 淡化 未聚焦 分屏 高亮 active dimming pane focus", ), - L10nKey::SettingsSearchExplorerContextMenuKeywords => ( - "windows explorer context menu right click folder directory background shell menu register unregister open here", - "Windows 文件资源管理器 右键 菜单 文件夹 目录 背景 注册 取消注册 打开 explorer context menu right click folder directory background shell register unregister open here", - ), L10nKey::SettingsSearchFocusFollowsMouseKeywords => ( "pane hover activate", "焦点跟随鼠标 悬停 激活 窗格 focus follows mouse hover activate pane", @@ -1885,7 +1834,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::SettingsSearchGrokBuildKeywords => ( "agent integration hooks install xai grok build", - "Grok Build 智能体 集成 钩子 安装 xai grok build agent integration hooks install", + "Grok Build agent 集成 hook 安装 xai grok build agent integration hooks install", ), L10nKey::SettingsSearchHideMouseWhileTypingKeywords => ( "cursor pointer autohide", @@ -1938,7 +1887,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::SettingsSearchOpencodeKeywords => ( "agent integration plugin install opencode", - "OpenCode 智能体 集成 插件 安装 opencode agent integration plugin install", + "OpenCode agent 集成 插件 安装 opencode agent integration plugin install", ), L10nKey::SettingsSearchOptionAsMetaKeywords => ( "alt keyboard modifier escape macos option meta option acts as meta", @@ -1946,7 +1895,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::SettingsSearchPiKeywords => ( "agent integration extension install pi", - "Pi 智能体 集成 扩展 安装 pi agent integration extension install", + "Pi agent 集成 扩展 安装 pi agent integration extension install", ), L10nKey::SettingsSearchPortForwardingKeywords => ( "ssh tunnel local remote dynamic socks forward rule", @@ -1974,7 +1923,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::SettingsSearchScrollbackKeywords => ( "history buffer lines scroll", - "回滚 历史 缓冲区 行数 scrollback history buffer lines", + "scrollback 回看 向上滚动 历史 缓冲区 行数 scrollback history buffer lines", ), L10nKey::SettingsSearchShowTrayIconKeywords => ( "tray menu bar status item agent attention system icon", @@ -2095,7 +2044,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::FileTreeContextOpen => ("Open", "打开"), L10nKey::FileTreeContextCdHere => ("cd Here", "cd 到此处"), L10nKey::FileTreeContextInsertPath => ("Insert Path in Terminal", "在终端中插入路径"), - L10nKey::FileTreeContextAttachAgent => ("Attach to Agent", "附加到智能体"), + L10nKey::FileTreeContextAttachAgent => ("Attach to Agent", "附加到 agent"), L10nKey::FileTreeContextNewFile => ("New File", "新建文件"), L10nKey::FileTreeContextNewFolder => ("New Folder", "新建文件夹"), L10nKey::FileTreeContextRename => ("Rename", "重命名"), @@ -2160,7 +2109,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { "进入 git 仓库后,此标签页会列出未提交的变更。", ), L10nKey::PanelNoChanges => ("No uncommitted changes.", "没有未提交的变更。"), - L10nKey::PanelNoChangesHint => ("The working tree is clean.", "工作树是干净的。"), + L10nKey::PanelNoChangesHint => ("The working tree is clean.", "worktree 是干净的。"), L10nKey::PanelSessionSubtitle => ("Session", "会话"), L10nKey::PanelProcessesSubtitle => ("Processes", "进程"), L10nKey::PanelPortsSubtitle => ("Ports", "端口"), @@ -2169,7 +2118,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::PanelSsh => ("ssh", "ssh"), L10nKey::PanelBranch => ("branch", "分支"), L10nKey::PanelChangesRow => ("changes", "变更"), - L10nKey::PanelAgent => ("agent", "智能体"), + L10nKey::PanelAgent => ("agent", "agent"), L10nKey::PanelAgentIdle => ("idle", "空闲"), L10nKey::PanelAgentWorking => ("working", "进行中"), L10nKey::PanelAgentWaiting => ("waiting", "等待中"), @@ -2196,14 +2145,14 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { "{count} running shells will be ended and the layout forgotten.", "{count} 个正在运行的 shell 将会被终止,布局也将被清除。", ), - L10nKey::DiffReading => ("Reading diff…", "正在读取差异…"), + L10nKey::DiffReading => ("Reading diff…", "正在读取 diff…"), L10nKey::DiffNotARepo => ("Not a git repository", "不是 git 仓库"), L10nKey::DiffReadFailed => ( "Couldn't read the working-tree diff — retrying on the next refresh.", - "无法读取工作树差异——下次刷新时重试。", + "无法读取 worktree diff——下次刷新时重试。", ), - L10nKey::DiffWorkingTreeClean => ("Working tree clean", "工作树干净"), - L10nKey::DiffCloseTooltip => ("Close Diff (Esc)", "关闭差异 (Esc)"), + L10nKey::DiffWorkingTreeClean => ("Working tree clean", "worktree 干净"), + L10nKey::DiffCloseTooltip => ("Close Diff (Esc)", "关闭 diff (Esc)"), L10nKey::DiffChangedFiles => ("{count} changed files", "{count} 个变更文件"), L10nKey::DiffUntrackedCount => (" · {count} untracked", " · {count} 个未跟踪文件"), L10nKey::DiffMoreFiles => ( @@ -2212,25 +2161,25 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::DiffOversizedNotice => ( "This working tree is too large to render efficiently ({summary}). Every file is collapsed — expand individual files, or run `git diff` in the terminal.", - "此工作树太大,无法高效渲染({summary})。每个文件都已折叠——可展开单个文件,或在终端中运行 `git diff`。", + "此 worktree 太大,无法高效渲染({summary})。每个文件都已折叠——可展开单个文件,或在终端中运行 `git diff`。", ), L10nKey::DiffTruncatedPerFile => ( "Diff truncated at {limit} lines — run `git diff` in the terminal for the rest.", - "差异在 {limit} 行处截断——在终端中运行 `git diff` 查看其余部分。", + "diff 在 {limit} 行处截断——在终端中运行 `git diff` 查看其余部分。", ), L10nKey::DiffTruncatedBudget => ( "Body not loaded — this working tree is past tty7's diff budget. Run `git diff` in the terminal for this file.", - "内容未加载——此工作树已超出 tty7 的差异预算。在终端中运行 `git diff` 查看此文件。", + "内容未加载——此 worktree 已超出 tty7 的 diff 预算。在终端中运行 `git diff` 查看此文件。", ), L10nKey::DiffUntrackedHeader => ("Untracked files ({count})", "未跟踪文件 ({count})"), L10nKey::DiffMoreUntracked => ( "… and {count} more — run `git status` in the terminal to see them.", "…还有 {count} 个——在终端中运行 `git status` 查看。", ), - L10nKey::DiffLines => ("{count} diff lines", "{count} 行差异"), + L10nKey::DiffLines => ("{count} diff lines", "{count} 行 diff"), L10nKey::DiffChangedLines => ( "{total} changed lines, {loaded} diff rows loaded before {cap} cut the rest", - "{total} 行变更,在 {cap} 截断前已加载 {loaded} 行差异", + "{total} 行变更,在 {cap} 截断前已加载 {loaded} 行 diff", ), L10nKey::DiffBudgetAndCap => ( "tty7's budget and the per-file cap", @@ -2241,16 +2190,17 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::DiffUntrackedSummary => ("{count} untracked", "{count} 个未跟踪"), L10nKey::PendingConnecting => ("Connecting to {machine}…", "正在连接 {machine}…"), L10nKey::PendingUnreachable => ("Couldn't reach {machine}", "无法连接到 {machine}"), - L10nKey::WorktreePromptNeedsName => ("The worktree needs a name", "工作树需要一个名称"), - L10nKey::WorktreePromptTitle => ("New Worktree Tab", "新建工作树标签页"), - L10nKey::WorktreePromptName => ("Worktree Name", "工作树名称"), + L10nKey::WorktreePromptNeedsName => ("The worktree needs a name", "worktree 需要一个名称"), + L10nKey::WorktreePromptTitle => ("New Worktree Tab", "新建 worktree 标签页"), + L10nKey::WorktreePromptName => ("Worktree Name", "worktree 名称"), L10nKey::WorktreePromptBranch => ("New Branch", "新分支"), L10nKey::WorktreePromptBase => ("Start From", "起始分支"), L10nKey::WorktreePromptCreating => ("Creating…", "正在创建…"), L10nKey::WorktreePromptCreate => ("Create", "创建"), - L10nKey::AppNewWorktreeFailed => { - ("New worktree failed: {error}", "新建工作树失败:{error}") - } + L10nKey::AppNewWorktreeFailed => ( + "New worktree failed: {error}", + "新建 worktree 失败:{error}", + ), L10nKey::HomeTimeJustNow => ("just now", "刚刚"), L10nKey::HomeTimeMinutesAgo => ("{count} min ago", "{count} 分钟前"), L10nKey::HomeTimeHourAgo => ("1 hour ago", "1 小时前"), @@ -2449,7 +2399,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { } L10nKey::AppNoRunningCodingAgent => ( "No running coding agent found — start one (claude, codex, …) in a pane first.", - "未找到运行中的编码智能体——请先在某个窗格中启动一个(claude、codex 等)。", + "未找到运行中的编码 agent——请先在某个窗格中启动一个(claude、codex 等)。", ), L10nKey::SwitcherThisComputer => ("This Computer", "本机"), L10nKey::SwitcherRestartingServer => ("Restarting tty7's server…", "正在重启 tty7 服务器…"), @@ -2491,10 +2441,10 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::CmdGroupView => ("View", "视图"), L10nKey::CmdGroupTerminal => ("Terminal", "终端"), L10nKey::CmdGroupSsh => ("SSH", "SSH"), - L10nKey::CmdGroupAgents => ("Agents", "智能体"), + L10nKey::CmdGroupAgents => ("Agents", "Agents"), L10nKey::CmdGroupApplication => ("Application", "应用"), L10nKey::CmdNewTab => ("New Tab", "新标签页"), - L10nKey::CmdNewWorktreeTab => ("New Worktree Tab", "新建工作树标签页"), + L10nKey::CmdNewWorktreeTab => ("New Worktree Tab", "新建 worktree 标签页"), L10nKey::CmdNewWorktreeTabSubtitle => ( "isolated checkout on a fresh branch", "在全新分支上独立检出", @@ -2521,12 +2471,12 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::CmdCopySessionId => ("Copy Session ID", "复制会话 ID"), L10nKey::CmdCopySessionIdSubtitle => ( "the coding agent's own session id", - "编码智能体自身的会话 ID", + "编码 agent 自身的会话 ID", ), - L10nKey::CmdForkSession => ("Fork Session", "派生会话"), + L10nKey::CmdForkSession => ("Fork Session", "Fork 会话"), L10nKey::CmdForkSessionSubtitle => ( "branch this agent session into a new tab", - "将此智能体会话派生到新标签页", + "将此 agent 会话 fork 到新标签页", ), L10nKey::CmdMarkTabAsUnread => ("Mark Tab as Unread", "将标签页标记为未读"), L10nKey::CmdClosePaneTab => ("Close Pane / Tab", "关闭窗格/标签页"), @@ -2562,7 +2512,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::CmdChangeTheme => ("Change Theme…", "更改主题…"), L10nKey::CmdResetFontSize => ("Reset Font Size", "重置字号"), L10nKey::CmdEnterFullScreen => ("Enter Full Screen", "进入全屏"), - L10nKey::CmdClearScrollback => ("Clear Scrollback", "清除回滚"), + L10nKey::CmdClearScrollback => ("Clear Scrollback", "清除 scrollback"), L10nKey::CmdFindInTerminal => ("Find in Terminal…", "在终端中查找…"), L10nKey::CmdFindNext => ("Find Next", "查找下一个"), L10nKey::CmdFindPrevious => ("Find Previous", "查找上一个"), @@ -2576,18 +2526,18 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::CmdSshRemoteFiles => ("SSH: Remote Files", "SSH:远程文件"), L10nKey::CmdSshPortForwarding => ("SSH: Port Forwarding", "SSH:端口转发"), L10nKey::CmdSshConnectWithInput => ("SSH: Connect {input}", "SSH:连接 {input}"), - L10nKey::CmdAgentSendSelection => ("Agent: Send Selection", "智能体:发送选区"), + L10nKey::CmdAgentSendSelection => ("Agent: Send Selection", "Agent:发送选区"), L10nKey::CmdAgentSendSelectionSubtitle => ( "selection → running coding agent", - "选区 → 运行中的编码智能体", + "选区 → 运行中的编码 agent", ), L10nKey::CmdAgentSendGitDiffForReview => ( "Agent: Send Git Diff for Review", - "智能体:发送 Git diff 以供审查", + "Agent:发送 git diff 以供审查", ), L10nKey::CmdAgentSendGitDiffSubtitle => ( "git diff → running coding agent", - "git diff → 运行中的编码智能体", + "git diff → 运行中的编码 agent", ), L10nKey::CmdSettings => ("Settings…", "设置…"), L10nKey::CmdKeyboardShortcuts => ("Keyboard Shortcuts", "键盘快捷键"), @@ -2630,17 +2580,18 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::AppWorktreeRemoveDetailDirty => ( "The closed tab's worktree at {path} has uncommitted changes.", - "位于 {path} 的已关闭标签页的工作树有未提交的变更。", + "位于 {path} 的已关闭标签页的 worktree 有未提交的变更。", ), L10nKey::AppWorktreeRemoveDetailClean => ( "The closed tab's worktree at {path} is clean.", - "位于 {path} 的已关闭标签页的工作树是干净的。", + "位于 {path} 的已关闭标签页的 worktree 是干净的。", + ), + L10nKey::AppWorktreeRemoveTitle => ( + "Remove worktree \"{branch}\"?", + "删除 worktree\"{branch}\"?", ), - L10nKey::AppWorktreeRemoveTitle => { - ("Remove worktree \"{branch}\"?", "删除工作树\"{branch}\"?") - } L10nKey::AppWorktreeDiscardAndRemove => ("Discard Changes & Remove", "放弃变更并删除"), - L10nKey::AppWorktreeRemove => ("Remove Worktree", "删除工作树"), + L10nKey::AppWorktreeRemove => ("Remove Worktree", "删除 worktree"), L10nKey::AppWorktreeKeep => ("Keep", "保留"), L10nKey::AppReopenTabFailed => ( "Could not reopen the tab: no terminal started", @@ -2659,32 +2610,33 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::AppSplitPaneFailed => { ("Could not split the pane: {error}", "无法拆分窗格:{error}") } - L10nKey::AppWorktreeRemoved => { - ("Removed worktree \"{branch}\"", "已删除工作树\"{branch}\"") - } + L10nKey::AppWorktreeRemoved => ( + "Removed worktree \"{branch}\"", + "已删除 worktree\"{branch}\"", + ), L10nKey::AppWorktreeRemoveFailed => ( "Worktree removal failed: {error}", - "删除工作树失败:{error}", + "删除 worktree 失败:{error}", ), L10nKey::AppForkStillConnecting => ( "Could not fork: the pane is still connecting", - "无法派生:窗格仍在连接中", + "无法 fork:窗格仍在连接中", ), L10nKey::AppPaneNoCodingAgent => ( "This pane isn't running a coding agent", - "此窗格未运行编码智能体", + "此窗格未运行编码 agent", ), L10nKey::AppForkNoCommand => ( "tty7 has no fork command for {name}", - "tty7 没有用于 {name} 的派生命令", + "tty7 没有用于 {name} 的 fork 命令", ), L10nKey::AppForkLocalOnly => ( "{name} sessions can only be forked from a local pane", - "{name} 会话只能从本地窗格派生", + "{name} 会话只能从本地窗格 fork", ), L10nKey::AppForkNoSessionId => ( "tty7 hasn't seen a {name} session id in this pane — install its hooks in Settings → Agents", - "tty7 尚未在此窗格中看到 {name} 的会话 ID——请在设置 → 智能体中安装其钩子", + "tty7 尚未在此窗格中看到 {name} 的会话 ID——请在设置 → Agents 中安装其 hook", ), L10nKey::AppForkSessionIdNotToken => ( "{name}'s session id isn't a plain token", @@ -2692,7 +2644,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::AppForkMidTurn => ( "{name} is mid-turn — the fork won't include the turn in flight", - "{name} 正在处理中——派生不会包含进行中的这一轮", + "{name} 正在处理中——fork 不会包含进行中的这一轮", ), L10nKey::AppTabNoWorkingDirectory => ( "This tab has no working directory yet", @@ -2733,7 +2685,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { ), L10nKey::AppAgentHooksOffline => ( "Not connected to this machine, so its agent config can't be read or written. Open a workspace on it and come back.", - "未连接到这台机器,因此无法读取或写入其智能体配置。请在其上打开一个工作区后再回来。", + "未连接到这台机器,因此无法读取或写入其 agent 配置。请在其上打开一个工作区后再回来。", ), L10nKey::AppAgentHooksHomeDirUnresolved => { ("cannot resolve home directory", "无法解析主目录") @@ -2796,13 +2748,13 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::AppMenuHelp => ("Help", "帮助"), L10nKey::AppMenuNewTab => ("New Tab", "新标签页"), L10nKey::AppMenuNewWorkspace => ("New Workspace", "新工作区"), - L10nKey::AppMenuNewWorktreeTab => ("New Worktree Tab", "新工作树标签页"), + L10nKey::AppMenuNewWorktreeTab => ("New Worktree Tab", "新 worktree 标签页"), L10nKey::AppMenuSplitRight => ("Split Right", "向右分屏"), L10nKey::AppMenuSplitDown => ("Split Down", "向下分屏"), L10nKey::AppMenuRenameTab => ("Rename Tab…", "重命名标签页…"), L10nKey::AppMenuCopyWorkingDirectory => ("Copy Working Directory", "复制工作目录"), L10nKey::AppMenuCopySessionId => ("Copy Session ID", "复制会话 ID"), - L10nKey::AppMenuForkSession => ("Fork Session", "派生会话"), + L10nKey::AppMenuForkSession => ("Fork Session", "Fork 会话"), L10nKey::AppMenuClosePaneTab => ("Close Pane / Tab", "关闭窗格 / 标签页"), L10nKey::AppMenuCloseOtherTabs => ("Close Other Tabs", "关闭其他标签页"), L10nKey::AppMenuCloseTabsRight => ("Close Tabs to the Right", "关闭右侧标签页"), @@ -2830,7 +2782,7 @@ fn translate(locale: Locale, key: L10nKey) -> &'static str { L10nKey::AppMenuFocusNextPane => ("Focus Next Pane", "聚焦下一个窗格"), L10nKey::AppMenuFocusPreviousPane => ("Focus Previous Pane", "聚焦上一个窗格"), L10nKey::AppMenuZoomPane => ("Zoom Pane", "缩放窗格"), - L10nKey::AppMenuClearScrollback => ("Clear Scrollback", "清除回滚"), + L10nKey::AppMenuClearScrollback => ("Clear Scrollback", "清除 scrollback"), L10nKey::AppMenuEnterFullscreen => ("Enter Full Screen", "进入全屏"), L10nKey::AppMenuDocumentation => ("tty7 Documentation", "tty7 文档"), L10nKey::AppMenuKeyboardShortcuts => ("Keyboard Shortcuts", "键盘快捷键"), @@ -2883,15 +2835,15 @@ fn translate_variant(locale: Locale, key: L10nKey, branch: &'static str) -> &'st // --- Offline machines --- (SettingsOfflineMachines, "zero") => ( "0 more saved machines are not connected — open a workspace on one to install its hooks there.", - "还有 0 台已保存的机器未连接——在其中一台上打开工作区,即可在那台机器上安装钩子。", + "还有 0 台已保存的机器未连接——在其中一台上打开工作区,即可在那台机器上安装 hook。", ), (SettingsOfflineMachines, "one") => ( "1 more saved machine is not connected — open a workspace on it to install its hooks there.", - "还有 1 台已保存的机器未连接——在那台机器上打开工作区,即可在那里安装钩子。", + "还有 1 台已保存的机器未连接——在那台机器上打开工作区,即可在那里安装 hook。", ), (SettingsOfflineMachines, "other") => ( "{count} more saved machines are not connected — open a workspace on one to install its hooks there.", - "还有 {count} 台已保存的机器未连接——在其中一台上打开工作区,即可在那台机器上安装钩子。", + "还有 {count} 台已保存的机器未连接——在其中一台上打开工作区,即可在那台机器上安装 hook。", ), // --- Panel untracked files --- @@ -3341,21 +3293,6 @@ mod tests { L10nKey::SettingsCommandLine, L10nKey::SettingsCommandLineDesc, L10nKey::SettingsInstallCliOnPath, - L10nKey::SettingsExplorerContextMenu, - L10nKey::SettingsExplorerContextMenuDesc, - L10nKey::SettingsExplorerNotRegistered, - L10nKey::SettingsExplorerRegistered, - L10nKey::SettingsExplorerNeedsUpdate, - L10nKey::SettingsExplorerUnavailable, - L10nKey::SettingsExplorerStatusUnavailable, - L10nKey::SettingsExplorerRegister, - L10nKey::SettingsExplorerUpdate, - L10nKey::SettingsExplorerUnregister, - L10nKey::SettingsExplorerRegisteredNote, - L10nKey::SettingsExplorerUnregisteredNote, - L10nKey::SettingsExplorerRegisterFailed, - L10nKey::SettingsExplorerUnregisterFailed, - L10nKey::SettingsExplorerWindows11Note, L10nKey::SettingsServer, L10nKey::SettingsServerDesc, L10nKey::SettingsRestartServer, @@ -3383,7 +3320,6 @@ mod tests { L10nKey::SettingsSearchDetectUrlsKeywords, L10nKey::SettingsSearchDiffPreviewFromCountsKeywords, L10nKey::SettingsSearchDimInactivePanesKeywords, - L10nKey::SettingsSearchExplorerContextMenuKeywords, L10nKey::SettingsSearchFocusFollowsMouseKeywords, L10nKey::SettingsSearchFontFamilyKeywords, L10nKey::SettingsSearchFontLigaturesKeywords, @@ -3912,44 +3848,6 @@ mod tests { assert_eq!(current_locale(), Locale::En); } - #[test] - fn explorer_settings_are_translated_with_error_details() { - let keys = [ - L10nKey::SettingsExplorerContextMenu, - L10nKey::SettingsExplorerContextMenuDesc, - L10nKey::SettingsExplorerNotRegistered, - L10nKey::SettingsExplorerRegistered, - L10nKey::SettingsExplorerNeedsUpdate, - L10nKey::SettingsExplorerUnavailable, - L10nKey::SettingsExplorerStatusUnavailable, - L10nKey::SettingsExplorerRegister, - L10nKey::SettingsExplorerUpdate, - L10nKey::SettingsExplorerUnregister, - L10nKey::SettingsExplorerRegisteredNote, - L10nKey::SettingsExplorerUnregisteredNote, - L10nKey::SettingsExplorerRegisterFailed, - L10nKey::SettingsExplorerUnregisterFailed, - L10nKey::SettingsExplorerWindows11Note, - L10nKey::SettingsSearchExplorerContextMenuKeywords, - ]; - for key in keys { - assert_ne!( - translate(Locale::En, key), - translate(Locale::ZhHans, key), - "Simplified Chinese should not fall back to English for {key:?}" - ); - } - - assert_eq!( - apply_template( - translate(Locale::ZhHans, L10nKey::SettingsExplorerRegisterFailed), - &[("error", "access denied")], - None, - ), - "无法注册:access denied" - ); - } - #[test] fn plural_and_select_branches_are_translated() { let plural_keys = [ diff --git a/src/ui/settings.rs b/src/ui/settings.rs index b871ddc8..d5cd0f3f 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -85,30 +85,6 @@ struct SearchEntry { keywords: L10nKey, } -#[derive(Clone)] -pub(crate) enum ExplorerContextMenuNote { - Registered, - Unregistered, - RegisterFailed(String), - UnregisterFailed(String), -} - -impl ExplorerContextMenuNote { - fn localized(&self) -> String { - match self { - Self::Registered => t(L10nKey::SettingsExplorerRegisteredNote).to_string(), - Self::Unregistered => t(L10nKey::SettingsExplorerUnregisteredNote).to_string(), - Self::RegisterFailed(error) => { - t_fmt(L10nKey::SettingsExplorerRegisterFailed, &[("error", error)]) - } - Self::UnregisterFailed(error) => t_fmt( - L10nKey::SettingsExplorerUnregisterFailed, - &[("error", error)], - ), - } - } -} - fn localized_update_phase(phase: &crate::core::update::UpdatePhase) -> Option { use crate::core::update::{UpdateFailure, UpdatePhase}; @@ -457,15 +433,10 @@ fn settings_search_entries() -> &'static [SearchEntry] { keywords: SettingsSearchHowShellsWorkKeywords, }, SearchEntry { - section: About, + section: Agents, title: SettingsSearchCommandLineToolTitle, keywords: SettingsSearchCommandLineToolKeywords, }, - SearchEntry { - section: About, - title: SettingsExplorerContextMenu, - keywords: SettingsSearchExplorerContextMenuKeywords, - }, ] } @@ -519,9 +490,6 @@ pub(crate) struct SettingsState { pub(crate) theme_search: Entity, pub(crate) recording: Option, pub(crate) rebinding_note: Option, - pub(crate) explorer_context_menu_status: - Result, - pub(crate) explorer_context_menu_note: Option, pub(crate) ssh_form: Option, pub(crate) ssh_detail: SshDetail, pub(crate) ssh_filter: Entity, @@ -3757,22 +3725,21 @@ impl Tty7App { page = page.children(self.agent_hooks_machine_picker(selected_host, cx)); + // The hook rows describe whichever machine is selected above; the + // command-line section below is always about this GUI's own host, so it + // is appended after the match rather than inside the ready arm. match view { AgentHooksView::Loading => { - return page - .child( - div() - .py_4() - .text_sm() - .text_color(muted_fg) - .child(t(L10nKey::SettingsReadingAgentConfig)), - ) - .into_any_element(); + page = page.child( + div() + .py_4() + .text_sm() + .text_color(muted_fg) + .child(t(L10nKey::SettingsReadingAgentConfig)), + ); } AgentHooksView::Unavailable(reason) => { - return page - .child(div().py_4().text_sm().text_color(warning).child(reason)) - .into_any_element(); + page = page.child(div().py_4().text_sm().text_color(warning).child(reason)); } AgentHooksView::Ready(rows) => { for (i, row) in rows.into_iter().enumerate() { @@ -3847,7 +3814,46 @@ impl Tty7App { } } } - page.into_any_element() + + let install_cli_on_path = cx.global::().install_cli_on_path; + page.child( + v_flex() + .mt_6() + .gap_2() + .child(self.section_rule(cx)) + .child( + div() + .text_sm() + .font_weight(FontWeight::MEDIUM) + .text_color(foreground) + .child(t(L10nKey::SettingsCommandLine)), + ) + .child( + div() + .text_sm() + .text_color(muted_fg) + .child(t(L10nKey::SettingsCommandLineDesc)), + ) + .child( + h_flex() + .gap_2() + .items_center() + .child( + crate::ui::theme::switch("install-cli-on-path", cx) + .checked(install_cli_on_path) + .on_click(cx.listener(|this, on: &bool, _w, cx| { + this.set_install_cli_on_path(*on, cx) + })), + ) + .child( + div() + .text_sm() + .text_color(foreground) + .child(t(L10nKey::SettingsInstallCliOnPath)), + ), + ), + ) + .into_any_element() } fn agent_hooks_machine_picker(&self, selected: HostId, cx: &mut Context) -> Option
{ @@ -4701,12 +4707,7 @@ impl Tty7App { fn render_settings_about(&self, cx: &mut Context) -> AnyElement { let theme = cx.theme(); - let (foreground, muted_fg, success, warning) = ( - theme.foreground, - theme.muted_foreground, - theme.success, - theme.warning, - ); + let (foreground, muted_fg) = (theme.foreground, theme.muted_foreground); let update_status = cx .try_global::() @@ -4721,66 +4722,6 @@ impl Tty7App { ); let phase_text = localized_update_phase(&update_status.phase); let check_for_updates = cx.global::().check_for_updates; - let install_cli_on_path = cx.global::().install_cli_on_path; - let (explorer_status, explorer_note) = self - .active_settings() - .map(|settings| { - ( - settings.explorer_context_menu_status.clone(), - settings.explorer_context_menu_note.clone(), - ) - }) - .unwrap_or(( - Ok(crate::core::explorer_context_menu::Status::Unsupported), - None, - )); - let ( - explorer_status_text, - explorer_status_color, - register_label, - register_disabled, - unregister_disabled, - ) = match explorer_status.as_ref() { - Ok(crate::core::explorer_context_menu::Status::NotRegistered) => ( - t(L10nKey::SettingsExplorerNotRegistered), - muted_fg, - t(L10nKey::SettingsExplorerRegister), - false, - true, - ), - Ok(crate::core::explorer_context_menu::Status::Registered) => ( - t(L10nKey::SettingsExplorerRegistered), - success, - t(L10nKey::SettingsExplorerRegister), - true, - false, - ), - Ok(crate::core::explorer_context_menu::Status::NeedsUpdate) => ( - t(L10nKey::SettingsExplorerNeedsUpdate), - warning, - t(L10nKey::SettingsExplorerUpdate), - false, - false, - ), - Ok(crate::core::explorer_context_menu::Status::Unsupported) => ( - t(L10nKey::SettingsExplorerUnavailable), - muted_fg, - t(L10nKey::SettingsExplorerRegister), - true, - true, - ), - Err(_) => ( - t(L10nKey::SettingsExplorerStatusUnavailable), - warning, - t(L10nKey::SettingsExplorerRegister), - false, - false, - ), - }; - let explorer_feedback = explorer_note - .as_ref() - .map(ExplorerContextMenuNote::localized) - .or_else(|| explorer_status.err()); let logo = Arc::new(Image::from_bytes( ImageFormat::Png, @@ -4943,107 +4884,6 @@ impl Tty7App { ), ), ) - .when(cfg!(windows), |page| { - page.child( - v_flex() - .mt_6() - .gap_2() - .child(self.section_rule(cx)) - .child( - div() - .text_sm() - .font_weight(FontWeight::MEDIUM) - .text_color(foreground) - .child(t(L10nKey::SettingsExplorerContextMenu)), - ) - .child( - div() - .text_sm() - .text_color(muted_fg) - .child(t(L10nKey::SettingsExplorerContextMenuDesc)), - ) - .child( - h_flex() - .gap_2() - .items_center() - .child(div().size_2().rounded_full().bg(explorer_status_color)) - .child( - div() - .text_sm() - .text_color(foreground) - .child(explorer_status_text), - ), - ) - .child( - h_flex() - .gap_2() - .child( - Button::new("explorer-menu-register") - .label(register_label) - .small() - .disabled(register_disabled) - .on_click(cx.listener(|this, _, _window, cx| { - this.register_explorer_context_menu(cx) - })), - ) - .child( - Button::new("explorer-menu-unregister") - .label(t(L10nKey::SettingsExplorerUnregister)) - .small() - .disabled(unregister_disabled) - .on_click(cx.listener(|this, _, _window, cx| { - this.unregister_explorer_context_menu(cx) - })), - ), - ) - .when_some(explorer_feedback, |section, message| { - section.child(div().text_xs().text_color(muted_fg).child(message)) - }) - .child( - div() - .text_xs() - .text_color(muted_fg) - .child(t(L10nKey::SettingsExplorerWindows11Note)), - ), - ) - }) - .child( - v_flex() - .mt_6() - .gap_2() - .child(self.section_rule(cx)) - .child( - div() - .text_sm() - .font_weight(FontWeight::MEDIUM) - .text_color(foreground) - .child(t(L10nKey::SettingsCommandLine)), - ) - .child( - div() - .text_sm() - .text_color(muted_fg) - .child(t(L10nKey::SettingsCommandLineDesc)), - ) - .child( - h_flex() - .gap_2() - .items_center() - .child( - crate::ui::theme::switch("install-cli-on-path", cx) - .checked(install_cli_on_path) - .on_click(cx.listener(|this, on: &bool, _w, cx| { - this.set_install_cli_on_path(*on, cx) - })), - ) - .child( - div() - .text_sm() - .text_color(foreground) - .child(t(L10nKey::SettingsInstallCliOnPath)), - ), - ), - ) .child( v_flex() .mt_6() @@ -5141,7 +4981,7 @@ mod tests { ("bell", Terminal), ("known_hosts", Ssh), ("claude", Agents), - ("right click", About), + ("symlink", Agents), ]; for (query, expected) in cases { assert_eq!( @@ -5153,18 +4993,16 @@ mod tests { } } + /// The `tty7` CLI exists so scripts and coding agents can drive tty7, so it + /// lives with the other agent integrations rather than under About. #[test] - fn explorer_context_menu_search_entry_uses_localized_keys() { + fn command_line_tool_is_searchable_under_agents() { let entry = settings_search_entries() .iter() - .find(|entry| entry.title == L10nKey::SettingsExplorerContextMenu) - .expect("Explorer settings should be searchable"); + .find(|entry| entry.title == L10nKey::SettingsSearchCommandLineToolTitle) + .expect("the CLI setting should be searchable"); - assert_eq!(entry.section.profile_label(), "settings:about"); - assert_eq!( - entry.keywords, - L10nKey::SettingsSearchExplorerContextMenuKeywords - ); + assert_eq!(entry.section.profile_label(), "settings:agents"); } #[test]