Refine terminal close confirmation (#5541)
* Refine terminal close confirmation * Translate terminal close dialog strings
This commit is contained in:
parent
8da3f864b3
commit
62fbb51eb2
|
|
@ -0,0 +1,114 @@
|
|||
# Terminal Close Confirmation
|
||||
|
||||
## Problem
|
||||
|
||||
- `CloseTerminalDialog` repeats the action users already requested with "Close Terminal?" and a generic "process will be killed" warning (`src/renderer/src/components/terminal-pane/CloseTerminalDialog.tsx:30`).
|
||||
- `Cmd/Ctrl+W` closes only the focused split pane, or the tab when it is the last pane; the guard exists so tab-level close does not kill every pane by accident (`src/renderer/src/components/terminal-pane/keyboard-handlers.ts:364`).
|
||||
- The running-process guard probes the PTY over the active runtime/SSH path and shows the dialog only when child processes exist (`src/renderer/src/components/terminal-pane/TerminalPane.tsx:781`).
|
||||
- There is no way for power users to say "I understand, close it next time" even though similar destructive workflows persist skip-confirm settings (`src/shared/types.ts:2522`, `src/renderer/src/components/settings/GeneralWorkspaceSettingsSection.tsx:58`).
|
||||
|
||||
## Goal
|
||||
|
||||
Make the terminal close confirmation communicate the consequence, respect focused-pane scope, and allow users to disable future running-process close confirmations from the dialog or Settings.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not change window-close behavior; whole-window shutdown intentionally bypasses the child-process dialog.
|
||||
- Do not change idle-shell behavior; idle shells still close immediately.
|
||||
- Do not add bulk-close UI for this change.
|
||||
- Do not introduce provider-specific agent kill logic; closing still uses the existing terminal close path.
|
||||
- Do not add telemetry.
|
||||
|
||||
## Design
|
||||
|
||||
1. Add a persisted `skipCloseTerminalWithRunningProcessConfirm` boolean to `GlobalSettings`, defaulting to `false`.
|
||||
2. Keep the existing child-process probe. If the new setting is true, close immediately after the probe reports child processes instead of showing the dialog.
|
||||
3. Track the pending close as `{ paneId, copyKind }`, where `copyKind` is `agent` only when `agentStatusByPaneKey[makePaneKey(tabId, leafId)]` has a live non-unknown `agentType`; otherwise it is `command`.
|
||||
4. Update dialog copy:
|
||||
- command: title `Stop running command?`, body `Closing this terminal will stop the command running inside it.`, destructive button `Stop and Close`.
|
||||
- agent: title `Stop this agent?`, body `Closing this terminal will stop the agent's current work.`, destructive button `Stop Agent`.
|
||||
5. Add a checkbox: `Don't ask again for running terminals`. When checked and confirmed, persist `skipCloseTerminalWithRunningProcessConfirm: true` before closing the pane.
|
||||
6. Add a Terminal Interaction settings switch: `Ask Before Closing Running Terminals`, checked when the skip flag is false.
|
||||
7. Add the new setting to terminal settings search so "confirm", "close", "running", "agent", and "command" find it.
|
||||
|
||||
## Data flow
|
||||
|
||||
- `Cmd/Ctrl+W` or pane close action
|
||||
- `TerminalPane.handleRequestClosePane(paneId)`
|
||||
- Get `ptyId`; no PTY closes immediately
|
||||
- `inspectRuntimeTerminalProcess(settings, ptyId)`
|
||||
- No child processes closes immediately
|
||||
- Child processes + skip setting closes immediately
|
||||
- Child processes + confirmation enabled opens `CloseTerminalDialog(copyKind)`
|
||||
- Confirm optionally persists skip flag, then calls `executeClosePane(paneId)`
|
||||
|
||||
## Edge cases
|
||||
|
||||
- If process inspection rejects, preserve the existing fallback: close the pane instead of trapping the shortcut.
|
||||
- If the pane is removed before the dialog confirms, `executeClosePane` already no-ops when the manager cannot close it.
|
||||
- For split panes, only the active pane gets the prompt and closes.
|
||||
- For last-pane tabs, confirming still delegates to `onCloseTab`.
|
||||
- Agent copy appears only from live pane status. Freshly launched agents that have not emitted hooks yet may use command copy; that is acceptable because the consequence is still accurate.
|
||||
- SSH/runtime-host terminals still use the existing runtime process inspection; the setting lives in global renderer settings and is passed through the same update path.
|
||||
- The skip flag affects only terminal running-process close confirmations, not workspace deletion, automation deletion, window close, or future bulk-close prompts.
|
||||
|
||||
## Test plan
|
||||
|
||||
- Unit/component:
|
||||
- `CloseTerminalDialog` renders command copy, agent copy, checkbox, and reports the checked state on confirm.
|
||||
- Settings search includes the running-terminal confirmation entry.
|
||||
- Default settings include `skipCloseTerminalWithRunningProcessConfirm: false`.
|
||||
- Integration/lightweight:
|
||||
- Verify `TerminalPane` opens agent copy when live pane status has `agentType` and command copy otherwise.
|
||||
- Verify checked confirm persists the skip flag before closing.
|
||||
- Electron:
|
||||
- Running command + default setting shows command confirmation.
|
||||
- Running command + checkbox checked confirms and future close skips the dialog.
|
||||
- Agent pane with live status shows agent confirmation copy.
|
||||
- Idle shell closes without confirmation.
|
||||
|
||||
## UI quality bar
|
||||
|
||||
- Dialog uses existing shadcn `Dialog` and `Button` primitives, token colors, and current compact modal sizing.
|
||||
- Copy names the destructive consequence first and avoids implying every terminal/tab/window will close.
|
||||
- Checkbox is visually subordinate to the message and aligned with existing dense dialog spacing.
|
||||
- Settings row matches neighboring Terminal Interaction switch rows and is searchable.
|
||||
- No layout shift, clipping, or button text overflow at the current modal width.
|
||||
|
||||
## Review screenshots
|
||||
|
||||
1. Running-command confirmation dialog.
|
||||
2. Running-agent confirmation dialog.
|
||||
3. Terminal Interaction settings row for `Ask Before Closing Running Terminals`.
|
||||
|
||||
## Rollout
|
||||
|
||||
1. Add shared setting type/default.
|
||||
2. Add dialog copy modes and checkbox.
|
||||
3. Wire `TerminalPane` to derive copy kind, honor skip flag, and persist "don't ask again" on confirm.
|
||||
4. Add Terminal settings row and search entry.
|
||||
5. Add targeted tests.
|
||||
6. Force-add this design doc when staging because root `.gitignore` treats new `docs/**` files as local-only by default.
|
||||
|
||||
## Lightweight Eng Review
|
||||
|
||||
- Scope: kept focused on the existing running-process confirmation; no new close routing, bulk-close behavior, or native window-close changes.
|
||||
- Architecture/data flow: renderer-only UI setting rides the existing settings persistence path; process detection remains owned by `inspectRuntimeTerminalProcess` so SSH/runtime compatibility does not fork.
|
||||
- Failure modes covered:
|
||||
- process-inspection rejection preserves current close fallback
|
||||
- stale pane between prompt and confirm no-ops through existing manager guard
|
||||
- split-pane close remains pane-scoped
|
||||
- missing/stale agent status falls back to generic command copy
|
||||
- skip flag is scoped to terminal running-process confirmations only
|
||||
- Test coverage required:
|
||||
- component test for `CloseTerminalDialog` copy/checkbox
|
||||
- shared default/type coverage via existing typecheck plus default-setting assertion
|
||||
- settings search test for new discoverable entry
|
||||
- focused TerminalPane behavior test if practical; otherwise Electron validation covers prompt routing
|
||||
- Performance/blast radius: no polling, IPC, startup, or renderer-jank impact; only an extra settings boolean read during an already user-triggered close path.
|
||||
- UI quality bar: Electron validation should judge the modal and Terminal settings row against `docs/STYLEGUIDE.md`, existing `Dialog`/`Button`/`SettingsSwitchRow`, and adjacent Terminal Interaction density.
|
||||
- Required review screenshots:
|
||||
1. Running-command confirmation dialog
|
||||
2. Running-agent confirmation dialog
|
||||
3. Terminal Interaction settings row
|
||||
- Residual risks: agent-specific copy depends on live hook status, so newly launched or manually run agents can still receive generic command copy; the design doc is ignored by default and must be force-staged for the PR.
|
||||
|
|
@ -111,6 +111,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
|
|||
defaultTuiAgent: null,
|
||||
disabledTuiAgents: [],
|
||||
skipDeleteWorktreeConfirm: false,
|
||||
skipCloseTerminalWithRunningProcessConfirm: false,
|
||||
skipDeleteAutomationConfirm: false,
|
||||
defaultTaskViewPreset: 'all',
|
||||
defaultTaskSource: 'github',
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
|
|||
defaultTuiAgent: null,
|
||||
disabledTuiAgents: [],
|
||||
skipDeleteWorktreeConfirm: false,
|
||||
skipCloseTerminalWithRunningProcessConfirm: false,
|
||||
skipDeleteAutomationConfirm: false,
|
||||
defaultTaskViewPreset: 'all',
|
||||
defaultTaskSource: 'github',
|
||||
|
|
|
|||
|
|
@ -424,6 +424,47 @@ export function TerminalPane({
|
|||
</SearchableSetting>
|
||||
)}
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalPane.ask_before_closing_running_terminals_title',
|
||||
'Ask Before Closing Running Terminals'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalPane.ask_before_closing_running_terminals_description',
|
||||
'Show a confirmation before closing a terminal that has a running command or agent.'
|
||||
)}
|
||||
keywords={[
|
||||
'confirm',
|
||||
'confirmation',
|
||||
'close',
|
||||
'terminal',
|
||||
'running',
|
||||
'command',
|
||||
'agent',
|
||||
'process',
|
||||
'prompt',
|
||||
'stop'
|
||||
]}
|
||||
>
|
||||
<SettingsSwitchRow
|
||||
label={translate(
|
||||
'auto.components.settings.TerminalPane.ask_before_closing_running_terminals_title',
|
||||
'Ask Before Closing Running Terminals'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.TerminalPane.ask_before_closing_running_terminals_description',
|
||||
'Show a confirmation before closing a terminal that has a running command or agent.'
|
||||
)}
|
||||
checked={!settings.skipCloseTerminalWithRunningProcessConfirm}
|
||||
onChange={() =>
|
||||
updateSettings({
|
||||
skipCloseTerminalWithRunningProcessConfirm:
|
||||
!settings.skipCloseTerminalWithRunningProcessConfirm
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.TerminalPane.8eefeaa3da',
|
||||
|
|
|
|||
|
|
@ -34,6 +34,28 @@ export const getTerminalPaneAppearanceSearchEntries = createLocalizedCatalog(()
|
|||
])
|
||||
|
||||
export const getTerminalPaneInteractionSearchEntries = createLocalizedCatalog(() => [
|
||||
{
|
||||
title: translate(
|
||||
'auto.components.settings.terminal.search.ask_before_closing_running_terminals_title',
|
||||
'Ask Before Closing Running Terminals'
|
||||
),
|
||||
description: translate(
|
||||
'auto.components.settings.terminal.search.ask_before_closing_running_terminals_description',
|
||||
'Show a confirmation before closing a terminal that has a running command or agent.'
|
||||
),
|
||||
keywords: [
|
||||
...translateSearchKeyword('auto.components.settings.terminal.search.10f9fb6fea', 'settings'),
|
||||
...translateSearchKeyword('auto.components.settings.terminal.search.a0c44061ee', 'confirm'),
|
||||
...translateSearchKeyword('auto.components.settings.terminal.search.close_terminal', 'close'),
|
||||
...translateSearchKeyword('auto.components.settings.terminal.search.39ea7c0d28', 'terminal'),
|
||||
...translateSearchKeyword('auto.components.settings.terminal.search.running', 'running'),
|
||||
...translateSearchKeyword('auto.components.settings.terminal.search.command', 'command'),
|
||||
...translateSearchKeyword('auto.components.settings.terminal.search.agent', 'agent'),
|
||||
...translateSearchKeyword('auto.components.settings.terminal.search.process', 'process'),
|
||||
...translateSearchKeyword('auto.components.settings.terminal.search.prompt', 'prompt'),
|
||||
...translateSearchKeyword('auto.components.settings.terminal.search.stop', 'stop')
|
||||
]
|
||||
},
|
||||
{
|
||||
title: translate('auto.components.settings.terminal.search.c6178a2b4d', 'Focus Follows Mouse'),
|
||||
description: translate(
|
||||
|
|
|
|||
|
|
@ -69,6 +69,24 @@ describe('getTerminalPaneSearchEntries', () => {
|
|||
).toBe(true)
|
||||
})
|
||||
|
||||
it('includes the running-terminal close confirmation setting on all platforms', () => {
|
||||
const entriesWindows = getTerminalPaneSearchEntries({ isWindows: true, isMac: false })
|
||||
const entriesMac = getTerminalPaneSearchEntries({ isWindows: false, isMac: true })
|
||||
const entriesLinux = getTerminalPaneSearchEntries({ isWindows: false, isMac: false })
|
||||
const hasEntry = (entries: typeof entriesWindows): boolean =>
|
||||
entries.some(
|
||||
(entry) =>
|
||||
entry.title === 'Ask Before Closing Running Terminals' &&
|
||||
matchesSettingsSearch('confirm', [entry]) &&
|
||||
matchesSettingsSearch('agent', [entry]) &&
|
||||
matchesSettingsSearch('close', [entry])
|
||||
)
|
||||
|
||||
expect(hasEntry(entriesWindows)).toBe(true)
|
||||
expect(hasEntry(entriesMac)).toBe(true)
|
||||
expect(hasEntry(entriesLinux)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps terminal appearance settings in the Appearance search index', () => {
|
||||
const entriesWindows = getTerminalPaneSearchEntries({ isWindows: true, isMac: false })
|
||||
const entriesMac = getTerminalPaneSearchEntries({ isWindows: false, isMac: true })
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import CloseTerminalDialog from './CloseTerminalDialog'
|
||||
|
||||
const mountedRoots: Root[] = []
|
||||
|
||||
async function renderDialog(props: {
|
||||
copyKind?: 'command' | 'agent'
|
||||
onConfirm: (dontAskAgain: boolean) => void
|
||||
onCancel?: () => void
|
||||
}): Promise<void> {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const root = createRoot(container)
|
||||
mountedRoots.push(root)
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<CloseTerminalDialog
|
||||
open
|
||||
copyKind={props.copyKind}
|
||||
onCancel={props.onCancel ?? vi.fn()}
|
||||
onConfirm={props.onConfirm}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function clickButton(label: string): void {
|
||||
const button = [...document.body.querySelectorAll<HTMLButtonElement>('button')].find(
|
||||
(candidate) => candidate.textContent === label
|
||||
)
|
||||
if (!button) {
|
||||
throw new Error(`Button not found: ${label}`)
|
||||
}
|
||||
button.click()
|
||||
}
|
||||
|
||||
describe('CloseTerminalDialog', () => {
|
||||
afterEach(async () => {
|
||||
await act(async () => {
|
||||
for (const root of mountedRoots.splice(0)) {
|
||||
root.unmount()
|
||||
}
|
||||
})
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
it('renders running command copy and confirms without skipping by default', async () => {
|
||||
const onConfirm = vi.fn()
|
||||
|
||||
await renderDialog({ copyKind: 'command', onConfirm })
|
||||
|
||||
expect(document.body.textContent).toContain('Stop running command?')
|
||||
expect(document.body.textContent).toContain(
|
||||
'Closing this terminal will stop the command running inside it.'
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
clickButton('Stop and Close')
|
||||
})
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('renders agent copy and passes the skip preference when checked', async () => {
|
||||
const onConfirm = vi.fn()
|
||||
|
||||
await renderDialog({ copyKind: 'agent', onConfirm })
|
||||
|
||||
expect(document.body.textContent).toContain('Stop this agent?')
|
||||
expect(document.body.textContent).toContain(
|
||||
"Closing this terminal will stop the agent's current work."
|
||||
)
|
||||
|
||||
const checkbox = document.body.querySelector<HTMLButtonElement>('[role="checkbox"]')
|
||||
expect(checkbox).not.toBeNull()
|
||||
|
||||
await act(async () => {
|
||||
checkbox?.click()
|
||||
})
|
||||
await act(async () => {
|
||||
clickButton('Stop Agent')
|
||||
})
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { useEffect, useId, useState } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -7,17 +8,34 @@ import {
|
|||
DialogTitle
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
export type CloseTerminalDialogCopyKind = 'command' | 'agent'
|
||||
|
||||
export default function CloseTerminalDialog({
|
||||
open,
|
||||
copyKind = 'command',
|
||||
onCancel,
|
||||
onConfirm
|
||||
}: {
|
||||
open: boolean
|
||||
copyKind?: CloseTerminalDialogCopyKind
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
onConfirm: (dontAskAgain: boolean) => void
|
||||
}): React.JSX.Element {
|
||||
const checkboxId = useId()
|
||||
const [dontAskAgain, setDontAskAgain] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setDontAskAgain(false)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const isAgent = copyKind === 'agent'
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
|
|
@ -30,24 +48,61 @@ export default function CloseTerminalDialog({
|
|||
<DialogContent className="max-w-sm" showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-sm">
|
||||
{translate(
|
||||
'auto.components.terminal.pane.CloseTerminalDialog.78b79d854d',
|
||||
'Close Terminal?'
|
||||
)}
|
||||
{isAgent
|
||||
? translate(
|
||||
'auto.components.terminal.pane.CloseTerminalDialog.stop_agent_title',
|
||||
'Stop this agent?'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.terminal.pane.CloseTerminalDialog.stop_command_title',
|
||||
'Stop running command?'
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs">
|
||||
{translate(
|
||||
'auto.components.terminal.pane.CloseTerminalDialog.6b9a6975f8',
|
||||
'The terminal still has a running process. If you close the terminal, the process will be killed.'
|
||||
)}
|
||||
{isAgent
|
||||
? translate(
|
||||
'auto.components.terminal.pane.CloseTerminalDialog.stop_agent_description',
|
||||
"Closing this terminal will stop the agent's current work."
|
||||
)
|
||||
: translate(
|
||||
'auto.components.terminal.pane.CloseTerminalDialog.stop_command_description',
|
||||
'Closing this terminal will stop the command running inside it.'
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={checkboxId}
|
||||
checked={dontAskAgain}
|
||||
onCheckedChange={(checked) => setDontAskAgain(checked === true)}
|
||||
/>
|
||||
<Label htmlFor={checkboxId} className="text-xs font-normal text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.terminal.pane.CloseTerminalDialog.dont_ask_again',
|
||||
"Don't ask again for running terminals"
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
<DialogFooter className="gap-2">
|
||||
<Button type="button" variant="outline" size="sm" onClick={onCancel}>
|
||||
{translate('auto.components.terminal.pane.CloseTerminalDialog.1d1a7a9c1f', 'Cancel')}
|
||||
</Button>
|
||||
<Button type="button" variant="destructive" size="sm" autoFocus onClick={onConfirm}>
|
||||
{translate('auto.components.terminal.pane.CloseTerminalDialog.ebd2fa844d', 'Close')}
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
autoFocus
|
||||
onClick={() => onConfirm(dontAskAgain)}
|
||||
>
|
||||
{isAgent
|
||||
? translate(
|
||||
'auto.components.terminal.pane.CloseTerminalDialog.stop_agent_confirm',
|
||||
'Stop Agent'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.terminal.pane.CloseTerminalDialog.stop_command_confirm',
|
||||
'Stop and Close'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import { useTerminalKeyboardShortcuts, type SearchState } from './keyboard-handl
|
|||
import type { MacOptionAsAlt } from './terminal-shortcut-policy'
|
||||
import { useEffectiveMacOptionAsAlt } from '@/lib/keyboard-layout/use-effective-mac-option-as-alt'
|
||||
import { useTerminalFontZoom } from './useTerminalFontZoom'
|
||||
import CloseTerminalDialog from './CloseTerminalDialog'
|
||||
import CloseTerminalDialog, { type CloseTerminalDialogCopyKind } from './CloseTerminalDialog'
|
||||
import { MobileDriverOverlay } from './MobileDriverOverlay'
|
||||
import { TerminalErrorToast } from './TerminalErrorToast'
|
||||
import { TerminalSessionStateSaveFailureDialog } from './TerminalSessionStateSaveFailureDialog'
|
||||
|
|
@ -216,7 +216,10 @@ export default function TerminalPane({
|
|||
const searchOpenRef = useRef(false)
|
||||
searchOpenRef.current = searchOpen
|
||||
const searchStateRef = useRef<SearchState>({ query: '', caseSensitive: false, regex: false })
|
||||
const [closeConfirmPaneId, setCloseConfirmPaneId] = useState<number | null>(null)
|
||||
const [pendingCloseConfirmation, setPendingCloseConfirmation] = useState<{
|
||||
paneId: number
|
||||
copyKind: CloseTerminalDialogCopyKind
|
||||
} | null>(null)
|
||||
const [quickCommandEditorOpen, setQuickCommandEditorOpen] = useState(false)
|
||||
// Why: the terminal menu can be the first quick-command entry point, so each
|
||||
// Add action starts with a fresh draft instead of reusing cancelled text.
|
||||
|
|
@ -782,6 +785,19 @@ export default function TerminalPane({
|
|||
// a running child process (e.g. npm run dev), so the user doesn't
|
||||
// accidentally kill it. An idle shell prompt closes immediately. Ctrl+D
|
||||
// (explicit EOF) bypasses this by design.
|
||||
const getCloseDialogCopyKind = useCallback(
|
||||
(paneId: number): CloseTerminalDialogCopyKind => {
|
||||
const leafId = managerRef.current?.getLeafId(paneId)
|
||||
if (!leafId) {
|
||||
return 'command'
|
||||
}
|
||||
const agentType =
|
||||
useAppStore.getState().agentStatusByPaneKey[makePaneKey(tabId, leafId)]?.agentType
|
||||
return agentType && agentType !== 'unknown' ? 'agent' : 'command'
|
||||
},
|
||||
[tabId]
|
||||
)
|
||||
|
||||
const handleRequestClosePane = useCallback(
|
||||
(paneId: number) => {
|
||||
const transport = paneTransportsRef.current.get(paneId)
|
||||
|
|
@ -793,10 +809,10 @@ export default function TerminalPane({
|
|||
const settings = useAppStore.getState().settings
|
||||
void inspectRuntimeTerminalProcess(settings, ptyId)
|
||||
.then((process) => {
|
||||
if (process.hasChildProcesses) {
|
||||
setCloseConfirmPaneId(paneId)
|
||||
} else {
|
||||
if (!process.hasChildProcesses || settings?.skipCloseTerminalWithRunningProcessConfirm) {
|
||||
executeClosePane(paneId)
|
||||
} else {
|
||||
setPendingCloseConfirmation({ paneId, copyKind: getCloseDialogCopyKind(paneId) })
|
||||
}
|
||||
})
|
||||
// Why: if the child-process probe rejects (IPC wedged, handler
|
||||
|
|
@ -805,7 +821,7 @@ export default function TerminalPane({
|
|||
// had a child process. Matches the semantics of the !ptyId branch above.
|
||||
.catch(() => executeClosePane(paneId))
|
||||
},
|
||||
[executeClosePane]
|
||||
[executeClosePane, getCloseDialogCopyKind]
|
||||
)
|
||||
|
||||
const handleSearchSelectedText = useCallback((selectedText: string): void => {
|
||||
|
|
@ -813,13 +829,24 @@ export default function TerminalPane({
|
|||
state.showRightSidebarSearch({ query: selectedText })
|
||||
}, [])
|
||||
|
||||
const handleConfirmClose = useCallback(() => {
|
||||
if (closeConfirmPaneId === null) {
|
||||
return
|
||||
}
|
||||
executeClosePane(closeConfirmPaneId)
|
||||
setCloseConfirmPaneId(null)
|
||||
}, [closeConfirmPaneId, executeClosePane])
|
||||
const handleConfirmClose = useCallback(
|
||||
(dontAskAgain: boolean) => {
|
||||
if (pendingCloseConfirmation === null) {
|
||||
return
|
||||
}
|
||||
const paneId = pendingCloseConfirmation.paneId
|
||||
setPendingCloseConfirmation(null)
|
||||
if (dontAskAgain) {
|
||||
void updateSettings({ skipCloseTerminalWithRunningProcessConfirm: true })
|
||||
}
|
||||
executeClosePane(paneId)
|
||||
},
|
||||
[executeClosePane, pendingCloseConfirmation, updateSettings]
|
||||
)
|
||||
|
||||
const handleCancelClose = useCallback(() => {
|
||||
setPendingCloseConfirmation(null)
|
||||
}, [])
|
||||
|
||||
useTerminalPaneLifecycle({
|
||||
tabId,
|
||||
|
|
@ -2165,8 +2192,9 @@ export default function TerminalPane({
|
|||
)
|
||||
})}
|
||||
<CloseTerminalDialog
|
||||
open={closeConfirmPaneId !== null}
|
||||
onCancel={() => setCloseConfirmPaneId(null)}
|
||||
open={pendingCloseConfirmation !== null}
|
||||
copyKind={pendingCloseConfirmation?.copyKind}
|
||||
onCancel={handleCancelClose}
|
||||
onConfirm={handleConfirmClose}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -2224,7 +2224,14 @@
|
|||
"ebd2fa844d": "Close",
|
||||
"1d1a7a9c1f": "Cancel",
|
||||
"6b9a6975f8": "The terminal still has a running process. If you close the terminal, the process will be killed.",
|
||||
"78b79d854d": "Close Terminal?"
|
||||
"78b79d854d": "Close Terminal?",
|
||||
"stop_agent_title": "Stop this agent?",
|
||||
"stop_command_title": "Stop running command?",
|
||||
"stop_agent_description": "Closing this terminal will stop the agent's current work.",
|
||||
"stop_command_description": "Closing this terminal will stop the command running inside it.",
|
||||
"dont_ask_again": "Don't ask again for running terminals",
|
||||
"stop_agent_confirm": "Stop Agent",
|
||||
"stop_command_confirm": "Stop and Close"
|
||||
},
|
||||
"MobileDriverOverlay": {
|
||||
"c6460cf584": "Take back",
|
||||
|
|
@ -6067,7 +6074,9 @@
|
|||
"af0c3b6e39": "On Windows, right-click pastes the clipboard into the terminal. Use Ctrl+right-click to open the context menu.",
|
||||
"29154326bb": "on",
|
||||
"ab20575a8a": "off",
|
||||
"ab3a1f9068": "wsl.exe"
|
||||
"ab3a1f9068": "wsl.exe",
|
||||
"ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals",
|
||||
"ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent."
|
||||
},
|
||||
"TerminalSettingsPreview": {
|
||||
"a63953a48a": "Preview {{value0}} theme",
|
||||
|
|
@ -7675,7 +7684,9 @@
|
|||
"e989914ad6": "Font Family",
|
||||
"33031c1465": "text size",
|
||||
"0fe0073f0c": "Default terminal font size for new panes and live updates.",
|
||||
"5930244899": "Font Size"
|
||||
"5930244899": "Font Size",
|
||||
"ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals",
|
||||
"ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent."
|
||||
},
|
||||
"windows": {
|
||||
"search": {
|
||||
|
|
|
|||
|
|
@ -2224,7 +2224,14 @@
|
|||
"ebd2fa844d": "Cerca",
|
||||
"1d1a7a9c1f": "Cancelar",
|
||||
"6b9a6975f8": "La terminal todavía tiene un proceso en ejecución. Si cierra la terminal, el proceso finalizará.",
|
||||
"78b79d854d": "¿Cerrar terminal?"
|
||||
"78b79d854d": "¿Cerrar terminal?",
|
||||
"stop_agent_title": "¿Detener este agente?",
|
||||
"stop_command_title": "¿Detener comando en ejecución?",
|
||||
"stop_agent_description": "Al cerrar esta terminal, se detendrá el trabajo actual del agente.",
|
||||
"stop_command_description": "Al cerrar esta terminal, se detendrá el comando que se está ejecutando en ella.",
|
||||
"dont_ask_again": "No volver a preguntar para terminales en ejecución",
|
||||
"stop_agent_confirm": "Detener agente",
|
||||
"stop_command_confirm": "Detener y cerrar"
|
||||
},
|
||||
"MobileDriverOverlay": {
|
||||
"c6460cf584": "Devolver",
|
||||
|
|
@ -6030,7 +6037,9 @@
|
|||
"af0c3b6e39": "En Windows, haga clic derecho y pegue el portapapeles en la terminal. Utilice Ctrl+clic derecho para abrir el menú contextual.",
|
||||
"29154326bb": "en",
|
||||
"ab20575a8a": "apagado",
|
||||
"ab3a1f9068": "wsl.exe"
|
||||
"ab3a1f9068": "wsl.exe",
|
||||
"ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals",
|
||||
"ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent."
|
||||
},
|
||||
"TerminalSettingsPreview": {
|
||||
"a63953a48a": "Vista previa del tema {{value0}}",
|
||||
|
|
@ -7638,7 +7647,9 @@
|
|||
"description": "Import theme YAML files as Orca terminal themes.",
|
||||
"keyword_yaml": "yaml",
|
||||
"keyword_custom": "custom"
|
||||
}
|
||||
},
|
||||
"ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals",
|
||||
"ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent."
|
||||
},
|
||||
"windows": {
|
||||
"search": {
|
||||
|
|
|
|||
|
|
@ -2224,7 +2224,14 @@
|
|||
"ebd2fa844d": "閉じる",
|
||||
"1d1a7a9c1f": "キャンセル",
|
||||
"6b9a6975f8": "terminal にはまだ実行中のプロセスがあります。terminal を閉じるとプロセスが強制終了されます。",
|
||||
"78b79d854d": "Terminal を閉じますか?"
|
||||
"78b79d854d": "Terminal を閉じますか?",
|
||||
"stop_agent_title": "このエージェントを停止しますか?",
|
||||
"stop_command_title": "実行中のコマンドを停止しますか?",
|
||||
"stop_agent_description": "この terminal を閉じると、エージェントの現在の作業が停止します。",
|
||||
"stop_command_description": "この terminal を閉じると、その中で実行中のコマンドが停止します。",
|
||||
"dont_ask_again": "実行中の terminal では今後確認しない",
|
||||
"stop_agent_confirm": "エージェントを停止",
|
||||
"stop_command_confirm": "停止して閉じる"
|
||||
},
|
||||
"MobileDriverOverlay": {
|
||||
"c6460cf584": "操作を取り戻す",
|
||||
|
|
@ -6052,7 +6059,9 @@
|
|||
"af0c3b6e39": "Windows では、右クリックしてクリップボードを terminal に貼り付けます。 Ctrl キーを押しながら右クリックしてコンテキスト メニューを開きます。",
|
||||
"29154326bb": "の上",
|
||||
"ab20575a8a": "オフ",
|
||||
"ab3a1f9068": "wsl.exe"
|
||||
"ab3a1f9068": "wsl.exe",
|
||||
"ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals",
|
||||
"ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent."
|
||||
},
|
||||
"TerminalSettingsPreview": {
|
||||
"a63953a48a": "{{value0}} テーマのプレビュー",
|
||||
|
|
@ -7660,7 +7669,9 @@
|
|||
"e989914ad6": "フォントファミリー",
|
||||
"33031c1465": "文字サイズ",
|
||||
"0fe0073f0c": "新規ペインとライブ アップデートのデフォルトの terminal フォント サイズ。",
|
||||
"5930244899": "フォントサイズ"
|
||||
"5930244899": "フォントサイズ",
|
||||
"ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals",
|
||||
"ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent."
|
||||
},
|
||||
"windows": {
|
||||
"search": {
|
||||
|
|
|
|||
|
|
@ -2224,7 +2224,14 @@
|
|||
"ebd2fa844d": "닫기",
|
||||
"1d1a7a9c1f": "취소",
|
||||
"6b9a6975f8": "terminal 에는 여전히 실행 중인 프로세스가 있습니다. terminal을 닫으면 프로세스가 종료됩니다.",
|
||||
"78b79d854d": "Terminal을 닫으시겠습니까?"
|
||||
"78b79d854d": "Terminal을 닫으시겠습니까?",
|
||||
"stop_agent_title": "Stop this agent?",
|
||||
"stop_command_title": "Stop running command?",
|
||||
"stop_agent_description": "Closing this terminal will stop the agent's current work.",
|
||||
"stop_command_description": "Closing this terminal will stop the command running inside it.",
|
||||
"dont_ask_again": "Don't ask again for running terminals",
|
||||
"stop_agent_confirm": "Stop Agent",
|
||||
"stop_command_confirm": "Stop and Close"
|
||||
},
|
||||
"MobileDriverOverlay": {
|
||||
"c6460cf584": "회수",
|
||||
|
|
@ -6015,7 +6022,9 @@
|
|||
"af0c3b6e39": "Windows에서는 마우스 오른쪽 버튼을 클릭하여 클립보드를 terminal 에 붙여넣습니다. 컨텍스트 메뉴를 열려면 Ctrl+오른쪽 클릭을 사용하세요.",
|
||||
"29154326bb": "~에",
|
||||
"ab20575a8a": "끄다",
|
||||
"ab3a1f9068": "wsl.exe"
|
||||
"ab3a1f9068": "wsl.exe",
|
||||
"ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals",
|
||||
"ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent."
|
||||
},
|
||||
"TerminalSettingsPreview": {
|
||||
"a63953a48a": "{{value0}} 테마 미리보기",
|
||||
|
|
@ -7623,7 +7632,9 @@
|
|||
"description": "Import theme YAML files as Orca terminal themes.",
|
||||
"keyword_yaml": "yaml",
|
||||
"keyword_custom": "custom"
|
||||
}
|
||||
},
|
||||
"ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals",
|
||||
"ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent."
|
||||
},
|
||||
"windows": {
|
||||
"search": {
|
||||
|
|
|
|||
|
|
@ -2224,7 +2224,14 @@
|
|||
"ebd2fa844d": "关闭",
|
||||
"1d1a7a9c1f": "取消",
|
||||
"6b9a6975f8": "terminal 仍有正在运行的进程。如果关闭 terminal,该进程将被终止。",
|
||||
"78b79d854d": "关闭 Terminal?"
|
||||
"78b79d854d": "关闭 Terminal?",
|
||||
"stop_agent_title": "Stop this agent?",
|
||||
"stop_command_title": "Stop running command?",
|
||||
"stop_agent_description": "Closing this terminal will stop the agent's current work.",
|
||||
"stop_command_description": "Closing this terminal will stop the command running inside it.",
|
||||
"dont_ask_again": "Don't ask again for running terminals",
|
||||
"stop_agent_confirm": "Stop Agent",
|
||||
"stop_command_confirm": "Stop and Close"
|
||||
},
|
||||
"MobileDriverOverlay": {
|
||||
"c6460cf584": "收回",
|
||||
|
|
@ -6015,7 +6022,9 @@
|
|||
"af0c3b6e39": "在 Windows 上,右键单击将剪贴板粘贴到 terminal 中。使用 Ctrl+右键单击打开上下文菜单。",
|
||||
"29154326bb": "在",
|
||||
"ab20575a8a": "离开",
|
||||
"ab3a1f9068": "执行程序"
|
||||
"ab3a1f9068": "执行程序",
|
||||
"ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals",
|
||||
"ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent."
|
||||
},
|
||||
"TerminalSettingsPreview": {
|
||||
"a63953a48a": "预览 {{value0}} 主题",
|
||||
|
|
@ -7623,7 +7632,9 @@
|
|||
"description": "Import theme YAML files as Orca terminal themes.",
|
||||
"keyword_yaml": "yaml",
|
||||
"keyword_custom": "自定义"
|
||||
}
|
||||
},
|
||||
"ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals",
|
||||
"ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent."
|
||||
},
|
||||
"windows": {
|
||||
"search": {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ describe('getDefaultSettings', () => {
|
|||
expect(getDefaultSettings('/tmp').terminalUseSeparateLightTheme).toBe(true)
|
||||
})
|
||||
|
||||
it('asks before closing terminals with running processes by default', () => {
|
||||
expect(getDefaultSettings('/tmp').skipCloseTerminalWithRunningProcessConfirm).toBe(false)
|
||||
})
|
||||
|
||||
it('uses system language by default', () => {
|
||||
expect(getDefaultSettings('/tmp').uiLanguage).toBe('system')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
|
|||
disabledTuiAgents: [...DEFAULT_DISABLED_TUI_AGENTS],
|
||||
claudeAgentTeamsDefaultDisabledMigrated: true,
|
||||
skipDeleteWorktreeConfirm: false,
|
||||
skipCloseTerminalWithRunningProcessConfirm: false,
|
||||
skipDeleteAutomationConfirm: false,
|
||||
defaultTaskViewPreset: 'all',
|
||||
defaultTaskSource: 'github',
|
||||
|
|
|
|||
|
|
@ -2531,6 +2531,10 @@ export type GlobalSettings = {
|
|||
* again" checkbox inside it or from the General settings pane. We keep this
|
||||
* defaulted to false so first-time behavior stays safe. */
|
||||
skipDeleteWorktreeConfirm: boolean
|
||||
/** Why: closing a terminal with child processes kills foreground work. Keep
|
||||
* this separate from other destructive confirmations so power users can speed
|
||||
* up terminal cleanup without weakening workspace or automation safeguards. */
|
||||
skipCloseTerminalWithRunningProcessConfirm: boolean
|
||||
/** Why: deleting an automation also deletes its run history. Keep this
|
||||
* separate from worktree deletion so skipping one destructive confirmation
|
||||
* does not silently skip the other. */
|
||||
|
|
|
|||
Loading…
Reference in New Issue