Skip to content

Commit a50045a

Browse files
committed
Pre-release 0.32.110
1 parent 98e2f48 commit a50045a

File tree

68 files changed

+2552
-445
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2552
-445
lines changed

.github/actions/set-xcode-version/action.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ inputs:
66
Xcode version to use, in semver(ish)-style matching the format on the Actions runner image.
77
See available versions at https://github.com/actions/runner-images/blame/main/images/macos/macos-14-Readme.md#xcode
88
required: false
9-
default: '15.3'
9+
default: '16.2'
1010
outputs:
1111
xcode-path:
1212
description: "Path to current Xcode version"

CommunicationBridge/ServiceDelegate.swift

+93-21
Original file line numberDiff line numberDiff line change
@@ -136,28 +136,100 @@ actor ExtensionServiceLauncher {
136136
isLaunching = true
137137

138138
Logger.communicationBridge.info("Launching extension service app.")
139-
140-
NSWorkspace.shared.openApplication(
141-
at: appURL,
142-
configuration: {
143-
let configuration = NSWorkspace.OpenConfiguration()
144-
configuration.createsNewApplicationInstance = false
145-
configuration.addsToRecentItems = false
146-
configuration.activates = false
147-
return configuration
148-
}()
149-
) { app, error in
150-
if let error = error {
151-
Logger.communicationBridge.error(
152-
"Failed to launch extension service app: \(error)"
153-
)
154-
} else {
155-
Logger.communicationBridge.info(
156-
"Finished launching extension service app."
157-
)
139+
140+
// First check if the app is already running
141+
if let runningApp = NSWorkspace.shared.runningApplications.first(where: {
142+
$0.bundleIdentifier == appIdentifier
143+
}) {
144+
Logger.communicationBridge.info("Extension service app already running with PID: \(runningApp.processIdentifier)")
145+
self.application = runningApp
146+
self.isLaunching = false
147+
return
148+
}
149+
150+
// Implement a retry mechanism with exponential backoff
151+
Task {
152+
var retryCount = 0
153+
let maxRetries = 3
154+
var success = false
155+
156+
while !success && retryCount < maxRetries {
157+
do {
158+
// Add a delay between retries with exponential backoff
159+
if retryCount > 0 {
160+
let delaySeconds = pow(2.0, Double(retryCount - 1))
161+
Logger.communicationBridge.info("Retrying launch after \(delaySeconds) seconds (attempt \(retryCount + 1) of \(maxRetries))")
162+
try await Task.sleep(nanoseconds: UInt64(delaySeconds * 1_000_000_000))
163+
}
164+
165+
// Use a task-based approach for launching with timeout
166+
let launchTask = Task<NSRunningApplication?, Error> { () -> NSRunningApplication? in
167+
return await withCheckedContinuation { continuation in
168+
NSWorkspace.shared.openApplication(
169+
at: appURL,
170+
configuration: {
171+
let configuration = NSWorkspace.OpenConfiguration()
172+
configuration.createsNewApplicationInstance = false
173+
configuration.addsToRecentItems = false
174+
configuration.activates = false
175+
return configuration
176+
}()
177+
) { app, error in
178+
if let error = error {
179+
continuation.resume(returning: nil)
180+
} else {
181+
continuation.resume(returning: app)
182+
}
183+
}
184+
}
185+
}
186+
187+
// Set a timeout for the launch operation
188+
let timeoutTask = Task {
189+
try await Task.sleep(nanoseconds: 10_000_000_000) // 10 seconds
190+
return
191+
}
192+
193+
// Wait for either the launch or the timeout
194+
let app = try await withTaskCancellationHandler {
195+
try await launchTask.value ?? nil
196+
} onCancel: {
197+
launchTask.cancel()
198+
}
199+
200+
// Cancel the timeout task
201+
timeoutTask.cancel()
202+
203+
if let app = app {
204+
// Success!
205+
self.application = app
206+
success = true
207+
break
208+
} else {
209+
// App is nil, retry
210+
retryCount += 1
211+
Logger.communicationBridge.info("Launch attempt \(retryCount) failed, app is nil")
212+
}
213+
} catch {
214+
retryCount += 1
215+
Logger.communicationBridge.error("Error during launch attempt \(retryCount): \(error.localizedDescription)")
216+
}
158217
}
159-
160-
self.application = app
218+
219+
// Double-check we have a valid application
220+
if !success && self.application == nil {
221+
// After all retries, check once more if the app is running (it might have launched but we missed the callback)
222+
if let runningApp = NSWorkspace.shared.runningApplications.first(where: {
223+
$0.bundleIdentifier == appIdentifier
224+
}) {
225+
Logger.communicationBridge.info("Found running extension service after retries: \(runningApp.processIdentifier)")
226+
self.application = runningApp
227+
success = true
228+
} else {
229+
Logger.communicationBridge.info("Failed to launch extension service after \(maxRetries) attempts")
230+
}
231+
}
232+
161233
self.isLaunching = false
162234
}
163235
}

Copilot for Xcode/App.swift

+126-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,113 @@
1+
import SwiftUI
12
import Client
23
import HostApp
34
import LaunchAgentManager
45
import SharedUIComponents
5-
import SwiftUI
66
import UpdateChecker
77
import XPCShared
8+
import HostAppActivator
89

910
struct VisualEffect: NSViewRepresentable {
1011
func makeNSView(context: Self.Context) -> NSView { return NSVisualEffectView() }
1112
func updateNSView(_ nsView: NSView, context: Context) { }
1213
}
1314

1415
class AppDelegate: NSObject, NSApplicationDelegate {
15-
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { true }
16+
// Launch modes supported by the app
17+
enum LaunchMode {
18+
case chat
19+
case settings
20+
}
21+
22+
func applicationDidFinishLaunching(_ notification: Notification) {
23+
let launchMode = determineLaunchMode()
24+
handleLaunchMode(launchMode)
25+
}
26+
27+
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
28+
let launchMode = determineLaunchMode()
29+
handleLaunchMode(launchMode)
30+
return true
31+
}
32+
33+
// MARK: - Helper Methods
34+
35+
private func determineLaunchMode() -> LaunchMode {
36+
let launchArgs = CommandLine.arguments
37+
if launchArgs.contains("--settings") {
38+
return .settings
39+
} else {
40+
return .chat
41+
}
42+
}
43+
44+
private func handleLaunchMode(_ mode: LaunchMode) {
45+
switch mode {
46+
case .settings:
47+
openSettings()
48+
case .chat:
49+
openChat()
50+
}
51+
}
52+
53+
private func openSettings() {
54+
DispatchQueue.main.async {
55+
NSApp.activate(ignoringOtherApps: true)
56+
if #available(macOS 14.0, *) {
57+
let environment = SettingsEnvironment()
58+
environment.open()
59+
} else if #available(macOS 13.0, *) {
60+
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
61+
} else {
62+
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
63+
}
64+
}
65+
}
66+
67+
private func openChat() {
68+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
69+
Task {
70+
let service = try? getService()
71+
try? await service?.openChat()
72+
}
73+
}
74+
}
75+
76+
// MARK: - Application Termination
77+
78+
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
79+
// Immediately terminate extension service if it's running
80+
if let extensionService = NSWorkspace.shared.runningApplications.first(where: {
81+
$0.bundleIdentifier == "\(Bundle.main.bundleIdentifier!).ExtensionService"
82+
}) {
83+
extensionService.terminate()
84+
}
85+
86+
// Start cleanup in background without waiting
87+
Task {
88+
let quitTask = Task {
89+
let service = try? getService()
90+
try? await service?.quitService()
91+
}
92+
93+
// Wait just a tiny bit to allow cleanup to start
94+
try? await Task.sleep(nanoseconds: 100_000_000) // 100ms
95+
96+
DispatchQueue.main.async {
97+
NSApp.reply(toApplicationShouldTerminate: true)
98+
}
99+
}
100+
101+
return .terminateLater
102+
}
103+
104+
func applicationWillTerminate(_ notification: Notification) {
105+
if let extensionService = NSWorkspace.shared.runningApplications.first(where: {
106+
$0.bundleIdentifier == "\(Bundle.main.bundleIdentifier!).ExtensionService"
107+
}) {
108+
extensionService.terminate()
109+
}
110+
}
16111
}
17112

