From 233516db94c99fe9ad1946f3414427d223210222 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 8 May 2025 18:03:06 +0800 Subject: [PATCH 01/27] Bump GitHub Copilot language server to 1.48.0 --- .../CopilotLocalProcessServer.swift | 2 +- .../GitHubCopilotInstallationManager.swift | 2 +- .../LanguageServer/GitHubCopilotRequest.swift | 79 ++++++++++--------- .../LanguageServer/GitHubCopilotService.swift | 1 + .../Services/GitHubCopilotChatService.swift | 12 ++- 5 files changed, 53 insertions(+), 43 deletions(-) diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift index 3f79212e..884a4d83 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift @@ -141,7 +141,7 @@ extension CopilotLocalProcessServer: LanguageServerProtocol.Server { /// Cancel ongoing completion requests. public func cancelOngoingTasks() async { - guard let server = wrappedServer, process.isRunning else { + guard let _ = wrappedServer, process.isRunning else { return } diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift index 640a09a3..ed14198a 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift @@ -11,7 +11,7 @@ public struct GitHubCopilotInstallationManager { return URL(string: link)! } - static let latestSupportedVersion = "1.41.0" + static let latestSupportedVersion = "1.48.0" static let minimumSupportedVersion = "1.32.0" public init() {} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift index 87219683..50ae08ee 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift @@ -57,7 +57,7 @@ enum GitHubCopilotChatSource: String, Codable { enum GitHubCopilotRequest { struct SetEditorInfo: GitHubCopilotRequestType { let xcodeVersion: String - + struct Response: Codable {} var networkProxy: JSONValue? { @@ -143,7 +143,7 @@ enum GitHubCopilotRequest { var dict: [String: JSONValue] = [ "editorInfo": pretendToBeVSCode ? .hash([ "name": "vscode", - "version": "1.89.1", + "version": "1.99.3", ]) : .hash([ "name": "Xcode", "version": .string(xcodeVersion), @@ -351,32 +351,48 @@ enum GitHubCopilotRequest { } struct RequestBody: Codable { - var workDoneToken: String - var turns: [Turn]; struct Turn: Codable { - var request: String - var response: String? + public struct Reference: Codable, Equatable, Hashable { + public var type: String = "file" + public let uri: String + public let position: Position? + public let visibleRange: SuggestionBasic.CursorRange? + public let selection: SuggestionBasic.CursorRange? + public let openedAt: String? + public let activeAt: String? } - var capabilities: Capabilities; struct Capabilities: Codable { - var allSkills: Bool? - var skills: [String] + enum ConversationSource: String, Codable { + case panel, inline + } + + enum ConversationMode: String, Codable { + case agent = "Agent" } - var options: [String: String]? - var doc: GitHubCopilotDoc? - var computeSuggestions: Bool? - var references: [Reference]?; struct Reference: Codable { - var uri: String - var position: Position? - var visibleRange: CursorRange? - var selectionRange: CursorRange? - var openedAt: Date? - var activatedAt: Date? + struct ConversationTurn: Codable { + var request: String + var response: String? + var turnId: String? } - var source: GitHubCopilotChatSource? // inline or panel + var workDoneToken: String + var turns: [ConversationTurn] + var capabilities: Capabilities + var textDocument: GitHubCopilotDoc? + var references: [Reference]? + var computeSuggestions: Bool? + var source: ConversationSource? var workspaceFolder: String? + var workspaceFolders: [WorkspaceFolder]? + var ignoredSkills: [String]? + var model: String? + var chatMode: ConversationMode? var userLanguage: String? + + struct Capabilities: Codable { + var skills: [String] + var allSkills: Bool? + } } let requestBody: RequestBody @@ -395,24 +411,13 @@ enum GitHubCopilotRequest { var workDoneToken: String var conversationId: String var message: String - var followUp: FollowUp?; struct FollowUp: Codable { - var id: String - var type: String - } - - var options: [String: String]? - var doc: GitHubCopilotDoc? - var computeSuggestions: Bool? - var references: [Reference]?; struct Reference: Codable { - var uri: String - var position: Position? - var visibleRange: CursorRange? - var selectionRange: CursorRange? - var openedAt: Date? - var activatedAt: Date? - } - + var textDocument: GitHubCopilotDoc? + var ignoredSkills: [String]? + var references: [ConversationCreate.RequestBody.Reference]? + var model: String? var workspaceFolder: String? + var workspaceFolders: [WorkspaceFolder]? + var chatMode: String? } let requestBody: RequestBody diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index 7979c76e..aad10af0 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -273,6 +273,7 @@ public class GitHubCopilotBaseService { let notifications = NotificationCenter.default .notifications(named: .gitHubCopilotShouldRefreshEditorInformation) Task { [weak self] in + print(await xcodeVersion()) _ = try? await server.sendRequest( GitHubCopilotRequest.SetEditorInfo(xcodeVersion: xcodeVersion() ?? "16.0") ) diff --git a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift index 1534b961..0e4f102e 100644 --- a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift +++ b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift @@ -40,8 +40,8 @@ public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { let request = GitHubCopilotRequest.ConversationCreate(requestBody: .init( workDoneToken: workDoneToken, turns: turns, - capabilities: .init(allSkills: false, skills: []), - doc: doc, + capabilities: .init(skills: [], allSkills: false), + textDocument: doc, source: .panel, workspaceFolder: workspace.projectURL.path, userLanguage: { @@ -141,7 +141,7 @@ public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { } extension GitHubCopilotChatService { - typealias Turn = GitHubCopilotRequest.ConversationCreate.RequestBody.Turn + typealias Turn = GitHubCopilotRequest.ConversationCreate.RequestBody.ConversationTurn func convertHistory(history: [Message], message: String) -> [Turn] { guard let firstIndexOfUserMessage = history.firstIndex(where: { $0.role == .user }) else { return [.init(request: message, response: nil)] } @@ -220,6 +220,10 @@ extension GitHubCopilotChatService { var responseIsIncomplete: Bool? var message: String? } + + struct Annotation: Decodable { + var id: Int + } var kind: String var title: String? @@ -229,7 +233,7 @@ extension GitHubCopilotChatService { var followUp: FollowUp? var suggestedTitle: String? var reply: String? - var annotations: [String]? + var annotations: [Annotation]? var hideText: Bool? var cancellationReason: String? var error: Error? From 415088e49f43dc858321aa8490646f2bdcdb9180 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 8 May 2025 22:26:22 +0800 Subject: [PATCH 02/27] Add model settings for GitHub Copilot but looks like it's not yet implemented --- .../ChatModelEditView.swift | 26 ++---- .../GitHubCopilotModelPicker.swift | 91 +++++++++++++++++++ .../AccountSettings/GitHubCopilotView.swift | 12 ++- .../GitHubCopilotExtension.swift | 63 ++++++++++++- .../LanguageServer/GitHubCopilotRequest.swift | 39 +++++++- .../LanguageServer/GitHubCopilotService.swift | 1 - .../Services/GitHubCopilotChatService.swift | 9 +- .../GitHubCopilotChatCompletionsService.swift | 4 +- Tool/Sources/Preferences/Keys.swift | 8 ++ 9 files changed, 226 insertions(+), 27 deletions(-) create mode 100644 Core/Sources/HostApp/AccountSettings/GitHubCopilotModelPicker.swift diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift index 752d6c6c..fd4256cd 100644 --- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift +++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift @@ -49,7 +49,7 @@ struct ChatModelEditView: View { .controlSize(.small) } } - + CustomBodyEdit(store: store) .disabled({ switch store.format { @@ -495,7 +495,7 @@ struct ChatModelEditView: View { TextField(text: $store.ollamaKeepAlive, prompt: Text("Default Value")) { Text("Keep Alive") } - + VStack(alignment: .leading, spacing: 8) { Text(Image(systemName: "exclamationmark.triangle.fill")) + Text( " For more details, please visit [https://ollama.com](https://ollama.com)." @@ -555,22 +555,12 @@ struct ChatModelEditView: View { var body: some View { WithPerceptionTracking { - TextField("Model Name", text: $store.modelName) - .overlay(alignment: .trailing) { - Picker( - "", - selection: $store.modelName, - content: { - if AvailableGitHubCopilotModel(rawValue: store.modelName) == nil { - Text("Custom Model").tag(store.modelName) - } - ForEach(AvailableGitHubCopilotModel.allCases, id: \.self) { model in - Text(model.rawValue).tag(model.rawValue) - } - } - ) - .frame(width: 20) - } + #warning("Todo: use the old picker and update the context window limit.") + GitHubCopilotModelPicker( + title: "Model Name", + hasDefaultModel: false, + gitHubCopilotModelId: $store.modelName + ) MaxTokensTextField(store: store) SupportsFunctionCallingToggle(store: store) diff --git a/Core/Sources/HostApp/AccountSettings/GitHubCopilotModelPicker.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotModelPicker.swift new file mode 100644 index 00000000..9f4b0d8d --- /dev/null +++ b/Core/Sources/HostApp/AccountSettings/GitHubCopilotModelPicker.swift @@ -0,0 +1,91 @@ +import Dependencies +import Foundation +import GitHubCopilotService +import Perception +import SwiftUI +import Toast + +public struct GitHubCopilotModelPicker: View { + @Perceptible + final class ViewModel { + var availableModels: [GitHubCopilotLLMModel] = [] + @PerceptionIgnored @Dependency(\.toast) var toast + + init() {} + + func appear() { + reloadAvailableModels() + } + + func disappear() {} + + func reloadAvailableModels() { + Task { @MainActor in + do { + availableModels = try await GitHubCopilotExtension.fetchLLMModels() + } catch { + toast("Failed to fetch GitHub Copilot models: \(error)", .error) + } + } + } + } + + let title: String + let hasDefaultModel: Bool + @Binding var gitHubCopilotModelId: String + @State var viewModel: ViewModel + + init( + title: String, + hasDefaultModel: Bool = true, + gitHubCopilotModelId: Binding + ) { + self.title = title + _gitHubCopilotModelId = gitHubCopilotModelId + self.hasDefaultModel = hasDefaultModel + viewModel = .init() + } + + public var body: some View { + WithPerceptionTracking { + TextField(title, text: $gitHubCopilotModelId) + .overlay(alignment: .trailing) { + Picker( + "", + selection: $gitHubCopilotModelId, + content: { + if hasDefaultModel { + Text("Default").tag("") + } + + if !gitHubCopilotModelId.isEmpty, + !viewModel.availableModels.contains(where: { + $0.modelId == gitHubCopilotModelId + }) + { + Text(gitHubCopilotModelId).tag(gitHubCopilotModelId) + } + if viewModel.availableModels.isEmpty { + Text({ + viewModel.reloadAvailableModels() + return "Loading..." + }()).tag("Loading...") + } + ForEach(viewModel.availableModels) { model in + Text(model.modelId) + .tag(model.modelId) + } + } + ) + .frame(width: 20) + } + .onAppear { + viewModel.appear() + } + .onDisappear { + viewModel.disappear() + } + } + } +} + diff --git a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift index 8fd049c1..e02d6ed9 100644 --- a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift +++ b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift @@ -25,6 +25,7 @@ struct GitHubCopilotView: View { var disableGitHubCopilotSettingsAutoRefreshOnAppear @AppStorage(\.gitHubCopilotLoadKeyChainCertificates) var gitHubCopilotLoadKeyChainCertificates + @AppStorage(\.gitHubCopilotModelId) var gitHubCopilotModelId init() {} } @@ -199,7 +200,7 @@ struct GitHubCopilotView: View { .foregroundColor(.secondary) .font(.callout) .dynamicHeightTextInFormWorkaround() - + Toggle(isOn: $settings.gitHubCopilotLoadKeyChainCertificates) { Text("Load certificates in keychain") } @@ -267,6 +268,14 @@ struct GitHubCopilotView: View { Button("Refresh configurations") { refreshConfiguration() } + + // Not available yet +// Form { +// GitHubCopilotModelPicker( +// title: "Chat Model Name", +// gitHubCopilotModelId: $settings.gitHubCopilotModelId +// ) +// } } SettingsDivider("Advanced") @@ -349,7 +358,6 @@ struct GitHubCopilotView: View { if status != .ok, status != .notSignedIn { toast( "GitHub Copilot status is not \"ok\". Please check if you have a valid GitHub Copilot subscription.", - .error ) } diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift index f18ac2be..8ff625f8 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift @@ -222,7 +222,7 @@ extension GitHubCopilotExtension { public static func fetchToken() async throws -> Token { guard let authToken = authInfo?.oauth_token else { throw GitHubCopilotError.notLoggedIn } - + let oldToken = await MainActor.run { cachedToken } if let oldToken { let expiresAt = Date(timeIntervalSince1970: TimeInterval(oldToken.expires_at)) @@ -230,7 +230,7 @@ extension GitHubCopilotExtension { return oldToken } } - + let url = URL(string: "https://api.github.com/copilot_internal/v2/token")! var request = URLRequest(url: url) request.httpMethod = "GET" @@ -255,5 +255,64 @@ extension GitHubCopilotExtension { throw error } } + + public static func fetchLLMModels() async throws -> [GitHubCopilotLLMModel] { + let token = try await GitHubCopilotExtension.fetchToken() + guard let endpoint = URL(string: token.endpoints.api + "/models") else { + throw CancellationError() + } + var request = URLRequest(url: endpoint) + request.setValue( + "Copilot for Xcode/\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown")", + forHTTPHeaderField: "Editor-Version" + ) + request.setValue("Bearer \(token.token)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("vscode-chat", forHTTPHeaderField: "Copilot-Integration-Id") + request.setValue("2023-07-07", forHTTPHeaderField: "X-Github-Api-Version") + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let response = response as? HTTPURLResponse else { + throw CancellationError() + } + + guard response.statusCode == 200 else { + throw CancellationError() + } + + struct Model: Decodable { + struct Limit: Decodable { + var max_context_window_tokens: Int + } + + struct Capability: Decodable { + var type: String? + var family: String? + var limit: Limit? + } + + var id: String + var capabilities: Capability + } + + struct Body: Decodable { + var data: [Model] + } + + let models = try JSONDecoder().decode(Body.self, from: data) + .data + .filter { + $0.capabilities.type == "chat" + } + .map { + GitHubCopilotLLMModel( + modelId: $0.id, + familyName: $0.capabilities.family ?? "", + contextWindow: $0.capabilities.limit?.max_context_window_tokens ?? 0 + ) + } + return models + } } diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift index 50ae08ee..d5681c1e 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotRequest.swift @@ -364,7 +364,7 @@ enum GitHubCopilotRequest { enum ConversationSource: String, Codable { case panel, inline } - + enum ConversationMode: String, Codable { case agent = "Agent" } @@ -464,5 +464,42 @@ enum GitHubCopilotRequest { return .custom("conversation/destroy", dict) } } + + struct CopilotModels: GitHubCopilotRequestType { + typealias Response = [GitHubCopilotModel] + + var request: ClientRequest { + .custom("copilot/models", .hash([:])) + } + } +} + +public struct GitHubCopilotModel: Codable, Equatable { + public let modelFamily: String + public let modelName: String + public let id: String +// public let modelPolicy: CopilotModelPolicy? + public let scopes: [GitHubCopilotPromptTemplateScope] + public let preview: Bool + public let isChatDefault: Bool + public let isChatFallback: Bool +// public let capabilities: CopilotModelCapabilities +// public let billing: CopilotModelBilling? +} + +public struct GitHubCopilotLLMModel: Equatable, Decodable, Identifiable { + public var id: String { modelId } + public var modelId: String + public var familyName: String + public var contextWindow: Int +} + +public enum GitHubCopilotPromptTemplateScope: String, Codable, Equatable { + case chatPanel = "chat-panel" + case editPanel = "edit-panel" + case agentPanel = "agent-panel" + case editor + case inline + case completion } diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index aad10af0..7979c76e 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -273,7 +273,6 @@ public class GitHubCopilotBaseService { let notifications = NotificationCenter.default .notifications(named: .gitHubCopilotShouldRefreshEditorInformation) Task { [weak self] in - print(await xcodeVersion()) _ = try? await server.sendRequest( GitHubCopilotRequest.SetEditorInfo(xcodeVersion: xcodeVersion() ?? "16.0") ) diff --git a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift index 0e4f102e..f3a76bb1 100644 --- a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift +++ b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift @@ -44,6 +44,13 @@ public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { textDocument: doc, source: .panel, workspaceFolder: workspace.projectURL.path, + model: { + let selectedModel = UserDefaults.shared.value(for: \.gitHubCopilotModelId) + if selectedModel.isEmpty { + return nil + } + return selectedModel + }(), userLanguage: { let language = UserDefaults.shared.value(for: \.chatGPTLanguage) if language.isEmpty { @@ -220,7 +227,7 @@ extension GitHubCopilotChatService { var responseIsIncomplete: Bool? var message: String? } - + struct Annotation: Decodable { var id: Int } diff --git a/Tool/Sources/OpenAIService/APIs/GitHubCopilotChatCompletionsService.swift b/Tool/Sources/OpenAIService/APIs/GitHubCopilotChatCompletionsService.swift index d9b2a51b..2f200909 100644 --- a/Tool/Sources/OpenAIService/APIs/GitHubCopilotChatCompletionsService.swift +++ b/Tool/Sources/OpenAIService/APIs/GitHubCopilotChatCompletionsService.swift @@ -79,7 +79,7 @@ actor GitHubCopilotChatCompletionsService: ChatCompletionsStreamAPI, ChatComplet endpoint: endpoint, requestBody: requestBody ) { request in - + // POST /chat/completions HTTP/2 // :authority: api.individual.githubcopilot.com // authorization: Bearer * @@ -97,7 +97,7 @@ actor GitHubCopilotChatCompletionsService: ChatCompletionsStreamAPI, ChatComplet // content-length: 9061 // accept: */* // accept-encoding: gzip,deflate,br - + request.setValue( "Copilot for Xcode/\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown")", forHTTPHeaderField: "Editor-Version" diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 22aa716a..0b19c26d 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -195,6 +195,14 @@ public extension UserDefaultPreferenceKeys { var gitHubCopilotPretendIDEToBeVSCode: PreferenceKey { .init(defaultValue: false, key: "GitHubCopilotPretendIDEToBeVSCode") } + + var gitHubCopilotModelId: PreferenceKey { + .init(defaultValue: "", key: "GitHubCopilotModelId") + } + + var gitHubCopilotModelFamily: PreferenceKey { + .init(defaultValue: "", key: "GitHubCopilotModelFamily") + } } // MARK: - Codeium Settings From 519cce13b196b1b70153279cf2679dce4d43b0e2 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 8 May 2025 22:53:02 +0800 Subject: [PATCH 03/27] Stop handling multiple selections in modification --- .../WindowBaseCommandHandler.swift | 38 +++++-------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index 7f971649..ce89620c 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -357,7 +357,7 @@ extension WindowBaseCommandHandler { ) async throws { guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL else { return } - let (workspace, filespace) = try await Service.shared.workspacePool + let (workspace, _) = try await Service.shared.workspacePool .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) guard workspace.suggestionPlugin?.isSuggestionFeatureEnabled ?? false else { presenter.presentErrorMessage("Prompt to code is disabled for this project") @@ -367,34 +367,16 @@ extension WindowBaseCommandHandler { let codeLanguage = languageIdentifierFromFileURL(fileURL) let selections: [CursorRange] = { - var all = [CursorRange]() - - // join the ranges if they overlaps in line - - for selection in editor.selections { - let range = CursorRange(start: selection.start, end: selection.end) - - func intersect(_ lhs: CursorRange, _ rhs: CursorRange) -> Bool { - lhs.start.line <= rhs.end.line && lhs.end.line >= rhs.start.line - } - - if let last = all.last, intersect(last, range) { - all[all.count - 1] = CursorRange( - start: .init( - line: min(last.start.line, range.start.line), - character: min(last.start.character, range.start.character) - ), - end: .init( - line: max(last.end.line, range.end.line), - character: max(last.end.character, range.end.character) - ) - ) - } else { - all.append(range) - } + if let firstSelection = editor.selections.first, + let lastSelection = editor.selections.last + { + let range = CursorRange( + start: firstSelection.start, + end: lastSelection.end + ) + return [range] } - - return all + return [] }() let snippets = selections.map { selection in From 87cae85987e20eb87c795000f21eaa63dfadb959 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 8 May 2025 23:18:59 +0800 Subject: [PATCH 04/27] Support disabling chat panel always on top --- .../Chat/ChatSettingsGeneralSectionView.swift | 13 +++++++++++-- .../Sources/SuggestionWidget/ChatPanelWindow.swift | 2 +- .../SuggestionWidget/WidgetWindowsController.swift | 14 +++++++++++--- Tool/Sources/Preferences/Keys.swift | 14 +++++++++----- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift index 93316505..910f3046 100644 --- a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift +++ b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift @@ -25,6 +25,7 @@ struct ChatSettingsGeneralSectionView: View { @AppStorage(\.chatModels) var chatModels @AppStorage(\.embeddingModels) var embeddingModels @AppStorage(\.wrapCodeInChatCodeBlock) var wrapCodeInCodeBlock + @AppStorage(\.alwaysDisableFloatOnTopForChatPanel) var alwaysDisableFloatOnTopForChatPanel @AppStorage( \.keepFloatOnTopIfChatPanelAndXcodeOverlaps ) var keepFloatOnTopIfChatPanelAndXcodeOverlaps @@ -298,13 +299,21 @@ struct ChatSettingsGeneralSectionView: View { CodeHighlightThemePicker(scenario: .chat) + Toggle(isOn: $settings.alwaysDisableFloatOnTopForChatPanel) { + Text("Always disable always-on-top.") + } + Toggle(isOn: $settings.disableFloatOnTopWhenTheChatPanelIsDetached) { Text("Disable always-on-top when the chat panel is detached") - } + }.disabled(settings.alwaysDisableFloatOnTopForChatPanel) Toggle(isOn: $settings.keepFloatOnTopIfChatPanelAndXcodeOverlaps) { Text("Keep always-on-top if the chat panel and Xcode overlaps and Xcode is active") - }.disabled(!settings.disableFloatOnTopWhenTheChatPanelIsDetached) + } + .disabled( + !settings.disableFloatOnTopWhenTheChatPanelIsDetached + || settings.alwaysDisableFloatOnTopForChatPanel + ) } } diff --git a/Core/Sources/SuggestionWidget/ChatPanelWindow.swift b/Core/Sources/SuggestionWidget/ChatPanelWindow.swift index cad0c7c0..c51cccf1 100644 --- a/Core/Sources/SuggestionWidget/ChatPanelWindow.swift +++ b/Core/Sources/SuggestionWidget/ChatPanelWindow.swift @@ -20,9 +20,9 @@ final class ChatPanelWindow: WidgetWindow { override var defaultCollectionBehavior: NSWindow.CollectionBehavior { [ .fullScreenAuxiliary, - .transient, .fullScreenPrimary, .fullScreenAllowsTiling, + .participatesInCycle ] } diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index d2a2b2b3..623b0e1a 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -533,10 +533,18 @@ extension WidgetWindowsController { @MainActor func adjustChatPanelWindowLevel() async { + let alwaysDisableChatPanelFlowOnTop = UserDefaults.shared + .value(for: \.alwaysDisableFloatOnTopForChatPanel) let disableFloatOnTopWhenTheChatPanelIsDetached = UserDefaults.shared .value(for: \.disableFloatOnTopWhenTheChatPanelIsDetached) let window = windows.chatPanelWindow + + if alwaysDisableChatPanelFlowOnTop { + window.setFloatOnTop(false) + return + } + guard disableFloatOnTopWhenTheChatPanelIsDetached else { window.setFloatOnTop(true) return @@ -587,7 +595,7 @@ extension WidgetWindowsController { let activeXcode = await XcodeInspector.shared.safe.activeXcode let xcode = activeXcode?.appElement - + let isXcodeActive = xcode?.isFrontmost ?? false [ @@ -600,7 +608,7 @@ extension WidgetWindowsController { $0.moveToActiveSpace() } } - + if isXcodeActive, !windows.chatPanelWindow.isDetached { windows.chatPanelWindow.moveToActiveSpace() } @@ -867,7 +875,7 @@ class WidgetWindow: CanBecomeKeyWindow { } } } - + func moveToActiveSpace() { let previousState = state state = .switchingSpace diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 0b19c26d..49d3bc8f 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -195,11 +195,11 @@ public extension UserDefaultPreferenceKeys { var gitHubCopilotPretendIDEToBeVSCode: PreferenceKey { .init(defaultValue: false, key: "GitHubCopilotPretendIDEToBeVSCode") } - + var gitHubCopilotModelId: PreferenceKey { .init(defaultValue: "", key: "GitHubCopilotModelId") } - + var gitHubCopilotModelFamily: PreferenceKey { .init(defaultValue: "", key: "GitHubCopilotModelFamily") } @@ -478,11 +478,15 @@ public extension UserDefaultPreferenceKeys { var preferredChatModelIdForWebScope: PreferenceKey { .init(defaultValue: "", key: "PreferredChatModelIdForWebScope") } - + var preferredChatModelIdForUtilities: PreferenceKey { .init(defaultValue: "", key: "PreferredChatModelIdForUtilities") } + var alwaysDisableFloatOnTopForChatPanel: PreferenceKey { + .init(defaultValue: false, key: "AlwaysDisableFloatOnTopForChatPanel") + } + var disableFloatOnTopWhenTheChatPanelIsDetached: PreferenceKey { .init(defaultValue: true, key: "DisableFloatOnTopWhenTheChatPanelIsDetached") } @@ -494,7 +498,7 @@ public extension UserDefaultPreferenceKeys { var openChatMode: PreferenceKey> { .init(defaultValue: .init(.chatPanel), key: "DefaultOpenChatMode") } - + var legacyOpenChatMode: DeprecatedPreferenceKey { .init(defaultValue: .chatPanel, key: "OpenChatMode") } @@ -731,7 +735,7 @@ public extension UserDefaultPreferenceKeys { var useCloudflareDomainNameForLicenseCheck: FeatureFlag { .init(defaultValue: false, key: "FeatureFlag-UseCloudflareDomainNameForLicenseCheck") } - + var doNotInstallLaunchAgentAutomatically: FeatureFlag { .init(defaultValue: false, key: "FeatureFlag-DoNotInstallLaunchAgentAutomatically") } From 2bb0a70bed5f82995ccfe4e00f0467999ad93bd7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 8 May 2025 23:29:10 +0800 Subject: [PATCH 05/27] Do not hide modification panel, instead change the level --- .../SuggestionWidget/ChatPanelWindow.swift | 10 ------ .../WidgetWindowsController.swift | 36 ++++++++++++++++--- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/Core/Sources/SuggestionWidget/ChatPanelWindow.swift b/Core/Sources/SuggestionWidget/ChatPanelWindow.swift index c51cccf1..645c07e6 100644 --- a/Core/Sources/SuggestionWidget/ChatPanelWindow.swift +++ b/Core/Sources/SuggestionWidget/ChatPanelWindow.swift @@ -90,16 +90,6 @@ final class ChatPanelWindow: WidgetWindow { center() } - func setFloatOnTop(_ isFloatOnTop: Bool) { - let targetLevel: NSWindow.Level = isFloatOnTop - ? .init(NSWindow.Level.floating.rawValue + 1) - : .normal - - if targetLevel != level { - level = targetLevel - } - } - var isWindowHidden: Bool = false { didSet { alphaValue = isPanelDisplayed && !isWindowHidden ? 1 : 0 diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index 623b0e1a..7b7f3c73 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -114,6 +114,7 @@ private extension WidgetWindowsController { await hideSuggestionPanelWindow() } await adjustChatPanelWindowLevel() + await adjustModificationPanelLevel() } guard currentApplicationProcessIdentifier != app.processIdentifier else { return } currentApplicationProcessIdentifier = app.processIdentifier @@ -395,7 +396,7 @@ extension WidgetWindowsController { let application = activeApp.appElement /// We need this to hide the windows when Xcode is minimized. let noFocus = application.focusedWindow == nil - windows.sharedPanelWindow.alphaValue = noFocus ? 0 : 1 + windows.sharedPanelWindow.alphaValue = 1 windows.suggestionPanelWindow.alphaValue = noFocus ? 0 : 1 windows.widgetWindow.alphaValue = noFocus ? 0 : 1 windows.toastWindow.alphaValue = noFocus ? 0 : 1 @@ -418,7 +419,7 @@ extension WidgetWindowsController { let previousAppIsXcode = previousActiveApplication?.isXcode ?? false - windows.sharedPanelWindow.alphaValue = noFocus ? 0 : 1 + windows.sharedPanelWindow.alphaValue = 1 windows.suggestionPanelWindow.alphaValue = noFocus ? 0 : 1 windows.widgetWindow.alphaValue = if noFocus { 0 @@ -441,7 +442,7 @@ extension WidgetWindowsController { .chatPanelWindow.isKeyWindow } } else { - windows.sharedPanelWindow.alphaValue = 0 + windows.sharedPanelWindow.alphaValue = 1 windows.suggestionPanelWindow.alphaValue = 0 windows.widgetWindow.alphaValue = 0 windows.toastWindow.alphaValue = 0 @@ -503,6 +504,7 @@ extension WidgetWindowsController { } await adjustChatPanelWindowLevel() + await adjustModificationPanelLevel() } let now = Date() @@ -531,6 +533,20 @@ extension WidgetWindowsController { lastUpdateWindowLocationTime = Date() } + @MainActor + func adjustModificationPanelLevel() async { + let window = windows.sharedPanelWindow + + let latestApp = await xcodeInspector.safe.activeApplication + let latestAppIsXcodeOrExtension = if let latestApp { + latestApp.isXcode || latestApp.isExtensionService + } else { + false + } + + window.setFloatOnTop(latestAppIsXcodeOrExtension) + } + @MainActor func adjustChatPanelWindowLevel() async { let alwaysDisableChatPanelFlowOnTop = UserDefaults.shared @@ -539,12 +555,12 @@ extension WidgetWindowsController { .value(for: \.disableFloatOnTopWhenTheChatPanelIsDetached) let window = windows.chatPanelWindow - + if alwaysDisableChatPanelFlowOnTop { window.setFloatOnTop(false) return } - + guard disableFloatOnTopWhenTheChatPanelIsDetached else { window.setFloatOnTop(true) return @@ -884,6 +900,16 @@ class WidgetWindow: CanBecomeKeyWindow { self.state = previousState } } + + func setFloatOnTop(_ isFloatOnTop: Bool) { + let targetLevel: NSWindow.Level = isFloatOnTop + ? .init(NSWindow.Level.floating.rawValue + 1) + : .normal + + if targetLevel != level { + level = targetLevel + } + } } func widgetLevel(_ addition: Int) -> NSWindow.Level { From a8d2710a0c3ea2dd5e6a483df8d2425efe9e5b12 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 8 May 2025 23:48:34 +0800 Subject: [PATCH 06/27] Adjust chat panel level behavior --- .../WidgetWindowsController.swift | 55 ++++++++----------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index 7b7f3c73..ac626529 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -400,12 +400,6 @@ extension WidgetWindowsController { windows.suggestionPanelWindow.alphaValue = noFocus ? 0 : 1 windows.widgetWindow.alphaValue = noFocus ? 0 : 1 windows.toastWindow.alphaValue = noFocus ? 0 : 1 - - if isChatPanelDetached { - windows.chatPanelWindow.isWindowHidden = false - } else { - windows.chatPanelWindow.isWindowHidden = noFocus - } } else if let activeApp, activeApp.isExtensionService { let noFocus = { guard let xcode = latestActiveXcode else { return true } @@ -435,20 +429,11 @@ extension WidgetWindowsController { 0 } windows.toastWindow.alphaValue = noFocus ? 0 : 1 - if isChatPanelDetached { - windows.chatPanelWindow.isWindowHidden = false - } else { - windows.chatPanelWindow.isWindowHidden = noFocus && !windows - .chatPanelWindow.isKeyWindow - } } else { windows.sharedPanelWindow.alphaValue = 1 windows.suggestionPanelWindow.alphaValue = 0 windows.widgetWindow.alphaValue = 0 windows.toastWindow.alphaValue = 0 - if !isChatPanelDetached { - windows.chatPanelWindow.isWindowHidden = true - } } } } @@ -543,7 +528,7 @@ extension WidgetWindowsController { } else { false } - + window.setFloatOnTop(latestAppIsXcodeOrExtension) } @@ -561,19 +546,9 @@ extension WidgetWindowsController { return } - guard disableFloatOnTopWhenTheChatPanelIsDetached else { - window.setFloatOnTop(true) - return - } - let state = store.withState { $0 } let isChatPanelDetached = state.chatPanelState.isDetached - guard isChatPanelDetached else { - window.setFloatOnTop(true) - return - } - let floatOnTopWhenOverlapsXcode = UserDefaults.shared .value(for: \.keepFloatOnTopIfChatPanelAndXcodeOverlaps) @@ -584,10 +559,8 @@ extension WidgetWindowsController { false } - if !floatOnTopWhenOverlapsXcode || !latestAppIsXcodeOrExtension { - window.setFloatOnTop(false) - } else { - guard let xcode = await xcodeInspector.safe.latestActiveXcode else { return } + async let overlap: Bool = { @MainActor in + guard let xcode = await xcodeInspector.safe.latestActiveXcode else { return false } let windowElements = xcode.appElement.windows let overlap = windowElements.contains { if let position = $0.position, let size = $0.size { @@ -601,8 +574,27 @@ extension WidgetWindowsController { } return false } + return overlap + }() - window.setFloatOnTop(overlap) + if latestAppIsXcodeOrExtension { + if floatOnTopWhenOverlapsXcode { + let overlap = await overlap + window.setFloatOnTop(overlap) + } else { + if disableFloatOnTopWhenTheChatPanelIsDetached, isChatPanelDetached { + window.setFloatOnTop(false) + } else { + window.setFloatOnTop(true) + } + } + } else { + if floatOnTopWhenOverlapsXcode { + let overlap = await overlap + window.setFloatOnTop(overlap) + } else { + window.setFloatOnTop(false) + } } } @@ -907,6 +899,7 @@ class WidgetWindow: CanBecomeKeyWindow { : .normal if targetLevel != level { + self.orderFrontRegardless() level = targetLevel } } From 380cffcb6cb1813e1b9982731c624da147296ca7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 8 May 2025 23:53:12 +0800 Subject: [PATCH 07/27] Fix open chat --- .../Service/GUI/GraphicalUserInterfaceController.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index 66cf677e..761d40e3 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -325,10 +325,9 @@ public final class GraphicalUserInterfaceController { } suggestionDependency.suggestionWidgetDataSource = widgetDataSource - suggestionDependency.onOpenChatClicked = { [weak self] in - Task { [weak self] in - await self?.store.send(.createAndSwitchToChatGPTChatTabIfNeeded).finish() - self?.store.send(.openChatPanel(forceDetach: false, activateThisApp: true)) + suggestionDependency.onOpenChatClicked = { + Task { + PseudoCommandHandler().openChat(forceDetach: false, activateThisApp: true) } } suggestionDependency.onOpenModificationButtonClicked = { From 38fcd66afb7f8dec07f3ffd2191cb3f46f9ec43d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 00:00:25 +0800 Subject: [PATCH 08/27] Hide chat panel by default --- Core/Sources/SuggestionWidget/WidgetWindowsController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index ac626529..2c6b264c 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -97,6 +97,10 @@ actor WidgetWindowsController: NSObject { } } } + + Task { @MainActor in + windows.chatPanelWindow.isPanelDisplayed = false + } } } From 3bb2629b7567892a2acc115a033c61b5aeb66514 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 00:00:40 +0800 Subject: [PATCH 09/27] Support destroying workspacePool from menu --- ExtensionService/AppDelegate+Menu.swift | 16 ++++++++++++++++ Tool/Sources/Workspace/WorkspacePool.swift | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/ExtensionService/AppDelegate+Menu.swift b/ExtensionService/AppDelegate+Menu.swift index 4714abb5..cc665bb9 100644 --- a/ExtensionService/AppDelegate+Menu.swift +++ b/ExtensionService/AppDelegate+Menu.swift @@ -2,6 +2,8 @@ import AppKit import Foundation import Preferences import XcodeInspector +import Dependencies +import Workspace extension AppDelegate { fileprivate var statusBarMenuIdentifier: NSUserInterfaceItemIdentifier { @@ -95,6 +97,12 @@ extension AppDelegate { action: #selector(reactivateObservationsToXcode), keyEquivalent: "" ) + + let resetWorkspacesItem = NSMenuItem( + title: "Reset workspaces", + action: #selector(destroyWorkspacePool), + keyEquivalent: "" + ) reactivateObservationsItem.target = self @@ -108,6 +116,7 @@ extension AppDelegate { statusBarMenu.addItem(xcodeInspectorDebug) statusBarMenu.addItem(accessibilityAPIPermission) statusBarMenu.addItem(reactivateObservationsItem) + statusBarMenu.addItem(resetWorkspacesItem) statusBarMenu.addItem(quitItem) statusBarMenu.delegate = self @@ -248,6 +257,13 @@ private extension AppDelegate { ) } } + + @objc func destroyWorkspacePool() { + @Dependency(\.workspacePool) var workspacePool: WorkspacePool + Task { + await workspacePool.destroy() + } + } } private extension NSMenuItem { diff --git a/Tool/Sources/Workspace/WorkspacePool.swift b/Tool/Sources/Workspace/WorkspacePool.swift index 8188671b..12d86106 100644 --- a/Tool/Sources/Workspace/WorkspacePool.swift +++ b/Tool/Sources/Workspace/WorkspacePool.swift @@ -68,6 +68,11 @@ public class WorkspacePool { } return nil } + + @WorkspaceActor + public func destroy() { + workspaces = [:] + } @WorkspaceActor public func fetchOrCreateWorkspace(workspaceURL: URL) async throws -> Workspace { From 9340275e615197fd7cd3ee8b2ed992893119b38d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 00:47:32 +0800 Subject: [PATCH 10/27] Fix command injection --- Core/Sources/ChatGPTChatTab/Chat.swift | 4 ++-- .../SuggestionCommandHandler/PseudoCommandHandler.swift | 4 ++-- Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index 9c89d81a..2923d904 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -206,9 +206,9 @@ struct Chat { "/bin/bash", arguments: [ "-c", - "xed -l \(reference.startLine ?? 0) \"\(reference.uri)\"", + "xed -l \(reference.startLine ?? 0) ${TARGET_FILE}", ], - environment: [:] + environment: ["TARGET_FILE": reference.uri] ) } catch { print(error) diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift index aad43195..cdc52558 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift @@ -505,9 +505,9 @@ struct PseudoCommandHandler: CommandHandler { "/bin/bash", arguments: [ "-c", - "xed -l \(line) \"\(fileURL.path)\"", + "xed -l \(line) ${TARGET_FILE}", ], - environment: [:] + environment: ["TARGET_FILE": fileURL.path], ) } catch { print(error) diff --git a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift index b2c48114..53f5bf39 100644 --- a/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift +++ b/Tool/Sources/GitIgnoreCheck/GitIgnoreCheck.swift @@ -54,9 +54,9 @@ public struct DefaultGitIgnoredChecker: GitIgnoredChecker { do { let result = try await terminal.runCommand( "/bin/bash", - arguments: ["-c", "git check-ignore \"\(fileURL.path)\""], + arguments: ["-c", "git check-ignore ${TARGET_FILE}"], currentDirectoryURL: gitFolderURL, - environment: [:] + environment: ["TARGET_FILE": fileURL.path] ) if result.isEmpty { return false } return true @@ -76,9 +76,9 @@ public struct DefaultGitIgnoredChecker: GitIgnoredChecker { do { let result = try await terminal.runCommand( "/bin/bash", - arguments: ["-c", "git check-ignore \(filePaths)"], + arguments: ["-c", "git check-ignore ${TARGET_FILE}"], currentDirectoryURL: gitFolderURL, - environment: [:] + environment: ["TARGET_FILE": filePaths] ) return result .split(whereSeparator: \.isNewline) From 2822025610db5f8f8fa260d337e91b9aef00abe8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 00:47:43 +0800 Subject: [PATCH 11/27] Revert chat window behavior --- Core/Sources/SuggestionWidget/ChatPanelWindow.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Sources/SuggestionWidget/ChatPanelWindow.swift b/Core/Sources/SuggestionWidget/ChatPanelWindow.swift index 645c07e6..cf9a4690 100644 --- a/Core/Sources/SuggestionWidget/ChatPanelWindow.swift +++ b/Core/Sources/SuggestionWidget/ChatPanelWindow.swift @@ -20,9 +20,9 @@ final class ChatPanelWindow: WidgetWindow { override var defaultCollectionBehavior: NSWindow.CollectionBehavior { [ .fullScreenAuxiliary, + .transient, .fullScreenPrimary, .fullScreenAllowsTiling, - .participatesInCycle ] } From e8df300d9cb5e3a1c80f9f7749fb93bec4f0022e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 00:47:51 +0800 Subject: [PATCH 12/27] Bump version --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index f6d1a07e..655461da 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,4 +1,4 @@ -APP_VERSION = 0.35.7 -APP_BUILD = 455 +APP_VERSION = 0.35.8 +APP_BUILD = 456 RELEASE_CHANNEL = RELEASE_NUMBER = 1 From 2398a9c961d36613d5a0e8ed23be30e40a44e2bc Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 02:17:35 +0800 Subject: [PATCH 13/27] Update modification panel to show all modifications --- .../GraphicalUserInterfaceController.swift | 9 +- .../SuggestionWidget/ChatWindowView.swift | 4 +- .../FeatureReducers/ChatPanel.swift | 8 +- .../FeatureReducers/PromptToCodeGroup.swift | 81 +++++++--- .../FeatureReducers/SharedPanel.swift | 3 +- .../FeatureReducers/WidgetPanel.swift | 2 +- .../PromptToCodePanelGroupView.swift | 140 ++++++++++++++++++ .../SuggestionWidget/SharedPanelView.swift | 12 +- .../PromptToCodePanelView.swift | 10 -- .../WidgetWindowsController.swift | 2 +- 10 files changed, 221 insertions(+), 50 deletions(-) create mode 100644 Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index 761d40e3..dfbd719a 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -103,7 +103,7 @@ struct GUI { chatTabPool.removeTab(of: id) } - case let .chatTab(_, .openNewTab(builder)): + case let .chatTab(.element(_, .openNewTab(builder))): return .run { send in if let (_, chatTabInfo) = await chatTabPool .createTab(from: builder.chatTabBuilder) @@ -223,7 +223,7 @@ struct GUI { await send(.suggestionWidget(.circularWidget(.widgetClicked))) } - case let .suggestionWidget(.chatPanel(.chatTab(id, .tabContentUpdated))): + case let .suggestionWidget(.chatPanel(.chatTab(.element(id, .tabContentUpdated)))): #if canImport(ChatTabPersistent) // when a tab is updated, persist it. return .run { send in @@ -319,7 +319,10 @@ public final class GraphicalUserInterfaceController { state.chatTabGroup.tabInfo[id: id] ?? .init(id: id, title: "") }, action: { childAction in - .suggestionWidget(.chatPanel(.chatTab(id: id, action: childAction))) + .suggestionWidget(.chatPanel(.chatTab(.element( + id: id, + action: childAction + )))) } ) } diff --git a/Core/Sources/SuggestionWidget/ChatWindowView.swift b/Core/Sources/SuggestionWidget/ChatWindowView.swift index ee655d0b..2144100c 100644 --- a/Core/Sources/SuggestionWidget/ChatWindowView.swift +++ b/Core/Sources/SuggestionWidget/ChatWindowView.swift @@ -125,7 +125,7 @@ struct ChatTitleBar: View { } } -private extension View { +extension View { func hideScrollIndicator() -> some View { if #available(macOS 13.0, *) { return scrollIndicators(.hidden) @@ -200,7 +200,7 @@ struct ChatTabBar: View { draggingTabId: $draggingTabId ) ) - + } else { ChatTabBarButton( store: store, diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanel.swift b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanel.swift index d782e6fd..28bf5bfc 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanel.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanel.swift @@ -77,7 +77,7 @@ public struct ChatPanel { case moveChatTab(from: Int, to: Int) case focusActiveChatTab - case chatTab(id: String, action: ChatTabItem.Action) + case chatTab(IdentifiedActionOf) } @Dependency(\.chatTabPool) var chatTabPool @@ -280,10 +280,10 @@ public struct ChatPanel { let id = state.chatTabGroup.selectedTabInfo?.id guard let id else { return .none } return .run { send in - await send(.chatTab(id: id, action: .focus)) + await send(.chatTab(.element(id: id, action: .focus))) } - case let .chatTab(id, .close): + case let .chatTab(.element(id, .close)): return .run { send in await send(.closeTabButtonClicked(id: id)) } @@ -291,7 +291,7 @@ public struct ChatPanel { case .chatTab: return .none } - }.forEach(\.chatTabGroup.tabInfo, action: /Action.chatTab) { + }.forEach(\.chatTabGroup.tabInfo, action: \.chatTab) { ChatTabItem() } } diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift index 98ac96c9..a60c489a 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift @@ -11,20 +11,14 @@ public struct PromptToCodeGroup { public var promptToCodes: IdentifiedArrayOf = [] public var activeDocumentURL: PromptToCodePanel.State.ID? = XcodeInspector.shared .realtimeActiveDocumentURL + public var selectedTabId: URL? public var activePromptToCode: PromptToCodePanel.State? { get { - if let detached = promptToCodes - .first(where: { !$0.promptToCodeState.isAttachedToTarget }) - { - return detached - } - guard let id = activeDocumentURL else { return nil } - return promptToCodes[id: id] + guard let selectedTabId else { return promptToCodes.first } + return promptToCodes[id: selectedTabId] ?? promptToCodes.first } set { - if let id = newValue?.id { - promptToCodes[id: id] = newValue - } + selectedTabId = newValue?.id } } } @@ -41,7 +35,11 @@ public struct PromptToCodeGroup { case discardAcceptedPromptToCodeIfNotContinuous(id: PromptToCodePanel.State.ID) case updateActivePromptToCode(documentURL: URL) case discardExpiredPromptToCode(documentURLs: [URL]) - case promptToCode(PromptToCodePanel.State.ID, PromptToCodePanel.Action) + case tabClicked(id: URL) + case closeTabButtonClicked(id: URL) + case switchToNextTab + case switchToPreviousTab + case promptToCode(IdentifiedActionOf) case activePromptToCode(PromptToCodePanel.Action) } @@ -51,9 +49,12 @@ public struct PromptToCodeGroup { Reduce { state, action in switch action { case let .activateOrCreatePromptToCode(s): - if let promptToCode = state.activePromptToCode { + if let promptToCode = state.activePromptToCode, s.id == promptToCode.id { return .run { send in - await send(.promptToCode(promptToCode.id, .focusOnTextField)) + await send(.promptToCode(.element( + id: promptToCode.id, + action: .focusOnTextField + ))) } } return .run { send in @@ -61,12 +62,15 @@ public struct PromptToCodeGroup { } case let .createPromptToCode(newPromptToCode, sendImmediately): // insert at 0 so it has high priority then the other detached prompt to codes - state.promptToCodes.insert(newPromptToCode, at: 0) + state.promptToCodes.append(newPromptToCode) return .run { send in if sendImmediately, !newPromptToCode.contextInputController.instruction.string.isEmpty { - await send(.promptToCode(newPromptToCode.id, .modifyCodeButtonTapped)) + await send(.promptToCode(.element( + id: newPromptToCode.id, + action: .modifyCodeButtonTapped + ))) } }.cancellable( id: PromptToCodePanel.CancellationKey.modifyCode(newPromptToCode.id), @@ -94,6 +98,37 @@ public struct PromptToCodeGroup { } return .none + case let .tabClicked(id): + state.selectedTabId = id + return .none + + case let .closeTabButtonClicked(id): + return .run { send in + await send(.promptToCode(.element( + id: id, + action: .cancelButtonTapped + ))) + } + + case .switchToNextTab: + if let selectedTabId = state.selectedTabId, + let index = state.promptToCodes.index(id: selectedTabId) + { + let nextIndex = (index + 1) % state.promptToCodes.count + state.selectedTabId = state.promptToCodes[nextIndex].id + } + return .none + + case .switchToPreviousTab: + if let selectedTabId = state.selectedTabId, + let index = state.promptToCodes.index(id: selectedTabId) + { + let previousIndex = (index - 1 + state.promptToCodes.count) % state + .promptToCodes.count + state.selectedTabId = state.promptToCodes[previousIndex].id + } + return .none + case .promptToCode: return .none @@ -104,22 +139,28 @@ public struct PromptToCodeGroup { .ifLet(\.activePromptToCode, action: \.activePromptToCode) { PromptToCodePanel() } - .forEach(\.promptToCodes, action: /Action.promptToCode, element: { + .forEach(\.promptToCodes, action: \.promptToCode, element: { PromptToCodePanel() }) Reduce { state, action in switch action { - case let .promptToCode(id, .cancelButtonTapped): + case let .promptToCode(.element(id, .cancelButtonTapped)): state.promptToCodes.remove(id: id) + let isEmpty = state.promptToCodes.isEmpty return .run { _ in - activatePreviousActiveXcode() + if isEmpty { + activatePreviousActiveXcode() + } } case .activePromptToCode(.cancelButtonTapped): - guard let id = state.activePromptToCode?.id else { return .none } + guard let id = state.selectedTabId else { return .none } state.promptToCodes.remove(id: id) + let isEmpty = state.promptToCodes.isEmpty return .run { _ in - activatePreviousActiveXcode() + if isEmpty { + activatePreviousActiveXcode() + } } default: return .none } diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/SharedPanel.swift b/Core/Sources/SuggestionWidget/FeatureReducers/SharedPanel.swift index b255949c..9f38210e 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/SharedPanel.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/SharedPanel.swift @@ -7,7 +7,6 @@ public struct SharedPanel { public struct Content { public var promptToCodeGroup = PromptToCodeGroup.State() var suggestion: PresentingCodeSuggestion? - public var promptToCode: PromptToCodePanel.State? { promptToCodeGroup.activePromptToCode } var error: String? } @@ -19,7 +18,7 @@ public struct SharedPanel { var isPanelDisplayed: Bool = false var isEmpty: Bool { if content.error != nil { return false } - if content.promptToCode != nil { return false } + if !content.promptToCodeGroup.promptToCodes.isEmpty { return false } if content.suggestion != nil, UserDefaults.shared .value(for: \.suggestionPresentationMode) == .floatingWidget { return false } diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetPanel.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetPanel.swift index 059a377d..96414ef2 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetPanel.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetPanel.swift @@ -118,7 +118,7 @@ public struct WidgetPanel { case .sharedPanel(.promptToCodeGroup(.activateOrCreatePromptToCode)), .sharedPanel(.promptToCodeGroup(.createPromptToCode)): - let hasPromptToCode = state.content.promptToCode != nil + let hasPromptToCode = !state.content.promptToCodeGroup.promptToCodes.isEmpty return .run { send in await send(.displayPanelContent) diff --git a/Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift b/Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift new file mode 100644 index 00000000..a5b6bc96 --- /dev/null +++ b/Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift @@ -0,0 +1,140 @@ +import ComposableArchitecture +import Foundation +import SwiftUI + +struct PromptToCodePanelGroupView: View { + let store: StoreOf + + var body: some View { + WithPerceptionTracking { + VStack(spacing: 0) { + PromptToCodeTabBar(store: store) + .frame(height: 26) + + Divider() + + if let store = self.store.scope( + state: \.activePromptToCode, + action: \.activePromptToCode + ) { + PromptToCodePanelView(store: store) + } + } + .background(.ultraThickMaterial) + .xcodeStyleFrame() + } + } +} + +struct PromptToCodeTabBar: View { + let store: StoreOf + + struct TabInfo: Equatable, Identifiable { + var id: URL + var tabTitle: String + var isProcessing: Bool + } + + var body: some View { + HStack(spacing: 0) { + Divider() + Tabs(store: store) + } + .background { + Button(action: { store.send(.switchToNextTab) }) { EmptyView() } + .opacity(0) + .keyboardShortcut("]", modifiers: [.command, .shift]) + Button(action: { store.send(.switchToPreviousTab) }) { EmptyView() } + .opacity(0) + .keyboardShortcut("[", modifiers: [.command, .shift]) + } + } + + struct Tabs: View { + let store: StoreOf + + var body: some View { + WithPerceptionTracking { + let tabInfo = store.promptToCodes.map { + TabInfo( + id: $0.id, + tabTitle: $0.filename, + isProcessing: $0.promptToCodeState.isGenerating + ) + } + let selectedTabId = store.selectedTabId + ?? store.promptToCodes.first?.id + + ScrollViewReader { proxy in + ScrollView(.horizontal) { + HStack(spacing: 0) { + ForEach(tabInfo) { info in + WithPerceptionTracking { + PromptToCodeTabBarButton( + store: store, + info: info, + isSelected: info.id == store.selectedTabId + ) + .id(info.id) + } + } + } + } + .hideScrollIndicator() + .onChange(of: selectedTabId) { id in + withAnimation(.easeInOut(duration: 0.2)) { + proxy.scrollTo(id) + } + } + } + } + } + } +} + +struct PromptToCodeTabBarButton: View { + let store: StoreOf + let info: PromptToCodeTabBar.TabInfo + let isSelected: Bool + @State var isHovered: Bool = false + + var body: some View { + HStack(spacing: 0) { + HStack(spacing: 4) { + if info.isProcessing { + ProgressView() + .controlSize(.small) + } + Text(info.tabTitle) + } + .font(.callout) + .lineLimit(1) + .frame(maxWidth: 120) + .padding(.horizontal, 28) + .contentShape(Rectangle()) + .onTapGesture { + store.send(.tabClicked(id: info.id)) + } + .overlay(alignment: .leading) { + Button(action: { + store.send(.closeTabButtonClicked(id: info.id)) + }) { + Image(systemName: "xmark") + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + .padding(2) + .padding(.leading, 8) + .opacity(isHovered ? 1 : 0) + } + .onHover { isHovered = $0 } + .animation(.linear(duration: 0.1), value: isHovered) + .animation(.linear(duration: 0.1), value: isSelected) + + Divider().padding(.vertical, 6) + } + .background(isSelected ? Color(nsColor: .selectedControlColor) : Color.clear) + .frame(maxHeight: .infinity) + } +} + diff --git a/Core/Sources/SuggestionWidget/SharedPanelView.swift b/Core/Sources/SuggestionWidget/SharedPanelView.swift index 1c1c1a91..a00b2fee 100644 --- a/Core/Sources/SuggestionWidget/SharedPanelView.swift +++ b/Core/Sources/SuggestionWidget/SharedPanelView.swift @@ -72,7 +72,7 @@ struct SharedPanelView: View { ZStack(alignment: .topLeading) { if let errorMessage = store.content.error { error(errorMessage) - } else if let _ = store.content.promptToCode { + } else if !store.content.promptToCodeGroup.promptToCodes.isEmpty { promptToCode() } else if let suggestionProvider = store.content.suggestion { suggestion(suggestionProvider) @@ -93,12 +93,10 @@ struct SharedPanelView: View { @ViewBuilder func promptToCode() -> some View { - if let store = store.scope( - state: \.content.promptToCodeGroup.activePromptToCode, - action: \.promptToCodeGroup.activePromptToCode - ) { - PromptToCodePanelView(store: store) - } + PromptToCodePanelGroupView(store: store.scope( + state: \.content.promptToCodeGroup, + action: \.promptToCodeGroup + )) } @ViewBuilder diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanelView.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanelView.swift index 930149ec..01913e47 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanelView.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanelView.swift @@ -38,8 +38,6 @@ struct PromptToCodePanelView: View { } } } - .background(.ultraThickMaterial) - .xcodeStyleFrame() } .task { await MainActor.run { @@ -56,14 +54,6 @@ extension PromptToCodePanelView { var body: some View { WithPerceptionTracking { VStack(spacing: 0) { - HStack { - SelectionRangeButton(store: store) - Spacer() - } - .padding(2) - - Divider() - if let previousStep = store.promptToCodeState.history.last { Button(action: { store.send(.revertButtonTapped) diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index 2c6b264c..2318125e 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -755,7 +755,7 @@ public final class WidgetWindows { it.setIsVisible(true) it.canBecomeKeyChecker = { [store] in store.withState { state in - state.panelState.sharedPanelState.content.promptToCode != nil + !state.panelState.sharedPanelState.content.promptToCodeGroup.promptToCodes.isEmpty } } return it From cc6ed154304cb61e9940d91f96c3970ffbda47ea Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 02:18:41 +0800 Subject: [PATCH 14/27] Jump to new modification when started --- .../SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift index a60c489a..5225dd90 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift @@ -63,6 +63,7 @@ public struct PromptToCodeGroup { case let .createPromptToCode(newPromptToCode, sendImmediately): // insert at 0 so it has high priority then the other detached prompt to codes state.promptToCodes.append(newPromptToCode) + state.selectedTabId = newPromptToCode.id return .run { send in if sendImmediately, !newPromptToCode.contextInputController.instruction.string.isEmpty From 297076d07423ca95ecea61aeea48a39f2de3b9e0 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 02:28:16 +0800 Subject: [PATCH 15/27] Move to the target file before accepting a modification --- .../PseudoCommandHandler.swift | 29 +++++++++++++------ .../FeatureReducers/PromptToCodePanel.swift | 9 +++++- .../CommandHandler/CommandHandler.swift | 15 ++++++++-- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift index cdc52558..c2316a8b 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift @@ -498,17 +498,28 @@ struct PseudoCommandHandler: CommandHandler { } } - func presentFile(at fileURL: URL, line: Int = 0) async { + func presentFile(at fileURL: URL, line: Int?) async { let terminal = Terminal() do { - _ = try await terminal.runCommand( - "/bin/bash", - arguments: [ - "-c", - "xed -l \(line) ${TARGET_FILE}", - ], - environment: ["TARGET_FILE": fileURL.path], - ) + if let line { + _ = try await terminal.runCommand( + "/bin/bash", + arguments: [ + "-c", + "xed -l \(line) ${TARGET_FILE}", + ], + environment: ["TARGET_FILE": fileURL.path], + ) + } else { + _ = try await terminal.runCommand( + "/bin/bash", + arguments: [ + "-c", + "xed ${TARGET_FILE}", + ], + environment: ["TARGET_FILE": fileURL.path], + ) + } } catch { print(error) } diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift index f432f1c4..c5c5141e 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift @@ -8,6 +8,7 @@ import Preferences import PromptToCodeCustomization import PromptToCodeService import SuggestionBasic +import XcodeInspector @Reducer public struct PromptToCodePanel { @@ -247,7 +248,13 @@ public struct PromptToCodePanel { case .acceptButtonTapped: state.hasEnded = true + let url = state.promptToCodeState.source.documentURL + let startLine = state.snippetPanels.first?.snippet.attachedRange.start.line ?? 0 return .run { _ in + let activeDocumentURL = await XcodeInspector.shared.safe.activeDocumentURL + if activeDocumentURL != url { + await commandHandler.presentFile(at: url, line: startLine) + } await commandHandler.acceptModification() activatePreviousActiveXcode() } @@ -257,7 +264,7 @@ public struct PromptToCodePanel { await commandHandler.acceptModification() activateThisApp() } - + case let .statusUpdated(status): state.promptToCodeState.status = status return .none diff --git a/Tool/Sources/CommandHandler/CommandHandler.swift b/Tool/Sources/CommandHandler/CommandHandler.swift index ab19091e..7bb878bb 100644 --- a/Tool/Sources/CommandHandler/CommandHandler.swift +++ b/Tool/Sources/CommandHandler/CommandHandler.swift @@ -39,7 +39,16 @@ public protocol CommandHandler { // MARK: Others - func presentFile(at fileURL: URL, line: Int) async + func presentFile(at fileURL: URL, line: Int?) async + + func presentFile(at fileURL: URL) async +} + +public extension CommandHandler { + /// Default implementation for `presentFile(at:line:)`. + func presentFile(at fileURL: URL) async { + await presentFile(at: fileURL, line: nil) + } } public struct CommandHandlerDependencyKey: DependencyKey { @@ -117,7 +126,7 @@ public final class UniversalCommandHandler: CommandHandler { commandHandler.toast(string, as: type) } - public func presentFile(at fileURL: URL, line: Int) async { + public func presentFile(at fileURL: URL, line: Int?) async { await commandHandler.presentFile(at: fileURL, line: line) } } @@ -175,7 +184,7 @@ struct NOOPCommandHandler: CommandHandler { print("toast") } - func presentFile(at fileURL: URL, line: Int) async { + func presentFile(at fileURL: URL, line: Int?) async { print("present file") } } From a08e1bcc64ba08d43cd9bbe23dddd5913ece4e08 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 14:26:05 +0800 Subject: [PATCH 16/27] Tweak modification panel behavior --- .../FeatureReducers/PromptToCodeGroup.swift | 10 +++- .../FeatureReducers/PromptToCodePanel.swift | 16 +++-- .../PromptToCodePanelGroupView.swift | 2 + .../PromptToCodePanelView.swift | 24 +++++++- .../WidgetWindowsController.swift | 60 ++++++++++--------- 5 files changed, 73 insertions(+), 39 deletions(-) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift index 5225dd90..da626626 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift @@ -50,6 +50,7 @@ public struct PromptToCodeGroup { switch action { case let .activateOrCreatePromptToCode(s): if let promptToCode = state.activePromptToCode, s.id == promptToCode.id { + state.selectedTabId = promptToCode.id return .run { send in await send(.promptToCode(.element( id: promptToCode.id, @@ -61,10 +62,11 @@ public struct PromptToCodeGroup { await send(.createPromptToCode(s, sendImmediately: false)) } case let .createPromptToCode(newPromptToCode, sendImmediately): - // insert at 0 so it has high priority then the other detached prompt to codes + var newPromptToCode = newPromptToCode + newPromptToCode.isActiveDocument = newPromptToCode.id == state.activeDocumentURL state.promptToCodes.append(newPromptToCode) state.selectedTabId = newPromptToCode.id - return .run { send in + return .run { [newPromptToCode] send in if sendImmediately, !newPromptToCode.contextInputController.instruction.string.isEmpty { @@ -91,6 +93,10 @@ public struct PromptToCodeGroup { case let .updateActivePromptToCode(documentURL): state.activeDocumentURL = documentURL + for index in state.promptToCodes.indices { + state.promptToCodes[index].isActiveDocument = + state.promptToCodes[index].id == documentURL + } return .none case let .discardExpiredPromptToCode(documentURLs): diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift index c5c5141e..c6cb8de3 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift @@ -37,6 +37,8 @@ public struct PromptToCodePanel { public var generateDescriptionRequirement: Bool public var hasEnded = false + + public var isActiveDocument: Bool = false public var snippetPanels: IdentifiedArrayOf { get { @@ -85,6 +87,7 @@ public struct PromptToCodePanel { case cancelButtonTapped case acceptButtonTapped case acceptAndContinueButtonTapped + case revealFileButtonClicked case statusUpdated([String]) case snippetPanel(IdentifiedActionOf) } @@ -248,13 +251,7 @@ public struct PromptToCodePanel { case .acceptButtonTapped: state.hasEnded = true - let url = state.promptToCodeState.source.documentURL - let startLine = state.snippetPanels.first?.snippet.attachedRange.start.line ?? 0 return .run { _ in - let activeDocumentURL = await XcodeInspector.shared.safe.activeDocumentURL - if activeDocumentURL != url { - await commandHandler.presentFile(at: url, line: startLine) - } await commandHandler.acceptModification() activatePreviousActiveXcode() } @@ -265,6 +262,13 @@ public struct PromptToCodePanel { activateThisApp() } + case .revealFileButtonClicked: + let url = state.promptToCodeState.source.documentURL + let startLine = state.snippetPanels.first?.snippet.attachedRange.start.line ?? 0 + return .run { _ in + await commandHandler.presentFile(at: url, line: startLine) + } + case let .statusUpdated(status): state.promptToCodeState.status = status return .none diff --git a/Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift b/Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift index a5b6bc96..9957d10a 100644 --- a/Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift +++ b/Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift @@ -106,6 +106,8 @@ struct PromptToCodeTabBarButton: View { .controlSize(.small) } Text(info.tabTitle) + .truncationMode(.middle) + .allowsTightening(true) } .font(.callout) .lineLimit(1) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanelView.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanelView.swift index 01913e47..73c90e4b 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanelView.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanelView.swift @@ -288,8 +288,12 @@ extension PromptToCodePanelView { .buttonStyle(CommandButtonStyle(color: .gray)) .keyboardShortcut("w", modifiers: [.command]) - if !isCodeEmpty { - AcceptButton(store: store) + if store.isActiveDocument { + if !isCodeEmpty { + AcceptButton(store: store) + } + } else { + RevealButton(store: store) } } .fixedSize() @@ -357,6 +361,22 @@ extension PromptToCodePanelView { } } } + + struct RevealButton: View { + let store: StoreOf + + var body: some View { + WithPerceptionTracking { + Button(action: { + store.send(.revealFileButtonClicked) + }) { + Text("Jump to File(⌘ + ⏎)") + } + .buttonStyle(CommandButtonStyle(color: .accentColor)) + .keyboardShortcut(KeyEquivalent.return, modifiers: [.command]) + } + } + } struct AcceptButton: View { let store: StoreOf diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index 2318125e..11b06f11 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -97,7 +97,7 @@ actor WidgetWindowsController: NSObject { } } } - + Task { @MainActor in windows.chatPanelWindow.isPanelDisplayed = false } @@ -132,36 +132,38 @@ private extension WidgetWindowsController { observeToAppTask = Task { await windows.orderFront() - for await notification in await notifications.notifications() { - try Task.checkCancellation() - - /// Hide the widgets before switching to another window/editor - /// so the transition looks better. - func hideWidgetForTransitions() async { - let newDocumentURL = await xcodeInspector.safe.realtimeActiveDocumentURL - let documentURL = await MainActor - .run { store.withState { $0.focusingDocumentURL } } - if documentURL != newDocumentURL { - await send(.panel(.removeDisplayedContent)) - await hidePanelWindows() - } - await send(.updateFocusingDocumentURL) - } - - func removeContent() async { + /// Hide the widgets before switching to another window/editor + /// so the transition looks better. + func hideWidgetForTransitions() async { + let newDocumentURL = await xcodeInspector.safe.realtimeActiveDocumentURL + let documentURL = await MainActor + .run { store.withState { $0.focusingDocumentURL } } + if documentURL != newDocumentURL { await send(.panel(.removeDisplayedContent)) + await hidePanelWindows() } + await send(.updateFocusingDocumentURL) + } - func updateWidgetsAndNotifyChangeOfEditor(immediately: Bool) async { - await send(.panel(.switchToAnotherEditorAndUpdateContent)) - updateWindowLocation(animated: false, immediately: immediately) - updateWindowOpacity(immediately: immediately) - } + func removeContent() async { + await send(.panel(.removeDisplayedContent)) + } - func updateWidgets(immediately: Bool) async { - updateWindowLocation(animated: false, immediately: immediately) - updateWindowOpacity(immediately: immediately) - } + func updateWidgetsAndNotifyChangeOfEditor(immediately: Bool) async { + await send(.panel(.switchToAnotherEditorAndUpdateContent)) + updateWindowLocation(animated: false, immediately: immediately) + updateWindowOpacity(immediately: immediately) + } + + func updateWidgets(immediately: Bool) async { + updateWindowLocation(animated: false, immediately: immediately) + updateWindowOpacity(immediately: immediately) + } + + await updateWidgetsAndNotifyChangeOfEditor(immediately: true) + + for await notification in await notifications.notifications() { + try Task.checkCancellation() switch notification.kind { case .focusedWindowChanged: @@ -257,7 +259,7 @@ private extension WidgetWindowsController { extension WidgetWindowsController { @MainActor func hidePanelWindows() { - windows.sharedPanelWindow.alphaValue = 0 +// windows.sharedPanelWindow.alphaValue = 0 windows.suggestionPanelWindow.alphaValue = 0 } @@ -903,7 +905,7 @@ class WidgetWindow: CanBecomeKeyWindow { : .normal if targetLevel != level { - self.orderFrontRegardless() + orderFrontRegardless() level = targetLevel } } From 3b7825498388fea9098cfefd0f50de3e98ed8eb9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 14:54:30 +0800 Subject: [PATCH 17/27] Update model list --- .../Preferences/Types/ChatGPTModel.swift | 53 ++++++++----------- .../Types/GoogleGenerativeChatModel.swift | 12 +++++ 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/Tool/Sources/Preferences/Types/ChatGPTModel.swift b/Tool/Sources/Preferences/Types/ChatGPTModel.swift index bf372cd3..54893fb6 100644 --- a/Tool/Sources/Preferences/Types/ChatGPTModel.swift +++ b/Tool/Sources/Preferences/Types/ChatGPTModel.swift @@ -8,23 +8,19 @@ public enum ChatGPTModel: String, CaseIterable { case gpt4 = "gpt-4" case gpt432k = "gpt-4-32k" case gpt4Turbo = "gpt-4-turbo" - case gpt40314 = "gpt-4-0314" - case gpt40613 = "gpt-4-0613" - case gpt41106Preview = "gpt-4-1106-preview" case gpt4VisionPreview = "gpt-4-vision-preview" - case gpt4TurboPreview = "gpt-4-turbo-preview" - case gpt4Turbo20240409 = "gpt-4-turbo-2024-04-09" - case gpt35Turbo1106 = "gpt-3.5-turbo-1106" - case gpt35Turbo0125 = "gpt-3.5-turbo-0125" case gpt432k0314 = "gpt-4-32k-0314" case gpt432k0613 = "gpt-4-32k-0613" case gpt40125 = "gpt-4-0125-preview" + case gpt4_1 = "gpt-4.1" + case gpt4_1Mini = "gpt-4.1-mini" + case gpt4_1Nano = "gpt-4.1-nano" case o1 = "o1" case o1Preview = "o1-preview" - case o1Preview20240912 = "o1-preview-2024-09-12" - case o1Mini = "o1-mini" - case o1Mini20240912 = "o1-mini-2024-09-12" + case o1Pro = "o1-pro" case o3Mini = "o3-mini" + case o3 = "o3" + case o4Mini = "o4-mini" } public extension ChatGPTModel { @@ -32,55 +28,50 @@ public extension ChatGPTModel { switch self { case .gpt4: return 8192 - case .gpt40314: - return 8192 case .gpt432k: return 32768 case .gpt432k0314: return 32768 case .gpt35Turbo: return 16385 - case .gpt35Turbo1106: - return 16385 - case .gpt35Turbo0125: - return 16385 case .gpt35Turbo16k: return 16385 - case .gpt40613: - return 8192 case .gpt432k0613: return 32768 - case .gpt41106Preview: - return 128_000 case .gpt4VisionPreview: return 128_000 - case .gpt4TurboPreview: - return 128_000 case .gpt40125: return 128_000 case .gpt4Turbo: return 128_000 - case .gpt4Turbo20240409: - return 128_000 case .gpt4o: return 128_000 case .gpt4oMini: return 128_000 - case .o1Preview, .o1Preview20240912: - return 128_000 - case .o1Mini, .o1Mini20240912: + case .o1Preview: return 128_000 case .o1: return 200_000 case .o3Mini: return 200_000 + case .gpt4_1: + return 1_047_576 + case .gpt4_1Mini: + return 1_047_576 + case .gpt4_1Nano: + return 1_047_576 + case .o1Pro: + return 200_000 + case .o3: + return 200_000 + case .o4Mini: + return 200_000 } } var supportsImages: Bool { switch self { - case .gpt4VisionPreview, .gpt4Turbo, .gpt4Turbo20240409, .gpt4o, .gpt4oMini, .o1Preview, - .o1Preview20240912, .o1Mini, .o1Mini20240912, .o1, .o3Mini: + case .gpt4VisionPreview, .gpt4Turbo, .gpt4o, .gpt4oMini, .o1Preview, .o1, .o3Mini: return true default: return false @@ -89,7 +80,7 @@ public extension ChatGPTModel { var supportsTemperature: Bool { switch self { - case .o1Preview, .o1Preview20240912, .o1Mini, .o1Mini20240912, .o1, .o3Mini: + case .o1Preview, .o1, .o3Mini: return false default: return true @@ -98,7 +89,7 @@ public extension ChatGPTModel { var supportsSystemPrompt: Bool { switch self { - case .o1Preview, .o1Preview20240912, .o1Mini, .o1Mini20240912, .o1, .o3Mini: + case .o1Preview, .o1, .o3Mini: return false default: return true diff --git a/Tool/Sources/Preferences/Types/GoogleGenerativeChatModel.swift b/Tool/Sources/Preferences/Types/GoogleGenerativeChatModel.swift index 43e4af28..23de7f5e 100644 --- a/Tool/Sources/Preferences/Types/GoogleGenerativeChatModel.swift +++ b/Tool/Sources/Preferences/Types/GoogleGenerativeChatModel.swift @@ -1,6 +1,10 @@ import Foundation public enum GoogleGenerativeAIModel: String { + case gemini25FlashPreview = "gemini-2.5-flash-preview-04-17" + case gemini25ProPreview = "gemini-2.5-pro-preview-05-06" + case gemini20Flash = "gemini-2.0-flash" + case gemini20FlashLite = "gemini-2.0-flash-lite" case gemini15Pro = "gemini-1.5-pro" case gemini15Flash = "gemini-1.5-flash" case geminiPro = "gemini-pro" @@ -15,6 +19,14 @@ public extension GoogleGenerativeAIModel { return 1_048_576 case .gemini15Pro: return 2_097_152 + case .gemini25FlashPreview: + return 1_048_576 + case .gemini25ProPreview: + return 1_048_576 + case .gemini20Flash: + return 1_048_576 + case .gemini20FlashLite: + return 1_048_576 } } } From 6acaf19027d92b7561aa6f86a4a116ba3833583b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 15:56:50 +0800 Subject: [PATCH 18/27] Cherry pick accept first line of suggestion --- ...SuggestionSettingsGeneralSectionView.swift | 8 + .../TabToAcceptSuggestion.swift | 214 +++++++++--------- Core/Sources/Service/Service.swift | 12 + .../PseudoCommandHandler.swift | 91 ++++++++ .../SuggestionCommandHandler.swift | 3 + .../WindowBaseCommandHandler.swift | 26 +++ .../CodeBlockSuggestionPanelView.swift | 37 ++- EditorExtension/AcceptSuggestionCommand.swift | 27 +++ EditorExtension/SourceEditorExtension.swift | 1 + .../CommandHandler/CommandHandler.swift | 9 + Tool/Sources/Preferences/Keys.swift | 4 + .../XPCShared/XPCServiceProtocol.swift | 18 +- 12 files changed, 337 insertions(+), 113 deletions(-) diff --git a/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift b/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift index 272302e8..390c7f98 100644 --- a/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift +++ b/Core/Sources/HostApp/FeatureSettings/Suggestion/SuggestionSettingsGeneralSectionView.swift @@ -263,6 +263,8 @@ struct SuggestionSettingsGeneralSectionView: View { var needControl @AppStorage(\.acceptSuggestionWithModifierOnlyForSwift) var onlyForSwift + @AppStorage(\.acceptSuggestionLineWithModifierControl) + var acceptLineWithControl } @StateObject var settings = Settings() @@ -290,6 +292,12 @@ struct SuggestionSettingsGeneralSectionView: View { Toggle(isOn: $settings.onlyForSwift) { Text("Only require modifiers for Swift") } + + Divider() + + Toggle(isOn: $settings.acceptLineWithControl) { + Text("Accept suggestion first line with Control") + } } .padding() diff --git a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift index 451183cc..f99d0890 100644 --- a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift +++ b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift @@ -105,129 +105,139 @@ final class TabToAcceptSuggestion { switch keycode { case tab: - Logger.service.info("TabToAcceptSuggestion: Tab") - - guard let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL - else { - Logger.service.info("TabToAcceptSuggestion: No active document") - return .unchanged - } + return handleTab(event.flags) + case esc: + return handleEsc(event.flags) + default: + return .unchanged + } + } - let language = languageIdentifierFromFileURL(fileURL) + func handleTab(_ flags: CGEventFlags) -> CGEventManipulation.Result { + Logger.service.info("TabToAcceptSuggestion: Tab") - func checkKeybinding() -> Bool { - if event.flags.contains(.maskHelp) { return false } + guard let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL + else { + Logger.service.info("TabToAcceptSuggestion: No active document") + return .unchanged + } - let shouldCheckModifiers = if UserDefaults.shared - .value(for: \.acceptSuggestionWithModifierOnlyForSwift) - { - language == .builtIn(.swift) - } else { - true - } + let language = languageIdentifierFromFileURL(fileURL) - if shouldCheckModifiers { - if event.flags.contains(.maskShift) != UserDefaults.shared - .value(for: \.acceptSuggestionWithModifierShift) - { - return false - } - if event.flags.contains(.maskControl) != UserDefaults.shared - .value(for: \.acceptSuggestionWithModifierControl) - { - return false - } - if event.flags.contains(.maskAlternate) != UserDefaults.shared - .value(for: \.acceptSuggestionWithModifierOption) - { - return false - } - if event.flags.contains(.maskCommand) != UserDefaults.shared - .value(for: \.acceptSuggestionWithModifierCommand) - { - return false - } - } else { - if event.flags.contains(.maskShift) { return false } - if event.flags.contains(.maskControl) { return false } - if event.flags.contains(.maskAlternate) { return false } - if event.flags.contains(.maskCommand) { return false } - } + if flags.contains(.maskHelp) { return .unchanged } - return true + let requiredFlagsToTrigger: CGEventFlags = { + var all = CGEventFlags() + if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierShift) { + all.insert(.maskShift) } - - guard - checkKeybinding(), - canTapToAcceptSuggestion - else { - Logger.service.info("TabToAcceptSuggestion: Feature not available") - return .unchanged + if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierControl) { + all.insert(.maskControl) } - - guard ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil - else { - Logger.service.info("TabToAcceptSuggestion: Xcode not found") - return .unchanged + if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierOption) { + all.insert(.maskAlternate) } - guard let editor = ThreadSafeAccessToXcodeInspector.shared.focusedEditor - else { - Logger.service.info("TabToAcceptSuggestion: No editor found") - return .unchanged + if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierCommand) { + all.insert(.maskCommand) } - guard let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL) - else { - Logger.service.info( - "TabToAcceptSuggestion: No file found for file \(fileURL.lastPathComponent)" - ) - return .unchanged + if UserDefaults.shared.value(for: \.acceptSuggestionWithModifierOnlyForSwift) { + if language == .builtIn(.swift) { + return all + } else { + return [] + } + } else { + return all } - guard let presentingSuggestion = filespace.presentingSuggestion - else { - Logger.service.info("TabToAcceptSuggestion: No Suggestions found") + }() + + let flagsToAvoidWhenNotRequired: [CGEventFlags] = [ + .maskShift, .maskCommand, .maskHelp, .maskSecondaryFn, + ] + + guard flags.contains(requiredFlagsToTrigger) else { + Logger.service.info("TabToAcceptSuggestion: Modifier not found") + return .unchanged + } + + for flag in flagsToAvoidWhenNotRequired { + if flags.contains(flag), !requiredFlagsToTrigger.contains(flag) { return .unchanged } + } - let editorContent = editor.getContent() + guard canTapToAcceptSuggestion else { + Logger.service.info("TabToAcceptSuggestion: Feature not available") + return .unchanged + } - let shouldAcceptSuggestion = Self.checkIfAcceptSuggestion( - lines: editorContent.lines, - cursorPosition: editorContent.cursorPosition, - codeMetadata: filespace.codeMetadata, - presentingSuggestionText: presentingSuggestion.text - ) + guard ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil + else { + Logger.service.info("TabToAcceptSuggestion: Xcode not found") + return .unchanged + } + guard let editor = ThreadSafeAccessToXcodeInspector.shared.focusedEditor + else { + Logger.service.info("TabToAcceptSuggestion: No editor found") + return .unchanged + } + guard let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL) + else { + Logger.service.info("TabToAcceptSuggestion: No file found") + return .unchanged + } + guard let presentingSuggestion = filespace.presentingSuggestion + else { + Logger.service.info("TabToAcceptSuggestion: No Suggestions found") + return .unchanged + } - if shouldAcceptSuggestion { - Logger.service.info("TabToAcceptSuggestion: Accept") - Task { await commandHandler.acceptSuggestion() } - return .discarded + let editorContent = editor.getContent() + + let shouldAcceptSuggestion = Self.checkIfAcceptSuggestion( + lines: editorContent.lines, + cursorPosition: editorContent.cursorPosition, + codeMetadata: filespace.codeMetadata, + presentingSuggestionText: presentingSuggestion.text + ) + + if shouldAcceptSuggestion { + Logger.service.info("TabToAcceptSuggestion: Accept") + if flags.contains(.maskControl), + !requiredFlagsToTrigger.contains(.maskControl) + { + Task { await commandHandler.acceptActiveSuggestionLineInGroup(atIndex: nil) + } } else { - Logger.service.info("TabToAcceptSuggestion: Should not accept") - return .unchanged + Task { await commandHandler.acceptSuggestion() } } - case esc: - guard - !event.flags.contains(.maskShift), - !event.flags.contains(.maskControl), - !event.flags.contains(.maskAlternate), - !event.flags.contains(.maskCommand), - !event.flags.contains(.maskHelp), - canEscToDismissSuggestion - else { return .unchanged } - - guard - let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL, - ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil, - let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL), - filespace.presentingSuggestion != nil - else { return .unchanged } - - Task { await commandHandler.dismissSuggestion() } return .discarded - default: + } else { + Logger.service.info("TabToAcceptSuggestion: Should not accept") return .unchanged } } + + func handleEsc(_ flags: CGEventFlags) -> CGEventManipulation.Result { + guard + !flags.contains(.maskShift), + !flags.contains(.maskControl), + !flags.contains(.maskAlternate), + !flags.contains(.maskCommand), + !flags.contains(.maskHelp), + canEscToDismissSuggestion + else { return .unchanged } + + guard + let fileURL = ThreadSafeAccessToXcodeInspector.shared.activeDocumentURL, + ThreadSafeAccessToXcodeInspector.shared.activeXcode != nil, + let filespace = workspacePool.fetchFilespaceIfExisted(fileURL: fileURL), + filespace.presentingSuggestion != nil + else { return .unchanged } + + Task { await commandHandler.dismissSuggestion() } + return .discarded + } } extension TabToAcceptSuggestion { diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index b222fb81..876b2110 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -158,6 +158,18 @@ public extension Service { } } } + + try ExtensionServiceRequests.GetSuggestionLineAcceptedCode.handle( + endpoint: endpoint, + requestBody: requestBody, + reply: reply + ) { request in + let editor = request.editorContent + let handler = WindowBaseCommandHandler() + let updatedContent = try? await handler + .acceptSuggestionLine(editor: editor) + return updatedContent + } } catch is XPCRequestHandlerHitError { return } catch { diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift index c2316a8b..f79d7167 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift @@ -282,6 +282,68 @@ struct PseudoCommandHandler: CommandHandler { ), sendImmediately: false))) } + func acceptActiveSuggestionLineInGroup(atIndex index: Int?) async { + do { + if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) { + throw CancellationError() + } + do { + try await XcodeInspector.shared.safe.latestActiveXcode? + .triggerCopilotCommand(name: "Accept Suggestion Line") + } catch { + let last = Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI + let now = Date() + if now.timeIntervalSince(last) > 60 * 60 { + Self.lastTimeCommandFailedToTriggerWithAccessibilityAPI = now + toast.toast(content: """ + The app is using a fallback solution to accept suggestions. \ + For better experience, please restart Xcode to re-activate the Copilot \ + menu item. + """, type: .warning, duration: 10) + } + + throw error + } + } catch { + guard let xcode = ActiveApplicationMonitor.shared.activeXcode + ?? ActiveApplicationMonitor.shared.latestXcode else { return } + let application = AXUIElementCreateApplication(xcode.processIdentifier) + guard let focusElement = application.focusedElement, + focusElement.description == "Source Editor" + else { return } + guard let ( + content, + lines, + _, + cursorPosition, + cursorOffset + ) = await getFileContent(sourceEditor: nil) + else { + PresentInWindowSuggestionPresenter() + .presentErrorMessage("Unable to get file content.") + return + } + let handler = WindowBaseCommandHandler() + do { + guard let result = try await handler.acceptSuggestion(editor: .init( + content: content, + lines: lines, + uti: "", + cursorPosition: cursorPosition, + cursorOffset: cursorOffset, + selections: [], + tabSize: 0, + indentSize: 0, + usesTabsForIndentation: false + )) else { return } + + try injectUpdatedCodeWithAccessibilityAPI(result, focusElement: focusElement) + } catch { + PresentInWindowSuggestionPresenter().presentError(error) + } + } + } + func acceptSuggestion() async { do { if UserDefaults.shared.value(for: \.alwaysAcceptSuggestionWithAccessibilityAPI) { @@ -649,5 +711,34 @@ extension PseudoCommandHandler { usesTabsForIndentation: usesTabsForIndentation ) } + + func handleAcceptSuggestionLineCommand(editor: EditorContent) async throws -> CodeSuggestion? { + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return nil } + + return try await acceptSuggestionLineInGroup( + atIndex: 0, + editor: editor + ) + } + + func acceptSuggestionLineInGroup( + atIndex index: Int?, + editor: EditorContent + ) async throws -> CodeSuggestion? { + guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL + else { return nil } + let (workspace, _) = try await Service.shared.workspacePool + .fetchOrCreateWorkspaceAndFilespace(fileURL: fileURL) + + guard var acceptedSuggestion = await workspace.acceptSuggestion( + forFileAt: fileURL, + editor: editor + ) else { return nil } + + let text = acceptedSuggestion.text + acceptedSuggestion.text = String(text.splitByNewLine().first ?? "") + return acceptedSuggestion + } } diff --git a/Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift index 3d612e82..bc2742c9 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift @@ -11,6 +11,8 @@ protocol SuggestionCommandHandler { @ServiceActor func rejectSuggestion(editor: EditorContent) async throws -> UpdatedContent? @ServiceActor + func acceptSuggestionLine(editor: EditorContent) async throws -> UpdatedContent? + @ServiceActor func acceptSuggestion(editor: EditorContent) async throws -> UpdatedContent? @ServiceActor func acceptPromptToCode(editor: EditorContent) async throws -> UpdatedContent? @@ -23,3 +25,4 @@ protocol SuggestionCommandHandler { @ServiceActor func customCommand(id: String, editor: EditorContent) async throws -> UpdatedContent? } + diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index ce89620c..4df98546 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -172,6 +172,32 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler { return nil } + func acceptSuggestionLine(editor: EditorContent) async throws -> UpdatedContent? { + if let acceptedSuggestion = try await PseudoCommandHandler() + .handleAcceptSuggestionLineCommand(editor: editor) + { + let injector = SuggestionInjector() + var lines = editor.lines + var cursorPosition = editor.cursorPosition + var extraInfo = SuggestionInjector.ExtraInfo() + + injector.acceptSuggestion( + intoContentWithoutSuggestion: &lines, + cursorPosition: &cursorPosition, + completion: acceptedSuggestion, + extraInfo: &extraInfo + ) + + return .init( + content: String(lines.joined(separator: "")), + newSelection: .cursor(cursorPosition), + modifications: extraInfo.modifications + ) + } + + return nil + } + func acceptPromptToCode(editor: EditorContent) async throws -> UpdatedContent? { guard let fileURL = await XcodeInspector.shared.safe.realtimeActiveDocumentURL else { return nil } diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanelView.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanelView.swift index cef8a0ad..3c0d8da0 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanelView.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanelView.swift @@ -54,6 +54,8 @@ struct CodeBlockSuggestionPanelView: View { struct ToolBar: View { @Dependency(\.commandHandler) var commandHandler + @Environment(\.modifierFlags) var modifierFlags + @AppStorage(\.acceptSuggestionLineWithModifierControl) var acceptLineWithControl let suggestion: PresentingCodeSuggestion var body: some View { @@ -98,14 +100,25 @@ struct CodeBlockSuggestionPanelView: View { Text("Reject") }.buttonStyle(CommandButtonStyle(color: .gray)) - Button(action: { - Task { - await commandHandler.acceptSuggestion() - NSWorkspace.activatePreviousActiveXcode() - } - }) { - Text("Accept") - }.buttonStyle(CommandButtonStyle(color: .accentColor)) + if modifierFlags.contains(.control) && acceptLineWithControl { + Button(action: { + Task { + await commandHandler.acceptActiveSuggestionLineInGroup(atIndex: nil) + NSWorkspace.activatePreviousActiveXcode() + } + }) { + Text("Accept Line") + }.buttonStyle(CommandButtonStyle(color: .gray)) + } else { + Button(action: { + Task { + await commandHandler.acceptSuggestion() + NSWorkspace.activatePreviousActiveXcode() + } + }) { + Text("Accept") + }.buttonStyle(CommandButtonStyle(color: .accentColor)) + } } .padding(6) .foregroundColor(.secondary) @@ -116,6 +129,8 @@ struct CodeBlockSuggestionPanelView: View { struct CompactToolBar: View { @Dependency(\.commandHandler) var commandHandler + @Environment(\.modifierFlags) var modifierFlags + @AppStorage(\.acceptSuggestionLineWithModifierControl) var acceptLineWithControl let suggestion: PresentingCodeSuggestion var body: some View { @@ -139,6 +154,12 @@ struct CodeBlockSuggestionPanelView: View { }.buttonStyle(.plain) Spacer() + + if modifierFlags.contains(.control) && acceptLineWithControl { + Text("Accept Line") + .foregroundColor(.secondary) + .padding(.trailing, 4) + } Button(action: { Task { diff --git a/EditorExtension/AcceptSuggestionCommand.swift b/EditorExtension/AcceptSuggestionCommand.swift index 1556ce74..cbca64ed 100644 --- a/EditorExtension/AcceptSuggestionCommand.swift +++ b/EditorExtension/AcceptSuggestionCommand.swift @@ -31,3 +31,30 @@ class AcceptSuggestionCommand: NSObject, XCSourceEditorCommand, CommandType { } } +class AcceptSuggestionLineCommand: NSObject, XCSourceEditorCommand, CommandType { + var name: String { "Accept Suggestion Line" } + + func perform( + with invocation: XCSourceEditorCommandInvocation, + completionHandler: @escaping (Error?) -> Void + ) { + Task { + do { + try await (Task(timeout: 7) { + let service = try getService() + if let content = try await service.send( + requestBody: ExtensionServiceRequests + .GetSuggestionLineAcceptedCode(editorContent: .init(invocation)) + ) { + invocation.accept(content) + } + completionHandler(nil) + }.value) + } catch is CancellationError { + completionHandler(nil) + } catch { + completionHandler(error) + } + } + } +} diff --git a/EditorExtension/SourceEditorExtension.swift b/EditorExtension/SourceEditorExtension.swift index aedf6bf3..f102f9d4 100644 --- a/EditorExtension/SourceEditorExtension.swift +++ b/EditorExtension/SourceEditorExtension.swift @@ -12,6 +12,7 @@ class SourceEditorExtension: NSObject, XCSourceEditorExtension { [ GetSuggestionsCommand(), AcceptSuggestionCommand(), + AcceptSuggestionLineCommand(), RejectSuggestionCommand(), NextSuggestionCommand(), PreviousSuggestionCommand(), diff --git a/Tool/Sources/CommandHandler/CommandHandler.swift b/Tool/Sources/CommandHandler/CommandHandler.swift index 7bb878bb..f5067668 100644 --- a/Tool/Sources/CommandHandler/CommandHandler.swift +++ b/Tool/Sources/CommandHandler/CommandHandler.swift @@ -16,6 +16,7 @@ public protocol CommandHandler { func presentNextSuggestion() async func rejectSuggestions() async func acceptSuggestion() async + func acceptActiveSuggestionLineInGroup(atIndex index: Int?) async func dismissSuggestion() async func generateRealtimeSuggestions(sourceEditor: SourceEditor?) async @@ -93,6 +94,10 @@ public final class UniversalCommandHandler: CommandHandler { public func acceptSuggestion() async { await commandHandler.acceptSuggestion() } + + public func acceptActiveSuggestionLineInGroup(atIndex index: Int?) async { + await commandHandler.acceptActiveSuggestionLineInGroup(atIndex: index) + } public func dismissSuggestion() async { await commandHandler.dismissSuggestion() @@ -151,6 +156,10 @@ struct NOOPCommandHandler: CommandHandler { func acceptSuggestion() async { print("accept suggestion") } + + func acceptActiveSuggestionLineInGroup(atIndex index: Int?) async { + print("accept active suggestion line in group at index \(String(describing: index))") + } func dismissSuggestion() async { print("dismiss suggestion") diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 49d3bc8f..3d613e73 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -375,6 +375,10 @@ public extension UserDefaultPreferenceKeys { var acceptSuggestionWithModifierOnlyForSwift: PreferenceKey { .init(defaultValue: false, key: "SuggestionWithModifierOnlyForSwift") } + + var acceptSuggestionLineWithModifierControl: PreferenceKey { + .init(defaultValue: true, key: "SuggestionLineWithModifierControl") + } var dismissSuggestionWithEsc: PreferenceKey { .init(defaultValue: true, key: "DismissSuggestionWithEsc") diff --git a/Tool/Sources/XPCShared/XPCServiceProtocol.swift b/Tool/Sources/XPCShared/XPCServiceProtocol.swift index 4f0c1fee..ec5aea50 100644 --- a/Tool/Sources/XPCShared/XPCServiceProtocol.swift +++ b/Tool/Sources/XPCShared/XPCServiceProtocol.swift @@ -80,7 +80,7 @@ public enum ExtensionServiceRequests { public struct ServiceInfo: Codable { public var bundleIdentifier: String public var name: String - + public init(bundleIdentifier: String, name: String) { self.bundleIdentifier = bundleIdentifier self.name = name @@ -92,14 +92,14 @@ public enum ExtensionServiceRequests { public init() {} } - + public struct GetExtensionOpenChatHandlers: ExtensionServiceRequestType { public struct HandlerInfo: Codable { public var bundleIdentifier: String public var id: String public var tabName: String public var isBuiltIn: Bool - + public init(bundleIdentifier: String, id: String, tabName: String, isBuiltIn: Bool) { self.bundleIdentifier = bundleIdentifier self.id = id @@ -113,6 +113,18 @@ public enum ExtensionServiceRequests { public init() {} } + + public struct GetSuggestionLineAcceptedCode: ExtensionServiceRequestType { + public typealias ResponseBody = UpdatedContent? + + public static let endpoint = "GetSuggestionLineAcceptedCode" + + public let editorContent: EditorContent + + public init(editorContent: EditorContent) { + self.editorContent = editorContent + } + } } public struct XPCRequestHandlerHitError: Error, LocalizedError { From 2c394e391d8db795f9fc7b7d689712ab58681891 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 16:24:49 +0800 Subject: [PATCH 19/27] Fix modification panel not dismissing after accept --- .../FeatureReducers/PromptToCodeGroup.swift | 11 ++++++++++- .../FeatureReducers/PromptToCodePanel.swift | 10 ++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift index da626626..d844b336 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift @@ -19,6 +19,9 @@ public struct PromptToCodeGroup { } set { selectedTabId = newValue?.id + if let id = selectedTabId { + promptToCodes[id: id] = newValue + } } } } @@ -88,7 +91,13 @@ public struct PromptToCodeGroup { return .none case let .discardAcceptedPromptToCodeIfNotContinuous(id): - state.promptToCodes.removeAll { $0.id == id && $0.hasEnded } + for itemId in state.promptToCodes.ids { + if itemId == id, state.promptToCodes[id: itemId]?.clickedButton == .accept { + state.promptToCodes.remove(id: itemId) + } else { + state.promptToCodes[id: itemId]?.clickedButton = nil + } + } return .none case let .updateActivePromptToCode(documentURL): diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift index c6cb8de3..f32474df 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodePanel.swift @@ -17,6 +17,11 @@ public struct PromptToCodePanel { public enum FocusField: Equatable { case textField } + + public enum ClickedButton: Equatable { + case accept + case acceptAndContinue + } @Shared public var promptToCodeState: ModificationState @ObservationStateIgnored @@ -36,7 +41,7 @@ public struct PromptToCodePanel { public var generateDescriptionRequirement: Bool - public var hasEnded = false + public var clickedButton: ClickedButton? public var isActiveDocument: Bool = false @@ -250,13 +255,14 @@ public struct PromptToCodePanel { return .cancel(id: CancellationKey.modifyCode(state.id)) case .acceptButtonTapped: - state.hasEnded = true + state.clickedButton = .accept return .run { _ in await commandHandler.acceptModification() activatePreviousActiveXcode() } case .acceptAndContinueButtonTapped: + state.clickedButton = .acceptAndContinue return .run { _ in await commandHandler.acceptModification() activateThisApp() From f13b82d590a2fe9f1de9e9260799e583a2f2d8cd Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 17:18:54 +0800 Subject: [PATCH 20/27] Drop GitHub Copilot language server to 1.44.0 --- .../AccountSettings/GitHubCopilotView.swift | 17 +++++------ .../GitHubCopilotInstallationManager.swift | 4 +-- .../LanguageServer/GitHubCopilotService.swift | 29 +++++++++++++++++-- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift index e02d6ed9..d48d1486 100644 --- a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift +++ b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift @@ -158,7 +158,7 @@ struct GitHubCopilotView: View { "node" ) ) { - Text("Path to Node (v18+)") + Text("Path to Node (v20.8+)") } Text( @@ -261,7 +261,7 @@ struct GitHubCopilotView: View { if isRunningAction { ActivityIndicatorView() } - } + } .opacity(isRunningAction ? 0.8 : 1) .disabled(isRunningAction) @@ -269,13 +269,12 @@ struct GitHubCopilotView: View { refreshConfiguration() } - // Not available yet -// Form { -// GitHubCopilotModelPicker( -// title: "Chat Model Name", -// gitHubCopilotModelId: $settings.gitHubCopilotModelId -// ) -// } + Form { + GitHubCopilotModelPicker( + title: "Chat Model Name", + gitHubCopilotModelId: $settings.gitHubCopilotModelId + ) + } } SettingsDivider("Advanced") diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift index ed14198a..0bdd2ba2 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift @@ -6,12 +6,12 @@ public struct GitHubCopilotInstallationManager { public private(set) static var isInstalling = false static var downloadURL: URL { - let commitHash = "87038123804796ca7af20d1b71c3428d858a9124" + let commitHash = "a9228e015528c9307890c48083c925eb98a64a79" let link = "https://github.com/github/copilot.vim/archive/\(commitHash).zip" return URL(string: link)! } - static let latestSupportedVersion = "1.48.0" + static let latestSupportedVersion = "1.44.0" static let minimumSupportedVersion = "1.32.0" public init() {} diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index 7979c76e..4abcae22 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -1,6 +1,7 @@ import AppKit import enum CopilotForXcodeKit.SuggestionServiceError import Foundation +import JSONRPC import LanguageClient import LanguageServerProtocol import Logger @@ -133,6 +134,9 @@ public class GitHubCopilotBaseService { let urls = try GitHubCopilotBaseService.createFoldersIfNeeded() let executionParams: Process.ExecutionParameters let runner = UserDefaults.shared.value(for: \.runNodeWith) + let watchedFiles = JSONValue( + booleanLiteral: projectRootURL.path == "/" ? false : true + ) guard let agentJSURL = { () -> URL? in let languageServerDotJS = urls.executableURL @@ -148,7 +152,7 @@ public class GitHubCopilotBaseService { }() else { throw GitHubCopilotError.languageServerNotInstalled } - + let indexJSURL: URL = try { if UserDefaults.shared.value(for: \.gitHubCopilotLoadKeyChainCertificates) { let url = urls.executableURL @@ -244,6 +248,8 @@ public class GitHubCopilotBaseService { experimental: nil ) + let pretendToBeVSCode = UserDefaults.shared + .value(for: \.gitHubCopilotPretendIDEToBeVSCode) return InitializeParams( processId: Int(ProcessInfo.processInfo.processIdentifier), clientInfo: .init( @@ -254,7 +260,24 @@ public class GitHubCopilotBaseService { locale: nil, rootPath: projectRootURL.path, rootUri: projectRootURL.path, - initializationOptions: nil, + initializationOptions: [ + "editorInfo": pretendToBeVSCode ? .hash([ + "name": "vscode", + "version": "1.99.3", + ]) : .hash([ + "name": "Xcode", + "version": .string(xcodeVersion() ?? "16.0"), + ]), + "editorPluginInfo": .hash([ + "name": "Copilot for Xcode", + "version": .string(Bundle.main + .infoDictionary?["CFBundleShortVersionString"] as? String ?? ""), + ]), + "copilotCapabilities": [ + /// The editor has support for watching files over LSP + "watchedFiles": watchedFiles, + ] + ], capabilities: capabilities, trace: .off, workspaceFolders: [WorkspaceFolder( @@ -632,7 +655,7 @@ extension InitializingServer: GitHubCopilotLSP { } } -private func xcodeVersion() async -> String? { +private func xcodeVersion() -> String? { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") process.arguments = ["xcodebuild", "-version"] From bd03440cf9f67d9a470aafcf5a4baa39db55a7ad Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 17:53:12 +0800 Subject: [PATCH 21/27] Update --- .../CopilotLocalProcessServer.swift | 2 ++ .../GitHubCopilotInstallationManager.swift | 5 +++++ .../LanguageServer/GitHubCopilotService.swift | 16 +++++++++++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift index 884a4d83..817a6827 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/CopilotLocalProcessServer.swift @@ -360,6 +360,8 @@ final class ServerNotificationHandler { Logger.gitHubCopilot .info("\(anyNotification.method): \(debugDescription)") } + case "didChangeStatus": + Logger.gitHubCopilot.info("Did change status: \(debugDescription)") default: throw ServerError.handlerUnavailable(methodName) } diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift index 0bdd2ba2..1d9c2f84 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift @@ -11,6 +11,11 @@ public struct GitHubCopilotInstallationManager { return URL(string: link)! } + /// trying to update to a newer version but completions stop working for some reasons. + /// As a reference, the changes starts since 1.45.0 and GitHub's extension updates the + /// language server since this commit: + /// https://github.com/github/CopilotForXcode/commit/b2189de633417a49d6d2022aad5ff0748ebed2ac#diff-678798cf677bcd1ce276809cfccd33da9ff594b1b0c557180210a4ed2bd27ffa + /// It jumps directly to 1.48.0 from a much lower version. static let latestSupportedVersion = "1.44.0" static let minimumSupportedVersion = "1.32.0" diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index 4abcae22..2a4b56ef 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -152,7 +152,7 @@ public class GitHubCopilotBaseService { }() else { throw GitHubCopilotError.languageServerNotInstalled } - + let indexJSURL: URL = try { if UserDefaults.shared.value(for: \.gitHubCopilotLoadKeyChainCertificates) { let url = urls.executableURL @@ -241,7 +241,17 @@ public class GitHubCopilotBaseService { server.initializeParamsProvider = { let capabilities = ClientCapabilities( - workspace: nil, + workspace: .init( + applyEdit: false, + workspaceEdit: nil, + didChangeConfiguration: nil, + didChangeWatchedFiles: nil, + symbol: nil, + executeCommand: nil, + workspaceFolders: true, + configuration: nil, + semanticTokens: nil + ), textDocument: nil, window: nil, general: nil, @@ -276,7 +286,7 @@ public class GitHubCopilotBaseService { "copilotCapabilities": [ /// The editor has support for watching files over LSP "watchedFiles": watchedFiles, - ] + ], ], capabilities: capabilities, trace: .off, From f7863d93298d9ede41fed8bac6c9f97cc85a30fb Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 18:25:40 +0800 Subject: [PATCH 22/27] Bump Codeium language server --- .../LanguageServer/CodeiumInstallationManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift b/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift index f832d093..295a4467 100644 --- a/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift +++ b/Tool/Sources/CodeiumService/LanguageServer/CodeiumInstallationManager.swift @@ -3,7 +3,7 @@ import Terminal public struct CodeiumInstallationManager { private static var isInstalling = false - static let latestSupportedVersion = "1.42.7" + static let latestSupportedVersion = "1.46.3" static let minimumSupportedVersion = "1.20.0" public init() {} From a00642aa9c2efa39155a6166ae3791712ea4cf38 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 18:26:00 +0800 Subject: [PATCH 23/27] Bump GitHub Copilot language server --- .../GitHubCopilotInstallationManager.swift | 30 ++++++++++++------- .../LanguageServer/GitHubCopilotService.swift | 30 +++++++------------ .../Services/GitHubCopilotChatService.swift | 12 ++++++-- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift index 1d9c2f84..2971786e 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotInstallationManager.swift @@ -6,17 +6,15 @@ public struct GitHubCopilotInstallationManager { public private(set) static var isInstalling = false static var downloadURL: URL { - let commitHash = "a9228e015528c9307890c48083c925eb98a64a79" + let commitHash = "18f485d892b56b311fd752039d6977333ebc2a0f" let link = "https://github.com/github/copilot.vim/archive/\(commitHash).zip" return URL(string: link)! } - /// trying to update to a newer version but completions stop working for some reasons. - /// As a reference, the changes starts since 1.45.0 and GitHub's extension updates the - /// language server since this commit: - /// https://github.com/github/CopilotForXcode/commit/b2189de633417a49d6d2022aad5ff0748ebed2ac#diff-678798cf677bcd1ce276809cfccd33da9ff594b1b0c557180210a4ed2bd27ffa - /// It jumps directly to 1.48.0 from a much lower version. - static let latestSupportedVersion = "1.44.0" + /// The GitHub's version has quite a lot of changes about `watchedFiles` since the following + /// commit. + /// https://github.com/github/CopilotForXcode/commit/a50045aa3ab3b7d532cadf40c4c10bed32f81169#diff-678798cf677bcd1ce276809cfccd33da9ff594b1b0c557180210a4ed2bd27ffa + static let latestSupportedVersion = "1.48.0" static let minimumSupportedVersion = "1.32.0" public init() {} @@ -47,11 +45,23 @@ public struct GitHubCopilotInstallationManager { case .orderedAscending: switch version.compare(Self.minimumSupportedVersion) { case .orderedAscending: - return .outdated(current: version, latest: Self.latestSupportedVersion, mandatory: true) + return .outdated( + current: version, + latest: Self.latestSupportedVersion, + mandatory: true + ) case .orderedSame: - return .outdated(current: version, latest: Self.latestSupportedVersion, mandatory: false) + return .outdated( + current: version, + latest: Self.latestSupportedVersion, + mandatory: false + ) case .orderedDescending: - return .outdated(current: version, latest: Self.latestSupportedVersion, mandatory: false) + return .outdated( + current: version, + latest: Self.latestSupportedVersion, + mandatory: false + ) } case .orderedSame: return .installed(version) diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift index 2a4b56ef..2f874200 100644 --- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift @@ -134,9 +134,9 @@ public class GitHubCopilotBaseService { let urls = try GitHubCopilotBaseService.createFoldersIfNeeded() let executionParams: Process.ExecutionParameters let runner = UserDefaults.shared.value(for: \.runNodeWith) - let watchedFiles = JSONValue( - booleanLiteral: projectRootURL.path == "/" ? false : true - ) +// let watchedFiles = JSONValue( +// booleanLiteral: projectRootURL.path == "/" ? false : true +// ) guard let agentJSURL = { () -> URL? in let languageServerDotJS = urls.executableURL @@ -241,17 +241,7 @@ public class GitHubCopilotBaseService { server.initializeParamsProvider = { let capabilities = ClientCapabilities( - workspace: .init( - applyEdit: false, - workspaceEdit: nil, - didChangeConfiguration: nil, - didChangeWatchedFiles: nil, - symbol: nil, - executeCommand: nil, - workspaceFolders: true, - configuration: nil, - semanticTokens: nil - ), + workspace: nil, textDocument: nil, window: nil, general: nil, @@ -283,15 +273,15 @@ public class GitHubCopilotBaseService { "version": .string(Bundle.main .infoDictionary?["CFBundleShortVersionString"] as? String ?? ""), ]), - "copilotCapabilities": [ - /// The editor has support for watching files over LSP - "watchedFiles": watchedFiles, - ], +// "copilotCapabilities": [ +// /// The editor has support for watching files over LSP +// "watchedFiles": watchedFiles, +// ], ], capabilities: capabilities, trace: .off, workspaceFolders: [WorkspaceFolder( - uri: projectRootURL.path, + uri: projectRootURL.absoluteString, name: projectRootURL.lastPathComponent )] ) @@ -474,7 +464,7 @@ public final class GitHubCopilotService: GitHubCopilotBaseService, do { let completions = try await server .sendRequest(GitHubCopilotRequest.InlineCompletion(doc: .init( - textDocument: .init(uri: fileURL.path, version: 1), + textDocument: .init(uri: fileURL.absoluteString, version: 1), position: cursorPosition, formattingOptions: .init( tabSize: tabSize, diff --git a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift index f3a76bb1..280b7068 100644 --- a/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift +++ b/Tool/Sources/GitHubCopilotService/Services/GitHubCopilotChatService.swift @@ -31,8 +31,8 @@ public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { tabSize: 1, indentSize: 4, insertSpaces: true, - path: editorContent?.documentURL.path ?? "", - uri: editorContent?.documentURL.path ?? "", + path: editorContent?.documentURL.absoluteString ?? "", + uri: editorContent?.documentURL.absoluteString ?? "", relativePath: editorContent?.relativePath ?? "", languageId: editorContent?.language ?? .plaintext, position: editorContent?.editorContent?.cursorPosition ?? .zero @@ -43,7 +43,7 @@ public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { capabilities: .init(skills: [], allSkills: false), textDocument: doc, source: .panel, - workspaceFolder: workspace.projectURL.path, + workspaceFolder: workspace.projectURL.absoluteString, model: { let selectedModel = UserDefaults.shared.value(for: \.gitHubCopilotModelId) if selectedModel.isEmpty { @@ -97,6 +97,12 @@ public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType { "\(error). Please try enabling pretend IDE to be VSCode and click refresh configuration." ) ) + } else if error.contains("No model configuration found") { + continuation.finish( + throwing: GitHubCopilotError.chatEndsWithError( + "\(error). Please try enabling pretend IDE to be VSCode and click refresh configuration." + ) + ) } else { continuation.finish( throwing: GitHubCopilotError.chatEndsWithError(error) From 8b12291e0e4834418844a0f097a5fa510cba31e0 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 18:32:35 +0800 Subject: [PATCH 24/27] Update --- .../xcshareddata/swiftpm/Package.resolved | 229 ++++++++++++++---- 1 file changed, 182 insertions(+), 47 deletions(-) diff --git a/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved index a0493266..87fd4d4e 100644 --- a/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,23 @@ { "pins" : [ + { + "identity" : "aexml", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tadija/AEXML.git", + "state" : { + "revision" : "db806756c989760b35108146381535aec231092b", + "version" : "4.7.0" + } + }, + { + "identity" : "cgeventoverride", + "kind" : "remoteSourceControl", + "location" : "https://github.com/intitni/CGEventOverride", + "state" : { + "revision" : "571d36d63e68fac30e4a350600cd186697936f74", + "version" : "1.2.3" + } + }, { "identity" : "codablewrappers", "kind" : "remoteSourceControl", @@ -14,8 +32,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/combine-schedulers", "state" : { - "revision" : "0625932976b3ae23949f6b816d13bd97f3b40b7c", - "version" : "0.10.0" + "revision" : "5928286acce13def418ec36d05a001a9641086f2", + "version" : "1.0.3" + } + }, + { + "identity" : "copilotforxcodekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/intitni/CopilotForXcodeKit", + "state" : { + "branch" : "feature/custom-chat-tab", + "revision" : "63915ee1f8aba5375bc0f0166c8645fe81fe5b88" } }, { @@ -30,10 +57,10 @@ { "identity" : "generative-ai-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/google/generative-ai-swift", + "location" : "https://github.com/intitni/generative-ai-swift", "state" : { - "revision" : "f4a88085d5a6c1108f5a1aead83d19d02df8328d", - "version" : "0.4.9" + "branch" : "support-setting-base-url", + "revision" : "12d7b30b566a64cc0dd628130bfb99a07368fea7" } }, { @@ -50,8 +77,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/intitni/Highlightr", "state" : { - "branch" : "bump-highlight-js-version", - "revision" : "4ffbb1b0b721378263297cafea6f2838044eb1eb" + "branch" : "master", + "revision" : "81d8c8b3733939bf5d9e52cd6318f944cc033bd2" + } + }, + { + "identity" : "indexstore-db", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/indexstore-db.git", + "state" : { + "branch" : "release/6.1", + "revision" : "54212fce1aecb199070808bdb265e7f17e396015" } }, { @@ -90,6 +126,24 @@ "version" : "0.8.0" } }, + { + "identity" : "messagepacker", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hirotakan/MessagePacker.git", + "state" : { + "revision" : "4d8346c6bc579347e4df0429493760691c5aeca2", + "version" : "0.4.7" + } + }, + { + "identity" : "networkimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/NetworkImage", + "state" : { + "revision" : "2849f5323265386e200484b0d0f896e73c3411b9", + "version" : "6.0.1" + } + }, { "identity" : "operationplus", "kind" : "remoteSourceControl", @@ -99,6 +153,15 @@ "version" : "1.6.0" } }, + { + "identity" : "pathkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/PathKit.git", + "state" : { + "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", + "version" : "1.0.1" + } + }, { "identity" : "processenv", "kind" : "remoteSourceControl", @@ -109,30 +172,30 @@ } }, { - "identity" : "sparkle", + "identity" : "sourcekitten", "kind" : "remoteSourceControl", - "location" : "https://github.com/sparkle-project/Sparkle", + "location" : "https://github.com/jpsim/SourceKitten", "state" : { - "revision" : "87e4fcbac39912f9cdb9a9acf205cad60e1ca3bc", - "version" : "2.4.2" + "revision" : "eb6656ed26bdef967ad8d07c27e2eab34dc582f2", + "version" : "0.37.0" } }, { - "identity" : "sttextkitplus", + "identity" : "sparkle", "kind" : "remoteSourceControl", - "location" : "https://github.com/krzyzanowskim/STTextKitPlus", + "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "a57a2081e364c71b11e521ed8800481e8da300ac", - "version" : "0.1.0" + "revision" : "0ca3004e98712ea2b39dd881d28448630cce1c99", + "version" : "2.7.0" } }, { - "identity" : "sttextview", + "identity" : "spectre", "kind" : "remoteSourceControl", - "location" : "https://github.com/krzyzanowskim/STTextView", + "location" : "https://github.com/kylef/Spectre.git", "state" : { - "revision" : "e9e54718b882115db69ec1e17ac1bec844906cd9", - "version" : "0.9.0" + "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7", + "version" : "0.10.1" } }, { @@ -149,8 +212,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-async-algorithms", "state" : { - "revision" : "9cfed92b026c524674ed869a4ff2dcfdeedf8a2a", - "version" : "0.1.0" + "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b", + "version" : "1.0.4" } }, { @@ -158,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", - "version" : "0.14.1" + "revision" : "41b89b8b68d8c56c622dbb7132258f1a3e638b25", + "version" : "1.7.0" } }, { @@ -167,8 +230,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-clocks", "state" : { - "revision" : "f9acfa1a45f4483fe0f2c434a74e6f68f865d12d", - "version" : "0.3.0" + "revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e", + "version" : "1.0.6" + } + }, + { + "identity" : "swift-cmark", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-cmark", + "state" : { + "revision" : "b022b08312decdc46585e0b3440d97f6f22ef703", + "version" : "0.6.0" } }, { @@ -176,8 +248,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" } }, { @@ -185,8 +257,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture", "state" : { - "revision" : "89f80fe2400d21a853abc9556a060a2fa50eb2cb", - "version" : "0.55.0" + "revision" : "69247baf7be2fd6f5820192caef0082d01849cd0", + "version" : "1.16.1" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "82a4ae7170d98d8538ec77238b7eb8e7199ef2e8", + "version" : "1.3.1" } }, { @@ -194,8 +275,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump", "state" : { - "revision" : "3a35f7892e7cf6ba28a78cd46a703c0be4e0c6dc", - "version" : "0.11.0" + "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", + "version" : "1.3.3" } }, { @@ -203,8 +284,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "de1a984a71e51f6e488e98ce3652035563eb8acb", - "version" : "0.5.1" + "revision" : "4c90d6b2b9bf0911af87b103bb40f41771891596", + "version" : "1.9.2" } }, { @@ -212,8 +293,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-identified-collections", "state" : { - "revision" : "d01446a78fb768adc9a78cbb6df07767c8ccfc29", - "version" : "0.8.0" + "revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597", + "version" : "1.1.1" } }, { @@ -221,8 +302,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/gonzalezreal/swift-markdown-ui", "state" : { - "revision" : "12b351a75201a8124c2f2e1f9fc6ef5cd812c0b9", - "version" : "2.1.0" + "revision" : "5f613358148239d0292c0cef674a3c2314737f9e", + "version" : "2.4.1" + } + }, + { + "identity" : "swift-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-navigation", + "state" : { + "revision" : "db6bc9dbfed001f21e6728fd36413d9342c235b4", + "version" : "2.3.0" } }, { @@ -230,8 +320,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-parsing", "state" : { - "revision" : "27c941bbd22a4bbc53005a15a0440443fd892f70", - "version" : "0.12.1" + "revision" : "3432cb81164dd3d69a75d0d63205be5fbae2c34b", + "version" : "0.14.1" + } + }, + { + "identity" : "swift-perception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-perception", + "state" : { + "revision" : "d924c62a70fca5f43872f286dbd7cef0957f1c01", + "version" : "1.6.0" } }, { @@ -239,8 +338,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version" : "509.0.2" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { @@ -252,6 +351,15 @@ "version" : "2.6.1" } }, + { + "identity" : "swiftterm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/migueldeicaza/SwiftTerm", + "state" : { + "revision" : "e2b431dbf73f775fb4807a33e4572ffd3dc6933a", + "version" : "1.2.5" + } + }, { "identity" : "swifttreesitter", "kind" : "remoteSourceControl", @@ -262,12 +370,21 @@ } }, { - "identity" : "swiftui-navigation", + "identity" : "swiftui-introspect", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swiftui-navigation", + "location" : "https://github.com/siteline/swiftui-introspect", "state" : { - "revision" : "2aa885e719087ee19df251c08a5980ad3e787f12", - "version" : "0.8.0" + "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", + "version" : "1.3.0" + } + }, + { + "identity" : "swxmlhash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/drmohundro/SWXMLHash.git", + "state" : { + "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", + "version" : "7.0.2" } }, { @@ -297,13 +414,31 @@ "version" : "0.19.3" } }, + { + "identity" : "xcodeproj", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/XcodeProj.git", + "state" : { + "revision" : "b1caa062d4aaab3e3d2bed5fe0ac5f8ce9bf84f4", + "version" : "8.27.7" + } + }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865", - "version" : "0.9.0" + "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", + "version" : "1.5.2" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "b4b8042411dc7bbb696300a34a4bf3ba1b7ad19b", + "version" : "5.3.1" } } ], From 3905f1a2f5f1752492822bc7476a13fed6ce5966 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 22:38:58 +0800 Subject: [PATCH 25/27] Adjust always on top behavior --- .../Chat/ChatSettingsGeneralSectionView.swift | 13 ++++++++----- .../WidgetWindowsController.swift | 15 +++++++++++---- Tool/Sources/Preferences/Keys.swift | 10 ++++++++-- Version.xcconfig | 2 +- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift index 910f3046..dfb60355 100644 --- a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift +++ b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift @@ -25,7 +25,7 @@ struct ChatSettingsGeneralSectionView: View { @AppStorage(\.chatModels) var chatModels @AppStorage(\.embeddingModels) var embeddingModels @AppStorage(\.wrapCodeInChatCodeBlock) var wrapCodeInCodeBlock - @AppStorage(\.alwaysDisableFloatOnTopForChatPanel) var alwaysDisableFloatOnTopForChatPanel + @AppStorage(\.chatPanelFloatOnTopOption) var chatPanelFloatOnTopOption @AppStorage( \.keepFloatOnTopIfChatPanelAndXcodeOverlaps ) var keepFloatOnTopIfChatPanelAndXcodeOverlaps @@ -299,20 +299,23 @@ struct ChatSettingsGeneralSectionView: View { CodeHighlightThemePicker(scenario: .chat) - Toggle(isOn: $settings.alwaysDisableFloatOnTopForChatPanel) { - Text("Always disable always-on-top.") + Picker("Always-on-top behavior", selection: $settings.chatPanelFloatOnTopOption) { + Text("Always").tag(UserDefaultPreferenceKeys.ChatPanelFloatOnTopOption.alwaysOnTop) + Text("When Xcode is active") + .tag(UserDefaultPreferenceKeys.ChatPanelFloatOnTopOption.onTopWhenXcodeIsActive) + Text("Never").tag(UserDefaultPreferenceKeys.ChatPanelFloatOnTopOption.never) } Toggle(isOn: $settings.disableFloatOnTopWhenTheChatPanelIsDetached) { Text("Disable always-on-top when the chat panel is detached") - }.disabled(settings.alwaysDisableFloatOnTopForChatPanel) + }.disabled(settings.chatPanelFloatOnTopOption == .never) Toggle(isOn: $settings.keepFloatOnTopIfChatPanelAndXcodeOverlaps) { Text("Keep always-on-top if the chat panel and Xcode overlaps and Xcode is active") } .disabled( !settings.disableFloatOnTopWhenTheChatPanelIsDetached - || settings.alwaysDisableFloatOnTopForChatPanel + || settings.chatPanelFloatOnTopOption == .never ) } } diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index 11b06f11..a56ed49d 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -540,14 +540,14 @@ extension WidgetWindowsController { @MainActor func adjustChatPanelWindowLevel() async { - let alwaysDisableChatPanelFlowOnTop = UserDefaults.shared - .value(for: \.alwaysDisableFloatOnTopForChatPanel) + let flowOnTopOption = UserDefaults.shared + .value(for: \.chatPanelFloatOnTopOption) let disableFloatOnTopWhenTheChatPanelIsDetached = UserDefaults.shared .value(for: \.disableFloatOnTopWhenTheChatPanelIsDetached) let window = windows.chatPanelWindow - if alwaysDisableChatPanelFlowOnTop { + if flowOnTopOption == .never { window.setFloatOnTop(false) return } @@ -599,7 +599,14 @@ extension WidgetWindowsController { let overlap = await overlap window.setFloatOnTop(overlap) } else { - window.setFloatOnTop(false) + switch flowOnTopOption { + case .onTopWhenXcodeIsActive: + window.setFloatOnTop(false) + case .alwaysOnTop: + window.setFloatOnTop(true) + case .never: + window.setFloatOnTop(false) + } } } } diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 3d613e73..e7dae5b9 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -486,9 +486,15 @@ public extension UserDefaultPreferenceKeys { var preferredChatModelIdForUtilities: PreferenceKey { .init(defaultValue: "", key: "PreferredChatModelIdForUtilities") } + + enum ChatPanelFloatOnTopOption: Int, Codable, Equatable { + case alwaysOnTop + case onTopWhenXcodeIsActive + case never + } - var alwaysDisableFloatOnTopForChatPanel: PreferenceKey { - .init(defaultValue: false, key: "AlwaysDisableFloatOnTopForChatPanel") + var chatPanelFloatOnTopOption: PreferenceKey { + .init(defaultValue: .onTopWhenXcodeIsActive, key: "ChatPanelFloatOnTopOption") } var disableFloatOnTopWhenTheChatPanelIsDetached: PreferenceKey { diff --git a/Version.xcconfig b/Version.xcconfig index 655461da..bfd1a944 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,4 +1,4 @@ APP_VERSION = 0.35.8 -APP_BUILD = 456 +APP_BUILD = 458 RELEASE_CHANNEL = RELEASE_NUMBER = 1 From 58727f61e37e4eb89f4e2e6ddf152d5617800d8b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 22:50:06 +0800 Subject: [PATCH 26/27] Update default settings --- Tool/Sources/Preferences/Keys.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index e7dae5b9..b84c2835 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -498,7 +498,7 @@ public extension UserDefaultPreferenceKeys { } var disableFloatOnTopWhenTheChatPanelIsDetached: PreferenceKey { - .init(defaultValue: true, key: "DisableFloatOnTopWhenTheChatPanelIsDetached") + .init(defaultValue: false, key: "DisableFloatOnTopWhenTheChatPanelIsDetached") } var keepFloatOnTopIfChatPanelAndXcodeOverlaps: PreferenceKey { From 82307feb11e88489999e308f38a8a1f2d1fa8b04 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 9 May 2025 23:31:10 +0800 Subject: [PATCH 27/27] Fix window level --- .../SuggestionWidget/PromptToCodePanelGroupView.swift | 1 - .../Sources/SuggestionWidget/WidgetWindowsController.swift | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift b/Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift index 9957d10a..739fe6b7 100644 --- a/Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift +++ b/Core/Sources/SuggestionWidget/PromptToCodePanelGroupView.swift @@ -37,7 +37,6 @@ struct PromptToCodeTabBar: View { var body: some View { HStack(spacing: 0) { - Divider() Tabs(store: store) } .background { diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift index a56ed49d..e26fbaae 100644 --- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift +++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift @@ -396,7 +396,6 @@ extension WidgetWindowsController { let previousActiveApplication = xcodeInspector.previousActiveApplication await MainActor.run { let state = store.withState { $0 } - let isChatPanelDetached = state.chatPanelState.isDetached if let activeApp, activeApp.isXcode { let application = activeApp.appElement @@ -749,6 +748,7 @@ public final class WidgetWindows { it.isOpaque = false it.backgroundColor = .clear it.level = widgetLevel(2) + it.hoveringLevel = widgetLevel(2) it.hasShadow = true it.contentView = NSHostingView( rootView: SharedPanelView( @@ -811,6 +811,7 @@ public final class WidgetWindows { self?.store.send(.chatPanel(.hideButtonClicked)) } ) + it.hoveringLevel = widgetLevel(1) it.delegate = controller return it }() @@ -874,6 +875,8 @@ class WidgetWindow: CanBecomeKeyWindow { case normal(fullscreen: Bool) case switchingSpace } + + var hoveringLevel: NSWindow.Level = widgetLevel(0) var defaultCollectionBehavior: NSWindow.CollectionBehavior { [.fullScreenAuxiliary, .transient] @@ -908,7 +911,7 @@ class WidgetWindow: CanBecomeKeyWindow { func setFloatOnTop(_ isFloatOnTop: Bool) { let targetLevel: NSWindow.Level = isFloatOnTop - ? .init(NSWindow.Level.floating.rawValue + 1) + ? hoveringLevel : .normal if targetLevel != level {