Skip to content

Commit 9f74241

Browse files
authored
fix(focus): prime VS Code's Electron AX tree so suggestions work (#671)
1 parent 9584a35 commit 9f74241

3 files changed

Lines changed: 49 additions & 6 deletions

File tree

Cotabby/Services/Focus/ChromiumAccessibilityEnabler.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,37 @@ final class ChromiumAccessibilityEnabler {
2727
/// not advertise it). Recorded so we stop retrying a doomed call every tick.
2828
private var unsupportedPIDs: Set<pid_t> = []
2929

30+
/// The frontmost PID seen on the previous poll tick, used to detect activation edges. Updated on
31+
/// every call (including for apps we never prime) so a switch away and back is always an edge.
32+
private var lastFrontmostPID: pid_t?
33+
3034
/// Primes the application if it is a Chromium/Electron surface we cover and has not been primed
3135
/// (or marked unsupported) yet. Safe to call every poll tick.
36+
///
37+
/// Electron editors are additionally re-primed on every activation edge (each time the app
38+
/// becomes frontmost), not just once per PID. Observed on VS Code: the first
39+
/// `AXManualAccessibility` write returns success while the app is long-running, yet the web-AX
40+
/// tree stays dormant for minutes; a later re-assert wakes it promptly. One extra AX write per
41+
/// app switch is negligible, and Chromium browsers keep the once-per-PID behavior that already
42+
/// works for them.
3243
func primeIfNeeded(application: NSRunningApplication) {
44+
let pid = application.processIdentifier
45+
let isActivationEdge = pid != lastFrontmostPID
46+
lastFrontmostPID = pid
47+
3348
guard BrowserAppDetector.needsWebAccessibilityPriming(
3449
bundleIdentifier: application.bundleIdentifier)
3550
else {
3651
return
3752
}
3853

39-
let pid = application.processIdentifier
40-
guard pid > 0, !primedPIDs.contains(pid), !unsupportedPIDs.contains(pid) else {
54+
guard pid > 0, !unsupportedPIDs.contains(pid) else {
55+
return
56+
}
57+
58+
let reassertForElectronEditor = isActivationEdge
59+
&& BrowserAppDetector.isElectronEditor(bundleIdentifier: application.bundleIdentifier)
60+
guard !primedPIDs.contains(pid) || reassertForElectronEditor else {
4161
return
4262
}
4363

Cotabby/Support/BrowserAppDetector.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,21 @@ enum BrowserAppDetector {
3939
/// Electron apps (Chromium under the hood) that ship editors worth covering. This is an
4040
/// intentional named allowlist, not a blanket Electron opt-in: most Electron apps are not
4141
/// text-editing surfaces, and priming them wholesale risks unexpected behavior.
42+
///
43+
/// Entries are lowercased and matched case-insensitively (see `isElectronEditor`). VS Code's real
44+
/// bundle id is the mixed-case `com.microsoft.VSCode`, so an exact match would silently miss it
45+
/// and leave the editor's entire Electron AX tree dormant: no focused field resolves for the
46+
/// editor, the Copilot chat, or the integrated terminal, so no suggestions appear anywhere in the
47+
/// app even though screenshot-based OCR keeps working.
48+
///
49+
/// Cursor is intentionally absent: it ships under opaque ToDesktop bundle ids
50+
/// (`com.todesktop.<hash>`) that change between builds, so there is no stable id to allowlist
51+
/// here without a broad `com.todesktop.` prefix that would also prime unrelated ToDesktop apps.
4252
private static let electronEditorBundleIdentifiers: Set<String> = [
43-
"com.clickup.desktop-app"
53+
"com.clickup.desktop-app",
54+
"com.microsoft.vscode", // Visual Studio Code
55+
"com.microsoft.vscodeinsiders", // VS Code - Insiders
56+
"com.vscodium" // VSCodium (FOSS VS Code build)
4457
]
4558

4659
/// Broad check: is the user typing inside any web browser? Used for prompt tone hints.
@@ -53,10 +66,12 @@ enum BrowserAppDetector {
5366
hasMatchingPrefix(bundleIdentifier, in: chromiumBundlePrefixes)
5467
}
5568

56-
/// Is this a named Electron editor we intentionally cover?
69+
/// Is this a named Electron editor we intentionally cover? Case-insensitive because macOS bundle
70+
/// ids are case-insensitive in practice and VS Code's is mixed-case (`com.microsoft.VSCode`); a
71+
/// case-sensitive exact match here was the reason VS Code resolved no focus and got no suggestions.
5772
static func isElectronEditor(bundleIdentifier: String?) -> Bool {
58-
guard let bundleIdentifier else { return false }
59-
return electronEditorBundleIdentifiers.contains(bundleIdentifier)
73+
guard let lowered = bundleIdentifier?.lowercased() else { return false }
74+
return electronEditorBundleIdentifiers.contains(lowered)
6075
}
6176

6277
/// Gate for the Chromium/Electron-specific AX recovery paths (renderer priming, cursor

CotabbyTests/BrowserAppDetectorTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ final class BrowserAppDetectorTests: XCTestCase {
3232

3333
func testElectronEditorAllowlist() {
3434
XCTAssertTrue(BrowserAppDetector.isElectronEditor(bundleIdentifier: "com.clickup.desktop-app"))
35+
// VS Code ships under the mixed-case `com.microsoft.VSCode`; matching must be case-insensitive
36+
// or its entire Electron AX tree stays dormant and no suggestions ever resolve.
37+
XCTAssertTrue(BrowserAppDetector.isElectronEditor(bundleIdentifier: "com.microsoft.VSCode"))
38+
XCTAssertTrue(BrowserAppDetector.isElectronEditor(bundleIdentifier: "com.microsoft.VSCodeInsiders"))
39+
XCTAssertTrue(BrowserAppDetector.isElectronEditor(bundleIdentifier: "com.vscodium"))
40+
// Electron, but not a text-editing surface we cover: must stay out of the priming allowlist.
3541
XCTAssertFalse(BrowserAppDetector.isElectronEditor(bundleIdentifier: "com.hnc.Discord"))
3642
XCTAssertFalse(BrowserAppDetector.isElectronEditor(bundleIdentifier: nil))
3743
}
@@ -41,6 +47,8 @@ final class BrowserAppDetectorTests: XCTestCase {
4147
BrowserAppDetector.needsWebAccessibilityPriming(bundleIdentifier: "com.google.Chrome"))
4248
XCTAssertTrue(
4349
BrowserAppDetector.needsWebAccessibilityPriming(bundleIdentifier: "com.clickup.desktop-app"))
50+
XCTAssertTrue(
51+
BrowserAppDetector.needsWebAccessibilityPriming(bundleIdentifier: "com.microsoft.VSCode"))
4452
XCTAssertFalse(
4553
BrowserAppDetector.needsWebAccessibilityPriming(bundleIdentifier: "com.apple.Safari"))
4654
XCTAssertFalse(

0 commit comments

Comments
 (0)