18113
class AppUpdateCheckerDelegate: UpdateCheckerDelegate {
@@ -28,16 +123,40 @@ class AppUpdateCheckerDelegate: UpdateCheckerDelegate {
28123
@main
29124
struct CopilotForXcodeApp: App {
30125
@NSApplicationDelegateAdaptor private var appDelegate: AppDelegate
126+
127+
init() {
128+
UserDefaults.setupDefaultSettings()
129+
130+
Task {
131+
await hostAppStore
132+
.send(.general(.setupLaunchAgentIfNeeded))
133+
.finish()
134+
}
135+
136+
DistributedNotificationCenter.default().addObserver(
137+
forName: .openSettingsWindowRequest,
138+
object: nil,
139+
queue: .main
140+
) { _ in
141+
DispatchQueue.main.async {
142+
NSApp.activate(ignoringOtherApps: true)
143+
if #available(macOS 14.0, *) {
144+
let environment = SettingsEnvironment()
145+
environment.open()
146+
} else if #available(macOS 13.0, *) {
147+
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
148+
} else {
149+
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
150+
}
151+
}
152+
}
153+
}
31154

32155
var body: some Scene {
33-
WindowGroup {
156+
Settings {
34157
TabContainer()
35158
.frame(minWidth: 800, minHeight: 600)
36159
.background(VisualEffect().ignoresSafeArea())
37-
.onAppear {
38-
UserDefaults.setupDefaultSettings()
39-
}
40-
.copilotIntroSheet()
41160
.environment(\.updateChecker, UpdateChecker(
42161
hostBundle: Bundle.main,
43162
checkerDelegate: AppUpdateCheckerDelegate()

Copilot for Xcode/Assets.xcassets/ChatIcon.imageset/Chat.svg

-10
This file was deleted.
Loading
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
{
22
"images" : [
33
{
4-
"filename" : "Chat.svg",
4+
"filename" : "ChatIcon.svg",
55
"idiom" : "universal"
66
}
77
],
88
"info" : {
99
"author" : "xcode",
1010
"version" : 1
11+
},
12+
"properties" : {
13+
"preserves-vector-representation" : true
1114
}
1215
}

Core/Package.swift

+6-4
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,12 @@ let package = Package(
178178
.product(name: "AXHelper", package: "Tool"),
179179
.product(name: "ConversationServiceProvider", package: "Tool"),
180180
.product(name: "GitHubCopilotService", package: "Tool"),
181+
.product(name: "Workspace", package: "Tool")
181182
]),
183+
.testTarget(
184+
name: "ChatServiceTests",
185+
dependencies: ["ChatService"]
186+
),
182187

183188
.target(
184189
name: "ConversationTab",
@@ -196,10 +201,6 @@ let package = Package(
196201
.product(name: "Persist", package: "Tool")
197202
]
198203
),
199-
.testTarget(
200-
name: "ConversationTabTests",
201-
dependencies: ["ConversationTab"]
202-
),
203204

204205
// MARK: - UI
205206

@@ -218,6 +219,7 @@ let package = Package(
218219
.product(name: "ChatTab", package: "Tool"),
219220
.product(name: "Logger", package: "Tool"),
220221
.product(name: "CustomAsyncAlgorithms", package: "Tool"),
222+
.product(name: "HostAppActivator", package: "Tool"),
221223
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
222224
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
223225
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),

0 commit comments

Comments
 (0)