Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ type TerminalContextMenuProps = {
onAddQuickCommand: () => void
onToggleExpand: () => void
onSetTitle: () => void
onClearPaneTitle: () => void
canClearPaneTitle: boolean
onCopyTerminalId: () => void
onCopyPaneId: () => void
}
Expand Down Expand Up @@ -100,6 +102,8 @@ export default function TerminalContextMenu({
onAddQuickCommand,
onToggleExpand,
onSetTitle,
onClearPaneTitle,
canClearPaneTitle,
onCopyTerminalId,
onCopyPaneId
}: TerminalContextMenuProps): React.JSX.Element {
Expand All @@ -111,13 +115,17 @@ 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())
}),
[keybindings]
)
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) ? (
Expand Down Expand Up @@ -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(
Expand Down
177 changes: 109 additions & 68 deletions src/renderer/src/components/terminal-pane/TerminalPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1479,6 +1581,8 @@ export default function TerminalPane({
onSearchSelectedText: handleSearchSelectedText,
onRequestClosePane: handleRequestClosePane,
onClearPaneScrollback: clearPaneScrollback,
onSetTitle: handleStartRename,
onClearPaneTitle: handleClearPaneTitleShortcut,
searchOpenRef,
searchStateRef,
macOptionAsAltRef,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -2360,6 +2396,7 @@ export default function TerminalPane({
onRequestClosePane: handleRequestClosePane,
onClearPaneScrollback: clearPaneScrollback,
onSetTitle: handleStartRename,
onClearPaneTitle: handleClearPaneTitleShortcut,
onPasteError: setTerminalError,
onAgentSessionForkReady: setAgentSessionFork,
forceBracketedMultilineTextPaste,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
/>
Expand Down
33 changes: 33 additions & 0 deletions src/renderer/src/components/terminal-pane/keyboard-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,20 @@ 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>
keybindings?: KeybindingOverrides
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,
Expand All @@ -157,6 +164,8 @@ export function useTerminalKeyboardShortcuts({
onSearchSelectedText,
onRequestClosePane,
onClearPaneScrollback,
onSetTitle,
onClearPaneTitle,
searchOpenRef,
searchStateRef,
macOptionAsAltRef,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -458,6 +489,8 @@ export function useTerminalKeyboardShortcuts({
onSearchSelectedText,
onRequestClosePane,
onClearPaneScrollback,
onSetTitle,
onClearPaneTitle,
searchOpenRef,
searchStateRef,
macOptionAsAltRef,
Expand Down
Loading
Loading