mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
feat: add terminal pane title shortcuts (#6508)
Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
co-authored by
Jinwoo-H
Orca
parent
b7f7c30904
commit
a85218dd60
@@ -70,6 +70,8 @@ function renderMenu(overrides: Record<string, unknown> = {}): void {
|
||||
onAddQuickCommand: vi.fn(),
|
||||
onToggleExpand: vi.fn(),
|
||||
onSetTitle: vi.fn(),
|
||||
onClearPaneTitle: vi.fn(),
|
||||
canClearPaneTitle: false,
|
||||
onCopyTerminalId: vi.fn(),
|
||||
onCopyPaneId: vi.fn(),
|
||||
...overrides
|
||||
|
||||
@@ -67,6 +67,8 @@ type TerminalContextMenuProps = {
|
||||
onAddQuickCommand: () => void
|
||||
onToggleExpand: () => void
|
||||
onSetTitle: () => void
|
||||
onClearPaneTitle: () => void
|
||||
canClearPaneTitle: boolean
|
||||
onCopyTerminalId: () => void
|
||||
onCopyPaneId: () => void
|
||||
}
|
||||
@@ -100,6 +102,8 @@ export default function TerminalContextMenu({
|
||||
onAddQuickCommand,
|
||||
onToggleExpand,
|
||||
onSetTitle,
|
||||
onClearPaneTitle,
|
||||
canClearPaneTitle,
|
||||
onCopyTerminalId,
|
||||
onCopyPaneId
|
||||
}: TerminalContextMenuProps): React.JSX.Element {
|
||||
@@ -111,6 +115,8 @@ export default function TerminalContextMenu({
|
||||
splitDown: formatShortcutLabel('terminal.splitDown', keybindings),
|
||||
equalize: formatShortcutLabel('terminal.equalizePaneSizes', keybindings),
|
||||
expand: formatShortcutLabel('terminal.expandPane', keybindings),
|
||||
setTitle: formatShortcutLabel('terminal.setTitle', keybindings),
|
||||
clearPaneTitle: formatShortcutLabel('terminal.clearPaneTitle', keybindings),
|
||||
close: formatShortcutLabel('terminal.closePane', keybindings),
|
||||
nativeChat: nativeChatToggleShortcutLabel(isMacPlatform())
|
||||
}),
|
||||
@@ -118,6 +124,8 @@ export default function TerminalContextMenu({
|
||||
)
|
||||
const hasQuickCommands = repoQuickCommands.length > 0 || globalQuickCommands.length > 0
|
||||
const showEqualizeShortcut = shortcuts.equalize !== 'Unassigned'
|
||||
const showSetTitleShortcut = shortcuts.setTitle !== 'Unassigned'
|
||||
const showClearPaneTitleShortcut = shortcuts.clearPaneTitle !== 'Unassigned'
|
||||
const renderQuickCommandItem = (command: TerminalQuickCommand): React.JSX.Element => (
|
||||
<DropdownMenuItem key={command.id} onSelect={() => onQuickCommand(command)}>
|
||||
{isTerminalAgentQuickCommand(command) ? (
|
||||
@@ -337,7 +345,22 @@ export default function TerminalContextMenu({
|
||||
>
|
||||
<Pencil />
|
||||
{translate('auto.components.terminal.pane.TerminalContextMenu.39809d152f', 'Set Title…')}
|
||||
{showSetTitleShortcut ? (
|
||||
<DropdownMenuShortcut>{shortcuts.setTitle}</DropdownMenuShortcut>
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
{canClearPaneTitle ? (
|
||||
<DropdownMenuItem onSelect={onClearPaneTitle}>
|
||||
<X />
|
||||
{translate(
|
||||
'auto.components.terminal.pane.TerminalContextMenu.clearPaneTitle',
|
||||
'Clear Pane Title'
|
||||
)}
|
||||
{showClearPaneTitleShortcut ? (
|
||||
<DropdownMenuShortcut>{shortcuts.clearPaneTitle}</DropdownMenuShortcut>
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem onSelect={onCopyTerminalId}>
|
||||
<Copy />
|
||||
{translate(
|
||||
|
||||
@@ -458,6 +458,65 @@ export default function TerminalPane({
|
||||
const renameFocusFrameRef = useRef<number | null>(null)
|
||||
const renameEnableBlurFrameRef = useRef<number | null>(null)
|
||||
const renameRefocusFrameRef = useRef<number | null>(null)
|
||||
/**
|
||||
* Cancels deferred focus/blur work from inline title editing.
|
||||
* Rename sessions schedule multiple frames because xterm and Radix can both
|
||||
* move focus after the menu closes.
|
||||
*/
|
||||
const cancelPendingRenameFrames = useCallback(() => {
|
||||
const frameRefs = [renameFocusFrameRef, renameEnableBlurFrameRef, renameRefocusFrameRef]
|
||||
for (const frameRef of frameRefs) {
|
||||
if (frameRef.current !== null) {
|
||||
cancelAnimationFrame(frameRef.current)
|
||||
frameRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Invalidates the active inline title edit session before unmount or cancel.
|
||||
* Session IDs keep stale animation-frame callbacks from committing old titles.
|
||||
*/
|
||||
const closeRenameSession = useCallback(() => {
|
||||
renameSessionIdRef.current += 1
|
||||
renameBlurCommitEnabledRef.current = true
|
||||
renameUserRequestedBlurCommitRef.current = false
|
||||
cancelPendingRenameFrames()
|
||||
}, [cancelPendingRenameFrames])
|
||||
|
||||
/**
|
||||
* Owns the terminal container ref and closes rename work when that owner
|
||||
* detaches, preventing delayed focus callbacks from targeting stale DOM.
|
||||
*/
|
||||
const setContainerRef = useCallback(
|
||||
(node: HTMLDivElement | null): void => {
|
||||
containerRef.current = node
|
||||
if (node !== null) {
|
||||
return
|
||||
}
|
||||
// Why: inline title rename focus/blur frames are owned by the terminal
|
||||
// container; invalidate them when that DOM owner detaches.
|
||||
closeRenameSession()
|
||||
},
|
||||
[closeRenameSession]
|
||||
)
|
||||
|
||||
/**
|
||||
* Starts inline title editing from either the context menu or keyboard
|
||||
* shortcut while resetting stale blur-submit state from prior sessions.
|
||||
*/
|
||||
const handleStartRename = useCallback(
|
||||
(paneId: number) => {
|
||||
cancelPendingRenameFrames()
|
||||
renameSessionIdRef.current += 1
|
||||
renameBlurCommitEnabledRef.current = false
|
||||
renameUserRequestedBlurCommitRef.current = false
|
||||
renameSubmittedRef.current = false
|
||||
setRenameValue(paneTitlesRef.current[paneId] ?? '')
|
||||
setRenamingPaneId(paneId)
|
||||
},
|
||||
[cancelPendingRenameFrames]
|
||||
)
|
||||
const onPtyErrorRef = useRef((_paneId: number, message: string) => {
|
||||
if (isTerminalSessionStateSaveFailure(message)) {
|
||||
setTerminalError(null)
|
||||
@@ -888,6 +947,49 @@ export default function TerminalPane({
|
||||
[paneTransportsRef, persistLayoutSnapshot]
|
||||
)
|
||||
|
||||
/**
|
||||
* Removes a custom pane title from React state, the fresh persistence ref,
|
||||
* and the leaf-id tombstone set so the next layout snapshot stays cleared.
|
||||
*/
|
||||
const removePaneTitle = useCallback(
|
||||
(paneId: number) => {
|
||||
setPaneTitles((prev) => {
|
||||
if (!(paneId in prev)) {
|
||||
return prev
|
||||
}
|
||||
const next = { ...prev }
|
||||
delete next[paneId]
|
||||
return next
|
||||
})
|
||||
// Eagerly remove from the ref so persistLayoutSnapshot sees the change.
|
||||
if (paneId in paneTitlesRef.current) {
|
||||
const next = { ...paneTitlesRef.current }
|
||||
delete next[paneId]
|
||||
paneTitlesRef.current = next
|
||||
}
|
||||
const leafId = managerRef.current?.getPanes().find((pane) => pane.id === paneId)?.leafId
|
||||
if (leafId) {
|
||||
removedTitleLeafIdsRef.current.add(leafId)
|
||||
}
|
||||
persistLayoutSnapshot()
|
||||
},
|
||||
[persistLayoutSnapshot]
|
||||
)
|
||||
|
||||
/**
|
||||
* Ignores clear-title shortcuts for panes already using their automatic
|
||||
* title, keeping the command idempotent and avoiding unnecessary snapshots.
|
||||
*/
|
||||
const handleClearPaneTitleShortcut = useCallback(
|
||||
(paneId: number) => {
|
||||
if (!paneTitlesRef.current[paneId]) {
|
||||
return
|
||||
}
|
||||
removePaneTitle(paneId)
|
||||
},
|
||||
[removePaneTitle]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminalTab) {
|
||||
return
|
||||
@@ -1479,6 +1581,8 @@ export default function TerminalPane({
|
||||
onSearchSelectedText: handleSearchSelectedText,
|
||||
onRequestClosePane: handleRequestClosePane,
|
||||
onClearPaneScrollback: clearPaneScrollback,
|
||||
onSetTitle: handleStartRename,
|
||||
onClearPaneTitle: handleClearPaneTitleShortcut,
|
||||
searchOpenRef,
|
||||
searchStateRef,
|
||||
macOptionAsAltRef,
|
||||
@@ -2143,49 +2247,6 @@ export default function TerminalPane({
|
||||
}
|
||||
}, [tabId, worktreeId, setTabLayout])
|
||||
|
||||
const cancelPendingRenameFrames = useCallback(() => {
|
||||
const frameRefs = [renameFocusFrameRef, renameEnableBlurFrameRef, renameRefocusFrameRef]
|
||||
for (const frameRef of frameRefs) {
|
||||
if (frameRef.current !== null) {
|
||||
cancelAnimationFrame(frameRef.current)
|
||||
frameRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const closeRenameSession = useCallback(() => {
|
||||
renameSessionIdRef.current += 1
|
||||
renameBlurCommitEnabledRef.current = true
|
||||
renameUserRequestedBlurCommitRef.current = false
|
||||
cancelPendingRenameFrames()
|
||||
}, [cancelPendingRenameFrames])
|
||||
|
||||
const setContainerRef = useCallback(
|
||||
(node: HTMLDivElement | null): void => {
|
||||
containerRef.current = node
|
||||
if (node !== null) {
|
||||
return
|
||||
}
|
||||
// Why: inline title rename focus/blur frames are owned by the terminal
|
||||
// container; invalidate them when that DOM owner detaches.
|
||||
closeRenameSession()
|
||||
},
|
||||
[closeRenameSession]
|
||||
)
|
||||
|
||||
const handleStartRename = useCallback(
|
||||
(paneId: number) => {
|
||||
cancelPendingRenameFrames()
|
||||
renameSessionIdRef.current += 1
|
||||
renameBlurCommitEnabledRef.current = false
|
||||
renameUserRequestedBlurCommitRef.current = false
|
||||
renameSubmittedRef.current = false
|
||||
setRenameValue(paneTitlesRef.current[paneId] ?? '')
|
||||
setRenamingPaneId(paneId)
|
||||
},
|
||||
[cancelPendingRenameFrames]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (renamingPaneId === null) {
|
||||
return
|
||||
@@ -2212,31 +2273,6 @@ export default function TerminalPane({
|
||||
}
|
||||
}, [renamingPaneId])
|
||||
|
||||
const removePaneTitle = useCallback(
|
||||
(paneId: number) => {
|
||||
setPaneTitles((prev) => {
|
||||
if (!(paneId in prev)) {
|
||||
return prev
|
||||
}
|
||||
const next = { ...prev }
|
||||
delete next[paneId]
|
||||
return next
|
||||
})
|
||||
// Eagerly remove from the ref so persistLayoutSnapshot sees the change.
|
||||
if (paneId in paneTitlesRef.current) {
|
||||
const next = { ...paneTitlesRef.current }
|
||||
delete next[paneId]
|
||||
paneTitlesRef.current = next
|
||||
}
|
||||
const leafId = managerRef.current?.getPanes().find((pane) => pane.id === paneId)?.leafId
|
||||
if (leafId) {
|
||||
removedTitleLeafIdsRef.current.add(leafId)
|
||||
}
|
||||
persistLayoutSnapshot()
|
||||
},
|
||||
[persistLayoutSnapshot]
|
||||
)
|
||||
|
||||
const handleRenameSubmit = useCallback(() => {
|
||||
if (renamingPaneId === null || renameSubmittedRef.current) {
|
||||
return
|
||||
@@ -2360,6 +2396,7 @@ export default function TerminalPane({
|
||||
onRequestClosePane: handleRequestClosePane,
|
||||
onClearPaneScrollback: clearPaneScrollback,
|
||||
onSetTitle: handleStartRename,
|
||||
onClearPaneTitle: handleClearPaneTitleShortcut,
|
||||
onPasteError: setTerminalError,
|
||||
onAgentSessionForkReady: setAgentSessionFork,
|
||||
forceBracketedMultilineTextPaste,
|
||||
@@ -2638,6 +2675,8 @@ export default function TerminalPane({
|
||||
|
||||
const activePane = managerRef.current?.getActivePane()
|
||||
const managedPanes = managerRef.current?.getPanes() ?? []
|
||||
const menuPaneHasCustomTitle =
|
||||
contextMenu.menuPaneId !== null && Boolean(paneTitles[contextMenu.menuPaneId])
|
||||
const chatLeafStillMounted = chatLeafId
|
||||
? managedPanes.some((pane) => pane.leafId === chatLeafId)
|
||||
: false
|
||||
@@ -2814,6 +2853,8 @@ export default function TerminalPane({
|
||||
}
|
||||
onToggleExpand={contextMenu.onToggleExpand}
|
||||
onSetTitle={contextMenu.onSetTitle}
|
||||
onClearPaneTitle={contextMenu.onClearPaneTitle}
|
||||
canClearPaneTitle={menuPaneHasCustomTitle}
|
||||
onCopyTerminalId={() => void contextMenu.onCopyTerminalId()}
|
||||
onCopyPaneId={contextMenu.onCopyPaneId}
|
||||
/>
|
||||
|
||||
@@ -132,6 +132,8 @@ type KeyboardHandlersDeps = {
|
||||
onSearchSelectedText: (text: string) => void
|
||||
onRequestClosePane: (paneId: number) => void
|
||||
onClearPaneScrollback: (pane: ManagedPane) => void
|
||||
onSetTitle: (paneId: number) => void
|
||||
onClearPaneTitle: (paneId: number) => void
|
||||
searchOpenRef: React.RefObject<boolean>
|
||||
searchStateRef: React.RefObject<SearchState>
|
||||
macOptionAsAltRef: React.RefObject<MacOptionAsAlt>
|
||||
@@ -139,6 +141,11 @@ type KeyboardHandlersDeps = {
|
||||
terminalShortcutPolicy?: TerminalShortcutPolicy
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs terminal-pane shortcuts on the tab keyboard scope.
|
||||
* Uses the shared shortcut policy before forwarding unmatched input to xterm
|
||||
* so configurable Orca actions remain consistent across local and SSH panes.
|
||||
*/
|
||||
export function useTerminalKeyboardShortcuts({
|
||||
tabId,
|
||||
isActive,
|
||||
@@ -157,6 +164,8 @@ export function useTerminalKeyboardShortcuts({
|
||||
onSearchSelectedText,
|
||||
onRequestClosePane,
|
||||
onClearPaneScrollback,
|
||||
onSetTitle,
|
||||
onClearPaneTitle,
|
||||
searchOpenRef,
|
||||
searchStateRef,
|
||||
macOptionAsAltRef,
|
||||
@@ -389,6 +398,28 @@ export function useTerminalKeyboardShortcuts({
|
||||
return
|
||||
}
|
||||
|
||||
if (action.type === 'setTitle') {
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
const pane = manager.getActivePane() ?? manager.getPanes()[0]
|
||||
if (!pane) {
|
||||
return
|
||||
}
|
||||
onSetTitle(pane.id)
|
||||
return
|
||||
}
|
||||
|
||||
if (action.type === 'clearPaneTitle') {
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
const pane = manager.getActivePane() ?? manager.getPanes()[0]
|
||||
if (!pane) {
|
||||
return
|
||||
}
|
||||
onClearPaneTitle(pane.id)
|
||||
return
|
||||
}
|
||||
|
||||
// Cmd+W closes the active split pane (or the whole tab when only one
|
||||
// pane remains). Always intercepted here so the tab-level handler in
|
||||
// Terminal.tsx never closes the entire tab directly — that would kill
|
||||
@@ -458,6 +489,8 @@ export function useTerminalKeyboardShortcuts({
|
||||
onSearchSelectedText,
|
||||
onRequestClosePane,
|
||||
onClearPaneScrollback,
|
||||
onSetTitle,
|
||||
onClearPaneTitle,
|
||||
searchOpenRef,
|
||||
searchStateRef,
|
||||
macOptionAsAltRef,
|
||||
|
||||
@@ -262,6 +262,42 @@ describe('resolveTerminalShortcutAction', () => {
|
||||
).toEqual({ type: 'equalizePaneSizes' })
|
||||
})
|
||||
|
||||
it('resolves terminal title actions only when users assign them', () => {
|
||||
expect(
|
||||
resolveTerminalShortcutAction(event({ key: 't', code: 'KeyT', metaKey: true }), true)
|
||||
).toBeNull()
|
||||
expect(
|
||||
resolveTerminalShortcutAction(
|
||||
event({ key: 't', code: 'KeyT', metaKey: true }),
|
||||
true,
|
||||
'false',
|
||||
0,
|
||||
false,
|
||||
{ 'terminal.setTitle': ['Mod+T'] }
|
||||
)
|
||||
).toEqual({ type: 'setTitle' })
|
||||
expect(
|
||||
resolveTerminalShortcutAction(
|
||||
event({ key: 't', code: 'KeyT', metaKey: true, altKey: true }),
|
||||
true,
|
||||
'false',
|
||||
0,
|
||||
false,
|
||||
{ 'terminal.clearPaneTitle': ['Mod+Alt+T'] }
|
||||
)
|
||||
).toEqual({ type: 'clearPaneTitle' })
|
||||
expect(
|
||||
resolveTerminalShortcutAction(
|
||||
event({ key: 't', code: 'KeyT', metaKey: true, altKey: true, repeat: true }),
|
||||
true,
|
||||
'false',
|
||||
0,
|
||||
false,
|
||||
{ 'terminal.clearPaneTitle': ['Mod+Alt+T'] }
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('lets Ctrl+D pass through as EOF on non-Mac, requires Shift for split (#586)', () => {
|
||||
// Ctrl+D without Shift on Windows/Linux must NOT trigger split — it's EOF
|
||||
expect(
|
||||
|
||||
@@ -35,11 +35,18 @@ export type TerminalShortcutAction =
|
||||
| { type: 'focusPane'; direction: 'next' | 'previous' }
|
||||
| { type: 'equalizePaneSizes' }
|
||||
| { type: 'toggleExpandActivePane' }
|
||||
| { type: 'setTitle' }
|
||||
| { type: 'clearPaneTitle' }
|
||||
| { type: 'closeActivePane' }
|
||||
| { type: 'splitActivePane'; direction: 'vertical' | 'horizontal' }
|
||||
| { type: 'scrollViewport'; position: 'top' | 'bottom' }
|
||||
| { type: 'sendInput'; data: string }
|
||||
|
||||
/**
|
||||
* Resolves terminal keyboard events before xterm receives them.
|
||||
* Keeps configurable Orca shortcuts and terminal byte fallbacks in one
|
||||
* platform-aware policy so renderer handlers do not duplicate key checks.
|
||||
*/
|
||||
export function resolveTerminalShortcutAction(
|
||||
event: TerminalShortcutEvent,
|
||||
isMac: boolean,
|
||||
@@ -78,6 +85,14 @@ export function resolveTerminalShortcutAction(
|
||||
return { type: 'toggleExpandActivePane' }
|
||||
}
|
||||
|
||||
if (keybindingMatchesAction('terminal.setTitle', event, platform, keybindings)) {
|
||||
return { type: 'setTitle' }
|
||||
}
|
||||
|
||||
if (keybindingMatchesAction('terminal.clearPaneTitle', event, platform, keybindings)) {
|
||||
return { type: 'clearPaneTitle' }
|
||||
}
|
||||
|
||||
if (keybindingMatchesAction('terminal.closePane', event, platform, keybindings)) {
|
||||
return { type: 'closeActivePane' }
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ type UseTerminalPaneContextMenuDeps = {
|
||||
onRequestClosePane: (paneId: number) => void
|
||||
onClearPaneScrollback: (pane: ManagedPane) => void
|
||||
onSetTitle: (paneId: number) => void
|
||||
onClearPaneTitle: (paneId: number) => void
|
||||
onPasteError: (message: string) => void
|
||||
onAgentSessionForkReady: (fork: PreparedAgentSessionFork) => void
|
||||
forceBracketedMultilineTextPaste: boolean
|
||||
@@ -96,6 +97,7 @@ type TerminalMenuState = {
|
||||
onQuickCommand: (command: TerminalQuickCommand) => void
|
||||
onToggleExpand: () => void
|
||||
onSetTitle: () => void
|
||||
onClearPaneTitle: () => void
|
||||
runForPane: <Result>(paneId: number, action: () => Result) => Result
|
||||
}
|
||||
|
||||
@@ -112,6 +114,7 @@ export function useTerminalPaneContextMenu({
|
||||
onRequestClosePane,
|
||||
onClearPaneScrollback,
|
||||
onSetTitle,
|
||||
onClearPaneTitle,
|
||||
onPasteError,
|
||||
onAgentSessionForkReady,
|
||||
forceBracketedMultilineTextPaste,
|
||||
@@ -433,6 +436,7 @@ export function useTerminalPaneContextMenu({
|
||||
}
|
||||
}
|
||||
|
||||
/** Routes title edits through the resolved menu pane instead of active pane. */
|
||||
const handleSetTitle = (): void => {
|
||||
const pane = resolveMenuPane()
|
||||
if (pane) {
|
||||
@@ -440,6 +444,14 @@ export function useTerminalPaneContextMenu({
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears the title for the pane that opened the context menu. */
|
||||
const handleClearPaneTitle = (): void => {
|
||||
const pane = resolveMenuPane()
|
||||
if (pane) {
|
||||
onClearPaneTitle(pane.id)
|
||||
}
|
||||
}
|
||||
|
||||
const runForPane = <Result>(paneId: number, action: () => Result): Result => {
|
||||
const previousPaneId = contextPaneIdRef.current
|
||||
contextPaneIdRef.current = paneId
|
||||
@@ -548,6 +560,7 @@ export function useTerminalPaneContextMenu({
|
||||
onQuickCommand,
|
||||
onToggleExpand,
|
||||
onSetTitle: handleSetTitle,
|
||||
onClearPaneTitle: handleClearPaneTitle,
|
||||
runForPane
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2403,6 +2403,7 @@
|
||||
"copyTerminalId": "Copy Terminal ID",
|
||||
"2cf85a6a55": "Copy Pane ID",
|
||||
"39809d152f": "Set Title…",
|
||||
"clearPaneTitle": "Clear Pane Title",
|
||||
"06c2b0f043": "Equalize Pane Sizes",
|
||||
"98bccf4fa2": "Split Terminal Down",
|
||||
"20e565d865": "Split Terminal Right",
|
||||
|
||||
@@ -2403,6 +2403,7 @@
|
||||
"copyTerminalId": "Copiar ID de terminal",
|
||||
"2cf85a6a55": "Copiar ID del panel",
|
||||
"39809d152f": "Establecer título…",
|
||||
"clearPaneTitle": "Borrar título del panel",
|
||||
"06c2b0f043": "Igualar tamaños de paneles",
|
||||
"98bccf4fa2": "Terminal dividido hacia abajo",
|
||||
"20e565d865": "Terminal dividido a la derecha",
|
||||
|
||||
@@ -2403,6 +2403,7 @@
|
||||
"copyTerminalId": "ターミナル ID をコピー",
|
||||
"2cf85a6a55": "ペインIDのコピー",
|
||||
"39809d152f": "タイトルを設定…",
|
||||
"clearPaneTitle": "ペインタイトルをクリア",
|
||||
"06c2b0f043": "ペインのサイズを均等化する",
|
||||
"98bccf4fa2": "スプリット Terminal ダウン",
|
||||
"20e565d865": "分割 Terminal 右",
|
||||
|
||||
@@ -2403,6 +2403,7 @@
|
||||
"copyTerminalId": "터미널 ID 복사",
|
||||
"2cf85a6a55": "창 ID 복사",
|
||||
"39809d152f": "제목 설정…",
|
||||
"clearPaneTitle": "창 제목 지우기",
|
||||
"06c2b0f043": "창 크기 균등화",
|
||||
"98bccf4fa2": "Terminal을 아래로 분할",
|
||||
"20e565d865": "Terminal 오른쪽 분할",
|
||||
|
||||
@@ -2403,6 +2403,7 @@
|
||||
"copyTerminalId": "复制终端 ID",
|
||||
"2cf85a6a55": "复制窗格 ID",
|
||||
"39809d152f": "设置标题...",
|
||||
"clearPaneTitle": "清除窗格标题",
|
||||
"06c2b0f043": "均衡窗格大小",
|
||||
"98bccf4fa2": "向下拆分终端",
|
||||
"20e565d865": "向右拆分终端",
|
||||
|
||||
@@ -231,6 +231,7 @@ describe('keybindings', () => {
|
||||
expect(getEffectiveKeybindingsForAction('workspace.rename', 'darwin')).toEqual(['Mod+Alt+R'])
|
||||
expect(getEffectiveKeybindingsForAction('workspace.rename', 'linux')).toEqual([])
|
||||
expect(formatKeybindingList(['Mod+Alt+R'], 'darwin')).toBe('⌘⌥R')
|
||||
expect(getKeybindingDefinition('tab.rename')?.searchKeywords).not.toContain('set title')
|
||||
expect(
|
||||
keybindingMatchesAction(
|
||||
'tab.rename',
|
||||
@@ -420,6 +421,35 @@ describe('keybindings', () => {
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('names terminal title shortcuts after pane menu actions', () => {
|
||||
const setTitle = getKeybindingDefinition('terminal.setTitle')
|
||||
const clearTitle = getKeybindingDefinition('terminal.clearPaneTitle')
|
||||
|
||||
expect(setTitle?.title).toBe('Set Title…')
|
||||
expect(setTitle?.group).toBe('Terminal Panes')
|
||||
expect(setTitle?.scope).toBe('terminal')
|
||||
expect(setTitle?.searchKeywords).toContain('set title')
|
||||
expect(getEffectiveKeybindingsForAction('terminal.setTitle', 'darwin')).toEqual([])
|
||||
expect(getEffectiveKeybindingsForAction('terminal.setTitle', 'linux')).toEqual([])
|
||||
expect(getEffectiveKeybindingsForAction('terminal.setTitle', 'win32')).toEqual([])
|
||||
|
||||
expect(clearTitle?.title).toBe('Clear Pane Title')
|
||||
expect(clearTitle?.group).toBe('Terminal Panes')
|
||||
expect(clearTitle?.scope).toBe('terminal')
|
||||
expect(clearTitle?.searchKeywords).toContain('remove title')
|
||||
expect(getEffectiveKeybindingsForAction('terminal.clearPaneTitle', 'darwin')).toEqual([])
|
||||
expect(getEffectiveKeybindingsForAction('terminal.clearPaneTitle', 'linux')).toEqual([])
|
||||
expect(getEffectiveKeybindingsForAction('terminal.clearPaneTitle', 'win32')).toEqual([])
|
||||
expect(
|
||||
keybindingMatchesAction(
|
||||
'terminal.clearPaneTitle',
|
||||
{ key: 't', code: 'KeyT', control: false, meta: true, alt: true, shift: false },
|
||||
'darwin',
|
||||
{ 'terminal.clearPaneTitle': ['Mod+Alt+T'] }
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps workspace delete unassigned until users customize it', () => {
|
||||
const binding = {
|
||||
key: 'Backspace',
|
||||
|
||||
@@ -103,6 +103,8 @@ export type KeybindingActionId =
|
||||
| 'terminal.focusPreviousPane'
|
||||
| 'terminal.equalizePaneSizes'
|
||||
| 'terminal.expandPane'
|
||||
| 'terminal.setTitle'
|
||||
| 'terminal.clearPaneTitle'
|
||||
| 'terminal.closePane'
|
||||
| 'terminal.splitRight'
|
||||
| 'terminal.splitDown'
|
||||
@@ -941,6 +943,22 @@ export const KEYBINDING_DEFINITIONS: readonly KeybindingDefinition[] = [
|
||||
searchKeywords: ['shortcut', 'pane', 'expand', 'collapse'],
|
||||
defaultBindings: platformBindings(['Mod+Shift+Enter'])
|
||||
},
|
||||
{
|
||||
id: 'terminal.setTitle',
|
||||
title: 'Set Title…',
|
||||
group: 'Terminal Panes',
|
||||
scope: 'terminal',
|
||||
searchKeywords: ['shortcut', 'terminal', 'pane', 'set title', 'title', 'rename'],
|
||||
defaultBindings: platformBindings([])
|
||||
},
|
||||
{
|
||||
id: 'terminal.clearPaneTitle',
|
||||
title: 'Clear Pane Title',
|
||||
group: 'Terminal Panes',
|
||||
scope: 'terminal',
|
||||
searchKeywords: ['shortcut', 'terminal', 'pane', 'clear title', 'remove title', 'title'],
|
||||
defaultBindings: platformBindings([])
|
||||
},
|
||||
{
|
||||
id: 'terminal.closePane',
|
||||
title: 'Close active pane',
|
||||
|
||||
Reference in New Issue
Block a user