fix(macos): opt out of press-and-hold so held keys repeat (#14746)

macOS routes press-and-hold to the accent picker unless an app sets
ApplePressAndHoldEnabled=false for its own bundle, so holding j in vim
inserted one character instead of repeating. Orca never set it.

Written at most once, and never over an explicit value: `defaults read`
is domain-scoped and exits 1 when the key is absent, which is the only
way to tell "unset" from a deliberate false — Electron's
systemPreferences.getUserDefault reports false for both. A recorded
decision in userData keeps a later launch from re-clobbering a user who
deletes the key to get the accent picker back.
This commit is contained in:
Neil
2026-08-20 02:42:05 -07:00
parent c92f394cde
commit 740ea60c12
5 changed files with 725 additions and 0 deletions
+4
View File
@@ -3,6 +3,7 @@ import { existsSync, statSync } from 'node:fs'
import { isAbsolute, join } from 'node:path'
import os from 'node:os'
import { app, BrowserWindow, dialog, ipcMain, nativeTheme, powerMonitor, type Tray } from 'electron'
import { applyMacPressAndHoldDefaultAtStartup } from './macos-press-and-hold-default'
import { initTccPromptNotice, stopTccPromptNotice } from './macos-tcc-prompt-notice'
import { electronApp, is } from '@electron-toolkit/utils'
import {
@@ -857,6 +858,9 @@ if (hasSingleInstanceLock) {
installDevParentSignalQuit(shouldCoupleToDevParent)
// Why: run after configureDevUserDataPath but before app.setName('Orca') (whenReady), which changes the resolved path on case-sensitive filesystems.
initDataPath()
// Why here: AppKit reads ApplePressAndHoldEnabled as the process starts, so this is the last
// point that can still matter for this launch; it needs the canonical userData path above.
applyMacPressAndHoldDefaultAtStartup(getCanonicalUserDataPath())
// Why: use the canonical userData path — late app.getPath('userData') can resolve differently across restarts, defeating persistence.
initSessionParseCachePersistence({
filePath: join(getCanonicalUserDataPath(), 'ai-vault', 'session-parse-cache.json'),
@@ -0,0 +1,155 @@
import { execFileSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { rmSync } from 'node:fs'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
PRESS_AND_HOLD_KEY,
ensureMacPressAndHoldDefault,
interpretDefaultsReadFailure,
readDomainPressAndHoldPreference,
writeDomainPressAndHoldPreference,
type PressAndHoldHost,
type PressAndHoldRecord
} from './macos-press-and-hold-default'
/**
* Runs against the real `/usr/bin/defaults` on a throwaway Orca-owned domain.
*
* The whole design rests on one claim the mocks cannot make: a domain that has never been written
* is distinguishable from one explicitly set to `false`. Electron's `systemPreferences` cannot tell
* them apart, so if `defaults` could not either, "only write when unset" would be unimplementable.
*
* Every domain used here is a throwaway UUID under Orca's own prefix, deleted along with its plist
* in `afterEach`; the real `com.stablyai.orca` domain is never read or written.
*/
// Why a real Orca-owned domain shape: the ownership guard rejects anything else, so a fake prefix
// would exercise a different branch than production.
const domains: string[] = []
function throwawayDomain(): string {
const domain = `com.stablyai.orca.defaults-domain-test.${randomUUID()}`
domains.push(domain)
return domain
}
function write(domain: string, value: 'true' | 'false'): void {
execFileSync('/usr/bin/defaults', ['write', domain, PRESS_AND_HOLD_KEY, '-bool', value], {
stdio: 'ignore'
})
}
function rawRead(domain: string): string {
return execFileSync('/usr/bin/defaults', ['read', domain, PRESS_AND_HOLD_KEY], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
}).trim()
}
function hostFor(domain: string, record: PressAndHoldRecord | null = null): PressAndHoldHost {
return {
platform: 'darwin',
resolveBundleIdentifier: () => domain,
readRecord: () => record,
writeRecord: () => undefined,
readDomainPreference: readDomainPressAndHoldPreference,
writeDomainPreference: writeDomainPressAndHoldPreference,
now: () => new Date().toISOString()
}
}
describe.skipIf(process.platform !== 'darwin')(
'press-and-hold against the real defaults binary',
() => {
afterEach(() => {
for (const domain of domains.splice(0)) {
try {
execFileSync('/usr/bin/defaults', ['delete', domain], { stdio: 'ignore' })
} catch {
// The domain may never have been created; nothing to clean up.
}
// Why the file too: `defaults delete` empties the domain but leaves its plist behind, so
// without this every run litters the developer's ~/Library/Preferences.
rmSync(join(homedir(), 'Library', 'Preferences', `${domain}.plist`), { force: true })
}
})
it('tells an unset key apart from an explicit false', () => {
const unset = throwawayDomain()
const explicitlyFalse = throwawayDomain()
write(explicitlyFalse, 'false')
expect(readDomainPressAndHoldPreference(unset)).toBe('unset')
expect(readDomainPressAndHoldPreference(explicitlyFalse)).toBe('set')
expect(rawRead(explicitlyFalse)).toBe('0')
})
it('reports an explicit true as set', () => {
const domain = throwawayDomain()
write(domain, 'true')
expect(readDomainPressAndHoldPreference(domain)).toBe('set')
expect(rawRead(domain)).toBe('1')
})
it('writes false into a previously unset domain', () => {
const domain = throwawayDomain()
expect(ensureMacPressAndHoldDefault(hostFor(domain))).toBe('applied')
expect(rawRead(domain)).toBe('0')
})
it('leaves an explicit value untouched, in either direction', () => {
for (const value of ['true', 'false'] as const) {
const domain = throwawayDomain()
write(domain, value)
expect(ensureMacPressAndHoldDefault(hostFor(domain))).toBe('kept-user-preference')
expect(rawRead(domain)).toBe(value === 'true' ? '1' : '0')
}
})
it('refuses to call a failed probe "unset"', () => {
// A real spawn failure, not a hand-built object: `execFileSync` reports it with no exit status,
// which is precisely what must not be mistaken for "the key does not exist".
let spawnFailure: unknown
try {
execFileSync('/usr/bin/defaults-does-not-exist', ['read'], { stdio: 'ignore' })
} catch (error) {
spawnFailure = error
}
expect((spawnFailure as { status?: number | null }).status ?? null).toBe(null)
expect(interpretDefaultsReadFailure(spawnFailure)).toBe('unknown')
let missingKeyFailure: unknown
try {
execFileSync('/usr/bin/defaults', ['read', throwawayDomain(), PRESS_AND_HOLD_KEY], {
stdio: 'ignore'
})
} catch (error) {
missingKeyFailure = error
}
expect((missingKeyFailure as { status?: number }).status).toBe(1)
expect(interpretDefaultsReadFailure(missingKeyFailure)).toBe('unset')
})
it('sees the key disappear again after the user deletes it', () => {
const domain = throwawayDomain()
write(domain, 'false')
execFileSync('/usr/bin/defaults', ['delete', domain, PRESS_AND_HOLD_KEY], { stdio: 'ignore' })
expect(readDomainPressAndHoldPreference(domain)).toBe('unset')
// A launch that already decided must not rewrite it, which is what makes the delete stick.
const decided: PressAndHoldRecord = {
version: 1,
decision: 'applied',
domain,
decidedAt: '2026-01-01T00:00:00.000Z'
}
expect(ensureMacPressAndHoldDefault(hostFor(domain, decided))).toBe('already-decided')
expect(readDomainPressAndHoldPreference(domain)).toBe('unset')
})
}
)
@@ -0,0 +1,240 @@
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
ensureMacPressAndHoldDefault,
isOrcaPreferencesDomain,
readBundleIdentifierFromExecutablePath,
type PressAndHoldDecision,
type PressAndHoldHost,
type PressAndHoldRecord
} from './macos-press-and-hold-default'
const ORCA_DOMAIN = 'com.stablyai.orca'
type HostOverrides = Partial<PressAndHoldHost> & { record?: PressAndHoldRecord | null }
/** A macOS host whose domain has no explicit value — the state every affected user is in. */
function createHost(overrides: HostOverrides = {}): {
host: PressAndHoldHost
writes: { domain: string; value: boolean }[]
records: PressAndHoldRecord[]
} {
const writes: { domain: string; value: boolean }[] = []
const records: PressAndHoldRecord[] = []
let stored = overrides.record ?? null
const host: PressAndHoldHost = {
platform: 'darwin',
resolveBundleIdentifier: () => ORCA_DOMAIN,
readRecord: () => stored,
writeRecord: (record) => {
stored = record
records.push(record)
},
readDomainPreference: () => 'unset',
writeDomainPreference: (domain, value) => {
writes.push({ domain, value })
return true
},
now: () => '2026-08-20T00:00:00.000Z',
...overrides
}
return { host, writes, records }
}
function terminalRecord(decision: PressAndHoldDecision): PressAndHoldRecord {
return { version: 1, decision, domain: ORCA_DOMAIN, decidedAt: '2026-01-01T00:00:00.000Z' }
}
describe('ensureMacPressAndHoldDefault', () => {
it('writes the opt-out when the domain has no explicit value', () => {
const { host, writes, records } = createHost()
expect(ensureMacPressAndHoldDefault(host)).toBe('applied')
expect(writes).toEqual([{ domain: ORCA_DOMAIN, value: false }])
expect(records.at(-1)?.decision).toBe('applied')
})
describe('platform guard', () => {
for (const platform of ['win32', 'linux'] as const) {
it(`does nothing at all on ${platform}`, () => {
const probe = vi.fn(() => 'unset' as const)
const readRecord = vi.fn(() => null)
const { host, writes, records } = createHost({
platform,
readDomainPreference: probe,
readRecord
})
expect(ensureMacPressAndHoldDefault(host)).toBe('not-macos')
// Why assert the reads too: "inert" means no `defaults` subprocess and no userData I/O,
// not merely no write.
expect(probe).not.toHaveBeenCalled()
expect(readRecord).not.toHaveBeenCalled()
expect(writes).toEqual([])
expect(records).toEqual([])
})
}
})
describe('respects an explicit user setting', () => {
it('leaves the domain alone when the user already set a value', () => {
// One arm covers both `true` and `false`: the probe deliberately reports only presence, so
// whichever way the user's value points it is theirs. That the probe really does report
// presence for both is pinned against the real binary in the .defaults-domain test.
const { host, writes, records } = createHost({ readDomainPreference: () => 'set' })
expect(ensureMacPressAndHoldDefault(host)).toBe('kept-user-preference')
expect(writes).toEqual([])
expect(records.at(-1)?.decision).toBe('kept-user-preference')
})
it('never re-applies after a launch already decided', () => {
for (const decision of ['applied', 'kept-user-preference'] as const) {
const probe = vi.fn(() => 'unset' as const)
const { host, writes } = createHost({
record: terminalRecord(decision),
readDomainPreference: probe
})
expect(ensureMacPressAndHoldDefault(host)).toBe('already-decided')
expect(probe).not.toHaveBeenCalled()
expect(writes).toEqual([])
}
})
it('stays out of the way after the user deletes the key we wrote', () => {
// The documented way back to the accent picker is `defaults delete`, which returns the
// domain to "unset". Without the recorded decision the next launch would rewrite it.
const { host, writes } = createHost({
record: terminalRecord('applied'),
readDomainPreference: () => 'unset'
})
expect(ensureMacPressAndHoldDefault(host)).toBe('already-decided')
expect(writes).toEqual([])
})
})
describe('domains we do not own', () => {
it('skips a bare Electron bundle rather than writing into a shared domain', () => {
const probe = vi.fn(() => 'unset' as const)
const { host, writes, records } = createHost({
resolveBundleIdentifier: () => 'com.github.Electron',
readDomainPreference: probe
})
expect(ensureMacPressAndHoldDefault(host)).toBe('foreign-bundle')
expect(probe).not.toHaveBeenCalled()
expect(writes).toEqual([])
expect(records.at(-1)?.domain).toBe('com.github.Electron')
})
it('skips when the bundle identifier cannot be resolved', () => {
const { host, writes } = createHost({ resolveBundleIdentifier: () => null })
expect(ensureMacPressAndHoldDefault(host)).toBe('foreign-bundle')
expect(writes).toEqual([])
})
it('accepts Orca and its channel-scoped bundles, and nothing else', () => {
expect(isOrcaPreferencesDomain('com.stablyai.orca')).toBe(true)
expect(isOrcaPreferencesDomain('com.stablyai.orca.dev')).toBe(true)
expect(isOrcaPreferencesDomain('com.github.Electron')).toBe(false)
// Why: a prefix test without the dot would accept a lookalike bundle id.
expect(isOrcaPreferencesDomain('com.stablyai.orcafake')).toBe(false)
})
})
describe('failures leave the decision open', () => {
it('does not write when the probe cannot tell unset from user-set', () => {
const { host, writes, records } = createHost({ readDomainPreference: () => 'unknown' })
expect(ensureMacPressAndHoldDefault(host)).toBe('probe-failed')
expect(writes).toEqual([])
expect(records.at(-1)?.decision).toBe('probe-failed')
})
it('retries on the next launch after a failed write', () => {
const { host, records } = createHost({ writeDomainPreference: () => false })
expect(ensureMacPressAndHoldDefault(host)).toBe('write-failed')
const retry = createHost({ record: records.at(-1) })
expect(ensureMacPressAndHoldDefault(retry.host)).toBe('applied')
expect(retry.writes).toEqual([{ domain: ORCA_DOMAIN, value: false }])
})
})
it('does not rewrite an unchanged non-terminal record', () => {
const first = createHost({ resolveBundleIdentifier: () => 'com.github.Electron' })
expect(ensureMacPressAndHoldDefault(first.host)).toBe('foreign-bundle')
const second = createHost({
record: first.records.at(-1),
resolveBundleIdentifier: () => 'com.github.Electron'
})
expect(ensureMacPressAndHoldDefault(second.host)).toBe('foreign-bundle')
expect(second.records).toEqual([])
})
})
describe('readBundleIdentifierFromExecutablePath', () => {
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
function bundleWithPlist(body: string): string {
const root = mkdtempSync(join(tmpdir(), 'orca-press-hold-'))
roots.push(root)
mkdirSync(join(root, 'Orca.app', 'Contents', 'MacOS'), { recursive: true })
writeFileSync(join(root, 'Orca.app', 'Contents', 'Info.plist'), body)
return join(root, 'Orca.app', 'Contents', 'MacOS', 'Orca')
}
it('reads CFBundleIdentifier from the plist beside the executable', () => {
const exe = bundleWithPlist(
'<plist><dict>\n<key>CFBundleName</key>\n<string>Orca</string>\n' +
'<key>CFBundleIdentifier</key>\n\t<string>com.stablyai.orca</string>\n</dict></plist>'
)
expect(readBundleIdentifierFromExecutablePath(exe)).toBe('com.stablyai.orca')
})
it('returns null when the plist is missing or carries no identifier', () => {
expect(readBundleIdentifierFromExecutablePath('/nonexistent/App.app/Contents/MacOS/App')).toBe(
null
)
expect(readBundleIdentifierFromExecutablePath(bundleWithPlist('<plist><dict/></plist>'))).toBe(
null
)
expect(
readBundleIdentifierFromExecutablePath(
bundleWithPlist('<key>CFBundleIdentifier</key><string></string>')
)
).toBe(null)
})
})
describe('startup wiring', () => {
const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8')
it('runs before app.whenReady(), which is the last point AppKit could still see it', () => {
const callIndex = source.indexOf(
'applyMacPressAndHoldDefaultAtStartup(getCanonicalUserDataPath())'
)
const initDataPathIndex = source.indexOf('initDataPath()')
const readyIndex = source.indexOf('app.whenReady().then(')
expect(callIndex).toBeGreaterThanOrEqual(0)
expect(readyIndex).toBeGreaterThanOrEqual(0)
// Why after initDataPath: the record lives beside orca-data.json, and the canonical userData
// path is only captured there.
expect(callIndex).toBeGreaterThan(initDataPathIndex)
expect(callIndex).toBeLessThan(readyIndex)
})
})
+230
View File
@@ -0,0 +1,230 @@
import { execFileSync } from 'node:child_process'
import { mkdirSync, readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { writeFileAtomically } from './codex-accounts/fs-utils'
/**
* Turns off the macOS accent picker for Orca's own preferences domain (#14746).
*
* macOS routes press-and-hold to the accent popup unless an app opts out via
* `ApplePressAndHoldEnabled`, so holding `j` in vim inserts one character instead of repeating.
* The key is unset by default, which is why every terminal-hosting Mac app ships this opt-out.
*
* Written once and never again: a user who wants the accent picker back sets
* `defaults write com.stablyai.orca ApplePressAndHoldEnabled -bool true` (or deletes the key), and
* the recorded decision below keeps a later launch from overwriting that choice.
*
* AppKit reads the preference while the process starts, so a fresh write lands for the *next*
* launch, not the current one.
*/
export const PRESS_AND_HOLD_KEY = 'ApplePressAndHoldEnabled'
export const PRESS_AND_HOLD_RECORD_FILE = 'macos-press-and-hold-default.json'
export const PRESS_AND_HOLD_RECORD_VERSION = 1
/** Why not `systemPreferences.getUserDefault`: it reads through the whole NSUserDefaults search
* list and is typed non-nullable, so an unset key and an explicit `false` both come back `false` —
* and the system default is unset, so it reports `false` on a Mac where press-and-hold is on.
* `defaults read <domain> <key>` is domain-scoped and exits 1 when the key is absent. */
const DEFAULTS_BINARY = '/usr/bin/defaults'
const DEFAULTS_TIMEOUT_MS = 5_000
/** Why: `defaults` exits 1 for "does not exist"; other statuses mean the probe itself failed. */
const DEFAULTS_MISSING_STATUS = 1
const ORCA_BUNDLE_ID = 'com.stablyai.orca'
export type PressAndHoldDecision =
/** Not macOS — nothing is read or written. */
| 'not-macos'
/** A previous launch already decided; the domain is never touched again. */
| 'already-decided'
/** The running bundle is not Orca's (e.g. a bare `Electron.app`), whose domain we do not own. */
| 'foreign-bundle'
/** `defaults read` could not answer, so we cannot tell an unset key from a user's choice. */
| 'probe-failed'
/** `defaults write` failed. */
| 'write-failed'
/** The domain already carries an explicit value; the user's choice stands. */
| 'kept-user-preference'
/** The key was unset and we wrote `false`. */
| 'applied'
/** Decisions about the domain itself. Anything else is a condition that can change by next launch. */
const TERMINAL_DECISIONS = new Set<PressAndHoldDecision>(['applied', 'kept-user-preference'])
export type PressAndHoldRecord = {
version: number
decision: PressAndHoldDecision
domain: string | null
decidedAt: string
}
export type PressAndHoldHost = {
platform: NodeJS.Platform
resolveBundleIdentifier: () => string | null
readRecord: () => PressAndHoldRecord | null
writeRecord: (record: PressAndHoldRecord) => void
readDomainPreference: (domain: string) => 'set' | 'unset' | 'unknown'
writeDomainPreference: (domain: string, value: boolean) => boolean
now: () => string
}
/** Only Orca's own bundle: an unpackaged run is `com.github.Electron`, shared with every other
* unpackaged Electron app on the machine. */
export function isOrcaPreferencesDomain(domain: string): boolean {
return domain === ORCA_BUNDLE_ID || domain.startsWith(`${ORCA_BUNDLE_ID}.`)
}
/** `<bundle>/Contents/MacOS/<exe>` → `<bundle>/Contents/Info.plist`. */
export function readBundleIdentifierFromExecutablePath(execPath: string): string | null {
try {
const plist = readFileSync(join(dirname(dirname(execPath)), 'Info.plist'), 'utf8')
const match = /<key>CFBundleIdentifier<\/key>\s*<string>([^<]*)<\/string>/.exec(plist)
const identifier = match?.[1]?.trim()
return identifier ? identifier : null
} catch {
return null
}
}
/**
* Why this distinction carries the whole design: only a real "does not exist" answer may be read as
* unset. A spawn failure or a timeout leaves `status` unset, and calling that unset would let a
* broken probe overwrite a value the user chose.
*/
export function interpretDefaultsReadFailure(error: unknown): 'unset' | 'unknown' {
return (error as { status?: number | null }).status === DEFAULTS_MISSING_STATUS
? 'unset'
: 'unknown'
}
export function readDomainPressAndHoldPreference(domain: string): 'set' | 'unset' | 'unknown' {
try {
execFileSync(DEFAULTS_BINARY, ['read', domain, PRESS_AND_HOLD_KEY], {
encoding: 'utf8',
timeout: DEFAULTS_TIMEOUT_MS,
stdio: ['ignore', 'pipe', 'ignore']
})
return 'set'
} catch (error) {
return interpretDefaultsReadFailure(error)
}
}
export function writeDomainPressAndHoldPreference(domain: string, value: boolean): boolean {
try {
execFileSync(
DEFAULTS_BINARY,
['write', domain, PRESS_AND_HOLD_KEY, '-bool', value ? 'true' : 'false'],
{ encoding: 'utf8', timeout: DEFAULTS_TIMEOUT_MS, stdio: ['ignore', 'ignore', 'ignore'] }
)
return true
} catch {
return false
}
}
export function pressAndHoldRecordPath(userDataPath: string): string {
return join(userDataPath, PRESS_AND_HOLD_RECORD_FILE)
}
function parseRecord(raw: string): PressAndHoldRecord | null {
const parsed = JSON.parse(raw) as Partial<PressAndHoldRecord>
if (parsed.version !== PRESS_AND_HOLD_RECORD_VERSION || typeof parsed.decision !== 'string') {
return null
}
return {
version: PRESS_AND_HOLD_RECORD_VERSION,
decision: parsed.decision as PressAndHoldDecision,
domain: typeof parsed.domain === 'string' ? parsed.domain : null,
decidedAt: typeof parsed.decidedAt === 'string' ? parsed.decidedAt : ''
}
}
/**
* Apply Orca's press-and-hold default at most once, leaving any explicit user value alone.
*
* Returns the decision so startup can log it; the same value is persisted for support triage.
*/
export function ensureMacPressAndHoldDefault(host: PressAndHoldHost): PressAndHoldDecision {
// Why first and without touching disk: Windows and Linux must do no work at all here.
if (host.platform !== 'darwin') {
return 'not-macos'
}
const previous = host.readRecord()
if (previous && TERMINAL_DECISIONS.has(previous.decision)) {
return 'already-decided'
}
const record = (decision: PressAndHoldDecision, domain: string | null): PressAndHoldDecision => {
// Why skip an unchanged rewrite: the non-terminal decisions are re-evaluated every launch.
if (!previous || previous.decision !== decision || previous.domain !== domain) {
host.writeRecord({
version: PRESS_AND_HOLD_RECORD_VERSION,
decision,
domain,
decidedAt: host.now()
})
}
return decision
}
const domain = host.resolveBundleIdentifier()
if (!domain || !isOrcaPreferencesDomain(domain)) {
return record('foreign-bundle', domain)
}
const existing = host.readDomainPreference(domain)
if (existing === 'unknown') {
return record('probe-failed', domain)
}
if (existing === 'set') {
return record('kept-user-preference', domain)
}
if (!host.writeDomainPreference(domain, false)) {
return record('write-failed', domain)
}
return record('applied', domain)
}
/** Why log: silently rewriting a macOS default for the user's login account should leave a trace,
* and the failure paths are otherwise invisible. The quiet decisions are the steady state. */
const REPORTED_DECISIONS: Partial<Record<PressAndHoldDecision, string>> = {
applied: `set ${PRESS_AND_HOLD_KEY}=false so held keys repeat; takes effect next launch`,
'probe-failed': `could not read ${PRESS_AND_HOLD_KEY}; leaving it alone`,
'write-failed': `could not write ${PRESS_AND_HOLD_KEY}; held keys will not repeat`
}
/** Wires {@link ensureMacPressAndHoldDefault} to the real bundle, `defaults`, and userData. */
export function applyMacPressAndHoldDefaultAtStartup(userDataPath: string): PressAndHoldDecision {
const recordPath = pressAndHoldRecordPath(userDataPath)
const decision = ensureMacPressAndHoldDefault({
platform: process.platform,
resolveBundleIdentifier: () => readBundleIdentifierFromExecutablePath(process.execPath),
readRecord: () => {
try {
return parseRecord(readFileSync(recordPath, 'utf8'))
} catch {
return null
}
},
writeRecord: (record) => {
try {
// Why: this runs before `ready`, and Electron has not necessarily created userData yet.
mkdirSync(userDataPath, { recursive: true })
writeFileAtomically(recordPath, `${JSON.stringify(record, null, 2)}\n`)
} catch {
// Best-effort: an unwritten record only costs one repeated probe next launch.
}
},
readDomainPreference: readDomainPressAndHoldPreference,
writeDomainPreference: writeDomainPressAndHoldPreference,
now: () => new Date().toISOString()
})
const reported = REPORTED_DECISIONS[decision]
if (reported) {
console.log(`[press-and-hold] ${reported}`)
}
return decision
}
@@ -0,0 +1,96 @@
/**
* Startup wiring for the macOS press-and-hold opt-out (#14746).
*
* What this can prove: the routine runs during startup, resolves the *real* running bundle's
* identifier from its Info.plist, records a decision, and stays completely inert off macOS.
*
* What it cannot prove: that held keys repeat. Key repeat versus the accent picker is decided by
* AppKit from a preference it reads as the process starts, so observing it needs a relaunch and a
* physically held key — neither of which Playwright's synthesized CDP key events go through.
* The preference semantics the fix depends on are pinned against the real `/usr/bin/defaults` in
* src/main/macos-press-and-hold-default.defaults-domain.test.ts.
*
* The E2E app runs unpackaged, so its bundle identifier is Electron's own rather than Orca's. That
* is the ownership guard's case, and asserting it here is the point: `defaults` resolves the user's
* real home regardless of the harness's HOME isolation, so a run that wrote anything would be
* writing into a domain shared with every other unpackaged Electron app on the machine.
*/
import type { ElectronApplication } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
test.use({ seedTestRepo: false })
type StartupState = {
recordRaw: string | null
bundleIdentifier: string | null
execPath: string
}
async function readPressAndHoldStartupState(
electronApp: ElectronApplication
): Promise<StartupState> {
return electronApp.evaluate(({ app }) => {
const nodeFs = process.getBuiltinModule('node:fs')
const nodePath = process.getBuiltinModule('node:path')
const recordPath = nodePath.join(app.getPath('userData'), 'macos-press-and-hold-default.json')
const read = (file: string): string | null => {
try {
return nodeFs.readFileSync(file, 'utf8')
} catch {
return null
}
}
// Why resolved here rather than hardcoded: this is the independent half of the cross-check —
// the spec derives the identifier from the launched bundle without reusing product code.
const plist = read(
nodePath.join(nodePath.dirname(nodePath.dirname(process.execPath)), 'Info.plist')
)
const match = plist
? /<key>CFBundleIdentifier<\/key>\s*<string>([^<]*)<\/string>/.exec(plist)
: null
return {
recordRaw: read(recordPath),
bundleIdentifier: match ? match[1].trim() : null,
execPath: process.execPath
}
})
}
test.describe('macOS press-and-hold default', () => {
test.skip(process.platform !== 'darwin', 'macOS-only startup routine')
test('records a decision for the bundle it is actually running from', async ({ electronApp }) => {
const state = await readPressAndHoldStartupState(electronApp)
expect(state.recordRaw, 'startup must leave a decision record').not.toBeNull()
const record = JSON.parse(state.recordRaw!) as {
version: number
decision: string
domain: string | null
}
expect(record.version).toBe(1)
expect(state.bundleIdentifier).not.toBeNull()
expect(record.domain).toBe(state.bundleIdentifier)
const ownsDomain =
record.domain === 'com.stablyai.orca' || record.domain!.startsWith('com.stablyai.orca.')
if (ownsDomain) {
// A packaged or dev-identity bundle: the write path is live and must have settled.
expect(['applied', 'kept-user-preference']).toContain(record.decision)
} else {
// The unpackaged harness. Anything but a refusal means Orca wrote into a foreign domain.
expect(record.decision).toBe('foreign-bundle')
}
})
})
test.describe('off macOS', () => {
test.skip(process.platform === 'darwin', 'covers the non-macOS branch')
test('writes nothing at all', async ({ electronApp }) => {
const state = await readPressAndHoldStartupState(electronApp)
expect(state.recordRaw).toBeNull()
})
})