Skip to content

Commit c32cc7c

Browse files
committed
Pre-release 0.32.111
1 parent a50045a commit c32cc7c

File tree

11 files changed

+132
-20
lines changed

11 files changed

+132
-20
lines changed

Diff for: Copilot for Xcode/App.swift

+32
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,28 @@ struct VisualEffect: NSViewRepresentable {
1313
}
1414

1515
class AppDelegate: NSObject, NSApplicationDelegate {
16+
private var permissionAlertShown = false
17+
1618
// Launch modes supported by the app
1719
enum LaunchMode {
1820
case chat
1921
case settings
2022
}
2123

2224
func applicationDidFinishLaunching(_ notification: Notification) {
25+
if #available(macOS 13.0, *) {
26+
checkBackgroundPermissions()
27+
}
28+
2329
let launchMode = determineLaunchMode()
2430
handleLaunchMode(launchMode)
2531
}
2632

2733
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
34+
if #available(macOS 13.0, *) {
35+
checkBackgroundPermissions()
36+
}
37+
2838
let launchMode = determineLaunchMode()
2939
handleLaunchMode(launchMode)
3040
return true
@@ -73,6 +83,28 @@ class AppDelegate: NSObject, NSApplicationDelegate {
7383
}
7484
}
7585

86+
@available(macOS 13.0, *)
87+
private func checkBackgroundPermissions() {
88+
Task {
89+
// Direct check of permission status
90+
let launchAgentManager = LaunchAgentManager()
91+
let isPermissionGranted = await launchAgentManager.isBackgroundPermissionGranted()
92+
93+
if !isPermissionGranted {
94+
// Only show alert if permission isn't granted
95+
DispatchQueue.main.async {
96+
if !self.permissionAlertShown {
97+
showBackgroundPermissionAlert()
98+
self.permissionAlertShown = true
99+
}
100+
}
101+
} else {
102+
// Permission is granted, reset flag
103+
self.permissionAlertShown = false
104+
}
105+
}
106+
}
107+
76108
// MARK: - Application Termination
77109

