feat(terminal): expose right-click paste on every platform (#8322)

* WIP: Changes before auto-review fixes

* fix: preserve terminal paste defaults across platforms
This commit is contained in:
Neil 2026-07-11 15:02:46 -07:00 committed by GitHub
parent 3fffe635b4
commit 8a4ec4e856
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 127 additions and 56 deletions

View File

@ -6061,6 +6061,65 @@ describe('Store', () => {
expect(store.getSettings().terminalMacOptionAsAltMigrated).toBe(true)
})
it('migrates inherited right-click paste to each platform default once', async () => {
for (const [platform, expected] of [
['win32', true],
['darwin', false],
['linux', false]
] as const) {
await withPlatform(platform, async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: { terminalRightClickToPaste: true },
ui: {},
githubCache: { pr: {}, issue: {} },
workspaceSession: {}
})
const store = await createStore()
expect(store.getSettings().terminalRightClickToPaste).toBe(expected)
expect(store.getSettings().terminalRightClickToPasteDefaultedForPlatform).toBe(true)
})
}
})
it('preserves an explicit Windows right-click paste opt-out during migration', async () => {
await withPlatform('win32', async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: { terminalRightClickToPaste: false },
ui: {},
githubCache: { pr: {}, issue: {} },
workspaceSession: {}
})
const store = await createStore()
expect(store.getSettings().terminalRightClickToPaste).toBe(false)
expect(store.getSettings().terminalRightClickToPasteDefaultedForPlatform).toBe(true)
})
})
it('preserves right-click paste choices after the platform migration', async () => {
await withPlatform('darwin', async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: {
terminalRightClickToPaste: true,
terminalRightClickToPasteDefaultedForPlatform: true
},
ui: {},
githubCache: { pr: {}, issue: {} },
workspaceSession: {}
})
const store = await createStore()
expect(store.getSettings().terminalRightClickToPaste).toBe(true)
})
})
it('migrates inherited terminal bar cursor defaults to block on first load', async () => {
writeDataFile({
schemaVersion: 1,

View File

@ -2999,6 +2999,11 @@ export class Store {
const migratedTerminalLineHeight = normalizeTerminalLineHeight(
parsed.settings?.terminalLineHeight
)
const terminalRightClickToPasteDefaultedForPlatform =
parsed.settings?.terminalRightClickToPasteDefaultedForPlatform === true
if (!terminalRightClickToPasteDefaultedForPlatform) {
this.loadNeedsSave = true
}
if (
parsed.settings?.terminalLineHeight !== undefined &&
parsed.settings.terminalLineHeight !== migratedTerminalLineHeight
@ -3138,6 +3143,15 @@ export class Store {
...migratedAutoRenameBranchFromWork,
...migratedTerminalCursorStyle,
terminalLineHeight: migratedTerminalLineHeight,
// Why: the old global true default was inherited, while false was
// always an explicit opt-out and must survive this one-shot reset.
terminalRightClickToPaste: terminalRightClickToPasteDefaultedForPlatform
? (parsed.settings?.terminalRightClickToPaste ??
defaults.settings.terminalRightClickToPaste)
: parsed.settings?.terminalRightClickToPaste === false
? false
: defaults.settings.terminalRightClickToPaste,
terminalRightClickToPasteDefaultedForPlatform: true,
...migratedTerminalTuiScrollSensitivity.settings,
experimentalActivity: migratedExperimentalActivity,
experimentalActivityDefaultedOffForAllUsers: true,

View File

@ -24,7 +24,6 @@ type TerminalInteractionSectionProps = {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void
searchQuery: string
isWindows: boolean
}
type ScrollSpeedSliderProps = {
@ -89,8 +88,7 @@ function ScrollSpeedSlider({
export function TerminalInteractionSection({
settings,
updateSettings,
searchQuery,
isWindows
searchQuery
}: TerminalInteractionSectionProps): React.JSX.Element {
return (
<section key="pane-interaction" className="space-y-3">
@ -223,11 +221,7 @@ export function TerminalInteractionSection({
</div>
</SearchableSetting>
{/* Why: the Windows-only right-click toggle lives in this section, so the
section must also match that search term or settings search would hide
the control even though it is present. */}
{isWindows &&
matchesSettingsSearch(searchQuery, getTerminalRightClickToPasteSearchEntry()) ? (
{matchesSettingsSearch(searchQuery, getTerminalRightClickToPasteSearchEntry()) ? (
<SearchableSetting
title={translate(
'auto.components.settings.TerminalPane.9c178cf8aa',
@ -235,9 +229,9 @@ export function TerminalInteractionSection({
)}
description={translate(
'auto.components.settings.TerminalPane.af0c3b6e39',
'On Windows, right-click pastes the clipboard into the terminal. Use Ctrl+right-click to open the context menu.'
'Right-click pastes the clipboard into the terminal. Use Ctrl+right-click to open the context menu.'
)}
keywords={['terminal', 'windows', 'right click', 'paste', 'context menu']}
keywords={['terminal', 'right click', 'paste', 'context menu']}
>
<SettingsSwitchRow
label={translate(
@ -246,7 +240,7 @@ export function TerminalInteractionSection({
)}
description={translate(
'auto.components.settings.TerminalPane.16753eea48',
'On Windows, right-click pastes the clipboard. Ctrl+right-click opens the context menu.'
'Right-click pastes the clipboard. Ctrl+right-click opens the context menu.'
)}
checked={settings.terminalRightClickToPaste}
onChange={() =>

View File

@ -79,13 +79,12 @@ export function TerminalPane({
/>
) : null,
matchesSettingsSearch(searchQuery, getTerminalPaneInteractionSearchEntries()) ||
(isWindows && matchesSettingsSearch(searchQuery, getTerminalRightClickToPasteSearchEntry())) ? (
matchesSettingsSearch(searchQuery, getTerminalRightClickToPasteSearchEntry()) ? (
<TerminalInteractionSection
key="pane-interaction"
settings={settings}
updateSettings={updateSettings}
searchQuery={searchQuery}
isWindows={isWindows}
/>
) : null,
matchesSettingsSearch(searchQuery, getTerminalSetupScriptSearchEntries()) ? (

View File

@ -24,7 +24,7 @@ describe('getTerminalPaneSearchEntries', () => {
expect(entries.some((entry) => entry.title === 'Default Shell')).toBe(true)
expect(entries.some((entry) => entry.title === 'PowerShell Version')).toBe(true)
expect(entries.some((entry) => entry.title === 'Right-click to paste')).toBe(false)
expect(entries.some((entry) => entry.title === 'Right-click to paste')).toBe(true)
})
it('omits legacy WSL distribution terminal settings on Windows', () => {
@ -33,9 +33,11 @@ describe('getTerminalPaneSearchEntries', () => {
expect(matchesSettingsSearch('ubuntu distro', entries)).toBe(false)
})
it('omits the Windows right-click setting elsewhere', () => {
it('includes the right-click setting on macOS and Linux', () => {
const entries = getTerminalPaneSearchEntries({ isWindows: false, isMac: false })
expect(entries.some((entry) => entry.title === 'Right-click to paste')).toBe(false)
const macEntries = getTerminalPaneSearchEntries({ isWindows: false, isMac: true })
expect(entries.some((entry) => entry.title === 'Right-click to paste')).toBe(true)
expect(macEntries.some((entry) => entry.title === 'Right-click to paste')).toBe(true)
})
it('omits the PowerShell version setting elsewhere', () => {

View File

@ -115,7 +115,7 @@ export function getTerminalPaneSearchEntries(platform: {
...getTerminalWindowsPowershellImplementationSearchEntry()
]
: []),
...(platform.isWindows ? getTerminalRightClickToPasteSearchEntry() : []),
...getTerminalRightClickToPasteSearchEntry(),
...getTerminalSetupScriptSearchEntries(),
...getManageSessionsSearchEntries(),
...getTerminalAdvancedSearchEntries(),

View File

@ -113,17 +113,13 @@ export const getTerminalRightClickToPasteSearchEntry = createLocalizedCatalog(()
),
description: translate(
'auto.components.settings.terminal.windows.search.8ba875c132',
'On Windows, right-click pastes the clipboard into the terminal. Use Ctrl+right-click to open the context menu.'
'Right-click pastes the clipboard into the terminal. Use Ctrl+right-click to open the context menu.'
),
keywords: [
...translateSearchKeyword(
'auto.components.settings.terminal.windows.search.e7d2793b03',
'terminal'
),
...translateSearchKeyword(
'auto.components.settings.terminal.windows.search.28ff08ed35',
'windows'
),
...translateSearchKeyword(
'auto.components.settings.terminal.windows.search.e55186fe2b',
'right click'

View File

@ -785,11 +785,7 @@ export default function TerminalPane({
const updateSettings = useAppStore((store) => store.updateSettings)
const requestLinkRoutingPreference = useLinkRoutingPreferenceDialog()
const keybindings = useAppStore((store) => store.keybindings)
// Why: Windows is the only platform where bare right-click is repurposed as
// a paste gesture; on macOS/Linux the terminal still owns right-click for the
// context menu. The settings default keeps the Windows shortcut feeling native
// without changing the other platforms' interaction model.
const rightClickToPaste = isWindowsUserAgent() && (settings?.terminalRightClickToPaste ?? true)
const rightClickToPaste = settings?.terminalRightClickToPaste ?? isWindowsUserAgent()
// Why: Windows ConPTY does not forward DECSET 2004 from foreground TUIs, so
// xterm may not know multi-line text needs bracketed-paste protection.
const forceBracketedMultilineTextPaste = isWindowsUserAgent()

View File

@ -480,10 +480,8 @@ export function useTerminalPaneContextMenu({
: null
contextPaneIdRef.current = clickedPane?.id ?? null
// Why: Windows terminals treat right-click as copy-or-paste depending on
// whether text is selected. With a selection, right-click copies it and
// clears the selection; without one, it pastes. Ctrl+right-click still
// reaches the app menu so the menu remains discoverable.
// Why: when users opt into terminal-style right-click, a selection copies
// and no selection pastes. Ctrl+right-click keeps the app menu reachable.
if (rightClickToPaste && !event.ctrlKey) {
event.stopPropagation()
if (!clickedPane) {

View File

@ -6768,9 +6768,9 @@
"348246b06f": "false",
"5936387ddd": "auto",
"adbafefe56": "custom",
"16753eea48": "On Windows, right-click pastes the clipboard. Ctrl+right-click opens the context menu.",
"16753eea48": "Right-click pastes the clipboard. Ctrl+right-click opens the context menu.",
"9c178cf8aa": "Right-click to paste",
"af0c3b6e39": "On Windows, right-click pastes the clipboard into the terminal. Use Ctrl+right-click to open the context menu.",
"af0c3b6e39": "Right-click pastes the clipboard into the terminal. Use Ctrl+right-click to open the context menu.",
"29154326bb": "on",
"ab20575a8a": "off",
"ab3a1f9068": "wsl.exe",
@ -8496,7 +8496,7 @@
"e55186fe2b": "right click",
"28ff08ed35": "windows",
"e7d2793b03": "terminal",
"8ba875c132": "On Windows, right-click pastes the clipboard into the terminal. Use Ctrl+right-click to open the context menu.",
"8ba875c132": "Right-click pastes the clipboard into the terminal. Use Ctrl+right-click to open the context menu.",
"f0b8448570": "Right-click to paste",
"04994f6929": "default",
"fc564eadaf": "debian",

View File

@ -6731,9 +6731,9 @@
"348246b06f": "FALSO",
"5936387ddd": "auto",
"adbafefe56": "personalizado",
"16753eea48": "En Windows, hacer clic derecho pega el portapapeles. Ctrl+clic derecho abre el menú contextual.",
"16753eea48": "Hacer clic derecho pega el portapapeles. Ctrl+clic derecho abre el menú contextual.",
"9c178cf8aa": "Clic derecho para pegar",
"af0c3b6e39": "En Windows, hacer clic derecho pega el portapapeles en el terminal. Usa Ctrl+clic derecho para abrir el menú contextual.",
"af0c3b6e39": "Hacer clic derecho pega el portapapeles en el terminal. Usa Ctrl+clic derecho para abrir el menú contextual.",
"29154326bb": "activado",
"ab20575a8a": "apagado",
"ab3a1f9068": "wsl.exe",
@ -8459,7 +8459,7 @@
"e55186fe2b": "clic derecho",
"28ff08ed35": "Windows",
"e7d2793b03": "terminal",
"8ba875c132": "En Windows, hacer clic derecho pega el portapapeles en el terminal. Usa Ctrl+clic derecho para abrir el menú contextual.",
"8ba875c132": "Hacer clic derecho pega el portapapeles en el terminal. Usa Ctrl+clic derecho para abrir el menú contextual.",
"f0b8448570": "Clic derecho para pegar",
"04994f6929": "predeterminado",
"fc564eadaf": "debian",

View File

@ -6753,9 +6753,9 @@
"348246b06f": "false",
"5936387ddd": "自動",
"adbafefe56": "カスタム",
"16753eea48": "Windows では、右クリックしてクリップボードを貼り付けます。 Ctrl キーを押しながら右クリックすると、コンテキスト メニューが開きます。",
"16753eea48": "右クリックしてクリップボードを貼り付けます。 Ctrl キーを押しながら右クリックすると、コンテキスト メニューが開きます。",
"9c178cf8aa": "右クリックして貼り付けます",
"af0c3b6e39": "Windows では、右クリックしてクリップボードを terminal に貼り付けます。 Ctrl キーを押しながら右クリックしてコンテキスト メニューを開きます。",
"af0c3b6e39": "右クリックしてクリップボードを terminal に貼り付けます。 Ctrl キーを押しながら右クリックしてコンテキスト メニューを開きます。",
"29154326bb": "の上",
"ab20575a8a": "オフ",
"ab3a1f9068": "wsl.exe",
@ -8481,7 +8481,7 @@
"e55186fe2b": "右クリック",
"28ff08ed35": "窓",
"e7d2793b03": "ターミナル",
"8ba875c132": "Windows では、右クリックしてクリップボードを terminal に貼り付けます。 Ctrl キーを押しながら右クリックしてコンテキスト メニューを開きます。",
"8ba875c132": "右クリックしてクリップボードを terminal に貼り付けます。 Ctrl キーを押しながら右クリックしてコンテキスト メニューを開きます。",
"f0b8448570": "右クリックして貼り付けます",
"04994f6929": "デフォルト",
"fc564eadaf": "デビアン",

View File

@ -6716,9 +6716,9 @@
"348246b06f": "false",
"5936387ddd": "자동",
"adbafefe56": "custom",
"16753eea48": "Windows에서는 마우스 오른쪽 버튼을 클릭하여 클립보드를 붙여넣습니다. Ctrl+마우스 오른쪽 버튼을 클릭하면 컨텍스트 메뉴가 열립니다.",
"16753eea48": "마우스 오른쪽 버튼을 클릭하여 클립보드를 붙여넣습니다. Ctrl+마우스 오른쪽 버튼을 클릭하면 컨텍스트 메뉴가 열립니다.",
"9c178cf8aa": "붙여넣으려면 마우스 오른쪽 버튼을 클릭하세요.",
"af0c3b6e39": "Windows에서는 마우스 오른쪽 버튼을 클릭하여 클립보드를 terminal에 붙여넣습니다. 컨텍스트 메뉴를 열려면 Ctrl+오른쪽 클릭을 사용하세요.",
"af0c3b6e39": "마우스 오른쪽 버튼을 클릭하여 클립보드를 terminal에 붙여넣습니다. 컨텍스트 메뉴를 열려면 Ctrl+오른쪽 클릭을 사용하세요.",
"29154326bb": "켜기",
"ab20575a8a": "끄기",
"ab3a1f9068": "wsl.exe",
@ -8444,7 +8444,7 @@
"e55186fe2b": "오른쪽 클릭",
"28ff08ed35": "창",
"e7d2793b03": "터미널",
"8ba875c132": "Windows에서는 마우스 오른쪽 버튼을 클릭하여 클립보드를 terminal에 붙여넣습니다. 컨텍스트 메뉴를 열려면 Ctrl+오른쪽 클릭을 사용하세요.",
"8ba875c132": "마우스 오른쪽 버튼을 클릭하여 클립보드를 terminal에 붙여넣습니다. 컨텍스트 메뉴를 열려면 Ctrl+오른쪽 클릭을 사용하세요.",
"f0b8448570": "붙여넣으려면 마우스 오른쪽 버튼을 클릭하세요.",
"04994f6929": "기본값",
"fc564eadaf": "데비안",

View File

@ -6716,9 +6716,9 @@
"348246b06f": "false",
"5936387ddd": "自动",
"adbafefe56": "风俗",
"16753eea48": "在 Windows 上,右键单击粘贴剪贴板。 Ctrl+右键单击打开上下文菜单。",
"16753eea48": "右键单击粘贴剪贴板。 Ctrl+右键单击打开上下文菜单。",
"9c178cf8aa": "右键单击粘贴",
"af0c3b6e39": "在 Windows 上,右键单击将剪贴板粘贴到终端中。使用 Ctrl+右键单击打开上下文菜单。",
"af0c3b6e39": "右键单击将剪贴板粘贴到终端中。使用 Ctrl+右键单击打开上下文菜单。",
"29154326bb": "开",
"ab20575a8a": "关",
"ab3a1f9068": "执行程序",
@ -8444,7 +8444,7 @@
"e55186fe2b": "右键单击",
"28ff08ed35": "视窗",
"e7d2793b03": "终端",
"8ba875c132": "在 Windows 上,右键单击将剪贴板粘贴到终端中。使用 Ctrl+右键单击打开上下文菜单。",
"8ba875c132": "右键单击将剪贴板粘贴到终端中。使用 Ctrl+右键单击打开上下文菜单。",
"f0b8448570": "右键单击粘贴",
"04994f6929": "默认",
"fc564eadaf": "德比安",

View File

@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
getDefaultNotificationSettings,
getDefaultPrimarySelectionMiddleClickPaste,
getDefaultTerminalRightClickToPaste,
getDefaultSettings
} from './constants'
@ -126,6 +127,14 @@ describe('getDefaultPrimarySelectionMiddleClickPaste', () => {
})
})
describe('getDefaultTerminalRightClickToPaste', () => {
it('defaults on only for Windows', () => {
expect(getDefaultTerminalRightClickToPaste('win32')).toBe(true)
expect(getDefaultTerminalRightClickToPaste('darwin')).toBe(false)
expect(getDefaultTerminalRightClickToPaste('linux')).toBe(false)
})
})
describe('MiniMax defaults', () => {
it('starts MiniMax with empty group id and the canonical default model', () => {
const settings = getDefaultSettings('/tmp')

View File

@ -101,6 +101,10 @@ export const getDefaultPrimarySelectionMiddleClickPaste = (
platform = typeof process !== 'undefined' ? process.platform : ''
): boolean => platform === 'linux' || platform === 'darwin'
export const getDefaultTerminalRightClickToPaste = (
platform = typeof process !== 'undefined' ? process.platform : ''
): boolean => platform === 'win32'
/**
* Why: ProseMirror builds an in-memory tree for the entire document, so large
* markdown files cause noticeable typing lag in the rich editor. Files above
@ -240,10 +244,10 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
terminalActivePaneOpacity: 1,
terminalPaneOpacityTransitionMs: 140,
terminalDividerThicknessPx: 3,
// Default true so Windows users get native right-click paste out of the
// box. Other platforms ignore this field because the UI never exposes it,
// and Ctrl+right-click still opens the context menu when paste is enabled.
terminalRightClickToPaste: true,
// Why: Windows follows its native terminal paste convention, while macOS
// and Linux keep right-click available for the context menu by default.
terminalRightClickToPaste: getDefaultTerminalRightClickToPaste(),
terminalRightClickToPasteDefaultedForPlatform: true,
terminalWindowsShell: 'powershell.exe',
terminalWindowsWslDistro: null,
localAccountRuntime: 'host',

View File

@ -2573,10 +2573,12 @@ export type GlobalSettings = {
* system tray instead of quitting Orca; off keeps the default quit-on-close.
* The tray icon itself is always present on Windows regardless of this flag. */
minimizeToTrayOnClose?: boolean
/** Why: Windows terminals conventionally use right-click as a paste gesture.
* The setting stays Windows-only so macOS/Linux keep their existing context
* menu behavior and users can still reach the menu with Ctrl+right-click. */
/** Why: Windows terminals conventionally use right-click as a paste gesture,
* while macOS/Linux default to their existing context menu behavior. */
terminalRightClickToPaste: boolean
/** One-shot guard that distinguishes the old global true default from a
* choice made after the setting became available on every platform. */
terminalRightClickToPasteDefaultedForPlatform?: boolean
/** Why: COMSPEC always points to cmd.exe on stock Windows, so without an
* explicit setting the terminal would always open CMD instead of the
* user's preferred shell. Defaults to 'powershell.exe' which is the

View File

@ -362,13 +362,11 @@ test.describe('terminal paste ownership', () => {
}
})
test('Windows right-click paste sends clipboard text to the focused terminal exactly once', async ({
test('right-click paste sends clipboard text to the focused terminal exactly once', async ({
electronApp,
orcaPage,
testRepoPath
}) => {
test.skip(process.platform !== 'win32', 'Windows right-click paste is Windows-only')
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
await ensureTerminalVisible(orcaPage)