From 66efd938e69aab681b3469d1ed7bd770da9eb991 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 12:48:19 -0700 Subject: [PATCH] Add terminal pane ID copy action (#3172) Co-authored-by: Jinwoo-H --- .../terminal-pane/TerminalContextMenu.tsx | 8 +++++++- .../components/terminal-pane/TerminalPane.tsx | 2 ++ .../use-terminal-pane-context-menu.ts | 18 ++++++++++++++++++ tests/e2e/terminal-panes.spec.ts | 18 ++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx index f93a72f4e..861ac8ca0 100644 --- a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx @@ -53,6 +53,7 @@ type TerminalContextMenuProps = { onAddQuickCommand: () => void onToggleExpand: () => void onSetTitle: () => void + onCopyPaneId: () => void } export default function TerminalContextMenu({ @@ -77,7 +78,8 @@ export default function TerminalContextMenu({ onQuickCommand, onAddQuickCommand, onToggleExpand, - onSetTitle + onSetTitle, + onCopyPaneId }: TerminalContextMenuProps): React.JSX.Element { const copyShortcut = useShortcutLabel('terminal.copySelection') const pasteShortcut = useShortcutLabel('terminal.paste') @@ -239,6 +241,10 @@ export default function TerminalContextMenu({ Set Title… + + + Copy Pane ID + {canClosePane && ( <> diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index c60890620..01ac2163a 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -1540,6 +1540,7 @@ export default function TerminalPane({ managerRef, paneTransportsRef, paneCwdRef, + tabId, worktreeId, groupId: quickCommandGroupId, fallbackCwd: cwd ?? '', @@ -1759,6 +1760,7 @@ export default function TerminalPane({ } onToggleExpand={contextMenu.onToggleExpand} onSetTitle={contextMenu.onSetTitle} + onCopyPaneId={contextMenu.onCopyPaneId} /> paneTransportsRef: React.RefObject> paneCwdRef: React.RefObject + tabId: string worktreeId: string groupId: string | null fallbackCwd: string @@ -36,6 +39,7 @@ type TerminalMenuState = { menuPaneId: number | null onContextMenuCapture: (event: React.MouseEvent) => void onCopy: () => Promise + onCopyPaneId: () => Promise onPaste: () => Promise onSplitRight: () => void onSplitDown: () => void @@ -51,6 +55,7 @@ export function useTerminalPaneContextMenu({ managerRef, paneTransportsRef, paneCwdRef, + tabId, worktreeId, groupId, fallbackCwd, @@ -107,6 +112,18 @@ export function useTerminalPaneContextMenu({ pane.terminal.focus() } + const onCopyPaneId = async (): Promise => { + const pane = resolveMenuPane() + if (!pane) { + return + } + // Why: orchestration targets use ORCA_PANE_KEY, which survives renderer + // remounts; the numeric PaneManager id is only a local runtime handle. + await window.api.ui.writeClipboardText(makePaneKey(tabId, pane.leafId)) + toast.success('Pane ID copied') + pane.terminal.focus() + } + const onPaste = async (): Promise => { const pane = resolveMenuPane() if (!pane) { @@ -267,6 +284,7 @@ export function useTerminalPaneContextMenu({ menuPaneId, onContextMenuCapture, onCopy, + onCopyPaneId, onPaste, onSplitRight, onSplitDown, diff --git a/tests/e2e/terminal-panes.spec.ts b/tests/e2e/terminal-panes.spec.ts index b4c347b3d..827b71c1e 100644 --- a/tests/e2e/terminal-panes.spec.ts +++ b/tests/e2e/terminal-panes.spec.ts @@ -224,6 +224,24 @@ test.describe('Terminal Panes', () => { expect(activeLeafId).toMatch(UUID_RE) }) + test('terminal context menu copies the stable pane ID', async ({ orcaPage }) => { + const snapshot = await waitForPaneIdentitySnapshot(orcaPage, 1) + const leafId = snapshot.panes[0]?.leafId + if (!leafId) { + throw new Error('No terminal pane leaf id found') + } + const expectedPaneKey = `${snapshot.tabId}:${leafId}` + + await openTerminalContextMenu(orcaPage) + await orcaPage.getByText('Copy Pane ID', { exact: true }).click() + + await expect + .poll(() => orcaPage.evaluate(() => window.api.ui.readClipboardText()), { timeout: 3_000 }) + .toBe(expectedPaneKey) + await expect(orcaPage.getByText('Pane ID copied', { exact: true })).toBeVisible() + expect(leafId).toMatch(UUID_RE) + }) + test('first Set Title from terminal context menu stays open for typing', async ({ orcaPage }) => { const title = `First menu title ${Date.now()}`