Merge remote-tracking branch 'origin/main' into brennanb2025/sta-3919-mobile-picker-lag

This commit is contained in:
Brennan Benson
2026-08-27 13:52:21 -07:00
12 changed files with 1171 additions and 8 deletions
+1
View File
@@ -103,6 +103,7 @@ docs/**
!docs/reference/git-compatibility.md
!docs/reference/headless-linux-server.md
!docs/reference/linux-glibc-compatibility.md
!docs/reference/macos-press-and-hold.md
!docs/reference/orcad-operations.md
!docs/reference/relay-grace-time-reconfiguration.md
!docs/reference/windows-process-enumeration.md
+58
View File
@@ -0,0 +1,58 @@
# macOS press-and-hold and key repeat
macOS opens the accent picker when a key is held unless an application opts out in its preferences
domain. That prevents held keys from repeating in terminal applications such as vim. On the first
eligible launch, Orca writes:
```sh
defaults write com.stablyai.orca ApplePressAndHoldEnabled -bool false
```
The write is scoped to Orca's packaged bundle domain. Bare Electron development bundles and
non-macOS platforms are left untouched. A fresh write is conservatively treated as taking effect
on the next launch.
## Precedence and decision record
Orca checks for an explicit domain value before writing. Either `true` or `false` is treated as a
user choice and preserved. Only an unset key receives the `false` default.
The decision is stored once in
`<userData>/macos-press-and-hold-default.json`. An `applied` or
`kept-user-preference` decision prevents future launches from touching the domain again.
Probe and write failures remain retryable so a transient failure does not permanently disable the
fix.
`defaults read <domain> <key>` is used instead of
`systemPreferences.getUserDefault`: the Electron API cannot distinguish an unset key from an
explicit `false`. Only the missing-key exit status is interpreted as unset; spawn failures,
timeouts, and other exit statuses leave the preference alone.
## Restoring the accent picker
Set the preference explicitly, then restart Orca:
```sh
defaults write com.stablyai.orca ApplePressAndHoldEnabled -bool true
```
After Orca has recorded its one-time decision, deleting the key also restores the macOS default
without Orca recreating it:
```sh
defaults delete com.stablyai.orca ApplePressAndHoldEnabled
```
Development and prerelease channels may use a channel-suffixed Orca bundle identifier; use that
domain instead when applicable.
## Reverting
Deleting the startup code is not enough. AppKit reads the persisted preference, so a code revert
must also arrange to delete the key for users who ran an affected build.
## Test coverage
Unit tests cover platform guards, explicit-value preservation, retry behavior, domain ownership,
record persistence, and subprocess exit interpretation on CI. A macOS-only test additionally pins
the real `defaults(1)` behavior, but current PR CI does not execute tests on macOS.
+4
View File
@@ -13,6 +13,7 @@ import {
type Tray,
session
} 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 {
@@ -938,6 +939,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: initDataPath above gives the canonical userData path for the record file; the write
// itself lands for the next launch (see macos-press-and-hold-default.ts).
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,173 @@
import { execFileSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { chmodSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
import { homedir, tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { runProcessSync } from '../shared/child-process/run-process'
import {
PRESS_AND_HOLD_KEY,
ensureMacPressAndHoldDefault,
interpretDefaultsRead,
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[] = []
/** Pinned against the real binary below, then reused by the platform-agnostic unit tests. */
const DEFAULTS_MISSING_EXIT_CODE = 1
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"', () => {
// Real runs, not hand-built results: a binary that is not there must come back 'unknown'
// through the same path production takes, not be mistaken for "the key does not exist".
const spawnFailure = (): ReturnType<typeof runProcessSync> =>
runProcessSync({
program: '/usr/bin/defaults-does-not-exist',
args: ['read'],
stdio: ['ignore', 'ignore', 'ignore']
})
expect(spawnFailure).toThrow()
expect(interpretDefaultsRead(spawnFailure)).toBe('unknown')
const missingKey = runProcessSync({
program: '/usr/bin/defaults',
args: ['read', throwawayDomain(), PRESS_AND_HOLD_KEY],
stdio: ['ignore', 'ignore', 'ignore']
})
// The one exit code the whole design reads as unset; every other outcome is 'unknown'.
expect(missingKey.code).toBe(DEFAULTS_MISSING_EXIT_CODE)
expect(missingKey.timedOut).toBe(false)
expect(interpretDefaultsRead(() => missingKey)).toBe('unset')
})
// Skipped as root, where the read-only directory below would still be writable.
it.skipIf(process.getuid?.() === 0)('reports a write the binary refused', () => {
// Why a real refusal: `defaults write` exits non-zero instead of throwing, so a caller that
// only caught exceptions would record 'applied' for a value that never reached the plist.
const readOnly = join(mkdtempSync(join(tmpdir(), 'orca-press-hold-ro-')), 'locked')
mkdirSync(readOnly)
chmodSync(readOnly, 0o500)
try {
expect(writeDomainPressAndHoldPreference(join(readOnly, 'domain'), false)).toBe(false)
} finally {
chmodSync(readOnly, 0o700)
rmSync(readOnly, { recursive: true, force: true })
}
})
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,286 @@
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 type { ProcessResult } from '../shared/child-process/run-process'
import {
ensureMacPressAndHoldDefault,
interpretDefaultsRead,
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([])
})
})
/**
* Why here and not only in the .defaults-domain file: that file is macOS-only and CI has no macOS
* runner, so this three-way rule would otherwise be enforced nowhere in CI. These cases need no
* `defaults` binary — the sibling file pins that the real binary produces them.
*/
describe('interpretDefaultsRead', () => {
function probe(overrides: Partial<ProcessResult>): () => ProcessResult {
return () => ({ code: 0, signal: null, stdout: '', stderr: '', timedOut: false, ...overrides })
}
it('reads a clean exit as an explicit value', () => {
expect(interpretDefaultsRead(probe({ code: 0 }))).toBe('set')
})
it('reads only "does not exist" as unset', () => {
expect(interpretDefaultsRead(probe({ code: 1 }))).toBe('unset')
})
describe('refuses to call a failed probe unset', () => {
// Each of these would otherwise overwrite a value the user deliberately set.
const failures: [string, () => ProcessResult][] = [
['killed before it exited', probe({ code: null, signal: 'SIGTERM' })],
['timed out', probe({ code: null, timedOut: true })],
[
'timed out after somehow reporting the missing-key code',
probe({ code: 1, timedOut: true })
],
['exited with some other error', probe({ code: 2 })],
['exited with a negative status', probe({ code: -1 })],
[
'never started at all',
() => {
throw Object.assign(new Error('spawn ENOENT'), { code: 'ENOENT' })
}
]
]
for (const [name, failing] of failures) {
it(name, () => {
expect(interpretDefaultsRead(failing)).toBe('unknown')
})
}
})
})
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)
})
})
+257
View File
@@ -0,0 +1,257 @@
import { mkdirSync, readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { runProcessSync, type ProcessResult } from '../shared/child-process/run-process'
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.
*
* A fresh write is assumed to land for the *next* launch, not the current one: it goes out through
* a separate `defaults` process, so this app's own cached copy may not observe it. Whether AppKit
* re-reads sooner is untested, so the conservative assumption is the one encoded here.
*
* If this is ever reverted, delete the key as well — AppKit reads the plist, not this file, so
* removing the code alone leaves press-and-hold disabled forever for everyone who ran an affected
* build, with nothing left in the tree to explain it.
*
* The `defaults`-binary behaviour this relies on is pinned by the sibling test file, which runs
* only on a developer Mac: CI has no macOS test runner, so those tests never execute there.
*/
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'
/** A hang cutoff, not a latency budget: the probe measures ~6ms, and cutting a slow-but-working
* `defaults` short costs the user the feature for that launch. The shared 30s default is what
* must not apply — this runs before `ready`, so a 30s block is a hung app. */
const DEFAULTS_TIMEOUT_MS = 5_000
/** Why: `defaults` exits 1 for "does not exist"; anything else means 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 killed or timed-out probe never answered, and calling that unset would let a broken
* probe overwrite a value the user chose.
*/
export function interpretDefaultsRead(probe: () => ProcessResult): 'set' | 'unset' | 'unknown' {
let result: ProcessResult
try {
result = probe()
} catch {
// `runProcessSync` throws only when the child never started, which answers nothing at all.
return 'unknown'
}
// Why before the code: a timeout kills mid-run, so any exit it leaves behind is not an answer.
if (result.timedOut) {
return 'unknown'
}
if (result.code === 0) {
return 'set'
}
return result.code === DEFAULTS_MISSING_STATUS ? 'unset' : 'unknown'
}
export function readDomainPressAndHoldPreference(domain: string): 'set' | 'unset' | 'unknown' {
// Why no captured output: the exit code is the whole answer, and reading only presence is what
// makes an explicit `true` and an explicit `false` both count as the user's.
return interpretDefaultsRead(() =>
runProcessSync({
program: DEFAULTS_BINARY,
args: ['read', domain, PRESS_AND_HOLD_KEY],
timeoutMs: DEFAULTS_TIMEOUT_MS,
stdio: ['ignore', 'ignore', 'ignore']
})
)
}
export function writeDomainPressAndHoldPreference(domain: string, value: boolean): boolean {
try {
// Why the code check: a non-zero `defaults write` returns rather than throwing, so treating a
// completed run as success would record 'applied' for a write that never landed.
return (
runProcessSync({
program: DEFAULTS_BINARY,
args: ['write', domain, PRESS_AND_HOLD_KEY, '-bool', value ? 'true' : 'false'],
timeoutMs: DEFAULTS_TIMEOUT_MS,
stdio: ['ignore', 'ignore', 'ignore']
}).code === 0
)
} 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
}
@@ -499,3 +499,29 @@ describe('FloatingTerminalPanel close behavior', () => {
expect(mocks.closeTab).not.toHaveBeenCalledWith('tab-c')
})
})
describe('FloatingTerminalPanel tab drag wiring', () => {
beforeEach(setupFloatingTerminalPanelTest)
afterEach(() => {
vi.unstubAllGlobals()
})
it('hosts the tab strip in a drag context so floating tabs can be reordered', async () => {
setFloatingTabs([makeTab({ id: 'tab-1' }), makeTab({ id: 'tab-2' })])
const element = await renderPanel(true)
const dragContext = findByTypeName(element, 'FloatingWorkspaceTabDragContext')
expect(dragContext.props.enabled).toBe(true)
expect(findByTypeName(dragContext.props.children, 'TabBar')).toBeDefined()
})
it('leaves the drag context inactive while the closed panel stays mounted', async () => {
setFloatingTabs([makeTab({ id: 'tab-1' })])
const element = await renderPanel(false)
expect(findByTypeName(element, 'FloatingWorkspaceTabDragContext').props.enabled).toBe(false)
})
})
@@ -90,6 +90,8 @@ import type { Tab } from '../../../../shared/tab-types'
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
import { resolveUnifiedTabLabel } from '../../../../shared/tab-title-resolution'
import { FloatingBrowserSlot } from './FloatingBrowserSlot'
import { FloatingWorkspaceTabDragContext } from './FloatingWorkspaceTabDragContext'
import { isFloatingTerminalDragTarget } from './floating-terminal-titlebar-drag-target'
import { FloatingTerminalOrchestrationDialog } from './FloatingTerminalOrchestrationDialog'
import { FloatingTerminalResizeHandles } from './FloatingTerminalResizeHandles'
import { FloatingTerminalWindowControls } from './FloatingTerminalWindowControls'
@@ -153,8 +155,6 @@ type FloatingPanelShortcutResolution =
| { kind: 'index'; index: number }
| { kind: 'chrome'; action: KeybindingActionId }
const FLOATING_TERMINAL_NO_DRAG_SELECTOR =
'button,input,textarea,select,[role="menuitem"],[data-testid="sortable-tab"],[data-floating-terminal-no-drag]'
const FLOATING_TERMINAL_SHORTCUT_SURFACE_SELECTOR = '[data-floating-terminal-shortcut-surface]'
type FloatingTerminalPanelBoundsState = {
@@ -163,10 +163,6 @@ type FloatingTerminalPanelBoundsState = {
source: FloatingTerminalPanelBoundsSource
}
function isFloatingTerminalDragTarget(target: EventTarget): boolean {
return !(target instanceof HTMLElement && target.closest(FLOATING_TERMINAL_NO_DRAG_SELECTOR))
}
function readInitialPanelBounds(): FloatingTerminalPanelBoundsState {
const defaultCommittedBounds = getDefaultFloatingTerminalCommittedBounds()
const defaultRenderedBounds = getDefaultFloatingTerminalBounds()
@@ -1825,7 +1821,7 @@ export function FloatingTerminalPanel({
onPointerCancel={handleDragEnd}
onDoubleClick={handleTitlebarDoubleClick}
>
<div className="flex h-full min-w-0 flex-1">
<FloatingWorkspaceTabDragContext enabled={open}>
<TabBar
tabs={terminalItems}
activeTabId={activeTerminalId}
@@ -1874,7 +1870,7 @@ export function FloatingTerminalPanel({
tabBarOrder={tabBarOrder}
tabStripChrome="floating-panel"
/>
</div>
</FloatingWorkspaceTabDragContext>
<FloatingTerminalWindowControls
maximized={maximized}
onToggleMaximized={toggleMaximized}
@@ -0,0 +1,49 @@
import { cloneElement } from 'react'
import { DndContext, DragOverlay } from '@dnd-kit/core'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
import TabDragPreview from '../tab-bar/TabDragPreview'
import { TabDragProvider } from '../tab-group/tab-drag-context'
import { useTabDragSplit, type HoveredTabInsertion } from '../tab-group/useTabDragSplit'
/** dnd-kit host for the floating workspace tab strip, so its tabs reorder with
* the same gesture and insertion bar the main workspace tab bar uses. The panel
* renders one group and no split layout, so a drop only ever resolves to a
* same-group reorder — the pane-column split branches find no panel geometry. */
export function FloatingWorkspaceTabDragContext({
enabled,
children
}: {
/** False while the panel is hidden: a closed panel must not register sensors that compete with the workspace DndContext. */
enabled: boolean
children: React.ReactElement<{ hoveredTabInsertion?: HoveredTabInsertion | null }>
}): React.JSX.Element {
const dragSplit = useTabDragSplit({ worktreeId: FLOATING_TERMINAL_WORKTREE_ID, enabled })
return (
<TabDragProvider
isTabDragActive={dragSplit.activeDrag !== null}
isTabDragActiveRef={dragSplit.isTabDragActiveRef}
>
<DndContext
sensors={dragSplit.sensors}
collisionDetection={dragSplit.collisionDetection}
onDragStart={dragSplit.onDragStart}
onDragMove={dragSplit.onDragMove}
onDragOver={dragSplit.onDragOver}
onDragEnd={dragSplit.onDragEnd}
onDragCancel={dragSplit.onDragCancel}
// Why: same feedback loop the workspace strip hits — autoscroll shifts tabs under a still cursor and `over` re-resolves.
autoScroll={false}
>
<div ref={dragSplit.setDragRootNode} className="flex h-full min-w-0 flex-1">
{/* Why cloneElement over a render prop: the tab bar stays a plain child element, which keeps the panel's tree shallow-walkable. */}
{cloneElement(children, { hoveredTabInsertion: dragSplit.hoveredTabInsertion })}
</div>
{/* Why: the source tab stays anchored in the overflow-hidden strip; this ghost is what follows the cursor. */}
<DragOverlay dropAnimation={null}>
{dragSplit.activeDrag ? <TabDragPreview drag={dragSplit.activeDrag} /> : null}
</DragOverlay>
</DndContext>
</TabDragProvider>
)
}
@@ -0,0 +1,13 @@
/** Interactive chrome living inside the draggable titlebar. `[data-tab-id]` and the
* client-hosted row marker cover every tab kind: a press
* on a tab starts a dnd-kit reorder, so it must never also move the panel. */
const FLOATING_TERMINAL_NO_DRAG_SELECTOR =
'button,input,textarea,select,[role="menuitem"],[data-tab-id],[data-client-hosted-browser-row-id],[data-floating-terminal-no-drag]'
export function isFloatingTerminalDragTarget(target: EventTarget): boolean {
if (typeof Element === 'undefined' || !(target instanceof Element)) {
return true
}
return target.closest(FLOATING_TERMINAL_NO_DRAG_SELECTOR) === null
}
@@ -0,0 +1,204 @@
/**
* @vitest-environment happy-dom
*/
import { act, createElement } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
import type { Tab, TabGroup } from '../../../../shared/tab-types'
import { useAppStore } from '../../store'
import { useTabDragSplit, type TabDragItemData } from '../tab-group/useTabDragSplit'
import { isFloatingTerminalDragTarget } from './floating-terminal-titlebar-drag-target'
vi.mock('../browser-pane/host-guest/webview-registry', () => ({
acquireWebviewsDragPassthrough: vi.fn(() => vi.fn())
}))
vi.mock('../../runtime/web-runtime-session', () => ({
isWebRuntimeSessionActive: vi.fn(() => false),
moveWebRuntimeSessionTab: vi.fn()
}))
const GROUP_ID = 'floating-group'
const mounted: { container: HTMLDivElement; root: Root }[] = []
function makeTab(id: string, sortOrder: number): Tab {
return {
id,
groupId: GROUP_ID,
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
contentType: 'terminal',
entityId: `term-${id}`,
label: id,
customLabel: null,
color: null,
sortOrder,
createdAt: sortOrder
}
}
function makeDragData(unifiedTabId: string): TabDragItemData {
return {
kind: 'tab',
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
groupId: GROUP_ID,
unifiedTabId,
visibleTabId: `term-${unifiedTabId}`,
tabType: 'terminal',
label: unifiedTabId
}
}
function makeDropEvent(
activeData: TabDragItemData,
overData: TabDragItemData | null,
pointerX: number
) {
return {
active: { data: { current: activeData }, rect: { current: { initial: null } } },
over: overData
? {
id: overData.visibleTabId,
data: { current: overData },
// Tabs sit in a 100px-wide strip slot starting at x=200; midpoint is 250.
rect: { left: 200, top: 0, width: 100, height: 32 }
}
: null,
delta: { x: 0, y: 0 },
activatorEvent: { clientX: pointerX, clientY: 16 }
}
}
function renderFloatingDragHook(): ReturnType<typeof useTabDragSplit> {
let result: ReturnType<typeof useTabDragSplit> | null = null
function Probe(): null {
result = useTabDragSplit({ worktreeId: FLOATING_TERMINAL_WORKTREE_ID, enabled: true })
return null
}
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
act(() => root.render(createElement(Probe)))
mounted.push({ container, root })
if (!result) {
throw new Error('useTabDragSplit did not render')
}
return result
}
function dropTab(from: string, over: string, pointerX: number): void {
const drag = renderFloatingDragHook()
const activeData = makeDragData(from)
act(() => {
drag.onDragStart(
makeDropEvent(activeData, null, pointerX) as unknown as Parameters<typeof drag.onDragStart>[0]
)
})
act(() => {
drag.onDragEnd(
makeDropEvent(activeData, makeDragData(over), pointerX) as unknown as Parameters<
typeof drag.onDragEnd
>[0]
)
})
}
function floatingTabOrder(): string[] {
const groups: TabGroup[] =
useAppStore.getState().groupsByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? []
return groups.find((group) => group.id === GROUP_ID)?.tabOrder ?? []
}
beforeEach(() => {
useAppStore.setState({
// The floating workspace is never the active worktree; its tabs still reorder.
activeWorktreeId: 'wt-main',
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: GROUP_ID },
groupsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [
{
id: GROUP_ID,
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
activeTabId: 'tab-a',
tabOrder: ['tab-a', 'tab-b', 'tab-c']
}
]
},
unifiedTabsByWorktree: {
[FLOATING_TERMINAL_WORKTREE_ID]: [
makeTab('tab-a', 0),
makeTab('tab-b', 1),
makeTab('tab-c', 2)
]
},
layoutByWorktree: {}
})
})
afterEach(() => {
for (const { container, root } of mounted.splice(0)) {
act(() => root.unmount())
container.remove()
}
document.body.replaceChildren()
vi.clearAllMocks()
})
describe('floating workspace tab reorder', () => {
it('moves a tab after the hovered tab when released on its right half', () => {
dropTab('tab-a', 'tab-c', 290)
expect(floatingTabOrder()).toEqual(['tab-b', 'tab-c', 'tab-a'])
})
it('moves a tab before the hovered tab when released on its left half', () => {
dropTab('tab-c', 'tab-a', 210)
expect(floatingTabOrder()).toEqual(['tab-c', 'tab-a', 'tab-b'])
})
it('leaves the order alone when a tab is dropped on itself', () => {
dropTab('tab-b', 'tab-b', 290)
expect(floatingTabOrder()).toEqual(['tab-a', 'tab-b', 'tab-c'])
})
it('republishes sortOrder so the panel renders the committed order', () => {
dropTab('tab-a', 'tab-c', 290)
const tabs = useAppStore.getState().unifiedTabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID] ?? []
expect(tabs.map((tab) => [tab.id, tab.sortOrder])).toEqual([
['tab-a', 2],
['tab-b', 0],
['tab-c', 1]
])
})
})
describe('floating panel titlebar drag target', () => {
it.each(['sortable-tab', 'browser-tab', 'editor-tab', 'client-hosted-browser-row-id'])(
'does not move the panel when the press lands on a %s',
(kind) => {
const tab = document.createElement('div')
tab.dataset.tabId = kind
const label = document.createElement('span')
tab.appendChild(label)
document.body.appendChild(tab)
expect(isFloatingTerminalDragTarget(tab)).toBe(false)
expect(isFloatingTerminalDragTarget(label)).toBe(false)
}
)
it('does not move the panel when a tab SVG icon receives the press', () => {
const tab = document.createElement('div')
tab.dataset.tabId = 'terminal-tab'
const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
tab.appendChild(icon)
document.body.appendChild(tab)
expect(isFloatingTerminalDragTarget(icon)).toBe(false)
})
it('still moves the panel from bare titlebar chrome', () => {
const titlebar = document.createElement('div')
document.body.appendChild(titlebar)
expect(isFloatingTerminalDragTarget(titlebar)).toBe(true)
})
})
@@ -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 this process does not re-read after writing, 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()
})
})