Skip to content

Commit 3404070

Browse files
committedOct 31, 2024·
Pre-release 0.27.92
1 parent a9ad824 commit 3404070

24 files changed

+310
-66
lines changed
 

‎Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"kind" : "remoteSourceControl",
66
"location" : "https://github.com/devm33/CGEventOverride",
77
"state" : {
8-
"revision" : "571d36d63e68fac30e4a350600cd186697936f74",
9-
"version" : "1.2.3"
8+
"branch" : "devm33/fix-stale-AXIsProcessTrusted",
9+
"revision" : "06a9bf1f8f8d47cca221344101cc0274f04cc513"
1010
}
1111
},
1212
{

‎Core/Package.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ let package = Package(
5050
// quick hack to support custom UserDefaults
5151
// https://github.com/sindresorhus/KeyboardShortcuts
5252
.package(url: "https://github.com/devm33/KeyboardShortcuts", branch: "main"),
53-
.package(url: "https://github.com/devm33/CGEventOverride", from: "1.2.1"),
53+
.package(url: "https://github.com/devm33/CGEventOverride", branch: "devm33/fix-stale-AXIsProcessTrusted"),
5454
.package(url: "https://github.com/devm33/Highlightr", branch: "master"),
5555
],
5656
targets: [
@@ -83,6 +83,7 @@ let package = Package(
8383
.product(name: "UserDefaultsObserver", package: "Tool"),
8484
.product(name: "AppMonitoring", package: "Tool"),
8585
.product(name: "SuggestionBasic", package: "Tool"),
86+
.product(name: "Status", package: "Tool"),
8687
.product(name: "ChatTab", package: "Tool"),
8788
.product(name: "Logger", package: "Tool"),
8889
.product(name: "ChatAPIService", package: "Tool"),

‎Core/Sources/HostApp/General.swift

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Client
22
import ComposableArchitecture
33
import Foundation
44
import LaunchAgentManager
5+
import Status
56
import SwiftUI
67
import XPCShared
78
import Logger
@@ -11,7 +12,7 @@ struct General {
1112
@ObservableState
1213
struct State: Equatable {
1314
var xpcServiceVersion: String?
14-
var isAccessibilityPermissionGranted: Bool?
15+
var isAccessibilityPermissionGranted: ObservedAXStatus = .unknown
1516
var isReloading = false
1617
}
1718

@@ -20,7 +21,7 @@ struct General {
2021
case setupLaunchAgentIfNeeded
2122
case openExtensionManager
2223
case reloadStatus
23-
case finishReloading(xpcServiceVersion: String, permissionGranted: Bool)
24+
case finishReloading(xpcServiceVersion: String, permissionGranted: ObservedAXStatus)
2425
case failedReloading
2526
case retryReloading
2627
}
@@ -35,7 +36,7 @@ struct General {
3536
case .appear:
3637
return .run { send in
3738
await send(.setupLaunchAgentIfNeeded)
38-
for await _ in DistributedNotificationCenter.default().notifications(named: NSNotification.Name("com.apple.accessibility.api")) {
39+
for await _ in DistributedNotificationCenter.default().notifications(named: .serviceStatusDidChange) {
3940
await send(.reloadStatus)
4041
}
4142
}

‎Core/Sources/HostApp/GeneralSettings/CopilotConnectionView.swift

+3-17
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
11
import ComposableArchitecture
22
import SwiftUI
33

4-
struct ActivityIndicatorView: NSViewRepresentable {
5-
func makeNSView(context _: Context) -> NSProgressIndicator {
6-
let progressIndicator = NSProgressIndicator()
7-
progressIndicator.style = .spinning
8-
progressIndicator.controlSize = .small
9-
progressIndicator.startAnimation(nil)
10-
return progressIndicator
11-
}
12-
13-
func updateNSView(_: NSProgressIndicator, context _: Context) {
14-
// No-op
15-
}
16-
}
17-
184
struct CopilotConnectionView: View {
195
@AppStorage("username") var username: String = ""
206
@Environment(\.toast) var toast
@@ -38,6 +24,9 @@ struct CopilotConnectionView: View {
3824
title: "GitHub Account Status Permissions",
3925
subtitle: "GitHub Connection: \(viewModel.status?.description ?? "Loading...")"
4026
) {
27+
if viewModel.isRunningAction || waitingForSignIn {
28+
ProgressView().controlSize(.small)
29+
}
4130
Button("Refresh Connection") {
4231
viewModel.checkStatus()
4332
}
@@ -72,9 +61,6 @@ struct CopilotConnectionView: View {
7261
viewModel.isSignInAlertPresented = false
7362
}
7463
}
75-
if viewModel.isRunningAction || waitingForSignIn {
76-
ActivityIndicatorView()
77-
}
7864
}
7965
}
8066

‎Core/Sources/HostApp/GeneralSettings/GeneralSettingsView.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ struct GeneralSettingsView: View {
99
let store: StoreOf<General>
1010

1111
var accessibilityPermissionSubtitle: String {
12-
guard let granted = store.isAccessibilityPermissionGranted else { return "Loading..." }
13-
return granted ? "Granted" : "Not Granted. Required to run. Click to open System Preferences."
12+
switch store.isAccessibilityPermissionGranted {
13+
case .granted:
14+
return "Granted"
15+
case .notGranted:
16+
return "Not Granted. Required to run. Click to open System Preferences."
17+
case .unknown:
18+
return ""
19+
}
1420
}
1521

1622
var body: some View {

‎Core/Sources/Service/RealtimeSuggestionController.swift

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Combine
66
import Foundation
77
import Logger
88
import Preferences
9+
import Status
910
import QuartzCore
1011
import Workspace
1112
import XcodeInspector
@@ -124,10 +125,12 @@ public actor RealtimeSuggestionController {
124125
do {
125126
try await XcodeInspector.shared.safe.latestActiveXcode?
126127
.triggerCopilotCommand(name: "Sync Text Settings")
128+
await Status.shared.updateExtensionStatus(.succeeded)
127129
} catch {
128130
if filespace.codeMetadata.uti?.isEmpty ?? true {
129131
filespace.codeMetadata.uti = nil
130132
}
133+
await Status.shared.updateExtensionStatus(.failed)
131134
}
132135
}
133136
}

‎Core/Sources/Service/Service.swift

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public final class Service {
3232
let globalShortcutManager: GlobalShortcutManager
3333
let keyBindingManager: KeyBindingManager
3434
let xcodeThemeController: XcodeThemeController = .init()
35+
public var markAsProcessing: (Bool) -> Void = { _ in }
3536

3637
@Dependency(\.toast) var toast
3738
var cancellable = Set<AnyCancellable>()

‎Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ struct PresentInWindowSuggestionPresenter {
3030
Task { @MainActor in
3131
let controller = Service.shared.guiController.widgetController
3232
controller.markAsProcessing(isProcessing)
33+
Service.shared.markAsProcessing(isProcessing)
3334
}
3435
}
3536

‎Core/Sources/Service/XPCService.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import GitHubCopilotService
44
import LanguageServerProtocol
55
import Logger
66
import Preferences
7+
import Status
78
import XPCShared
89

910
public class XPCService: NSObject, XPCServiceProtocol {
@@ -16,8 +17,10 @@ public class XPCService: NSObject, XPCServiceProtocol {
1617
)
1718
}
1819

19-
public func getXPCServiceAccessibilityPermission(withReply reply: @escaping (Bool) -> Void) {
20-
reply(AXIsProcessTrusted())
20+
public func getXPCServiceAccessibilityPermission(withReply reply: @escaping (ObservedAXStatus) -> Void) {
21+
Task {
22+
reply(await Status.shared.getAXStatus())
23+
}
2124
}
2225

2326
// MARK: - Suggestion

‎ExtensionService/AppDelegate+Menu.swift

+16-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AppKit
22
import Foundation
33
import Preferences
4+
import Status
45
import SuggestionBasic
56
import XcodeInspector
67
import Logger
@@ -14,10 +15,6 @@ extension AppDelegate {
1415
.init("xcodeInspectorDebugMenu")
1516
}
1617

17-
fileprivate var accessibilityAPIPermissionMenuItemIdentifier: NSUserInterfaceItemIdentifier {
18-
.init("accessibilityAPIPermissionMenuItem")
19-
}
20-
2118
fileprivate var sourceEditorDebugMenu: NSUserInterfaceItemIdentifier {
2219
.init("sourceEditorDebugMenu")
2320
}
@@ -72,12 +69,12 @@ extension AppDelegate {
7269
xcodeInspectorDebug.submenu = xcodeInspectorDebugMenu
7370
xcodeInspectorDebug.isHidden = false
7471

75-
let accessibilityAPIPermission = NSMenuItem(
76-
title: "Accessibility Permission: N/A",
77-
action: nil,
72+
statusMenuItem = NSMenuItem(
73+
title: "",
74+
action: #selector(openStatusLink),
7875
keyEquivalent: ""
7976
)
80-
accessibilityAPIPermission.identifier = accessibilityAPIPermissionMenuItemIdentifier
77+
statusMenuItem.isHidden = true
8178

8279
let quitItem = NSMenuItem(
8380
title: "Quit",
@@ -126,7 +123,7 @@ extension AppDelegate {
126123
statusBarMenu.addItem(toggleIgnoreLanguage)
127124
statusBarMenu.addItem(.separator())
128125
statusBarMenu.addItem(copilotStatus)
129-
statusBarMenu.addItem(accessibilityAPIPermission)
126+
statusBarMenu.addItem(statusMenuItem)
130127
statusBarMenu.addItem(.separator())
131128
statusBarMenu.addItem(openDocs)
132129
statusBarMenu.addItem(openForum)
@@ -160,22 +157,14 @@ extension AppDelegate: NSMenuDelegate {
160157
item.identifier == toggleIgnoreLanguageMenuItemIdentifier
161158
}) {
162159
if let lang = DisabledLanguageList.shared.activeDocumentLanguage {
163-
toggleLanguage.title = "\(DisabledLanguageList.shared.isEnabled(lang) ? "Disable" : "Enable") Completions For \(lang.rawValue)"
160+
toggleLanguage.title = "\(DisabledLanguageList.shared.isEnabled(lang) ? "Disable" : "Enable") Completions for \(lang.rawValue)"
164161
toggleLanguage.action = #selector(toggleIgnoreLanguage)
165162
} else {
166163
toggleLanguage.title = "No Active Document"
167164
toggleLanguage.action = nil
168165
}
169166
}
170167

171-
if let accessibilityAPIPermission = menu.items.first(where: { item in
172-
item.identifier == accessibilityAPIPermissionMenuItemIdentifier
173-
}) {
174-
AXIsProcessTrusted()
175-
accessibilityAPIPermission.title =
176-
"Accessibility Permission: \(AXIsProcessTrusted() ? "Granted" : "Not Granted")"
177-
}
178-
179168
statusChecker.updateStatusInBackground(notify: { (status: String, isOk: Bool) in
180169
if let statusItem = menu.items.first(where: { item in
181170
item.identifier == self.copilotStatusMenuItemIdentifier
@@ -321,6 +310,15 @@ private extension AppDelegate {
321310
}
322311
}
323312
}
313+
314+
@objc func openStatusLink() {
315+
Task {
316+
let status = await Status.shared.getStatus()
317+
if let s = status.url, let url = URL(string: s) {
318+
NSWorkspace.shared.open(url)
319+
}
320+
}
321+
}
324322
}
325323

326324
private extension NSMenuItem {

‎ExtensionService/AppDelegate.swift

+75-10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Logger
66
import Preferences
77
import Service
88
import ServiceManagement
9+
import Status
910
import SwiftUI
1011
import UpdateChecker
1112
import UserDefaultsObserver
@@ -30,6 +31,7 @@ class ExtensionUpdateCheckerDelegate: UpdateCheckerDelegate {
3031
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
3132
let service = Service.shared
3233
var statusBarItem: NSStatusItem!
34+
var statusMenuItem: NSMenuItem!
3335
var xpcController: XPCController?
3436
let updateChecker =
3537
UpdateChecker(
@@ -39,21 +41,29 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
3941
let statusChecker: AuthStatusChecker = AuthStatusChecker()
4042
var xpcExtensionService: XPCExtensionService?
4143
private var cancellables = Set<AnyCancellable>()
44+
private var progressView: NSProgressIndicator?
45+
private var idleIcon = NSImage(named: "MenuBarIcon")
4246

4347
func applicationDidFinishLaunching(_: Notification) {
4448
if ProcessInfo.processInfo.environment["IS_UNIT_TEST"] == "YES" { return }
4549
_ = XcodeInspector.shared
50+
service.markAsProcessing = { [weak self] in
51+
guard let self = self else { return }
52+
self.markAsProcessing($0)
53+
}
4654
service.start()
4755
AXIsProcessTrustedWithOptions([
4856
kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true,
4957
] as CFDictionary)
5058
setupQuitOnUpdate()
5159
setupQuitOnUserTerminated()
52-
setupQuitOnFeatureFlag()
5360
xpcController = .init()
5461
Logger.service.info("XPC Service started.")
5562
NSApp.setActivationPolicy(.accessory)
5663
buildStatusBarMenu()
64+
watchServiceStatus()
65+
watchAXStatus()
66+
updateStatusBarItem() // set the initial status
5767
}
5868

5969
@objc func quit() {
@@ -132,15 +142,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
132142
}
133143
}
134144

135-
func setupQuitOnFeatureFlag() {
136-
FeatureFlagNotifierImpl.shared.featureFlagsDidChange.sink { [weak self] (flags) in
137-
if flags.x != true {
138-
Logger.service.info("Xcode feature flag not granted, quitting.")
139-
self?.quit()
140-
}
141-
}.store(in: &cancellables)
142-
}
143-
144145
func requestAccessoryAPIPermission() {
145146
AXIsProcessTrustedWithOptions([
146147
kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true,
@@ -161,6 +162,70 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
161162
xpcExtensionService = service
162163
return service
163164
}
165+
166+
func watchServiceStatus() {
167+
let notifications = NotificationCenter.default.notifications(named: .serviceStatusDidChange)
168+
Task { [weak self] in
169+
for await _ in notifications {
170+
guard let self else { return }
171+
self.updateStatusBarItem()
172+
}
173+
}
174+
}
175+
176+
func watchAXStatus() {
177+
let osNotifications = DistributedNotificationCenter.default().notifications(named: NSNotification.Name("com.apple.accessibility.api"))
178+
Task { [weak self] in
179+
for await _ in osNotifications {
180+
guard let self else { return }
181+
self.updateStatusBarItem()
182+
}
183+
}
184+
}
185+
186+
func updateStatusBarItem() {
187+
Task { @MainActor in
188+
let status = await Status.shared.getStatus()
189+
let image = if status.system {
190+
NSImage(systemSymbolName: status.icon, accessibilityDescription: nil)
191+
} else {
192+
NSImage(named: status.icon)
193+
}
194+
idleIcon = image
195+
self.statusBarItem.button?.image = image
196+
if let message = status.message {
197+
// TODO switch to attributedTitle to enable line breaks and color.
198+
self.statusMenuItem.title = message
199+
self.statusMenuItem.isHidden = false
200+
self.statusMenuItem.isEnabled = status.url != nil
201+
} else {
202+
self.statusMenuItem.isHidden = true
203+
}
204+
}
205+
}
206+
207+
func markAsProcessing(_ isProcessing: Bool) {
208+
if !isProcessing {
209+
// No longer in progress
210+
progressView?.removeFromSuperview()
211+
progressView = nil
212+
statusBarItem.button?.image = idleIcon
213+
return
214+
}
215+
if progressView != nil {
216+
// Already in progress
217+
return
218+
}
219+
let progress = NSProgressIndicator()
220+
progress.style = .spinning
221+
progress.sizeToFit()
222+
progress.frame = statusBarItem.button?.bounds ?? .zero
223+
progress.isIndeterminate = true
224+
progress.startAnimation(nil)
225+
statusBarItem.button?.addSubview(progress)
226+
statusBarItem.button?.image = nil
227+
progressView = progress
228+
}
164229
}
165230

166231
extension NSRunningApplication {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "copilot-warning-24.png",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"filename" : "copilot-warning-48.png",
10+
"idiom" : "universal",
11+
"scale" : "2x"
12+
},
13+
{
14+
"idiom" : "universal",
15+
"scale" : "3x"
16+
}
17+
],
18+
"info" : {
19+
"author" : "xcode",
20+
"version" : 1
21+
},
22+
"properties" : {
23+
"template-rendering-intent" : "template"
24+
}
25+
}
Loading
Loading

‎Server/package-lock.json

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Server/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
"description": "Package for downloading @github/copilot-language-server",
55
"private": true,
66
"dependencies": {
7-
"@github/copilot-language-server": "^1.241.0"
7+
"@github/copilot-language-server": "^1.243.0"
88
}
99
}

‎Tool/Package.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let package = Package(
1717
.library(name: "SuggestionBasic", targets: ["SuggestionBasic"]),
1818
.library(name: "Toast", targets: ["Toast"]),
1919
.library(name: "SharedUIComponents", targets: ["SharedUIComponents"]),
20+
.library(name: "Status", targets: ["Status"]),
2021
.library(name: "UserDefaultsObserver", targets: ["UserDefaultsObserver"]),
2122
.library(name: "Workspace", targets: ["Workspace", "WorkspaceSuggestionService"]),
2223
.library(
@@ -68,7 +69,7 @@ let package = Package(
6869
targets: [
6970
// MARK: - Helpers
7071

71-
.target(name: "XPCShared", dependencies: ["SuggestionBasic", "Logger"]),
72+
.target(name: "XPCShared", dependencies: ["SuggestionBasic", "Logger", "Status"]),
7273

7374
.target(name: "Configs"),
7475

@@ -128,6 +129,7 @@ let package = Package(
128129
dependencies: [
129130
"Preferences",
130131
"Logger",
132+
"Status",
131133
]
132134
),
133135

@@ -141,6 +143,7 @@ let package = Package(
141143
"Toast",
142144
"Preferences",
143145
"AsyncPassthroughSubject",
146+
"Status",
144147
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
145148
]
146149
),
@@ -206,6 +209,8 @@ let package = Package(
206209

207210
// MARK: - Services
208211

212+
.target(name: "Status"),
213+
209214
.target(name: "SuggestionProvider", dependencies: [
210215
"SuggestionBasic",
211216
"UserDefaultsObserver",

‎Tool/Sources/AXNotificationStream/AXNotificationStream.swift

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ApplicationServices
33
import Foundation
44
import Logger
55
import Preferences
6+
import Status
67

78
public final class AXNotificationStream: AsyncSequence {
89
public typealias Stream = AsyncStream<Element>
@@ -125,13 +126,15 @@ public final class AXNotificationStream: AsyncSequence {
125126
switch e {
126127
case .success:
127128
pendingRegistrationNames.remove(name)
129+
await Status.shared.updateAXStatus(.granted)
128130
case .actionUnsupported:
129131
Logger.service.error("AXObserver: Action unsupported: \(name)")
130132
pendingRegistrationNames.remove(name)
131133
case .apiDisabled:
132134
Logger.service
133135
.error("AXObserver: Accessibility API disabled, will try again later")
134136
retry -= 1
137+
await Status.shared.updateAXStatus(.notGranted)
135138
case .invalidUIElement:
136139
Logger.service
137140
.error("AXObserver: Invalid UI element, notification name \(name)")

‎Tool/Sources/GitHubCopilotService/Services/FeatureFlagNotifier.swift

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ public struct FeatureFlags: Hashable, Codable {
55
public var rt: Bool
66
public var sn: Bool
77
public var chat: Bool
8-
public var x: Bool?
98
public var xc: Bool?
109
}
1110

‎Tool/Sources/Status/Status.swift

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import AppKit
2+
import Foundation
3+
4+
public enum ExtensionPermissionStatus {
5+
case unknown
6+
case succeeded
7+
case failed
8+
}
9+
10+
@objc public enum ObservedAXStatus: Int {
11+
case unknown = -1
12+
case granted = 1
13+
case notGranted = 0
14+
}
15+
16+
public extension Notification.Name {
17+
static let serviceStatusDidChange = Notification.Name("com.github.CopilotForXcode.serviceStatusDidChange")
18+
}
19+
20+
public struct StatusResponse {
21+
public let icon: String
22+
public let system: Bool // Temporary workaround for status images
23+
public let message: String?
24+
public let url: String?
25+
}
26+
27+
public final actor Status {
28+
public static let shared = Status()
29+
30+
private var extensionStatus: ExtensionPermissionStatus = .unknown
31+
private var axStatus: ObservedAXStatus = .unknown
32+
33+
private init() {}
34+
35+
public func updateExtensionStatus(_ status: ExtensionPermissionStatus) {
36+
guard status != extensionStatus else { return }
37+
extensionStatus = status
38+
broadcast()
39+
}
40+
41+
public func updateAXStatus(_ status: ObservedAXStatus) {
42+
guard status != axStatus else { return }
43+
axStatus = status
44+
broadcast()
45+
}
46+
47+
public func getAXStatus() -> ObservedAXStatus {
48+
// if Xcode is running, return the observed status
49+
if isXcodeRunning() {
50+
return axStatus
51+
} else if AXIsProcessTrusted() {
52+
// if Xcode is not running but AXIsProcessTrusted() is true, return granted
53+
return .granted
54+
} else {
55+
// otherwise, return the last observed status, which may be unknown
56+
return axStatus
57+
}
58+
}
59+
60+
private func isXcodeRunning() -> Bool {
61+
let xcode = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
62+
return !xcode.isEmpty
63+
}
64+
65+
public func getStatus() -> StatusResponse {
66+
if extensionStatus == .failed {
67+
// TODO differentiate between the permission not being granted and the
68+
// extension just getting disabled by Xcode.
69+
return .init(
70+
icon: "exclamationmark.circle",
71+
system: true,
72+
message: """
73+
Extension is not enabled. Enable GitHub Copilot under Xcode
74+
and then restart Xcode.
75+
""",
76+
url: "x-apple.systempreferences:com.apple.ExtensionsPreferences"
77+
)
78+
}
79+
80+
switch getAXStatus() {
81+
case .granted:
82+
return .init(icon: "MenuBarIcon", system: false, message: nil, url: nil)
83+
case .notGranted:
84+
return .init(
85+
icon: "exclamationmark.circle",
86+
system: true,
87+
message: """
88+
Accessibility permission not granted. \
89+
Click to open System Preferences.
90+
""",
91+
url: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
92+
)
93+
case .unknown:
94+
return .init(
95+
icon: "exclamationmark.circle",
96+
system: true,
97+
message: """
98+
Accessibility permission not granted or Copilot restart needed.
99+
""",
100+
url: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
101+
)
102+
}
103+
}
104+
105+
private func broadcast() {
106+
NotificationCenter.default.post(
107+
name: .serviceStatusDidChange,
108+
object: nil
109+
)
110+
// Can remove DistributedNotificationCenter if the settings UI moves in-process
111+
DistributedNotificationCenter.default().post(
112+
name: .serviceStatusDidChange,
113+
object: nil
114+
)
115+
}
116+
}

‎Tool/Sources/XPCShared/XPCExtensionService.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import Logger
3+
import Status
34

45
public enum XPCExtensionServiceError: Swift.Error, LocalizedError {
56
case failedToGetServiceEndpoint
@@ -48,7 +49,7 @@ public class XPCExtensionService {
4849
}
4950
}
5051

51-
public func getXPCServiceAccessibilityPermission() async throws -> Bool {
52+
public func getXPCServiceAccessibilityPermission() async throws -> ObservedAXStatus {
5253
try await withXPCServiceConnected {
5354
service, continuation in
5455
service.getXPCServiceAccessibilityPermission { isGranted in

‎Tool/Sources/XPCShared/XPCServiceProtocol.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import Status
23
import SuggestionBasic
34

45
@objc(XPCServiceProtocol)
@@ -53,7 +54,7 @@ public protocol XPCServiceProtocol {
5354
)
5455

5556
func getXPCServiceVersion(withReply reply: @escaping (String, String) -> Void)
56-
func getXPCServiceAccessibilityPermission(withReply reply: @escaping (Bool) -> Void)
57+
func getXPCServiceAccessibilityPermission(withReply reply: @escaping (ObservedAXStatus) -> Void)
5758
func postNotification(name: String, withReply reply: @escaping () -> Void)
5859
func send(endpoint: String, requestBody: Data, reply: @escaping (Data?, Error?) -> Void)
5960
func quit(reply: @escaping () -> Void)

‎Tool/Sources/XcodeInspector/SourceEditor.swift

+15-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import AsyncPassthroughSubject
33
import AXNotificationStream
44
import Foundation
55
import Logger
6+
import Status
67
import SuggestionBasic
78

89
/// Representing a source editor inside Xcode.
@@ -54,7 +55,7 @@ public class SourceEditor {
5455
/// - note: This method is expensive. It needs to convert index based ranges to line based
5556
/// ranges.
5657
public func getContent() -> Content {
57-
let content = element.value
58+
let content = getElementValueAndRecordStatus()
5859
let selectionRange = element.selectedTextRange
5960
let (lines, selections) = cache.get(content: content, selectedTextRange: selectionRange)
6061

@@ -73,6 +74,19 @@ public class SourceEditor {
7374
)
7475
}
7576

77+
private func getElementValueAndRecordStatus() -> String {
78+
do {
79+
let value: String = try element.copyValue(key: kAXValueAttribute)
80+
Task { await Status.shared.updateAXStatus(.granted) }
81+
return value
82+
} catch AXError.apiDisabled {
83+
Task { await Status.shared.updateAXStatus(.notGranted) }
84+
} catch {
85+
// ignore
86+
}
87+
return ""
88+
}
89+
7690
public init(runningApplication: NSRunningApplication, element: AXUIElement) {
7791
self.runningApplication = runningApplication
7892
self.element = element

‎Tool/Sources/XcodeInspector/XcodeInspector.swift

+16-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Combine
55
import Foundation
66
import Logger
77
import Preferences
8+
import Status
89
import SuggestionBasic
910
import Toast
1011

@@ -285,7 +286,21 @@ public final class XcodeInspector: ObservableObject {
285286

286287
let setFocusedElement = { @XcodeInspectorActor [weak self] in
287288
guard let self else { return }
288-
focusedElement = xcode.appElement.focusedElement
289+
290+
func getFocusedElementAndRecordStatus(_ element: AXUIElement) -> AXUIElement? {
291+
do {
292+
let focused: AXUIElement = try element.copyValue(key: kAXFocusedUIElementAttribute)
293+
Task { await Status.shared.updateAXStatus(.granted) }
294+
return focused
295+
} catch AXError.apiDisabled {
296+
Task { await Status.shared.updateAXStatus(.notGranted) }
297+
} catch {
298+
// ignore
299+
}
300+
return nil
301+
}
302+
303+
focusedElement = getFocusedElementAndRecordStatus(xcode.appElement)
289304
if let editorElement = focusedElement, editorElement.isSourceEditor {
290305
focusedEditor = .init(
291306
runningApplication: xcode.runningApplication,

0 commit comments

Comments
 (0)
Please sign in to comment.