From cff54543468e830b812d4ea82f1cba67b9440c1d Mon Sep 17 00:00:00 2001 From: Bill Date: Thu, 15 Oct 2026 10:35:20 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(workspace):=20=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E4=BA=8C=E6=AC=A1=E7=A1=AE=E8=AE=A4=EF=BC=88?= =?UTF-8?q?=E7=BB=88=E7=AB=AF/SSH=20=E5=8F=8C=E5=BC=80=E5=85=B3=20+=20?= =?UTF-8?q?=E9=94=AE=E7=9B=98=20Backspace/Delete=20=E8=AF=AF=E5=85=B3?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=EF=BC=89=EF=BC=8Cv1.0.27?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + CHANGELOG.en.md | 14 ++ CHANGELOG.ja.md | 14 ++ CHANGELOG.md | 14 ++ CHANGELOG.zh-TW.md | 14 ++ package-lock.json | 4 +- package.json | 2 +- src/renderer/src/settings/SettingsTabs.tsx | 20 +++ src/renderer/src/workspace/TerminalTab.tsx | 197 ++++++++++++++++----- src/shared/i18n/dicts/en/settings.ts | 6 + src/shared/i18n/dicts/en/workspace.ts | 4 + src/shared/i18n/dicts/ja/settings.ts | 6 + src/shared/i18n/dicts/ja/workspace.ts | 4 + src/shared/i18n/dicts/zh-CN/settings.ts | 4 + src/shared/i18n/dicts/zh-CN/workspace.ts | 4 + src/shared/i18n/dicts/zh-TW/settings.ts | 4 + src/shared/i18n/dicts/zh-TW/workspace.ts | 4 + src/shared/settings.ts | 6 + tests/settings-store.mjs | 11 ++ 19 files changed, 283 insertions(+), 50 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2fe4642..1aa221b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,6 +116,7 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。 - 会话计数不靠累加器:`releaseSession` 直接扫 `api.panels` 判断还有没有面板在显示该会话——累加器与「`updateParameters` 原地换会话」「整块布局替换」这类无事件变化脱节,会漏杀或误杀 - **旧版(M6.1 前单 dockview)模板只喂 terminal dockview**:成功路径的 rebind 与收尾 kill 都必须按 `sshTouched` / `live` 集合门控——rebind 会把面板参数改写成新**本地**会话,对没被 fromJSON 触碰的 ssh dockview 跑一遍等于把活 SSH 面板降级成终端再杀会话 - 主机密钥队列只渲染队头,且主进程 `DEFAULT_TIMEOUTS.prompt`(120s)超时后**不再广播**:渲染层用同 TTL 的定时清扫(`HOST_KEY_PROMPT_TTL_MS`)移除过期条目,否则陈旧弹窗永远占住队头挡住后续主机;两处 TTL 必须同步改。`respondHostKey` 等 IPC 调用**不得写进 setState 的 updater**(React 可能重放 updater) +- **tab 关闭二次确认**(`settings.terminal.confirmTabClose` / `confirmSshTabClose`,默认关):拦截点在 `TerminalTab.tsx` 的 `requestClose`——dockview 8.2.0 没有可否决的关闭前钩子,杀会话发生在 `onDidRemovePanel` 之后无法回滚,所以所有用户关闭入口(× 按钮、右键三关闭项、键盘 Delete/Backspace)必须在调用 `api.close()` 之前过它;键盘路径靠挂在 `.dv-tab` 外壳(自定义 div 的 dockview 祖先元素,DOM 焦点落点)上的原生捕获监听拦截。**新增用户可触发的关闭入口必须走 `requestClose`**,会话已死自动关闭/模板应用等程序性路径不走 ## 关键词高亮 diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 045d6a2..d373fa6 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -1,5 +1,19 @@ # OpenTerminal Changelog +## v1.0.27 - 2026-10-15 + +This release adds close confirmation for terminal tabs: an accidental close no longer kills a terminal or SSH session you are working in — especially the frequent case of pressing Backspace right after clicking a tab. + +### New: confirm before closing tabs +- Two new switches under Settings → Rendering & Behavior (**off by default**; leaving them off keeps the previous behaviour exactly): + - **Confirm before closing terminal tabs**: ask before closing a local terminal tab + - **Confirm before closing SSH tabs**: ask before closing an SSH connection tab (SSH disconnects are costly, so you can enable it for SSH only) +- Covers every user-initiated close path: the tab × button, the context menu's Close / Close others / Close right, and the Delete / Backspace keys +- "Close others" / "Close right" ask once for the whole batch, with the number of tabs shown in the dialog +- Pressing Backspace / Delete while a tab has keyboard focus (the main source of accidental closes) no longer closes it directly — it goes through the same confirmation +- Switches take effect immediately after saving, no restart needed; typing Backspace / Delete inside the terminal is completely unaffected +- Programmatic closes (a session exiting on its own, applying a layout template) never prompt + ## v1.0.26 - 2026-10-11 This release is about performance and diagnosability: it fixes the multi-second UI freezes when dragging to resize the window or when a terminal floods output, and adds an on-disk diagnostic log (on by default, auto-cleaned after 7 days) so freeze reports finally come with evidence. diff --git a/CHANGELOG.ja.md b/CHANGELOG.ja.md index dd0997a..c6ac17a 100644 --- a/CHANGELOG.ja.md +++ b/CHANGELOG.ja.md @@ -1,5 +1,19 @@ # OpenTerminal 更新履歴 +## v1.0.27 - 2026-10-15 + +本リリースではタブを閉じる前の確認機能を追加しました。うっかり閉じて作業中のターミナルや SSH セッションを終了させてしまう——特にタブをクリックした直後に Backspace を押してタブごと閉じてしまう操作——を防ぎます。 + +### 新機能:タブを閉じる前の確認 +- 設定 → 描画と動作 に 2 つのスイッチを追加(**既定はオフ**。オフのままなら従来の動作と完全に同じ): + - **ターミナルタブを閉じる前に確認**:ローカルターミナルタブを閉じる前に確認ダイアログを表示 + - **SSH タブを閉じる前に確認**:SSH 接続タブを閉じる前に確認ダイアログを表示(SSH の切断はコストが高いため、SSH のみ有効にすることもできます) +- ユーザーによるすべての閉じ操作を対象:タブの × ボタン、右クリックメニューの「閉じる / ほかを閉じる / 右側を閉じる」、および Delete / Backspace キー +- 「ほかを閉じる / 右側を閉じる」はバッチ全体で 1 回だけ確認し、閉じるタブ数を表示します +- タブにキーボードフォーカスがある状態での Backspace / Delete は直接閉じず、同じ確認フローを通ります——誤操作の主な原因でした +- スイッチは保存後すぐに反映され、再起動は不要です。ターミナル内での通常の Backspace / Delete 入力には一切影響しません +- セッションの自然終了やレイアウトテンプレート適用などのプログラム的な閉じでは確認しません + ## v1.0.26 - 2026-10-11 今回の重点はパフォーマンスと診断性です:ウィンドウのドラッグリサイズや端末の高頻度出力で UI 全体が数秒間フリーズする問題を修正し、診断ログの保存(デフォルト有効・7 日で自動削除)を追加しました。フリーズ報告に証拠が添えられるようになります。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e0e16c..f8c8812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # OpenTerminal 更新日志 +## v1.0.27 - 2026-10-15 + +本次新增标签页关闭二次确认:误触关闭不再直接杀掉正在工作的终端或 SSH 会话,尤其是点击标签后顺手按 Backspace 把整个 tab 关掉的高频误操作。 + +### 新增:关闭标签二次确认 +- 设置 → 渲染与行为 新增两个开关(**默认关闭**,不开启则与原行为完全一致): + - **关闭终端标签二次确认**:关闭本地终端标签前弹出确认框 + - **关闭 SSH 连接标签二次确认**:关闭 SSH 连接标签前弹出确认框(SSH 断开代价高,可以只对它开启) +- 覆盖所有用户关闭入口:标签 × 按钮、右键菜单的「关闭 / 关闭其他 / 关闭右侧」、以及键盘 Delete / Backspace +- 「关闭其他 / 关闭右侧」对整批只弹一次确认,确认框会显示将关闭的数量 +- 点击标签获得焦点后按 Backspace / Delete 不再直接关闭标签,而是走同样的确认逻辑——这是误关闭的主要来源 +- 开关保存后立即生效,无需重启;终端内正常输入退格、删除完全不受影响 +- 会话自然退出、应用布局模板等程序性关闭不弹确认 + ## v1.0.26 - 2026-10-11 本次重点是性能与可诊断性:修复了拖动缩放窗口或终端高频输出时整个界面冻结数秒的问题;新增落盘的诊断日志(默认开启,7 天自动清理),以后反馈卡顿可以附上日志,定位不再靠猜。 diff --git a/CHANGELOG.zh-TW.md b/CHANGELOG.zh-TW.md index 8f98b14..2369553 100644 --- a/CHANGELOG.zh-TW.md +++ b/CHANGELOG.zh-TW.md @@ -1,5 +1,19 @@ # OpenTerminal 更新日誌 +## v1.0.27 - 2026-10-15 + +本次新增分頁關閉二次確認:誤觸關閉不再直接殺掉正在工作的終端機或 SSH 連線,尤其是點擊分頁後順手按 Backspace 把整個分頁關掉的高頻誤操作。 + +### 新增:關閉分頁二次確認 +- 設定 → 渲染與行為 新增兩個開關(**預設關閉**,不開啟則與原行為完全一致): + - **關閉終端機標籤二次確認**:關閉本地終端機分頁前彈出確認框 + - **關閉 SSH 連線標籤二次確認**:關閉 SSH 連線分頁前彈出確認框(SSH 斷開代價高,可以只對它開啟) +- 涵蓋所有使用者關閉入口:分頁 × 按鈕、右鍵選單的「關閉 / 關閉其他 / 關閉右側」、以及鍵盤 Delete / Backspace +- 「關閉其他 / 關閉右側」對整批只彈一次確認,確認框會顯示將關閉的數量 +- 點擊分頁取得焦點後按 Backspace / Delete 不再直接關閉分頁,而是走同樣的確認邏輯——這是誤關閉的主要來源 +- 開關儲存後立即生效,無需重啟;終端機內正常輸入退格、刪除完全不受影響 +- 連線自然退出、套用版面配置範本等程式性關閉不彈確認 + ## v1.0.26 - 2026-10-11 本次重點是效能與可診斷性:修復拖曳縮放視窗或終端機高頻輸出時整個介面凍結數秒的問題;新增落盤的診斷日誌(預設開啟,7 天自動清理),日後回報卡頓可附上月誌,定位不再靠猜。 diff --git a/package-lock.json b/package-lock.json index ecaf203..37f3646 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-terminal", - "version": "1.0.26", + "version": "1.0.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-terminal", - "version": "1.0.26", + "version": "1.0.27", "license": "MIT", "dependencies": { "@lydell/node-pty": "^1.2.0-beta.15", diff --git a/package.json b/package.json index b4c5e6c..f9a159f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "open-terminal", "productName": "OpenTerminal", - "version": "1.0.26", + "version": "1.0.27", "description": "Open-source terminal with SSH, split panes, themes and fonts", "main": "out/main/index.js", "author": "CodingPlan.Site", diff --git a/src/renderer/src/settings/SettingsTabs.tsx b/src/renderer/src/settings/SettingsTabs.tsx index 036137a..553dc02 100644 --- a/src/renderer/src/settings/SettingsTabs.tsx +++ b/src/renderer/src/settings/SettingsTabs.tsx @@ -275,6 +275,26 @@ export function RenderSettingsTab(): React.JSX.Element { /> } /> + void updateTerminal({ confirmTabClose: checked })} + /> + } + /> + void updateTerminal({ confirmSshTabClose: checked })} + /> + } + /> (api.title) + const [pending, setPending] = useState(null) + const rootRef = useRef(null) const selfIndex = panel.panels.findIndex((p) => p.api === api) // dockview keeps the tab component in its own render slot — a re-render of @@ -56,10 +61,86 @@ export function TerminalTab({ api, params }: TerminalTabProps): React.JSX.Elemen return () => disposable.dispose() }, [api]) - const closePanel = (target: typeof panel.panels[number]): void => { - target.api.close() + // `api.group` is read live rather than reusing the render-time `panel`: + // dragging this tab into another group re-parents the DOM without re-mounting + // the React component (dockview keeps the portal element and only swaps the + // `.dv-tab` shell around it), so anything captured at render would still name + // the group this tab has left. + const selfPanel = (): TabPanel | undefined => api.group.panels.find((p) => p.api === api) + + // Confirmation has to happen *before* `api.close()`: dockview has no + // vetoable close hook, and the workspace kills the session from + // `onDidRemovePanel`, which is already too late to ask. Every user-initiated + // close entry point funnels through here; programmatic closes (a dead + // session, applying a layout template) call `api.close()` directly. + // + // Read the settings at call time: this runs from long-lived menu / keydown + // handlers, so a value captured at render would go stale after a change. + const needsConfirmFor = (targets: TabPanel[]): boolean => { + const terminal = useSettingsStore.getState().settings.terminal + return targets.some((p) => + (p.params as { sessionKind?: 'local' | 'ssh' } | undefined)?.sessionKind === 'ssh' + ? terminal.confirmSshTabClose + : terminal.confirmTabClose + ) } + const requestClose = (targets: TabPanel[]): void => { + if (targets.length === 0) return + if (!needsConfirmFor(targets)) { + for (const target of targets) target.api.close() + return + } + setPending(targets) + } + + const closePending = (): void => { + if (!pending) return + // Liveness filter: something else (a dead session, another tab's menu) may + // have closed a target while the dialog was up. Asked of the panel itself — + // its `group` follows it across groups, unlike a render-time snapshot. + for (const target of pending) if (target.group.panels.includes(target)) target.api.close() + setPending(null) + } + + // dockview closes the focused tab on Delete/Backspace from a keydown listener + // on the tab list, but only when the event target *is* the `.dv-tab` shell — + // which is what has focus, and an ancestor of this component's root, so a + // React handler on the root never sees the key. The listener therefore has to + // be native, on the shell, in the capture phase (dockview listens during the + // bubble phase). It is attached to the document rather than to the shell + // itself because dragging a tab into another group re-parents this component's + // DOM into a fresh shell *without* re-mounting or re-rendering it: a + // shell-scoped listener would be stranded on the discarded element and + // Delete/Backspace would silently stop working for that tab. Everything the + // handler reads is resolved live (`api.group`, the settings), so the closure + // from the first render stays correct. + // + // The event is only swallowed when a confirmation is actually required; + // otherwise dockview's own handler runs as before, focus moved to the + // neighbouring tab included, so "off" is indistinguishable from the old + // behaviour. + useEffect(() => { + const doc = rootRef.current?.ownerDocument ?? document + const onKeyDown = (event: KeyboardEvent): void => { + if (event.key !== 'Delete' && event.key !== 'Backspace') return + const shell = rootRef.current?.closest('.dv-tab') + if (!shell || event.target !== shell) return + // Only the real tab strip: the overflow dropdown renders a second + // `.dv-tab`-classed wrapper around a copy of this component, and dockview's + // own handler is scoped to the tab list, so match that scope instead of + // closing a panel the platform would have left alone. + if (!shell.closest('.dv-tabs-container')) return + const self = selfPanel() + if (!self || !needsConfirmFor([self])) return + event.preventDefault() + event.stopPropagation() + requestClose([self]) + } + doc.addEventListener('keydown', onKeyDown, true) + return () => doc.removeEventListener('keydown', onKeyDown, true) + }, [api]) + const menu: MenuProps = { items: [ { key: 'close', label: tFor(language, 'common.close') }, @@ -67,56 +148,78 @@ export function TerminalTab({ api, params }: TerminalTabProps): React.JSX.Elemen { key: 'closeRight', label: tFor(language, 'workspace.tab.closeRight'), disabled: selfIndex === -1 || selfIndex === panel.panels.length - 1 } ], onClick: ({ key }) => { - if (key === 'close') { + const self = selfPanel() + if (!self) { api.close() + return + } + // `api.group.panels` is the live group array and `close()` splices it, so + // the targets must be snapshotted first — otherwise every element that + // shifts into the current index is skipped. Read through `api.group` + // rather than a render-time copy in case the tab has since been dragged + // into another group. + const siblings = [...api.group.panels] + if (key === 'close') { + requestClose([self]) } else if (key === 'closeOthers') { - // `panel.panels` is the live group array and `close()` splices it, so - // the targets must be snapshotted first — otherwise every element that - // shifts into the current index is skipped. - for (const sibling of [...panel.panels]) { - if (sibling.api !== api) closePanel(sibling) - } + requestClose(siblings.filter((sibling) => sibling.api !== api)) } else if (key === 'closeRight') { - for (const target of [...panel.panels].slice(selfIndex + 1)) { - closePanel(target) - } + requestClose(siblings.slice(siblings.indexOf(self) + 1)) } } } return ( - -
- {isSsh && } - {title} - {isSsh && hostLabel && } - {isBroadcastTarget && ( - - {tFor(language, 'workspace.tab.broadcastBadge')} - - )} - -
-
+ {isSsh && } + {title} + {isSsh && hostLabel && } + {isBroadcastTarget && ( + + {tFor(language, 'workspace.tab.broadcastBadge')} + + )} + + + + setPending(null)} + > + {pending && pending.length === 1 + ? tFor(language, 'workspace.tab.closeConfirmOne', { title: pending[0].title ?? '' }) + : tFor(language, 'workspace.tab.closeConfirmMany', { count: pending?.length ?? 0 })} + + ) } diff --git a/src/shared/i18n/dicts/en/settings.ts b/src/shared/i18n/dicts/en/settings.ts index 3b54636..3307da6 100644 --- a/src/shared/i18n/dicts/en/settings.ts +++ b/src/shared/i18n/dicts/en/settings.ts @@ -74,6 +74,12 @@ const settings: Record = { 'settings.render.pasteRiskConfirm': 'Confirm risky paste', 'settings.render.pasteRiskConfirmDesc': 'Ask before pasting multi-line text (or a very long single line); single lines paste directly', + 'settings.render.confirmTabClose': 'Confirm before closing terminal tabs', + 'settings.render.confirmTabCloseDesc': + 'Ask for confirmation before closing a local terminal tab', + 'settings.render.confirmSshTabClose': 'Confirm before closing SSH tabs', + 'settings.render.confirmSshTabCloseDesc': + 'Ask for confirmation before closing an SSH connection tab', 'settings.render.suggest': 'Input suggestions', 'settings.render.suggestDesc': 'Show command suggestions while typing; Tab accepts, Enter always runs the command right away', diff --git a/src/shared/i18n/dicts/en/workspace.ts b/src/shared/i18n/dicts/en/workspace.ts index 9ced72b..8ba2049 100644 --- a/src/shared/i18n/dicts/en/workspace.ts +++ b/src/shared/i18n/dicts/en/workspace.ts @@ -4,6 +4,10 @@ const workspace: Record = { 'workspace.tab.localTerminal': 'Local terminal', 'workspace.tab.closeOthers': 'Close others', 'workspace.tab.closeRight': 'Close to the right', + 'workspace.tab.closeConfirmTitle': 'Close tab?', + 'workspace.tab.closeConfirmOne': 'Close "{title}"? Its session will be terminated.', + 'workspace.tab.closeConfirmMany': 'Close {count} tabs? Their sessions will be terminated.', + 'workspace.tab.closeConfirmOk': 'Close', 'workspace.tab.broadcastTarget': 'Broadcast target', 'workspace.tab.broadcastBadge': 'B', 'workspace.tab.addInSplit': 'New terminal in this split', diff --git a/src/shared/i18n/dicts/ja/settings.ts b/src/shared/i18n/dicts/ja/settings.ts index 8914781..5731424 100644 --- a/src/shared/i18n/dicts/ja/settings.ts +++ b/src/shared/i18n/dicts/ja/settings.ts @@ -72,6 +72,12 @@ const settings: Record = { 'settings.render.pasteRiskConfirm': '貼り付け前の確認', 'settings.render.pasteRiskConfirmDesc': '複数行(または極端に長い 1 行)を貼り付ける前に確認します。1 行はそのまま貼り付けます', + 'settings.render.confirmTabClose': 'ターミナルタブを閉じる前に確認', + 'settings.render.confirmTabCloseDesc': + 'ローカルターミナルタブを閉じる前に確認ダイアログを表示', + 'settings.render.confirmSshTabClose': 'SSH タブを閉じる前に確認', + 'settings.render.confirmSshTabCloseDesc': + 'SSH 接続タブを閉じる前に確認ダイアログを表示', 'settings.render.suggest': '入力候補', 'settings.render.suggestDesc': '入力中にコマンド候補を表示します。Tab で確定、Enter は常にそのまま実行します', diff --git a/src/shared/i18n/dicts/ja/workspace.ts b/src/shared/i18n/dicts/ja/workspace.ts index f97e1dd..1fc3a54 100644 --- a/src/shared/i18n/dicts/ja/workspace.ts +++ b/src/shared/i18n/dicts/ja/workspace.ts @@ -4,6 +4,10 @@ const workspace: Record = { 'workspace.tab.localTerminal': 'ローカルターミナル', 'workspace.tab.closeOthers': 'ほかを閉じる', 'workspace.tab.closeRight': '右側を閉じる', + 'workspace.tab.closeConfirmTitle': 'タブを閉じますか?', + 'workspace.tab.closeConfirmOne': '「{title}」を閉じますか?対応するセッションは終了します。', + 'workspace.tab.closeConfirmMany': '{count} 個のタブを閉じますか?対応するセッションは終了します。', + 'workspace.tab.closeConfirmOk': '閉じる', 'workspace.tab.broadcastTarget': 'ブロードキャスト対象', 'workspace.tab.broadcastBadge': '配', 'workspace.tab.addInSplit': 'この分割に新しいターミナル', diff --git a/src/shared/i18n/dicts/zh-CN/settings.ts b/src/shared/i18n/dicts/zh-CN/settings.ts index 61579b0..d4497d4 100644 --- a/src/shared/i18n/dicts/zh-CN/settings.ts +++ b/src/shared/i18n/dicts/zh-CN/settings.ts @@ -71,6 +71,10 @@ const settings: Record = { 'settings.render.copyOnSelectDesc': '鼠标选中文字时自动复制', 'settings.render.pasteRiskConfirm': '粘贴风险确认', 'settings.render.pasteRiskConfirmDesc': '多行文本(或超长单行)粘贴前弹出确认;单行直接粘贴', + 'settings.render.confirmTabClose': '关闭终端标签二次确认', + 'settings.render.confirmTabCloseDesc': '关闭本地终端标签前弹出确认框', + 'settings.render.confirmSshTabClose': '关闭 SSH 连接标签二次确认', + 'settings.render.confirmSshTabCloseDesc': '关闭 SSH 连接标签前弹出确认框', 'settings.render.suggest': '输入建议', 'settings.render.suggestDesc': '输入时弹出命令建议;Tab 接受,回车始终直接执行', 'settings.render.history': '记录命令历史', diff --git a/src/shared/i18n/dicts/zh-CN/workspace.ts b/src/shared/i18n/dicts/zh-CN/workspace.ts index c08243a..499a253 100644 --- a/src/shared/i18n/dicts/zh-CN/workspace.ts +++ b/src/shared/i18n/dicts/zh-CN/workspace.ts @@ -4,6 +4,10 @@ const workspace: Record = { 'workspace.tab.localTerminal': '本地终端', 'workspace.tab.closeOthers': '关闭其他', 'workspace.tab.closeRight': '关闭右侧', + 'workspace.tab.closeConfirmTitle': '确认关闭标签', + 'workspace.tab.closeConfirmOne': '确定要关闭「{title}」吗?对应的会话将被终止。', + 'workspace.tab.closeConfirmMany': '确定要关闭 {count} 个标签吗?对应的会话将被终止。', + 'workspace.tab.closeConfirmOk': '关闭', 'workspace.tab.broadcastTarget': '广播目标', 'workspace.tab.broadcastBadge': '播', 'workspace.tab.addInSplit': '在此分屏中新建终端', diff --git a/src/shared/i18n/dicts/zh-TW/settings.ts b/src/shared/i18n/dicts/zh-TW/settings.ts index 16a2bb3..9d7e1ee 100644 --- a/src/shared/i18n/dicts/zh-TW/settings.ts +++ b/src/shared/i18n/dicts/zh-TW/settings.ts @@ -71,6 +71,10 @@ const settings: Record = { 'settings.render.copyOnSelectDesc': '以滑鼠選取文字時自動複製', 'settings.render.pasteRiskConfirm': '貼上風險確認', 'settings.render.pasteRiskConfirmDesc': '多行文字(或超長單行)貼上前彈出確認;單行直接貼上', + 'settings.render.confirmTabClose': '關閉終端標籤二次確認', + 'settings.render.confirmTabCloseDesc': '關閉本地終端標籤前彈出確認框', + 'settings.render.confirmSshTabClose': '關閉 SSH 連線標籤二次確認', + 'settings.render.confirmSshTabCloseDesc': '關閉 SSH 連線標籤前彈出確認框', 'settings.render.suggest': '輸入建議', 'settings.render.suggestDesc': '輸入時顯示指令建議;Tab 接受,Enter 一律直接執行', 'settings.render.history': '記錄指令歷史', diff --git a/src/shared/i18n/dicts/zh-TW/workspace.ts b/src/shared/i18n/dicts/zh-TW/workspace.ts index 29e854a..59a11d6 100644 --- a/src/shared/i18n/dicts/zh-TW/workspace.ts +++ b/src/shared/i18n/dicts/zh-TW/workspace.ts @@ -4,6 +4,10 @@ const workspace: Record = { 'workspace.tab.localTerminal': '本機終端機', 'workspace.tab.closeOthers': '關閉其他', 'workspace.tab.closeRight': '關閉右側', + 'workspace.tab.closeConfirmTitle': '確認關閉標籤', + 'workspace.tab.closeConfirmOne': '確定要關閉「{title}」嗎?對應的會話將被終止。', + 'workspace.tab.closeConfirmMany': '確定要關閉 {count} 個標籤嗎?對應的會話將被終止。', + 'workspace.tab.closeConfirmOk': '關閉', 'workspace.tab.broadcastTarget': '廣播目標', 'workspace.tab.broadcastBadge': '播', 'workspace.tab.addInSplit': '在此分割中新增終端機', diff --git a/src/shared/settings.ts b/src/shared/settings.ts index c7eb035..1b0140d 100644 --- a/src/shared/settings.ts +++ b/src/shared/settings.ts @@ -66,6 +66,10 @@ export interface TerminalSettings { copyOnSelect: boolean /** confirm before pasting multi-line / suspicious text */ pasteRiskConfirm: boolean + /** Ask for confirmation before a user-initiated close of a local terminal tab. */ + confirmTabClose: boolean + /** Same for SSH connection tabs. */ + confirmSshTabClose: boolean /** inline command suggestion popup while typing (history + library) */ suggestEnabled: boolean /** record executed commands into the history panel */ @@ -476,6 +480,8 @@ export const DEFAULT_SETTINGS: AppSettings = { rendererMode: 'auto', copyOnSelect: false, pasteRiskConfirm: true, + confirmTabClose: false, + confirmSshTabClose: false, suggestEnabled: false, historyEnabled: false, historyLimit: 100, diff --git a/tests/settings-store.mjs b/tests/settings-store.mjs index 5c90033..1dbdff2 100644 --- a/tests/settings-store.mjs +++ b/tests/settings-store.mjs @@ -185,6 +185,17 @@ console.log('[value bands]') ok(back.terminal.highlightStats === on, `the stats switch '${on}' survives the round trip`) } } +{ + for (const on of [true, false]) { + const back = await roundTrip({ + terminal: { ...store.loadSettings().terminal, confirmTabClose: on, confirmSshTabClose: on } + }) + ok( + back.terminal.confirmTabClose === on && back.terminal.confirmSshTabClose === on, + `tab close confirm switches '${on}' survive the round trip` + ) + } +} { const rules = [rule({ id: 'keep-1' }), rule({ id: 'keep-2' })] await roundTrip({ highlightRules: rules }) From 2b658520a4d9d00bbc64e8db1c5ca7294213400c Mon Sep 17 00:00:00 2001 From: Bill Date: Fri, 16 Oct 2026 00:53:00 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(updater):=20=E7=A6=81=E7=94=A8=E6=B3=A8?= =?UTF-8?q?=E5=AE=9A=E5=A4=B1=E8=B4=A5=E7=9A=84=E5=B7=AE=E9=87=8F=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=EF=BC=8C=E4=BF=AE=E5=A4=8D=E6=9B=B4=E6=96=B0=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E4=B8=A4=E6=AC=A1=EF=BC=9B=E5=AE=98=E7=BD=91=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=20v1.0.28=EF=BC=88=E8=A1=A5=201.0.26/1.0.27=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + CHANGELOG.en.md | 9 ++++ CHANGELOG.ja.md | 9 ++++ CHANGELOG.md | 9 ++++ CHANGELOG.zh-TW.md | 9 ++++ package-lock.json | 4 +- package.json | 2 +- src/main/updater.ts | 15 +++++++ tests/updater-fallback.mjs | 7 +++ website/assets/i18n.js | 92 +++++++++++++++++--------------------- website/index.html | 57 +++++++++++------------ 11 files changed, 129 insertions(+), 85 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1aa221b..9199069 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,6 +59,7 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。 - **调度器是注入的**:`applyUpdateSchedule(system)` 由 `index.ts` 经 `settingsStore.setUpdateScheduleApplier` 注入——`updater.ts` 已 import `settingsStore`,反向 import 成环。applier 幂等(先清旧 interval 再建)且每次 `mutateSettings` 都会重跑;启动检查用模块级 `startupArmed` 保证**每进程只武装一次**,否则改一次设置就多排一次启动检查 - **自动下载**(`settings.system.autoDownloadUpdate`,默认关):`update-available` 时若开关开且无在途下载 → `handleDownload('auto')`(`UpdateState.auto` 标记)。`autoUpdater.autoDownload` **保持 false**——单一事实源在 settings,不交给 electron-updater。失败落 `status:'error'` 且**不重试**,等下一个周期 - **安装永远交给用户**:绝不因下载完成自动退出;`autoInstallOnAppQuit = true` 只覆盖「用户自己退出时装上」 +- **差量下载已禁用**(`autoUpdater.disableDifferentialDownload = true`,configureAutoUpdater 里):electron-updater 每次 `downloadUpdate()` 先试差量(拉新旧 blockmap + 多段 Range)失败再回退全量——用户看到进度条跑两遍、最坏近乎双倍流量(v1.0.x 用户报的「下载两次」)。而本项目三个前提让它注定失败:GitHub release 资产落 Azure Blob,多段 Range 返回 `501 Unsupported client range`(已实测);国内 Gitea 通道被 `pruneChannel` 删得只剩最新版 exe/blockmap,老客户端取老 blockmap 得 404;NSIS 是 LZMA 固实压缩,版本间小改动让后续压缩数据整体漂移,差量区间≈整个文件。所以直接全量是唯一正确行为 - **同版本抑制**(`actedRelease` 单槽记录,`actedFor(version)` 版本不同即整体替换):`announced`(发现气泡)、`fetchHandled`(已发起下载)、`downloaded`(包已落盘)、`downloadedAnnounced`(已下载气泡)——否则长开机的应用每 4 小时重复弹气泡/重复走一遍 `downloadUpdate`。`runCheck('manual')` **无条件清空**记录(手动是明确意图,必须如实反映);`update-not-available`/`error` **不清空**;下载失败回滚该次 `fetchHandled` - `downloaded` 与 `fetchHandled` **必须分开**:前者只在 `update-downloaded` 置位,用它判断「已完成」;拿 `fetchHandled` 判断会把下载中的版本误跳过。`update-available` 里若该版本 `downloaded` 为真,要 `setState` 一个等价的 `downloaded` 状态(**不能**直接 return 不写——`performCheck` 已把状态置成 `checking`,早退会让「关于」页永远转圈),这样按钮不会从「重启安装」退回「下载」 - **通知**:`tray.ts` 的 `notifyUpdate(title, content)` 弹 Windows 托盘气泡;非 win32 / tray 为空 / **有可见窗口**(用户在看「关于」页)都不弹。自动下载开启时不弹「发现新版本」(有意义的是「已下载」那条),手动下载完成也不弹 diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index d373fa6..b3f173d 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -1,5 +1,14 @@ # OpenTerminal Changelog +## v1.0.28 - 2026-10-15 + +This release fixes the "downloads twice" issue during in-app updates: when updating from an older version the progress bar ran through twice, costing nearly double the traffic in the worst case. + +### Fixed: update downloads twice +- The root cause: the updater tried a "differential download" (fetching only the changed chunks) on every download and fell back to a full re-download when it failed — and neither of our update sources can serve differential downloads, so every update ran as a differential attempt followed by a full package +- Differential download is now disabled: **every update is exactly one full download**, and the progress bar runs through once +- Note: users still on an older version will see the double download one last time while updating TO this version (the fix has to reach them first); once this version is installed, every future update downloads once + ## v1.0.27 - 2026-10-15 This release adds close confirmation for terminal tabs: an accidental close no longer kills a terminal or SSH session you are working in — especially the frequent case of pressing Backspace right after clicking a tab. diff --git a/CHANGELOG.ja.md b/CHANGELOG.ja.md index c6ac17a..b946fc6 100644 --- a/CHANGELOG.ja.md +++ b/CHANGELOG.ja.md @@ -1,5 +1,14 @@ # OpenTerminal 更新履歴 +## v1.0.28 - 2026-10-15 + +本リリースではアプリ内アップデートの「2 回ダウンロード」問題を修正しました。旧バージョンからの更新時にプログレスバーが 2 周し、最悪の場合ほぼ倍の通信量が発生していました。 + +### 修正:アップデートが 2 回ダウンロードされる +- 根本原因は、アップデーターが毎回「差分ダウンロード」(変更されたチャンクのみ取得)を試み、失敗するとパッケージ全体を再ダウンロードすることでした——本プロジェクトの 2 つの更新ソースはいずれも差分ダウンロードの条件を満たさないため、毎回「差分試行 + 全パッケージ」の 2 段階になっていました +- 差分ダウンロードを無効化し、**毎回ちょうど 1 回のフルダウンロード**になり、プログレスバーは 1 周だけです +- 注意:旧バージョンのユーザーが本バージョンへ更新する際は、最後にもう一度だけ 2 回ダウンロードが発生します(修正コードが先に手元へ届く必要があるため)。本バージョン導入後の更新はすべて 1 回のダウンロードになります + ## v1.0.27 - 2026-10-15 本リリースではタブを閉じる前の確認機能を追加しました。うっかり閉じて作業中のターミナルや SSH セッションを終了させてしまう——特にタブをクリックした直後に Backspace を押してタブごと閉じてしまう操作——を防ぎます。 diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c8812..b439c62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # OpenTerminal 更新日志 +## v1.0.28 - 2026-10-15 + +本次修复应用内更新「下载两次」的问题:从旧版本更新时进度条会跑两遍,最坏情况消耗近双倍流量。 + +### 修复:更新下载两次 +- 根因是更新组件每次下载都先尝试「差量下载」(只取变化的数据块),失败后回退整包重下——而本项目的两个更新源都不具备差量条件,导致每次更新都是"差量尝试 + 整包"两段式 +- 现已禁用差量下载,**每次更新恰好一次整包下载**,进度条只走一遍 +- 注意:还在旧版本上的用户,更新到本版本时会经历**最后一次**下载两次(修复代码要先到达用户手里);装上本版本后,之后的每次更新都是单次下载 + ## v1.0.27 - 2026-10-15 本次新增标签页关闭二次确认:误触关闭不再直接杀掉正在工作的终端或 SSH 会话,尤其是点击标签后顺手按 Backspace 把整个 tab 关掉的高频误操作。 diff --git a/CHANGELOG.zh-TW.md b/CHANGELOG.zh-TW.md index 2369553..201433c 100644 --- a/CHANGELOG.zh-TW.md +++ b/CHANGELOG.zh-TW.md @@ -1,5 +1,14 @@ # OpenTerminal 更新日誌 +## v1.0.28 - 2026-10-15 + +本次修復應用內更新「下載兩次」的問題:從舊版本更新時進度條會跑兩遍,最壞情況消耗近雙倍流量。 + +### 修復:更新下載兩次 +- 根因是更新元件每次下載都先嘗試「差量下載」(只取變化的資料塊),失敗後回退整包重下——而本專案的兩個更新來源都不具備差量條件,導致每次更新都是「差量嘗試 + 整包」兩段式 +- 現已停用差量下載,**每次更新恰好一次整包下載**,進度條只走一遍 +- 注意:還在舊版本上的使用者,更新到此版本時會經歷**最後一次**下載兩次(修復程式要先到達使用者手裡);裝上此版本後,之後的每次更新都是單次下載 + ## v1.0.27 - 2026-10-15 本次新增分頁關閉二次確認:誤觸關閉不再直接殺掉正在工作的終端機或 SSH 連線,尤其是點擊分頁後順手按 Backspace 把整個分頁關掉的高頻誤操作。 diff --git a/package-lock.json b/package-lock.json index 37f3646..70de57e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-terminal", - "version": "1.0.27", + "version": "1.0.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-terminal", - "version": "1.0.27", + "version": "1.0.28", "license": "MIT", "dependencies": { "@lydell/node-pty": "^1.2.0-beta.15", diff --git a/package.json b/package.json index f9a159f..95d9e09 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "open-terminal", "productName": "OpenTerminal", - "version": "1.0.27", + "version": "1.0.28", "description": "Open-source terminal with SSH, split panes, themes and fonts", "main": "out/main/index.js", "author": "CodingPlan.Site", diff --git a/src/main/updater.ts b/src/main/updater.ts index b2ce8d0..ead33cb 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -499,6 +499,21 @@ export function configureAutoUpdater(): void { // ever installs on its own — autoInstallOnAppQuit only covers quitting. autoUpdater.autoDownload = false autoUpdater.autoInstallOnAppQuit = true + // Differential download is off because it cannot succeed against either of + // our feeds, and every failed attempt costs the user a second full transfer: + // electron-updater's differential path fetches the old and new blockmaps and + // then multi-range-requests the changed chunks, falling back to a plain whole- + // file download when any of that fails. GitHub serves the release assets from + // Azure Blob, which answers multi-range requests with `501 Unsupported client + // range` (measured), and the Gitea channel keeps only the newest exe + blockmap + // (`pruneChannel` deletes the previous release's), so an older client's + // blockmap 404s. Even where both were available the NSIS payload is LZMA solid + // compression: a small version-to-version change shifts everything after it, + // making the differential range nearly the whole file. The user then watches + // the progress bar run twice — worst case for roughly double the traffic — so + // going straight to the full download is the only behavior that is actually + // correct here. + autoUpdater.disableDifferentialDownload = true // The startup check and the periodic one are armed by applyUpdateSchedule // (wired in index.ts through settingsStore's injected applier) instead of diff --git a/tests/updater-fallback.mjs b/tests/updater-fallback.mjs index 56259d8..d68fe28 100644 --- a/tests/updater-fallback.mjs +++ b/tests/updater-fallback.mjs @@ -439,6 +439,13 @@ console.log('updater events drive the state the renderer reads') ok(live.logger === console, 'the updater logs through console') ok(live.autoDownload === false, 'auto-download is left to the user (the UI offers the button)') ok(live.autoInstallOnAppQuit === true, 'a downloaded update installs on app quit') + // Differential download is disabled: it cannot work against either feed (GitHub + // assets reject multi-range requests, the Gitea channel keeps no old blockmap), + // and each failure costs a second full transfer. + ok( + live.disableDifferentialDownload === true, + 'differential download is off (a failed attempt would download the package twice)' + ) const before = await call(Ipc.UPDATE_STATE_GET) ok(before !== undefined && typeof before.currentVersion === 'string', 'UPDATE_STATE_GET returns the current state') diff --git a/website/assets/i18n.js b/website/assets/i18n.js index e07ff25..5f25d06 100644 --- a/website/assets/i18n.js +++ b/website/assets/i18n.js @@ -155,6 +155,16 @@ window.OPENTERMINAL_I18N = { 'cl.eyebrow': 'Changelog', 'cl.title': '近期更新', + 'changelog.1.0.28.li.1': '修复应用内更新会下载两次的问题:禁用注定失败的差量下载,每次更新恰好一次整包下载,进度条只走一遍', + + 'changelog.1.0.27.li.1': '新增关闭标签二次确认:终端 / SSH 两个独立开关(默认关闭),设置 → 渲染与行为', + 'changelog.1.0.27.li.2': '覆盖标签 × 按钮、右键「关闭 / 关闭其他 / 关闭右侧」与键盘 Delete / Backspace 误关——点击标签后按 Backspace 不再直接关闭', + 'changelog.1.0.27.li.3': '批量关闭整批只确认一次;开关保存后立即生效,无需重启', + + 'changelog.1.0.26.li.1': '修复拖动缩放窗口或终端高频输出时界面冻结数秒(网格重排节流 + 输出批量写入)', + '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': '下载完成 / 发现新版本时窗口最小化到托盘则弹提示', @@ -165,19 +175,6 @@ window.OPENTERMINAL_I18N = { 'changelog.1.0.24.li.3': '会话日志退出不再丢尾部', 'changelog.1.0.24.li.4': 'SFTP 删除不再误报失败;ZMODEM 接收大文件内存可控;新增 3 组渲染层测试', - 'changelog.1.0.23.li.1': 'SFTP 文件面板改为真正的表格:六列对齐、表头吸顶、点击列头排序', - 'changelog.1.0.23.li.2': '字体可缩放 0.8–1.8 倍(A- / A+ 或 Ctrl+滚轮),行高联动并记忆设置', - 'changelog.1.0.23.li.3': '用户 / 组列显示真实名称,不再只有数字 UID / GID', - 'changelog.1.0.23.li.4': '列表空白处右键即可唤出文件菜单;修复六列此前未真正按网格对齐的问题', - - 'changelog.1.0.22.li.1': '修复中文输入法候选窗不跟随光标(v1.0.21 回归)', - 'changelog.1.0.22.li.2': '不再提供 MSI 安装包,统一使用 NSIS exe', - 'changelog.1.0.22.li.3': '打包流程新增依赖一致性校验,防止旧依赖混入安装包', - - 'changelog.1.0.21.li.1': '背景图体验:任意主题下调透明度均为压暗、文字自动提亮到 WCAG AA、新增背景压暗滑杆', - 'changelog.1.0.21.li.2': '性能:设置热更不再触发全窗格重排,SFTP 大目录虚拟滚动,面板独立错误边界', - 'changelog.1.0.21.li.3': '安装包精简约 90MB;SFTP/ZMODEM 本地路径准入与多处安全加固', - 'cl.morePre': '完整日志见仓库', 'cl.morePost': '。', @@ -332,6 +329,16 @@ window.OPENTERMINAL_I18N = { 'cl.eyebrow': 'Changelog', 'cl.title': '近期更新', + 'changelog.1.0.28.li.1': '修復應用內更新會下載兩次的問題:停用注定失敗的差量下載,每次更新恰好一次整包下載,進度條只走一遍', + + 'changelog.1.0.27.li.1': '新增關閉分頁二次確認:終端機 / SSH 兩個獨立開關(預設關閉),設定 → 渲染與行為', + 'changelog.1.0.27.li.2': '涵蓋分頁 × 按鈕、右鍵「關閉 / 關閉其他 / 關閉右側」與鍵盤 Delete / Backspace 誤關——點擊分頁後按 Backspace 不再直接關閉', + 'changelog.1.0.27.li.3': '批量關閉整批只確認一次;開關儲存後立即生效,無需重啟', + + 'changelog.1.0.26.li.1': '修復拖動縮放視窗或終端機高頻輸出時介面凍結數秒(網格重排節流 + 輸出批量寫入)', + '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': '下載完成 / 發現新版本時,視窗若最小化到系統匣會彈出提示', @@ -342,19 +349,6 @@ window.OPENTERMINAL_I18N = { 'changelog.1.0.24.li.3': '工作階段日誌結束時不再遺失尾段', 'changelog.1.0.24.li.4': 'SFTP 刪除不再誤報失敗;ZMODEM 接收大檔案的記憶體用量可控;新增 3 組渲染層測試', - 'changelog.1.0.23.li.1': 'SFTP 檔案面板改為真正的表格:六欄對齊、表頭固定、點擊表頭排序', - 'changelog.1.0.23.li.2': '字型可縮放 0.8–1.8 倍(A- / A+ 或 Ctrl+滾輪),行高連動並記憶設定', - 'changelog.1.0.23.li.3': '使用者 / 群組欄顯示真實名稱,不再只有數字 UID / GID', - 'changelog.1.0.23.li.4': '清單空白處按右鍵即可喚出檔案選單;修復六欄先前未真正依格線對齊的問題', - - 'changelog.1.0.22.li.1': '修復中文輸入法候選窗不跟隨游標(v1.0.21 回歸)', - 'changelog.1.0.22.li.2': '不再提供 MSI 安裝程式,統一使用 NSIS exe', - 'changelog.1.0.22.li.3': '打包流程新增相依性一致性校驗,防止舊相依性混入安裝程式', - - 'changelog.1.0.21.li.1': '背景圖片體驗:任何主題下調低不透明度都是壓暗、文字自動提亮至 WCAG AA、新增背景壓暗滑桿', - 'changelog.1.0.21.li.2': '效能:設定熱更新不再觸發全部窗格重排,SFTP 大目錄虛擬捲動,面板獨立錯誤邊界', - 'changelog.1.0.21.li.3': '安裝程式精簡約 90MB;SFTP/ZMODEM 本機路徑准入與多處安全加固', - 'cl.morePre': '完整日誌請見倉庫', 'cl.morePost': '。', @@ -509,6 +503,16 @@ window.OPENTERMINAL_I18N = { 'cl.eyebrow': 'Changelog', 'cl.title': 'Recent updates', + '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', + 'changelog.1.0.27.li.2': 'Covers the tab × button, the context menu\'s Close / Close others / Close right, and accidental Delete / Backspace — pressing Backspace after clicking a tab no longer closes it', + 'changelog.1.0.27.li.3': 'Batch closes ask once for the whole batch; switches take effect immediately, no restart needed', + + 'changelog.1.0.26.li.1': 'Fixed the UI freezing for seconds while resizing the window or under high-frequency terminal output (reflow throttling + batched writes)', + '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', @@ -519,19 +523,6 @@ window.OPENTERMINAL_I18N = { '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', - 'changelog.1.0.23.li.1': 'SFTP file panel is now a real table: six aligned columns, sticky header, click-to-sort', - 'changelog.1.0.23.li.2': 'Font scaling from 0.8x to 1.8x (A- / A+ or Ctrl+wheel); row height follows and the setting is remembered', - 'changelog.1.0.23.li.3': 'User / group columns show real names instead of bare UID / GID numbers', - 'changelog.1.0.23.li.4': 'Right-click empty list space for the file menu; fixed columns that were not actually aligned to the grid', - - 'changelog.1.0.22.li.1': 'Fixed the IME candidate window not following the cursor (regression in v1.0.21)', - 'changelog.1.0.22.li.2': 'MSI installer dropped; NSIS exe is now the only package', - 'changelog.1.0.22.li.3': 'Build pipeline now verifies dependency consistency to keep stale dependencies out of the installer', - - 'changelog.1.0.21.li.1': 'Background images: lowering opacity now always dims the image in any theme, text is auto-brightened to WCAG AA, and a new dim slider was added', - 'changelog.1.0.21.li.2': 'Performance: settings hot-updates no longer reflow every pane, SFTP virtual scrolling for large directories, per-pane error boundaries', - 'changelog.1.0.21.li.3': 'Installer slimmed by about 90MB; local path admission for SFTP / ZMODEM plus several security hardening fixes', - 'cl.morePre': 'Full changelog in', 'cl.morePost': '.', @@ -686,6 +677,16 @@ window.OPENTERMINAL_I18N = { 'cl.eyebrow': 'Changelog', 'cl.title': '最近の更新', + 'changelog.1.0.28.li.1': 'アプリ内アップデートが 2 回ダウンロードされる問題を修正:成功しない差分ダウンロードを無効化し、毎回ちょうど 1 回のフルダウンロードに', + + 'changelog.1.0.27.li.1': 'タブを閉じる前の確認機能を追加:ターミナル / SSH それぞれ個別のスイッチ(既定はオフ)、設定 → 描画と動作', + 'changelog.1.0.27.li.2': 'タブの × ボタン、右クリックメニューの「閉じる / ほかを閉じる / 右側を閉じる」、および Delete / Backspace による誤クローズに対応——タブクリック後の Backspace で閉じなくなります', + 'changelog.1.0.27.li.3': 'バッチクローズは全体で 1 回だけ確認;スイッチは保存後すぐに反映され、再起動不要', + + 'changelog.1.0.26.li.1': 'ウィンドウのリサイズや高頻度ターミナル出力時に UI が数秒固まる問題を修正(リフローのスロットリング + 書き込みバッチ化)', + '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': '新バージョンの検出時やダウンロード完了時に、ウィンドウがトレイに最小化されていれば通知を表示', @@ -696,19 +697,6 @@ window.OPENTERMINAL_I18N = { 'changelog.1.0.24.li.3': '終了時にセッションログの末尾が欠けなくなりました', 'changelog.1.0.24.li.4': 'SFTP の削除が誤って失敗扱いにならなくなりました。ZMODEM の大容量受信もメモリ使用量を抑制。レンダラー層のテストを 3 組追加', - 'changelog.1.0.23.li.1': 'SFTP ファイルパネルを本格的なテーブルに変更:6 列の整列、ヘッダーの固定表示、ヘッダークリックで並べ替え', - 'changelog.1.0.23.li.2': '文字サイズを 0.8〜1.8 倍に変更可能(A- / A+ または Ctrl+ホイール)。行の高さも連動し、設定は記憶されます', - 'changelog.1.0.23.li.3': 'ユーザー / グループ列に実際の名前を表示。数字だけの UID / GID ではなくなりました', - 'changelog.1.0.23.li.4': '一覧の空白部分を右クリックでファイルメニューを表示。6 列が実際にはグリッドに揃っていなかった問題を修正', - - 'changelog.1.0.22.li.1': 'IME の変換候補ウィンドウがカーソルに追従しない問題を修正(v1.0.21 のリグレッション)', - 'changelog.1.0.22.li.2': 'MSI インストーラーの提供を終了し、NSIS exe に統一', - 'changelog.1.0.22.li.3': 'ビルド手順に依存関係の整合性チェックを追加し、古い依存関係が混入しないようにしました', - - 'changelog.1.0.21.li.1': '背景画像の扱いを改善。どのテーマでも不透明度を下げると画像が暗くなり、文字は WCAG AA まで自動的に明るく調整。減光スライダーも追加', - 'changelog.1.0.21.li.2': 'パフォーマンス:設定のホットアップデートで全ペインが再レイアウトされなくなり、SFTP の大規模ディレクトリは仮想スクロール、ペインごとにエラーバウンダリを追加', - 'changelog.1.0.21.li.3': 'インストーラーを約 90MB 削減。SFTP / ZMODEM のローカルパス検証と複数のセキュリティ強化', - 'cl.morePre': '完全な変更履歴は', 'cl.morePost': ' をご覧ください。', diff --git a/website/index.html b/website/index.html index bb467f0..9a4d319 100644 --- a/website/index.html +++ b/website/index.html @@ -118,14 +118,14 @@ 内置 SFTP 文件管理与广播输入。安装即用,支持自动更新。

-

当前版本 v1.0.25 · NSIS exe 安装包,支持应用内自动更新。

+

当前版本 v1.0.28 · NSIS exe 安装包,支持应用内自动更新。

OpenTerminal 主界面:三路分屏并行运行 Kimi Code、Claude Code 与 Codex @@ -133,7 +133,7 @@
- 当前版本 v1.0.25 + 当前版本 v1.0.28 Windows 10 / 11 · x64 MIT 开源协议 exe 支持应用内自动更新 @@ -341,11 +341,11 @@ - OpenTerminal-1.0.25-setup.exe + OpenTerminal-1.0.28-setup.exe NSIS 安装包,支持应用内自动更新(推荐) 109 MB - 下载 + 下载 @@ -375,6 +375,28 @@

Changelog

近期更新

+
+

1.0.282026-10-16

+
    +
  • 修复应用内更新会下载两次的问题:禁用注定失败的差量下载,每次更新恰好一次整包下载,进度条只走一遍
  • +
+
+
+

1.0.272026-10-15

+
    +
  • 新增关闭标签二次确认:终端 / SSH 两个独立开关(默认关闭),设置 → 渲染与行为
  • +
  • 覆盖标签 × 按钮、右键「关闭 / 关闭其他 / 关闭右侧」与键盘 Delete / Backspace 误关——点击标签后按 Backspace 不再直接关闭
  • +
  • 批量关闭整批只确认一次;开关保存后立即生效,无需重启
  • +
+
+
+

1.0.262026-10-11

+
    +
  • 修复拖动缩放窗口或终端高频输出时界面冻结数秒(网格重排节流 + 输出批量写入)
  • +
  • 新增落盘诊断日志:默认开启,按天一个文件保留 7 天,设置 → 系统可关闭
  • +
  • 反馈卡顿问题时附上当天的日志文件即可精确定位
  • +
+

1.0.252026-10-10

    @@ -393,31 +415,6 @@
  • SFTP 删除不再误报失败;ZMODEM 接收大文件内存可控;新增 3 组渲染层测试
-
-

1.0.232026-10-09

-
    -
  • SFTP 文件面板改为真正的表格:六列对齐、表头吸顶、点击列头排序
  • -
  • 字体可缩放 0.8–1.8 倍(A- / A+ 或 Ctrl+滚轮),行高联动并记忆设置
  • -
  • 用户 / 组列显示真实名称,不再只有数字 UID / GID
  • -
  • 列表空白处右键即可唤出文件菜单;修复六列此前未真正按网格对齐的问题
  • -
-
-
-

1.0.222026-10-08

-
    -
  • 修复中文输入法候选窗不跟随光标(v1.0.21 回归)
  • -
  • 不再提供 MSI 安装包,统一使用 NSIS exe
  • -
  • 打包流程新增依赖一致性校验,防止旧依赖混入安装包
  • -
-
-
-

1.0.212026-10-07

-
    -
  • 背景图体验:任意主题下调透明度均为压暗、文字自动提亮到 WCAG AA、新增背景压暗滑杆
  • -
  • 性能:设置热更不再触发全窗格重排,SFTP 大目录虚拟滚动,面板独立错误边界
  • -
  • 安装包精简约 90MB;SFTP/ZMODEM 本地路径准入与多处安全加固
  • -
-

完整日志见仓库