diff --git a/Core/Package.swift b/Core/Package.swift index fcfe44af..1e05f08c 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -50,6 +50,7 @@ let package = Package( .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"), .package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"), .package(url: "https://github.com/pvieito/PythonKit.git", branch: "master"), + .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"), ], targets: [ // MARK: - Main @@ -182,6 +183,7 @@ let package = Package( // plugins "MathChatPlugin", "SearchChatPlugin", + "ShortcutChatPlugin", .product(name: "OpenAIService", package: "Tool"), .product(name: "Preferences", package: "Tool"), @@ -315,6 +317,16 @@ let package = Package( ], path: "Sources/ChatPlugins/SearchChatPlugin" ), + + .target( + name: "ShortcutChatPlugin", + dependencies: [ + "ChatPlugin", + .product(name: "Parsing", package: "swift-parsing"), + .product(name: "Terminal", package: "Tool"), + ], + path: "Sources/ChatPlugins/ShortcutChatPlugin" + ), ] ) diff --git a/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutChatPlugin.swift b/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutChatPlugin.swift new file mode 100644 index 00000000..2a1c5254 --- /dev/null +++ b/Core/Sources/ChatPlugins/ShortcutChatPlugin/ShortcutChatPlugin.swift @@ -0,0 +1,139 @@ +import ChatPlugin +import Environment +import Foundation +import OpenAIService +import Parsing +import Terminal + +public actor ShortcutChatPlugin: ChatPlugin { + public static var command: String { "shortcut" } + public nonisolated var name: String { "Shortcut" } + + let chatGPTService: any ChatGPTServiceType + var terminal: TerminalType = Terminal() + var isCancelled = false + weak var delegate: ChatPluginDelegate? + + public init(inside chatGPTService: any ChatGPTServiceType, delegate: ChatPluginDelegate) { + self.chatGPTService = chatGPTService + self.delegate = delegate + } + + public func send(content: String, originalMessage: String) async { + delegate?.pluginDidStart(self) + delegate?.pluginDidStartResponding(self) + + defer { + delegate?.pluginDidEndResponding(self) + delegate?.pluginDidEnd(self) + } + + let id = "\(Self.command)-\(UUID().uuidString)" + var message = ChatMessage(id: id, role: .assistant, content: "") + + var content = content[...] + let firstParenthesisParser = PrefixThrough("(") + let shortcutNameParser = PrefixUpTo(")") + + _ = try? firstParenthesisParser.parse(&content) + let shortcutName = try? shortcutNameParser.parse(&content) + _ = try? PrefixThrough(")").parse(&content) + + guard let shortcutName, !shortcutName.isEmpty else { + message.content = + "Please provide the shortcut name in format: `/\(Self.command)(shortcut name)`." + await chatGPTService.mutateHistory { history in + history.append(message) + } + return + } + + var input = String(content).trimmingCharacters(in: .whitespacesAndNewlines) + if input.isEmpty { + // if no input detected, use the previous message as input + input = await chatGPTService.history.last?.content ?? "" + await chatGPTService.mutateHistory { history in + history.append(.init(role: .user, content: originalMessage)) + } + } else { + await chatGPTService.mutateHistory { history in + history.append(.init(role: .user, content: originalMessage)) + } + } + + do { + if isCancelled { throw CancellationError() } + + let env = ProcessInfo.processInfo.environment + let shell = env["SHELL"] ?? "/bin/bash" + let temporaryURL = FileManager.default.temporaryDirectory + let temporaryInputFileURL = temporaryURL + .appendingPathComponent("\(id)-input.txt") + let temporaryOutputFileURL = temporaryURL + .appendingPathComponent("\(id)-output") + + try input.write(to: temporaryInputFileURL, atomically: true, encoding: .utf8) + + let command = """ + shortcuts run "\(shortcutName)" \ + -i "\(temporaryInputFileURL.path)" \ + -o "\(temporaryOutputFileURL.path)" + """ + + _ = try await terminal.runCommand( + shell, + arguments: ["-i", "-l", "-c", command], + currentDirectoryPath: "/", + environment: [:] + ) + + await Task.yield() + + if FileManager.default.fileExists(atPath: temporaryOutputFileURL.path) { + let data = try Data(contentsOf: temporaryOutputFileURL) + if let text = String(data: data, encoding: .utf8) { + message.content = text + if text.isEmpty { + message.content = "Finished" + } + await chatGPTService.mutateHistory { history in + history.append(message) + } + } else { + message.content = """ + [View File](\(temporaryOutputFileURL)) + """ + await chatGPTService.mutateHistory { history in + history.append(message) + } + } + + return + } + + message.content = "Finished" + await chatGPTService.mutateHistory { history in + history.append(message) + } + } catch { + message.content = error.localizedDescription + if error.localizedDescription.isEmpty { + message.content = "Error" + } + await chatGPTService.mutateHistory { history in + history.append(message) + } + } + } + + public func cancel() async { + isCancelled = true + await terminal.terminate() + } + + public func stopResponding() async { + isCancelled = true + await terminal.terminate() + } +} + diff --git a/Core/Sources/ChatService/AllPlugins.swift b/Core/Sources/ChatService/AllPlugins.swift index 949bd4da..efb4bfd5 100644 --- a/Core/Sources/ChatService/AllPlugins.swift +++ b/Core/Sources/ChatService/AllPlugins.swift @@ -1,10 +1,13 @@ import ChatPlugin import MathChatPlugin import SearchChatPlugin +import ShortcutChatPlugin let allPlugins: [ChatPlugin.Type] = [ TerminalChatPlugin.self, AITerminalChatPlugin.self, MathChatPlugin.self, SearchChatPlugin.self, + ShortcutChatPlugin.self, ] + diff --git a/Core/Sources/GitHubCopilotService/GitHubCopilotRequest.swift b/Core/Sources/GitHubCopilotService/GitHubCopilotRequest.swift index e46527b5..f7ccefb1 100644 --- a/Core/Sources/GitHubCopilotService/GitHubCopilotRequest.swift +++ b/Core/Sources/GitHubCopilotService/GitHubCopilotRequest.swift @@ -1,7 +1,7 @@ -import SuggestionModel import Foundation import JSONRPC import LanguageServerProtocol +import SuggestionModel struct GitHubCopilotDoc: Codable { var source: String @@ -26,8 +26,49 @@ enum GitHubCopilotRequest { struct SetEditorInfo: GitHubCopilotRequestType { struct Response: Codable {} + var networkProxy: JSONValue? { + let host = UserDefaults.shared.value(for: \.gitHubCopilotProxyHost) + if host.isEmpty { return nil } + var port = UserDefaults.shared.value(for: \.gitHubCopilotProxyPort) + if port.isEmpty { port = "80" } + let username = UserDefaults.shared.value(for: \.gitHubCopilotProxyUsername) + if username.isEmpty { + return .hash([ + "host": .string(host), + "port": .number(Double(Int(port) ?? 80)), + "rejectUnauthorized": .bool(UserDefaults.shared + .value(for: \.gitHubCopilotUseStrictSSL)), + ]) + } else { + return .hash([ + "host": .string(host), + "port": .number(Double(Int(port) ?? 80)), + "rejectUnauthorized": .bool(UserDefaults.shared + .value(for: \.gitHubCopilotUseStrictSSL)), + "username": .string(username), + "password": .string(UserDefaults.shared + .value(for: \.gitHubCopilotProxyPassword)), + + ]) + } + } + var request: ClientRequest { - .custom("setEditorInfo", .hash([ + if let networkProxy { + return .custom("setEditorInfo", .hash([ + "editorInfo": .hash([ + "name": "Xcode", + "version": "", + ]), + "editorPluginInfo": .hash([ + "name": "Copilot for Xcode", + "version": "", + ]), + "networkProxy": networkProxy, + ])) + } + + return .custom("setEditorInfo", .hash([ "editorInfo": .hash([ "name": "Xcode", "version": "", @@ -171,3 +212,4 @@ enum GitHubCopilotRequest { } } } + diff --git a/Core/Sources/GitHubCopilotService/GitHubCopilotService.swift b/Core/Sources/GitHubCopilotService/GitHubCopilotService.swift index 6bbd37f7..feeaa9c2 100644 --- a/Core/Sources/GitHubCopilotService/GitHubCopilotService.swift +++ b/Core/Sources/GitHubCopilotService/GitHubCopilotService.swift @@ -165,7 +165,13 @@ public class GitHubCopilotBaseService { self.server = server localProcessServer = localServer + + Task { + try await server.sendRequest(GitHubCopilotRequest.SetEditorInfo()) + } } + + public static func createFoldersIfNeeded() throws -> ( applicationSupportURL: URL, @@ -211,9 +217,6 @@ public final class GitHubCopilotAuthService: GitHubCopilotBaseService, public init() throws { let home = FileManager.default.homeDirectoryForCurrentUser try super.init(projectRootURL: home) - Task { - try? await server.sendRequest(GitHubCopilotRequest.SetEditorInfo()) - } } public func checkStatus() async throws -> GitHubCopilotAccountStatus { diff --git a/Core/Sources/HostApp/AccountSettings/CopilotView.swift b/Core/Sources/HostApp/AccountSettings/CopilotView.swift index 3e586d65..0b9e6666 100644 --- a/Core/Sources/HostApp/AccountSettings/CopilotView.swift +++ b/Core/Sources/HostApp/AccountSettings/CopilotView.swift @@ -13,6 +13,11 @@ struct CopilotView: View { @AppStorage(\.runNodeWith) var runNodeWith @AppStorage("username") var username: String = "" @AppStorage(\.gitHubCopilotVerboseLog) var gitHubCopilotVerboseLog + @AppStorage(\.gitHubCopilotProxyHost) var gitHubCopilotProxyHost + @AppStorage(\.gitHubCopilotProxyPort) var gitHubCopilotProxyPort + @AppStorage(\.gitHubCopilotProxyUsername) var gitHubCopilotProxyUsername + @AppStorage(\.gitHubCopilotProxyPassword) var gitHubCopilotProxyPassword + @AppStorage(\.gitHubCopilotUseStrictSSL) var gitHubCopilotUseStrictSSL init() {} } @@ -182,9 +187,9 @@ struct CopilotView: View { uninstallButton } } - + Text("Language Server Version: \(version ?? "Loading..")") - + Text("Status: \(status?.description ?? "Loading..")") HStack(alignment: .center) { @@ -226,6 +231,24 @@ struct CopilotView: View { Form { Toggle("Verbose Log", isOn: $settings.gitHubCopilotVerboseLog) } + + Divider() + + Form { + TextField(text: $settings.gitHubCopilotProxyHost, prompt: Text("xxx.xxx.xxx.xxx, leave it blank to disable proxy.")) { + Text("Proxy Host") + } + TextField(text: $settings.gitHubCopilotProxyPort, prompt: Text("80")) { + Text("Proxy Port") + } + TextField(text: $settings.gitHubCopilotProxyUsername) { + Text("Proxy Username") + } + SecureField(text: $settings.gitHubCopilotProxyPassword) { + Text("Proxy Password") + } + Toggle("Proxy Strict SSL", isOn: $settings.gitHubCopilotUseStrictSSL) + } } Spacer() }.onAppear { diff --git a/Core/Sources/HostApp/DebugView.swift b/Core/Sources/HostApp/DebugView.swift index fcf7047c..02d354e6 100644 --- a/Core/Sources/HostApp/DebugView.swift +++ b/Core/Sources/HostApp/DebugView.swift @@ -2,7 +2,9 @@ import Preferences import SwiftUI final class DebugSettings: ObservableObject { - @AppStorage(\.disableLazyVStack) var disableLazyVStack + @AppStorage(\.animationACrashSuggestion) var animationACrashSuggestion + @AppStorage(\.animationBCrashSuggestion) var animationBCrashSuggestion + @AppStorage(\.animationCCrashSuggestion) var animationCCrashSuggestion @AppStorage(\.preCacheOnFileOpen) var preCacheOnFileOpen @AppStorage(\.useCustomScrollViewWorkaround) var useCustomScrollViewWorkaround @AppStorage(\.triggerActionWithAccessibilityAPI) var triggerActionWithAccessibilityAPI @@ -15,8 +17,14 @@ struct DebugSettingsView: View { var body: some View { ScrollView { Form { - Toggle(isOn: $settings.disableLazyVStack) { - Text("Disable LazyVStack") + Toggle(isOn: $settings.animationACrashSuggestion) { + Text("Enable Animation A") + } + Toggle(isOn: $settings.animationBCrashSuggestion) { + Text("Enable Animation B") + } + Toggle(isOn: $settings.animationCCrashSuggestion) { + Text("Enable Widget Breathing Animation") } Toggle(isOn: $settings.preCacheOnFileOpen) { Text("Cache editor information on file open") diff --git a/Core/Sources/SuggestionWidget/CustomScrollView/CustomScrollView.swift b/Core/Sources/SuggestionWidget/CustomScrollView/CustomScrollView.swift index baccdffd..6828dce4 100644 --- a/Core/Sources/SuggestionWidget/CustomScrollView/CustomScrollView.swift +++ b/Core/Sources/SuggestionWidget/CustomScrollView/CustomScrollView.swift @@ -40,7 +40,9 @@ struct CustomScrollView: View { .listStyle(.plain) .frame(idealHeight: max(10, height)) .onPreferenceChange(CustomScrollViewHeightPreferenceKey.self) { newHeight in - height = newHeight + Task { @MainActor in + height = newHeight + } } } else { ScrollView { @@ -49,3 +51,4 @@ struct CustomScrollView: View { } } } + diff --git a/Core/Sources/SuggestionWidget/CustomTextEditor.swift b/Core/Sources/SuggestionWidget/CustomTextEditor.swift index 00251edc..6b14307c 100644 --- a/Core/Sources/SuggestionWidget/CustomTextEditor.swift +++ b/Core/Sources/SuggestionWidget/CustomTextEditor.swift @@ -30,6 +30,7 @@ struct CustomTextEditor: NSViewRepresentable { let textView = (context.coordinator.theTextView.documentView as! NSTextView) guard textView.string != text else { return } textView.string = text + textView.undoManager?.removeAllActions() } } diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift index 5683c2e1..14031bf5 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift @@ -362,6 +362,7 @@ struct ChatPanelInputArea: View { "/airun", "/math", "/search", + "/shortcut", "/exit", "@selection", "@file", diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelView.swift b/Core/Sources/SuggestionWidget/SuggestionPanelView.swift index e265dd33..2eea432d 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelView.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelView.swift @@ -1,4 +1,5 @@ import Environment +import Preferences import SwiftUI @MainActor @@ -20,40 +21,35 @@ final class SuggestionPanelViewModel: ObservableObject { } } - enum ActiveTab { - case suggestion - } - - @Published var content: Content? { - didSet { - requestApplicationPolicyUpdate?(self) - } - } - - @Published var activeTab: ActiveTab { - didSet { - requestApplicationPolicyUpdate?(self) - } - } - + @Published var content: Content? @Published var isPanelDisplayed: Bool @Published var alignTopToAnchor = false @Published var colorScheme: ColorScheme - var requestApplicationPolicyUpdate: ((SuggestionPanelViewModel) -> Void)? - public init( content: Content? = nil, isPanelDisplayed: Bool = false, - activeTab: ActiveTab = .suggestion, - colorScheme: ColorScheme = .dark, - requestApplicationPolicyUpdate: ((SuggestionPanelViewModel) -> Void)? = nil + colorScheme: ColorScheme = .dark ) { self.content = content self.isPanelDisplayed = isPanelDisplayed - self.activeTab = activeTab self.colorScheme = colorScheme - self.requestApplicationPolicyUpdate = requestApplicationPolicyUpdate + } +} + +extension View { + @ViewBuilder + func animation( + featureFlag: KeyPath, + _ animation: Animation?, + value: V + ) -> some View { + let isOn = UserDefaults.shared.value(for: featureFlag) + if isOn { + self.animation(animation, value: value) + } else { + self + } } } @@ -70,21 +66,19 @@ struct SuggestionPanelView: View { VStack { if let content = viewModel.content { - if case .suggestion = viewModel.activeTab { - ZStack(alignment: .topLeading) { - switch content { - case let .suggestion(suggestion): - CodeBlockSuggestionPanel(suggestion: suggestion) - case let .promptToCode(provider): - PromptToCodePanel(provider: provider) - case let .error(description): - ErrorPanel(viewModel: viewModel, description: description) - } + ZStack(alignment: .topLeading) { + switch content { + case let .suggestion(suggestion): + CodeBlockSuggestionPanel(suggestion: suggestion) + case let .promptToCode(provider): + PromptToCodePanel(provider: provider) + case let .error(description): + ErrorPanel(viewModel: viewModel, description: description) } - .frame(maxWidth: .infinity, maxHeight: Style.panelHeight) - .fixedSize(horizontal: false, vertical: true) - .allowsHitTesting(viewModel.isPanelDisplayed) } + .frame(maxWidth: .infinity, maxHeight: Style.panelHeight) + .fixedSize(horizontal: false, vertical: true) + .allowsHitTesting(viewModel.isPanelDisplayed) } } .frame(maxWidth: .infinity) @@ -101,9 +95,16 @@ struct SuggestionPanelView: View { guard viewModel.content != nil else { return 0 } return 1 }()) - .animation(.easeInOut(duration: 0.2), value: viewModel.content?.contentHash) - .animation(.easeInOut(duration: 0.2), value: viewModel.activeTab) - .animation(.easeInOut(duration: 0.2), value: viewModel.isPanelDisplayed) + .animation( + featureFlag: \.animationACrashSuggestion, + .easeInOut(duration: 0.2), + value: viewModel.content?.contentHash + ) + .animation( + featureFlag: \.animationBCrashSuggestion, + .easeInOut(duration: 0.2), + value: viewModel.isPanelDisplayed + ) .frame(maxWidth: Style.panelWidth, maxHeight: Style.panelHeight) } } @@ -155,7 +156,6 @@ struct SuggestionPanelView_Both_DisplayingSuggestion_Preview: PreviewProvider { currentSuggestionIndex: 0 )), isPanelDisplayed: true, - activeTab: .suggestion, colorScheme: .dark )) .frame(width: 450, height: 200) diff --git a/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift b/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift index 88087ec9..8c9b458f 100644 --- a/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift +++ b/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift @@ -27,7 +27,7 @@ public final class SuggestionWidgetController: NSObject { it.canBecomeKeyChecker = { false } return it }() - + private lazy var widgetWindow = { let it = CanBecomeKeyWindow( contentRect: .zero, @@ -262,7 +262,7 @@ public final class SuggestionWidgetController: NSObject { } } } - + func orderFront() { widgetWindow.orderFrontRegardless() tabWindow.orderFrontRegardless() @@ -324,7 +324,7 @@ public extension SuggestionWidgetController { } } } - + func presentDetachedGlobalChat() { chatWindowViewModel.chatPanelInASeparateWindow = true Task { diff --git a/Core/Sources/SuggestionWidget/WidgetView.swift b/Core/Sources/SuggestionWidget/WidgetView.swift index 305214b0..87e1faf8 100644 --- a/Core/Sources/SuggestionWidget/WidgetView.swift +++ b/Core/Sources/SuggestionWidget/WidgetView.swift @@ -19,7 +19,7 @@ final class WidgetViewModel: ObservableObject { let deadline = date.timeIntervalSince1970 + 20 isProcessingCounters.append(IsProcessingCounter(expirationDate: deadline)) isProcessing = true - + cleanupIsProcessingCounterTask?.cancel() cleanupIsProcessingCounterTask = Task { [weak self] in try await Task.sleep(nanoseconds: 20 * 1_000_000_000) @@ -105,6 +105,7 @@ struct WidgetView: View { .scaleEffect(x: scale, y: scale) .opacity(!empty || viewModel.isProcessing ? 1 : 0) .animation( + featureFlag: \.animationCCrashSuggestion, .easeInOut(duration: 1).repeatForever(autoreverses: true), value: processingProgress ) @@ -117,7 +118,11 @@ struct WidgetView: View { .padding(minimumLineWidth / 2) .scaleEffect(x: scale, y: scale) .opacity(!empty || viewModel.isProcessing ? 1 : 0) - .animation(.easeInOut(duration: 1), value: processingProgress) + .animation( + featureFlag: \.animationCCrashSuggestion, + .easeInOut(duration: 1), + value: processingProgress + ) } } } diff --git a/README.md b/README.md index aa742258..cefeb04b 100644 --- a/README.md +++ b/README.md @@ -214,12 +214,13 @@ If you need to end a plugin, you can just type /exit ``` -| Command | Description | -| :-------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/run` | Runs the command under the project root. You can also use environment variable `PROJECT_ROOT` to get the project root and `FILE_PATH` to get the editing file path. | -| `/airun` | Creates a command with natural language. You can ask to modify the command if it is not what you want. After confirming, the command will be executed by calling the `/run` plugin. | -| `/math` | Solves a math problem in natural language | -| `/search` | Search on Bing and summarize the results. You have to setup the Bing Search API in the host app before using it. | +| Command | Description | +| :------------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/run` | Runs the command under the project root. You can also use environment variable `PROJECT_ROOT` to get the project root and `FILE_PATH` to get the editing file path. | +| `/airun` | Creates a command with natural language. You can ask to modify the command if it is not what you want. After confirming, the command will be executed by calling the `/run` plugin. | +| `/math` | Solves a math problem in natural language | +| `/search` | Search on Bing and summarize the results. You have to setup the Bing Search API in the host app before using it. | +| `/shortcut(shortcut name)` | Run a shortcut from the Shortcuts.app, and use the following message as the input. If the message is empty, it will use the previous message as input. The output of the shortcut will be printed as a reply from the bot | ### Prompt to Code diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 1fc39649..60cbbe7a 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -122,6 +122,26 @@ public extension UserDefaultPreferenceKeys { .init(defaultValue: false, key: "GitHubCopilotVerboseLog") } + var gitHubCopilotProxyHost: PreferenceKey { + .init(defaultValue: "", key: "GitHubCopilotProxyHost") + } + + var gitHubCopilotProxyPort: PreferenceKey { + .init(defaultValue: "", key: "GitHubCopilotProxyPort") + } + + var gitHubCopilotUseStrictSSL: PreferenceKey { + .init(defaultValue: true, key: "GitHubCopilotUseStrictSSL") + } + + var gitHubCopilotProxyUsername: PreferenceKey { + .init(defaultValue: "", key: "GitHubCopilotProxyUsername") + } + + var gitHubCopilotProxyPassword: PreferenceKey { + .init(defaultValue: "", key: "GitHubCopilotProxyPassword") + } + var nodePath: PreferenceKey { .init(defaultValue: "", key: "NodePath") } @@ -245,7 +265,7 @@ public extension UserDefaultPreferenceKeys { key: "DefaultChatSystemPrompt" ) } - + var chatSearchPluginMaxIterations: PreferenceKey { .init(defaultValue: 3, key: "ChatSearchPluginMaxIterations") } @@ -257,9 +277,12 @@ public extension UserDefaultPreferenceKeys { var bingSearchSubscriptionKey: PreferenceKey { .init(defaultValue: "", key: "BingSearchSubscriptionKey") } - + var bingSearchEndpoint: PreferenceKey { - .init(defaultValue: "https://api.bing.microsoft.com/v7.0/search/", key: "BingSearchEndpoint") + .init( + defaultValue: "https://api.bing.microsoft.com/v7.0/search/", + key: "BingSearchEndpoint" + ) } } @@ -326,5 +349,17 @@ public extension UserDefaultPreferenceKeys { var triggerActionWithAccessibilityAPI: FeatureFlag { .init(defaultValue: true, key: "FeatureFlag-TriggerActionWithAccessibilityAPI") } + + var animationACrashSuggestion: FeatureFlag { + .init(defaultValue: true, key: "FeatureFlag-AnimationACrashSuggestion") + } + + var animationBCrashSuggestion: FeatureFlag { + .init(defaultValue: true, key: "FeatureFlag-AnimationBCrashSuggestion") + } + + var animationCCrashSuggestion: FeatureFlag { + .init(defaultValue: true, key: "FeatureFlag-AnimationCCrashSuggestion") + } } diff --git a/Version.xcconfig b/Version.xcconfig index bfae9a50..f31677c4 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,2 +1,2 @@ -APP_VERSION = 0.18.0 -APP_BUILD = 180 +APP_VERSION = 0.18.1 +APP_BUILD = 181 diff --git a/appcast.xml b/appcast.xml index fb6c7c74..2704adfa 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.18.1 + Sat, 10 Jun 2023 17:03:06 +0800 + 181 + 0.18.1 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.18.1 + + + + 0.18.0 Thu, 08 Jun 2023 14:34:14 +0800