Compare commits

...
2 Commits
Author SHA1 Message Date
Bill 7bccd5303a feat(workspace): 模式按钮双击收展侧栏
CI / typecheck + test + build (windows) (push) Waiting to run
图标栏的「终端工作区 / SSH 工作区」按钮新增双击手势:双击当前工作区
按钮展开/收起侧栏;单击(含双击另一个按钮)只切换工作区,侧栏不动。

判定逻辑抽成零 import 纯模块 modeRailGesture.ts,用浏览器原生
MouseEvent.detail 连击计数识别手势——detail=2 到达时 store 已被
第一击更新,故在 detail=1 时把「手势前模式」存入 ref 再比对。

新增 tests/mode-rail-gesture.mjs 表驱动测试(10 例),测试数 35→36。
2026-10-20 23:59:28 +08:00
Bill eac4704e9e docs(website): 更新站点至 v1.0.30
CI / typecheck + test + build (windows) (push) Waiting to run
Deploy website to GitHub Pages / deploy (push) Waiting to run
- hero 下载按钮/版本号、spec 版本号、下载表文件名与直链改为 1.0.30
- 更新日志新增 1.0.30、1.0.29 两节,按规则裁到最近 5 条
- i18n.js 四语言块补齐新键并移除被裁版本(1.0.24 / 1.0.25)
- README 保留列表同步为实际内容
2026-10-20 15:12:56 +08:00
9 changed files with 385 additions and 73 deletions

No files matched your search

