feat(keybindings): swap tab-switch chords to the common convention for new users

New installs now get the widespread mapping — Mod+Shift+[ / ] cycles across
all tabs, Mod+Alt+[ / ] cycles within the active tab type. Pre-existing installs
keep today's mapping: a one-time cohort seed (frozen on first launch via the
fileExistedOnLoad signal, mirroring the telemetry migration) pins the legacy
chords into keybindings.json, skipping any action the user already customized.

- shared registry: swap the four tab.*SameType / tab.*AllTypes defaults; export
  LEGACY_TAB_SWITCH_BINDINGS for the seed
- persistence: migrateTabSwitchKeybindings freezes the existing-vs-fresh cohort
  (tabSwitchKeybindingSeed = pending | done)
- keybinding-file: seedLegacyTabSwitchBindings writes the legacy pins into the
  active-platform section so Settings reset still works
- refresh stale default-chord comments
- tests for the swapped defaults, the seed (fresh/existing/customized/idempotent),
  and the cohort migration
This commit is contained in:
Brennan Benson
2026-07-17 15:44:52 -07:00
parent 3fd73ee848
commit 30e2fad672
11 changed files with 317 additions and 32 deletions
+7 -1
View File
@@ -1880,7 +1880,13 @@ app.whenReady().then(async () => {
rateLimits.setNetworkProxySettingsResolver(() => store!.getSettings())
keybindings = new KeybindingService({
homePath: app.getPath('home'),
getLegacyOverrides: () => store!.getSettings().keybindings
getLegacyOverrides: () => store!.getSettings().keybindings,
legacyTabSwitchSeed: {
isPending: () => store!.getSettings().tabSwitchKeybindingSeed === 'pending',
markSeeded: () => {
store!.updateSettings({ tabSwitchKeybindingSeed: 'done' })
}
}
})
browserManager.setSettingsResolver(() => ({ keybindings: keybindings?.getOverrides() }))
rateLimits.setInactiveClaudeAccountsResolver(() => {
@@ -2,10 +2,12 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { LEGACY_TAB_SWITCH_BINDINGS } from '../../shared/keybindings'
import {
getUserKeybindingsPath,
migrateLegacyKeybindings,
readKeybindingFile,
seedLegacyTabSwitchBindings,
writeKeybindingOverride
} from './keybinding-file'
@@ -224,4 +226,50 @@ describe('keybinding-file', () => {
'view.tasks': ['Ctrl+Alt+T']
})
})
it('seeds the legacy tab-switch chords into the active platform section', () => {
const result = seedLegacyTabSwitchBindings(filePath, 'darwin', LEGACY_TAB_SWITCH_BINDINGS)
expect(result.seeded).toBe(true)
const snapshot = readKeybindingFile(filePath, 'darwin')
// Written to the platform section so Settings reset still works normally.
expect(snapshot.platformOverrides.darwin).toEqual({
'tab.nextSameType': ['Mod+Shift+BracketRight'],
'tab.previousSameType': ['Mod+Shift+BracketLeft'],
'tab.nextAllTypes': ['Mod+Alt+BracketRight'],
'tab.previousAllTypes': ['Mod+Alt+BracketLeft']
})
// Effective bindings reproduce the pre-swap behavior with no conflicts dropped.
expect(snapshot.overrides).toMatchObject({
'tab.nextSameType': ['Mod+Shift+BracketRight'],
'tab.nextAllTypes': ['Mod+Alt+BracketRight']
})
expect(snapshot.diagnostics).toEqual([])
})
it('skips the seed when the user has already customized any swapped action', () => {
writeKeybindingOverride(filePath, 'darwin', 'tab.nextSameType', ['Mod+Alt+K'])
const result = seedLegacyTabSwitchBindings(filePath, 'darwin', LEGACY_TAB_SWITCH_BINDINGS)
expect(result.seeded).toBe(false)
// The user's choice is untouched and the other actions stay on their defaults.
expect(readKeybindingFile(filePath, 'darwin').overrides).toEqual({
'tab.nextSameType': ['Mod+Alt+K']
})
})
it('leaves unrelated overrides intact and stays idempotent when seeding', () => {
writeKeybindingOverride(filePath, 'darwin', 'terminal.search', ['Mod+Shift+F'])
const first = seedLegacyTabSwitchBindings(filePath, 'darwin', LEGACY_TAB_SWITCH_BINDINGS)
const second = seedLegacyTabSwitchBindings(filePath, 'darwin', LEGACY_TAB_SWITCH_BINDINGS)
expect(first.seeded).toBe(true)
// A second pass is a no-op: the pins now read as existing customization.
expect(second.seeded).toBe(false)
const snapshot = readKeybindingFile(filePath, 'darwin')
expect(snapshot.overrides['terminal.search']).toEqual(['Mod+Shift+F'])
expect(snapshot.overrides['tab.nextAllTypes']).toEqual(['Mod+Alt+BracketRight'])
})
})
+61
View File
@@ -318,6 +318,67 @@ export function migrateLegacyKeybindings(
writeJsonDocument(path, document)
}
/**
* Pin the pre-swap tab-switch chords for a pre-existing install so upgrading
* users keep the shortcuts they learned. Writes into the active-platform
* section (mirroring `writeKeybindingOverride`) so the seeded values stay
* resettable from Settings. No-op when the user has already customized any of
* the swapped actions — that is custom territory, and a half-swapped pin would
* be worse than leaving their choices alone.
*/
export function seedLegacyTabSwitchBindings(
path: string,
platform: NodeJS.Platform,
legacyBindings: Readonly<Partial<Record<KeybindingActionId, string[]>>>
): { seeded: boolean; snapshot: KeybindingFileSnapshot } {
const keybindingPlatform = getKeybindingPlatform(platform)
const actionIds = Object.keys(legacyBindings) as KeybindingActionId[]
const current = readKeybindingFile(path, platform)
const alreadyCustomized = actionIds.some(
(actionId) =>
Object.prototype.hasOwnProperty.call(current.commonOverrides, actionId) ||
PLATFORM_KEYS.some((os) =>
Object.prototype.hasOwnProperty.call(current.platformOverrides[os] ?? {}, actionId)
)
)
if (alreadyCustomized) {
return { seeded: false, snapshot: current }
}
const readResult = readJsonDocument(path)
const document = isJsonObject(readResult.document)
? { ...readResult.document }
: createEmptyDocument()
for (const rootKey of Object.keys(document)) {
if (isKeybindingActionId(rootKey)) {
delete document[rootKey]
}
}
const common = isJsonObject(document.keybindings) ? { ...document.keybindings } : {}
const platforms = isJsonObject(document.platforms) ? { ...document.platforms } : {}
const activePlatform = isJsonObject(platforms[keybindingPlatform])
? { ...(platforms[keybindingPlatform] as JsonObject) }
: {}
for (const actionId of actionIds) {
const normalized = normalizeKeybindingArrayForAction(actionId, legacyBindings[actionId] ?? [])
if (Array.isArray(normalized)) {
activePlatform[actionId] = normalized
}
}
document.version = FILE_VERSION
document.keybindings = common
document.platforms = {
...platforms,
darwin: isJsonObject(platforms.darwin) ? platforms.darwin : {},
linux: isJsonObject(platforms.linux) ? platforms.linux : {},
win32: isJsonObject(platforms.win32) ? platforms.win32 : {},
[keybindingPlatform]: activePlatform
}
writeJsonDocument(path, document)
return { seeded: true, snapshot: readKeybindingFile(path, platform) }
}
export function writeKeybindingOverride(
path: string,
platform: NodeJS.Platform,
+24 -4
View File
@@ -1,13 +1,15 @@
import type {
KeybindingActionId,
KeybindingFileSnapshot,
KeybindingOverrides
import {
LEGACY_TAB_SWITCH_BINDINGS,
type KeybindingActionId,
type KeybindingFileSnapshot,
type KeybindingOverrides
} from '../../shared/keybindings'
import {
ensureKeybindingFile,
getUserKeybindingsPath,
migrateLegacyKeybindings,
readKeybindingFile,
seedLegacyTabSwitchBindings,
writeKeybindingOverride
} from './keybinding-file'
@@ -15,6 +17,13 @@ export type KeybindingServiceOptions = {
homePath: string
platform?: NodeJS.Platform
getLegacyOverrides?: () => KeybindingOverrides | undefined
/** Cohort seed for the tab-switch convention swap. `isPending` is true only
* for pre-existing installs on the first launch after the swap; `markSeeded`
* freezes the one-shot so it never runs again. */
legacyTabSwitchSeed?: {
isPending: () => boolean
markSeeded: () => void
}
}
export class KeybindingService {
@@ -28,6 +37,17 @@ export class KeybindingService {
// Why: older builds persisted custom shortcuts inside global settings.
// Once a keybindings file exists, it is the sole source of truth.
migrateLegacyKeybindings(this.configPath, this.platform, options.getLegacyOverrides?.())
// Why: pre-existing installs keep the old tab-switch chords. Only mark the
// one-shot done on success so a transient IO failure retries next launch
// instead of silently dropping the pin.
if (options.legacyTabSwitchSeed?.isPending()) {
try {
seedLegacyTabSwitchBindings(this.configPath, this.platform, LEGACY_TAB_SWITCH_BINDINGS)
options.legacyTabSwitchSeed.markSeeded()
} catch (error) {
console.error('Failed to seed legacy tab-switch keybindings:', error)
}
}
}
getPath(): string {
+57
View File
@@ -9797,6 +9797,63 @@ describe('Store', () => {
})
})
describe('Store.migrateTabSwitchKeybindings', () => {
// Freezes the install cohort for the tab-switch convention swap on first load.
// Keys on `fileExistedOnLoad` (not field presence) so the verdict is stable
// even after a fresh install writes its own data file on later launches.
beforeEach(() => {
testState.dir = mkdtempSync(join(tmpdir(), 'orca-test-'))
})
afterEach(() => {
rmSync(testState.dir, { recursive: true, force: true })
})
it('marks a truly fresh install done so it adopts the new defaults', async () => {
const store = await createStore()
expect(store.getSettings().tabSwitchKeybindingSeed).toBe('done')
})
it('marks a pre-existing install pending so the legacy chords get seeded', async () => {
writeDataFile({
schemaVersion: 1,
repos: [makeRepo()],
worktreeMeta: {},
settings: { theme: 'dark' },
ui: {},
githubCache: { pr: {}, issue: {} },
workspaceSession: {}
})
const store = await createStore()
expect(store.getSettings().tabSwitchKeybindingSeed).toBe('pending')
expect(store.getSettings().theme).toBe('dark')
})
it('treats a corrupt data file as a pre-existing install', async () => {
mkdirSync(testState.dir, { recursive: true })
writeFileSync(dataFile(), '{{{corrupt json', 'utf-8')
const store = await createStore()
expect(store.getSettings().tabSwitchKeybindingSeed).toBe('pending')
})
it('preserves an already-frozen cohort on subsequent launches', async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: { tabSwitchKeybindingSeed: 'done' },
ui: {},
githubCache: { pr: {}, issue: {} },
workspaceSession: {}
})
const store = await createStore()
// Existing file but cohort already resolved to 'done' — must not flip to
// 'pending' just because the data file happens to exist.
expect(store.getSettings().tabSwitchKeybindingSeed).toBe('done')
})
})
describe('Store.migrateWorktreeIdentity', () => {
const OLD = 'repo1::/ws/cunner'
const NEW = 'repo1::/ws/worktree-creation-spinner'
+32 -1
View File
@@ -3544,7 +3544,10 @@ export class Store {
this.loadNeedsSave = true
}
const migrated = this.migrateTelemetry(result, fileExistedOnLoad)
const migrated = this.migrateTabSwitchKeybindings(
this.migrateTelemetry(result, fileExistedOnLoad),
fileExistedOnLoad
)
// githubCache lives in a sidecar file now (see getGithubCacheFile). A
// legacy in-file cache (pre-sidecar build, or a downgrade round-trip) is
@@ -3580,6 +3583,34 @@ export class Store {
// the banner resolves (the consent resolver returns `pending_banner`
// until then, so nothing transmits).
// - `installId` — anonymous UUID v4. Stable across launches; not surfaced in the UI.
// One-shot cohort freeze for the tab-switch keybinding convention swap. Runs
// on every `load()` but is a no-op once `tabSwitchKeybindingSeed` is set. The
// decision must be frozen on the first post-swap launch: `fileExistedOnLoad`
// only distinguishes existing vs fresh on that first run (a fresh install's
// data file exists on every subsequent launch), so persist the verdict now.
private migrateTabSwitchKeybindings(
state: PersistedState,
fileExistedOnLoad: boolean
): PersistedState {
const existing = state.settings?.tabSwitchKeybindingSeed
if (existing === 'pending' || existing === 'done') {
return state
}
// Why: mark dirty so the frozen cohort survives the next restart even if no
// other setting changes this session; without this a fresh install could be
// re-read as "existing" once its data file lands on disk.
this.loadNeedsSave = true
return {
...state,
settings: {
...state.settings,
// Existing installs pin the old chords via a keybindings.json seed;
// fresh installs need nothing beyond the new registry defaults.
tabSwitchKeybindingSeed: fileExistedOnLoad ? 'pending' : 'done'
}
}
}
private migrateTelemetry(state: PersistedState, fileExistedOnLoad: boolean): PersistedState {
const existing = state.settings?.telemetry
// Why: the one-shot is complete only when all three invariants hold.
@@ -44,8 +44,8 @@ type GetNextTabAcrossAllTypesParams = {
direction: number
}
// Why: companion to getNextTabWithinActiveType for the Cmd/Ctrl+Alt+Shift+]/[
// "cycle across every tab" chord. Keeps the same dual-id matching semantics
// Why: companion to getNextTabWithinActiveType for the "cycle across every tab"
// chord (Cmd/Ctrl+Shift+]/[ on fresh installs). Keeps the same dual-id matching semantics
// (prefer the active group's unified tabId to disambiguate split layouts, fall
// back to the backing entity id) so behavior matches what the TabBar renders.
export function getNextTabAcrossAllTypes({
+9 -8
View File
@@ -87,7 +87,9 @@ export function activateCyclableTab(store: AppStoreState, next: TypeCyclableTab)
}
/**
* Handle Cmd/Ctrl+Shift+[ / ] direction switching within the active tab type.
* Handle direction switching within the active tab type (same-type cycle).
* The default chord is Cmd/Ctrl+Alt+[ / ] for fresh installs; pre-swap installs
* keep Cmd/Ctrl+Shift+[ / ] via the seed migration, and either is rebindable.
* Extracted from useIpcEvents to keep file size under the max-lines lint threshold.
* Returns true if a tab switch occurred, false otherwise.
*/
@@ -114,14 +116,13 @@ export function handleSwitchTab(direction: number): boolean {
}
/**
* Handle Cmd/Ctrl+Alt+Shift+[ / ] cycling across every visible tab,
* regardless of tab type.
* Handle cycling across every visible tab, regardless of tab type.
*
* Why: companion chord to the type-scoped Cmd/Ctrl+Shift+[ / ] that ships as
* the default. The type-scoped chord is the VS Code-style per-pane cycle;
* this one gives users an escape hatch back to the pre-scope "cycle through
* everything" behavior without needing a settings toggle (see PR #1281
* discussion). Returns true if a tab switch occurred, false otherwise.
* Why: companion chord to the type-scoped same-type cycle. Fresh installs bind
* this broad cycle to Cmd/Ctrl+Shift+[ / ] — the widespread "switch tab" chord —
* and the same-type cycle to Cmd/Ctrl+Alt+[ / ]; pre-swap installs keep the
* inverse via the seed migration (see PR #1281 for the original type-scope
* rationale). Returns true if a tab switch occurred, false otherwise.
*/
export function handleSwitchTabAcrossAllTypes(direction: number): boolean {
const ctx = resolveCycleContext()
+52 -12
View File
@@ -12,6 +12,7 @@ import {
isDigitIndexActionId,
isDoubleTapBinding,
keybindingFromInput,
LEGACY_TAB_SWITCH_BINDINGS,
keybindingFromInputForAction,
keybindingMatchesAction,
keybindingMatchesInput,
@@ -443,6 +444,42 @@ describe('keybindings', () => {
])
})
it('defaults tab-switch chords to the swapped convention for fresh installs', () => {
// New users get the widespread mapping: Shift+bracket cycles all tabs,
// Alt+bracket cycles within the active type.
expect(getEffectiveKeybindingsForAction('tab.nextAllTypes', 'darwin')).toEqual([
'Mod+Shift+BracketRight'
])
expect(getEffectiveKeybindingsForAction('tab.previousAllTypes', 'darwin')).toEqual([
'Mod+Shift+BracketLeft'
])
expect(getEffectiveKeybindingsForAction('tab.nextSameType', 'darwin')).toEqual([
'Mod+Alt+BracketRight'
])
expect(getEffectiveKeybindingsForAction('tab.previousSameType', 'darwin')).toEqual([
'Mod+Alt+BracketLeft'
])
})
it('pins the pre-swap chords via LEGACY_TAB_SWITCH_BINDINGS for upgrading installs', () => {
// These are what the seed migration writes so pre-existing users keep the
// shortcuts they learned; overriding an action with its legacy value must
// reproduce the old effective binding.
expect(LEGACY_TAB_SWITCH_BINDINGS).toEqual({
'tab.nextSameType': ['Mod+Shift+BracketRight'],
'tab.previousSameType': ['Mod+Shift+BracketLeft'],
'tab.nextAllTypes': ['Mod+Alt+BracketRight'],
'tab.previousAllTypes': ['Mod+Alt+BracketLeft']
})
for (const [actionId, bindings] of Object.entries(LEGACY_TAB_SWITCH_BINDINGS)) {
expect(
getEffectiveKeybindingsForAction(actionId as KeybindingActionId, 'darwin', {
[actionId]: bindings
})
).toEqual(bindings)
}
})
it('defines browser history shortcuts for Logitech side-button remaps', () => {
expect(getEffectiveKeybindingsForAction('browser.back', 'darwin')).toEqual(['Mod+BracketLeft'])
expect(getEffectiveKeybindingsForAction('browser.forward', 'darwin')).toEqual([
@@ -1216,29 +1253,30 @@ describe('keybindings', () => {
expect(keybindingMatchesAction('terminal.focusNextPane', jisLeftBracket, 'darwin')).toBe(false)
expect(keybindingMatchesAction('terminal.focusNextPane', jisRightBracket, 'darwin')).toBe(true)
// Alt+bracket is the fresh-install same-type default after the convention swap.
expect(
keybindingMatchesAction('tab.previousAllTypes', { ...jisLeftBracket, alt: true }, 'darwin')
keybindingMatchesAction('tab.previousSameType', { ...jisLeftBracket, alt: true }, 'darwin')
).toBe(true)
expect(
keybindingMatchesAction('tab.nextAllTypes', { ...jisRightBracket, alt: true }, 'darwin')
keybindingMatchesAction('tab.nextSameType', { ...jisRightBracket, alt: true }, 'darwin')
).toBe(true)
expect(
keybindingMatchesAction(
'tab.previousAllTypes',
'tab.previousSameType',
{ ...jisLeftBracket, control: true, meta: false, alt: true },
'linux'
)
).toBe(true)
expect(
keybindingMatchesAction(
'tab.nextAllTypes',
'tab.nextSameType',
{ ...jisLeftBracket, control: true, meta: false, alt: true },
'linux'
)
).toBe(false)
expect(
keybindingMatchesAction(
'tab.nextAllTypes',
'tab.nextSameType',
{ ...jisRightBracket, control: true, meta: false, alt: true },
'linux'
)
@@ -1268,7 +1306,7 @@ describe('keybindings', () => {
expect(
keybindingMatchesAction(
'tab.previousAllTypes',
'tab.previousSameType',
{
key: '[',
code: 'Digit8',
@@ -1282,7 +1320,7 @@ describe('keybindings', () => {
).toBe(false)
expect(
keybindingMatchesAction(
'tab.previousAllTypes',
'tab.previousSameType',
{
key: 'Dead',
code: 'BracketLeft',
@@ -1385,7 +1423,9 @@ describe('keybindings', () => {
expect(formatKeybindingList(['DoubleTap+Shift'], 'linux')).toBe('Shift Shift')
})
it('matches macOS Option-composed bracket shortcuts for all-type tab switching', () => {
it('matches macOS Option-composed bracket shortcuts for same-type tab switching', () => {
// Cmd+Alt+bracket is the fresh-install same-type default after the swap, so
// Option-composed dead keys (\u2325[ -> "\u201c") must still resolve to that action.
const macOptionLeftBracket = {
key: '\u201c',
code: 'BracketLeft',
@@ -1403,12 +1443,12 @@ describe('keybindings', () => {
shift: false
}
expect(keybindingMatchesAction('tab.previousAllTypes', macOptionLeftBracket, 'darwin')).toBe(
expect(keybindingMatchesAction('tab.previousSameType', macOptionLeftBracket, 'darwin')).toBe(
true
)
expect(keybindingMatchesAction('tab.nextAllTypes', macOptionLeftBracket, 'darwin')).toBe(false)
expect(keybindingMatchesAction('tab.nextAllTypes', macOptionRightBracket, 'darwin')).toBe(true)
expect(keybindingMatchesAction('tab.previousAllTypes', macOptionRightBracket, 'darwin')).toBe(
expect(keybindingMatchesAction('tab.nextSameType', macOptionLeftBracket, 'darwin')).toBe(false)
expect(keybindingMatchesAction('tab.nextSameType', macOptionRightBracket, 'darwin')).toBe(true)
expect(keybindingMatchesAction('tab.previousSameType', macOptionRightBracket, 'darwin')).toBe(
false
)
})
+19 -4
View File
@@ -643,7 +643,10 @@ export const KEYBINDING_DEFINITIONS: readonly KeybindingDefinition[] = [
group: 'Tab Navigation',
scope: 'tabs',
searchKeywords: ['shortcut', 'tab', 'next', 'switch', 'cycle'],
defaultBindings: platformBindings(['Mod+Shift+BracketRight'])
// Why: Mod+Shift+Bracket is the widespread "switch tab" chord, so it now
// drives the broad all-types cycle for new users; same-type moves to
// Mod+Alt. Pre-release installs keep the old mapping via the seed migration.
defaultBindings: platformBindings(['Mod+Alt+BracketRight'])
},
{
id: 'tab.previousSameType',
@@ -651,7 +654,7 @@ export const KEYBINDING_DEFINITIONS: readonly KeybindingDefinition[] = [
group: 'Tab Navigation',
scope: 'tabs',
searchKeywords: ['shortcut', 'tab', 'previous', 'switch', 'cycle'],
defaultBindings: platformBindings(['Mod+Shift+BracketLeft'])
defaultBindings: platformBindings(['Mod+Alt+BracketLeft'])
},
{
id: 'tab.nextAllTypes',
@@ -659,7 +662,7 @@ export const KEYBINDING_DEFINITIONS: readonly KeybindingDefinition[] = [
group: 'Tab Navigation',
scope: 'tabs',
searchKeywords: ['shortcut', 'tab', 'next', 'switch', 'cycle', 'all', 'any'],
defaultBindings: platformBindings(['Mod+Alt+BracketRight'])
defaultBindings: platformBindings(['Mod+Shift+BracketRight'])
},
{
id: 'tab.previousAllTypes',
@@ -667,7 +670,7 @@ export const KEYBINDING_DEFINITIONS: readonly KeybindingDefinition[] = [
group: 'Tab Navigation',
scope: 'tabs',
searchKeywords: ['shortcut', 'tab', 'previous', 'switch', 'cycle', 'all', 'any'],
defaultBindings: platformBindings(['Mod+Alt+BracketLeft'])
defaultBindings: platformBindings(['Mod+Shift+BracketLeft'])
},
{
id: 'tab.previousRecent',
@@ -1070,6 +1073,18 @@ export const KEYBINDING_DEFINITIONS: readonly KeybindingDefinition[] = [
...buildAgentTabKeybindingDefinitions()
]
/**
* The tab-switch bindings as they shipped before the convention swap. A one-time
* migration pins these for pre-release installs so upgrading users keep the
* shortcuts they learned; fresh installs get the new registry defaults above.
*/
export const LEGACY_TAB_SWITCH_BINDINGS: Readonly<Partial<Record<KeybindingActionId, string[]>>> = {
'tab.nextSameType': ['Mod+Shift+BracketRight'],
'tab.previousSameType': ['Mod+Shift+BracketLeft'],
'tab.nextAllTypes': ['Mod+Alt+BracketRight'],
'tab.previousAllTypes': ['Mod+Alt+BracketLeft']
}
export function agentTabActionId(agent: TuiAgent): AgentTabActionId {
return `tab.newAgent.${agent}`
}
+6
View File
@@ -3042,6 +3042,12 @@ export type GlobalSettings = {
* false for fresh installs (no first-launch surface). */
existedBeforeTelemetryRelease: boolean
}
/** One-shot cohort marker for the tab-switch keybinding convention swap.
* Absent before the migration runs. Set once on first boot after the swap:
* `'pending'` for pre-existing installs (a seed then pins the old chords in
* keybindings.json before flipping this to `'done'`), or `'done'` for fresh
* installs, which adopt the new registry defaults with no seed. */
tabSwitchKeybindingSeed?: 'pending' | 'done'
/** Local voice/dictation configuration (Phase 1 voice feature). Optional
* because profiles created before voice landed won't have the key;
* `getDefaultSettings()` hydrates `getDefaultVoiceSettings()` via the