UI/细节调整

This commit is contained in:
zhongluofeng
2026-09-22 19:10:16 +08:00
parent b018abd922
commit bccd21fde5
135 changed files with 11393 additions and 5104 deletions
+388
View File
@@ -794,6 +794,394 @@ ZMODEM 剩余价值主要在串口/老旧嵌入式设备。成本收益不成立
3. 连接复用:同主机双标签秒连、关一个另一个不受影响、全关后连接断开。
4. ProxyJump 跳板链、端口转发 -L/-R、`.ppk` 导入、会话模板拉起、AI 助手(需翻译引擎配置)。
### 界面重构与标题口径(2026-09-20)
用户反馈两条:「本地 Shell 的标题显示成了全路径」「终端页像内嵌了一个独立终端页面」。
两条都成立,根因不同,分别处理。
#### 一、会话标题为什么曾经是全路径
标题有两个来源,后者会覆盖前者:
1. 创建时写入的标题(`terminal_open_local``ConPTYSession::spawn`);
2. shell 通过 OSC 0/2 上报的「自定义标题」。
`cmd.exe``powershell.exe` **默认就把自己的可执行文件全路径设成控制台标题**
cmd 运行命令时还会追加 ` - <命令>`),经 PTY 原样到达 → 标签栏与状态栏被顶成
`C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`。Windows Terminal /
VS Code 同样要过滤这类标题,属于同类问题。
**提权会话还多一层坑**:以管理员身份运行时标题前面会带一个本地化前缀
(简体中文下是 `管理员: `,英文是 `Administrator: `)。首版过滤只判「以盘符开头」,
日常(非提权)会话表现完全正确,只有提权运行时才漏 —— 因此
`osc_title_is_executable_path` 现在对「原串」与「剥掉前缀后的串」各判一次,
并有单测覆盖 `管理员: ` / `Administrator: ` / 全角冒号三种形态。
修复分四处:
| 改动 | 位置 | 理由 |
|---|---|---|
| 过滤「绝对路径 + `.exe`/`.com`」形态的 OSC 标题(含提权前缀) | `shell.rs::osc_title_is_executable_path` + `session.rs` | 判据收紧到绝对路径,WSL/Git Bash 的 `user@host: ~/dir``/usr/bin/htop` 必须保留 |
| **标题改用 `shell::session_label()`**(按可执行文件名取短名:`pwsh.exe``PowerShell 7``powershell.exe``PowerShell``cmd.exe``cmd``bash.exe``Git Bash`,认不出则退回 `name` | `shell.rs::session_label` | 标签要短、稳定、一行放下多个会话;设置页的 `name` 可编辑、可很长,两者诉求不同 |
| 设置页的探测名**保持不变**`PowerShell 7` / `Windows PowerShell` / `命令提示符` / `Git Bash` | `shell.rs::detect_shells` | 用户明确要求:命名问题在标签位置,不动设置里的命名 |
| 历史默认名迁移同时覆盖两代取值 | `shell.rs::legacy_default_names` | 合并规则是「保留用户设置的名字」,不迁移则默认名对老用户**永远不生效**;列表里含上一版误当作展示名的短名,用于把它们收回描述性默认名 |
状态栏随之调整:本地会话不再显示从 `ShellProfile::name` 拼出的副标题 ——
标题已经是短名(`PowerShell`),再并排一个 `Windows PowerShell` 会让人以为是两个东西。
SSH 不受影响(标题是主机名、副标题是 `user@host:port`,后者标题给不出)。
顺带修掉一个相邻缺陷:`terminal_rename_session` 设的标题会被下一条提示符的 OSC 标题
覆盖(表现为「重命名无效」)。现在 `set_title_by_user` 会置 `title_locked`
一旦用户显式命名,OSC 标题一律忽略;trait 方法名从 `set_title` 改为带 `_by_user`
后缀,从命名上堵住「程序自动改标题」这条误用路径。
#### 二、终端页为什么像内嵌独立应用
| 症状 | 处理 |
|---|---|
| 工具栏一行 + 会话标签一行,两套横向导航 | 合并为单行头部:左侧会话标签,右侧操作组(省 36px 垂直空间) |
| 工具栏写原始 `sessionId` | 从界面移除,改放标签的悬停提示(排障时仍可见) |
| 十来个别扭的图标按钮铺满一行 | 高频动作(搜索/字号/清屏/独立窗口)外露,其余入「更多」下拉 |
| 「+」只能新建上次用过的 shell,连主机要去主机页 | 改为「+▾」下拉:本地 Shell 列表 + 已保存主机 + 管理主机 |
| 七个页面头部高度/内边距/计数写法各不相同 | 新增 `TerminalPageHeader`h-11、icon size-4、`共 N …`),六页统一 |
| 父层与页面各套一层圆角边框 | 容器下沉到各页自身,历史页的双边框随之消失 |
| 「返回终端」「关闭」按钮与模块 Tabs 重复 | 移除;`close` 意图事件保留,由父层映射为切 tab |
| 唯一未接入 `useModuleTabs` 的模块 | 接入(标题栏浮动切换器 + tab 记忆 + **全局搜索跳转到指定页** |
#### 三、Tabs 由 7 项收敛为 5 项
同类模块(proxy / monitor / music / downloader / clipboard)的 TabsList 都不超过
5 项、统一 `max-w-md`;终端原有 7 项只能靠 `max-w-2xl` 撑开,每项被压得很窄,
横向比别处宽出半屏 —— 是「不像是同一套导航」的直接来源之一。
| tab | 组成 | 说明 |
|---|---|---|
| 终端 | 会话(xterm + SFTP + 状态栏) | forceMount,切走不卸载 |
| 主机 | SSH 主机管理 | 不变 |
| 凭据 | 密钥 + 已知主机 | 都是「SSH 信任材料」,同一类列表 |
| 记录 | 命令历史 + 命令片段 | 都是「命令资产」,同一类用途 |
| 设置 | 外观 / 布局 / 选择 / 安全 / 快捷键 / Shell | 不变 |
合并的前提是**子分区仍能被直接到达**,否则用户要在一页里找两块内容,比多一个 tab 更糟。
因此新增 `TerminalSectionSwitch`(按钮组实现,视觉规格对齐 `TabsList`/`TabsTrigger`
不是嵌套 reka Tabs —— 那会带来两层键盘导航语义)。分区状态由 `TerminalModule`
持有(`credSection` / `recordSection`),面板卸载重挂不丢「上次看的是哪一半」。
**已知取舍**`SearchIndexItem.tab` 只能表达「哪个 tab」,无法表达「tab 里的哪个分区」,
所以全局搜索「已知主机」会落在「凭据」页的密钥分区,需再点一次页内开关。
要精确落位需给 `moduleTabsStore` / `useModuleTabs` / `TitleBar` 三处共享基建加字段,
收益不抵成本,故不做(已在 `index.ts` 的注释里写明)。
未改动的部分及原因:底部状态栏保留(终端习惯,且它是唯一承载编码/尺寸/cwd 的位置);
会话标签仍可拖动重排;会话面板仍常驻挂载用 `v-show` 切换(见模块文件头)。
`useModuleTabs` 的浮动切换器在终端模块**实际不会出现** —— 模块根是
`overflow-hidden`,页面不滚动,TabsList 不会滚出可视区。接入的实际收益是
tab 记忆与全局搜索跳转(此前搜索「SSH 密钥」只切模块、停在终端页)。
#### 验证
| 检查 | 结果 |
|---|---|
| `cargo check` | exit 0 |
| `cargo test --lib terminal::` | shell.rs 新增 5 个单测(提权前缀 / 有效标题保留 / 短名取值 / 兜底 / 历史默认名) |
| `vue-tsc --noEmit` | exit 0 |
| `vite build` | 成功 |
| 待真机确认 | 提权 PowerShell 标签显示为 `PowerShell`;重命名后不再被覆盖;窄窗口下头部不打架;5 个 tab 与页内分区开关的可达性 |
### 容器口径统一与终端页拆分(2026-09-20 第二轮)
峰提出:「终端模块的结构完全是单独的内部窗口,各 tabs 的内部窗口用同一个框架,
与其他模块 UI 模式差别很大」。勘察确认成立,并定位到三处偏离。
#### 一、偏离的取证(改动前)
| # | 偏离 | 事实 |
|---|---|---|
| 1 | 每页多一层**内嵌窗口框** | `h-full flex flex-col rounded-lg border border-border bg-background overflow-hidden`,全项目**仅终端使用**,共 7 处(终端页 + 6 个子面板) |
| 2 | 每页多一条**页中页标题栏** | `TerminalPageHeader`h-11 + border-b + px-4);6 个子面板各自手写同一套外框再内嵌该头部 |
| 3 | 滚动实现不统一 | 子面板用原生 `overflow-y-auto`,其它模块统一用 `ScrollArea`,滚动条外观不同 |
对照口径(proxy / monitor / music / clipboard / downloader 一致遵守):
模块根 `h-full p-6` → TabsList 包 `div[ref=tabsListRef]``max-w-md !bg-transparent`
`TabsContent flex-1 min-h-0 mt-4 tab-animate` → 内容**直接铺开**
`ScrollArea` + Card 流 / 列表项),**没有任何容器边框**。
#### 二、改动
| 项 | 内容 |
|---|---|
| 新增 `components/layout/PageToolbar.vue` | 漂浮工具条(无边框、无底边、`mb-3`),API 与原 `TerminalPageHeader` 一致(icon / title / meta / 默认槽=动作 / info 槽);提升到 layout 层,其它模块可复用 |
| 6 个子面板 | 去外框(保留 `flex flex-col h-full overflow-hidden`);`TerminalPageHeader``PageToolbar``overflow-y-auto``ScrollArea`padding 移到 ScrollArea 上(沿用 proxy/monitor 的 `pr-3` 写法) |
| 删除 `components/TerminalPageHeader.vue` | 已被 `PageToolbar` 取代,避免留两份近似组件 |
| 新增 `components/TerminalWorkspace.vue` | 终端页整体:工具条 + 终端屏幕(画布 / SFTP / 状态栏)+ 右键菜单 + 4 个对话框 |
| `TerminalModule.vue` 1197 → 383 行 | 只剩模块级编排:5 个 tab、tab 记忆、快捷键分发、初始化、主机密钥弹窗 |
#### 三、两个关键取舍
**1. 终端屏幕保留边框,但边界从「整页」收缩到「画布 + 状态栏」。**
xterm 是画布而不是内容流,需要明确的视觉边界;但此前边框包住的是
「标签行 + 主体 + 状态栏」整体,框内还有一条 h-11 标题栏 —— 那正是
「内嵌窗口」的观感来源。现在会话标签与操作组移到框外成为页面级工具条,
边框只包画布与状态栏。
**2. `useTerminalStream` 全模块只创建一次。**
该 composable 的 `tabOrder` / `panes` / `activeSessionId` 是**每次调用各创建一份**的
局部状态。拆分后若模块根与终端页各调一次,会出现两套互不相干的标签顺序与分屏布局
(表现为「点击标签没反应」)。因此实例在模块根创建,经 prop 传给 `TerminalWorkspace`
终端页里由模块级快捷键触发的动作(复制/粘贴/搜索/清屏/分屏/独立窗口/SFTP/重命名)
通过 `defineExpose` 的显式清单暴露,由模块根做一次转发。
#### 四、约束(后续改动须遵守)
1. **`h-9` 是卡片内的横向分隔线高度**:控制栏 / 状态栏 / `SftpPanel` 头部同为 h-9
改其一必须同步(原契约是「SftpPanel 头部对齐卡片外的工具条行 h-11」,2026-09-21
控制栏收进卡片并降为 h-9 后该层已不存在)。
2. **xterm 尺寸链不能断**`TabsContent → 工具条 + 屏幕容器 → 画布` 全靠 `flex-1 min-h-0`
3. `ModuleContainer.vue``[data-main-scroll] … > div { height: 100% }` 是给全高模块(终端/翻译)补高度链的补丁,勿删。
4. `TerminalWindow.vue`(独立窗口)与主窗口共用 `TerminalPane` / `TerminalStatusBar`,共享逻辑留在 `useSessionStream` 与 store,不下移到页面组件。
#### 五、非终端页与其它模块的口径对齐(2026-09-21)
第二轮对齐只动**观感与组件选型**,不改终端页(画布 + 状态栏)的结构:
1. **外层 padding 归模块根**:根容器 `p-6``p-5`,各页不再自带 `p-3/p-5`
—— 此前「根 24px + 页内 12/20px」双重内缩,列表左边缘比其它模块多缩 16px。
分栏页(片段、设置)的沟槽也由模块根提供:`UI_DESIGN_SYSTEM.md` §3.4 的
「split 无 padding」指**分栏容器自身**不加 padding,不是让页面顶到窗口边缘。
2. **列表一律用 `Card` 承载**(与 proxy「订阅列表」同口径):`Card` + `CardContent`
内部行保持 `rounded-md border`;页名与计数仍由 `PageToolbar` 承担,不再加 `CardTitle`
3. **表单控件走 `@/components/ui`**`Input` / `Select` / `Textarea` / `Checkbox`),
不再出现裸 `<input>` / `<select>` / `<textarea>``SftpPanel` 的远程文件编辑器是
文档显式豁免,保持原生 `<textarea>`
4. **四态与徽章用 `@/components/common`**:空/加载用 `StateBlock`(注意它在 ScrollArea 内
不会垂直居中,需显式 `min-height`),状态类徽章用 `StatusBadge`
纯计数的内联标记(历史 `×N`、Shell kind)保持 `text-[11px]` 文本,
避免 StatusBadge 的 20px 最小高度把行高抬起来。
5. **设置页用 `SettingGroup` + `SettingRow`**(本仓库首批真实消费者):分组自带卡片与
`divide-y`,行内不要再加边框/分隔;下拉/滑杆/输入这类表单项按 §3.4 的
「label 在上 + 控件全宽」纵排,不塞进 `SettingRow` 的值区。
控制列保留 `max-w-3xl`(§3.3 的例外:`SettingRow``justify-between`
不限宽会把标签与控件拉到 900px 之外)。
6. **分栏页保留页内滚动**:设置页/片段页的左右栏各自滚动(§3.4 split 模式),
不改外部滚动 —— 模块根有 `overflow-hidden`,改外部滚动会让左栏导航跟着滚走
`sticky` 会被祖先的 `overflow-hidden` 杀死)。
7. **历史页的键盘守卫**`onKeydown` 挂在外层,对 Enter/↑↓ 无条件 `preventDefault`
焦点落在按钮/下拉触发器/文本域上时先让路(`closest('button,[data-slot="select-trigger"],select,textarea')`),
否则换成 `Select` 后一次 Enter 会「先展开下拉、再把选中命令填进终端」。
搜索框不在让路之列:在那里 ↑↓ 选条目、Enter 填入是刻意保留的操作方式。
#### 六、验证
| 检查 | 结果 |
|---|---|
| `vue-tsc --noEmit` | exit 0 |
| `vite build` | 成功(28.00sTerminalModule chunk 160.34 kB |
| 待真机确认 | 6 个子页去框后与其它模块观感是否一致;终端屏幕的边界感;SFTP 面板与工具条行的对齐;分屏 / 右键菜单 / 搜索 / 会话模板 / AI 助手行为未变 |
| 第二轮待真机确认 | 列表卡片化与 20px 沟槽;历史页在搜索框/来源下拉里按 Enter 的行为;设置页 8 个分区的行对齐;片段页右栏滚动条为自绘 |
### 控制栏收进终端卡片与控件规范化(2026-09-21 第三轮)
峰提出:「控制栏放在卡片外面不对」、「按钮 hover 应是统一的主题色字 + 半透明主题色底,
看来有遗漏」、「字号三连图标改成『字体』按钮 + popover,图标按钮用 tooltip 而不是 title」、
「状态栏调到与控制栏同高、字号调大、编码换成 popover」。
#### 改动
| 项 | 前 | 后 |
|---|---|---|
| 控制栏位置 | 卡片**外**的页面级工具条(h-11,`mt-1` 与卡片分隔) | 卡片**内**顶部(h-9 + `border-b`),与画布、状态栏同属一张「终端窗口」 |
| 图标按钮 | 自定义 `.icon-btn`hover = `--accent` + `--foreground` | `Button variant="ghost"` —— 全项目唯一口径 `--primary-soft` + `--primary-soft-text` |
| 按钮提示 | 原生 `title` | `Tooltip` + `TooltipContent`(菜单触发器用 `:disabled="open"`,避免提示压在菜单上) |
| 字号 | 三个并排图标(缩小 / 放大 / 重置) | 「字体」按钮 + popover:滑块(6–48+ 当前值 + 重置 |
| 状态栏 | h-6 / 10px / 图标 `size-3` | h-9 / 12px / 图标 `size-3.5` |
| 编码 | 原生 `<select>` | `Popover` 选项列表(当前项打勾 + 非默认编码提示) |
| `SftpPanel` 头部 | h-11(对齐卡片外的工具条行) | h-9(对齐卡片内的两条栏) |
#### 取舍
1. **字号滑块复用 `bumpFont(delta)`**,不新增 `setFont` 接口:`bumpFont` 内部以
「临时覆盖值 ?? 设置值」为基准并收敛到 6–48,与状态栏展示的 `fontSize` 同源,
因此「基准 + 增量 = 目标值」恒成立。
2. **编码的浮层反而是更稳的选择**2026-09 早期用原生 select 的理由是
「shadcn 的浮层会被终端容器裁剪」,但 `PopoverContent``PopoverPortal`(挂 body),
不受卡片 `overflow-hidden` 影响 —— 该顾虑不成立,原生 select 的样式代价则一直存在。
3. **高亮态加在 svg 上**`Button.ghost` 已给按钮设 `text-fg-secondary`
同元素再写 `text-primary` 的胜负取决于 Tailwind 输出顺序;`[&_svg]:text-primary`
落在子元素上确定覆盖继承色(「日志记录中」的 ⋯ 用它)。
#### 验证
| 检查 | 结果 |
|---|---|
| `eslint src/modules/terminal` | exit 0 |
| `vue-tsc --noEmit` | exit 0 |
| 待真机确认 | tooltip 与下拉菜单不叠加;滑块拖动跟手且「重置」回落;编码浮层在卡片/独立窗口内均不被裁剪;SFTP 头部与上下栏等高 |
### 独立窗口与 Ctrl+3 快捷新建(2026-09-21 第四轮)
峰提出:「控制栏的 tooltip 似乎和点击事件冲突,新建会话/更多操作点不动」、
「独立窗口复用终端页内部窗口,再加最小化/最大化/关闭」、「加一个 Ctrl+3 直接开独立窗口,
打开时列出 SSH + 本地终端 + 新建的选择」。
#### 一、修 tooltip 与菜单触发器冲突
现象:控制栏的「新建会话」「更多操作」点击后毫无反应(菜单不出现)。
三轮定位过程:① `TooltipTrigger as-child` 直接包 `DropdownMenuTrigger as-child`(同时给
`DropdownMenu` 绑了 `v-model:open` 以便展开时收起提示);② 中间夹一层真实 `<span>` 作锚点
(仍带着 `v-model:open`);③ 去掉 Tooltip 与 `v-model:open` —— 菜单恢复可用。
结论:**受控 `open` 是元凶**。给 `DropdownMenu`(本项目对 `DropdownMenuRoot` 的封装)
`v-model:open` 后菜单点不开;Tooltip 只要锚在**包裹元素**上(而不是把菜单触发器当作
它的 as-child 子节点)就没有副作用。最终形态:
```
Tooltip > TooltipTrigger as-child > div(包裹元素)
└─ DropdownMenu @update:open(仅监听,不绑 open
└─ DropdownMenuTrigger as-child > Button
```
两个菜单触发器(本组件的「更多操作」、标签栏的「新建会话」)现在都有 Tooltip,
文案统一 `side="bottom"`(控制栏在卡片/窗口最顶部,向上弹会被裁掉)。
**约束**:① 本项目不给 `DropdownMenu``v-model:open`(需要开合状态时只监听
`@update:open`);② Tooltip 的锚点用普通元素,不要直接套在菜单/弹层触发器上。
#### 二、独立窗口 = 单会话的终端卡片
| 项 | 前 | 后 |
|---|---|---|
| 结构 | h-8 标题栏(菜单图标 + 会话名 + 窗口按钮)+ 画布 + 状态栏 | 与终端页同构:控制栏(h-9)+ 画布 + 状态栏 |
| 控制栏左端 | — | 会话身份(图标 + 名称 + 目标),**兼窗口拖拽区**(`data-tauri-drag-region` + 子元素 `pointer-events-none` |
| 控制栏右侧 | — | 复用的 `TerminalToolbar``standalone` 模式)+ 最小化/最大化/关闭 |
| Tooltip | 无(窗口是独立入口,没有 TooltipProvider | 窗口根加 `TooltipProvider` |
| 主题 | **不跟随主窗口**(窗口不加载 appStore,也没有各自的 applyTheme 副本) | 补上独立窗口的 `applyTheme`:读同一份 `localStorage` 设置 → `win.setTheme` + `dark` class + `applyStoredAccent`,并监听 `storage` / `prefers-color-scheme` 跟随主窗口改动(与快速面板、剪贴板弹窗、下载窗口同一套做法);**不应用 mica/acrylic**(本窗口是不透明窗口,见 `terminal/window.rs` |
| 提示条 | 无 Toaster`toast.*` 静默失效) | 自带 `Toaster`Toaster 是窗口级的) |
`standalone` 模式隐藏三类只对主窗口终端页成立的动作:独立窗口按钮(自己就是那个窗口)、
文件面板 / 端口转发 / 会话模板(它们开的是终端页的面板与分屏);保留搜索、字体、清屏、
重命名(文案改「重命名会话」)、AI 助手、会话日志。
**取舍**:窗口仍是「一个会话 ↔ 一个窗口」,不渲染标签栏 —— 主窗口那侧的「已在独立窗口打开」
提示条与标签上的「独立」徽标以此为准。控制栏动作与 TerminalWorkspace 有**少量有意重复**
(搜索/清屏/重命名/日志):那边还要管分屏与 SFTP,抽共享 composable 会把状态搅在一起。
#### 三、Ctrl+3:全局快捷键 + 选择框
**最终形态(2026-09-21 第二轮调整后)**
- **先开窗、再问**:快捷键触发 → 立刻创建「选择窗口」(`#new-terminal-window`
`NewTerminalWindow.vue`label 前缀 `terminal-window-new-` 以命中同名 capability
→ 用户在**这个窗口里**选本地 Shell / SSH 主机 → 建会话 →
`terminal_detach_session` 交给 Rust 用规范 label`terminal-window-<id>`)开真正的会话窗口
→ 选择窗口关闭(两者同尺寸同居中,视觉上是「原地换成了会话窗口」)。
**不再经过主窗口弹窗**,也不再把主窗口拉到前台 —— 会话窗口由 Rust 创建且此时还没有
sessionId,窗口 label 在 Tauri 里不可改,所以「选择窗口」与「会话窗口」只能是两个窗口。
想要零切换需要新增 Rust 命令(把已有窗口认领为某会话的窗口),当前未做。
- **键位可自定义**`newDetachedWindow` 作为一条动作放进 `TERMINAL_ACTIONS`
于是它跟着终端设置一起存、一起改(设置 → 快捷键 → 会话 → 新建独立窗口,默认 `Ctrl+3`
与其它键位一样支持录制与开关);Rust `default_shortcuts()` 同步加一条,`heal()` 会为老配置补齐。
`TerminalQuickLaunch`(常驻 App 层)读该键位并用 `plugin-global-shortcut` 注册,
键位变化即重新注册;关掉该行开关 = 不注册。
- **终端内分发不处理它**:全局注册在应用前台时同样生效,两处都处理会开出两个窗口,
因此 `TerminalModule``onAction` 里是一个带注释的空分支。
#### 四、窗口首帧不再闪白(2026-09-21 第二轮追加)
现象:深色模式下打开独立窗口 / 选择窗口,会先出现一个纯白窗口,随后才变深色。
原因:两个窗口都是**可见创建**的 —— WebView 初始底色是白的,而深浅色主题要等前端
读到 `localStorage` 设置后才应用。窗口在主题生效之前就已经显示出来了。
做法(沿用本项目的既有范式,见 `translate/popup.rs` 的预创建):
| 窗口 | 创建方式 | 显示时机 |
|---|---|---|
| 会话窗口(Rust `terminal/window.rs` | `visible(false)` + `focused(false)`(新增) | `TerminalWindow.vue``onMounted``applyTheme()``show()` + `setFocus()` |
| 选择窗口(前端 `openWindow.ts` | `visible: false` + `focus: false` | `NewTerminalWindow.vue` 同上 |
同时两个窗口的 `applyTheme()` 里补了 `win.setBackgroundColor(--background 的值)`
`show()` 那一刻 DOM 可能还没绘制完,窗口底色是主题色才不会露出白底。
capability 因此多了 `core:window:allow-show``terminal-window.json`)。
`show()` 放在主题之后、会话校验之前 —— 即使后面初始化失败(会话已结束等),
窗口也已可见,错误提示才看得见。
#### 五、设置跨窗口同步与「跟随应用主题」修复(2026-09-21 第三轮追加)
峰提出三件事:独立窗口不跟随主窗口改的终端配色;其它设置要不要/能不能同步;
「跟随应用主题」开关像没生效(改应用深浅时终端不变)。
**1. 跨窗口同步(原来完全没有)**
终端设置存在 Rust 侧,而每个窗口各有一份前端副本 —— 主窗口改完,已打开的独立窗口
永远用旧值。现在:Rust 在 6 个保存命令(settings / appearance / layout / selection /
security / shortcuts)后广播 `terminal-settings`(无负载;`save_settings` 上没有 AppHandle
故在命令层发);前端 `terminalStore` 监听该事件 → `loadSettingsInner()` 重读 →
`effectiveAppearance` / 状态栏显隐等随之更新。**能同步的就是这些**:外观、布局、选择、
安全、快捷键;主机 / 密钥 / 片段等列表数据在各自面板打开时重读,不需要推送。
**2. 「跟随应用主题」此前是死开关**
`followAppTheme` 从来没被任何代码读过(主题只按 `resolveTheme(appearance.theme)` 解析),
`resolveTheme('system')` 读的是**非响应式**的 DOM class —— 应用切深浅时没人重算。
现在按 Rust 字段说明的原始意图实现:**开关打开时,所选主题只作亮/暗取色基准**
`vscode-dark↔vscode-light``solarized-dark↔solarized-light`Dracula / One Dark 只有
深色,浅色下回落 VS Code 浅色);关闭时用所选主题本身(`system` 按当时深浅解析一次)。
实现要点:新增响应式 `appIsDark``terminalThemes.ts`),由 appStore 与各独立窗口的
`applyTheme` 写入;`useSessionStream.effectiveAppearance``effectiveThemeName()` 解析出
**具体主题名**后再交给面板,于是应用改深浅 → computed 重算 → xterm 换皮肤。
**3. 顺带修掉陈旧默认值**Rust 默认 `theme``thing-dark`(一套从未实现的主题,
解析时静默回落到 vscode-dark,用户会以为「选了主题不生效」)。默认改为 `system`
`heal()` 里把不认识的 key 一律收敛到 `system`
#### 六、设置项逐个核对与修复(2026-09-21 第四轮追加)
峰报了两个具体问题(背景不透明度无效、独立窗口右键无反应),并要求核对其余设置。
逐字段核对后确认了**一批「设置了但没有任何代码读它」的死设置**,以及若干只在终端页
生效、独立窗口漏掉的行为。
**已修**
| 问题 | 原因 | 修法 |
|---|---|---|
| 背景不透明度无效 | `appearance.opacity` 从没被读过 | `allowTransparency: true`(构造时固定)+ 主题背景写成带 alpha 的 8 位 hex`themeWithOpacity`);不透明度 < 100 时**主动丢弃 WebGL**WebGL 渲染器不认 `allowTransparency`,否则看起来仍是没反应);面板容器底色同带 alpha,避免四周取整边缘颜色不一致 |
| 独立窗口右键无反应 | 右键菜单只是终端页里的一段内联标记;`main.ts` 又全局 `preventDefault` 了原生菜单 | 菜单抽成共用组件 `TerminalContextMenu.vue`,两个宿主都接 `@contextmenu`menu / paste / select-word 三种行为一致) |
| 独立窗口不尊重「显示状态栏」 | 窗口无条件渲染状态栏 | 加 `showStatusBar !== false` 守卫 |
| 独立窗口复制忽略「去掉末尾换行」 | 窗口内联硬编码 `replace(/\n+$/,'')` | 键盘与菜单统一走 `doCopy()` |
| 「面板数量上限」下拉改不动 | `<Select :model-value>` 没有 `@update:model-value` | 补上处理 |
| 字号滑杆可拖到后端不认的值 | UI 6–48,后端 heal 只认 840 → 静默改回 14 | UI 收敛到 840 |
| 快捷键表两侧不一致 | `history` 只在 Rust 有(设置页看不到,保存时会被抹掉);`detachWindow` 只在设置页有(Rust `heal()` 会剔除) | 两侧都补齐;`heal()` 只保留默认表内 action,这条约束写进了注释 |
**死设置:全部实现(同日追加)**
上一版列出的 8 项「设置了但没有代码读它」的字段,按「让设置真的驱动行为」逐项落地。
原则:**不删除用户能看见的开关**(除确实无宿主的两项),把开关接到已有链路上。
| 设置 | 修法 |
|---|---|
| `layout.confirmCloseRunning` | `closeTab()` 加守卫:关闭时**连 `sessionAlive` 查询都不发**(那是一次 IPC + 后端进程探测),直接关 |
| `layout.inheritCwd` | `newLocalTab()` 在调用方未显式传 cwd 时取**当前激活会话**的 cwd;只对**本地**会话生效 —— SSH 的 cwd 是远端路径,本地 shell 起不来(Windows 上 `/home/ops` 会被判非法目录) |
| `layout.sidebarOpen` / `sidebarWidth` | 无宿主(终端模块的侧栏是 host 面板的 tab,不是可折叠轨道)→ 从 Rust `LayoutSettings`、前端类型、设置页草稿三处**删除**;容器级 `#[serde(default)]` 让旧配置里的多余字段被忽略,无需迁移 |
| `selection.copyOnSelect` | 在 `TerminalPane`**mouseup**(仅左键)写剪贴板,复用 `trimTrailingNewline`。刻意不放 `onSelectionChange`:拖动过程中它每次变化都触发,会把一次拖选切成几十次剪贴板写入 |
| `selection.middleClickPaste` | `TerminalPane``@mousedown.capture.middle``preventDefault` 挡掉 Chromium/Windows 的「中键自动滚动」,捕获阶段 `stopPropagation` 让 xterm 的鼠标状态机完全看不到这次中键;因事件被吞,焦点由处理函数自己 emit |
| `security.hostKeyPolicy` | `check_server_key` **显式读取**该字段(此前写死走 ask)。唯一合法取值仍是 `"ask"`,其它取值按 `"ask"` 处理并记日志——安全开关的失败方向必须朝更严一侧;`heal()` 同时把非法值收敛回 `"ask"`(修数据 + 防空两道保险) |
| `security.blockOnFingerprintChange` | 关闭时「与记录不符」**不再弹确认**,改为自动 `hostkey::accept`(旧指纹进变更历史,事后仍可查出「密钥曾被悄悄换过」)+ 记一条 warn。开着时行为不变(红框阻断式确认) |
| `security.auditLog` | 原来描述与实现是两件事:字段注释写的是「输入输出落盘」,而 UI 写的是「主机密钥确认 / 指纹变更等事件写日志」。按 **UI 的口径**实现(会话输出落盘由工具栏的 `terminal_toggle_logging` 按会话显式开启,`audit` 模块的取舍说明里已论证过「不自动记录屏幕内容」)。`SshHandler::audit()` 单入口收敛五类事件(已信任 / 变更 / 变更被接受 / 首次接受 / 自动接受),命中前先判开关;`terminal_confirm_host_key` 也记一条用户的接受/拒绝(该命令新增 `app` 参数,specta 会把 `AppHandle` 从 JS 签名剔除,前端调用不变)。**默认值 `false` 改 `true`**:只写事件行、不含屏幕内容,没有隐私代价,默认开才能保证事后可查 |
顺带修掉一处陈旧文案:布局页「面板数量上限」的说明还写着「分屏功能在后续版本提供」,
而分屏早已实现,改为说明真实含义与封顶 4 的原因。
#### 验证
| 检查 | 结果 |
|---|---|
| `eslint src` | exit 0 |
| `vue-tsc --noEmit` | exit 0 |
| `cargo check`src-tauri | exit 0 |
| 待真机确认(第四轮 · 死设置) | 关闭「关闭运行中的会话前确认」后关标签不再弹窗;开启「新会话继承当前目录」后新建本地标签落在当前 cwd(SSH 会话下不继承);「选中即复制」选中后剪贴板有内容、「中键粘贴」中键一次只粘一次;关掉「指纹变更时阻断连接」后换过密钥的主机直接连上且日志有 warn;关掉「记录安全事件」后日志不再出现主机密钥事件行;布局页不再有陈旧文案 |
| 待真机确认(第三轮) | 新建/更多菜单可点开、tooltip 正常;独立窗口拖拽/最小化/最大化/关闭;Ctrl+3 从其它应用按下能唤到前台并建出窗口;终端模块禁用后 Ctrl+3 不再占用;深色模式下开窗不再闪白;主窗口改终端配色/字号后独立窗口即时同步;应用切深浅时终端画布一起变 |
## 附:P1 新增命令清单(供前端对接与后续维护)
| 命令 | 参数 | 返回 |