78110
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {

Diff for: Core/Sources/ConversationTab/ChatPanel.swift

+18-5
Original file line numberDiff line numberDiff line change
@@ -761,11 +761,24 @@ struct ChatPanelInputArea: View {
761761
return result
762762
}
763763
func subscribeToActiveDocumentChangeEvent() {
764-
XcodeInspector.shared.$activeDocumentURL.receive(on: DispatchQueue.main)
765-
.sink { newDocURL in
766-
if supportedFileExtensions.contains(newDocURL?.pathExtension ?? "") {
767-
let currentEditor = FileReference(url: newDocURL!, isCurrentEditor: true)
768-
chat.send(.setCurrentEditor(currentEditor))
764+
Publishers.CombineLatest(
765+
XcodeInspector.shared.$latestActiveXcode,
766+
XcodeInspector.shared.$activeDocumentURL
767+
.removeDuplicates()
768+
)
769+
.receive(on: DispatchQueue.main)
770+
.sink { newXcode, newDocURL in
771+
// First check for realtimeWorkspaceURL if activeWorkspaceURL is nil
772+
if let realtimeURL = newXcode?.realtimeDocumentURL, newDocURL == nil {
773+
if supportedFileExtensions.contains(realtimeURL.pathExtension) {
774+
let currentEditor = FileReference(url: realtimeURL, isCurrentEditor: true)
775+
chat.send(.setCurrentEditor(currentEditor))
776+
}
777+
} else {
778+
if supportedFileExtensions.contains(newDocURL?.pathExtension ?? "") {
779+
let currentEditor = FileReference(url: newDocURL!, isCurrentEditor: true)
780+
chat.send(.setCurrentEditor(currentEditor))
781+
}
769782
}
770783
}
771784
.store(in: &cancellable)

Diff for: Core/Sources/HostApp/General.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public struct General {
5858
.setupLaunchAgentForTheFirstTimeIfNeeded()
5959
} catch {
6060
Logger.ui.error("Failed to setup launch agent. \(error.localizedDescription)")
61-
toast(error.localizedDescription, .error)
61+
toast("Operation failed: permission denied. This may be due to missing background permissions.", .error)
6262
}
6363
await send(.reloadStatus)
6464
}
@@ -103,7 +103,7 @@ public struct General {
103103
} catch let error as XPCCommunicationBridgeError {
104104
Logger.ui.error("Failed to reach communication bridge. \(error.localizedDescription)")
105105
toast(
106-
"Failed to reach communication bridge. \(error.localizedDescription)",
106+
"Unable to connect to the communication bridge. The helper application didn't respond. This may be due to missing background permissions.",
107107
.error
108108
)
109109
await send(.failedReloading)

Diff for: Core/Sources/HostApp/LaunchAgentManager.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22
import LaunchAgentManager
33

4-
extension LaunchAgentManager {
4+
public extension LaunchAgentManager {
55
init() {
66
self.init(
77
serviceIdentifier: Bundle.main

Diff for: Core/Sources/LaunchAgentManager/LaunchAgentManager.swift

+8
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ public struct LaunchAgentManager {
3333
await removeObsoleteLaunchAgent()
3434
}
3535
}
36+
37+
@available(macOS 13.0, *)
38+
public func isBackgroundPermissionGranted() async -> Bool {
39+
// On macOS 13+, check SMAppService status
40+
let bridgeLaunchAgent = SMAppService.agent(plistName: "bridgeLaunchAgent.plist")
41+
let status = bridgeLaunchAgent.status
42+
return status != .requiresApproval
43+
}
3644

3745
public func setupLaunchAgent() async throws {
3846
if #available(macOS 13, *) {

Diff for: Core/Sources/Service/Service.swift

+17-4
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,28 @@ public final class Service {
114114
}.store(in: &cancellable)
115115

116116
// Combine both workspace and auth status changes into a single stream
117-
await Publishers.CombineLatest(
117+
await Publishers.CombineLatest3(
118+
XcodeInspector.shared.safe.$latestActiveXcode,
118119
XcodeInspector.shared.safe.$activeWorkspaceURL
119120
.removeDuplicates(),
120121
StatusObserver.shared.$authStatus
121122
.removeDuplicates()
122123
)
123124
.receive(on: DispatchQueue.main)
124-
.sink { [weak self] newURL, newStatus in
125-
self?.onNewActiveWorkspaceURLOrAuthStatus(newURL: newURL, newStatus: newStatus)
125+
.sink { [weak self] newXcode, newURL, newStatus in
126+
// First check for realtimeWorkspaceURL if activeWorkspaceURL is nil
127+
if let realtimeURL = newXcode?.realtimeWorkspaceURL, newURL == nil {
128+
self?.onNewActiveWorkspaceURLOrAuthStatus(
129+
newURL: realtimeURL,
130+
newStatus: newStatus
131+
)
132+
} else if let newURL = newURL {
133+
// Then use activeWorkspaceURL if available
134+
self?.onNewActiveWorkspaceURLOrAuthStatus(
135+
newURL: newURL,
136+
newStatus: newStatus
137+
)
138+
}
126139
}
127140
.store(in: &cancellable)
128141
}
@@ -137,7 +150,7 @@ public final class Service {
137150

138151
private func getDisplayNameOfXcodeWorkspace(url: URL) -> String {
139152
var name = url.lastPathComponent
140-
let suffixes = [".xcworkspace", ".xcodeproj"]
153+
let suffixes = [".xcworkspace", ".xcodeproj", ".playground"]
141154
for suffix in suffixes {
142155
if name.hasSuffix(suffix) {
143156
name = String(name.dropLast(suffix.count))

Diff for: ExtensionService/XPCController.swift

+23-4
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,38 @@ final class XPCController: XPCServiceDelegate {
3939
func createPingTask() {
4040
pingTask?.cancel()
4141
pingTask = Task { [weak self] in
42+
var consecutiveFailures = 0
43+
var backoffDelay = 1_000_000_000 // Start with 1 second
44+
4245
while !Task.isCancelled {
4346
guard let self else { return }
4447
do {
4548
try await self.bridge.updateServiceEndpoint(self.xpcListener.endpoint)
46-
try await Task.sleep(nanoseconds: 60_000_000_000)
49+
// Reset on success
50+
consecutiveFailures = 0
51+
backoffDelay = 1_000_000_000
52+
try await Task.sleep(nanoseconds: 60_000_000_000) // 60 seconds between successful pings
4753
} catch {
48-
try await Task.sleep(nanoseconds: 1_000_000_000)
54+
consecutiveFailures += 1
55+
// Log only on 1st, 5th (31 sec), 10th failures, etc. to avoid flooding
56+
let shouldLog = consecutiveFailures == 1 || consecutiveFailures % 5 == 0
57+
4958
#if DEBUG
5059
// No log, but you should run CommunicationBridge, too.
5160
#else
52-
Logger.service
53-
.error("Failed to connect to bridge: \(error.localizedDescription)")
61+
if consecutiveFailures == 5 {
62+
if #available(macOS 13.0, *) {
63+
showBackgroundPermissionAlert()
64+
}
65+
}
66+
if shouldLog {
67+
Logger.service.error("Failed to connect to bridge (\(consecutiveFailures) consecutive failures): \(error.localizedDescription)")
68+
}
5469
#endif
70+
71+
// Exponential backoff with a cap
72+
backoffDelay = min(backoffDelay * 2, 120_000_000_000) // Cap at 120 seconds
73+
try await Task.sleep(nanoseconds: UInt64(backoffDelay))
5574
}
5675
}
5776
}

Diff for: Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ extension CustomJSONRPCLanguageServer {
311311
.updateCLSStatus(
312312
payload.kind.clsStatus,
313313
busy: payload.busy,
314-
message: payload.message
314+
message: payload.message ?? ""
315315
)
316316
}
317317
}

Diff for: Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ public enum GitHubCopilotNotification {
423423

424424
public var kind: StatusKind
425425
public var busy: Bool
426-
public var message: String
426+
public var message: String?
427427

428428
public static func decode(fromParams params: JSONValue?) -> StatusNotification? {
429429
try? JSONDecoder().decode(Self.self, from: (try? JSONEncoder().encode(params)) ?? Data())

Diff for: Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift

+13-2
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,18 @@ public class GitHubCopilotBaseService {
207207
server.defaultTimeout = 60
208208
server.initializeParamsProvider = {
209209
let capabilities = ClientCapabilities(
210-
workspace: nil,
210+
workspace: .init(
211+
applyEdit: false,
212+
workspaceEdit: nil,
213+
didChangeConfiguration: nil,
214+
didChangeWatchedFiles: nil,
215+
symbol: nil,
216+
executeCommand: nil,
217+
/// enable for "watchedFiles capability", set others to default value
218+
workspaceFolders: true,
219+
configuration: nil,
220+
semanticTokens: nil
221+
),
211222
textDocument: nil,
212223
window: nil,
213224
general: nil,
@@ -726,13 +737,13 @@ public final class GitHubCopilotService:
726737
private func updateServiceAuthStatus(_ status: GitHubCopilotRequest.CheckStatus.Response) async {
727738
Logger.gitHubCopilot.info("check status response: \(status)")
728739
if status.status == .ok || status.status == .maybeOk {
740+
await Status.shared.updateAuthStatus(.loggedIn, username: status.user)
729741
if !CopilotModelManager.hasLLMs() {
730742
let models = try? await models()
731743
if let models = models, !models.isEmpty {
732744
CopilotModelManager.updateLLMs(models)
733745
}
734746
}
735-
await Status.shared.updateAuthStatus(.loggedIn, username: status.user)
736747
await unwatchAuthStatus()
737748
} else if status.status == .notAuthorized {
738749
await Status.shared

Diff for: Tool/Sources/XPCShared/XPCCommunicationBridge.swift

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

45
public enum XPCCommunicationBridgeError: Swift.Error, LocalizedError {
56
case failedToCreateXPCConnection
@@ -79,3 +80,18 @@ extension XPCCommunicationBridge {
7980
}
8081
}
8182

83+
@available(macOS 13.0, *)
84+
public func showBackgroundPermissionAlert() {
85+
let alert = NSAlert()
86+
alert.messageText = "Background Permission Required"
87+
alert.informativeText = "GitHub Copilot for Xcode needs permission to run in the background. Without this permission, features won't work correctly."
88+
alert.alertStyle = .warning
89+
90+
alert.addButton(withTitle: "Open Settings")
91+
alert.addButton(withTitle: "Later")
92+
93+
let response = alert.runModal()
94+
if response == .alertFirstButtonReturn {
95+
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.LoginItems-Settings.extension")!)
96+
}
97+
}

0 commit comments

Comments
 (0)