+2 -2
View File
@@ -7,10 +7,10 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
- 开发:`npm run dev`(主进程改动不热重建,需重启)
- dev 实例使用独立用户数据目录 `%APPDATA%\OpenTerminal-dev` 与独立单实例锁(`src/main/index.ts` 顶部 `!app.isPackaged` 分支),窗口标题带 `(dev)`:**可与已安装的正式版同时运行,互不干扰**,也不会把测试设置/会话写进真实配置
- 类型检查:`npm run typecheck`(tsconfig.node.json + tsconfig.web.json;只看渲染层可单跑 `npx tsc --noEmit -p tsconfig.web.json`)
- 测试:`npm test`(**npm 生命周期先自动跑 `pretest` 做类型检查**,再 `node tests/build-bundles.cjs` 重建 esbuild bundle,然后依次跑可离线运行的 35 个测试:ssh-loopback、commands-store、connections-store、settings-store、local-path-grants、lock-store、lock-controller、lock-shortcuts、reserved-accelerators、terminal-title、ipc-guard、updater-fallback、log-sanitizer、app-log、sftp-timeout、hl-split-smoke、hl-rules、redos-probes、rule-validation、redos-validator、perf-diagnostics、zmodem-e2e、ssh-session-e2e、sysinfo-e2e、terminal-cwd、terminal-links、broadcast-store、write-batcher、refit-throttle、semantic-theme、theme-contrast、css-theme-vars、ot-var-names、font-stack、link-rules;真实服务器测试需 JD_* 凭据,不在此列)。其中渲染层纯函数测试(semantic-theme、theme-contrast、ot-var-names、font-stack、write-batcher、refit-throttle、perf-diagnostics、redos-probes、rule-validation、broadcast-store、terminal-cwd、terminal-links、terminal-title、hl-split-smoke、hl-rules、link-rules 等)用 Node 22.18+ 的原生 TS 类型剥离**直接 import `.ts` 源文件**(不经 esbuild bundle),Node 20/22.17 以下会 `ERR_UNKNOWN_FILE_EXTENSION`
- 测试:`npm test`(**npm 生命周期先自动跑 `pretest` 做类型检查**,再 `node tests/build-bundles.cjs` 重建 esbuild bundle,然后依次跑可离线运行的 36 个测试:ssh-loopback、commands-store、connections-store、settings-store、local-path-grants、lock-store、lock-controller、lock-shortcuts、reserved-accelerators、terminal-title、ipc-guard、updater-fallback、log-sanitizer、app-log、sftp-timeout、hl-split-smoke、hl-rules、redos-probes、rule-validation、redos-validator、perf-diagnostics、zmodem-e2e、ssh-session-e2e、sysinfo-e2e、terminal-cwd、terminal-links、broadcast-store、write-batcher、refit-throttle、semantic-theme、theme-contrast、css-theme-vars、ot-var-names、font-stack、link-rules、mode-rail-gesture;真实服务器测试需 JD_* 凭据,不在此列)。其中渲染层纯函数测试(semantic-theme、theme-contrast、ot-var-names、font-stack、write-batcher、refit-throttle、perf-diagnostics、redos-probes、rule-validation、broadcast-store、terminal-cwd、terminal-links、terminal-title、hl-split-smoke、hl-rules、link-rules、mode-rail-gesture 等)用 Node 22.18+ 的原生 TS 类型剥离**直接 import `.ts` 源文件**(不经 esbuild bundle),Node 20/22.17 以下会 `ERR_UNKNOWN_FILE_EXTENSION`
- 依赖分类规则:**只有 `src/main/`/`src/preload/` 实际 import 的包才能进 `dependencies`**(node-pty/ssh2/zmodem.js/font-list/electron-updater);纯渲染层依赖一律 devDependencies(Vite 全量打包进 out/renderer,`externalizeDepsPlugin` 不作用渲染层)——这条让 asar 从 98MB 瘦到 8.1MB,加新依赖时别放错边
- 下载量统计:`node scripts/download-stats.cjs`(Gitea + GitHub release 资产的 download_count 汇总;GitHub 优先直连、失败自动回退 `HTTPS_PROXY`/本地 7897;更新通道无计数接口不计入)
- 打包:`npm run dist`(**生命周期先自动跑 `predist` → `npm test`,即类型检查 + 35 个离线测试全部通过后才 build/package**,typecheck 全程只跑一次;`electron-vite build` 之后、electron-builder 之前会跑 `node tests/worker-artifact.mjs`——真实加载 `out/main/redosProbeWorker-*.js` 并回包,防止 `?nodeWorker` 产物静默丢失;predist 末尾的 `npm install --package-lock-only` 会把 `package-lock.json` 根版本号对齐 `package.json`,**发布提交必须带上 package-lock.json**),产物在 `release/`(exe + latest.yml + blockmap)
- 打包:`npm run dist`(**生命周期先自动跑 `predist` → `npm test`,即类型检查 + 36 个离线测试全部通过后才 build/package**,typecheck 全程只跑一次;`electron-vite build` 之后、electron-builder 之前会跑 `node tests/worker-artifact.mjs`——真实加载 `out/main/redosProbeWorker-*.js` 并回包,防止 `?nodeWorker` 产物静默丢失;predist 末尾的 `npm install --package-lock-only` 会把 `package-lock.json` 根版本号对齐 `package.json`,**发布提交必须带上 package-lock.json**),产物在 `release/`(exe + latest.yml + blockmap)
- GitHub Actions:`.github/workflows/ci.yml` 在 windows-latest + Node 22 上跑 `npm ci` / `npm test`(含 pretest typecheck)/ `npm run build`,只做验证,不打包安装器、不发布
- 国内网络需镜像:`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ npm run dist`
- 依赖一致性闸门:`predist` 开头跑 `node scripts/verify-deps.cjs`(逐条比对 `node_modules` 与 `package-lock.json` 的版本,跨平台 optional 依赖缺失跳过;不一致就 exit 1)——`npm install --package-lock-only` **只重算 lockfile、不碰 node_modules**(本版 npm 还会把 `node_modules/.package-lock.json` 一并重写成理想树,制造「已经装好了」的假象),所以**改动依赖版本后必须真实 `npm install` 或 `npm ci` 再构建**,别指望 lockfile 对齐就等于树里换了包;v1.0.21 的中文输入法回归正是这个坑(`@xterm/xterm` 锁 6.1.0-beta.304,树里还是 6.0.0,打进去的是旧代码)
@@ -0,0 +1,237 @@
# 侧栏模式按钮双击收展侧栏 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 图标栏的「终端工作区 / SSH 工作区」模式按钮新增双击手势:双击当前工作区按钮 = 展开/收起侧栏;单击(含双击另一个按钮)只切换工作区。
**Architecture:** 不挂 `onDoubleClick`,直接在按钮 `onClick` 里用浏览器原生 `MouseEvent.detail` 连击计数判定手势。关键约束:双击时序是 `click(detail=1) → click(detail=2) → dblclick`,detail=2 到达时 zustand store 已被第一击更新,因此「手势前模式」必须在 detail===1 时存进 ref。判定逻辑抽成零 import 纯模块(仓库惯例,Node 原生类型剥离直连测试)。
**Tech Stack:** React 19.2.8 / zustand 5.0.15 / TypeScript strict / Node ≥22.18 原生 TS 剥离测试。
**已核实前提**(子代理两轮核实,全部属实):
- `ModeRailButtons`(Workspace.tsx:1545)与 `IconRail`(:1506)全链路无条件渲染、无 key、无 StrictMode —— ref 生命周期 = 应用生命周期
- antd Tooltip(6.6.2)只代理 8 种事件,不含 click 之外的干扰;`e.detail` 在合成事件上可靠
- zustand v5 选择器走 `Object.is`,同值 set 不重渲染(双击当前按钮的第二击零副作用)
- `toggleSidebar`(Workspace.tsx:341,useCallback 稳定引用)已作为 `onToggleSidebar` 传入 `IconRail`(:1274),透传即可,Workspace 自身 props 接线不动
- `WorkspaceMode` 类型已在 Workspace.tsx:42 导入;`useRef` 已在用
- 渲染层无全局 click 监听、无既有 `e.detail` 消费者,无双击手势冲突
## Global Constraints
- **不执行任何 git commit / push** —— 改动留在工作区由用户审查(仓库 owner 惯例)
- 新纯模块 `modeRailGesture.ts` **零 import**(Node 原生类型剥离直连的硬约束,同 semantic.ts / workspaceMode.ts)
- 不改 CSS、不改 i18n 词典、不动现有收展按钮(rail 顶部 MenuFold/MenuUnfold)
- 行为语义锁定:detail 0/1 → 切换;detail 2 且手势前模式=本按钮 → 翻侧栏;detail 2 其余 → 无操作
- 测试脚手架照 `tests/semantic-theme.mjs`:`ok(cond, msg)` 计数器 + 末尾 `process.exit(failed ? 1 : 0)`
---
### Task 1: 纯手势判定模块 + 表驱动测试(TDD)
**Files:**
- Test: `tests/mode-rail-gesture.mjs`(新建)
- Create: `src/renderer/src/workspace/modeRailGesture.ts`(新建)
- Modify: `package.json:16`(test 链尾追加)
- Modify: `AGENTS.md`(测试计数 35→36 + 两处清单)
**Interfaces:**
- Produces: `decideModeClick(detail: number, preGestureMode: RailMode | null, buttonMode: RailMode): ModeClickAction`,`RailMode = 'terminal' | 'ssh'`,`ModeClickAction = 'switch' | 'toggle-sidebar' | 'noop'` —— Task 2 的 `ModeRailButtons` 消费此签名。
- [ ] **Step 1: 先写测试 `tests/mode-rail-gesture.mjs`**
```js
/**
* modeRailGesture.ts self-test。零 import 纯模块,Node 原生类型剥离直连 .ts。
* 锁定双击手势语义:
* 1) 单击 / 键盘触发(detail 1 / 0)一律 switch;
* 2) detail 2 只有「手势前模式 === 本按钮模式」才 toggle-sidebar(双击当前工作区按钮);
* 3) detail 2 其余情况 noop(双击另一个按钮时第一击已完成切换,侧栏不动)。
* Run: node tests/mode-rail-gesture.mjs (must exit 0)
*/
import { decideModeClick } from '../src/renderer/src/workspace/modeRailGesture.ts'
let failed = 0
const ok = (cond, msg) => {
console.log(` ${cond ? 'ok' : 'FAIL'}: ${msg}`)
if (!cond) failed += 1
}
console.log('single click / keyboard always switch:')
ok(decideModeClick(1, null, 'terminal') === 'switch', 'detail=1 preMode=null → switch')
ok(decideModeClick(1, 'terminal', 'terminal') === 'switch', 'detail=1 same mode → switch')
ok(decideModeClick(1, 'terminal', 'ssh') === 'switch', 'detail=1 other mode → switch')
ok(decideModeClick(0, 'terminal', 'ssh') === 'switch', 'detail=0 keyboard → switch')
console.log('double click toggles sidebar only on the already-active button:')
ok(decideModeClick(2, 'terminal', 'terminal') === 'toggle-sidebar', 'detail=2 active terminal → toggle-sidebar')
ok(decideModeClick(2, 'ssh', 'ssh') === 'toggle-sidebar', 'detail=2 active ssh → toggle-sidebar')
ok(decideModeClick(2, 'terminal', 'ssh') === 'noop', 'detail=2 inactive ssh → noop(第一击已切换)')
ok(decideModeClick(2, 'ssh', 'terminal') === 'noop', 'detail=2 inactive terminal → noop')
ok(decideModeClick(2, null, 'terminal') === 'noop', 'detail=2 preMode=null → noop(防御)')
console.log('click streaks beyond 2 fall back to switch semantics:')
ok(decideModeClick(3, 'terminal', 'terminal') === 'switch', 'detail=3 → switch(不产生第二次翻转)')
if (failed > 0) {
console.error(`${failed} check(s) failed`)
process.exit(1)
}
console.log('mode-rail-gesture: all ok')
```
- [ ] **Step 2: 运行测试确认失败**
Run: `node tests/mode-rail-gesture.mjs`
Expected: FAIL —— `ERR_MODULE_NOT_FOUND`(模块尚不存在)
- [ ] **Step 3: 实现 `src/renderer/src/workspace/modeRailGesture.ts`**
```ts
/**
* 图标栏模式按钮(终端工作区 / SSH 工作区)的单击·双击手势判定。
* 零 import 纯模块 —— tests/mode-rail-gesture.mjs 经 Node 原生类型剥离直连本文件。
*
* 浏览器双击时序是 click(detail=1) → click(detail=2) → dblclick,且 detail=2 到达时
* store 里的模式已被第一击更新,所以调用方必须在 detail===1 时把「手势前模式」存进 ref、
* detail===2 时传入本函数比对 —— 绝不能在第二击时读当前 store 值。
*/
export type RailMode = 'terminal' | 'ssh'
export type ModeClickAction = 'switch' | 'toggle-sidebar' | 'noop'
/**
* 判定一次 click 属于哪种动作:
* - detail 1(单击或双击的第一击)→ 'switch':记录手势前模式并切换工作区
* - detail 2 且手势前模式 === 本按钮模式 → 'toggle-sidebar':双击的是当前工作区按钮
* - detail 2 且不等 → 'noop':第一击已完成切换,侧栏不动
* - detail 0(键盘 Enter/Space 触发)→ 'switch':无障碍路径保持单击语义
*/
export function decideModeClick(detail: number, preGestureMode: RailMode | null, buttonMode: RailMode): ModeClickAction {
if (detail === 2) return preGestureMode === buttonMode ? 'toggle-sidebar' : 'noop'
return 'switch'
}
```
- [ ] **Step 4: 运行测试确认通过**
Run: `node tests/mode-rail-gesture.mjs`
Expected: PASS —— 10 行 `ok`,末尾 `mode-rail-gesture: all ok`,exit 0
- [ ] **Step 5: 登记测试到 package.json**
`package.json:16` 的 `"test"` 链尾,在 `node tests/link-rules.mjs` 之后追加:
```
&& node tests/mode-rail-gesture.mjs
```
(新测试是零 import 直连型,不依赖 bundle,位置在链尾即可。)
- [ ] **Step 6: 同步 AGENTS.md 测试清单(项目规则:改 AGENTS.md 所载内容须同步)**
两处编辑:
1. 「测试」条目里 `可离线运行的 35 个测试` → `36 个测试`,枚举末尾 `font-stack、link-rules;` → `font-stack、link-rules、mode-rail-gesture;`
2. 同条目括号内的渲染层纯函数测试枚举 `…hl-rules、link-rules 等)` → `…hl-rules、link-rules、mode-rail-gesture 等)`
### Task 2: Workspace.tsx 接线
**Files:**
- Modify: `src/renderer/src/workspace/Workspace.tsx`(import 一处、ModeRailButtons :1545、IconRail JSX :1532)
**Interfaces:**
- Consumes: Task 1 的 `decideModeClick` / `ModeClickAction`;既有 `useWorkspaceModeStore`(含 `.getState()`)、`WorkspaceMode` 类型(已在 :42 导入)、`useRef`(已在用)、IconRail 已有的 `onToggleSidebar` prop。
- [ ] **Step 1: 加 import**
在 Workspace.tsx:43 `import { useWorkspaceModeStore } from './workspaceModeStore'` 之后加:
```ts
import { decideModeClick } from './modeRailGesture'
```
- [ ] **Step 2: 替换 ModeRailButtons(:1545–1564 整段)**
旧代码(原样,供 Edit 的 old_string):
```tsx
/** M6.1: terminal / SSH workspace entries in the icon rail. */
function ModeRailButtons(): React.JSX.Element {
const mode = useWorkspaceModeStore((s) => s.mode)
const setMode = useWorkspaceModeStore((s) => s.setMode)
return (
<>
<RailButton
title={t('workspace.rail.terminalWorkspace')}
icon={<CodeOutlined />}
active={mode === 'terminal'}
onClick={() => setMode('terminal')}
/>
<RailButton
title={t('workspace.rail.sshWorkspace')}
icon={<CloudServerOutlined />}
active={mode === 'ssh'}
onClick={() => setMode('ssh')}
/>
</>
)
}
```
新代码:
```tsx
/** M6.1: terminal / SSH workspace entries in the icon rail. 双击当前工作区按钮 = 收展侧栏。 */
function ModeRailButtons({ onToggleSidebar }: { onToggleSidebar: () => void }): React.JSX.Element {
const mode = useWorkspaceModeStore((s) => s.mode)
const setMode = useWorkspaceModeStore((s) => s.setMode)
/** 手势开始前的模式。detail===2 到达时 store 已被第一击更新,判定「双击的是不是当前工作区」只能用这里存的值。 */
const preGestureModeRef = useRef<WorkspaceMode | null>(null)
const handleModeClick = (buttonMode: WorkspaceMode) => (e: React.MouseEvent<HTMLButtonElement>): void => {
const action = decideModeClick(e.detail, preGestureModeRef.current, buttonMode)
if (action === 'toggle-sidebar') {
onToggleSidebar()
return
}
if (action === 'noop') return
preGestureModeRef.current = useWorkspaceModeStore.getState().mode
setMode(buttonMode)
}
return (
<>
<RailButton
title={t('workspace.rail.terminalWorkspace')}
icon={<CodeOutlined />}
active={mode === 'terminal'}
onClick={handleModeClick('terminal')}
/>
<RailButton
title={t('workspace.rail.sshWorkspace')}
icon={<CloudServerOutlined />}
active={mode === 'ssh'}
onClick={handleModeClick('ssh')}
/>
</>
)
}
```
- [ ] **Step 3: IconRail 内透传(:1532)**
`<ModeRailButtons />` → `<ModeRailButtons onToggleSidebar={onToggleSidebar} />`
- [ ] **Step 4: 渲染层类型检查**
Run: `npx tsc --noEmit -p tsconfig.web.json`
Expected: 无输出,exit 0
- [ ] **Step 5: 全量测试**
Run: `npm test`
Expected: pretest 类型检查 + 36 个离线测试全部通过,exit 0
- [ ] **Step 6: 自审 diff**
Run: `git diff --stat` + `git status`
Expected: 恰好 3 改 2 新:`Workspace.tsx`、`package.json`、`AGENTS.md` 修改;`modeRailGesture.ts`、`tests/mode-rail-gesture.mjs` 新增(共 5 个路径;本计划文档自身 untracked 属预期,无其它文件);无 CSS / i18n 词典改动
+1 -1
View File
@@ -13,7 +13,7 @@
"preview": "electron-vite preview",
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
"pretest": "npm run typecheck",
"test": "node tests/build-bundles.cjs && node tests/ssh-loopback.mjs && node tests/commands-store.mjs && node tests/connections-store.mjs && node tests/settings-store.mjs && node tests/local-path-grants.mjs && node tests/lock-store.mjs && node tests/lock-controller.mjs && node tests/lock-shortcuts.mjs && node tests/reserved-accelerators.mjs && node tests/.terminal-title.cjs && node tests/ipc-guard.mjs && node tests/updater-fallback.mjs && node tests/log-sanitizer.mjs && node tests/app-log.mjs && node tests/sftp-timeout.mjs && node tests/.hl-split-smoke.cjs && node tests/.hl-rules.cjs && node tests/redos-probes.mjs && node tests/rule-validation.mjs && node tests/redos-validator.mjs && node tests/perf-diagnostics.mjs && node tests/zmodem-e2e.mjs && node tests/ssh-session-e2e.mjs && node tests/sysinfo-e2e.mjs && node tests/terminal-cwd.mjs && node tests/terminal-links.mjs && node tests/broadcast-store.mjs && node tests/write-batcher.mjs && node tests/refit-throttle.mjs && node tests/semantic-theme.mjs && node tests/theme-contrast.mjs && node tests/css-theme-vars.mjs && node tests/ot-var-names.mjs && node tests/font-stack.mjs && node tests/link-rules.mjs",
"test": "node tests/build-bundles.cjs && node tests/ssh-loopback.mjs && node tests/commands-store.mjs && node tests/connections-store.mjs && node tests/settings-store.mjs && node tests/local-path-grants.mjs && node tests/lock-store.mjs && node tests/lock-controller.mjs && node tests/lock-shortcuts.mjs && node tests/reserved-accelerators.mjs && node tests/.terminal-title.cjs && node tests/ipc-guard.mjs && node tests/updater-fallback.mjs && node tests/log-sanitizer.mjs && node tests/app-log.mjs && node tests/sftp-timeout.mjs && node tests/.hl-split-smoke.cjs && node tests/.hl-rules.cjs && node tests/redos-probes.mjs && node tests/rule-validation.mjs && node tests/redos-validator.mjs && node tests/perf-diagnostics.mjs && node tests/zmodem-e2e.mjs && node tests/ssh-session-e2e.mjs && node tests/sysinfo-e2e.mjs && node tests/terminal-cwd.mjs && node tests/terminal-links.mjs && node tests/broadcast-store.mjs && node tests/write-batcher.mjs && node tests/refit-throttle.mjs && node tests/semantic-theme.mjs && node tests/theme-contrast.mjs && node tests/css-theme-vars.mjs && node tests/ot-var-names.mjs && node tests/font-stack.mjs && node tests/link-rules.mjs && node tests/mode-rail-gesture.mjs",
"predist": "node scripts/verify-deps.cjs && npm test && npm install --package-lock-only",
"dist": "electron-vite build && node tests/worker-artifact.mjs && electron-builder --win nsis",
"dist:dir": "electron-vite build && node tests/worker-artifact.mjs && electron-builder --win --dir"
+21 -6
View File
@@ -41,6 +41,7 @@ import { useSettingsStore } from '@renderer/settings/store'
import type { SessionSnapshot, SessionPanelState } from '@shared/ipc'
import type { WorkspaceMode } from './workspaceMode'
import { useWorkspaceModeStore } from './workspaceModeStore'
import { decideModeClick } from './modeRailGesture'
import './workspace.css'
export interface WorkspaceProps {
@@ -1529,7 +1530,7 @@ const IconRail = memo(function IconRail({
<RailButton title={t('workspace.newTerminal')} icon={<PlusOutlined />} onClick={onNewTerminal} />
<LayoutFlyout menu={layoutMenu} />
<div className="workspace-rail-divider" />
<ModeRailButtons />
<ModeRailButtons onToggleSidebar={onToggleSidebar} />
<div className="workspace-rail-spacer" />
{connecting && (
<Tooltip title={t('workspace.rail.connecting')} placement="right">
@@ -1541,23 +1542,37 @@ const IconRail = memo(function IconRail({
)
})
/** M6.1: terminal / SSH workspace entries in the icon rail. */
function ModeRailButtons(): React.JSX.Element {
/** M6.1: terminal / SSH workspace entries in the icon rail. 双击当前工作区按钮 = 收展侧栏。 */
function ModeRailButtons({ onToggleSidebar }: { onToggleSidebar: () => void }): React.JSX.Element {
const mode = useWorkspaceModeStore((s) => s.mode)
const setMode = useWorkspaceModeStore((s) => s.setMode)
/** 手势开始前的模式。detail===2 到达时 store 已被第一击更新,判定「双击的是不是当前工作区」只能用这里存的值。 */
const preGestureModeRef = useRef<WorkspaceMode | null>(null)
const handleModeClick = (buttonMode: WorkspaceMode) => (e: React.MouseEvent<HTMLButtonElement>): void => {
const action = decideModeClick(e.detail, preGestureModeRef.current, buttonMode)
if (action === 'toggle-sidebar') {
onToggleSidebar()
return
}
if (action === 'noop') return
preGestureModeRef.current = useWorkspaceModeStore.getState().mode
setMode(buttonMode)
}
return (
<>
<RailButton
title={t('workspace.rail.terminalWorkspace')}
icon={<CodeOutlined />}
active={mode === 'terminal'}
onClick={() => setMode('terminal')}
onClick={handleModeClick('terminal')}
/>
<RailButton
title={t('workspace.rail.sshWorkspace')}
icon={<CloudServerOutlined />}
active={mode === 'ssh'}
onClick={() => setMode('ssh')}
onClick={handleModeClick('ssh')}
/>
</>
)
@@ -1644,7 +1659,7 @@ function RailButton({
}: {
title: string
icon: ReactNode
onClick?: () => void
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
active?: boolean
hideTooltip?: boolean
}): React.JSX.Element {
@@ -0,0 +1,23 @@
/**
* 图标栏模式按钮(终端工作区 / SSH 工作区)的单击·双击手势判定。
* 零 import 纯模块 —— tests/mode-rail-gesture.mjs 经 Node 原生类型剥离直连本文件。
*
* 浏览器双击时序是 click(detail=1) → click(detail=2) → dblclick,且 detail=2 到达时
* store 里的模式已被第一击更新,所以调用方必须在 detail===1 时把「手势前模式」存进 ref、
* detail===2 时传入本函数比对 —— 绝不能在第二击时读当前 store 值。
*/
export type RailMode = 'terminal' | 'ssh'
export type ModeClickAction = 'switch' | 'toggle-sidebar' | 'noop'
/**
* 判定一次 click 属于哪种动作:
* - detail 1(单击或双击的第一击)→ 'switch':记录手势前模式并切换工作区
* - detail 2 且手势前模式 === 本按钮模式 → 'toggle-sidebar':双击的是当前工作区按钮
* - detail 2 且不等 → 'noop':第一击已完成切换,侧栏不动
* - detail 0(键盘 Enter/Space 触发)→ 'switch':无障碍路径保持单击语义
*/
export function decideModeClick(detail: number, preGestureMode: RailMode | null, buttonMode: RailMode): ModeClickAction {
if (detail === 2) return preGestureMode === buttonMode ? 'toggle-sidebar' : 'noop'
return 'switch'
}
+37
View File
@@ -0,0 +1,37 @@
/**
* modeRailGesture.ts self-test。零 import 纯模块,Node 原生类型剥离直连 .ts。
* 锁定双击手势语义:
* 1) 单击 / 键盘触发(detail 1 / 0)一律 switch;
* 2) detail 2 只有「手势前模式 === 本按钮模式」才 toggle-sidebar(双击当前工作区按钮);
* 3) detail 2 其余情况 noop(双击另一个按钮时第一击已完成切换,侧栏不动)。
* Run: node tests/mode-rail-gesture.mjs (must exit 0)
*/
import { decideModeClick } from '../src/renderer/src/workspace/modeRailGesture.ts'
let failed = 0
const ok = (cond, msg) => {
console.log(` ${cond ? 'ok' : 'FAIL'}: ${msg}`)
if (!cond) failed += 1
}
console.log('single click / keyboard always switch:')
ok(decideModeClick(1, null, 'terminal') === 'switch', 'detail=1 preMode=null → switch')
ok(decideModeClick(1, 'terminal', 'terminal') === 'switch', 'detail=1 same mode → switch')
ok(decideModeClick(1, 'terminal', 'ssh') === 'switch', 'detail=1 other mode → switch')
ok(decideModeClick(0, 'terminal', 'ssh') === 'switch', 'detail=0 keyboard → switch')
console.log('double click toggles sidebar only on the already-active button:')
ok(decideModeClick(2, 'terminal', 'terminal') === 'toggle-sidebar', 'detail=2 active terminal → toggle-sidebar')
ok(decideModeClick(2, 'ssh', 'ssh') === 'toggle-sidebar', 'detail=2 active ssh → toggle-sidebar')
ok(decideModeClick(2, 'terminal', 'ssh') === 'noop', 'detail=2 inactive ssh → noop(第一击已切换)')
ok(decideModeClick(2, 'ssh', 'terminal') === 'noop', 'detail=2 inactive terminal → noop')
ok(decideModeClick(2, null, 'terminal') === 'noop', 'detail=2 preMode=null → noop(防御)')
console.log('click streaks beyond 2 fall back to switch semantics:')
ok(decideModeClick(3, 'terminal', 'terminal') === 'switch', 'detail=3 → switch(不产生第二次翻转)')
if (failed > 0) {
console.error(`${failed} check(s) failed`)
process.exit(1)
}
console.log('mode-rail-gesture: all ok')
+1 -1
View File
@@ -98,7 +98,7 @@ website/
2. 下载表格里的文件大小(`.mono` 单元格,如 `109 MB`)
3. 「近期更新」小节:新增一条 `<article>`(最新的在最前面,结构保持
`<h3><span class="mono">版本</span><span class="rel-date">日期</span></h3>` + `<ul><li>`),
并删掉最旧的一条以控制长度(当前保留最近 5 条:1.0.25 / 1.0.24 / 1.0.23 / 1.0.22 / 1.0.21)
并删掉最旧的一条以控制长度(当前保留最近 5 条:1.0.30 / 1.0.29 / 1.0.28 / 1.0.27 / 1.0.26)
4. `assets/i18n.js`:给新版本加 `changelog.<新版本>.li.N` 键(**四个语言块都要加**),
并删掉被移除那条的 `changelog.<旧版本>.li.*` 键(同样是四个语言块)
+40 -40
View File
@@ -155,6 +155,16 @@ window.OPENTERMINAL_I18N = {
'cl.eyebrow': 'Changelog',
'cl.title': '近期更新',
'changelog.1.0.30.li.1': '中文显示更准:终端启用 Unicode 11 宽度表,中文、全角符号与表格线对齐正确;设置新增「中文字体」回退槽位',
'changelog.1.0.30.li.2': '新增自定义可点击链接规则:正则捕获组可展开为 http/https 链接,Ctrl+点击打开;高亮规则新增「按行匹配」开关',
'changelog.1.0.30.li.3': '修复彩色 TUI 输出在高亮区段后掉色、ls -l 权限位高亮在多行下失效、含空格或括号的字体名静默失效;非法配置导致窗格崩溃会在启动时自动修复',
'changelog.1.0.30.li.4': '自定义正则加入 ReDoS 防护:执行前在主进程 Worker 校验,单条 500ms 硬超时,超时规则自动隔离;内置预设免校验',
'changelog.1.0.30.li.5': '诊断日志区分「真实长任务」与调度延迟,并新增 reflow、写入积压、WebGL 生命周期记录,卡顿定位更准',
'changelog.1.0.29.li.1': '浅色主题可读性重做:三套内置浅色主题的终端 16 色与前景文字全部达到 WCAG AA(≥ 4.5:1),并开启最低对比度兜底',
'changelog.1.0.29.li.2': 'GitHub Light 纯白背景改为柔和的 #f6f8fa,长时间使用不刺眼',
'changelog.1.0.29.li.3': '侧栏、标签栏、SFTP / 监控 / 设置等面板配色统一跟随主题,浅色下不再有看不清的文字',
'changelog.1.0.28.li.1': '修复应用内更新会下载两次的问题:禁用注定失败的差量下载,每次更新恰好一次整包下载,进度条只走一遍',
'changelog.1.0.27.li.1': '新增关闭标签二次确认:终端 / SSH 两个独立开关(默认关闭),设置 → 渲染与行为',
@@ -165,16 +175,6 @@ window.OPENTERMINAL_I18N = {
'changelog.1.0.26.li.2': '新增落盘诊断日志:默认开启,按天一个文件保留 7 天,设置 → 系统可关闭',
'changelog.1.0.26.li.3': '反馈卡顿问题时附上当天的日志文件即可精确定位',
'changelog.1.0.25.li.1': '定时检查更新(从不 / 4 / 12 / 24 小时,默认 4)',
'changelog.1.0.25.li.2': '自动下载更新开关(后台静默下载,安装时机仍由用户决定,绝不自动退出)',
'changelog.1.0.25.li.3': '下载完成 / 发现新版本时窗口最小化到托盘则弹提示',
'changelog.1.0.25.li.4': '同版本不重复提示或重复下载',
'changelog.1.0.24.li.1': '修复应用旧版布局模板会误杀全部 SSH 会话',
'changelog.1.0.24.li.2': '配置文件损坏时先备份再重置(不再静默销毁命令库 / 书签 / 设置)',
'changelog.1.0.24.li.3': '会话日志退出不再丢尾部',
'changelog.1.0.24.li.4': 'SFTP 删除不再误报失败;ZMODEM 接收大文件内存可控;新增 3 组渲染层测试',
'cl.morePre': '完整日志见仓库',
'cl.morePost': '。',
@@ -329,6 +329,16 @@ window.OPENTERMINAL_I18N = {
'cl.eyebrow': 'Changelog',
'cl.title': '近期更新',
'changelog.1.0.30.li.1': '中文顯示更準:終端機啟用 Unicode 11 寬度表,中文、全形符號與表格線對齊正確;設定新增「中文字型」回退欄位',
'changelog.1.0.30.li.2': '新增自訂可點擊連結規則:正規表達式捕獲組可展開為 http/https 連結,Ctrl+點擊開啟;高亮規則新增「逐行比對」開關',
'changelog.1.0.30.li.3': '修復彩色 TUI 輸出在高亮區段後掉色、ls -l 權限位高亮在多行下失效、含空格或括號的字型名稱默默失效;非法設定導致窗格崩潰會在啟動時自動修復',
'changelog.1.0.30.li.4': '自訂正規表達式加入 ReDoS 防護:執行前先在主程序 Worker 校驗,單條規則 500ms 硬性逾時,逾時規則自動隔離;內建預設免校驗',
'changelog.1.0.30.li.5': '診斷日誌區分「真實長任務」與排程延遲,並新增 reflow、寫入積壓、WebGL 生命週期記錄,卡頓定位更準',
'changelog.1.0.29.li.1': '淺色主題可讀性重做:三套內建淺色主題的終端機 16 色與前景文字全部達到 WCAG AA(≥ 4.5:1),並開啟最低對比度補底',
'changelog.1.0.29.li.2': 'GitHub Light 純白背景改為柔和的 #f6f8fa,長時間使用不刺眼',
'changelog.1.0.29.li.3': '側邊欄、分頁列、SFTP / 監控 / 設定等面板配色統一跟隨主題,淺色下不再有看不清的文字',
'changelog.1.0.28.li.1': '修復應用內更新會下載兩次的問題:停用注定失敗的差量下載,每次更新恰好一次整包下載,進度條只走一遍',
'changelog.1.0.27.li.1': '新增關閉分頁二次確認:終端機 / SSH 兩個獨立開關(預設關閉),設定 → 渲染與行為',
@@ -339,16 +349,6 @@ window.OPENTERMINAL_I18N = {
'changelog.1.0.26.li.2': '新增落盤診斷日誌:預設開啟,按天一個檔案保留 7 天,設定 → 系統可關閉',
'changelog.1.0.26.li.3': '回報卡頓問題時附上當天的日誌檔案即可精確定位',
'changelog.1.0.25.li.1': '定時檢查更新(從不 / 4 / 12 / 24 小時,預設 4)',
'changelog.1.0.25.li.2': '自動下載更新開關(背景靜默下載,安裝時機仍由使用者決定,絕不自動結束)',
'changelog.1.0.25.li.3': '下載完成 / 發現新版本時,視窗若最小化到系統匣會彈出提示',
'changelog.1.0.25.li.4': '同一版本不重複提示、不重複下載',
'changelog.1.0.24.li.1': '修復套用舊版版面配置範本會誤殺全部 SSH 工作階段的問題',
'changelog.1.0.24.li.2': '設定檔損毀時先備份再重置(不再靜默銷毀指令庫 / 書籤 / 設定)',
'changelog.1.0.24.li.3': '工作階段日誌結束時不再遺失尾段',
'changelog.1.0.24.li.4': 'SFTP 刪除不再誤報失敗;ZMODEM 接收大檔案的記憶體用量可控;新增 3 組渲染層測試',
'cl.morePre': '完整日誌請見倉庫',
'cl.morePost': '。',
@@ -503,6 +503,16 @@ window.OPENTERMINAL_I18N = {
'cl.eyebrow': 'Changelog',
'cl.title': 'Recent updates',
'changelog.1.0.30.li.1': 'Better CJK rendering: the terminal now uses the Unicode 11 width table so Chinese, full-width punctuation and box-drawing characters align correctly; a CJK font fallback slot was added to Settings',
'changelog.1.0.30.li.2': 'Added custom clickable link rules: a regex capture group can expand into an http/https link, opened with Ctrl+click; highlight rules gained a line-anchored matching switch',
'changelog.1.0.30.li.3': 'Fixed coloured TUI output losing its colour after a highlighted span, ls -l permission bits not highlighting across wrapped lines, and font names with spaces or parentheses silently failing; panes no longer crash on invalid settings, which are repaired at startup',
'changelog.1.0.30.li.4': 'Custom regexes now go through ReDoS protection: validated in a main-process Worker before running, with a hard 500 ms timeout that quarantines a slow rule; built-in presets skip validation',
'changelog.1.0.30.li.5': 'Diagnostic logs now separate real long tasks from scheduling delays and record reflow, write backlog and WebGL lifecycle, making freezes easier to pin down',
'changelog.1.0.29.li.1': 'Light themes redone for readability: all 16 ANSI colours plus foreground text in the three built-in light themes now meet WCAG AA (≥ 4.5:1), with a minimum-contrast floor enabled',
'changelog.1.0.29.li.2': 'GitHub Light\'s pure-white background is now the softer #f6f8fa, easier on the eyes over long sessions',
'changelog.1.0.29.li.3': 'Sidebar, tab bar, SFTP, monitor and settings panels all derive their colours from the active theme, so nothing becomes unreadable in light mode',
'changelog.1.0.28.li.1': 'Fixed in-app updates downloading twice: differential download (which could never succeed) is disabled, so every update is exactly one full download with a single progress pass',
'changelog.1.0.27.li.1': 'Added close-confirmation for tabs: separate switches for terminal and SSH tabs (off by default), under Settings → Rendering & Behavior',
@@ -513,16 +523,6 @@ window.OPENTERMINAL_I18N = {
'changelog.1.0.26.li.2': 'Added on-disk diagnostic logs: on by default, one file per day kept for 7 days, can be disabled in Settings → System',
'changelog.1.0.26.li.3': 'Attach the day\'s log file when reporting freezes for precise diagnosis',
'changelog.1.0.25.li.1': 'Scheduled update checks (never / 4 / 12 / 24 hours, default 4)',
'changelog.1.0.25.li.2': 'Auto-download toggle: silent background download, install timing still up to you, never quits on its own',
'changelog.1.0.25.li.3': 'Tray notification when a new version is found or a download finishes while the window is minimized',
'changelog.1.0.25.li.4': 'No repeated prompts or repeated downloads for the same version',
'changelog.1.0.24.li.1': 'Fixed: applying an older layout template could kill every SSH session',
'changelog.1.0.24.li.2': 'Corrupted config files are now backed up before resetting — no more silent loss of the command library, bookmarks or settings',
'changelog.1.0.24.li.3': 'Session logs no longer lose their tail when the app exits',
'changelog.1.0.24.li.4': 'SFTP delete no longer reports false failures; ZMODEM downloads of large files keep memory bounded; 3 new renderer test suites',
'cl.morePre': 'Full changelog in',
'cl.morePost': '.',
@@ -677,6 +677,16 @@ window.OPENTERMINAL_I18N = {
'cl.eyebrow': 'Changelog',
'cl.title': '最近の更新',
'changelog.1.0.30.li.1': '中国語表示の精度を改善:ターミナルが Unicode 11 の文字幅テーブルを使うようになり、中国語・全角記号・罫線の位置が揃います。設定に「中国語フォント」のフォールバック欄を追加',
'changelog.1.0.30.li.2': 'カスタムのクリック可能リンク規則を追加:正規表現のキャプチャグループを http/https リンクに展開し、Ctrl+クリックで開けます。ハイライト規則に「行単位で照合」スイッチを追加',
'changelog.1.0.30.li.3': '色付き TUI 出力がハイライト区間の後で色を失う問題、ls -l のパーミッション表示が折り返し時にハイライトされない問題、空白や括弧を含むフォント名が黙って無効になる問題を修正。不正な設定でペインが落ちる問題は起動時に自動修復',
'changelog.1.0.30.li.4': 'カスタム正規表現に ReDoS 対策を追加:実行前にメインプロセスの Worker で検証し、1 件あたり 500ms のハードタイムアウトで遅い規則を自動的に隔離。組み込みプリセットは検証対象外',
'changelog.1.0.30.li.5': '診断ログが「実際の長いタスク」とスケジューリング遅延を区別し、リフロー・書き込み滞留・WebGL のライフサイクルも記録するようになり、固まりの原因を特定しやすくなりました',
'changelog.1.0.29.li.1': 'ライトテーマの可読性を作り直し:組み込みのライトテーマ 3 種すべてで、ターミナルの 16 色と前景色が WCAG AA(4.5:1 以上)を満たし、最低コントラストの下限も有効化',
'changelog.1.0.29.li.2': 'GitHub Light の純白背景を、長時間でも目に優しい #f6f8fa に変更',
'changelog.1.0.29.li.3': 'サイドバー、タブバー、SFTP / モニター / 設定などのパネル配色がテーマに追従するようになり、ライトテーマでも読めない文字がなくなりました',
'changelog.1.0.28.li.1': 'アプリ内アップデートが 2 回ダウンロードされる問題を修正:成功しない差分ダウンロードを無効化し、毎回ちょうど 1 回のフルダウンロードに',
'changelog.1.0.27.li.1': 'タブを閉じる前の確認機能を追加:ターミナル / SSH それぞれ個別のスイッチ(既定はオフ)、設定 → 描画と動作',
@@ -687,16 +697,6 @@ window.OPENTERMINAL_I18N = {
'changelog.1.0.26.li.2': '診断ログを追加:既定で有効、日ごとに 1 ファイルを 7 日間保持、設定 → システムで無効化可能',
'changelog.1.0.26.li.3': '固まりの報告時に当日のログファイルを添付すれば正確に特定できます',
'changelog.1.0.25.li.1': '定期的な更新チェック(しない / 4 / 12 / 24 時間、既定は 4 時間)',
'changelog.1.0.25.li.2': '自動ダウンロードのオン/オフ(バックグラウンドで静かにダウンロード。インストールのタイミングは従来どおりユーザーが決定し、自動で終了することはありません)',
'changelog.1.0.25.li.3': '新バージョンの検出時やダウンロード完了時に、ウィンドウがトレイに最小化されていれば通知を表示',
'changelog.1.0.25.li.4': '同じバージョンで通知やダウンロードを繰り返しません',
'changelog.1.0.24.li.1': '古いレイアウトテンプレートを適用すると SSH セッションがすべて切断される問題を修正',
'changelog.1.0.24.li.2': '設定ファイルが壊れている場合は先にバックアップしてから初期化(コマンドライブラリ、ブックマーク、設定が黙って失われることはなくなりました)',
'changelog.1.0.24.li.3': '終了時にセッションログの末尾が欠けなくなりました',
'changelog.1.0.24.li.4': 'SFTP の削除が誤って失敗扱いにならなくなりました。ZMODEM の大容量受信もメモリ使用量を抑制。レンダラー層のテストを 3 組追加',
'cl.morePre': '完全な変更履歴は',
'cl.morePost': ' をご覧ください。',
+23 -23
View File
@@ -118,14 +118,14 @@
内置 SFTP 文件管理与广播输入。安装即用,支持自动更新。
</p>
<div class="hero-actions">
<a class="btn btn-primary" id="dl-exe" href="https://github.com/billowliu2/OpenTerminal/releases/download/v1.0.28/OpenTerminal-1.0.28-setup.exe" data-i18n="hero.download">
<a class="btn btn-primary" id="dl-exe" href="https://github.com/billowliu2/OpenTerminal/releases/download/v1.0.30/OpenTerminal-1.0.30-setup.exe" data-i18n="hero.download">
下载 Windows 安装包
</a>
<a class="btn btn-ghost" href="https://github.com/billowliu2/OpenTerminal" target="_blank" rel="noopener" data-i18n="hero.source">
查看源码
</a>
</div>
<p class="hero-note"><span data-i18n="hero.notePre">当前版本</span> <span class="mono">v1.0.28</span> <span data-i18n="hero.notePost">· NSIS exe 安装包,支持应用内自动更新。</span></p>
<p class="hero-note"><span data-i18n="hero.notePre">当前版本</span> <span class="mono">v1.0.30</span> <span data-i18n="hero.notePost">· NSIS exe 安装包,支持应用内自动更新。</span></p>
</div>
<figure class="hero-shot">
<img src="assets/shot-terminal.jpg" alt="OpenTerminal 主界面:三路分屏并行运行 Kimi Code、Claude Code 与 Codex" width="1739" height="870" data-i18n-attr="alt:hero.shot.alt" />
@@ -133,7 +133,7 @@
</figure>
</div>
<div class="container spec-strip">
<span><span data-i18n="spec.version">当前版本</span> <strong class="mono">v1.0.28</strong></span>
<span><span data-i18n="spec.version">当前版本</span> <strong class="mono">v1.0.30</strong></span>
<span data-i18n="spec.os">Windows 10 / 11 · x64</span>
<span data-i18n="spec.license">MIT 开源协议</span>
<span data-i18n="spec.update">exe 支持应用内自动更新</span>
@@ -341,11 +341,11 @@
</thead>
<tbody>
<tr>
<td class="mono">OpenTerminal-1.0.28-setup.exe</td>
<td class="mono">OpenTerminal-1.0.30-setup.exe</td>
<td data-i18n="dl.table.exe">NSIS 安装包,支持应用内自动更新(推荐)</td>
<td class="mono">109 MB</td>
<td>
<a href="https://github.com/billowliu2/OpenTerminal/releases/download/v1.0.28/OpenTerminal-1.0.28-setup.exe" data-i18n="dl.table.download">下载</a>
<a href="https://github.com/billowliu2/OpenTerminal/releases/download/v1.0.30/OpenTerminal-1.0.30-setup.exe" data-i18n="dl.table.download">下载</a>
</td>
</tr>
<tr>
@@ -375,6 +375,24 @@
<p class="section-eyebrow" data-i18n="cl.eyebrow">Changelog</p>
<h2 data-i18n="cl.title">近期更新</h2>
<div class="changelog-list">
<article>
<h3><span class="mono">1.0.30</span><span class="rel-date">2026-10-18</span></h3>
<ul>
<li data-i18n="changelog.1.0.30.li.1">中文显示更准:终端启用 Unicode 11 宽度表,中文、全角符号与表格线对齐正确;设置新增「中文字体」回退槽位</li>
<li data-i18n="changelog.1.0.30.li.2">新增自定义可点击链接规则:正则捕获组可展开为 http/https 链接,Ctrl+点击打开;高亮规则新增「按行匹配」开关</li>
<li data-i18n="changelog.1.0.30.li.3">修复彩色 TUI 输出在高亮区段后掉色、ls -l 权限位高亮在多行下失效、含空格或括号的字体名静默失效;非法配置导致窗格崩溃会在启动时自动修复</li>
<li data-i18n="changelog.1.0.30.li.4">自定义正则加入 ReDoS 防护:执行前在主进程 Worker 校验,单条 500ms 硬超时,超时规则自动隔离;内置预设免校验</li>
<li data-i18n="changelog.1.0.30.li.5">诊断日志区分「真实长任务」与调度延迟,并新增 reflow、写入积压、WebGL 生命周期记录,卡顿定位更准</li>
</ul>
</article>
<article>
<h3><span class="mono">1.0.29</span><span class="rel-date">2026-10-17</span></h3>
<ul>
<li data-i18n="changelog.1.0.29.li.1">浅色主题可读性重做:三套内置浅色主题的终端 16 色与前景文字全部达到 WCAG AA(≥ 4.5:1),并开启最低对比度兜底</li>
<li data-i18n="changelog.1.0.29.li.2">GitHub Light 纯白背景改为柔和的 #f6f8fa,长时间使用不刺眼</li>
<li data-i18n="changelog.1.0.29.li.3">侧栏、标签栏、SFTP / 监控 / 设置等面板配色统一跟随主题,浅色下不再有看不清的文字</li>
</ul>
</article>
<article>
<h3><span class="mono">1.0.28</span><span class="rel-date">2026-10-16</span></h3>
<ul>
@@ -397,24 +415,6 @@
<li data-i18n="changelog.1.0.26.li.3">反馈卡顿问题时附上当天的日志文件即可精确定位</li>
</ul>
</article>
<article>
<h3><span class="mono">1.0.25</span><span class="rel-date">2026-10-10</span></h3>
<ul>
<li data-i18n="changelog.1.0.25.li.1">定时检查更新(从不 / 4 / 12 / 24 小时,默认 4)</li>
<li data-i18n="changelog.1.0.25.li.2">自动下载更新开关(后台静默下载,安装时机仍由用户决定,绝不自动退出)</li>
<li data-i18n="changelog.1.0.25.li.3">下载完成 / 发现新版本时窗口最小化到托盘则弹提示</li>
<li data-i18n="changelog.1.0.25.li.4">同版本不重复提示或重复下载</li>
</ul>
</article>
<article>
<h3><span class="mono">1.0.24</span><span class="rel-date">2026-10-09</span></h3>
<ul>
<li data-i18n="changelog.1.0.24.li.1">修复应用旧版布局模板会误杀全部 SSH 会话</li>
<li data-i18n="changelog.1.0.24.li.2">配置文件损坏时先备份再重置(不再静默销毁命令库 / 书签 / 设置)</li>
<li data-i18n="changelog.1.0.24.li.3">会话日志退出不再丢尾部</li>
<li data-i18n="changelog.1.0.24.li.4">SFTP 删除不再误报失败;ZMODEM 接收大文件内存可控;新增 3 组渲染层测试</li>
</ul>
</article>
</div>
<p class="changelog-more">
<span data-i18n="cl.morePre">完整日志见仓库</span>