From 4dd4f2f36fc43832cc680f31262be748bb5643ea Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 16 Oct 2023 01:02:04 +0800 Subject: [PATCH 01/31] Add SuggestionServiceMiddleware --- Pro | 2 +- .../CodeiumSuggestionProvider.swift | 32 +++---- .../GitHubCopilotSuggestionProvider.swift | 28 +++--- .../SuggestionService/SuggestionService.swift | 93 +++++++++++++------ .../SuggestionServiceMiddleware.swift | 41 ++++++++ 5 files changed, 133 insertions(+), 63 deletions(-) create mode 100644 Tool/Sources/SuggestionService/SuggestionServiceMiddleware.swift diff --git a/Pro b/Pro index 72fd9411..064b3ef6 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 72fd9411fb566b45e8dbb2df418cb5ef7a99d5cd +Subproject commit 064b3ef62ce3094c088e2b27abfa1231b109eb1c diff --git a/Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift b/Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift index eb03b335..a990db46 100644 --- a/Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift +++ b/Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift @@ -28,23 +28,17 @@ actor CodeiumSuggestionProvider: SuggestionServiceProvider { } extension CodeiumSuggestionProvider { - func getSuggestions( - fileURL: URL, - content: String, - cursorPosition: SuggestionModel.CursorPosition, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool, - ignoreSpaceOnlySuggestions: Bool - ) async throws -> [SuggestionModel.CodeSuggestion] { - try await (try createCodeiumServiceIfNeeded()).getCompletions( - fileURL: fileURL, - content: content, - cursorPosition: cursorPosition, - tabSize: tabSize, - indentSize: indentSize, - usesTabsForIndentation: usesTabsForIndentation, - ignoreSpaceOnlySuggestions: ignoreSpaceOnlySuggestions + func getSuggestions(_ request: SuggestionRequest) async throws + -> [SuggestionModel.CodeSuggestion] + { + try await (createCodeiumServiceIfNeeded()).getCompletions( + fileURL: request.fileURL, + content: request.content, + cursorPosition: request.cursorPosition, + tabSize: request.tabSize, + indentSize: request.indentSize, + usesTabsForIndentation: request.usesTabsForIndentation, + ignoreSpaceOnlySuggestions: request.ignoreSpaceOnlySuggestions ) } @@ -70,12 +64,12 @@ extension CodeiumSuggestionProvider { } func notifySaveTextDocument(fileURL: URL) async throws {} - + func cancelRequest() async { await (try? createCodeiumServiceIfNeeded())? .cancelRequest() } - + func terminate() async { (try? createCodeiumServiceIfNeeded())?.terminate() } diff --git a/Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift b/Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift index f7baeea9..3632ba77 100644 --- a/Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift +++ b/Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift @@ -26,23 +26,17 @@ actor GitHubCopilotSuggestionProvider: SuggestionServiceProvider { } extension GitHubCopilotSuggestionProvider { - func getSuggestions( - fileURL: URL, - content: String, - cursorPosition: SuggestionModel.CursorPosition, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool, - ignoreSpaceOnlySuggestions: Bool - ) async throws -> [SuggestionModel.CodeSuggestion] { - try await (try createGitHubCopilotServiceIfNeeded()).getCompletions( - fileURL: fileURL, - content: content, - cursorPosition: cursorPosition, - tabSize: tabSize, - indentSize: indentSize, - usesTabsForIndentation: usesTabsForIndentation, - ignoreSpaceOnlySuggestions: ignoreSpaceOnlySuggestions, + func getSuggestions(_ request: SuggestionRequest) async throws + -> [SuggestionModel.CodeSuggestion] + { + try await (createGitHubCopilotServiceIfNeeded()).getCompletions( + fileURL: request.fileURL, + content: request.content, + cursorPosition: request.cursorPosition, + tabSize: request.tabSize, + indentSize: request.indentSize, + usesTabsForIndentation: request.usesTabsForIndentation, + ignoreSpaceOnlySuggestions: request.ignoreSpaceOnlySuggestions, ignoreTrailingNewLinesAndSpaces: UserDefaults.shared .value(for: \.gitHubCopilotIgnoreTrailingNewLines) ) diff --git a/Tool/Sources/SuggestionService/SuggestionService.swift b/Tool/Sources/SuggestionService/SuggestionService.swift index 4213121a..7439d707 100644 --- a/Tool/Sources/SuggestionService/SuggestionService.swift +++ b/Tool/Sources/SuggestionService/SuggestionService.swift @@ -4,8 +4,16 @@ import Preferences import SuggestionModel import UserDefaultsObserver -public protocol SuggestionServiceType { - func getSuggestions( +public struct SuggestionRequest { + public var fileURL: URL + public var content: String + public var cursorPosition: CursorPosition + public var tabSize: Int + public var indentSize: Int + public var usesTabsForIndentation: Bool + public var ignoreSpaceOnlySuggestions: Bool + + public init( fileURL: URL, content: String, cursorPosition: CursorPosition, @@ -13,7 +21,19 @@ public protocol SuggestionServiceType { indentSize: Int, usesTabsForIndentation: Bool, ignoreSpaceOnlySuggestions: Bool - ) async throws -> [CodeSuggestion] + ) { + self.fileURL = fileURL + self.content = content + self.cursorPosition = cursorPosition + self.tabSize = tabSize + self.indentSize = indentSize + self.usesTabsForIndentation = usesTabsForIndentation + self.ignoreSpaceOnlySuggestions = ignoreSpaceOnlySuggestions + } +} + +public protocol SuggestionServiceType { + func getSuggestions(_ request: SuggestionRequest) async throws -> [CodeSuggestion] func notifyAccepted(_ suggestion: CodeSuggestion) async func notifyRejected(_ suggestions: [CodeSuggestion]) async @@ -25,9 +45,45 @@ public protocol SuggestionServiceType { func terminate() async } +public extension SuggestionServiceType { + func getSuggestions( + fileURL: URL, + content: String, + cursorPosition: CursorPosition, + tabSize: Int, + indentSize: Int, + usesTabsForIndentation: Bool, + ignoreSpaceOnlySuggestions: Bool + ) async throws -> [CodeSuggestion] { + return try await getSuggestions(.init( + fileURL: fileURL, + content: content, + cursorPosition: cursorPosition, + tabSize: tabSize, + indentSize: indentSize, + usesTabsForIndentation: usesTabsForIndentation, + ignoreSpaceOnlySuggestions: ignoreSpaceOnlySuggestions + )) + } +} + protocol SuggestionServiceProvider: SuggestionServiceType {} public actor SuggestionService: SuggestionServiceType { + static var builtInMiddlewares: [SuggestionServiceMiddleware] = [ + DisabledLanguageSuggestionServiceMiddleware() + ] + + static var customMiddlewares: [SuggestionServiceMiddleware] = [] + + static var middlewares: [SuggestionServiceMiddleware] { + builtInMiddlewares + customMiddlewares + } + + public static func addMiddleware(_ middleware: SuggestionServiceMiddleware) { + customMiddlewares.append(middleware) + } + let projectRootURL: URL let onServiceLaunched: (SuggestionServiceType) -> Void let providerChangeObserver = UserDefaultsObserver( @@ -75,31 +131,16 @@ public actor SuggestionService: SuggestionServiceType { } public extension SuggestionService { - func getSuggestions( - fileURL: URL, - content: String, - cursorPosition: SuggestionModel.CursorPosition, - tabSize: Int, - indentSize: Int, - usesTabsForIndentation: Bool, - ignoreSpaceOnlySuggestions: Bool - ) async throws -> [SuggestionModel.CodeSuggestion] { - let language = languageIdentifierFromFileURL(fileURL) - if UserDefaults.shared.value(for: \.suggestionFeatureDisabledLanguageList) - .contains(where: { $0 == language.rawValue }) - { - return [] + func getSuggestions(_ request: SuggestionRequest) async throws -> [SuggestionModel.CodeSuggestion] { + var getSuggestion = suggestionProvider.getSuggestions + + for middleware in Self.middlewares.reversed() { + getSuggestion = { request in + try await middleware.getSuggestion(request, next: getSuggestion) + } } - return try await suggestionProvider.getSuggestions( - fileURL: fileURL, - content: content, - cursorPosition: cursorPosition, - tabSize: tabSize, - indentSize: indentSize, - usesTabsForIndentation: usesTabsForIndentation, - ignoreSpaceOnlySuggestions: ignoreSpaceOnlySuggestions - ) + return try await getSuggestion(request) } func notifyAccepted(_ suggestion: SuggestionModel.CodeSuggestion) async { diff --git a/Tool/Sources/SuggestionService/SuggestionServiceMiddleware.swift b/Tool/Sources/SuggestionService/SuggestionServiceMiddleware.swift new file mode 100644 index 00000000..29575a40 --- /dev/null +++ b/Tool/Sources/SuggestionService/SuggestionServiceMiddleware.swift @@ -0,0 +1,41 @@ +import Foundation +import SuggestionModel +import Logger + +public protocol SuggestionServiceMiddleware { + typealias Next = (SuggestionRequest) async throws -> [CodeSuggestion] + + func getSuggestion(_ request: SuggestionRequest, next: Next) async throws -> [CodeSuggestion] +} + +struct DisabledLanguageSuggestionServiceMiddleware: SuggestionServiceMiddleware { + func getSuggestion(_ request: SuggestionRequest, next: Next) async throws -> [CodeSuggestion] { + let language = languageIdentifierFromFileURL(request.fileURL) + if UserDefaults.shared.value(for: \.suggestionFeatureDisabledLanguageList) + .contains(where: { $0 == language.rawValue }) + { + #if DEBUG + Logger.service.info("Suggestion service is disabled for \(language).") + #endif + return [] + } + + return try await next(request) + } +} + +public struct DebugSuggestionServiceMiddleware: SuggestionServiceMiddleware { + public init() {} + + public func getSuggestion(_ request: SuggestionRequest, next: Next) async throws -> [CodeSuggestion] { + Logger.service.debug(""" + Get suggestion for \(request.fileURL) at \(request.cursorPosition) + """) + let suggestions = try await next(request) + Logger.service.debug(""" + Receive \(suggestions.count) suggestions for \(request.fileURL) at \(request.cursorPosition) + """) + + return suggestions + } +} From e1a6722e440ba5c975323b43469893da3368a61f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 16 Oct 2023 01:17:25 +0800 Subject: [PATCH 02/31] Update --- Pro | 2 +- .../SuggestionService/SuggestionService.swift | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Pro b/Pro index 064b3ef6..943c9447 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 064b3ef62ce3094c088e2b27abfa1231b109eb1c +Subproject commit 943c94477524098f3cd90baa5e2eca0f4dead323 diff --git a/Tool/Sources/SuggestionService/SuggestionService.swift b/Tool/Sources/SuggestionService/SuggestionService.swift index 7439d707..ed66e19f 100644 --- a/Tool/Sources/SuggestionService/SuggestionService.swift +++ b/Tool/Sources/SuggestionService/SuggestionService.swift @@ -71,19 +71,19 @@ protocol SuggestionServiceProvider: SuggestionServiceType {} public actor SuggestionService: SuggestionServiceType { static var builtInMiddlewares: [SuggestionServiceMiddleware] = [ - DisabledLanguageSuggestionServiceMiddleware() + DisabledLanguageSuggestionServiceMiddleware(), ] - + static var customMiddlewares: [SuggestionServiceMiddleware] = [] - + static var middlewares: [SuggestionServiceMiddleware] { builtInMiddlewares + customMiddlewares } - + public static func addMiddleware(_ middleware: SuggestionServiceMiddleware) { customMiddlewares.append(middleware) } - + let projectRootURL: URL let onServiceLaunched: (SuggestionServiceType) -> Void let providerChangeObserver = UserDefaultsObserver( @@ -131,9 +131,11 @@ public actor SuggestionService: SuggestionServiceType { } public extension SuggestionService { - func getSuggestions(_ request: SuggestionRequest) async throws -> [SuggestionModel.CodeSuggestion] { + func getSuggestions( + _ request: SuggestionRequest + ) async throws -> [SuggestionModel.CodeSuggestion] { var getSuggestion = suggestionProvider.getSuggestions - + for middleware in Self.middlewares.reversed() { getSuggestion = { request in try await middleware.getSuggestion(request, next: getSuggestion) @@ -167,6 +169,7 @@ public extension SuggestionService { try await suggestionProvider.notifySaveTextDocument(fileURL: fileURL) } + #warning("Move the cancellation to this type so that we can also cancel middlewares") func cancelRequest() async { await suggestionProvider.cancelRequest() } From 5116c1d144b8de53a76a95c3ead5e235920e329e Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 16 Oct 2023 15:03:14 +0800 Subject: [PATCH 03/31] Fix calling middleware creates infinite loop --- Pro | 2 +- Tool/Sources/SuggestionService/SuggestionService.swift | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Pro b/Pro index 943c9447..7796423c 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 943c94477524098f3cd90baa5e2eca0f4dead323 +Subproject commit 7796423c9827e16a928d5611cf30cec2908b33b0 diff --git a/Tool/Sources/SuggestionService/SuggestionService.swift b/Tool/Sources/SuggestionService/SuggestionService.swift index ed66e19f..2cc194e3 100644 --- a/Tool/Sources/SuggestionService/SuggestionService.swift +++ b/Tool/Sources/SuggestionService/SuggestionService.swift @@ -131,13 +131,13 @@ public actor SuggestionService: SuggestionServiceType { } public extension SuggestionService { - func getSuggestions( + func getSuggestions( _ request: SuggestionRequest ) async throws -> [SuggestionModel.CodeSuggestion] { var getSuggestion = suggestionProvider.getSuggestions - + for middleware in Self.middlewares.reversed() { - getSuggestion = { request in + getSuggestion = { [getSuggestion] request in try await middleware.getSuggestion(request, next: getSuggestion) } } @@ -169,7 +169,6 @@ public extension SuggestionService { try await suggestionProvider.notifySaveTextDocument(fileURL: fileURL) } - #warning("Move the cancellation to this type so that we can also cancel middlewares") func cancelRequest() async { await suggestionProvider.cancelRequest() } From b8ae34e2b5df21eb0f792df19536e346ab1bc054 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 16 Oct 2023 22:55:48 +0800 Subject: [PATCH 04/31] Support chat tab self-closing --- .../SuggestionWidget/FeatureReducers/ChatPanelFeature.swift | 5 +++++ Tool/Sources/ChatTab/ChatTabItem.swift | 3 +++ 2 files changed, 8 insertions(+) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift index b63d74b3..47018a0e 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift @@ -218,6 +218,11 @@ public struct ChatPanelFeature: ReducerProtocol { state.chatTabGroup.tabInfo.insert(tab, at: to) return .none + case let .chatTab(id, .close): + return .run { send in + await send(.closeTabButtonClicked(id: id)) + } + case .chatTab: return .none } diff --git a/Tool/Sources/ChatTab/ChatTabItem.swift b/Tool/Sources/ChatTab/ChatTabItem.swift index b74063cf..128af1e7 100644 --- a/Tool/Sources/ChatTab/ChatTabItem.swift +++ b/Tool/Sources/ChatTab/ChatTabItem.swift @@ -20,6 +20,7 @@ public struct ChatTabItem: ReducerProtocol { case updateTitle(String) case openNewTab(AnyChatTabBuilder) case tabContentUpdated + case close } public init() {} @@ -34,6 +35,8 @@ public struct ChatTabItem: ReducerProtocol { return .none case .tabContentUpdated: return .none + case .close: + return .none } } } From 08b0d7612b162c1b713d68a31d84cfcf4df57205 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 16 Oct 2023 22:56:06 +0800 Subject: [PATCH 05/31] Add TerminalChatTab --- Core/Sources/Service/GUI/ChatTabFactory.swift | 1 + Pro | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Core/Sources/Service/GUI/ChatTabFactory.swift b/Core/Sources/Service/GUI/ChatTabFactory.swift index 6164f7c9..82114837 100644 --- a/Core/Sources/Service/GUI/ChatTabFactory.swift +++ b/Core/Sources/Service/GUI/ChatTabFactory.swift @@ -31,6 +31,7 @@ enum ChatTabFactory { ), title: BrowserChatTab.name ), + folderIfNeeded(TerminalChatTab.chatBuilders(), title: TerminalChatTab.name), ].compactMap { $0 } return collection diff --git a/Pro b/Pro index 7796423c..4587c284 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 7796423c9827e16a928d5611cf30cec2908b33b0 +Subproject commit 4587c2844d715fcc68c76cc63dbea3b544547d72 From 6cfab9042ee460a44945ff265c51f9d391f59f00 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 16 Oct 2023 23:39:07 +0800 Subject: [PATCH 06/31] Update TerminalChatTab to support restoration --- .../Service/GUI/GraphicalUserInterfaceController.swift | 8 +++++++- Pro | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index bad5aca2..b253f89b 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -388,6 +388,12 @@ extension ChatTabPool { externalDependency: ChatTabFactory.externalDependenciesForBrowserChatTab() ) else { break } return await createTab(id: data.id, from: builder) + case TerminalChatTab.name: + guard let builder = try? await TerminalChatTab.restore( + from: data.data, + externalDependency: () + ) else { break } + return await createTab(id: data.id, from: builder) default: break } @@ -397,7 +403,7 @@ extension ChatTabPool { ) else { return nil } - return await createTab(from: builder) + return await createTab(id: data.id, from: builder) } #endif } diff --git a/Pro b/Pro index 4587c284..691ee9a6 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 4587c2844d715fcc68c76cc63dbea3b544547d72 +Subproject commit 691ee9a6bd58f35e92929c09bd5a92c604685c0e From c674551ebd90a1311dbf16c09b3dcb86884ac773 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 17 Oct 2023 00:06:06 +0800 Subject: [PATCH 07/31] Add icons to chat tab --- .../ChatGPTChatTab/ChatGPTChatTab.swift | 11 ++++++++++ .../SuggestionWidget/ChatWindowView.swift | 13 ++++++++---- Pro | 2 +- Tool/Sources/ChatTab/ChatTab.swift | 20 ++++++++++++++++++- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift index 3841089f..17babd6d 100644 --- a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift +++ b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift @@ -46,6 +46,16 @@ public class ChatGPTChatTab: ChatTab { ChatTabItemView(chat: chat) } + public func buildIcon() -> any View { + WithViewStore(chat, observe: \.isReceivingMessage) { viewStore in + if viewStore.state { + Image(systemName: "ellipsis.message") + } else { + Image(systemName: "message") + } + } + } + public func buildMenu() -> any View { ChatContextMenu(store: chat.scope(state: \.chatMenu, action: Chat.Action.chatMenu)) } @@ -125,3 +135,4 @@ public class ChatGPTChatTab: ChatTab { }.store(in: &cancellable) } } + diff --git a/Core/Sources/SuggestionWidget/ChatWindowView.swift b/Core/Sources/SuggestionWidget/ChatWindowView.swift index f82d7a89..a0d53ba9 100644 --- a/Core/Sources/SuggestionWidget/ChatWindowView.swift +++ b/Core/Sources/SuggestionWidget/ChatWindowView.swift @@ -177,6 +177,7 @@ struct ChatTabBar: View { store: store, info: info, content: { tab.tabItem }, + icon: { tab.icon }, isSelected: info.id == viewStore.state.selectedTabId ) .contextMenu { @@ -280,7 +281,7 @@ struct ChatTabBarDropDelegate: DropDelegate { let tabs: IdentifiedArray let itemId: String @Binding var draggingTabId: String? - + func dropUpdated(info: DropInfo) -> DropProposal? { return DropProposal(operation: .move) } @@ -299,20 +300,24 @@ struct ChatTabBarDropDelegate: DropDelegate { } } -struct ChatTabBarButton: View { +struct ChatTabBarButton: View { let store: StoreOf let info: ChatTabInfo let content: () -> Content + let icon: () -> Icon let isSelected: Bool @State var isHovered: Bool = false var body: some View { HStack(spacing: 0) { - content() + HStack(spacing: 4) { + icon().foregroundColor(.secondary) + content() + } .font(.callout) .lineLimit(1) .frame(maxWidth: 120) - .padding(.horizontal, 32) + .padding(.horizontal, 28) .contentShape(Rectangle()) .onTapGesture { store.send(.tabClicked(id: info.id)) diff --git a/Pro b/Pro index 691ee9a6..c0e446d3 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 691ee9a6bd58f35e92929c09bd5a92c604685c0e +Subproject commit c0e446d33ce2451fde612564d3b1e2c57d529e9c diff --git a/Tool/Sources/ChatTab/ChatTab.swift b/Tool/Sources/ChatTab/ChatTab.swift index fc2fe82f..1c09ef5e 100644 --- a/Tool/Sources/ChatTab/ChatTab.swift +++ b/Tool/Sources/ChatTab/ChatTab.swift @@ -26,6 +26,9 @@ public protocol ChatTabType { /// Build the tabItem for this chat tab. @ViewBuilder func buildTabItem() -> any View + /// Build the icon for this chat tab. + @ViewBuilder + func buildIcon() -> any View /// Build the menu for this chat tab. @ViewBuilder func buildMenu() -> any View @@ -88,7 +91,7 @@ open class BaseChatTab { /// The tab item for this chat tab. @ViewBuilder public var tabItem: some View { - let id = "ChatTabMenu\(id)" + let id = "ChatTabTab\(id)" if let tab = self as? (any ChatTabType) { ContentView(buildView: tab.buildTabItem).id(id) .onAppear { @@ -99,6 +102,17 @@ open class BaseChatTab { } } + /// The icon for this chat tab. + @ViewBuilder + public var icon: some View { + let id = "ChatTabIcon\(id)" + if let tab = self as? (any ChatTabType) { + ContentView(buildView: tab.buildIcon).id(id) + } else { + EmptyView().id(id) + } + } + /// The tab item for this chat tab. @ViewBuilder public var menu: some View { @@ -183,6 +197,10 @@ public class EmptyChatTab: ChatTab { Text("Empty-\(id)") } + public func buildIcon() -> any View { + Image(systemName: "square") + } + public func buildMenu() -> any View { Text("Empty-\(id)") } From 9c1a59200ac3ad0282e1eecec099c100d4ddd31d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 17 Oct 2023 14:31:59 +0800 Subject: [PATCH 08/31] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index c0e446d3..ef0377cf 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit c0e446d33ce2451fde612564d3b1e2c57d529e9c +Subproject commit ef0377cf28719970c6fef11ad7094ef980972052 From f531278d3d3aea3dd5b303fbf42ca2450719e274 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 17 Oct 2023 22:52:03 +0800 Subject: [PATCH 09/31] Support ChatGPTService in debug window --- Pro | 2 +- .../OpenAIService/ChatGPTService.swift | 121 +++++++++++------- .../OpenAIService/CompletionStreamAPI.swift | 4 +- Tool/Sources/OpenAIService/Debug/Debug.swift | 91 +++++++++++++ 4 files changed, 171 insertions(+), 47 deletions(-) create mode 100644 Tool/Sources/OpenAIService/Debug/Debug.swift diff --git a/Pro b/Pro index ef0377cf..5f31b6c5 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit ef0377cf28719970c6fef11ad7094ef980972052 +Subproject commit 5f31b6c5baa18c63e3c87fa7ab08dc9d47221982 diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index 0dc01d6b..cacb7a02 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -98,41 +98,59 @@ public class ChatGPTService: ChatGPTServiceType { await memory.appendMessage(newMessage) } - return AsyncThrowingStream { continuation in - Task(priority: .userInitiated) { - do { - var functionCall: ChatMessage.FunctionCall? - var functionCallMessageID = "" - var isInitialCall = true - loop: while functionCall != nil || isInitialCall { - isInitialCall = false - if let call = functionCall { - if !configuration.runFunctionsAutomatically { - break loop + return Debugger.$id.withValue(.init()) { + AsyncThrowingStream { continuation in + Task(priority: .userInitiated) { + do { + var functionCall: ChatMessage.FunctionCall? + var functionCallMessageID = "" + var isInitialCall = true + loop: while functionCall != nil || isInitialCall { + isInitialCall = false + if let call = functionCall { + if !configuration.runFunctionsAutomatically { + break loop + } + functionCall = nil + await runFunctionCall(call, messageId: functionCallMessageID) } - functionCall = nil - await runFunctionCall(call, messageId: functionCallMessageID) - } - let stream = try await sendMemory() - for try await content in stream { - switch content { - case let .text(text): - continuation.yield(text) - case let .functionCall(call): - if functionCall == nil { - functionCallMessageID = uuidGenerator() - functionCall = call - } else { - functionCall?.name.append(call.name) - functionCall?.arguments.append(call.arguments) + let stream = try await sendMemory() + + #if DEBUG + var reply = "" + #endif + + for try await content in stream { + switch content { + case let .text(text): + continuation.yield(text) + #if DEBUG + reply.append(text) + #endif + case let .functionCall(call): + if functionCall == nil { + functionCallMessageID = uuidGenerator() + functionCall = call + } else { + functionCall?.name.append(call.name) + functionCall?.arguments.append(call.arguments) + } + await prepareFunctionCall( + call, + messageId: functionCallMessageID + ) } - await prepareFunctionCall(call, messageId: functionCallMessageID) } + #if DEBUG + Debugger.didReceiveResponse(content: reply) + #endif } + + Debugger.didFinish() + continuation.finish() + } catch { + continuation.finish(throwing: error) } - continuation.finish() - } catch { - continuation.finish(throwing: error) } } } @@ -152,22 +170,26 @@ public class ChatGPTService: ChatGPTServiceType { ) await memory.appendMessage(newMessage) } - - let message = try await sendMemoryAndWait() - var finalResult = message?.content - var functionCall = message?.functionCall - while let call = functionCall { - if !configuration.runFunctionsAutomatically { - break + return try await Debugger.$id.withValue(.init()) { + let message = try await sendMemoryAndWait() + var finalResult = message?.content + var functionCall = message?.functionCall + while let call = functionCall { + if !configuration.runFunctionsAutomatically { + break + } + functionCall = nil + await runFunctionCall(call) + guard let nextMessage = try await sendMemoryAndWait() else { break } + finalResult = nextMessage.content + functionCall = nextMessage.functionCall } - functionCall = nil - await runFunctionCall(call) - guard let nextMessage = try await sendMemoryAndWait() else { break } - finalResult = nextMessage.content - functionCall = nextMessage.functionCall - } - return finalResult + Debugger.didReceiveResponse(content: finalResult ?? "N/A") + Debugger.didFinish() + + return finalResult + } } public func stopReceivingMessage() { @@ -239,6 +261,8 @@ extension ChatGPTService { requestBody ) + Debugger.didSendRequestBody(body: requestBody) + return AsyncThrowingStream { continuation in Task { do { @@ -348,6 +372,8 @@ extension ChatGPTService { requestBody ) + Debugger.didSendRequestBody(body: requestBody) + let response = try await api() guard let choice = response.choices.first else { return nil } @@ -388,6 +414,8 @@ extension ChatGPTService { _ call: ChatMessage.FunctionCall, messageId: String? = nil ) async -> String { + Debugger.didReceiveFunction(name: call.name, arguments: call.arguments) + let messageId = messageId ?? uuidGenerator() guard let function = functionProvider.function(named: call.name) else { @@ -413,6 +441,8 @@ extension ChatGPTService { } } + Debugger.didReceiveFunctionResult(result: result.botReadableContent) + await memory.updateMessage(id: messageId) { message in message.content = result.botReadableContent } @@ -421,6 +451,9 @@ extension ChatGPTService { } catch { // For errors, use the error message as the result. let content = "Error: \(error.localizedDescription)" + + Debugger.didReceiveFunctionResult(result: content) + await memory.updateMessage(id: messageId) { message in message.content = content } diff --git a/Tool/Sources/OpenAIService/CompletionStreamAPI.swift b/Tool/Sources/OpenAIService/CompletionStreamAPI.swift index 2524257b..8e2c3098 100644 --- a/Tool/Sources/OpenAIService/CompletionStreamAPI.swift +++ b/Tool/Sources/OpenAIService/CompletionStreamAPI.swift @@ -13,7 +13,7 @@ protocol CompletionStreamAPI { ) } -public enum FunctionCallStrategy: Encodable, Equatable { +public enum FunctionCallStrategy: Codable, Equatable { /// Forbid the bot to call any function. case none /// Let the bot choose what function to call. @@ -39,7 +39,7 @@ public enum FunctionCallStrategy: Encodable, Equatable { } /// https://platform.openai.com/docs/api-reference/chat/create -struct CompletionRequestBody: Encodable, Equatable { +struct CompletionRequestBody: Codable, Equatable { struct Message: Codable, Equatable { /// The role of the message. var role: ChatMessage.Role diff --git a/Tool/Sources/OpenAIService/Debug/Debug.swift b/Tool/Sources/OpenAIService/Debug/Debug.swift new file mode 100644 index 00000000..aae68656 --- /dev/null +++ b/Tool/Sources/OpenAIService/Debug/Debug.swift @@ -0,0 +1,91 @@ +import AppKit +import Foundation + +enum Debugger { + #if DEBUG + @TaskLocal + static var id: UUID? + #endif + + static func didSendRequestBody(body: CompletionRequestBody) { + #if DEBUG + + do { + let json = try JSONEncoder().encode(body) + let center = NSWorkspace.shared.notificationCenter + center.post( + name: .init("ServiceDebugger.ChatRequestDebug.requestSent"), + object: nil, + userInfo: [ + "id": id ?? UUID(), + "data": json, + ] + ) + } catch { + print("Failed to encode request body: \(error)") + } + + #endif + } + + static func didReceiveFunction(name: String, arguments: String) { + #if DEBUG + + let center = NSWorkspace.shared.notificationCenter + center.post( + name: .init("ServiceDebugger.ChatRequestDebug.receivedFunctionCall"), + object: nil, + userInfo: [ + "id": id ?? UUID(), + "name": name, + "arguments": arguments, + ] + ) + + #endif + } + + static func didReceiveFunctionResult(result: String) { + #if DEBUG + + let center = NSWorkspace.shared.notificationCenter + center.post( + name: .init("ServiceDebugger.ChatRequestDebug.receivedFunctionResult"), + object: nil, + userInfo: [ + "id": id ?? UUID(), + "result": result, + ] + ) + + #endif + } + + static func didReceiveResponse(content: String) { + #if DEBUG + + let center = NSWorkspace.shared.notificationCenter + center.post( + name: .init("ServiceDebugger.ChatRequestDebug.responseReceived"), + object: nil, + userInfo: [ + "id": id ?? UUID(), + "response": content, + ] + ) + + #endif + } + + static func didFinish() { + let center = NSWorkspace.shared.notificationCenter + center.post( + name: .init("ServiceDebugger.ChatRequestDebug.finished"), + object: nil, + userInfo: [ + "id": id ?? UUID(), + ] + ) + } +} + From 2ad4d33346db06a715acaa6ea397a7bda5a4bf1c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 18 Oct 2023 17:08:18 +0800 Subject: [PATCH 10/31] Adjust system prompt --- .../SystemInfoChatContextCollector.swift | 10 ++++++---- .../Sources/ChatService/DynamicContextController.swift | 2 +- Pro | 2 +- .../ActiveDocumentChatContextCollector.swift | 7 ++----- .../Memory/AutoManagedChatGPTMemory.swift | 6 +++++- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift b/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift index 795d14e0..d5563b51 100644 --- a/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift @@ -19,10 +19,12 @@ public final class SystemInfoChatContextCollector: ChatContextCollector { ) -> ChatContext { return .init( systemPrompt: """ - Current Time: \( - Self.dateFormatter.string(from: Date()) - ) (You can use it to calculate time in another time zone) - """, + ## System Info + + Current Time: \( + Self.dateFormatter.string(from: Date()) + ) (You can use it to calculate time in another time zone) + """, retrievedContent: [], functions: [] ) diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index 17c876b8..30af71a8 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -69,7 +69,7 @@ final class DynamicContextController { let extraSystemPrompt = contexts .map(\.systemPrompt) .filter { !$0.isEmpty } - .joined(separator: "\n") + .joined(separator: "\n\n") let contextPrompts = contexts .flatMap(\.retrievedContent) diff --git a/Pro b/Pro index 5f31b6c5..c55315ef 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 5f31b6c5baa18c63e3c87fa7ab08dc9d47221982 +Subproject commit c55315ef8143842aa1e78c48e6a5e8cf3e3d114b diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index bf297931..d1d90264 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -101,9 +101,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { : "When you don't known what I am asking, I am probably referring to the code." ) - Editing Document Context: ### + ### Editing Document Context """ - let end = "###" let relativePath = "Document Relative Path: \(context.relativePath)" let language = "Language: \(context.language.rawValue)" @@ -160,7 +159,6 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { code, codeAnnotations, fileAnnotations, - end, ] .filter { !$0.isEmpty } .joined(separator: "\n\n") @@ -180,10 +178,9 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { language, lineAnnotations, selectionRange, - end, ] .filter { !$0.isEmpty } - .joined(separator: "\n") + .joined(separator: "\n\n") } } diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index 8462c5ac..9a117d63 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -135,9 +135,13 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { for (index, content) in retrievedContent.filter({ !$0.isEmpty }).enumerated() { if index == 0 { if !appendToSystemPrompt(""" - + + + ## Relevant Content + Below are information related to the conversation, separated by \(separator) + """) { break } } else { if !appendToSystemPrompt("\n\(separator)\n") { break } From 6e1e387fe98a2983654c8c39a800e692bb3838c0 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 19 Oct 2023 22:26:04 +0800 Subject: [PATCH 11/31] Revert SwiftFocusCodeFinder behavior --- Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift index 52c4181b..53387616 100644 --- a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift @@ -61,10 +61,7 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinder { } } guard let focusedNode else { - var result = - UnknownLanguageFocusedCodeFinder( - proposedSearchRange: maxFocusedCodeLineCount / 2 - ) + var result = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 8) .findFocusedCode( containingRange: range, activeDocumentContext: activeDocumentContext From ec796af23dec2c6f7ee793b884fbc791a95d61a2 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 19 Oct 2023 22:31:06 +0800 Subject: [PATCH 12/31] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index c55315ef..61992373 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit c55315ef8143842aa1e78c48e6a5e8cf3e3d114b +Subproject commit 61992373b8ad0c988f223ecb225ad3d9939053ee From eec4881a7a4fc59b5e966632fce333ffbe190afa Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 20 Oct 2023 16:55:27 +0800 Subject: [PATCH 13/31] Update --- .../OpenAIService/ChatGPTService.swift | 4 ++++ Tool/Sources/OpenAIService/Debug/Debug.swift | 20 ++----------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index cacb7a02..65906183 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -146,7 +146,9 @@ public class ChatGPTService: ChatGPTServiceType { #endif } + #if DEBUG Debugger.didFinish() + #endif continuation.finish() } catch { continuation.finish(throwing: error) @@ -185,8 +187,10 @@ public class ChatGPTService: ChatGPTServiceType { functionCall = nextMessage.functionCall } + #if DEBUG Debugger.didReceiveResponse(content: finalResult ?? "N/A") Debugger.didFinish() + #endif return finalResult } diff --git a/Tool/Sources/OpenAIService/Debug/Debug.swift b/Tool/Sources/OpenAIService/Debug/Debug.swift index aae68656..d90883e9 100644 --- a/Tool/Sources/OpenAIService/Debug/Debug.swift +++ b/Tool/Sources/OpenAIService/Debug/Debug.swift @@ -2,14 +2,11 @@ import AppKit import Foundation enum Debugger { - #if DEBUG @TaskLocal static var id: UUID? - #endif + #if DEBUG static func didSendRequestBody(body: CompletionRequestBody) { - #if DEBUG - do { let json = try JSONEncoder().encode(body) let center = NSWorkspace.shared.notificationCenter @@ -24,13 +21,9 @@ enum Debugger { } catch { print("Failed to encode request body: \(error)") } - - #endif } static func didReceiveFunction(name: String, arguments: String) { - #if DEBUG - let center = NSWorkspace.shared.notificationCenter center.post( name: .init("ServiceDebugger.ChatRequestDebug.receivedFunctionCall"), @@ -41,13 +34,9 @@ enum Debugger { "arguments": arguments, ] ) - - #endif } static func didReceiveFunctionResult(result: String) { - #if DEBUG - let center = NSWorkspace.shared.notificationCenter center.post( name: .init("ServiceDebugger.ChatRequestDebug.receivedFunctionResult"), @@ -57,13 +46,9 @@ enum Debugger { "result": result, ] ) - - #endif } static func didReceiveResponse(content: String) { - #if DEBUG - let center = NSWorkspace.shared.notificationCenter center.post( name: .init("ServiceDebugger.ChatRequestDebug.responseReceived"), @@ -73,8 +58,6 @@ enum Debugger { "response": content, ] ) - - #endif } static func didFinish() { @@ -87,5 +70,6 @@ enum Debugger { ] ) } + #endif } From 80994102181fd1293a804e560af554abba9d9a91 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 20 Oct 2023 22:39:03 +0800 Subject: [PATCH 14/31] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 61992373..53eaa38d 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 61992373b8ad0c988f223ecb225ad3d9939053ee +Subproject commit 53eaa38d75aa8150f65050e234f63d60be0984e3 From 6f970defa5ef3eb2ab2dc4ae93868e562fe95a13 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 20 Oct 2023 23:08:05 +0800 Subject: [PATCH 15/31] Update getEditorContent to actively get the focused editor content when the param is nil --- .../SuggestionCommandHandler/PseudoCommandHandler.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift index 5161bcec..bdae5dea 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift @@ -337,7 +337,9 @@ extension PseudoCommandHandler { @WorkspaceActor func getEditorContent(sourceEditor: SourceEditor?) async -> EditorContent? { - guard let filespace = await getFilespace(), let sourceEditor else { return nil } + guard let filespace = await getFilespace(), + let sourceEditor = sourceEditor ?? XcodeInspector.shared.focusedEditor + else { return nil } let content = sourceEditor.content let uti = filespace.codeMetadata.uti ?? "" let tabSize = filespace.codeMetadata.tabSize ?? 4 From 2d864eae7af4aac34fd5fdf81f8ab6b44c8822d4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 20 Oct 2023 23:08:39 +0800 Subject: [PATCH 16/31] Update custom command template to support {{clipboard}} --- .../CustomCommandTemplateProcessor.swift | 5 +++++ README.md | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift index e2f565d7..c8cf6147 100644 --- a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift +++ b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift @@ -1,3 +1,4 @@ +import AppKit import Foundation import SuggestionModel import XcodeInspector @@ -22,6 +23,10 @@ struct CustomCommandTemplateProcessor { of: "{{active_editor_file_name}}", with: info.documentURL?.lastPathComponent ?? "" ) + .replacingOccurrences( + of: "{{clipboard}}", + with: NSPasteboard.general.string(forType: .string) ?? "" + ) return updatedText } diff --git a/README.md b/README.md index 86485205..9eb82081 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ Copilot for Xcode is an Xcode Source Editor Extension that provides GitHub Copil - [Granting Permissions to the App](#granting-permissions-to-the-app) - [Setting Up Key Bindings](#setting-up-key-bindings) - [Setting Up Suggestion Feature](#setting-up-suggestion-feature) - - [Setting Up GitHub Copilot](#setting-up-github-copilot) - - [Setting Up Codeium](#setting-up-codeium) + - [Setting Up GitHub Copilot](#setting-up-github-copilot) + - [Setting Up Codeium](#setting-up-codeium) - [Setting Up Chat Feature](#setting-up-chat-feature) - [Managing `CopilotForXcodeExtensionService.app`](#managing-copilotforxcodeextensionserviceapp) - [Update](#update) @@ -129,11 +129,11 @@ Another convenient method to access commands is by using the `⇧⌘/` shortcut 1. In the host app, navigate to "Service - GitHub Copilot" to access your GitHub Copilot account settings. 2. Click on "Install" to install the language server. 3. Optionally, set up the path to Node. The default value is simply `node`. Copilot for Xcode.app will attempt to locate Node from the following directories: `/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`. - - If your Node installation is located elsewhere, you can run `which node` from the terminal to obtain the correct path. - - If you are using a node version manager that provides a shim executable, you will need to find the path to the actual executable. Please refer to the FAQ for more information. - + + If your Node installation is located elsewhere, you can run `which node` from the terminal to obtain the correct path. + + If you are using a node version manager that provides a shim executable, you will need to find the path to the actual executable. Please refer to the FAQ for more information. + 4. Click on "Sign In", and you will be redirected to a verification website provided by GitHub. A user code will be copied to your clipboard. 5. After signing in, return to the app and click on "Confirm Sign-in" to complete the process. 6. Go to "Feature - Suggestion" and update the feature provider to "GitHub Copilot". @@ -235,7 +235,7 @@ You can detach the chat panel by simply dragging it away. Once detached, the cha | :------: | --------------------------------------------------------------------------------------------------- | | `⌘W` | Close the chat tab. | | `⌘M` | Minimize the chat, you can bring it back with any chat commands or by clicking the circular widget. | -| `⇧↩︎` | Add new line. | +| `⇧↩︎` | Add new line. | | `⇧⌘]` | Move to next tab | | `⇧⌘[` | Move to previous tab | @@ -297,7 +297,7 @@ This feature is recommended when you need to update a specific piece of code. So #### Commands - Prompt to Code: Open a prompt to code window, where you can use natural language to write or edit selected code. -- Accept Prompt to Code: Accept the result of prompt to code. +- Accept Prompt to Code: Accept the result of prompt to code. ### Custom Commands @@ -316,6 +316,7 @@ For Send Message, Single Round Dialog and Custom Chat commands, you can use the | `{{active_editor_language}}` | The programming language of the active editor. | | `{{active_editor_file_url}}` | The URL of the active file in the editor. | | `{{active_editor_file_name}}` | The name of the active file in the editor. | +| `{{clipboard}}` | The content in clipboard | ## Plus Features From 770137793aa41b5891b4cd0b427c252634cb4178 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 20 Oct 2023 23:18:04 +0800 Subject: [PATCH 17/31] Update custom prompt to code to support templates --- .../ChatService/CustomCommandTemplateProcessor.swift | 6 ++++-- .../WindowBaseCommandHandler.swift | 8 ++++++-- README.md | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift index c8cf6147..01c592f4 100644 --- a/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift +++ b/Core/Sources/ChatService/CustomCommandTemplateProcessor.swift @@ -3,8 +3,10 @@ import Foundation import SuggestionModel import XcodeInspector -struct CustomCommandTemplateProcessor { - func process(_ text: String) -> String { +public struct CustomCommandTemplateProcessor { + public init() {} + + public func process(_ text: String) -> String { let info = getEditorInformation() let editorContent = info.editorContent let updatedText = text diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index aa271874..d4668628 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -409,6 +409,10 @@ extension WindowBaseCommandHandler { }() as (String, CursorRange) let viewStore = Service.shared.guiController.viewStore + + let customCommandTemplateProcessor = CustomCommandTemplateProcessor() + let newExtraSystemPrompt = extraSystemPrompt.map(customCommandTemplateProcessor.process) + let newPrompt = prompt.map(customCommandTemplateProcessor.process) _ = await Task { @MainActor in // if there is already a prompt to code presenting, we should not present another one @@ -423,8 +427,8 @@ extension WindowBaseCommandHandler { allCode: editor.content, isContinuous: isContinuous, commandName: name, - defaultPrompt: prompt ?? "", - extraSystemPrompt: extraSystemPrompt, + defaultPrompt: newPrompt ?? "", + extraSystemPrompt: newExtraSystemPrompt, generateDescriptionRequirement: generateDescription )))) }.result diff --git a/README.md b/README.md index 9eb82081..006adc62 100644 --- a/README.md +++ b/README.md @@ -308,7 +308,7 @@ You can create custom commands that run Chat and Prompt to Code with personalize - Custom Chat: Open the chat window and immediately send a message, if provided. You can overwrite the entire system prompt through the system prompt field. - Single Round Dialog: Send a message to a temporary chat. Useful when you want to run a terminal command with `/run`. -For Send Message, Single Round Dialog and Custom Chat commands, you can use the following template arguments: +You can use the following template arguments in custom commands: | Argument | Description | | ----------------------------- | ---------------------------------------------- | @@ -316,7 +316,7 @@ For Send Message, Single Round Dialog and Custom Chat commands, you can use the | `{{active_editor_language}}` | The programming language of the active editor. | | `{{active_editor_file_url}}` | The URL of the active file in the editor. | | `{{active_editor_file_name}}` | The name of the active file in the editor. | -| `{{clipboard}}` | The content in clipboard | +| `{{clipboard}}` | The content in clipboard. | ## Plus Features From 8dbb5eb84baab74bbf0dfbdf27402c2dfb205822 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 01:54:19 +0800 Subject: [PATCH 18/31] Add global hotkey to show/hide widget --- Core/Package.swift | 9 +++++++-- Core/Sources/HostApp/GeneralView.swift | 5 ++++- Core/Sources/HostApp/HostApp.swift | 22 +++++++++++++++------- Core/Sources/Service/Service.swift | 11 +++++++++++ Pro | 2 +- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Core/Package.swift b/Core/Package.swift index e3f59806..10d744d4 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -38,8 +38,8 @@ let isProIncluded: Bool = { return false } do { - if let content = String( - data: try Data(contentsOf: confURL), + if let content = try String( + data: Data(contentsOf: confURL), encoding: .utf8 ) { if content.hasPrefix("YES") { @@ -98,6 +98,9 @@ let package = Package( url: "https://github.com/pointfreeco/swift-composable-architecture", from: "0.55.0" ), + // quick hack to support custom UserDefaults + // https://github.com/sindresorhus/KeyboardShortcuts + .package(url: "https://github.com/intitni/KeyboardShortcuts", branch: "main"), ].pro, targets: [ // MARK: - Main @@ -134,6 +137,7 @@ let package = Package( .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "Dependencies", package: "swift-dependencies"), + .product(name: "KeyboardShortcuts", package: "KeyboardShortcuts"), ].pro([ "ProService", ]) @@ -168,6 +172,7 @@ let package = Package( .product(name: "OpenAIService", package: "Tool"), .product(name: "Preferences", package: "Tool"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "KeyboardShortcuts", package: "KeyboardShortcuts"), ].pro([ "ProHostApp", ]) diff --git a/Core/Sources/HostApp/GeneralView.swift b/Core/Sources/HostApp/GeneralView.swift index 7ce32d54..67df2e5b 100644 --- a/Core/Sources/HostApp/GeneralView.swift +++ b/Core/Sources/HostApp/GeneralView.swift @@ -1,5 +1,6 @@ import Client import ComposableArchitecture +import KeyboardShortcuts import LaunchAgentManager import Preferences import SwiftUI @@ -285,6 +286,8 @@ struct GeneralSettingsView: View { Text("pt") } + + KeyboardShortcuts.Recorder("Global Shortcut to Toggle Widgets", name: .showHideWidget) Toggle(isOn: $settings.hideCircularWidget) { Text("Hide circular widget") @@ -342,7 +345,7 @@ struct LargeIconPicker< } } } - + var body: some View { if #available(macOS 13.0, *) { LabeledContent { diff --git a/Core/Sources/HostApp/HostApp.swift b/Core/Sources/HostApp/HostApp.swift index 65f0ab1d..61915f8f 100644 --- a/Core/Sources/HostApp/HostApp.swift +++ b/Core/Sources/HostApp/HostApp.swift @@ -1,11 +1,16 @@ import Client import ComposableArchitecture import Foundation +import KeyboardShortcuts #if canImport(LicenseManagement) import LicenseManagement #endif +extension KeyboardShortcuts.Name { + static let showHideWidget = Self("ShowHideWidget") +} + struct HostApp: ReducerProtocol { struct State: Equatable { var general = General.State() @@ -27,11 +32,11 @@ struct HostApp: ReducerProtocol { Scope(state: \.general, action: /Action.general) { General() } - + Scope(state: \.chatModelManagement, action: /Action.chatModelManagement) { ChatModelManagement() } - + Scope(state: \.embeddingModelManagement, action: /Action.embeddingModelManagement) { EmbeddingModelManagement() } @@ -39,8 +44,9 @@ struct HostApp: ReducerProtocol { Reduce { _, action in switch action { case .appear: + KeyboardShortcuts.userDefaults = .shared return .none - + case .informExtensionServiceAboutLicenseKeyChange: #if canImport(LicenseManagement) return .run { _ in @@ -55,13 +61,13 @@ struct HostApp: ReducerProtocol { #else return .none #endif - + case .general: return .none - + case .chatModelManagement: return .none - + case .embeddingModelManagement: return .none } @@ -70,8 +76,8 @@ struct HostApp: ReducerProtocol { } import Dependencies -import Preferences import Keychain +import Preferences struct UserDefaultsDependencyKey: DependencyKey { static var liveValue: UserDefaultsType = UserDefaults.shared @@ -80,6 +86,7 @@ struct UserDefaultsDependencyKey: DependencyKey { it.removePersistentDomain(forName: "HostAppPreview") return it }() + static var testValue: UserDefaultsType = { let it = UserDefaults(suiteName: "HostAppTest")! it.removePersistentDomain(forName: "HostAppTest") @@ -106,3 +113,4 @@ extension DependencyValues { set { self[APIKeyKeychainDependencyKey.self] = newValue } } } + diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index 04a72f6c..d949d4c8 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -1,5 +1,6 @@ import Dependencies import Foundation +import KeyboardShortcuts import Workspace import WorkspaceSuggestionService @@ -12,6 +13,10 @@ import ProService public static let shared = TheActor() } +extension KeyboardShortcuts.Name { + static let showHideWidget = Self("ShowHideWidget") +} + /// The running extension service. public final class Service { public static let shared = Service() @@ -43,6 +48,8 @@ public final class Service { ProService() } #endif + + KeyboardShortcuts.userDefaults = .shared } @MainActor @@ -54,6 +61,10 @@ public final class Service { proService.start() #endif DependencyUpdater().update() + + KeyboardShortcuts.onKeyUp(for: .showHideWidget) { [guiController] in + guiController.viewStore.send(.suggestionWidget(.circularWidget(.widgetClicked))) + } } } diff --git a/Pro b/Pro index 53eaa38d..0a2dc978 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 53eaa38d75aa8150f65050e234f63d60be0984e3 +Subproject commit 0a2dc978acc519fbc7ca427ad50975be7124f5d1 From e77fd16ed2cf2db3bf7dbaab35bc1b41200e9a23 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 02:45:46 +0800 Subject: [PATCH 19/31] Allow making hotkey non-global --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 1 + Core/Sources/HostApp/GeneralView.swift | 8 ++- Core/Sources/Service/Service.swift | 49 +++++++++++++++++-- Tool/Sources/Preferences/Keys.swift | 5 ++ .../XcodeInspector/XcodeInspector.swift | 8 +-- 5 files changed, 64 insertions(+), 7 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index c7a9d482..2b28793c 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -133,6 +133,7 @@ struct ChatPanelMessages: View { .foregroundStyle(.secondary) .padding(4) } + .keyboardShortcut(.downArrow, modifiers: [.command]) .opacity(pinnedToBottom ? 0 : 1) .buttonStyle(.plain) .onChange(of: viewStore.state) { _ in diff --git a/Core/Sources/HostApp/GeneralView.swift b/Core/Sources/HostApp/GeneralView.swift index 67df2e5b..54d5271a 100644 --- a/Core/Sources/HostApp/GeneralView.swift +++ b/Core/Sources/HostApp/GeneralView.swift @@ -225,6 +225,8 @@ struct GeneralSettingsView: View { var preferWidgetToStayInsideEditorWhenWidthGreaterThan @AppStorage(\.hideCircularWidget) var hideCircularWidget + @AppStorage(\.showHideWidgetShortcutGlobally) + var showHideWidgetShortcutGlobally } @StateObject var settings = Settings() @@ -287,7 +289,11 @@ struct GeneralSettingsView: View { Text("pt") } - KeyboardShortcuts.Recorder("Global Shortcut to Toggle Widgets", name: .showHideWidget) + KeyboardShortcuts.Recorder("Hotkey to Toggle Widgets", name: .showHideWidget) + + Toggle(isOn: $settings.showHideWidgetShortcutGlobally) { + Text("Enable the Hotkey Globally") + } Toggle(isOn: $settings.hideCircularWidget) { Text("Hide circular widget") diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index d949d4c8..c5903ad4 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -1,8 +1,10 @@ +import Combine import Dependencies import Foundation import KeyboardShortcuts import Workspace import WorkspaceSuggestionService +import XcodeInspector #if canImport(ProService) import ProService @@ -27,6 +29,7 @@ public final class Service { public let guiController = GraphicalUserInterfaceController() public let realtimeSuggestionController = RealtimeSuggestionController() public let scheduledCleaner: ScheduledCleaner + let globalShortcutManager: GlobalShortcutManager #if canImport(ProService) let proService: ProService @@ -38,6 +41,7 @@ public final class Service { scheduledCleaner = .init(workspacePool: workspacePool, guiController: guiController) workspacePool.registerPlugin { SuggestionServiceWorkspacePlugin(workspace: $0) } self.workspacePool = workspacePool + globalShortcutManager = .init(guiController: guiController) #if canImport(ProService) proService = withDependencies { dependencyValues in @@ -48,8 +52,6 @@ public final class Service { ProService() } #endif - - KeyboardShortcuts.userDefaults = .shared } @MainActor @@ -61,10 +63,51 @@ public final class Service { proService.start() #endif DependencyUpdater().update() - + globalShortcutManager.start() + } +} + +@MainActor +final class GlobalShortcutManager { + let guiController: GraphicalUserInterfaceController + private var cancellable = Set() + + nonisolated init(guiController: GraphicalUserInterfaceController) { + self.guiController = guiController + } + + func start() { + KeyboardShortcuts.userDefaults = .shared + setupShortcutIfNeeded() + KeyboardShortcuts.onKeyUp(for: .showHideWidget) { [guiController] in guiController.viewStore.send(.suggestionWidget(.circularWidget(.widgetClicked))) } + + XcodeInspector.shared.$activeApplication.sink { app in + if !UserDefaults.shared.value(for: \.showHideWidgetShortcutGlobally) { + let shouldBeEnabled = if let app, app.isXcode || app.isExtensionService { + true + } else { + false + } + if shouldBeEnabled { + self.setupShortcutIfNeeded() + } else { + self.removeShortcutIfNeeded() + } + } else { + self.setupShortcutIfNeeded() + } + }.store(in: &cancellable) + } + + func setupShortcutIfNeeded() { + KeyboardShortcuts.enable(.showHideWidget) + } + + func removeShortcutIfNeeded() { + KeyboardShortcuts.disable(.showHideWidget) } } diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 276f1e3d..dd7e3f24 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -88,6 +88,11 @@ public struct UserDefaultPreferenceKeys { defaultValue: false, key: "HideCircularWidget" ) + + public let showHideWidgetShortcutGlobally = PreferenceKey( + defaultValue: false, + key: "ShowHideWidgetShortcutGlobally" + ) } // MARK: - OpenAI Account Settings diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index 08d74805..c218f021 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -24,13 +24,13 @@ public final class XcodeInspector: ObservableObject { @Published public internal(set) var focusedEditor: SourceEditor? @Published public internal(set) var focusedElement: AXUIElement? @Published public internal(set) var completionPanel: AXUIElement? - + public var focusedEditorContent: EditorInformation? { guard let documentURL = XcodeInspector.shared.realtimeActiveDocumentURL, let workspaceURL = XcodeInspector.shared.realtimeActiveWorkspaceURL, let projectURL = XcodeInspector.shared.activeProjectRootURL else { return nil } - + let editorContent = XcodeInspector.shared.focusedEditor?.content let language = languageIdentifierFromFileURL(documentURL) let relativePath = documentURL.path.replacingOccurrences(of: projectURL.path, with: "") @@ -143,8 +143,8 @@ public final class XcodeInspector: ObservableObject { @MainActor func setActiveXcode(_ xcode: XcodeAppInstanceInspector) { + activeApplication = xcode xcode.refresh() - for task in activeXcodeObservations { task.cancel() } for cancellable in activeXcodeCancellable { cancellable.cancel() } activeXcodeObservations.removeAll() @@ -214,6 +214,8 @@ public class AppInstanceInspector: ObservableObject { public let appElement: AXUIElement public let runningApplication: NSRunningApplication public var isActive: Bool { runningApplication.isActive } + public var isXcode: Bool { runningApplication.isXcode } + public var isExtensionService: Bool { runningApplication.isCopilotForXcodeExtensionService } init(runningApplication: NSRunningApplication) { self.runningApplication = runningApplication From 9982ee3d607b0cfdfa123b73f3f2c92c9f65e109 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 02:47:31 +0800 Subject: [PATCH 20/31] Fix the timing of KeyboardShortcuts initialization --- Core/Sources/HostApp/HostApp.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Core/Sources/HostApp/HostApp.swift b/Core/Sources/HostApp/HostApp.swift index 61915f8f..e5379319 100644 --- a/Core/Sources/HostApp/HostApp.swift +++ b/Core/Sources/HostApp/HostApp.swift @@ -27,6 +27,10 @@ struct HostApp: ReducerProtocol { } @Dependency(\.toast) var toast + + init() { + KeyboardShortcuts.userDefaults = .shared + } var body: some ReducerProtocol { Scope(state: \.general, action: /Action.general) { @@ -44,7 +48,6 @@ struct HostApp: ReducerProtocol { Reduce { _, action in switch action { case .appear: - KeyboardShortcuts.userDefaults = .shared return .none case .informExtensionServiceAboutLicenseKeyChange: From 88fc06b838965f8aef52a7815b450fee192d7965 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 02:51:28 +0800 Subject: [PATCH 21/31] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 006adc62..ae75d6a0 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,12 @@ Essentially using `⌥⇧` as the "access" key combination for all bindings. Another convenient method to access commands is by using the `⇧⌘/` shortcut to search for a command in the menu bar. +#### Setting Up Global Hotkeys + +Currently, the is only one global hotkey you can set to show/hide the widgets under the General tab from the host app. + +When this hotkey is not set to enabled globally, it will only work when the service app or Xcode is active. + ### Setting Up Suggestion Feature #### Setting Up GitHub Copilot @@ -234,7 +240,6 @@ You can detach the chat panel by simply dragging it away. Once detached, the cha | Shortcut | Description | | :------: | --------------------------------------------------------------------------------------------------- | | `⌘W` | Close the chat tab. | -| `⌘M` | Minimize the chat, you can bring it back with any chat commands or by clicking the circular widget. | | `⇧↩︎` | Add new line. | | `⇧⌘]` | Move to next tab | | `⇧⌘[` | Move to previous tab | From 49837500bf07843f8f3cbb563a7e47c207058e1a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 02:53:20 +0800 Subject: [PATCH 22/31] Fix that command+M is not hiding chat panel --- Core/Sources/SuggestionWidget/ChatWindowView.swift | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/Core/Sources/SuggestionWidget/ChatWindowView.swift b/Core/Sources/SuggestionWidget/ChatWindowView.swift index a0d53ba9..955e73fa 100644 --- a/Core/Sources/SuggestionWidget/ChatWindowView.swift +++ b/Core/Sources/SuggestionWidget/ChatWindowView.swift @@ -77,6 +77,7 @@ struct ChatTitleBar: View { } } } + .keyboardShortcut("m", modifiers: [.command]) WithViewStore(store, observe: { $0.chatPanelInASeparateWindow }) { viewStore in Button(action: { diff --git a/README.md b/README.md index ae75d6a0..1d424dda 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,7 @@ You can detach the chat panel by simply dragging it away. Once detached, the cha | Shortcut | Description | | :------: | --------------------------------------------------------------------------------------------------- | | `⌘W` | Close the chat tab. | +| `⌘M` | Minimize the chat, you can bring it back with any chat commands or by clicking the circular widget. | | `⇧↩︎` | Add new line. | | `⇧⌘]` | Move to next tab | | `⇧⌘[` | Move to previous tab | From c2837ee7bbc9be3e2aff99b30ef9031992ab5660 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 12:16:21 +0800 Subject: [PATCH 23/31] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 0a2dc978..51ea02d3 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 0a2dc978acc519fbc7ca427ad50975be7124f5d1 +Subproject commit 51ea02d33a1ab6c42386af3221dde3e6d77b0a5d From 0b93f92b7147d97a23ed213f9a819413ced3ff89 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 15:54:51 +0800 Subject: [PATCH 24/31] Move MessageScopeParser to ChatContextCollector --- .../DynamicContextController.swift | 33 +++-------------- Pro | 2 +- .../ChatContextCollector.swift | 36 +++++++++++++++++++ 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index 30af71a8..4757fbec 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -1,7 +1,6 @@ import ChatContextCollector import Foundation import OpenAIService -import Parsing import Preferences import XcodeInspector @@ -65,17 +64,17 @@ final class DynamicContextController { } return contexts } - + let extraSystemPrompt = contexts .map(\.systemPrompt) .filter { !$0.isEmpty } .joined(separator: "\n\n") - + let contextPrompts = contexts .flatMap(\.retrievedContent) .filter { !$0.content.isEmpty } .sorted { $0.priority > $1.priority } - + let contextualSystemPrompt = """ \(language.isEmpty ? "" : "You must always reply in \(language)") \(systemPrompt)\(extraSystemPrompt.isEmpty ? "" : "\n\(extraSystemPrompt)") @@ -88,30 +87,8 @@ final class DynamicContextController { extension DynamicContextController { static func parseScopes(_ prompt: inout String) -> Set { - guard !prompt.isEmpty else { return [] } - do { - let parser = Parse { - "@" - Many { - Prefix { $0.isLetter } - } separator: { - "+" - } terminator: { - " " - } - Skip { - Many { - " " - } - } - Rest() - } - let (scopes, rest) = try parser.parse(prompt) - prompt = String(rest) - return Set(scopes.map(String.init)) - } catch { - return [] - } + let parser = MessageScopeParser() + return parser(&prompt) } } diff --git a/Pro b/Pro index 51ea02d3..7292e085 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 51ea02d33a1ab6c42386af3221dde3e6d77b0a5d +Subproject commit 7292e0854b94ea1e6de09c59335fcb8078971874 diff --git a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift index 05a06da6..5b2d0cc0 100644 --- a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift @@ -1,5 +1,6 @@ import Foundation import OpenAIService +import Parsing public struct ChatContext { public struct RetrievedContent { @@ -39,3 +40,38 @@ public protocol ChatContextCollector { ) async -> ChatContext } +public struct MessageScopeParser { + public init() {} + + public func callAsFunction(_ content: inout String) -> Set { + return parseScopes(&content) + } + + func parseScopes(_ prompt: inout String) -> Set { + guard !prompt.isEmpty else { return [] } + do { + let parser = Parse { + "@" + Many { + Prefix { $0.isLetter } + } separator: { + "+" + } terminator: { + " " + } + Skip { + Many { + " " + } + } + Rest() + } + let (scopes, rest) = try parser.parse(prompt) + prompt = String(rest) + return Set(scopes.map(String.init)) + } catch { + return [] + } + } +} + From 27c9cdc7777a702a439d504945e6148047dce57b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 16:40:46 +0800 Subject: [PATCH 25/31] Fix that the host app is observing the global shortcut --- Core/Sources/HostApp/GeneralView.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Core/Sources/HostApp/GeneralView.swift b/Core/Sources/HostApp/GeneralView.swift index 54d5271a..c011de4a 100644 --- a/Core/Sources/HostApp/GeneralView.swift +++ b/Core/Sources/HostApp/GeneralView.swift @@ -288,9 +288,12 @@ struct GeneralSettingsView: View { Text("pt") } - - KeyboardShortcuts.Recorder("Hotkey to Toggle Widgets", name: .showHideWidget) - + + KeyboardShortcuts.Recorder("Hotkey to Toggle Widgets", name: .showHideWidget) { _ in + // It's not used in this app! + KeyboardShortcuts.disable(.showHideWidget) + } + Toggle(isOn: $settings.showHideWidgetShortcutGlobally) { Text("Enable the Hotkey Globally") } From dd398b59286bc9962d54baabedb626add1da333b Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 16:40:57 +0800 Subject: [PATCH 26/31] Fix archiving --- Tool/Sources/OpenAIService/ChatGPTService.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index 65906183..a1ef374f 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -265,7 +265,9 @@ extension ChatGPTService { requestBody ) + #if DEBUG Debugger.didSendRequestBody(body: requestBody) + #endif return AsyncThrowingStream { continuation in Task { @@ -376,7 +378,9 @@ extension ChatGPTService { requestBody ) + #if DEBUG Debugger.didSendRequestBody(body: requestBody) + #endif let response = try await api() @@ -418,7 +422,9 @@ extension ChatGPTService { _ call: ChatMessage.FunctionCall, messageId: String? = nil ) async -> String { + #if DEBUG Debugger.didReceiveFunction(name: call.name, arguments: call.arguments) + #endif let messageId = messageId ?? uuidGenerator() @@ -445,7 +451,9 @@ extension ChatGPTService { } } + #if DEBUG Debugger.didReceiveFunctionResult(result: result.botReadableContent) + #endif await memory.updateMessage(id: messageId) { message in message.content = result.botReadableContent @@ -456,7 +464,9 @@ extension ChatGPTService { // For errors, use the error message as the result. let content = "Error: \(error.localizedDescription)" + #if DEBUG Debugger.didReceiveFunctionResult(result: content) + #endif await memory.updateMessage(id: messageId) { message in message.content = content From b72dccab533bfd0d0ee10f3017aec9702b8d8b23 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 16:41:05 +0800 Subject: [PATCH 27/31] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1d424dda..cf82fa66 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,7 @@ These features are included in another repo, and are not open sourced. The currently available Plus features include: +- Terminal tab in chat panel. - Unlimited chat/embedding models. - Tab to accept suggestions. - Persisted chat panel. From 2cbd4b6a7a3c3cfd04abc814602cec1694fd5240 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 16:44:59 +0800 Subject: [PATCH 28/31] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 7292e085..f6ea84b3 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 7292e0854b94ea1e6de09c59335fcb8078971874 +Subproject commit f6ea84b394f212dde54d669e9a4dc5cc72e989de From 48f92d54711c8198c21ddd356800c57dc446d37f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 16:57:37 +0800 Subject: [PATCH 29/31] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index f6ea84b3..5feae55e 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit f6ea84b394f212dde54d669e9a4dc5cc72e989de +Subproject commit 5feae55e1e9cb9d60166d9126419bf23c25a995d From d64c81727c0239cb7a831f37444db9e3ff6076f6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 16:57:51 +0800 Subject: [PATCH 30/31] Bump version to 0.26.0 --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index 35911ea7..b0ca9172 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.25.0 -APP_BUILD = 261 +APP_VERSION = 0.26.0 +APP_BUILD = 272 From f9d22410b3e62f36549e2e699a940b29d0f6d3d6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 19:03:50 +0800 Subject: [PATCH 31/31] Update appcast.xml --- appcast.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/appcast.xml b/appcast.xml index ae698149..07f4f9e8 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.26.0 + Sun, 22 Oct 2023 18:58:49 +0800 + 272 + 0.26.0 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.26.0 + + + + 0.25.0 Wed, 11 Oct 2023 23:08:08 +0800