From ec07019a122250cd7dfe38951976e4a1f00a4ce5 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 7 Nov 2026 23:16:47 -0800 Subject: [PATCH] Use Ctrl+K for search on Windows and Linux (#39767) * Use Ctrl+K for search on Linux and Windows Preserve existing search and terminal clear shortcuts for legacy profiles until reset. * Preserve existing Windows and Linux search shortcuts on rollout * Preserve saved shortcuts in new profiles * Keep window shortcut tests independent of defaults --- .../keybindings/keybinding-service.test.ts | 31 +++++++- src/main/keybindings/keybinding-service.ts | 6 +- .../loading-store/loaded-cohort-migrations.ts | 6 +- ...ence-cohort-and-identity-migration.test.ts | 19 +++++ .../startup/main-process-account-services.ts | 5 +- ...ainWindow-terminal-focus-shortcuts.test.ts | 39 +++++----- .../web/preload-api/web-keybindings-api.ts | 16 ++-- .../web/web-preload-api-keybindings.test.ts | 74 ++++++++++++++++--- src/shared/keybindings-conflicts.test.ts | 51 ++++++++++++- src/shared/keybindings/definitions-core-1.ts | 6 +- src/shared/keybindings/definitions-core-3.ts | 2 +- src/shared/keybindings/definitions.ts | 9 ++- src/shared/keybindings/effective.ts | 17 ++++- src/shared/keybindings/formatting.ts | 9 +-- src/shared/keybindings/global-settings.ts | 4 +- src/shared/keybindings/profile-defaults.ts | 14 +++- 16 files changed, 244 insertions(+), 64 deletions(-) diff --git a/src/main/keybindings/keybinding-service.test.ts b/src/main/keybindings/keybinding-service.test.ts index 6a90da49ac5..8676e2c9e66 100644 --- a/src/main/keybindings/keybinding-service.test.ts +++ b/src/main/keybindings/keybinding-service.test.ts @@ -205,7 +205,7 @@ describe('KeybindingService profile defaults', () => { }) it.each(['linux', 'win32'] as const)( - 'keeps tab defaults local and search unchanged on %s', + 'preserves legacy search and tab defaults on %s', (platform) => { const older = new KeybindingService({ homePath: home, @@ -217,7 +217,9 @@ describe('KeybindingService profile defaults', () => { 'tab.nextSameType': ['Mod+Shift+BracketRight'], 'tab.previousSameType': ['Mod+Shift+BracketLeft'], 'tab.nextAllTypes': ['Mod+Alt+BracketRight'], - 'tab.previousAllTypes': ['Mod+Alt+BracketLeft'] + 'tab.previousAllTypes': ['Mod+Alt+BracketLeft'], + 'worktree.palette': ['Mod+Shift+J'], + 'terminal.clear': ['Mod+K'] }) older.setActionBindings('tab.nextAllTypes', []) const reset = older.setActionBindings('tab.nextSameType', null) @@ -231,6 +233,31 @@ describe('KeybindingService profile defaults', () => { } ) + it.each(['linux', 'win32'] as const)( + 'uses Ctrl+K for new profiles and preserves old shortcuts until reset on %s', + (platform) => { + const fresh = new KeybindingService({ homePath: home, platform }) + const older = new KeybindingService({ homePath: home, platform, legacySearchDefaults: true }) + const ctrlK = { key: 'k', code: 'KeyK', meta: false, control: true, alt: false, shift: false } + expect( + keybindingMatchesAction('worktree.palette', ctrlK, platform, fresh.getOverrides()) + ).toBe(true) + expect(keybindingMatchesAction('terminal.clear', ctrlK, platform, older.getOverrides())).toBe( + true + ) + expect( + getEffectiveKeybindingsForAction('worktree.palette', platform, older.getOverrides()) + ).toEqual(['Mod+Shift+J']) + const reset = older.setActionBindings('worktree.palette', null) + expect(keybindingMatchesAction('worktree.palette', ctrlK, platform, reset.overrides)).toBe( + true + ) + expect(keybindingMatchesAction('terminal.clear', ctrlK, platform, reset.overrides)).toBe( + false + ) + } + ) + it('yields inherited tab shortcuts to explicit assignments during loads and settings writes', () => { write({ platforms: { darwin: { 'tab.previousAllTypes': ['Mod+Shift+BracketLeft'] } } }) const older = new KeybindingService({ diff --git a/src/main/keybindings/keybinding-service.ts b/src/main/keybindings/keybinding-service.ts index 6e926929ca2..74f168eef84 100644 --- a/src/main/keybindings/keybinding-service.ts +++ b/src/main/keybindings/keybinding-service.ts @@ -1,5 +1,5 @@ import { - LEGACY_MAC_SEARCH_BINDINGS, + getLegacySearchBindings, LEGACY_TAB_SWITCH_BINDINGS, type KeybindingActionId, type KeybindingFileSnapshot, @@ -33,9 +33,7 @@ export class KeybindingService { this.platform = options.platform ?? process.platform this.defaultOverrides = { ...(options.legacyTabSwitchDefaults ? LEGACY_TAB_SWITCH_BINDINGS : {}), - ...(options.legacySearchDefaults && this.platform === 'darwin' - ? LEGACY_MAC_SEARCH_BINDINGS - : {}) + ...(options.legacySearchDefaults ? getLegacySearchBindings(this.platform) : {}) } const legacyOverrides = options.getLegacyOverrides?.() try { diff --git a/src/main/persistence/loading-store/loaded-cohort-migrations.ts b/src/main/persistence/loading-store/loaded-cohort-migrations.ts index 124c4cde593..85eef2c9028 100644 --- a/src/main/persistence/loading-store/loaded-cohort-migrations.ts +++ b/src/main/persistence/loading-store/loaded-cohort-migrations.ts @@ -23,7 +23,11 @@ export class LoadedCohortMigrationOperations { Object.keys(state.settings).length === 1 && isRecord(state.settings.telemetry) const existingProfile = fileExistedOnLoad && !telemetryOnly - for (const key of ['tabSwitchKeybindingSeed', 'searchKeybindingSeed'] as const) { + for (const key of [ + 'tabSwitchKeybindingSeed', + 'searchKeybindingSeed', + 'nonMacSearchKeybindingSeed' + ] as const) { const existing = state.settings?.[key] if (existing === 'legacy' || existing === 'done') { continue diff --git a/src/main/persistence/persistence-cohort-and-identity-migration.test.ts b/src/main/persistence/persistence-cohort-and-identity-migration.test.ts index 9293cc5bbd2..53b6da9d8bd 100644 --- a/src/main/persistence/persistence-cohort-and-identity-migration.test.ts +++ b/src/main/persistence/persistence-cohort-and-identity-migration.test.ts @@ -109,9 +109,28 @@ describe('Store', () => { const store = await createStore() expect(store.getSettings().tabSwitchKeybindingSeed).toBe('done') expect(store.getSettings().searchKeybindingSeed).toBe('done') + expect(store.getSettings().nonMacSearchKeybindingSeed).toBe('done') await closeTestStores() const reopened = await createStore() expect(reopened.getSettings().searchKeybindingSeed).toBe('done') + expect(reopened.getSettings().nonMacSearchKeybindingSeed).toBe('done') + }) + + it('keeps profiles created after the Mac rollout in the legacy non-Mac search cohort', async () => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + ui: {}, + settings: { searchKeybindingSeed: 'done' } + }) + const store = await createStore() + expect(store.getSettings().nonMacSearchKeybindingSeed).toBe('legacy') + store.flush() + await closeTestStores() + const reopened = await createStore() + expect(reopened.getSettings().searchKeybindingSeed).toBe('done') + expect(reopened.getSettings().nonMacSearchKeybindingSeed).toBe('legacy') }) }) diff --git a/src/main/startup/main-process-account-services.ts b/src/main/startup/main-process-account-services.ts index f6e19024c80..18670a8ffa2 100644 --- a/src/main/startup/main-process-account-services.ts +++ b/src/main/startup/main-process-account-services.ts @@ -79,7 +79,10 @@ export function initializeMainProcessAccountServices(): void { homePath: app.getPath('home'), getLegacyOverrides: () => store.getSettings().keybindings, legacyTabSwitchDefaults: store.getSettings().tabSwitchKeybindingSeed === 'legacy', - legacySearchDefaults: store.getSettings().searchKeybindingSeed === 'legacy' + legacySearchDefaults: + (process.platform === 'darwin' + ? store.getSettings().searchKeybindingSeed + : store.getSettings().nonMacSearchKeybindingSeed) === 'legacy' }) browserManager.setSettingsResolver(() => ({ keybindings: state.keybindings?.getOverrides() })) } diff --git a/src/main/window/createMainWindow-terminal-focus-shortcuts.test.ts b/src/main/window/createMainWindow-terminal-focus-shortcuts.test.ts index 760b92aa9a4..ef8db46c8a4 100644 --- a/src/main/window/createMainWindow-terminal-focus-shortcuts.test.ts +++ b/src/main/window/createMainWindow-terminal-focus-shortcuts.test.ts @@ -220,7 +220,7 @@ describe('createMainWindow', () => { expect(webContents.send).toHaveBeenCalledWith('ui:dictationKeyDown') }) - it('forwards the search shortcut to the worktree palette toggle event', () => { + it('forwards the configured search shortcut to the worktree palette toggle event', () => { const windowHandlers: Record void> = {} const webContents = { on: vi.fn((event, handler) => { @@ -253,27 +253,29 @@ describe('createMainWindow', () => { return browserWindowInstance }) - createMainWindow(null) + createMainWindow(null, { + getKeybindings: () => ({ 'worktree.palette': ['Mod+Alt+K'] }) + }) const isDarwin = process.platform === 'darwin' for (const input of [ { type: 'keyDown', - code: isDarwin ? 'KeyK' : 'KeyJ', - key: isDarwin ? 'k' : 'j', + code: 'KeyK', + key: 'k', meta: isDarwin, control: !isDarwin, - alt: false, - shift: !isDarwin + alt: true, + shift: false }, { type: 'keyDown', - code: isDarwin ? 'KeyK' : 'KeyJ', + code: 'KeyK', key: '', meta: isDarwin, control: !isDarwin, - alt: false, - shift: !isDarwin + alt: true, + shift: false } ]) { const preventDefault = vi.fn() @@ -583,10 +585,13 @@ describe('createMainWindow', () => { return browserWindowInstance }) - createMainWindow({ - getUI: () => ({}), - getSettings: () => ({ terminalShortcutPolicy: 'orca-first' }) - } as never) + createMainWindow( + { + getUI: () => ({}), + getSettings: () => ({ terminalShortcutPolicy: 'orca-first' }) + } as never, + { getKeybindings: () => ({ 'worktree.palette': ['Mod+Alt+K'] }) } + ) const setFocusedListener = vi .mocked(ipcMain.on) @@ -598,12 +603,12 @@ describe('createMainWindow', () => { const isDarwin = process.platform === 'darwin' windowHandlers['before-input-event']({ preventDefault } as never, { type: 'keyDown', - code: isDarwin ? 'KeyK' : 'KeyJ', - key: isDarwin ? 'k' : 'j', + code: 'KeyK', + key: 'k', meta: isDarwin, control: !isDarwin, - alt: false, - shift: !isDarwin + alt: true, + shift: false }) expect(preventDefault).toHaveBeenCalledTimes(1) diff --git a/src/renderer/src/web/preload-api/web-keybindings-api.ts b/src/renderer/src/web/preload-api/web-keybindings-api.ts index dde32e2bfea..fe588bfddd8 100644 --- a/src/renderer/src/web/preload-api/web-keybindings-api.ts +++ b/src/renderer/src/web/preload-api/web-keybindings-api.ts @@ -5,7 +5,7 @@ import { getKeybindingPlatform, getKeybindingResetBindings, isKeybindingActionId, - LEGACY_MAC_SEARCH_BINDINGS, + getLegacySearchBindings, normalizeKeybindingArrayForAction, resolveKeybindingOverrides } from '../../../../shared/keybindings' @@ -40,15 +40,21 @@ export type WebKeybindingsApi = NonNullable export const webKeybindingListeners = new Set<(snapshot: KeybindingFileSnapshot) => void>() const SEARCH_DEFAULTS_STORAGE_KEY = 'orca.web.searchKeybindingSeed.v1' +const NON_MAC_SEARCH_DEFAULTS_STORAGE_KEY = 'orca.web.nonMacSearchKeybindingSeed.v1' let legacySearchDefaults = false function migrateWebSearchKeybindings(): void { - const cohort = window.localStorage.getItem(SEARCH_DEFAULTS_STORAGE_KEY) + const storageKey = + getWebKeybindingPlatform() === 'darwin' + ? SEARCH_DEFAULTS_STORAGE_KEY + : NON_MAC_SEARCH_DEFAULTS_STORAGE_KEY + const cohort = window.localStorage.getItem(storageKey) if (cohort === 'done' || cohort === 'legacy') { legacySearchDefaults = cohort === 'legacy' return } legacySearchDefaults = [ + SEARCH_DEFAULTS_STORAGE_KEY, SETTINGS_STORAGE_KEY, KEYBINDINGS_STORAGE_KEY, UI_STORAGE_KEY, @@ -56,13 +62,11 @@ function migrateWebSearchKeybindings(): void { ONBOARDING_STORAGE_KEY, GITHUB_CACHE_STORAGE_KEY ].some((key) => window.localStorage.getItem(key) !== null) - window.localStorage.setItem(SEARCH_DEFAULTS_STORAGE_KEY, legacySearchDefaults ? 'legacy' : 'done') + window.localStorage.setItem(storageKey, legacySearchDefaults ? 'legacy' : 'done') } function getWebDefaultOverrides(): KeybindingOverrides { - return legacySearchDefaults && getWebKeybindingPlatform() === 'darwin' - ? LEGACY_MAC_SEARCH_BINDINGS - : {} + return legacySearchDefaults ? getLegacySearchBindings(getWebKeybindingPlatform()) : {} } export function createEmptyWebKeybindingDocument(): WebKeybindingDocument { diff --git a/src/renderer/src/web/web-preload-api-keybindings.test.ts b/src/renderer/src/web/web-preload-api-keybindings.test.ts index 8fd9b1d23a1..d50275cf3f8 100644 --- a/src/renderer/src/web/web-preload-api-keybindings.test.ts +++ b/src/renderer/src/web/web-preload-api-keybindings.test.ts @@ -1,5 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { getEffectiveKeybindingsForAction } from '../../../shared/keybindings' +import { + getEffectiveKeybindingsForAction, + keybindingMatchesAction +} from '../../../shared/keybindings' import { installApi, installBrowserGlobals } from './web-preload-api-test-harness' describe('web keybindings preload API', () => { @@ -55,6 +58,37 @@ describe('web keybindings preload API', () => { }) }) + it.each([ + ['Linux x86_64', 'linux'], + ['Windows NT 10.0', 'win32'] + ] as const)( + 'preserves existing %s web shortcuts and resets to Ctrl+K', + async (userAgent, platform) => { + const { storage } = installBrowserGlobals(userAgent) + storage.setItem('orca.web.settings.v1', '{}') + storage.setItem('orca.web.searchKeybindingSeed.v1', 'done') + const { installWebPreloadApi } = await import('./web-preload-api') + installWebPreloadApi() + const old = await window.api.keybindings.get() + expect(getEffectiveKeybindingsForAction('worktree.palette', platform, old.overrides)).toEqual( + ['Mod+Shift+J'] + ) + expect(getEffectiveKeybindingsForAction('terminal.clear', platform, old.overrides)).toEqual([ + 'Mod+K' + ]) + const reset = await window.api.keybindings.setAction({ + actionId: 'worktree.palette', + bindings: null + }) + expect( + getEffectiveKeybindingsForAction('worktree.palette', platform, reset.overrides) + ).toEqual(['Mod+K']) + expect(getEffectiveKeybindingsForAction('terminal.clear', platform, reset.overrides)).toEqual( + [] + ) + } + ) + it.each(['common', 'platform'])( 'keeps an explicit %s palette chord ahead of the generated clear shortcut', async (section) => { @@ -77,6 +111,25 @@ describe('web keybindings preload API', () => { } ) + it('keeps saved Ctrl+K clear bindings reachable after the earlier Mac rollout', async () => { + const { storage } = installBrowserGlobals('Windows NT 10.0') + storage.setItem('orca.web.searchKeybindingSeed.v1', 'done') + storage.setItem( + 'orca.web.keybindings.v1', + JSON.stringify({ platforms: { win32: { 'terminal.clear': ['Ctrl+K'] } } }) + ) + const { installWebPreloadApi } = await import('./web-preload-api') + installWebPreloadApi() + installWebPreloadApi() + const { overrides } = await window.api.keybindings.get() + const ctrlK = { key: 'k', control: true } + expect(keybindingMatchesAction('terminal.clear', ctrlK, 'win32', overrides)).toBe(true) + expect(keybindingMatchesAction('worktree.palette', ctrlK, 'win32', overrides)).toBe(false) + await expect( + window.api.keybindings.setAction({ actionId: 'worktree.palette', bindings: null }) + ).rejects.toThrow('conflicts') + }) + it('leaves an invalid Mac section intact and retries after repair', async () => { const { storage } = installBrowserGlobals('MacIntel') const raw = '{"platforms":{"darwin":null}}' @@ -112,14 +165,17 @@ describe('web keybindings preload API', () => { }) }) - it('keeps a fresh web profile on the new defaults after saving settings and restarting', async () => { - const { api, storage } = await installApi('MacIntel') - expect((await api.keybindings.get()).overrides).toEqual({}) - storage.setItem('orca.web.settings.v1', '{}') - const { installWebPreloadApi } = await import('./web-preload-api') - installWebPreloadApi() - expect((await window.api.keybindings.get()).overrides).toEqual({}) - }) + it.each(['MacIntel', 'Windows NT 10.0'])( + 'keeps a fresh %s web profile on the new defaults after saving settings and restarting', + async (userAgent) => { + const { api, storage } = await installApi(userAgent) + expect((await api.keybindings.get()).overrides).toEqual({}) + storage.setItem('orca.web.settings.v1', '{}') + const { installWebPreloadApi } = await import('./web-preload-api') + installWebPreloadApi() + expect((await window.api.keybindings.get()).overrides).toEqual({}) + } + ) it('does not overwrite unreadable shortcuts while upgrading an existing web profile', async () => { const { storage } = installBrowserGlobals('MacIntel') diff --git a/src/shared/keybindings-conflicts.test.ts b/src/shared/keybindings-conflicts.test.ts index 32d0c0b8bba..b2a0679e25f 100644 --- a/src/shared/keybindings-conflicts.test.ts +++ b/src/shared/keybindings-conflicts.test.ts @@ -5,10 +5,59 @@ import { findKeybindingConflicts, getEffectiveKeybindingsForAction, keybindingMatchesAction, - KEYBINDING_DEFINITIONS + KEYBINDING_DEFINITIONS, + resolveKeybindingOverrides, + getLegacySearchBindings } from './keybindings' describe('keybindings', () => { + it('yields fresh-profile search to a saved global Ctrl+K assignment', () => { + expect(resolveKeybindingOverrides('linux', { 'worktree.quickOpen': ['Ctrl+K'] }, {})).toEqual({ + 'worktree.palette': [], + 'worktree.quickOpen': ['Ctrl+K'] + }) + }) + + it('yields fresh-profile search to a saved terminal Ctrl+K assignment', () => { + expect(resolveKeybindingOverrides('win32', { 'terminal.clear': ['Ctrl+K'] }, {})).toEqual({ + 'worktree.palette': [], + 'terminal.clear': ['Ctrl+K'] + }) + }) + + it('keeps the inherited search chord available for terminal-first chat toggling', () => { + const overrides = resolveKeybindingOverrides('win32', {}, getLegacySearchBindings('win32')) + const ctrlShiftJ = { key: 'j', control: true, shift: true } + const terminalFirst = { context: 'terminal', terminalShortcutPolicy: 'terminal-first' } as const + expect( + keybindingMatchesAction( + 'terminal.toggleNativeChat', + ctrlShiftJ, + 'win32', + overrides, + terminalFirst + ) + ).toBe(true) + expect( + keybindingMatchesAction('worktree.palette', ctrlShiftJ, 'win32', overrides, terminalFirst) + ).toBe(false) + expect( + findKeybindingConflicts('win32', overrides, { includeMainProcessInterception: true }) + ).toEqual([]) + expect( + keybindingMatchesAction( + 'terminal.toggleNativeChat', + ctrlShiftJ, + 'win32', + { + ...overrides, + 'worktree.navigateDown': ['Ctrl+Shift+J'] + }, + terminalFirst + ) + ).toBe(false) + }) + it('uses overrides as the complete effective binding list for an action', () => { const overrides = { 'worktree.quickOpen': ['Ctrl+Alt+O', 'not-a-shortcut'] diff --git a/src/shared/keybindings/definitions-core-1.ts b/src/shared/keybindings/definitions-core-1.ts index fe0ccb37a56..bf28dee7192 100644 --- a/src/shared/keybindings/definitions-core-1.ts +++ b/src/shared/keybindings/definitions-core-1.ts @@ -36,11 +36,7 @@ export const KEYBINDING_DEFINITION_CORE_1: readonly KeybindingDefinition[] = [ group: 'Global', scope: 'global', searchKeywords: ['shortcut', 'global', 'worktree', 'switch', 'jump'], - defaultBindings: { - darwin: ['Mod+K'], - linux: ['Mod+Shift+J'], - win32: ['Mod+Shift+J'] - } + defaultBindings: platformBindings(['Mod+K']) }, { id: 'worktree.navigateUp', diff --git a/src/shared/keybindings/definitions-core-3.ts b/src/shared/keybindings/definitions-core-3.ts index bcc508e5236..2d6beb206da 100644 --- a/src/shared/keybindings/definitions-core-3.ts +++ b/src/shared/keybindings/definitions-core-3.ts @@ -260,7 +260,7 @@ export const KEYBINDING_DEFINITION_CORE_3: readonly KeybindingDefinition[] = [ group: 'Terminal Panes', scope: 'terminal', searchKeywords: ['shortcut', 'pane', 'clear'], - defaultBindings: { darwin: [], linux: ['Mod+K'], win32: ['Mod+K'] } + defaultBindings: platformBindings([]) }, { id: 'terminal.focusNextPane', diff --git a/src/shared/keybindings/definitions.ts b/src/shared/keybindings/definitions.ts index ba96a48d624..a524ccd70e2 100644 --- a/src/shared/keybindings/definitions.ts +++ b/src/shared/keybindings/definitions.ts @@ -6,6 +6,7 @@ import type { AgentTabActionId, KeybindingActionId, KeybindingDefinition, + KeybindingOverrides, PluginKeybindingActionId } from './types' import type { TuiAgent } from '../agent-launch/catalog/tui-agent' @@ -53,9 +54,11 @@ export const LEGACY_TAB_SWITCH_BINDINGS: Readonly>> = { - 'worktree.palette': ['Mod+J'], - 'terminal.clear': ['Mod+K'] +export function getLegacySearchBindings(platform: NodeJS.Platform): KeybindingOverrides { + return { + 'worktree.palette': [platform === 'darwin' ? 'Mod+J' : 'Mod+Shift+J'], + 'terminal.clear': ['Mod+K'] + } } export const DEFINITIONS_BY_ID = new Map( diff --git a/src/shared/keybindings/effective.ts b/src/shared/keybindings/effective.ts index 7ff483bfc90..06e09c0cd59 100644 --- a/src/shared/keybindings/effective.ts +++ b/src/shared/keybindings/effective.ts @@ -9,6 +9,7 @@ import { DEFINITIONS_BY_ID, KEYBINDING_DEFINITIONS, getKeybindingPlatform, + getLegacySearchBindings, isDigitIndexActionId } from './definitions' import { keybindingConflictIdentities } from './matching-key' @@ -84,6 +85,18 @@ export function bindingsOverlap( ) } +export function isDefaultKeybinding( + definition: KeybindingDefinition, + binding: string, + platform: NodeJS.Platform +): boolean { + // Previous defaults retain intentional sharing when profiles inherit or save them. + return [ + ...getDefaultBindings(definition, platform), + ...(getLegacySearchBindings(platform)[definition.id] ?? []) + ].some((ownDefault) => bindingsOverlap(definition, binding, definition, ownDefault, platform)) +} + export function getEffectiveKeybindingsForAction( actionId: KeybindingActionId, platform: NodeJS.Platform, @@ -124,9 +137,7 @@ export function getEffectiveKeybindingsForDefinition( getConfiguredBindings(other.id, platform, overrides, other).some( (claim) => bindingsOverlap(definition, binding, other, claim, platform) && - !getDefaultBindings(other, platform).some((ownDefault) => - bindingsOverlap(other, claim, other, ownDefault, platform) - ) + !isDefaultKeybinding(other, claim, platform) ) ) ) diff --git a/src/shared/keybindings/formatting.ts b/src/shared/keybindings/formatting.ts index 593788946c9..d96907f6f4c 100644 --- a/src/shared/keybindings/formatting.ts +++ b/src/shared/keybindings/formatting.ts @@ -14,6 +14,7 @@ import { getEffectiveKeybindingsForAction, getEffectiveKeybindingsForDefinition, getDefaultBindings, + isDefaultKeybinding, keybindingConflictGroups, bindingsOverlap, isKeybindingInterceptedBeforeBrowserChords, @@ -227,13 +228,7 @@ export function findKeybindingConflictsForDefinitions( ) || !Array.from(actionIds).every((otherId) => { const other = byId.get(otherId) - return ( - otherId === id || - (other && - getDefaultBindings(other, platform).some((ownDefault) => - bindingsOverlap(definition, binding, other, ownDefault, platform) - )) - ) + return otherId === id || (other && isDefaultKeybinding(other, binding, platform)) }) ) }) diff --git a/src/shared/keybindings/global-settings.ts b/src/shared/keybindings/global-settings.ts index 36643616a00..13c805bce1d 100644 --- a/src/shared/keybindings/global-settings.ts +++ b/src/shared/keybindings/global-settings.ts @@ -7,6 +7,8 @@ export type KeybindingGlobalSettings = { keybindings?: KeybindingOverrides /** Profile-owned defaults; legacy profiles keep their shortcuts without writing the shared file. */ tabSwitchKeybindingSeed?: 'pending' | 'done' | 'legacy' - /** The Mac search/clear rollout cohort, independent of the shared shortcut choices. */ + /** The original Mac search/clear rollout cohort. */ searchKeybindingSeed?: 'pending' | 'done' | 'legacy' + /** The later Windows/Linux rollout must not reuse the Mac cohort. */ + nonMacSearchKeybindingSeed?: 'pending' | 'done' | 'legacy' } diff --git a/src/shared/keybindings/profile-defaults.ts b/src/shared/keybindings/profile-defaults.ts index 1f4fbfb391a..3cd5cfd795a 100644 --- a/src/shared/keybindings/profile-defaults.ts +++ b/src/shared/keybindings/profile-defaults.ts @@ -2,7 +2,7 @@ import { findKeybindingConflicts } from './formatting' import { getKeybindingDefinition, isKeybindingActionId, - LEGACY_MAC_SEARCH_BINDINGS, + getLegacySearchBindings, LEGACY_TAB_SWITCH_BINDINGS } from './definitions' import { getDefaultBindings } from './effective' @@ -30,7 +30,15 @@ export function resolveKeybindingOverrides( for (const conflict of conflicts) { const identity = getKeybindingConflictIdentity(conflict.binding, platform) for (const actionId of conflict.actionIds) { - const bindings = inherited[actionId] + const definition = getKeybindingDefinition(actionId) + // Fresh profiles must also yield the new search default to shared explicit choices. + const bindings = + inherited[actionId] ?? + (actionId === 'worktree.palette' && + definition && + !Object.hasOwn(explicitOverrides, actionId) + ? getDefaultBindings(definition, platform) + : undefined) if (!bindings) { continue } @@ -61,7 +69,7 @@ export function getKeybindingResetBindings( // Persist rollout resets so another profile's inherited defaults cannot reappear. const migratedAction = Object.hasOwn(LEGACY_TAB_SWITCH_BINDINGS, actionId) || - (platform === 'darwin' && Object.hasOwn(LEGACY_MAC_SEARCH_BINDINGS, actionId)) + Object.hasOwn(getLegacySearchBindings(platform), actionId) const definition = getKeybindingDefinition(actionId) return migratedAction && definition ? getDefaultBindings(definition, platform) : null }