Compare commits

..
1 Commits
Author SHA1 Message Date
Bill cff5454346 feat(workspace): 标签关闭二次确认(终端/SSH 双开关 + 键盘 Backspace/Delete 误关拦截),v1.0.27
CI / typecheck + test + build (windows) (push) Waiting to run
2026-10-15 10:35:20 +08:00
19 changed files with 283 additions and 50 deletions

No files matched your search

+1
View File
@@ -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`**,会话已死自动关闭/模板应用等程序性路径不走
## 关键词高亮
+14
View File
@@ -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.
+14
View File
@@ -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 日で自動削除)を追加しました。フリーズ報告に証拠が添えられるようになります。
+14
View File
@@ -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 天自动清理),以后反馈卡顿可以附上日志,定位不再靠猜。
+14
View File
@@ -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 天自動清理),日後回報卡頓可附上月誌,定位不再靠猜。
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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",
@@ -275,6 +275,26 @@ export function RenderSettingsTab(): React.JSX.Element {
/>
}
/>
<SettingRow
label={t('settings.render.confirmTabClose')}
desc={t('settings.render.confirmTabCloseDesc')}
control={
<Switch
checked={settings.terminal.confirmTabClose}
onChange={(checked) => void updateTerminal({ confirmTabClose: checked })}
/>
}
/>
<SettingRow
label={t('settings.render.confirmSshTabClose')}
desc={t('settings.render.confirmSshTabCloseDesc')}
control={
<Switch
checked={settings.terminal.confirmSshTabClose}
onChange={(checked) => void updateTerminal({ confirmSshTabClose: checked })}
/>
}
/>
<SettingRow
label={t('settings.render.suggest')}
desc={t('settings.render.suggestDesc')}
+150 -47
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { App, Button, Dropdown, Input, Modal, Popconfirm, Space } from 'antd'
import type { MenuProps } from 'antd'
import { CloudServerOutlined } from '@ant-design/icons'
@@ -22,13 +22,18 @@ export type TerminalTabProps = IDockviewPanelHeaderProps<{
* 关闭 / 关闭其他 / 关闭右侧
*
* SSH sessions get a 🖥 server icon and a green left-border accent so they're
* visually distinct from local terminals. All menu actions close panels
* directly through `api.close()`. The workspace's `onDidRemovePanel` handles
* the pty kill, so nothing leaks.
* visually distinct from local terminals. Every user-initiated close button,
* menu action and the tablist's Delete/Backspace accelerator goes through
* `requestClose`, which may ask for confirmation first (see below). Whatever
* the path, the workspace's `onDidRemovePanel` handles the pty kill, so
* nothing leaks.
*/
export function TerminalTab({ api, params }: TerminalTabProps): React.JSX.Element {
const panel = api.group // bound panel via the panel api
type TabPanel = (typeof panel.panels)[number]
const [title, setTitle] = useState<string | undefined>(api.title)
const [pending, setPending] = useState<TabPanel[] | null>(null)
const rootRef = useRef<HTMLDivElement>(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 (
<Dropdown menu={menu} trigger={['contextMenu']}>
<div
className={`workspace-terminal-tab${isSsh ? ' workspace-terminal-tab-ssh' : ''}`}
title={tooltip}
>
{isSsh && <CloudServerOutlined className="workspace-terminal-tab-icon" />}
<span className="workspace-terminal-tab-title">{title}</span>
{isSsh && hostLabel && <SshHostBadge label={hostLabel} language={language} />}
{isBroadcastTarget && (
<span
className="workspace-terminal-tab-bcast"
title={tFor(language, 'workspace.tab.broadcastTarget')}
aria-label={tFor(language, 'workspace.tab.broadcastTarget')}
>
{tFor(language, 'workspace.tab.broadcastBadge')}
</span>
)}
<button
type="button"
className="workspace-terminal-tab-close"
aria-label={tFor(language, 'common.close')}
onPointerDown={(event) => event.preventDefault()}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
api.close()
}}
<>
<Dropdown menu={menu} trigger={['contextMenu']}>
<div
ref={rootRef}
className={`workspace-terminal-tab${isSsh ? ' workspace-terminal-tab-ssh' : ''}`}
title={tooltip}
>
<CloseGlyph />
</button>
</div>
</Dropdown>
{isSsh && <CloudServerOutlined className="workspace-terminal-tab-icon" />}
<span className="workspace-terminal-tab-title">{title}</span>
{isSsh && hostLabel && <SshHostBadge label={hostLabel} language={language} />}
{isBroadcastTarget && (
<span
className="workspace-terminal-tab-bcast"
title={tFor(language, 'workspace.tab.broadcastTarget')}
aria-label={tFor(language, 'workspace.tab.broadcastTarget')}
>
{tFor(language, 'workspace.tab.broadcastBadge')}
</span>
)}
<button
type="button"
className="workspace-terminal-tab-close"
aria-label={tFor(language, 'common.close')}
onPointerDown={(event) => event.preventDefault()}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
const self = selfPanel()
if (self) requestClose([self])
}}
>
<CloseGlyph />
</button>
</div>
</Dropdown>
<Modal
open={pending !== null}
title={tFor(language, 'workspace.tab.closeConfirmTitle')}
okText={tFor(language, 'workspace.tab.closeConfirmOk')}
okButtonProps={{ danger: true }}
cancelText={tFor(language, 'common.cancel')}
maskClosable={false}
onOk={closePending}
onCancel={() => setPending(null)}
>
{pending && pending.length === 1
? tFor(language, 'workspace.tab.closeConfirmOne', { title: pending[0].title ?? '' })
: tFor(language, 'workspace.tab.closeConfirmMany', { count: pending?.length ?? 0 })}
</Modal>
</>
)
}
+6
View File
@@ -74,6 +74,12 @@ const settings: Record<string, string> = {
'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',
+4
View File
@@ -4,6 +4,10 @@ const workspace: Record<string, string> = {
'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',
+6
View File
@@ -72,6 +72,12 @@ const settings: Record<string, string> = {
'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 は常にそのまま実行します',
+4
View File
@@ -4,6 +4,10 @@ const workspace: Record<string, string> = {
'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': 'この分割に新しいターミナル',
+4
View File
@@ -71,6 +71,10 @@ const settings: Record<string, string> = {
'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': '记录命令历史',
+4
View File
@@ -4,6 +4,10 @@ const workspace: Record<string, string> = {
'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': '在此分屏中新建终端',
+4
View File
@@ -71,6 +71,10 @@ const settings: Record<string, string> = {
'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': '記錄指令歷史',
+4
View File
@@ -4,6 +4,10 @@ const workspace: Record<string, string> = {
'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': '在此分割中新增終端機',
+6
View File
@@ -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,
+11
View File
@@ -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 })