From 41c95ffe9d7ccf6efe5692e11f808f559040dd39 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Sep 2023 16:20:25 +0800 Subject: [PATCH 01/37] Fix UI glitches created by Xcode 15 --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 10 +++++++++- .../APIKeyManagement/APIKeyManagementView.swift | 8 ++++++++ .../ChatModelManagement/ChatModelManagementView.swift | 2 +- .../SharedModelManagement/AIModelManagementVIew.swift | 10 +++++++++- .../CustomCommandSettings/CustomCommandView.swift | 8 ++++++++ .../SuggestionFeatureDisabledLanguageListView.swift | 8 ++++++++ .../SuggestionFeatureEnabledProjectListView.swift | 8 ++++++++ Pro | 2 +- Tool/Sources/SharedUIComponents/CustomScrollView.swift | 7 +++++++ Tool/Sources/SharedUIComponents/View+Modify.swift | 10 ++++++++++ 10 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 Tool/Sources/SharedUIComponents/View+Modify.swift diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index ad537aa4..a74169d7 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -1,6 +1,6 @@ import AppKit -import OpenAIService import MarkdownUI +import OpenAIService import SharedUIComponents import SwiftUI @@ -68,6 +68,14 @@ struct ChatPanelMessages: View { Instruction() Spacer() + + } + .modify { view in + if #available(macOS 13.0, *) { + view.listRowSeparator(.hidden).listSectionSeparator(.hidden) + } else { + view + } } .scaleEffect(x: -1, y: 1, anchor: .center) } diff --git a/Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeyManagementView.swift b/Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeyManagementView.swift index 101c4e49..48bd8632 100644 --- a/Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeyManagementView.swift +++ b/Core/Sources/HostApp/AccountSettings/APIKeyManagement/APIKeyManagementView.swift @@ -1,4 +1,5 @@ import ComposableArchitecture +import SharedUIComponents import SwiftUI struct APIKeyManagementView: View { @@ -49,6 +50,13 @@ struct APIKeyManagementView: View { .buttonStyle(.plain) } } + .modify { view in + if #available(macOS 13.0, *) { + view.listRowSeparator(.hidden).listSectionSeparator(.hidden) + } else { + view + } + } } } .removeBackground() diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelManagementView.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelManagementView.swift index cac7184f..6101de58 100644 --- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelManagementView.swift +++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelManagementView.swift @@ -12,7 +12,7 @@ struct ChatModelManagementView: View { action: ChatModelManagement.Action.chatModelItem )) { store in ChatModelEditView(store: store) - .frame(minWidth: 400) + .frame(width: 800) } } } diff --git a/Core/Sources/HostApp/AccountSettings/SharedModelManagement/AIModelManagementVIew.swift b/Core/Sources/HostApp/AccountSettings/SharedModelManagement/AIModelManagementVIew.swift index 37bcac29..621ed75d 100644 --- a/Core/Sources/HostApp/AccountSettings/SharedModelManagement/AIModelManagementVIew.swift +++ b/Core/Sources/HostApp/AccountSettings/SharedModelManagement/AIModelManagementVIew.swift @@ -1,6 +1,7 @@ import AIModel import ComposableArchitecture import PlusFeatureFlag +import SharedUIComponents import SwiftUI protocol AIModelManagementAction { @@ -54,7 +55,7 @@ struct AIModelManagementView= 2 - + Button(disabled ? "Add More Model (Plus)" : "Add Model") { store.send(.createModel) }.disabled(disabled) @@ -102,6 +103,13 @@ struct AIModelManagementView: View { .modifier(CustomScrollViewUpdateHeightModifier()) } .listStyle(.plain) + .modify { view in + if #available(macOS 13.0, *) { + view.listRowSeparator(.hidden).listSectionSeparator(.hidden) + } else { + view + } + } .frame(idealHeight: max(10, height)) .onPreferenceChange(CustomScrollViewHeightPreferenceKey.self) { newHeight in Task { @MainActor in diff --git a/Tool/Sources/SharedUIComponents/View+Modify.swift b/Tool/Sources/SharedUIComponents/View+Modify.swift new file mode 100644 index 00000000..59820772 --- /dev/null +++ b/Tool/Sources/SharedUIComponents/View+Modify.swift @@ -0,0 +1,10 @@ +import SwiftUI + +public extension View { + @ViewBuilder func modify(@ViewBuilder transform: (Self) -> Content) + -> some View + { + transform(self) + } +} + From 8076180e3691f3725ddfa1c393993b1cfb0bf7b5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Sep 2023 16:31:36 +0800 Subject: [PATCH 02/37] Fix concurrency warning --- .../RealtimeSuggestionController.swift | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift index e0069d84..aa187f7c 100644 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ b/Core/Sources/Service/RealtimeSuggestionController.swift @@ -26,7 +26,7 @@ public actor RealtimeSuggestionController { func start() { Task { [weak self] in if let app = ActiveApplicationMonitor.shared.activeXcode { - await self?.handleXcodeChanged(app) + await self?.handleXcodeChanged() } var previousApp = ActiveApplicationMonitor.shared.activeXcode for await app in ActiveApplicationMonitor.shared.createStream() { @@ -35,13 +35,14 @@ public actor RealtimeSuggestionController { defer { previousApp = app } if let app = ActiveApplicationMonitor.shared.activeXcode, app != previousApp { - await self.handleXcodeChanged(app) + await self.handleXcodeChanged() } } } } - private func handleXcodeChanged(_ app: NSRunningApplication) { + private func handleXcodeChanged() { + guard let app = ActiveApplicationMonitor.shared.activeXcode else { return } windowChangeObservationTask?.cancel() windowChangeObservationTask = nil observeXcodeWindowChangeIfNeeded(app) @@ -50,12 +51,13 @@ public actor RealtimeSuggestionController { private func observeXcodeWindowChangeIfNeeded(_ app: NSRunningApplication) { guard windowChangeObservationTask == nil else { return } handleFocusElementChange() + + let notifications = AXNotificationStream( + app: app, + notificationNames: kAXFocusedUIElementChangedNotification, + kAXMainWindowChangedNotification + ) windowChangeObservationTask = Task { [weak self] in - let notifications = AXNotificationStream( - app: app, - notificationNames: kAXFocusedUIElementChangedNotification, - kAXMainWindowChangedNotification - ) for await _ in notifications { guard let self else { return } try Task.checkCancellation() @@ -84,6 +86,12 @@ public actor RealtimeSuggestionController { editorObservationTask?.cancel() editorObservationTask = nil + let notificationsFromEditor = AXNotificationStream( + app: activeXcode, + element: focusElement, + notificationNames: kAXValueChangedNotification, kAXSelectedTextChangedNotification + ) + editorObservationTask = Task { [weak self] in let fileURL = try await Environment.fetchCurrentFileURL() if let sourceEditor = await self?.sourceEditor { @@ -93,12 +101,6 @@ public actor RealtimeSuggestionController { ) } - let notificationsFromEditor = AXNotificationStream( - app: activeXcode, - element: focusElement, - notificationNames: kAXValueChangedNotification, kAXSelectedTextChangedNotification - ) - for await notification in notificationsFromEditor { guard let self else { return } try Task.checkCancellation() From f342e00bfaf6cf37e5f7834a29eaa257f7a63748 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Sep 2023 17:11:32 +0800 Subject: [PATCH 03/37] Fix Swift 5.9 warnings --- .../RealtimeSuggestionController.swift | 10 +- Core/Sources/Service/ScheduledCleaner.swift | 4 +- .../FeatureReducers/WidgetFeature.swift | 119 +++++++++--------- Pro | 2 +- .../AXNotificationStream.swift | 14 ++- .../ActiveApplicationMonitor.swift | 58 ++++++--- 6 files changed, 120 insertions(+), 87 deletions(-) diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift index aa187f7c..99cc85e6 100644 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ b/Core/Sources/Service/RealtimeSuggestionController.swift @@ -25,16 +25,18 @@ public actor RealtimeSuggestionController { nonisolated func start() { Task { [weak self] in - if let app = ActiveApplicationMonitor.shared.activeXcode { + if ActiveApplicationMonitor.shared.activeXcode != nil { await self?.handleXcodeChanged() } - var previousApp = ActiveApplicationMonitor.shared.activeXcode - for await app in ActiveApplicationMonitor.shared.createStream() { + var previousApp = ActiveApplicationMonitor.shared.activeXcode?.info + for await app in ActiveApplicationMonitor.shared.createInfoStream() { guard let self else { return } try Task.checkCancellation() defer { previousApp = app } - if let app = ActiveApplicationMonitor.shared.activeXcode, app != previousApp { + if let app = ActiveApplicationMonitor.shared.activeXcode, + app.processIdentifier != previousApp?.processIdentifier + { await self.handleXcodeChanged() } } diff --git a/Core/Sources/Service/ScheduledCleaner.swift b/Core/Sources/Service/ScheduledCleaner.swift index 35a86543..66f1d438 100644 --- a/Core/Sources/Service/ScheduledCleaner.swift +++ b/Core/Sources/Service/ScheduledCleaner.swift @@ -19,7 +19,6 @@ public final class ScheduledCleaner { } func start() { - // occasionally cleanup workspaces. Task { @ServiceActor in while !Task.isCancelled { try await Task.sleep(nanoseconds: 10 * 60 * 1_000_000_000) @@ -27,9 +26,8 @@ public final class ScheduledCleaner { } } - // cleanup when Xcode becomes inactive Task { @ServiceActor in - for await app in ActiveApplicationMonitor.shared.createStream() { + for await app in ActiveApplicationMonitor.shared.createInfoStream() { try Task.checkCancellation() if let app, !app.isXcode { await cleanUp() diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index d89dfeb5..bee2092e 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -79,7 +79,7 @@ public struct WidgetFeature: ReducerProtocol { ) } } - + var lastUpdateWindowOpacityTime = Date(timeIntervalSince1970: 0) public init() {} @@ -213,10 +213,10 @@ public struct WidgetFeature: ReducerProtocol { case .observeActiveApplicationChange: return .run { send in - var previousApp: NSRunningApplication? - for await app in activeApplicationMonitor.createStream() { + var previousApp: RunningApplicationInfo? + for await app in activeApplicationMonitor.createInfoStream() { try Task.checkCancellation() - if app != previousApp { + if app?.processIdentifier != previousApp?.processIdentifier { await send(.updateActiveApplication) } previousApp = app @@ -311,26 +311,26 @@ public struct WidgetFeature: ReducerProtocol { case .observeWindowChange: guard let app = activeApplicationMonitor.activeApplication else { return .none } guard app.isXcode else { return .none } - + let documentURL = state.focusingDocumentURL - return .run { [app] send in - await send(.observeEditorChange) + let notifications = AXNotificationStream( + app: app, + notificationNames: + kAXApplicationActivatedNotification, + kAXMovedNotification, + kAXResizedNotification, + kAXMainWindowChangedNotification, + kAXFocusedWindowChangedNotification, + kAXFocusedUIElementChangedNotification, + kAXWindowMovedNotification, + kAXWindowResizedNotification, + kAXWindowMiniaturizedNotification, + kAXWindowDeminiaturizedNotification + ) - let notifications = AXNotificationStream( - app: app, - notificationNames: - kAXApplicationActivatedNotification, - kAXMovedNotification, - kAXResizedNotification, - kAXMainWindowChangedNotification, - kAXFocusedWindowChangedNotification, - kAXFocusedUIElementChangedNotification, - kAXWindowMovedNotification, - kAXWindowResizedNotification, - kAXWindowMiniaturizedNotification, - kAXWindowDeminiaturizedNotification - ) + return .run { send in + await send(.observeEditorChange) for await notification in notifications { try Task.checkCancellation() @@ -369,45 +369,46 @@ public struct WidgetFeature: ReducerProtocol { case .observeEditorChange: guard let app = activeApplicationMonitor.activeApplication else { return .none } - return .run { send in - let appElement = AXUIElementCreateApplication(app.processIdentifier) - if let focusedElement = appElement.focusedElement, - focusedElement.description == "Source Editor", - let scrollView = focusedElement.parent, - let scrollBar = scrollView.verticalScrollBar - { - let selectionRangeChange = AXNotificationStream( - app: app, - element: focusedElement, - notificationNames: kAXSelectedTextChangedNotification - ) - let scroll = AXNotificationStream( - app: app, - element: scrollBar, - notificationNames: kAXValueChangedNotification - ) + let appElement = AXUIElementCreateApplication(app.processIdentifier) + guard let focusedElement = appElement.focusedElement, + focusedElement.description == "Source Editor", + let scrollView = focusedElement.parent, + let scrollBar = scrollView.verticalScrollBar + else { return .none } + + let selectionRangeChange = AXNotificationStream( + app: app, + element: focusedElement, + notificationNames: kAXSelectedTextChangedNotification + ) + let scroll = AXNotificationStream( + app: app, + element: scrollBar, + notificationNames: kAXValueChangedNotification + ) - if #available(macOS 13.0, *) { - for await _ in merge( - selectionRangeChange.debounce(for: Duration.milliseconds(500)), - scroll - ) { - guard activeApplicationMonitor.latestXcode != nil - else { return } - try Task.checkCancellation() - await send(.updateWindowLocation(animated: false)) - await send(.updateWindowOpacity) - } - } else { - for await _ in merge(selectionRangeChange, scroll) { - guard activeApplicationMonitor.latestXcode != nil - else { return } - try Task.checkCancellation() - await send(.updateWindowLocation(animated: false)) - await send(.updateWindowOpacity) - } + return .run { send in + if #available(macOS 13.0, *) { + for await _ in merge( + selectionRangeChange.debounce(for: Duration.milliseconds(500)), + scroll + ) { + guard activeApplicationMonitor.latestXcode != nil + else { return } + try Task.checkCancellation() + await send(.updateWindowLocation(animated: false)) + await send(.updateWindowOpacity) + } + } else { + for await _ in merge(selectionRangeChange, scroll) { + guard activeApplicationMonitor.latestXcode != nil + else { return } + try Task.checkCancellation() + await send(.updateWindowLocation(animated: false)) + await send(.updateWindowOpacity) } } + }.cancellable(id: CancelID.observeEditorChange, cancelInFlight: true) case .updateActiveApplication: @@ -447,7 +448,7 @@ public struct WidgetFeature: ReducerProtocol { state.panelState.suggestionPanelState.colorScheme = scheme state.chatPanelState.colorScheme = scheme return .none - + case .updateFocusingDocumentURL: state.focusingDocumentURL = xcodeInspector.realtimeActiveDocumentURL return .none @@ -574,7 +575,7 @@ public struct WidgetFeature: ReducerProtocol { await send(.updateWindowOpacityFinished) } .cancellable(id: DebounceKey.updateWindowOpacity, cancelInFlight: true) - + case .updateWindowOpacityFinished: state.lastUpdateWindowOpacityTime = Date() return .none diff --git a/Pro b/Pro index 326ddb76..aeeba0a1 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 326ddb767de17c6822af2f1a275796dff011f772 +Subproject commit aeeba0a1839eb90800809c8511e8402f9fd41541 diff --git a/Tool/Sources/AXNotificationStream/AXNotificationStream.swift b/Tool/Sources/AXNotificationStream/AXNotificationStream.swift index ab338be8..77df9b66 100644 --- a/Tool/Sources/AXNotificationStream/AXNotificationStream.swift +++ b/Tool/Sources/AXNotificationStream/AXNotificationStream.swift @@ -86,12 +86,14 @@ public final class AXNotificationStream: AsyncSequence { guard let self else { return } retry += 1 for name in notificationNames { - let e = AXObserverAddNotification( - observer, - observingElement, - name as CFString, - &self.continuation - ) + let e = withUnsafeMutablePointer(to: &self.continuation) { pointer in + AXObserverAddNotification( + observer, + observingElement, + name as CFString, + pointer + ) + } switch e { case .success: pendingRegistrationNames.remove(name) diff --git a/Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift b/Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift index bc7167b6..12c309ed 100644 --- a/Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift +++ b/Tool/Sources/ActiveApplicationMonitor/ActiveApplicationMonitor.swift @@ -1,5 +1,35 @@ import AppKit +public struct RunningApplicationInfo: Sendable { + public let isXcode: Bool + public let isActive: Bool + public let isHidden: Bool + public let localizedName: String? + public let bundleIdentifier: String? + public let bundleURL: URL? + public let executableURL: URL? + public let processIdentifier: pid_t + public let launchDate: Date? + public let executableArchitecture: Int + + init(_ application: NSRunningApplication) { + isXcode = application.isXcode + isActive = application.isActive + isHidden = application.isHidden + localizedName = application.localizedName + bundleIdentifier = application.bundleIdentifier + bundleURL = application.bundleURL + executableURL = application.executableURL + processIdentifier = application.processIdentifier + launchDate = application.launchDate + executableArchitecture = application.executableArchitecture + } +} + +public extension NSRunningApplication { + var info: RunningApplicationInfo { RunningApplicationInfo(self) } +} + public final class ActiveApplicationMonitor { public static let shared = ActiveApplicationMonitor() public private(set) var latestXcode: NSRunningApplication? = NSWorkspace.shared @@ -17,11 +47,11 @@ public final class ActiveApplicationMonitor { } } - private var continuations: [UUID: AsyncStream.Continuation] = [:] + private var infoContinuations: [UUID: AsyncStream.Continuation] = [:] private init() { activeApplication = NSWorkspace.shared.runningApplications.first(where: \.isActive) - + Task { let sequence = NSWorkspace.shared.notificationCenter .notifications(named: NSWorkspace.didActivateApplicationNotification) @@ -36,7 +66,7 @@ public final class ActiveApplicationMonitor { } deinit { - for continuation in continuations { + for continuation in infoContinuations { continuation.value.finish() } } @@ -48,33 +78,33 @@ public final class ActiveApplicationMonitor { return nil } - public func createStream() -> AsyncStream { + public func createInfoStream() -> AsyncStream { .init { continuation in let id = UUID() Task { @MainActor in continuation.onTermination = { _ in - self.removeContinuation(id: id) + self.removeInfoContinuation(id: id) } - addContinuation(continuation, id: id) - continuation.yield(activeApplication) + addInfoContinuation(continuation, id: id) + continuation.yield(activeApplication?.info) } } } - func addContinuation( - _ continuation: AsyncStream.Continuation, + func addInfoContinuation( + _ continuation: AsyncStream.Continuation, id: UUID ) { - continuations[id] = continuation + infoContinuations[id] = continuation } - func removeContinuation(id: UUID) { - continuations[id] = nil + func removeInfoContinuation(id: UUID) { + infoContinuations[id] = nil } private func notifyContinuations() { - for continuation in continuations { - continuation.value.yield(activeApplication) + for continuation in infoContinuations { + continuation.value.yield(activeApplication?.info) } } } From 7f9ad179b317f1e390281b6c00f1490687b82841 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 28 Sep 2023 23:00:04 +0800 Subject: [PATCH 04/37] Adjust package location --- Core/Package.resolved | 206 ++++++++++++-- Core/Package.swift | 54 +--- Core/Sources/Service/Service.swift | 1 + .../PseudoCommandHandler.swift | 1 + .../WindowBaseCommandHandler.swift | 1 + .../Workspace+Cleanup.swift | 1 + Tool/Package.resolved | 266 ++++++++++++++++++ Tool/Package.swift | 55 +++- .../CodeiumService/CodeiumAuthService.swift | 0 .../CodeiumInstallationManager.swift | 0 .../CodeiumLanguageServer.swift | 0 .../CodeiumService/CodeiumModels.swift | 0 .../CodeiumService/CodeiumRequest.swift | 0 .../CodeiumService/CodeiumService.swift | 0 .../CodeiumSupportedLanguage.swift | 0 .../CodeiumService/OpendDocumentPool.swift | 0 .../CopilotLocalProcessServer.swift | 0 .../CustomStdioTransport.swift | 0 .../GitHubCopilotAccountStatus.swift | 0 .../GitHubCopilotInstallationManager.swift | 0 .../GitHubCopilotRequest.swift | 0 .../GitHubCopilotService.swift | 0 .../CodeiumSuggestionProvider.swift | 0 .../GitHubCopilotSuggestionProvider.swift | 0 .../SuggestionService/SuggestionService.swift | 0 .../Filespace+SuggestionService.swift | 19 +- .../SuggestionWorkspacePlugin.swift | 26 +- .../Workspace+SuggestionService.swift | 8 +- .../FetchSuggestionsTests.swift | 0 ...leExtensionToLanguageIdentifierTests.swift | 0 30 files changed, 541 insertions(+), 97 deletions(-) create mode 100644 Tool/Package.resolved rename {Core => Tool}/Sources/CodeiumService/CodeiumAuthService.swift (100%) rename {Core => Tool}/Sources/CodeiumService/CodeiumInstallationManager.swift (100%) rename {Core => Tool}/Sources/CodeiumService/CodeiumLanguageServer.swift (100%) rename {Core => Tool}/Sources/CodeiumService/CodeiumModels.swift (100%) rename {Core => Tool}/Sources/CodeiumService/CodeiumRequest.swift (100%) rename {Core => Tool}/Sources/CodeiumService/CodeiumService.swift (100%) rename {Core => Tool}/Sources/CodeiumService/CodeiumSupportedLanguage.swift (100%) rename {Core => Tool}/Sources/CodeiumService/OpendDocumentPool.swift (100%) rename {Core => Tool}/Sources/GitHubCopilotService/CopilotLocalProcessServer.swift (100%) rename {Core => Tool}/Sources/GitHubCopilotService/CustomStdioTransport.swift (100%) rename {Core => Tool}/Sources/GitHubCopilotService/GitHubCopilotAccountStatus.swift (100%) rename {Core => Tool}/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift (100%) rename {Core => Tool}/Sources/GitHubCopilotService/GitHubCopilotRequest.swift (100%) rename {Core => Tool}/Sources/GitHubCopilotService/GitHubCopilotService.swift (100%) rename {Core => Tool}/Sources/SuggestionService/CodeiumSuggestionProvider.swift (100%) rename {Core => Tool}/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift (100%) rename {Core => Tool}/Sources/SuggestionService/SuggestionService.swift (100%) rename {Core/Sources/Service/WorkspaceExtension => Tool/Sources/WorkspaceSuggestionService}/Filespace+SuggestionService.swift (84%) rename {Core/Sources/Service/WorkspaceExtension => Tool/Sources/WorkspaceSuggestionService}/SuggestionWorkspacePlugin.swift (82%) rename {Core/Sources/Service/WorkspaceExtension => Tool/Sources/WorkspaceSuggestionService}/Workspace+SuggestionService.swift (97%) rename {Core => Tool}/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift (100%) rename {Core => Tool}/Tests/GitHubCopilotServiceTests/FileExtensionToLanguageIdentifierTests.swift (100%) diff --git a/Core/Package.resolved b/Core/Package.resolved index 95bd917e..020dcdc9 100644 --- a/Core/Package.resolved +++ b/Core/Package.resolved @@ -1,12 +1,30 @@ { "pins" : [ { - "identity" : "feedkit", + "identity" : "cgeventoverride", "kind" : "remoteSourceControl", - "location" : "https://github.com/nmdias/FeedKit", + "location" : "https://github.com/intitni/CGEventOverride", "state" : { - "revision" : "68493a33d862c33c9a9f67ec729b3b7df1b20ade", - "version" : "9.1.2" + "revision" : "ae83f8ef1de2ad4c1a1473a0453b9675281d0d2c", + "version" : "1.2.1" + } + }, + { + "identity" : "codablewrappers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GottaGetSwifty/CodableWrappers", + "state" : { + "revision" : "4eb46a4c656333e8514db8aad204445741de7d40", + "version" : "2.0.7" + } + }, + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "ec62f32d21584214a4b27c8cee2b2ad70ab2c38a", + "version" : "0.11.0" } }, { @@ -28,21 +46,21 @@ } }, { - "identity" : "gptencoder", + "identity" : "highlightr", "kind" : "remoteSourceControl", - "location" : "https://github.com/alfianlosari/GPTEncoder", + "location" : "https://github.com/intitni/Highlightr", "state" : { - "revision" : "a86968867ab4380e36b904a14c42215f71efe8b4", - "version" : "1.0.4" + "branch" : "bump-highlight-js-version", + "revision" : "4ffbb1b0b721378263297cafea6f2838044eb1eb" } }, { - "identity" : "highlightr", + "identity" : "indexstore-db", "kind" : "remoteSourceControl", - "location" : "https://github.com/raspu/Highlightr", + "location" : "https://github.com/apple/indexstore-db.git", "state" : { - "revision" : "93199b9e434f04bda956a613af8f571933f9f037", - "version" : "2.1.2" + "branch" : "release/5.9", + "revision" : "89ec16c2ac1bb271614e734a2ee792224809eb20" } }, { @@ -50,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ChimeHQ/JSONRPC", "state" : { - "revision" : "e0a30db87e70d31c821f99b9699c0bef61748aac", - "version" : "0.6.1" + "revision" : "5da978702aece6ba5c7879b0d253c180d61e4ef3", + "version" : "0.6.0" } }, { @@ -100,21 +118,30 @@ } }, { - "identity" : "splash", + "identity" : "swift-async-algorithms", "kind" : "remoteSourceControl", - "location" : "https://github.com/JohnSundell/Splash", + "location" : "https://github.com/apple/swift-async-algorithms", "state" : { - "branch" : "master", - "revision" : "2e3f17c2d09689c8bf175c4a84ff7f2ad3353301" + "revision" : "9cfed92b026c524674ed869a4ff2dcfdeedf8a2a", + "version" : "0.1.0" } }, { - "identity" : "swift-async-algorithms", + "identity" : "swift-case-paths", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms", + "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "9cfed92b026c524674ed869a4ff2dcfdeedf8a2a", - "version" : "0.1.0" + "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", + "version" : "0.14.1" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "0fbaebfc013715dab44d715a4d350ba37f297e4d", + "version" : "0.4.0" } }, { @@ -126,6 +153,51 @@ "version" : "1.0.4" } }, + { + "identity" : "swift-composable-architecture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-composable-architecture", + "state" : { + "revision" : "9f4202ab5b8422aa90f0ed983bf7652c3af7abf0", + "version" : "0.59.0" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "479750bd98fac2e813fffcf2af0728b5b0085795", + "version" : "0.1.1" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "4a87bb75be70c983a9548597e8783236feb3401e", + "version" : "0.11.1" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "16fd42ae04c6e7f74a6a86395d04722c641cccee", + "version" : "0.6.0" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "d01446a78fb768adc9a78cbb6df07767c8ccfc29", + "version" : "0.8.0" + } + }, { "identity" : "swift-markdown-ui", "kind" : "remoteSourceControl", @@ -134,6 +206,96 @@ "revision" : "12b351a75201a8124c2f2e1f9fc6ef5cd812c0b9", "version" : "2.1.0" } + }, + { + "identity" : "swift-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-parsing", + "state" : { + "revision" : "27c941bbd22a4bbc53005a15a0440443fd892f70", + "version" : "0.12.1" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "branch" : "main", + "revision" : "e149b01cfd3e96240e102729697e2095c19157ef" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "8b6cf29eead8841a1fa7822481cb3af4ddaadba6", + "version" : "2.6.1" + } + }, + { + "identity" : "swifttreesitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/SwiftTreeSitter", + "state" : { + "revision" : "a9b1335d5151b62b11f07599bd07d07dc5965de3", + "version" : "0.7.2" + } + }, + { + "identity" : "swiftui-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swiftui-navigation", + "state" : { + "revision" : "2aa885e719087ee19df251c08a5980ad3e787f12", + "version" : "0.8.0" + } + }, + { + "identity" : "tiktoken", + "kind" : "remoteSourceControl", + "location" : "https://github.com/intitni/Tiktoken", + "state" : { + "branch" : "main", + "revision" : "6a2ac324c6daec167ca95268d5a487e6de6a1cea" + } + }, + { + "identity" : "tree-sitter-objc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lukepistrol/tree-sitter-objc", + "state" : { + "branch" : "feature/spm", + "revision" : "1b54ef0b5efddddf393b45e173788499cc572048" + } + }, + { + "identity" : "tree-sitter-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/alex-pinkus/tree-sitter-swift", + "state" : { + "branch" : "with-generated-files", + "revision" : "eda05af7ac41adb4eb19c346883c0fa32fe3bdd8" + } + }, + { + "identity" : "usearch", + "kind" : "remoteSourceControl", + "location" : "https://github.com/unum-cloud/usearch", + "state" : { + "revision" : "33c53288b44ccb55de77776820676132a6e4c42a", + "version" : "0.23.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865", + "version" : "0.9.0" + } } ], "version" : 2 diff --git a/Core/Package.swift b/Core/Package.swift index 0ed63b34..d76ba9f0 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -28,7 +28,6 @@ let package = Package( name: "HostApp", targets: [ "HostApp", - "GitHubCopilotService", "Client", "XPCShared", "LaunchAgentManager", @@ -38,9 +37,6 @@ let package = Package( ], dependencies: [ .package(path: "../Tool"), - // TODO: Update LanguageClient some day. - .package(url: "https://github.com/ChimeHQ/LanguageClient", exact: "0.3.1"), - .package(url: "https://github.com/ChimeHQ/LanguageServerProtocol", exact: "0.8.0"), .package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"), .package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.1.0"), .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"), @@ -58,7 +54,7 @@ let package = Package( name: "Client", dependencies: [ "XPCShared", - "GitHubCopilotService", + .product(name: "SuggestionService", package: "Tool"), .product(name: "SuggestionModel", package: "Tool"), .product(name: "Logger", package: "Tool"), .product(name: "Preferences", package: "Tool"), @@ -67,14 +63,13 @@ let package = Package( .target( name: "Service", dependencies: [ - "SuggestionService", - "GitHubCopilotService", "XPCShared", "SuggestionWidget", "ChatService", "PromptToCodeService", "ServiceUpdateMigration", "ChatGPTChatTab", + .product(name: "SuggestionService", package: "Tool"), .product(name: "Workspace", package: "Tool"), .product(name: "UserDefaultsObserver", package: "Tool"), .product(name: "AppMonitoring", package: "Tool"), @@ -96,9 +91,9 @@ let package = Package( dependencies: [ "Service", "Client", - "GitHubCopilotService", "SuggestionInjector", "XPCShared", + .product(name: "SuggestionService", package: "Tool"), .product(name: "SuggestionModel", package: "Tool"), .product(name: "Environment", package: "Tool"), .product(name: "Preferences", package: "Tool"), @@ -111,10 +106,9 @@ let package = Package( name: "HostApp", dependencies: [ "Client", - "GitHubCopilotService", - "CodeiumService", "LaunchAgentManager", "PlusFeatureFlag", + .product(name: "SuggestionService", package: "Tool"), .product(name: "Toast", package: "Tool"), .product(name: "SharedUIComponents", package: "Tool"), .product(name: "SuggestionModel", package: "Tool"), @@ -144,11 +138,6 @@ let package = Package( name: "SuggestionInjectorTests", dependencies: ["SuggestionInjector"] ), - .target(name: "SuggestionService", dependencies: [ - "GitHubCopilotService", - "CodeiumService", - .product(name: "UserDefaultsObserver", package: "Tool"), - ]), // MARK: - Prompt To Code @@ -248,7 +237,7 @@ let package = Package( .target( name: "ServiceUpdateMigration", dependencies: [ - "GitHubCopilotService", + .product(name: "SuggestionService", package: "Tool"), .product(name: "Preferences", package: "Tool"), .product(name: "Keychain", package: "Tool"), ] @@ -267,39 +256,6 @@ let package = Package( ]) ), - // MARK: - GitHub Copilot - - .target( - name: "GitHubCopilotService", - dependencies: [ - "LanguageClient", - "XPCShared", - .product(name: "SuggestionModel", package: "Tool"), - .product(name: "Logger", package: "Tool"), - .product(name: "Preferences", package: "Tool"), - .product(name: "Terminal", package: "Tool"), - .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol"), - ] - ), - .testTarget( - name: "GitHubCopilotServiceTests", - dependencies: ["GitHubCopilotService"] - ), - - // MARK: - Codeium - - .target( - name: "CodeiumService", - dependencies: [ - "LanguageClient", - .product(name: "Keychain", package: "Tool"), - .product(name: "SuggestionModel", package: "Tool"), - .product(name: "AppMonitoring", package: "Tool"), - .product(name: "Preferences", package: "Tool"), - .product(name: "Terminal", package: "Tool"), - ] - ), - // MARK: - Chat Plugins .target( diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index 9c586e6b..0d840fad 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -1,6 +1,7 @@ import Dependencies import Foundation import Workspace +import WorkspaceSuggestionService #if canImport(KeyBindingManager) import EnhancedWorkspace diff --git a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift index 99a2e269..5161bcec 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift @@ -5,6 +5,7 @@ import Preferences import SuggestionInjector import SuggestionModel import Workspace +import WorkspaceSuggestionService import XcodeInspector import XPCShared diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index 91c72710..aa271874 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -10,6 +10,7 @@ import SuggestionModel import SuggestionWidget import UserNotifications import Workspace +import WorkspaceSuggestionService import XPCShared struct WindowBaseCommandHandler: SuggestionCommandHandler { diff --git a/Core/Sources/Service/WorkspaceExtension/Workspace+Cleanup.swift b/Core/Sources/Service/WorkspaceExtension/Workspace+Cleanup.swift index 3a5475da..a2f439d1 100644 --- a/Core/Sources/Service/WorkspaceExtension/Workspace+Cleanup.swift +++ b/Core/Sources/Service/WorkspaceExtension/Workspace+Cleanup.swift @@ -1,6 +1,7 @@ import Foundation import Workspace import SuggestionService +import WorkspaceSuggestionService extension Workspace { @WorkspaceActor diff --git a/Tool/Package.resolved b/Tool/Package.resolved new file mode 100644 index 00000000..141e454d --- /dev/null +++ b/Tool/Package.resolved @@ -0,0 +1,266 @@ +{ + "pins" : [ + { + "identity" : "codablewrappers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GottaGetSwifty/CodableWrappers", + "state" : { + "revision" : "4eb46a4c656333e8514db8aad204445741de7d40", + "version" : "2.0.7" + } + }, + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "ec62f32d21584214a4b27c8cee2b2ad70ab2c38a", + "version" : "0.11.0" + } + }, + { + "identity" : "fseventswrapper", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Frizlab/FSEventsWrapper", + "state" : { + "revision" : "e0c59a2ce2775e5f6642da6d19207445f10112d0", + "version" : "1.0.2" + } + }, + { + "identity" : "glob", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Bouke/Glob", + "state" : { + "revision" : "deda6e163d2ff2a8d7e138e2c3326dbd71157faf", + "version" : "1.0.5" + } + }, + { + "identity" : "highlightr", + "kind" : "remoteSourceControl", + "location" : "https://github.com/intitni/Highlightr", + "state" : { + "branch" : "bump-highlight-js-version", + "revision" : "4ffbb1b0b721378263297cafea6f2838044eb1eb" + } + }, + { + "identity" : "jsonrpc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/JSONRPC", + "state" : { + "revision" : "5da978702aece6ba5c7879b0d253c180d61e4ef3", + "version" : "0.6.0" + } + }, + { + "identity" : "languageclient", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/LanguageClient", + "state" : { + "revision" : "f0198ee0a102d266078f7d9c28f086f2989f988a", + "version" : "0.3.1" + } + }, + { + "identity" : "languageserverprotocol", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/LanguageServerProtocol", + "state" : { + "revision" : "6e97f943dc024307c5524a80bd33cdbd1cc621de", + "version" : "0.8.0" + } + }, + { + "identity" : "operationplus", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/OperationPlus", + "state" : { + "revision" : "1340f95dce3e93d742497d88db18f8676f4badf4", + "version" : "1.6.0" + } + }, + { + "identity" : "processenv", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/ProcessEnv", + "state" : { + "revision" : "29487b6581bb785c372c611c943541ef4309d051", + "version" : "0.3.1" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms", + "state" : { + "revision" : "9cfed92b026c524674ed869a4ff2dcfdeedf8a2a", + "version" : "0.1.0" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", + "version" : "0.14.1" + } + }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "0fbaebfc013715dab44d715a4d350ba37f297e4d", + "version" : "0.4.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" + } + }, + { + "identity" : "swift-composable-architecture", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-composable-architecture", + "state" : { + "revision" : "9f4202ab5b8422aa90f0ed983bf7652c3af7abf0", + "version" : "0.59.0" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "479750bd98fac2e813fffcf2af0728b5b0085795", + "version" : "0.1.1" + } + }, + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "4a87bb75be70c983a9548597e8783236feb3401e", + "version" : "0.11.1" + } + }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "16fd42ae04c6e7f74a6a86395d04722c641cccee", + "version" : "0.6.0" + } + }, + { + "identity" : "swift-identified-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-identified-collections", + "state" : { + "revision" : "d01446a78fb768adc9a78cbb6df07767c8ccfc29", + "version" : "0.8.0" + } + }, + { + "identity" : "swift-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-parsing", + "state" : { + "revision" : "27c941bbd22a4bbc53005a15a0440443fd892f70", + "version" : "0.12.1" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "branch" : "main", + "revision" : "e149b01cfd3e96240e102729697e2095c19157ef" + } + }, + { + "identity" : "swiftsoup", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scinfu/SwiftSoup.git", + "state" : { + "revision" : "8b6cf29eead8841a1fa7822481cb3af4ddaadba6", + "version" : "2.6.1" + } + }, + { + "identity" : "swifttreesitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/SwiftTreeSitter", + "state" : { + "revision" : "a9b1335d5151b62b11f07599bd07d07dc5965de3", + "version" : "0.7.2" + } + }, + { + "identity" : "swiftui-navigation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swiftui-navigation", + "state" : { + "revision" : "2aa885e719087ee19df251c08a5980ad3e787f12", + "version" : "0.8.0" + } + }, + { + "identity" : "tiktoken", + "kind" : "remoteSourceControl", + "location" : "https://github.com/intitni/Tiktoken", + "state" : { + "branch" : "main", + "revision" : "6a2ac324c6daec167ca95268d5a487e6de6a1cea" + } + }, + { + "identity" : "tree-sitter-objc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lukepistrol/tree-sitter-objc", + "state" : { + "branch" : "feature/spm", + "revision" : "1b54ef0b5efddddf393b45e173788499cc572048" + } + }, + { + "identity" : "tree-sitter-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/alex-pinkus/tree-sitter-swift", + "state" : { + "branch" : "with-generated-files", + "revision" : "eda05af7ac41adb4eb19c346883c0fa32fe3bdd8" + } + }, + { + "identity" : "usearch", + "kind" : "remoteSourceControl", + "location" : "https://github.com/unum-cloud/usearch", + "state" : { + "revision" : "33c53288b44ccb55de77776820676132a6e4c42a", + "version" : "0.23.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "50843cbb8551db836adec2290bb4bc6bac5c1865", + "version" : "0.9.0" + } + } + ], + "version" : 2 +} diff --git a/Tool/Package.swift b/Tool/Package.swift index 5f5218a6..a58ae617 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -23,7 +23,11 @@ let package = Package( .library(name: "Keychain", targets: ["Keychain"]), .library(name: "SharedUIComponents", targets: ["SharedUIComponents"]), .library(name: "UserDefaultsObserver", targets: ["UserDefaultsObserver"]), - .library(name: "Workspace", targets: ["Workspace"]), + .library(name: "Workspace", targets: ["Workspace", "WorkspaceSuggestionService"]), + .library( + name: "SuggestionService", + targets: ["SuggestionService", "GitHubCopilotService", "CodeiumService"] + ), .library( name: "AppMonitoring", targets: [ @@ -37,7 +41,9 @@ let package = Package( dependencies: [ // A fork of https://github.com/aespinilla/Tiktoken to allow loading from local files. .package(url: "https://github.com/intitni/Tiktoken", branch: "main"), + // TODO: Update LanguageClient some day. .package(url: "https://github.com/ChimeHQ/LanguageClient", exact: "0.3.1"), + .package(url: "https://github.com/ChimeHQ/LanguageServerProtocol", exact: "0.8.0"), .package(url: "https://github.com/apple/swift-async-algorithms", from: "0.1.0"), .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"), .package(url: "https://github.com/ChimeHQ/JSONRPC", exact: "0.6.0"), @@ -189,7 +195,15 @@ let package = Package( "Environment", "Logger", "Preferences", - "XcodeInspector" + "XcodeInspector", + ] + ), + + .target( + name: "WorkspaceSuggestionService", + dependencies: [ + "Workspace", + "SuggestionService", ] ), @@ -226,6 +240,43 @@ let package = Package( .target(name: "BingSearchService"), + .target(name: "SuggestionService", dependencies: [ + "GitHubCopilotService", + "CodeiumService", + "UserDefaultsObserver", + ]), + + // MARK: - GitHub Copilot + + .target( + name: "GitHubCopilotService", + dependencies: [ + "LanguageClient", + "SuggestionModel", + "Logger", + "Preferences", + "Terminal", + .product(name: "LanguageServerProtocol", package: "LanguageServerProtocol"), + ] + ), + .testTarget( + name: "GitHubCopilotServiceTests", + dependencies: ["GitHubCopilotService"] + ), + + // MARK: - Codeium + + .target( + name: "CodeiumService", + dependencies: [ + "LanguageClient", + "Keychain", + "SuggestionModel", + "Preferences", + "Terminal", + ] + ), + // MARK: - OpenAI .target( diff --git a/Core/Sources/CodeiumService/CodeiumAuthService.swift b/Tool/Sources/CodeiumService/CodeiumAuthService.swift similarity index 100% rename from Core/Sources/CodeiumService/CodeiumAuthService.swift rename to Tool/Sources/CodeiumService/CodeiumAuthService.swift diff --git a/Core/Sources/CodeiumService/CodeiumInstallationManager.swift b/Tool/Sources/CodeiumService/CodeiumInstallationManager.swift similarity index 100% rename from Core/Sources/CodeiumService/CodeiumInstallationManager.swift rename to Tool/Sources/CodeiumService/CodeiumInstallationManager.swift diff --git a/Core/Sources/CodeiumService/CodeiumLanguageServer.swift b/Tool/Sources/CodeiumService/CodeiumLanguageServer.swift similarity index 100% rename from Core/Sources/CodeiumService/CodeiumLanguageServer.swift rename to Tool/Sources/CodeiumService/CodeiumLanguageServer.swift diff --git a/Core/Sources/CodeiumService/CodeiumModels.swift b/Tool/Sources/CodeiumService/CodeiumModels.swift similarity index 100% rename from Core/Sources/CodeiumService/CodeiumModels.swift rename to Tool/Sources/CodeiumService/CodeiumModels.swift diff --git a/Core/Sources/CodeiumService/CodeiumRequest.swift b/Tool/Sources/CodeiumService/CodeiumRequest.swift similarity index 100% rename from Core/Sources/CodeiumService/CodeiumRequest.swift rename to Tool/Sources/CodeiumService/CodeiumRequest.swift diff --git a/Core/Sources/CodeiumService/CodeiumService.swift b/Tool/Sources/CodeiumService/CodeiumService.swift similarity index 100% rename from Core/Sources/CodeiumService/CodeiumService.swift rename to Tool/Sources/CodeiumService/CodeiumService.swift diff --git a/Core/Sources/CodeiumService/CodeiumSupportedLanguage.swift b/Tool/Sources/CodeiumService/CodeiumSupportedLanguage.swift similarity index 100% rename from Core/Sources/CodeiumService/CodeiumSupportedLanguage.swift rename to Tool/Sources/CodeiumService/CodeiumSupportedLanguage.swift diff --git a/Core/Sources/CodeiumService/OpendDocumentPool.swift b/Tool/Sources/CodeiumService/OpendDocumentPool.swift similarity index 100% rename from Core/Sources/CodeiumService/OpendDocumentPool.swift rename to Tool/Sources/CodeiumService/OpendDocumentPool.swift diff --git a/Core/Sources/GitHubCopilotService/CopilotLocalProcessServer.swift b/Tool/Sources/GitHubCopilotService/CopilotLocalProcessServer.swift similarity index 100% rename from Core/Sources/GitHubCopilotService/CopilotLocalProcessServer.swift rename to Tool/Sources/GitHubCopilotService/CopilotLocalProcessServer.swift diff --git a/Core/Sources/GitHubCopilotService/CustomStdioTransport.swift b/Tool/Sources/GitHubCopilotService/CustomStdioTransport.swift similarity index 100% rename from Core/Sources/GitHubCopilotService/CustomStdioTransport.swift rename to Tool/Sources/GitHubCopilotService/CustomStdioTransport.swift diff --git a/Core/Sources/GitHubCopilotService/GitHubCopilotAccountStatus.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotAccountStatus.swift similarity index 100% rename from Core/Sources/GitHubCopilotService/GitHubCopilotAccountStatus.swift rename to Tool/Sources/GitHubCopilotService/GitHubCopilotAccountStatus.swift diff --git a/Core/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift similarity index 100% rename from Core/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift rename to Tool/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift diff --git a/Core/Sources/GitHubCopilotService/GitHubCopilotRequest.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift similarity index 100% rename from Core/Sources/GitHubCopilotService/GitHubCopilotRequest.swift rename to Tool/Sources/GitHubCopilotService/GitHubCopilotRequest.swift diff --git a/Core/Sources/GitHubCopilotService/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift similarity index 100% rename from Core/Sources/GitHubCopilotService/GitHubCopilotService.swift rename to Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift diff --git a/Core/Sources/SuggestionService/CodeiumSuggestionProvider.swift b/Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift similarity index 100% rename from Core/Sources/SuggestionService/CodeiumSuggestionProvider.swift rename to Tool/Sources/SuggestionService/CodeiumSuggestionProvider.swift diff --git a/Core/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift b/Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift similarity index 100% rename from Core/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift rename to Tool/Sources/SuggestionService/GitHubCopilotSuggestionProvider.swift diff --git a/Core/Sources/SuggestionService/SuggestionService.swift b/Tool/Sources/SuggestionService/SuggestionService.swift similarity index 100% rename from Core/Sources/SuggestionService/SuggestionService.swift rename to Tool/Sources/SuggestionService/SuggestionService.swift diff --git a/Core/Sources/Service/WorkspaceExtension/Filespace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift similarity index 84% rename from Core/Sources/Service/WorkspaceExtension/Filespace+SuggestionService.swift rename to Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift index 9d8437e5..ae773b96 100644 --- a/Core/Sources/Service/WorkspaceExtension/Filespace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Filespace+SuggestionService.swift @@ -2,17 +2,22 @@ import Foundation import SuggestionModel import Workspace -struct FilespaceSuggestionSnapshot: Equatable { - var linesHash: Int - var cursorPosition: CursorPosition +public struct FilespaceSuggestionSnapshot: Equatable { + public var linesHash: Int + public var cursorPosition: CursorPosition + + public init(linesHash: Int, cursorPosition: CursorPosition) { + self.linesHash = linesHash + self.cursorPosition = cursorPosition + } } -struct FilespaceSuggestionSnapshotKey: FilespacePropertyKey { - static func createDefaultValue() +public struct FilespaceSuggestionSnapshotKey: FilespacePropertyKey { + public static func createDefaultValue() -> FilespaceSuggestionSnapshot { .init(linesHash: -1, cursorPosition: .outOfScope) } } -extension FilespacePropertyValues { +public extension FilespacePropertyValues { @WorkspaceActor var suggestionSourceSnapshot: FilespaceSuggestionSnapshot { get { self[FilespaceSuggestionSnapshotKey.self] } @@ -20,7 +25,7 @@ extension FilespacePropertyValues { } } -extension Filespace { +public extension Filespace { @WorkspaceActor func resetSnapshot() { // swiftformat:disable redundantSelf diff --git a/Core/Sources/Service/WorkspaceExtension/SuggestionWorkspacePlugin.swift b/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift similarity index 82% rename from Core/Sources/Service/WorkspaceExtension/SuggestionWorkspacePlugin.swift rename to Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift index 9dd2d63f..7d17b684 100644 --- a/Core/Sources/Service/WorkspaceExtension/SuggestionWorkspacePlugin.swift +++ b/Tool/Sources/WorkspaceSuggestionService/SuggestionWorkspacePlugin.swift @@ -6,7 +6,7 @@ import SuggestionService import SuggestionModel import Preferences -final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { +public final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { let userDefaultsObserver = UserDefaultsObserver( object: UserDefaults.shared, forKeyPaths: [ UserDefaultPreferenceKeys().suggestionFeatureEnabledProjectList.key, @@ -14,13 +14,13 @@ final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { ], context: nil ) - var isRealtimeSuggestionEnabled: Bool { + public var isRealtimeSuggestionEnabled: Bool { UserDefaults.shared.value(for: \.realtimeSuggestionToggle) } private var _suggestionService: SuggestionServiceType? - var suggestionService: SuggestionServiceType? { + public var suggestionService: SuggestionServiceType? { // Check if the workspace is disabled. let isSuggestionDisabledGlobally = UserDefaults.shared .value(for: \.disableSuggestionFeatureGlobally) @@ -45,7 +45,7 @@ final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { return _suggestionService } - var isSuggestionFeatureEnabled: Bool { + public var isSuggestionFeatureEnabled: Bool { let isSuggestionDisabledGlobally = UserDefaults.shared .value(for: \.disableSuggestionFeatureGlobally) if isSuggestionDisabledGlobally { @@ -57,7 +57,7 @@ final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { return true } - override init(workspace: Workspace) { + public override init(workspace: Workspace) { super.init(workspace: workspace) userDefaultsObserver.onChange = { [weak self] in @@ -66,25 +66,25 @@ final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { } } - override func didOpenFilespace(_ filespace: Filespace) { + public override func didOpenFilespace(_ filespace: Filespace) { notifyOpenFile(filespace: filespace) } - override func didSaveFilespace(_ filespace: Filespace) { + public override func didSaveFilespace(_ filespace: Filespace) { notifySaveFile(filespace: filespace) } - override func didUpdateFilespace(_ filespace: Filespace, content: String) { + public override func didUpdateFilespace(_ filespace: Filespace, content: String) { notifyUpdateFile(filespace: filespace, content: content) } - override func didCloseFilespace(_ fileURL: URL) { + public override func didCloseFilespace(_ fileURL: URL) { Task { try await suggestionService?.notifyCloseTextDocument(fileURL: fileURL) } } - func notifyOpenFile(filespace: Filespace) { + public func notifyOpenFile(filespace: Filespace) { workspace?.refreshUpdateTime() workspace?.openedFileRecoverableStorage.openFile(fileURL: filespace.fileURL) Task { @@ -102,7 +102,7 @@ final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { } } - func notifyUpdateFile(filespace: Filespace, content: String) { + public func notifyUpdateFile(filespace: Filespace, content: String) { filespace.refreshUpdateTime() workspace?.refreshUpdateTime() Task { @@ -113,7 +113,7 @@ final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { } } - func notifySaveFile(filespace: Filespace) { + public func notifySaveFile(filespace: Filespace) { filespace.refreshUpdateTime() workspace?.refreshUpdateTime() Task { @@ -121,7 +121,7 @@ final class SuggestionServiceWorkspacePlugin: WorkspacePlugin { } } - func terminateSuggestionService() async { + public func terminateSuggestionService() async { await _suggestionService?.terminate() } } diff --git a/Core/Sources/Service/WorkspaceExtension/Workspace+SuggestionService.swift b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift similarity index 97% rename from Core/Sources/Service/WorkspaceExtension/Workspace+SuggestionService.swift rename to Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift index de9df342..2aa15bf8 100644 --- a/Core/Sources/Service/WorkspaceExtension/Workspace+SuggestionService.swift +++ b/Tool/Sources/WorkspaceSuggestionService/Workspace+SuggestionService.swift @@ -4,7 +4,7 @@ import SuggestionService import Workspace import XPCShared -extension Workspace { +public extension Workspace { var suggestionPlugin: SuggestionServiceWorkspacePlugin? { plugin(for: SuggestionServiceWorkspacePlugin.self) } @@ -18,13 +18,13 @@ extension Workspace { } struct SuggestionFeatureDisabledError: Error, LocalizedError { - var errorDescription: String? { + public var errorDescription: String? { "Suggestion feature is disabled for this project." } } } -extension Workspace { +public extension Workspace { @WorkspaceActor @discardableResult func generateSuggestions( @@ -59,7 +59,7 @@ extension Workspace { usesTabsForIndentation: editor.usesTabsForIndentation, ignoreSpaceOnlySuggestions: true ) - + filespace.setSuggestions(completions) return completions diff --git a/Core/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift b/Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift similarity index 100% rename from Core/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift rename to Tool/Tests/GitHubCopilotServiceTests/FetchSuggestionsTests.swift diff --git a/Core/Tests/GitHubCopilotServiceTests/FileExtensionToLanguageIdentifierTests.swift b/Tool/Tests/GitHubCopilotServiceTests/FileExtensionToLanguageIdentifierTests.swift similarity index 100% rename from Core/Tests/GitHubCopilotServiceTests/FileExtensionToLanguageIdentifierTests.swift rename to Tool/Tests/GitHubCopilotServiceTests/FileExtensionToLanguageIdentifierTests.swift From bf4277ce4546ff671abb598c02cafc07d461f369 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 29 Sep 2023 14:04:05 +0800 Subject: [PATCH 05/37] Adjust package location --- Core/Package.swift | 16 +++------------- Pro | 2 +- Tool/Package.swift | 5 +++++ .../GitHubCopilotService.swift | 1 - {Core => Tool}/Sources/XPCShared/Models.swift | 0 .../Sources/XPCShared/XPCServiceProtocol.swift | 0 6 files changed, 9 insertions(+), 15 deletions(-) rename {Core => Tool}/Sources/XPCShared/Models.swift (100%) rename {Core => Tool}/Sources/XPCShared/XPCServiceProtocol.swift (100%) diff --git a/Core/Package.swift b/Core/Package.swift index d76ba9f0..553fe1bc 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -21,7 +21,6 @@ let package = Package( name: "Client", targets: [ "Client", - "XPCShared", ] ), .library( @@ -29,7 +28,6 @@ let package = Package( targets: [ "HostApp", "Client", - "XPCShared", "LaunchAgentManager", "UpdateChecker", ] @@ -53,7 +51,7 @@ let package = Package( .target( name: "Client", dependencies: [ - "XPCShared", + .product(name: "XPCShared", package: "Tool"), .product(name: "SuggestionService", package: "Tool"), .product(name: "SuggestionModel", package: "Tool"), .product(name: "Logger", package: "Tool"), @@ -63,12 +61,12 @@ let package = Package( .target( name: "Service", dependencies: [ - "XPCShared", "SuggestionWidget", "ChatService", "PromptToCodeService", "ServiceUpdateMigration", "ChatGPTChatTab", + .product(name: "XPCShared", package: "Tool"), .product(name: "SuggestionService", package: "Tool"), .product(name: "Workspace", package: "Tool"), .product(name: "UserDefaultsObserver", package: "Tool"), @@ -92,7 +90,7 @@ let package = Package( "Service", "Client", "SuggestionInjector", - "XPCShared", + .product(name: "XPCShared", package: "Tool"), .product(name: "SuggestionService", package: "Tool"), .product(name: "SuggestionModel", package: "Tool"), .product(name: "Environment", package: "Tool"), @@ -121,13 +119,6 @@ let package = Package( ]) ), - // MARK: - XPC Related - - .target( - name: "XPCShared", - dependencies: [.product(name: "SuggestionModel", package: "Tool")] - ), - // MARK: - Suggestion Service .target( @@ -380,4 +371,3 @@ let isProIncluded: Bool = { return isProIncluded() }() - diff --git a/Pro b/Pro index aeeba0a1..e9cb881d 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit aeeba0a1839eb90800809c8511e8402f9fd41541 +Subproject commit e9cb881d71b90f1978e1330c74dd56fc12526286 diff --git a/Tool/Package.swift b/Tool/Package.swift index a58ae617..cbbe1a9e 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -7,6 +7,7 @@ let package = Package( name: "Tool", platforms: [.macOS(.v12)], products: [ + .library(name: "XPCShared", targets: ["XPCShared"]), .library(name: "Terminal", targets: ["Terminal"]), .library(name: "LangChain", targets: ["LangChain"]), .library(name: "ExternalServices", targets: ["BingSearchService"]), @@ -68,6 +69,8 @@ let package = Package( targets: [ // MARK: - Helpers + .target(name: "XPCShared", dependencies: ["SuggestionModel"]), + .target(name: "Configs"), .target(name: "Preferences", dependencies: ["Configs", "AIModel"]), @@ -204,6 +207,7 @@ let package = Package( dependencies: [ "Workspace", "SuggestionService", + "XPCShared", ] ), @@ -274,6 +278,7 @@ let package = Package( "SuggestionModel", "Preferences", "Terminal", + "XcodeInspector", ] ), diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift index 3c4a96b2..d3ac0034 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotService.swift @@ -4,7 +4,6 @@ import LanguageServerProtocol import Logger import Preferences import SuggestionModel -import XPCShared public protocol GitHubCopilotAuthServiceType { func checkStatus() async throws -> GitHubCopilotAccountStatus diff --git a/Core/Sources/XPCShared/Models.swift b/Tool/Sources/XPCShared/Models.swift similarity index 100% rename from Core/Sources/XPCShared/Models.swift rename to Tool/Sources/XPCShared/Models.swift diff --git a/Core/Sources/XPCShared/XPCServiceProtocol.swift b/Tool/Sources/XPCShared/XPCServiceProtocol.swift similarity index 100% rename from Core/Sources/XPCShared/XPCServiceProtocol.swift rename to Tool/Sources/XPCShared/XPCServiceProtocol.swift From f07f5e6c6fd8d7fe23b4fa90d12f72dbdd0e8f68 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 4 Oct 2023 15:29:34 +0800 Subject: [PATCH 06/37] Update --- Core/Package.swift | 2 +- Pro | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Package.swift b/Core/Package.swift index 553fe1bc..38529328 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -338,7 +338,7 @@ extension [Package.Dependency] { var pro: [Package.Dependency] { if isProIncluded { // include the pro package - return self + [.package(path: "../Pro")] + return self + [.package(path: "../Pro/Pro")] } return self } diff --git a/Pro b/Pro index e9cb881d..8bb1ad6f 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit e9cb881d71b90f1978e1330c74dd56fc12526286 +Subproject commit 8bb1ad6f8fbbc64c0afb13a85d7c3cefa9e8e80c From abc9a87e75b07d468bb48d9717959e59b61ea8d9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 4 Oct 2023 21:08:23 +0800 Subject: [PATCH 07/37] Implement basic structure of suggestion cheatsheet --- Core/Package.swift | 100 +++++++++--------- .../RealtimeSuggestionController.swift | 15 ++- Core/Sources/Service/Service.swift | 39 +++---- Pro | 2 +- 4 files changed, 82 insertions(+), 74 deletions(-) diff --git a/Core/Package.swift b/Core/Package.swift index 38529328..b6ea0acb 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -2,6 +2,57 @@ // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription +import Foundation + +// MARK: - Pro + +extension [Target.Dependency] { + func pro(_ targetNames: [String]) -> [Target.Dependency] { + if isProIncluded { + // include the pro package + return self + targetNames.map { Target.Dependency.product(name: $0, package: "Pro") } + } + return self + } +} + +extension [Package.Dependency] { + var pro: [Package.Dependency] { + if isProIncluded { + // include the pro package + return self + [.package(path: "../Pro/Pro")] + } + return self + } +} + +let isProIncluded: Bool = { + func isProIncluded(file: StaticString = #file) -> Bool { + let filePath = "\(file)" + let fileURL = URL(fileURLWithPath: filePath) + let rootURL = fileURL + .deletingLastPathComponent() + .deletingLastPathComponent() + let confURL = rootURL.appendingPathComponent("PLUS") + if !FileManager.default.fileExists(atPath: confURL.path) { + return false + } + do { + let content = String( + data: try Data(contentsOf: confURL), + encoding: .utf8 + ) + print("") + return content?.hasPrefix("YES") ?? false + } catch { + return false + } + } + + return isProIncluded() +}() + +// MARK: - Package let package = Package( name: "Core", @@ -322,52 +373,3 @@ let package = Package( ] ) -// MARK: - Pro - -extension [Target.Dependency] { - func pro(_ targetNames: [String]) -> [Target.Dependency] { - if isProIncluded { - // include the pro package - return self + targetNames.map { Target.Dependency.product(name: $0, package: "Pro") } - } - return self - } -} - -extension [Package.Dependency] { - var pro: [Package.Dependency] { - if isProIncluded { - // include the pro package - return self + [.package(path: "../Pro/Pro")] - } - return self - } -} - -import Foundation - -let isProIncluded: Bool = { - func isProIncluded(file: StaticString = #file) -> Bool { - let filePath = "\(file)" - let fileURL = URL(fileURLWithPath: filePath) - let rootURL = fileURL - .deletingLastPathComponent() - .deletingLastPathComponent() - let confURL = rootURL.appendingPathComponent("PLUS") - if !FileManager.default.fileExists(atPath: confURL.path) { - return false - } - do { - let content = String( - data: try Data(contentsOf: confURL), - encoding: .utf8 - ) - print("") - return content?.hasPrefix("YES") ?? false - } catch { - return false - } - } - - return isProIncluded() -}() diff --git a/Core/Sources/Service/RealtimeSuggestionController.swift b/Core/Sources/Service/RealtimeSuggestionController.swift index 99cc85e6..2b529c2b 100644 --- a/Core/Sources/Service/RealtimeSuggestionController.swift +++ b/Core/Sources/Service/RealtimeSuggestionController.swift @@ -21,10 +21,23 @@ public actor RealtimeSuggestionController { private var sourceEditor: SourceEditor? init() {} + + deinit { + task?.cancel() + inflightPrefetchTask?.cancel() + windowChangeObservationTask?.cancel() + activeApplicationMonitorTask?.cancel() + editorObservationTask?.cancel() + } nonisolated func start() { - Task { [weak self] in + Task { await observeXcodeChange() } + } + + private func observeXcodeChange() { + task?.cancel() + task = Task { [weak self] in if ActiveApplicationMonitor.shared.activeXcode != nil { await self?.handleXcodeChanged() } diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index 0d840fad..04a72f6c 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -3,9 +3,8 @@ import Foundation import Workspace import WorkspaceSuggestionService -#if canImport(KeyBindingManager) -import EnhancedWorkspace -import KeyBindingManager +#if canImport(ProService) +import ProService #endif @globalActor public enum ServiceActor { @@ -23,33 +22,27 @@ public final class Service { public let guiController = GraphicalUserInterfaceController() public let realtimeSuggestionController = RealtimeSuggestionController() public let scheduledCleaner: ScheduledCleaner - #if canImport(KeyBindingManager) - let keyBindingManager: KeyBindingManager + + #if canImport(ProService) + let proService: ProService #endif private init() { @Dependency(\.workspacePool) var workspacePool scheduledCleaner = .init(workspacePool: workspacePool, guiController: guiController) - #if canImport(KeyBindingManager) - keyBindingManager = .init( - workspacePool: workspacePool, - acceptSuggestion: { - Task { - await PseudoCommandHandler().acceptSuggestion() - } - } - ) - #endif - workspacePool.registerPlugin { SuggestionServiceWorkspacePlugin(workspace: $0) } - #if canImport(EnhancedWorkspace) - if !UserDefaults.shared.value(for: \.disableEnhancedWorkspace) { - workspacePool.registerPlugin { EnhancedWorkspacePlugin(workspace: $0) } + self.workspacePool = workspacePool + + #if canImport(ProService) + proService = withDependencies { dependencyValues in + dependencyValues.proServiceAcceptSuggestion = { + Task { await PseudoCommandHandler().acceptSuggestion() } + } + } operation: { + ProService() } #endif - - self.workspacePool = workspacePool } @MainActor @@ -57,8 +50,8 @@ public final class Service { scheduledCleaner.start() realtimeSuggestionController.start() guiController.start() - #if canImport(KeyBindingManager) - keyBindingManager.start() + #if canImport(ProService) + proService.start() #endif DependencyUpdater().update() } diff --git a/Pro b/Pro index 8bb1ad6f..71f5088f 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 8bb1ad6f8fbbc64c0afb13a85d7c3cefa9e8e80c +Subproject commit 71f5088ffbdff30b9cd2027fb3fa359f4ae2a25e From e164e8e8a0239923e1037efc575c4cb9d4f3ad5f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 5 Oct 2023 00:10:49 +0800 Subject: [PATCH 08/37] Migrate ChatGPTChatTab to use TCA, remove list inversion --- Core/Package.swift | 3 +- Core/Sources/ChatGPTChatTab/Chat.swift | 311 +++++++++++++++ .../ChatGPTChatTab/ChatContextMenu.swift | 148 ++++---- .../ChatGPTChatTab/ChatGPTChatTab.swift | 111 +----- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 359 +++++++++++------- .../Sources/ChatGPTChatTab/ChatProvider.swift | 119 ------ Core/Sources/ChatService/ChatService.swift | 5 +- 7 files changed, 635 insertions(+), 421 deletions(-) create mode 100644 Core/Sources/ChatGPTChatTab/Chat.swift delete mode 100644 Core/Sources/ChatGPTChatTab/ChatProvider.swift diff --git a/Core/Package.swift b/Core/Package.swift index b6ea0acb..aa895eee 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -1,8 +1,8 @@ // swift-tools-version: 5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. -import PackageDescription import Foundation +import PackageDescription // MARK: - Pro @@ -242,6 +242,7 @@ let package = Package( .product(name: "Logger", package: "Tool"), .product(name: "ChatTab", package: "Tool"), .product(name: "MarkdownUI", package: "swift-markdown-ui"), + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), ] ), diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift new file mode 100644 index 00000000..72d00503 --- /dev/null +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -0,0 +1,311 @@ +import ChatService +import ComposableArchitecture +import Foundation +import OpenAIService +import Preferences + +public struct ChatMessage: Equatable { + public enum Role { + case user + case assistant + case function + case ignored + } + + public var id: String + public var role: Role + public var text: String + + public init(id: String, role: Role, text: String) { + self.id = id + self.role = role + self.text = text + } +} + +struct Chat: ReducerProtocol { + public typealias MessageID = String + + struct State: Equatable { + var title: String = "Chat" + @BindingState var typedMessage = "" + var history: [ChatMessage] = [] + @BindingState var isReceivingMessage = false + var chatMenu = ChatMenu.State() + } + + enum Action: Equatable, BindableAction { + case binding(BindingAction) + + case appear + case sendButtonTapped + case returnButtonTapped + case stopRespondingButtonTapped + case clearButtonTap + case deleteMessageButtonTapped(MessageID) + case resendMessageButtonTapped(MessageID) + case setAsExtraPromptButtonTapped(MessageID) + + case observeChatService + case observeHistoryChange + case observeIsReceivingMessageChange + case observeSystemPromptChange + case observeExtraSystemPromptChange + + case historyChanged + case isReceivingMessageChanged + case systemPromptChanged + case extraSystemPromptChanged + + case chatMenu(ChatMenu.Action) + } + + let service: ChatService + let id = UUID() + + enum CancelID: Hashable { + case observeHistoryChange(UUID) + case observeIsReceivingMessageChange(UUID) + case observeSystemPromptChange(UUID) + case observeExtraSystemPromptChange(UUID) + } + + var body: some ReducerProtocol { + BindingReducer() + + Scope(state: \.chatMenu, action: /Action.chatMenu) { + ChatMenu(service: service) + } + + Reduce { state, action in + switch action { + case .appear: + return .run { send in + await send(.observeChatService) + await send(.historyChanged) + await send(.isReceivingMessageChanged) + await send(.systemPromptChanged) + await send(.extraSystemPromptChanged) + } + + case .sendButtonTapped: + guard !state.typedMessage.isEmpty else { return .none } + let message = state.typedMessage + state.typedMessage = "" + return .run { _ in + try await service.send(content: message) + } + + case .returnButtonTapped: + state.typedMessage += "\n" + return .none + + case .stopRespondingButtonTapped: + return .run { _ in + await service.stopReceivingMessage() + } + + case .clearButtonTap: + return .run { _ in + await service.clearHistory() + } + + case let .deleteMessageButtonTapped(id): + return .run { _ in + await service.deleteMessage(id: id) + } + + case let .resendMessageButtonTapped(id): + return .run { _ in + try await service.resendMessage(id: id) + } + + case let .setAsExtraPromptButtonTapped(id): + return .run { _ in + await service.setMessageAsExtraPrompt(id: id) + } + + case .observeChatService: + return .run { send in + await send(.observeHistoryChange) + await send(.observeIsReceivingMessageChange) + await send(.observeSystemPromptChange) + await send(.observeExtraSystemPromptChange) + } + + case .observeHistoryChange: + return .run { send in + let stream = AsyncStream { continuation in + let cancellable = service.$chatHistory.sink { _ in + continuation.yield() + } + continuation.onTermination = { _ in + cancellable.cancel() + } + } + for await _ in stream { + await send(.historyChanged) + } + }.cancellable(id: CancelID.observeHistoryChange(id), cancelInFlight: true) + + case .observeIsReceivingMessageChange: + return .run { send in + let stream = AsyncStream { continuation in + let cancellable = service.$isReceivingMessage + .sink { _ in + continuation.yield() + } + continuation.onTermination = { _ in + cancellable.cancel() + } + } + for await _ in stream { + await send(.isReceivingMessageChanged) + } + }.cancellable( + id: CancelID.observeIsReceivingMessageChange(id), + cancelInFlight: true + ) + + case .observeSystemPromptChange: + return .run { send in + let stream = AsyncStream { continuation in + let cancellable = service.$systemPrompt.sink { _ in + continuation.yield() + } + continuation.onTermination = { _ in + cancellable.cancel() + } + } + for await _ in stream { + await send(.systemPromptChanged) + } + }.cancellable(id: CancelID.observeSystemPromptChange(id), cancelInFlight: true) + + case .observeExtraSystemPromptChange: + return .run { send in + let stream = AsyncStream { continuation in + let cancellable = service.$extraSystemPrompt + .sink { _ in + continuation.yield() + } + continuation.onTermination = { _ in + cancellable.cancel() + } + } + for await _ in stream { + await send(.extraSystemPromptChanged) + } + }.cancellable(id: CancelID.observeExtraSystemPromptChange(id), cancelInFlight: true) + + case .historyChanged: + state.history = service.chatHistory.map { message in + .init( + id: message.id, + role: { + switch message.role { + case .system: return .ignored + case .user: return .user + case .assistant: + if let text = message.summary ?? message.content, + !text.isEmpty + { + return .assistant + } + return .ignored + case .function: return .function + } + }(), + text: message.summary ?? message.content ?? "" + ) + } + + state.title = { + let defaultTitle = "Chat" + guard let lastMessageText = state.history + .filter({ $0.role == .assistant || $0.role == .user }) + .last? + .text else { return defaultTitle } + if lastMessageText.isEmpty { return defaultTitle } + let trimmed = lastMessageText + .trimmingCharacters(in: .punctuationCharacters) + .trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.starts(with: "```") { + return "Code Block" + } else { + return trimmed + } + }() + return .none + + case .isReceivingMessageChanged: + state.isReceivingMessage = service.isReceivingMessage + return .none + + case .systemPromptChanged: + state.chatMenu.systemPrompt = service.systemPrompt + return .none + + case .extraSystemPromptChanged: + state.chatMenu.extraSystemPrompt = service.extraSystemPrompt + return .none + + case .binding: + return .none + + case .chatMenu: + return .none + } + } + } +} + +struct ChatMenu: ReducerProtocol { + struct State: Equatable { + var systemPrompt: String = "" + var extraSystemPrompt: String = "" + var temperatureOverride: Double? = nil + var chatModelIdOverride: String? = nil + } + + enum Action: Equatable { + case appear + case resetPromptButtonTapped + case temperatureOverrideSelected(Double?) + case chatModelIdOverrideSelected(String?) + case customCommandButtonTapped(CustomCommand) + } + + let service: ChatService + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .appear: + state.temperatureOverride = service.configuration.overriding.temperature + state.chatModelIdOverride = service.configuration.overriding.modelId + return .none + + case .resetPromptButtonTapped: + return .run { _ in + await service.resetPrompt() + } + case let .temperatureOverrideSelected(temperature): + state.temperatureOverride = temperature + return .run { _ in + service.configuration.overriding.temperature = temperature + } + case let .chatModelIdOverrideSelected(chatModelId): + state.chatModelIdOverride = chatModelId + return .run { _ in + service.configuration.overriding.modelId = chatModelId + } + case let .customCommandButtonTapped(command): + return .run { _ in + try await service.handleCustomCommand(command) + } + } + } + } +} + diff --git a/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift b/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift index ab9a2aa7..bff64f4e 100644 --- a/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift +++ b/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift @@ -1,17 +1,20 @@ import AppKit +import ComposableArchitecture import SharedUIComponents import SwiftUI struct ChatTabItemView: View { - @ObservedObject var chat: ChatProvider + let chat: StoreOf var body: some View { - Text(chat.title) + WithViewStore(chat, observe: \.title) { viewStore in + Text(viewStore.state) + } } } struct ChatContextMenu: View { - @ObservedObject var chat: ChatProvider + let store: StoreOf @AppStorage(\.customCommands) var customCommands @AppStorage(\.chatModels) var chatModels @AppStorage(\.defaultChatFeatureChatModelId) var defaultChatModelId @@ -19,6 +22,7 @@ struct ChatContextMenu: View { var body: some View { currentSystemPrompt + .onAppear { store.send(.appear) } currentExtraSystemPrompt resetPrompt @@ -35,77 +39,81 @@ struct ChatContextMenu: View { @ViewBuilder var currentSystemPrompt: some View { Text("System Prompt:") - Text({ - var text = chat.systemPrompt - if text.isEmpty { text = "N/A" } - if text.count > 30 { text = String(text.prefix(30)) + "..." } - return text - }() as String) + WithViewStore(store, observe: \.systemPrompt) { viewStore in + Text({ + var text = viewStore.state + if text.isEmpty { text = "N/A" } + if text.count > 30 { text = String(text.prefix(30)) + "..." } + return text + }() as String) + } } @ViewBuilder var currentExtraSystemPrompt: some View { Text("Extra Prompt:") - Text({ - var text = chat.extraSystemPrompt - if text.isEmpty { text = "N/A" } - if text.count > 30 { text = String(text.prefix(30)) + "..." } - return text - }() as String) + WithViewStore(store, observe: \.extraSystemPrompt) { viewStore in + Text({ + var text = viewStore.state + if text.isEmpty { text = "N/A" } + if text.count > 30 { text = String(text.prefix(30)) + "..." } + return text + }() as String) + } } var resetPrompt: some View { Button("Reset System Prompt") { - chat.resetPrompt() + store.send(.resetPromptButtonTapped) } } @ViewBuilder var chatModel: some View { Menu("Chat Model") { - Button(action: { - chat.chatModelId = nil - }) { - HStack { - if let defaultModel = chatModels.first(where: { $0.id == defaultChatModelId }) { - Text("Default (\(defaultModel.name))") - if chat.chatModelId == nil { - Image(systemName: "checkmark") - } - } else { - Text("No Model Available") - } - } - } - - if let id = chat.chatModelId, - !chatModels.map(\.id).contains(id) - { + WithViewStore(store, observe: \.chatModelIdOverride) { viewStore in Button(action: { - chat.chatModelId = nil - chat.objectWillChange.send() + viewStore.send(.chatModelIdOverrideSelected(nil)) }) { HStack { - Text("Default (Selected Model Not Found)") - Image(systemName: "checkmark") + if let defaultModel = chatModels + .first(where: { $0.id == defaultChatModelId }) + { + Text("Default (\(defaultModel.name))") + if viewStore.state == nil { + Image(systemName: "checkmark") + } + } else { + Text("No Model Available") + } } } - } - - Divider() - ForEach(chatModels, id: \.id) { model in - Button(action: { - chat.chatModelId = model.id - chat.objectWillChange.send() - }) { - HStack { - Text(model.name) - if model.id == chat.chatModelId { + if let id = viewStore.state, !chatModels.map(\.id).contains(id) { + Button(action: { + viewStore.send(.chatModelIdOverrideSelected(nil)) + }) { + HStack { + Text("Default (Selected Model Not Found)") Image(systemName: "checkmark") } } } + + Divider() + + ForEach(chatModels, id: \.id) { model in + Button(action: { + viewStore.send(.chatModelIdOverrideSelected(model.id)) + }) { + HStack { + Text(model.name) + if model.id == viewStore.state { + Image(systemName: "checkmark") + } + } + } + } } } } @@ -113,32 +121,34 @@ struct ChatContextMenu: View { @ViewBuilder var temperature: some View { Menu("Temperature") { - Button(action: { - chat.temperature = nil - }) { - HStack { - Text( - "Default (\(defaultTemperature.formatted(.number.precision(.fractionLength(1)))))" - ) - if chat.temperature == nil { - Image(systemName: "checkmark") - } - } - } - - Divider() - - ForEach(Array(stride(from: 0.0, through: 2.0, by: 0.1)), id: \.self) { value in + WithViewStore(store, observe: \.temperatureOverride) { viewStore in Button(action: { - chat.temperature = value + viewStore.send(.temperatureOverrideSelected(nil)) }) { HStack { - Text("\(value.formatted(.number.precision(.fractionLength(1))))") - if value == chat.temperature { + Text( + "Default (\(defaultTemperature.formatted(.number.precision(.fractionLength(1)))))" + ) + if viewStore.state == nil { Image(systemName: "checkmark") } } } + + Divider() + + ForEach(Array(stride(from: 0.0, through: 2.0, by: 0.1)), id: \.self) { value in + Button(action: { + viewStore.send(.temperatureOverrideSelected(value)) + }) { + HStack { + Text("\(value.formatted(.number.precision(.fractionLength(1))))") + if value == viewStore.state { + Image(systemName: "checkmark") + } + } + } + } } } } @@ -156,7 +166,7 @@ struct ChatContextMenu: View { id: \.name ) { command in Button(action: { - chat.triggerCustomCommand(command) + store.send(.customCommandButtonTapped(command)) }) { Text(command.name) } diff --git a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift index 17532b24..96c7e526 100644 --- a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift +++ b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift @@ -12,7 +12,8 @@ public class ChatGPTChatTab: ChatTab { public static var name: String { "Chat" } public let service: ChatService - public let provider: ChatProvider + let chat: StoreOf + let viewStore: ViewStoreOf private var cancellable = Set() struct RestorableState: Codable { @@ -28,7 +29,7 @@ public class ChatGPTChatTab: ChatTab { var afterBuild: (ChatGPTChatTab) async -> Void = { _ in } func build(store: StoreOf) async -> (any ChatTab)? { - let tab = ChatGPTChatTab(store: store) + let tab = await ChatGPTChatTab(store: store) if let customCommand { try? await tab.service.handleCustomCommand(customCommand) } @@ -38,15 +39,15 @@ public class ChatGPTChatTab: ChatTab { } public func buildView() -> any View { - ChatPanel(chat: provider) + ChatPanel(chat: chat) } public func buildTabItem() -> any View { - ChatTabItemView(chat: provider) + ChatTabItemView(chat: chat) } public func buildMenu() -> any View { - ChatContextMenu(chat: provider) + ChatContextMenu(store: chat.scope(state: \.chatMenu, action: Chat.Action.chatMenu)) } public func restorableState() async -> Data { @@ -87,9 +88,11 @@ public class ChatGPTChatTab: ChatTab { return [Builder(title: "New Chat", customCommand: nil)] + customCommands } + @MainActor public init(service: ChatService = .init(), store: StoreOf) { self.service = service - provider = .init(service: service) + chat = .init(initialState: .init(), reducer: Chat(service: service)) + viewStore = .init(chat) super.init(store: store) } @@ -108,15 +111,14 @@ public class ChatGPTChatTab: ChatTab { } }.store(in: &cancellable) - provider.$history.sink { [weak self] _ in + viewStore.publisher.map(\.title).removeDuplicates().sink { [weak self] title in Task { @MainActor [weak self] in - if let title = self?.provider.title { - self?.chatTabViewStore.send(.updateTitle(title)) - } + self?.chatTabViewStore.send(.updateTitle(title)) } }.store(in: &cancellable) - provider.objectWillChange.debounce(for: .seconds(1), scheduler: DispatchQueue.main) + viewStore.publisher.removeDuplicates() + .debounce(for: .seconds(1), scheduler: DispatchQueue.main) .sink { [weak self] _ in Task { @MainActor [weak self] in self?.chatTabViewStore.send(.tabContentUpdated) @@ -124,90 +126,3 @@ public class ChatGPTChatTab: ChatTab { }.store(in: &cancellable) } } - -extension ChatProvider { - convenience init(service: ChatService) { - self.init( - configuration: service.configuration, - pluginIdentifiers: service.allPluginCommands - ) - - let cancellable = service.objectWillChange.sink { [weak self] in - guard let self else { return } - Task { @MainActor in - self.history = (await service.memory.history).map { message in - .init( - id: message.id, - role: { - switch message.role { - case .system: return .ignored - case .user: return .user - case .assistant: - if let text = message.summary ?? message.content, !text.isEmpty { - return .assistant - } - return .ignored - case .function: return .function - } - }(), - text: message.summary ?? message.content ?? "" - ) - } - self.isReceivingMessage = service.isReceivingMessage - self.systemPrompt = service.systemPrompt - self.extraSystemPrompt = service.extraSystemPrompt - } - } - - service.objectWillChange.send() - - onMessageSend = { [cancellable] message in - _ = cancellable - Task { - try await service.send(content: message) - } - } - onStop = { - Task { - await service.stopReceivingMessage() - } - } - - onClear = { - Task { - await service.clearHistory() - } - } - - onDeleteMessage = { id in - Task { - await service.deleteMessage(id: id) - } - } - - onResendMessage = { id in - Task { - try await service.resendMessage(id: id) - } - } - - onResetPrompt = { - Task { - await service.resetPrompt() - } - } - - onRunCustomCommand = { command in - Task { - try await service.handleCustomCommand(command) - } - } - - onSetAsExtraPrompt = { id in - Task { - await service.setMessageAsExtraPrompt(id: id) - } - } - } -} - diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index a74169d7..4766a43a 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -1,4 +1,5 @@ import AppKit +import ComposableArchitecture import MarkdownUI import OpenAIService import SharedUIComponents @@ -7,90 +8,188 @@ import SwiftUI private let r: Double = 8 public struct ChatPanel: View { - @ObservedObject var chat: ChatProvider + let chat: StoreOf @Namespace var inputAreaNamespace - @State var typedMessage = "" - - public init(chat: ChatProvider, typedMessage: String = "") { - self.chat = chat - self.typedMessage = typedMessage - } public var body: some View { VStack(spacing: 0) { - ChatPanelMessages( - chat: chat - ) + ChatPanelMessages(chat: chat) Divider() - ChatPanelInputArea( - chat: chat, - typedMessage: $typedMessage - ) + ChatPanelInputArea(chat: chat) } .background(.regularMaterial) + .onAppear { chat.send(.appear) } + } +} + +private struct ScrollViewOffsetPreferenceKey: PreferenceKey { + static var defaultValue = CGFloat.zero + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value += nextValue() + } +} + +private struct ListHeightPreferenceKey: PreferenceKey { + static var defaultValue = CGFloat.zero + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value += nextValue() } } struct ChatPanelMessages: View { - @ObservedObject var chat: ChatProvider + let chat: StoreOf + @State var pinnedToBottom = true + @Namespace var bottomID + @Namespace var scrollSpace + @State var scrollOffset: Double = 0 + @State var listHeight: Double = 0 + @State var isInitialLoad = true var body: some View { - List { - Group { - Spacer() - - if chat.isReceivingMessage { - StopRespondingButton(chat: chat) - .padding(.vertical, 4) - .listRowInsets(EdgeInsets(top: 0, leading: -8, bottom: 0, trailing: -8)) + ScrollViewReader { proxy in + GeometryReader { listGeo in + List { + Group { + Spacer(minLength: 12) + + Instruction() + + ChatHistory(chat: chat) + .listItemTint(.clear) + + WithViewStore(chat, observe: \.isReceivingMessage) { viewStore in + if viewStore.state { + StopRespondingButton(chat: chat) + .padding(.vertical, 4) + .listRowInsets(EdgeInsets( + top: 0, + leading: -8, + bottom: 0, + trailing: -8 + )) + } + } + + Spacer(minLength: 12) + .background(GeometryReader { geo in + let offset = geo.frame(in: .named(scrollSpace)).minY + Color.clear + .preference( + key: ScrollViewOffsetPreferenceKey.self, + value: offset + ) + }) + .preference( + key: ListHeightPreferenceKey.self, + value: listGeo.size.height + ) + .id(bottomID) + } + .modify { view in + if #available(macOS 13.0, *) { + view.listRowSeparator(.hidden).listSectionSeparator(.hidden) + } else { + view + } + } } - - ForEach(chat.history.reversed(), id: \.id) { message in - let text = message.text - - switch message.role { - case .user: - UserMessage(id: message.id, text: text, chat: chat) - .listRowInsets(EdgeInsets(top: 0, leading: -8, bottom: 0, trailing: -8)) - .padding(.vertical, 4) - case .assistant: - BotMessage(id: message.id, text: text, chat: chat) - .listRowInsets(EdgeInsets(top: 0, leading: -8, bottom: 0, trailing: -8)) - .padding(.vertical, 4) - case .function: - FunctionMessage(id: message.id, text: text) - case .ignored: - EmptyView() + .listStyle(.plain) + .coordinateSpace(name: scrollSpace) + .onPreferenceChange(ListHeightPreferenceKey.self) { value in + listHeight = value + updatePinningState() + } + .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in + scrollOffset = value + updatePinningState() + } + .overlay(alignment: .bottomTrailing) { + WithViewStore(chat, observe: \.history.last) { viewStore in + Button(action: { + withAnimation(.easeInOut(duration: 0.1)) { + proxy.scrollTo(bottomID, anchor: .bottom) + } + }) { + Image(systemName: "arrow.down") + .padding(4) + .background { + Circle().fill(.thickMaterial) + } + .foregroundStyle(.secondary) + .padding(4) + } + .opacity(pinnedToBottom ? 0 : 1) + .buttonStyle(.plain) + .onChange(of: viewStore.state) { _ in + if pinnedToBottom || isInitialLoad { + if isInitialLoad { + isInitialLoad = false + } + withAnimation(.easeInOut(duration: 0.1)) { + proxy.scrollTo(bottomID, anchor: .bottom) + } + } + } } } - .listItemTint(.clear) + } + } + } + + func updatePinningState() { + if scrollOffset > listHeight + 24 + 100 || scrollOffset <= 0 { + pinnedToBottom = false + } else { + pinnedToBottom = true + } + } +} - Instruction() +struct ChatHistory: View { + let chat: StoreOf - Spacer() - - } - .modify { view in - if #available(macOS 13.0, *) { - view.listRowSeparator(.hidden).listSectionSeparator(.hidden) - } else { - view + var body: some View { + WithViewStore(chat, observe: \.history) { viewStore in + ForEach(viewStore.state, id: \.id) { message in + let text = message.text + + switch message.role { + case .user: + UserMessage(id: message.id, text: text, chat: chat) + .listRowInsets(EdgeInsets( + top: 0, + leading: -8, + bottom: 0, + trailing: -8 + )) + .padding(.vertical, 4) + case .assistant: + BotMessage(id: message.id, text: text, chat: chat) + .listRowInsets(EdgeInsets( + top: 0, + leading: -8, + bottom: 0, + trailing: -8 + )) + .padding(.vertical, 4) + case .function: + FunctionMessage(id: message.id, text: text) + case .ignored: + EmptyView() } } - .scaleEffect(x: -1, y: 1, anchor: .center) } - .id("\(chat.history.count), \(chat.isReceivingMessage)") - .listStyle(.plain) - .scaleEffect(x: 1, y: -1, anchor: .center) } } private struct StopRespondingButton: View { - let chat: ChatProvider + let chat: StoreOf var body: some View { Button(action: { - chat.stop() + chat.send(.stopRespondingButtonTapped) }) { HStack(spacing: 4) { Image(systemName: "stop.fill") @@ -107,7 +206,6 @@ private struct StopRespondingButton: View { } } .buttonStyle(.borderless) - .scaleEffect(x: -1, y: -1, anchor: .center) .frame(maxWidth: .infinity, alignment: .center) } } @@ -182,7 +280,6 @@ private struct Instruction: View { RoundedRectangle(cornerRadius: 8) .stroke(Color(nsColor: .separatorColor), lineWidth: 1) } - .scaleEffect(x: -1, y: -1, anchor: .center) } } } @@ -190,7 +287,7 @@ private struct Instruction: View { private struct UserMessage: View { let id: String let text: String - let chat: ChatProvider + let chat: StoreOf @Environment(\.colorScheme) var colorScheme @AppStorage(\.chatFontSize) var chatFontSize @AppStorage(\.chatCodeFontSize) var chatCodeFontSize @@ -217,9 +314,8 @@ private struct UserMessage: View { } .padding(.leading) .padding(.trailing, 8) - .scaleEffect(x: -1, y: -1, anchor: .center) .shadow(color: .black.opacity(0.1), radius: 2) - .frame(maxWidth: .infinity, alignment: .leading) + .frame(maxWidth: .infinity, alignment: .trailing) .contextMenu { Button("Copy") { NSPasteboard.general.clearContents() @@ -227,17 +323,17 @@ private struct UserMessage: View { } Button("Send Again") { - chat.resendMessage(id: id) + chat.send(.resendMessageButtonTapped(id)) } Button("Set as Extra System Prompt") { - chat.setAsExtraPrompt(id: id) + chat.send(.setAsExtraPromptButtonTapped(id)) } Divider() Button("Delete") { - chat.deleteMessage(id: id) + chat.send(.deleteMessageButtonTapped(id)) } } } @@ -246,19 +342,13 @@ private struct UserMessage: View { private struct BotMessage: View { let id: String let text: String - let chat: ChatProvider + let chat: StoreOf @Environment(\.colorScheme) var colorScheme @AppStorage(\.chatFontSize) var chatFontSize @AppStorage(\.chatCodeFontSize) var chatCodeFontSize var body: some View { HStack(alignment: .bottom, spacing: 2) { - CopyButton { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(text, forType: .string) - } - .scaleEffect(x: -1, y: -1, anchor: .center) - Markdown(text) .textSelection(.enabled) .markdownTheme(.custom(fontSize: chatFontSize)) @@ -279,7 +369,6 @@ private struct BotMessage: View { .stroke(Color(nsColor: .separatorColor), lineWidth: 1) } .padding(.leading, 8) - .scaleEffect(x: -1, y: -1, anchor: .center) .shadow(color: .black.opacity(0.1), radius: 2) .contextMenu { Button("Copy") { @@ -288,17 +377,22 @@ private struct BotMessage: View { } Button("Set as Extra System Prompt") { - chat.setAsExtraPrompt(id: id) + chat.send(.setAsExtraPromptButtonTapped(id)) } Divider() Button("Delete") { - chat.deleteMessage(id: id) + chat.send(.deleteMessageButtonTapped(id)) } } + + CopyButton { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(text, forType: .string) + } } - .frame(maxWidth: .infinity, alignment: .trailing) + .frame(maxWidth: .infinity, alignment: .leading) .padding(.trailing, 2) } } @@ -312,15 +406,13 @@ struct FunctionMessage: View { Markdown(text) .textSelection(.enabled) .markdownTheme(.functionCall(fontSize: chatFontSize)) - .scaleEffect(x: -1, y: -1, anchor: .center) .padding(.vertical, 2) .padding(.trailing, 2) } } struct ChatPanelInputArea: View { - @ObservedObject var chat: ChatProvider - @Binding var typedMessage: String + let chat: StoreOf @FocusState var isInputAreaFocused: Bool var body: some View { @@ -335,9 +427,10 @@ struct ChatPanelInputArea: View { .background(.ultraThickMaterial) } + @MainActor var clearButton: some View { Button(action: { - chat.clear() + chat.send(.clearButtonTap) }) { Group { if #available(macOS 13.0, *) { @@ -351,46 +444,53 @@ struct ChatPanelInputArea: View { Circle().fill(Color(nsColor: .controlBackgroundColor)) } .overlay { - Circle() - .stroke(Color(nsColor: .controlColor), lineWidth: 1) + Circle().stroke(Color(nsColor: .controlColor), lineWidth: 1) } } .buttonStyle(.plain) } + @MainActor var textEditor: some View { HStack(spacing: 0) { - ZStack(alignment: .center) { - // a hack to support dynamic height of TextEditor - Text(typedMessage.isEmpty ? "Hi" : typedMessage).opacity(0) - .font(.system(size: 14)) - .frame(maxWidth: .infinity, maxHeight: 400) + WithViewStore(chat, removeDuplicates: { $0.typedMessage == $1.typedMessage }) { + viewStore in + ZStack(alignment: .center) { + // a hack to support dynamic height of TextEditor + Text( + viewStore.state.typedMessage.isEmpty ? "Hi" : viewStore.state.typedMessage + ).opacity(0) + .font(.system(size: 14)) + .frame(maxWidth: .infinity, maxHeight: 400) + .padding(.top, 1) + .padding(.bottom, 2) + .padding(.horizontal, 4) + + CustomTextEditor( + text: viewStore.$typedMessage, + font: .systemFont(ofSize: 14), + onSubmit: { viewStore.send(.sendButtonTapped) }, + completions: chatAutoCompletion + ) .padding(.top, 1) - .padding(.bottom, 2) - .padding(.horizontal, 4) - - CustomTextEditor( - text: $typedMessage, - font: .systemFont(ofSize: 14), - onSubmit: { submitText() }, - completions: chatAutoCompletion - ) - .padding(.top, 1) - .padding(.bottom, -1) + .padding(.bottom, -1) + } + .focused($isInputAreaFocused) + .padding(8) + .fixedSize(horizontal: false, vertical: true) } - .focused($isInputAreaFocused) - .padding(8) - .fixedSize(horizontal: false, vertical: true) - Button(action: { - submitText() - }) { - Image(systemName: "paperplane.fill") - .padding(8) + WithViewStore(chat, observe: \.isReceivingMessage) { viewStore in + Button(action: { + viewStore.send(.sendButtonTapped) + }) { + Image(systemName: "paperplane.fill") + .padding(8) + } + .buttonStyle(.plain) + .disabled(viewStore.state) + .keyboardShortcut(KeyEquivalent.return, modifiers: []) } - .buttonStyle(.plain) - .disabled(chat.isReceivingMessage) - .keyboardShortcut(KeyEquivalent.return, modifiers: []) } .frame(maxWidth: .infinity) .background { @@ -403,7 +503,7 @@ struct ChatPanelInputArea: View { } .background { Button(action: { - typedMessage += "\n" + chat.send(.returnButtonTapped) }) { EmptyView() } @@ -418,15 +518,9 @@ struct ChatPanelInputArea: View { } } - func submitText() { - if typedMessage.isEmpty { return } - chat.send(typedMessage) - typedMessage = "" - } - func chatAutoCompletion(text: String, proposed: [String], range: NSRange) -> [String] { guard text.count == 1 else { return [] } - let plugins = chat.pluginIdentifiers.map { "/\($0)" } + let plugins = [String]() // chat.pluginIdentifiers.map { "/\($0)" } let availableFeatures = plugins + [ "/exit", "@code", @@ -590,9 +684,8 @@ struct ChatPanel_Preview: PreviewProvider { static var previews: some View { ChatPanel(chat: .init( - configuration: UserPreferenceChatGPTConfiguration().overriding(.init()), - history: ChatPanel_Preview.history, - isReceivingMessage: true + initialState: .init(history: ChatPanel_Preview.history, isReceivingMessage: true), + reducer: Chat(service: .init()) )) .frame(width: 450, height: 1200) .colorScheme(.dark) @@ -602,9 +695,8 @@ struct ChatPanel_Preview: PreviewProvider { struct ChatPanel_EmptyChat_Preview: PreviewProvider { static var previews: some View { ChatPanel(chat: .init( - configuration: UserPreferenceChatGPTConfiguration().overriding(.init()), - history: [], - isReceivingMessage: false + initialState: .init(history: [], isReceivingMessage: false), + reducer: Chat(service: .init()) )) .padding() .frame(width: 450, height: 600) @@ -635,9 +727,8 @@ struct ChatCodeSyntaxHighlighter: CodeSyntaxHighlighter { struct ChatPanel_InputText_Preview: PreviewProvider { static var previews: some View { ChatPanel(chat: .init( - configuration: UserPreferenceChatGPTConfiguration().overriding(.init()), - history: ChatPanel_Preview.history, - isReceivingMessage: false + initialState: .init(history: ChatPanel_Preview.history, isReceivingMessage: false), + reducer: Chat(service: .init()) )) .padding() .frame(width: 450, height: 600) @@ -649,11 +740,14 @@ struct ChatPanel_InputMultilineText_Preview: PreviewProvider { static var previews: some View { ChatPanel( chat: .init( - configuration: UserPreferenceChatGPTConfiguration().overriding(.init()), - history: ChatPanel_Preview.history, - isReceivingMessage: false - ), - typedMessage: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce turpis dolor, malesuada quis fringilla sit amet, placerat at nunc. Suspendisse orci tortor, tempor nec blandit a, malesuada vel tellus. Nunc sed leo ligula. Ut at ligula eget turpis pharetra tristique. Integer luctus leo non elit rhoncus fermentum." + initialState: .init( + typedMessage: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce turpis dolor, malesuada quis fringilla sit amet, placerat at nunc. Suspendisse orci tortor, tempor nec blandit a, malesuada vel tellus. Nunc sed leo ligula. Ut at ligula eget turpis pharetra tristique. Integer luctus leo non elit rhoncus fermentum.", + + history: ChatPanel_Preview.history, + isReceivingMessage: false + ), + reducer: Chat(service: .init()) + ) ) .padding() .frame(width: 450, height: 600) @@ -664,9 +758,8 @@ struct ChatPanel_InputMultilineText_Preview: PreviewProvider { struct ChatPanel_Light_Preview: PreviewProvider { static var previews: some View { ChatPanel(chat: .init( - configuration: UserPreferenceChatGPTConfiguration().overriding(.init()), - history: ChatPanel_Preview.history, - isReceivingMessage: true + initialState: .init(history: ChatPanel_Preview.history, isReceivingMessage: true), + reducer: Chat(service: .init()) )) .padding() .frame(width: 450, height: 600) diff --git a/Core/Sources/ChatGPTChatTab/ChatProvider.swift b/Core/Sources/ChatGPTChatTab/ChatProvider.swift deleted file mode 100644 index cce47476..00000000 --- a/Core/Sources/ChatGPTChatTab/ChatProvider.swift +++ /dev/null @@ -1,119 +0,0 @@ -import Foundation -import OpenAIService -import Preferences -import SwiftUI - -public final class ChatProvider: ObservableObject { - public typealias MessageID = String - public let id = UUID() - @Published public var history: [ChatMessage] = [] - @Published public var isReceivingMessage = false - public var temperature: Double? { - get { - configuration.overriding.temperature - } - set { - configuration.overriding.temperature = newValue - objectWillChange.send() - } - } - public var chatModelId: String? { - get { - configuration.overriding.modelId - } - set { - configuration.overriding.modelId = newValue - objectWillChange.send() - } - } - private let configuration: OverridingChatGPTConfiguration - public var pluginIdentifiers: [String] = [] - public var systemPrompt = "" - - public var title: String { - let defaultTitle = "Chat" - guard let lastMessageText = history - .filter({ $0.role == .assistant || $0.role == .user }) - .last? - .text else { return defaultTitle } - if lastMessageText.isEmpty { return defaultTitle } - let trimmed = lastMessageText - .trimmingCharacters(in: .punctuationCharacters) - .trimmingCharacters(in: .whitespacesAndNewlines) - if trimmed.starts(with: "```") { - return "Code Block" - } else { - return trimmed - } - } - - public var extraSystemPrompt = "" - public var onMessageSend: (String) -> Void - public var onStop: () -> Void - public var onClear: () -> Void - public var onDeleteMessage: (MessageID) -> Void - public var onResendMessage: (MessageID) -> Void - public var onResetPrompt: () -> Void - public var onRunCustomCommand: (CustomCommand) -> Void = { _ in } - public var onSetAsExtraPrompt: (MessageID) -> Void - - public init( - configuration: OverridingChatGPTConfiguration, - history: [ChatMessage] = [], - isReceivingMessage: Bool = false, - pluginIdentifiers: [String] = [], - onMessageSend: @escaping (String) -> Void = { _ in }, - onStop: @escaping () -> Void = {}, - onClear: @escaping () -> Void = {}, - onDeleteMessage: @escaping (MessageID) -> Void = { _ in }, - onResendMessage: @escaping (MessageID) -> Void = { _ in }, - onResetPrompt: @escaping () -> Void = {}, - onRunCustomCommand: @escaping (CustomCommand) -> Void = { _ in }, - onSetAsExtraPrompt: @escaping (MessageID) -> Void = { _ in } - ) { - self.configuration = configuration - self.history = history - self.isReceivingMessage = isReceivingMessage - self.pluginIdentifiers = pluginIdentifiers - self.onMessageSend = onMessageSend - self.onStop = onStop - self.onClear = onClear - self.onDeleteMessage = onDeleteMessage - self.onResendMessage = onResendMessage - self.onResetPrompt = onResetPrompt - self.onRunCustomCommand = onRunCustomCommand - self.onSetAsExtraPrompt = onSetAsExtraPrompt - } - - public func send(_ message: String) { onMessageSend(message) } - public func stop() { onStop() } - public func clear() { onClear() } - public func deleteMessage(id: MessageID) { onDeleteMessage(id) } - public func resendMessage(id: MessageID) { onResendMessage(id) } - public func resetPrompt() { onResetPrompt() } - public func triggerCustomCommand(_ command: CustomCommand) { - onRunCustomCommand(command) - } - - public func setAsExtraPrompt(id: MessageID) { onSetAsExtraPrompt(id) } -} - -public struct ChatMessage: Equatable { - public enum Role { - case user - case assistant - case function - case ignored - } - - public var id: String - public var role: Role - public var text: String - - public init(id: String, role: Role, text: String) { - self.id = id - self.role = role - self.text = text - } -} - diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 57b6bbec..2be2c2f8 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -10,6 +10,7 @@ public final class ChatService: ObservableObject { public let configuration: OverridingChatGPTConfiguration public let chatGPTService: any ChatGPTServiceType public var allPluginCommands: [String] { allPlugins.map { $0.command } } + @Published public internal(set) var chatHistory: [ChatMessage] = [] @Published public internal(set) var isReceivingMessage = false @Published public internal(set) var systemPrompt = UserDefaults.shared .value(for: \.defaultChatSystemPrompt) @@ -54,7 +55,9 @@ public final class ChatService: ObservableObject { memory.chatService = self memory.observeHistoryChange { [weak self] in - self?.objectWillChange.send() + Task { [weak self] in + self?.chatHistory = await memory.history + } } } From e5b3d8f82816c81bd8aa189ac2f9453534fd58d3 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 5 Oct 2023 00:21:07 +0800 Subject: [PATCH 09/37] Make code blocks in chat horizontally scrollable --- Core/Sources/ChatGPTChatTab/Styles.swift | 84 +++++++++++++++++------- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Styles.swift b/Core/Sources/ChatGPTChatTab/Styles.swift index 330cc92f..d873f31b 100644 --- a/Core/Sources/ChatGPTChatTab/Styles.swift +++ b/Core/Sources/ChatGPTChatTab/Styles.swift @@ -41,31 +41,34 @@ extension MarkdownUI.Theme { FontSize(fontSize) } .codeBlock { configuration in - configuration.label - .relativeLineSpacing(.em(0.225)) - .markdownTextStyle { - FontFamilyVariant(.monospaced) - FontSize(.em(0.85)) - } - .padding(16) - .padding(.top, 14) - .background(Color(nsColor: .textBackgroundColor).opacity(0.7)) - .clipShape(RoundedRectangle(cornerRadius: 6)) - .overlay(alignment: .top) { - HStack(alignment: .center) { - Text(configuration.language ?? "code") - .foregroundStyle(.tertiary) - .font(.callout) - .padding(.leading, 8) - .lineLimit(1) - Spacer() - CopyButton { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(configuration.content, forType: .string) - } + ScrollView(.horizontal) { + configuration.label + .relativeLineSpacing(.em(0.225)) + .markdownTextStyle { + FontFamilyVariant(.monospaced) + FontSize(.em(0.85)) + } + .padding(16) + .padding(.top, 14) + } + .workaroundForVerticalScrollingBugInMacOS() + .background(Color(nsColor: .textBackgroundColor).opacity(0.7)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + .overlay(alignment: .top) { + HStack(alignment: .center) { + Text(configuration.language ?? "code") + .foregroundStyle(.tertiary) + .font(.callout) + .padding(.leading, 8) + .lineLimit(1) + Spacer() + CopyButton { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(configuration.content, forType: .string) } } - .markdownMargin(top: 4, bottom: 16) + } + .markdownMargin(top: 4, bottom: 16) } } @@ -98,3 +101,38 @@ extension MarkdownUI.Theme { } } +final class VerticalScrollingFixHostingView: NSHostingView where Content: View { + override func wantsForwardedScrollEvents(for axis: NSEvent.GestureAxis) -> Bool { + return axis == .vertical + } +} + +struct VerticalScrollingFixViewRepresentable: NSViewRepresentable where Content: View { + let content: Content + + func makeNSView(context: Context) -> NSHostingView { + return VerticalScrollingFixHostingView(rootView: content) + } + + func updateNSView(_ nsView: NSHostingView, context: Context) {} +} + +struct VerticalScrollingFixWrapper: View where Content: View { + let content: () -> Content + + init(@ViewBuilder content: @escaping () -> Content) { + self.content = content + } + + var body: some View { + VerticalScrollingFixViewRepresentable(content: self.content()) + } +} + +extension View { + /// https://stackoverflow.com/questions/64920744/swiftui-nested-scrollviews-problem-on-macos + @ViewBuilder func workaroundForVerticalScrollingBugInMacOS() -> some View { + VerticalScrollingFixWrapper { self } + } +} + From d76379670806ba538ca584f6368980f2a5526413 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 5 Oct 2023 00:47:25 +0800 Subject: [PATCH 10/37] Allow unwrapping code in code block --- Core/Sources/ChatGPTChatTab/Styles.swift | 56 +++++++++++++------ .../FeatureSettings/ChatSettingsView.swift | 5 ++ Tool/Sources/Preferences/Keys.swift | 4 ++ 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Styles.swift b/Core/Sources/ChatGPTChatTab/Styles.swift index d873f31b..2fa6c6be 100644 --- a/Core/Sources/ChatGPTChatTab/Styles.swift +++ b/Core/Sources/ChatGPTChatTab/Styles.swift @@ -33,25 +33,20 @@ extension NSAppearance { } } -extension MarkdownUI.Theme { - static func custom(fontSize: Double) -> MarkdownUI.Theme { - .gitHub.text { - ForegroundColor(.primary) - BackgroundColor(Color.clear) - FontSize(fontSize) - } - .codeBlock { configuration in - ScrollView(.horizontal) { - configuration.label - .relativeLineSpacing(.em(0.225)) - .markdownTextStyle { - FontFamilyVariant(.monospaced) - FontSize(.em(0.85)) - } - .padding(16) - .padding(.top, 14) +extension View { + func codeBlockLabelStyle() -> some View { + self + .relativeLineSpacing(.em(0.225)) + .markdownTextStyle { + FontFamilyVariant(.monospaced) + FontSize(.em(0.85)) } - .workaroundForVerticalScrollingBugInMacOS() + .padding(16) + .padding(.top, 14) + } + + func codeBlockStyle(_ configuration: CodeBlockConfiguration) -> some View { + self .background(Color(nsColor: .textBackgroundColor).opacity(0.7)) .clipShape(RoundedRectangle(cornerRadius: 6)) .overlay(alignment: .top) { @@ -69,6 +64,31 @@ extension MarkdownUI.Theme { } } .markdownMargin(top: 4, bottom: 16) + } +} + +extension MarkdownUI.Theme { + static func custom(fontSize: Double) -> MarkdownUI.Theme { + .gitHub.text { + ForegroundColor(.primary) + BackgroundColor(Color.clear) + FontSize(fontSize) + } + .codeBlock { configuration in + let wrapCode = UserDefaults.shared.value(for: \.wrapCodeInChatCodeBlock) + + if wrapCode { + configuration.label + .codeBlockLabelStyle() + .codeBlockStyle(configuration) + } else { + ScrollView(.horizontal) { + configuration.label + .codeBlockLabelStyle() + } + .workaroundForVerticalScrollingBugInMacOS() + .codeBlockStyle(configuration) + } } } diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index 880ab20d..7f903728 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -19,6 +19,7 @@ struct ChatSettingsView: View { @AppStorage(\.defaultChatFeatureEmbeddingModelId) var defaultChatFeatureEmbeddingModelId @AppStorage(\.chatModels) var chatModels @AppStorage(\.embeddingModels) var embeddingModels + @AppStorage(\.wrapCodeInChatCodeBlock) var wrapCodeInCodeBlock init() {} } @@ -159,6 +160,10 @@ struct ChatSettingsView: View { Text("pt") } + + Toggle(isOn: $settings.wrapCodeInCodeBlock) { + Text("Wrap code in code block") + } } } diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index eee92784..37d9344d 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -384,6 +384,10 @@ public extension UserDefaultPreferenceKeys { var chatSearchPluginMaxIterations: PreferenceKey { .init(defaultValue: 3, key: "ChatSearchPluginMaxIterations") } + + var wrapCodeInChatCodeBlock: PreferenceKey { + .init(defaultValue: true, key: "WrapCodeInChatCodeBlock") + } } // MARK: - Bing Search From 29240bc6145a68222568263acdfeaa509bf23138 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 5 Oct 2023 02:06:05 +0800 Subject: [PATCH 11/37] Support chat tab reordering in UI --- .../SuggestionWidget/ChatWindowView.swift | 89 +++++++++++++------ .../FeatureReducers/ChatPanelFeature.swift | 18 +++- 2 files changed, 78 insertions(+), 29 deletions(-) diff --git a/Core/Sources/SuggestionWidget/ChatWindowView.swift b/Core/Sources/SuggestionWidget/ChatWindowView.swift index 0b610bae..888efe54 100644 --- a/Core/Sources/SuggestionWidget/ChatWindowView.swift +++ b/Core/Sources/SuggestionWidget/ChatWindowView.swift @@ -160,6 +160,7 @@ struct ChatTabBar: View { } @Environment(\.chatTabPool) var chatTabPool + @State var draggingTabId: String? var body: some View { WithViewStore( @@ -186,6 +187,21 @@ struct ChatTabBar: View { tab.menu } .id(info.id) + .onDrag { + print("dragging tab \(info.id)") + draggingTabId = info.id + return NSItemProvider(object: info.id as NSString) + } + .onDrop( + of: [.text], + delegate: ChatTabBarDropDelegate( + store: store, + tabs: viewStore.state.tabInfo, + itemId: info.id, + draggingTabId: $draggingTabId + ) + ) + } else { EmptyView() } @@ -264,6 +280,30 @@ struct ChatTabBar: View { } } +struct ChatTabBarDropDelegate: DropDelegate { + let store: StoreOf + let tabs: IdentifiedArray + let itemId: String + @Binding var draggingTabId: String? + + func dropUpdated(info: DropInfo) -> DropProposal? { + return DropProposal(operation: .move) + } + + func performDrop(info: DropInfo) -> Bool { + draggingTabId = nil + return true + } + + func dropEntered(info: DropInfo) { + guard itemId != draggingTabId else { return } + let from = tabs.firstIndex { $0.id == draggingTabId } + let to = tabs.firstIndex { $0.id == itemId } + guard let from, let to, from != to else { return } + store.send(.moveChatTab(from: from, to: to)) + } +} + struct ChatTabBarButton: View { let store: StoreOf let info: ChatTabInfo @@ -273,33 +313,30 @@ struct ChatTabBarButton: View { var body: some View { HStack(spacing: 0) { - Button(action: { - store.send(.tabClicked(id: info.id)) - }) { - content() - .font(.callout) - .lineLimit(1) - .frame(maxWidth: 120) - .padding(.horizontal, 32) - .contentShape(Rectangle()) - } - .buttonStyle(PlainButtonStyle()) - - .overlay(alignment: .leading) { - Button(action: { - store.send(.closeTabButtonClicked(id: info.id)) - }) { - Image(systemName: "xmark") - .foregroundColor(.secondary) + content() + .font(.callout) + .lineLimit(1) + .frame(maxWidth: 120) + .padding(.horizontal, 32) + .contentShape(Rectangle()) + .onTapGesture { + store.send(.tabClicked(id: info.id)) } - .buttonStyle(.plain) - .padding(2) - .padding(.leading, 8) - .opacity(isHovered ? 1 : 0) - } - .onHover { isHovered = $0 } - .animation(.linear(duration: 0.1), value: isHovered) - .animation(.linear(duration: 0.1), value: isSelected) + .overlay(alignment: .leading) { + Button(action: { + store.send(.closeTabButtonClicked(id: info.id)) + }) { + Image(systemName: "xmark") + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + .padding(2) + .padding(.leading, 8) + .opacity(isHovered ? 1 : 0) + } + .onHover { isHovered = $0 } + .animation(.linear(duration: 0.1), value: isHovered) + .animation(.linear(duration: 0.1), value: isSelected) Divider().padding(.vertical, 6) } diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift index e6f2a517..b63d74b3 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift @@ -27,7 +27,7 @@ public struct ChatPanelFeature: ReducerProtocol { public var tabInfo: IdentifiedArray public var tabCollection: [ChatTabBuilderCollection] public var selectedTabId: String? - + public var selectedTabInfo: ChatTabInfo? { guard let id = selectedTabId else { return tabInfo.first } return tabInfo[id: id] @@ -69,7 +69,8 @@ public struct ChatPanelFeature: ReducerProtocol { case appendAndSelectTab(ChatTabInfo) case switchToNextTab case switchToPreviousTab - + case moveChatTab(from: Int, to: Int) + case chatTab(id: String, action: ChatTabItem.Action) } @@ -205,7 +206,18 @@ public struct ChatPanelFeature: ReducerProtocol { let targetId = state.chatTabGroup.tabInfo[previousIndex].id state.chatTabGroup.selectedTabId = targetId return .none - + + case let .moveChatTab(from, to): + guard from >= 0, from < state.chatTabGroup.tabInfo.endIndex, to >= 0, + to <= state.chatTabGroup.tabInfo.endIndex + else { + return .none + } + let tab = state.chatTabGroup.tabInfo[from] + state.chatTabGroup.tabInfo.remove(at: from) + state.chatTabGroup.tabInfo.insert(tab, at: to) + return .none + case .chatTab: return .none } From e973924e725624bc562af4a895483ca1a79474e5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 5 Oct 2023 15:05:50 +0800 Subject: [PATCH 12/37] Support persisting tab order --- .../GraphicalUserInterfaceController.swift | 250 ++++++++++-------- .../SuggestionWidget/ChatWindowView.swift | 1 - Pro | 2 +- 3 files changed, 138 insertions(+), 115 deletions(-) diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index 77d4e0fa..29f53cbe 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -31,12 +31,17 @@ struct GUI: ReducerProtocol { } #if canImport(ChatTabPersistent) + var isChatTabRestoreFinished: Bool = false var persistentState: ChatTabPersistent.State { get { - .init(chatTabInfo: chatTabGroup.tabInfo) + .init( + chatTabInfo: chatTabGroup.tabInfo, + isRestoreFinished: isChatTabRestoreFinished + ) } set { chatTabGroup.tabInfo = newValue.chatTabInfo + isChatTabRestoreFinished = newValue.isRestoreFinished } } #endif @@ -60,148 +65,167 @@ struct GUI: ReducerProtocol { } @Dependency(\.chatTabPool) var chatTabPool: ChatTabPool + + public enum Debounce: Hashable { + case updateChatTabOrder + } var body: some ReducerProtocol { - Scope(state: \.suggestionWidgetState, action: /Action.suggestionWidget) { - WidgetFeature() - } + CombineReducers { + Scope(state: \.suggestionWidgetState, action: /Action.suggestionWidget) { + WidgetFeature() + } - Scope( - state: \.chatTabGroup, - action: /Action.suggestionWidget .. /WidgetFeature.Action.chatPanel - ) { - Reduce { _, action in - switch action { - case let .createNewTapButtonClicked(kind): - return .run { send in - if let (_, chatTabInfo) = await chatTabPool.createTab(for: kind) { - await send(.appendAndSelectTab(chatTabInfo)) + Scope( + state: \.chatTabGroup, + action: /Action.suggestionWidget .. /WidgetFeature.Action.chatPanel + ) { + Reduce { _, action in + switch action { + case let .createNewTapButtonClicked(kind): + return .run { send in + if let (_, chatTabInfo) = await chatTabPool.createTab(for: kind) { + await send(.appendAndSelectTab(chatTabInfo)) + } } - } - case let .closeTabButtonClicked(id): - return .run { _ in - chatTabPool.removeTab(of: id) - } + case let .closeTabButtonClicked(id): + return .run { _ in + chatTabPool.removeTab(of: id) + } - case let .chatTab(_, .openNewTab(builder)): - return .run { send in - if let (_, chatTabInfo) = await chatTabPool - .createTab(from: builder.chatTabBuilder) - { - await send(.appendAndSelectTab(chatTabInfo)) + case let .chatTab(_, .openNewTab(builder)): + return .run { send in + if let (_, chatTabInfo) = await chatTabPool + .createTab(from: builder.chatTabBuilder) + { + await send(.appendAndSelectTab(chatTabInfo)) + } } - } - default: - return .none + default: + return .none + } } } - } - #if canImport(ChatTabPersistent) - Scope(state: \.persistentState, action: /Action.persistent) { - ChatTabPersistent() - } - #endif + #if canImport(ChatTabPersistent) + Scope(state: \.persistentState, action: /Action.persistent) { + ChatTabPersistent() + } + #endif - Reduce { state, action in - switch action { - case .start: - #if canImport(ChatTabPersistent) - return .run { send in - await send(.persistent(.restoreChatTabs)) - } - #else - return .none - #endif + Reduce { state, action in + switch action { + case .start: + #if canImport(ChatTabPersistent) + return .run { send in + await send(.persistent(.restoreChatTabs)) + } + #else + return .none + #endif - case let .openChatPanel(forceDetach): - return .run { send in - await send( - .suggestionWidget(.chatPanel(.presentChatPanel(forceDetach: forceDetach))) - ) - } + case let .openChatPanel(forceDetach): + return .run { send in + await send( + .suggestionWidget( + .chatPanel(.presentChatPanel(forceDetach: forceDetach)) + ) + ) + } - case .createChatGPTChatTabIfNeeded: - if state.chatTabGroup.tabInfo.contains(where: { - chatTabPool.getTab(of: $0.id) is ChatGPTChatTab - }) { - return .none - } - return .run { send in - if let (_, chatTabInfo) = await chatTabPool.createTab(for: nil) { - await send(.suggestionWidget(.chatPanel(.appendAndSelectTab(chatTabInfo)))) + case .createChatGPTChatTabIfNeeded: + if state.chatTabGroup.tabInfo.contains(where: { + chatTabPool.getTab(of: $0.id) is ChatGPTChatTab + }) { + return .none + } + return .run { send in + if let (_, chatTabInfo) = await chatTabPool.createTab(for: nil) { + await send( + .suggestionWidget(.chatPanel(.appendAndSelectTab(chatTabInfo))) + ) + } } - } - case let .sendCustomCommandToActiveChat(command): - @Sendable func stopAndHandleCommand(_ tab: ChatGPTChatTab) async { - if tab.service.isReceivingMessage { - await tab.service.stopReceivingMessage() + case let .sendCustomCommandToActiveChat(command): + @Sendable func stopAndHandleCommand(_ tab: ChatGPTChatTab) async { + if tab.service.isReceivingMessage { + await tab.service.stopReceivingMessage() + } + try? await tab.service.handleCustomCommand(command) } - try? await tab.service.handleCustomCommand(command) - } - if let info = state.chatTabGroup.selectedTabInfo, - let activeTab = chatTabPool.getTab(of: info.id) as? ChatGPTChatTab - { - return .run { send in - await send(.openChatPanel(forceDetach: false)) - await stopAndHandleCommand(activeTab) + if let info = state.chatTabGroup.selectedTabInfo, + let activeTab = chatTabPool.getTab(of: info.id) as? ChatGPTChatTab + { + return .run { send in + await send(.openChatPanel(forceDetach: false)) + await stopAndHandleCommand(activeTab) + } + } + + if let info = state.chatTabGroup.tabInfo.first(where: { + chatTabPool.getTab(of: $0.id) is ChatGPTChatTab + }), + let chatTab = chatTabPool.getTab(of: info.id) as? ChatGPTChatTab + { + state.chatTabGroup.selectedTabId = chatTab.id + return .run { send in + await send(.openChatPanel(forceDetach: false)) + await stopAndHandleCommand(chatTab) + } } - } - if let info = state.chatTabGroup.tabInfo.first(where: { - chatTabPool.getTab(of: $0.id) is ChatGPTChatTab - }), - let chatTab = chatTabPool.getTab(of: info.id) as? ChatGPTChatTab - { - state.chatTabGroup.selectedTabId = chatTab.id return .run { send in + guard let (chatTab, chatTabInfo) = await chatTabPool.createTab(for: nil) + else { + return + } + await send(.suggestionWidget(.chatPanel(.appendAndSelectTab(chatTabInfo)))) await send(.openChatPanel(forceDetach: false)) - await stopAndHandleCommand(chatTab) + if let chatTab = chatTab as? ChatGPTChatTab { + await stopAndHandleCommand(chatTab) + } } - } - return .run { send in - guard let (chatTab, chatTabInfo) = await chatTabPool.createTab(for: nil) else { - return + case let .suggestionWidget(.chatPanel(.chatTab(id, .tabContentUpdated))): + #if canImport(ChatTabPersistent) + // when a tab is updated, persist it. + return .run { send in + await send(.persistent(.chatTabUpdated(id: id))) } - await send(.suggestionWidget(.chatPanel(.appendAndSelectTab(chatTabInfo)))) - await send(.openChatPanel(forceDetach: false)) - if let chatTab = chatTab as? ChatGPTChatTab { - await stopAndHandleCommand(chatTab) + #else + return .none + #endif + + case let .suggestionWidget(.chatPanel(.closeTabButtonClicked(id))): + #if canImport(ChatTabPersistent) + // when a tab is closed, remove it from persistence. + return .run { send in + await send(.persistent(.chatTabClosed(id: id))) } - } + #else + return .none + #endif - case let .suggestionWidget(.chatPanel(.chatTab(id, .tabContentUpdated))): - #if canImport(ChatTabPersistent) - // when a tab is updated, persist it. - return .run { send in - await send(.persistent(.chatTabUpdated(id: id))) - } - #else - return .none - #endif + case .suggestionWidget: + return .none - case let .suggestionWidget(.chatPanel(.closeTabButtonClicked(id))): #if canImport(ChatTabPersistent) - // when a tab is closed, remove it from persistence. - return .run { send in - await send(.persistent(.chatTabClosed(id: id))) - } - #else - return .none + case .persistent: + return .none #endif - - case .suggestionWidget: - return .none - - #if canImport(ChatTabPersistent) - case .persistent: - return .none - #endif + } + } + }.onChange(of: \.chatTabGroup.tabInfo) { _, _ in + Reduce { _, _ in + .run { send in + #if canImport(ChatTabPersistent) + await send(.persistent(.chatOrderChanged)) + #endif + }.debounce(id: Debounce.updateChatTabOrder, for: 1, scheduler: DispatchQueue.main) } } } diff --git a/Core/Sources/SuggestionWidget/ChatWindowView.swift b/Core/Sources/SuggestionWidget/ChatWindowView.swift index 888efe54..370bccab 100644 --- a/Core/Sources/SuggestionWidget/ChatWindowView.swift +++ b/Core/Sources/SuggestionWidget/ChatWindowView.swift @@ -188,7 +188,6 @@ struct ChatTabBar: View { } .id(info.id) .onDrag { - print("dragging tab \(info.id)") draggingTabId = info.id return NSItemProvider(object: info.id as NSString) } diff --git a/Pro b/Pro index 71f5088f..d106b3bd 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 71f5088ffbdff30b9cd2027fb3fa359f4ae2a25e +Subproject commit d106b3bdd9d685b5886b00879e72e4dc216d6b6e From 91c6f81aee3a813abcc4bbb60f1754c86b41e48c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 5 Oct 2023 15:18:09 +0800 Subject: [PATCH 13/37] Fix that new chat tab was not starting at the bottom --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 52 ++++++++++++--------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index 4766a43a..ca5fc766 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -53,12 +53,12 @@ struct ChatPanelMessages: View { List { Group { Spacer(minLength: 12) - + Instruction() - + ChatHistory(chat: chat) .listItemTint(.clear) - + WithViewStore(chat, observe: \.isReceivingMessage) { viewStore in if viewStore.state { StopRespondingButton(chat: chat) @@ -71,8 +71,14 @@ struct ChatPanelMessages: View { )) } } - + Spacer(minLength: 12) + .onAppear { + withAnimation { + proxy.scrollTo(bottomID, anchor: .bottom) + } + } + .id(bottomID) .background(GeometryReader { geo in let offset = geo.frame(in: .named(scrollSpace)).minY Color.clear @@ -85,7 +91,6 @@ struct ChatPanelMessages: View { key: ListHeightPreferenceKey.self, value: listGeo.size.height ) - .id(bottomID) } .modify { view in if #available(macOS 13.0, *) { @@ -115,7 +120,12 @@ struct ChatPanelMessages: View { Image(systemName: "arrow.down") .padding(4) .background { - Circle().fill(.thickMaterial) + Circle() + .fill(.thickMaterial) + .shadow(color: .black.opacity(0.2), radius: 2) + } + .overlay { + Circle().stroke(Color(nsColor: .separatorColor), lineWidth: 1) } .foregroundStyle(.secondary) .padding(4) @@ -127,7 +137,7 @@ struct ChatPanelMessages: View { if isInitialLoad { isInitialLoad = false } - withAnimation(.easeInOut(duration: 0.1)) { + withAnimation { proxy.scrollTo(bottomID, anchor: .bottom) } } @@ -137,7 +147,7 @@ struct ChatPanelMessages: View { } } } - + func updatePinningState() { if scrollOffset > listHeight + 24 + 100 || scrollOffset <= 0 { pinnedToBottom = false @@ -216,19 +226,6 @@ private struct Instruction: View { var body: some View { Group { - Markdown( - """ - Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. - - \( - useCodeScopeByDefaultInChatContext - ? "Scope **`@code`** is enabled by default." - : "Scope **`@file`** is enabled by default." - ) - """ - ) - .modifier(InstructionModifier()) - Markdown( """ You can use scopes to give the bot extra abilities. @@ -263,6 +260,19 @@ private struct Instruction: View { """ ) .modifier(InstructionModifier()) + + Markdown( + """ + Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. + + \( + useCodeScopeByDefaultInChatContext + ? "Scope **`@code`** is enabled by default." + : "Scope **`@file`** is enabled by default." + ) + """ + ) + .modifier(InstructionModifier()) } } From 5e534b33115a57426580557cfc8d00178d8d4d1d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 5 Oct 2023 15:18:59 +0800 Subject: [PATCH 14/37] Fix --- .../Service/GUI/GraphicalUserInterfaceController.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index 29f53cbe..343b48f0 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -219,9 +219,12 @@ struct GUI: ReducerProtocol { #endif } } - }.onChange(of: \.chatTabGroup.tabInfo) { _, _ in + }.onChange(of: \.chatTabGroup.tabInfo) { old, new in Reduce { _, _ in - .run { send in + guard old.map(\.id) != new.map(\.id) else { + return .none + } + return .run { send in #if canImport(ChatTabPersistent) await send(.persistent(.chatOrderChanged)) #endif From 946e4b61c89011d8e6a79e9fea6dea5e90e449b3 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 5 Oct 2023 15:38:46 +0800 Subject: [PATCH 15/37] Prevent chat tab from being persisted after closed --- Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift | 1 - Pro | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift index 96c7e526..3841089f 100644 --- a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift +++ b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift @@ -118,7 +118,6 @@ public class ChatGPTChatTab: ChatTab { }.store(in: &cancellable) viewStore.publisher.removeDuplicates() - .debounce(for: .seconds(1), scheduler: DispatchQueue.main) .sink { [weak self] _ in Task { @MainActor [weak self] in self?.chatTabViewStore.send(.tabContentUpdated) diff --git a/Pro b/Pro index d106b3bd..a8895f1a 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit d106b3bdd9d685b5886b00879e72e4dc216d6b6e +Subproject commit a8895f1a92339a7d795aac2f58dbac2c646e567c From eebff15536a40422ba34152575724fc3d3c7c99f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 5 Oct 2023 15:51:13 +0800 Subject: [PATCH 16/37] Adjust the position of stop responding button --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index ca5fc766..e8efcf41 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -58,17 +58,10 @@ struct ChatPanelMessages: View { ChatHistory(chat: chat) .listItemTint(.clear) - + WithViewStore(chat, observe: \.isReceivingMessage) { viewStore in if viewStore.state { - StopRespondingButton(chat: chat) - .padding(.vertical, 4) - .listRowInsets(EdgeInsets( - top: 0, - leading: -8, - bottom: 0, - trailing: -8 - )) + Spacer(minLength: 12) } } @@ -110,6 +103,16 @@ struct ChatPanelMessages: View { scrollOffset = value updatePinningState() } + .overlay(alignment: .bottom) { + WithViewStore(chat, observe: \.isReceivingMessage) { viewStore in + StopRespondingButton(chat: chat) + .padding(.bottom, 8) + .opacity(viewStore.state ? 1 : 0) + .disabled(!viewStore.state) + .transformEffect(.init(translationX: 0, y: viewStore.state ? 0 : 20)) + .animation(.easeInOut(duration: 0.2), value: viewStore.state) + } + } .overlay(alignment: .bottomTrailing) { WithViewStore(chat, observe: \.history.last) { viewStore in Button(action: { From d63123499ec47e8597d0c8ba0517b07c57f382f4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 5 Oct 2023 16:25:56 +0800 Subject: [PATCH 17/37] Make generateContext async --- .../DynamicContextController.swift | 27 ++++++++++++++----- Pro | 2 +- .../ChatContextCollector.swift | 2 +- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index 5c1cc81a..bc2461dd 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -46,13 +46,26 @@ final class DynamicContextController { functionProvider.removeAll() let language = UserDefaults.shared.value(for: \.chatGPTLanguage) let oldMessages = await memory.history - let contexts = contextCollectors.compactMap { - $0.generateContext( - history: oldMessages, - scopes: scopes, - content: content, - configuration: configuration - ) + let contexts = await withTaskGroup( + of: ChatContext?.self + ) { [scopes, content, configuration] group in + for collector in contextCollectors { + group.addTask { + await collector.generateContext( + history: oldMessages, + scopes: scopes, + content: content, + configuration: configuration + ) + } + } + var contexts = [ChatContext]() + for await context in group { + if let context = context { + contexts.append(context) + } + } + return contexts } let contextualSystemPrompt = """ \(language.isEmpty ? "" : "You must always reply in \(language)") diff --git a/Pro b/Pro index a8895f1a..ba9117be 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit a8895f1a92339a7d795aac2f58dbac2c646e567c +Subproject commit ba9117be02ca4e73508639f917e3781d28e5b94a diff --git a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift index e890a882..b0d4b45f 100644 --- a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift @@ -16,6 +16,6 @@ public protocol ChatContextCollector { scopes: Set, content: String, configuration: ChatGPTConfiguration - ) -> ChatContext? + ) async -> ChatContext? } From 68575c2fc0f9bafd6bef419a42b52a0f250d0549 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 5 Oct 2023 17:12:00 +0800 Subject: [PATCH 18/37] ChatContextCollector --- .../ActiveDocumentChatContextCollector.swift | 17 +++-- ...cyActiveDocumentChatContextCollector.swift | 51 +++++++-------- .../SystemInfoChatContextCollector.swift | 15 +++-- .../WebChatContextCollector.swift | 11 +++- .../DynamicContextController.swift | 18 ++++-- Pro | 2 +- .../ChatContextCollector.swift | 62 ++++++++++++++++++- 7 files changed, 130 insertions(+), 46 deletions(-) diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index a3a32ff4..0fd68c2a 100644 --- a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -17,8 +17,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { scopes: Set, content: String, configuration: ChatGPTConfiguration - ) -> ChatContext? { - guard let info = getEditorInformation() else { return nil } + ) -> ChatContext { + guard let info = getEditorInformation() else { return .empty } let context = getActiveDocumentContext(info) activeDocumentContext = context @@ -27,11 +27,16 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { var removedCode = context removedCode.focusedContext = nil return .init( - systemPrompt: extractSystemPrompt(removedCode), + systemPrompt: [ + .init( + content: extractSystemPrompt(removedCode), + priority: .high + ), + ], functions: [] ) } - return nil + return .empty } var functions = [any ChatGPTFunction]() @@ -65,7 +70,9 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { } return .init( - systemPrompt: extractSystemPrompt(context), + systemPrompt: [ + .init(content: extractSystemPrompt(context), priority: .high) + ], functions: functions ) } diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift index 3e3af26b..6629ab35 100644 --- a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift @@ -1,9 +1,9 @@ +import ChatContextCollector import Foundation import OpenAIService import Preferences import SuggestionModel import XcodeInspector -import ChatContextCollector public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { public init() {} @@ -13,8 +13,8 @@ public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { scopes: Set, content: String, configuration: ChatGPTConfiguration - ) -> ChatContext? { - guard let content = getEditorInformation() else { return nil } + ) -> ChatContext { + guard let content = getEditorInformation() else { return .empty } let relativePath = content.relativePath let selectionRange = content.editorContent?.selections.first ?? .outOfScope let editorContent = { @@ -78,30 +78,31 @@ public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { }() return .init( - systemPrompt: """ - Active Document Context:### - Document Relative Path: \(relativePath) - Selection Range Start: \ - Line \(selectionRange.start.line) \ - Character \(selectionRange.start.character) - Selection Range End: \ - Line \(selectionRange.end.line) \ - Character \(selectionRange.end.character) - Cursor Position: \ - Line \(selectionRange.end.line) \ - Character \(selectionRange.end.character) - \(editorContent) - Line Annotations: - \( - content.editorContent?.lineAnnotations - .map { " - \($0)" } - .joined(separator: "\n") ?? "N/A" - ) - ### - """, + systemPrompt: [ + .init(content: """ + Active Document Context:### + Document Relative Path: \(relativePath) + Selection Range Start: \ + Line \(selectionRange.start.line) \ + Character \(selectionRange.start.character) + Selection Range End: \ + Line \(selectionRange.end.line) \ + Character \(selectionRange.end.character) + Cursor Position: \ + Line \(selectionRange.end.line) \ + Character \(selectionRange.end.character) + \(editorContent) + Line Annotations: + \( + content.editorContent?.lineAnnotations + .map { " - \($0)" } + .joined(separator: "\n") ?? "N/A" + ) + ### + """, priority: .high), + ], functions: [] ) } } - diff --git a/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift b/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift index 19aab821..061af519 100644 --- a/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift @@ -16,11 +16,18 @@ public final class SystemInfoChatContextCollector: ChatContextCollector { scopes: Set, content: String, configuration: ChatGPTConfiguration - ) -> ChatContext? { + ) -> ChatContext { return .init( - systemPrompt: """ - Current Time: \(Self.dateFormatter.string(from: Date())) (You can use it to calculate time in another time zone) - """, + systemPrompt: [ + .init( + content: """ + Current Time: \( + Self.dateFormatter.string(from: Date()) + ) (You can use it to calculate time in another time zone) + """, + priority: .custom(999999) + ), + ], functions: [] ) } diff --git a/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift b/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift index bf72667f..91b6b5bc 100644 --- a/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift @@ -12,8 +12,8 @@ public final class WebChatContextCollector: ChatContextCollector { scopes: Set, content: String, configuration: ChatGPTConfiguration - ) -> ChatContext? { - guard scopes.contains("web") || scopes.contains("w") else { return nil } + ) -> ChatContext { + guard scopes.contains("web") || scopes.contains("w") else { return .empty } let links = Self.detectLinks(from: history) + Self.detectLinks(from: content) let functions: [(any ChatGPTFunction)?] = [ SearchFunction(maxTokens: configuration.maxTokens), @@ -21,7 +21,12 @@ public final class WebChatContextCollector: ChatContextCollector { links.isEmpty ? nil : QueryWebsiteFunction(), ] return .init( - systemPrompt: "You prefer to answer questions with latest content on the internet.", + systemPrompt: [ + .init( + content: "You prefer to answer questions with latest content on the internet.", + priority: .low + ), + ], functions: functions.compactMap { $0 } ) } diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index bc2461dd..e5ccb34b 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -47,7 +47,7 @@ final class DynamicContextController { let language = UserDefaults.shared.value(for: \.chatGPTLanguage) let oldMessages = await memory.history let contexts = await withTaskGroup( - of: ChatContext?.self + of: ChatContext.self ) { [scopes, content, configuration] group in for collector in contextCollectors { group.addTask { @@ -61,17 +61,25 @@ final class DynamicContextController { } var contexts = [ChatContext]() for await context in group { - if let context = context { - contexts.append(context) - } + contexts.append(context) } return contexts } + + let separator = String(repeating: "=", count: 32) // only 1 token + + let contextPrompts = contexts + .flatMap(\.systemPrompt) + .filter { !$0.content.isEmpty } + .sorted { $0.priority > $1.priority } + let contextualSystemPrompt = """ \(language.isEmpty ? "" : "You must always reply in \(language)") \(systemPrompt) - \(contexts.map(\.systemPrompt).filter { !$0.isEmpty }.joined(separator: "\n\n")) + Below are information related to the conversation, separated by \(separator) + + \(contextPrompts.map(\.content).joined(separator: "\n\(separator)\n")) """ await memory.mutateSystemPrompt(contextualSystemPrompt) functionProvider.append(functions: contexts.flatMap(\.functions)) diff --git a/Pro b/Pro index ba9117be..9b5986f3 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit ba9117be02ca4e73508639f917e3781d28e5b94a +Subproject commit 9b5986f3a300cea117c3d80abbd4e0dd3927332d diff --git a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift index b0d4b45f..69d356b7 100644 --- a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift @@ -2,12 +2,68 @@ import Foundation import OpenAIService public struct ChatContext { - public var systemPrompt: String + public struct RetrievedPrompt { + public enum Priority: Equatable, Comparable { + case low + case medium + case high + case custom(Int) + + public var rawValue: Int { + switch self { + case .low: + return 20 + case .medium: + return 60 + case .high: + return 80 + case let .custom(value): + return value + } + } + + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.rawValue < rhs.rawValue + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.rawValue == rhs.rawValue + } + } + + public var content: String + public var priority: Priority + + public init(content: String, priority: Priority) { + self.content = content + self.priority = priority + } + } + + public var systemPrompt: [RetrievedPrompt] public var functions: [any ChatGPTFunction] - public init(systemPrompt: String, functions: [any ChatGPTFunction]) { + public init(systemPrompt: [RetrievedPrompt], functions: [any ChatGPTFunction]) { self.systemPrompt = systemPrompt self.functions = functions } + + public static var empty: Self { + .init(systemPrompt: [], functions: []) + } +} + +public func + ( + lhs: ChatContext.RetrievedPrompt.Priority, + rhs: Int +) -> ChatContext.RetrievedPrompt.Priority { + .custom(lhs.rawValue + rhs) +} + +public func - ( + lhs: ChatContext.RetrievedPrompt.Priority, + rhs: Int +) -> ChatContext.RetrievedPrompt.Priority { + .custom(lhs.rawValue - rhs) } public protocol ChatContextCollector { @@ -16,6 +72,6 @@ public protocol ChatContextCollector { scopes: Set, content: String, configuration: ChatGPTConfiguration - ) async -> ChatContext? + ) async -> ChatContext } From 81f723c9c2927a1413ff7e72b98188eb474aaf48 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 6 Oct 2023 15:00:44 +0800 Subject: [PATCH 19/37] Update AutoManagedChatGPTMemory to handle retrieved content --- .../DynamicContextController.swift | 7 +- .../Memory/AutoManagedChatGPTMemory.swift | 100 ++++++++++++++---- 2 files changed, 81 insertions(+), 26 deletions(-) diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index e5ccb34b..fa6ab549 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -66,8 +66,6 @@ final class DynamicContextController { return contexts } - let separator = String(repeating: "=", count: 32) // only 1 token - let contextPrompts = contexts .flatMap(\.systemPrompt) .filter { !$0.content.isEmpty } @@ -76,12 +74,9 @@ final class DynamicContextController { let contextualSystemPrompt = """ \(language.isEmpty ? "" : "You must always reply in \(language)") \(systemPrompt) - - Below are information related to the conversation, separated by \(separator) - - \(contextPrompts.map(\.content).joined(separator: "\n\(separator)\n")) """ await memory.mutateSystemPrompt(contextualSystemPrompt) + await memory.mutateRetrievedContent(contextPrompts.map(\.content)) functionProvider.append(functions: contexts.flatMap(\.functions)) } } diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index 6385b565..2066a6ea 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -8,7 +8,8 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { public private(set) var messages: [ChatMessage] = [] public private(set) var remainingTokens: Int? - public var systemPrompt: ChatMessage + public var systemPrompt: String + public var retrievedContent: [String] = [] public var history: [ChatMessage] = [] { didSet { onHistoryChange() } } @@ -25,7 +26,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { configuration: ChatGPTConfiguration, functionProvider: ChatGPTFunctionProvider ) { - self.systemPrompt = .init(role: .system, content: systemPrompt) + self.systemPrompt = systemPrompt self.configuration = configuration self.functionProvider = functionProvider _ = Self.encoder // force pre-initialize @@ -36,7 +37,11 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { } public func mutateSystemPrompt(_ newPrompt: String) { - systemPrompt.content = newPrompt + systemPrompt = newPrompt + } + + public func mutateRetrievedContent(_ newContent: [String]) { + retrievedContent = newContent } public nonisolated @@ -52,6 +57,17 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { } /// https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb + /// + /// Format: + /// ``` + /// [System Prompt] priority: high + /// [Retrieved Content] priority: low + /// [Retrieved Content A] + /// + /// [Retrieved Content B] + /// [Functions] priority: high + /// [Message History] priority: medium + /// ``` func generateSendingHistory( maxNumberOfMessages: Int = UserDefaults.shared.value(for: \.chatGPTMaxMessageCount), encoder: TokenEncoder = AutoManagedChatGPTMemory.encoder @@ -63,8 +79,8 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { return count } - var all: [ChatMessage] = [] - let systemMessageTokenCount = countToken(&systemPrompt) + var smallestSystemPromptMessage = ChatMessage(role: .system, content: systemPrompt) + let smallestSystemMessageTokenCount = countToken(&smallestSystemPromptMessage) let functionTokenCount = functionProvider.functions.reduce(into: 0) { partial, function in var count = encoder.countToken(text: function.name) + encoder.countToken(text: function.description) @@ -75,38 +91,82 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { } partial += count } - var allTokensCount = functionTokenCount + - 3 // every reply is primed with <|start|>assistant<|message|> - allTokensCount += systemPrompt.isEmpty ? 0 : systemMessageTokenCount + let mandatoryContentTokensCount = smallestSystemMessageTokenCount + + functionTokenCount + + 3 // every reply is primed with <|start|>assistant<|message|> + + /// the available tokens count for other messages and retrieved content + let availableTokenCountForMessages = configuration.maxTokens + - configuration.minimumReplyTokens + - mandatoryContentTokensCount + + var messageTokenCount = 0 + var allMessages: [ChatMessage] = [] for (index, message) in history.enumerated().reversed() { - if maxNumberOfMessages > 0, all.count >= maxNumberOfMessages { break } + if maxNumberOfMessages > 0, allMessages.count >= maxNumberOfMessages { break } if message.isEmpty { continue } let tokensCount = countToken(&history[index]) - if tokensCount + allTokensCount > - configuration.maxTokens - configuration.minimumReplyTokens - { - break + if tokensCount + messageTokenCount > availableTokenCountForMessages { break } + messageTokenCount += tokensCount + allMessages.append(message) + } + + /// the available tokens count for retrieved content + let availableTokenCountForRetrievedContent = availableTokenCountForMessages + - messageTokenCount + var retrievedContentTokenCount = 0 + + let separator = String(repeating: "=", count: 32) // only 1 token + + var systemPrompt = systemPrompt + + func appendToSystemPrompt(_ text: String) -> Bool { + let tokensCount = encoder.countToken(text: text) + if tokensCount + retrievedContentTokenCount > + availableTokenCountForRetrievedContent { return false } + retrievedContentTokenCount += tokensCount + systemPrompt += text + return true + } + + for (index, content) in retrievedContent.filter({ !$0.isEmpty }).enumerated() { + if index == 0 { + if !appendToSystemPrompt(""" + + Below are information related to the conversation, separated by \(separator) + + """) { break } + } else { + if !appendToSystemPrompt(separator) { break } } - allTokensCount += tokensCount - all.append(message) + + if !appendToSystemPrompt(content) { break } } if !systemPrompt.isEmpty { - all.append(systemPrompt) + let message = ChatMessage(role: .system, content: systemPrompt) + allMessages.append(message) } #if DEBUG Logger.service.info(""" Sending tokens count - - system prompt: \(systemMessageTokenCount) + - system prompt: \(smallestSystemPromptMessage) - functions: \(functionTokenCount) - - total: \(allTokensCount) - + - messages: \(messageTokenCount) + - retrieved content: \(retrievedContentTokenCount) + - total: \( + smallestSystemMessageTokenCount + + functionTokenCount + + messageTokenCount + + retrievedContentTokenCount + ) + """) #endif - return all.reversed() + return allMessages.reversed() } func generateRemainingTokens( From 60f4ff596443211406ae55df033d8d03061bb348 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 6 Oct 2023 15:24:20 +0800 Subject: [PATCH 20/37] Adjust ChatContext --- .../ActiveDocumentChatContextCollector.swift | 6 +++-- ...cyActiveDocumentChatContextCollector.swift | 3 ++- .../SystemInfoChatContextCollector.swift | 8 ++----- .../WebChatContextCollector.swift | 8 ++----- .../DynamicContextController.swift | 9 +++++-- Pro | 2 +- .../ChatContextCollector.swift | 24 ++++++++++++------- 7 files changed, 33 insertions(+), 27 deletions(-) diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index 0fd68c2a..be28668e 100644 --- a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -27,7 +27,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { var removedCode = context removedCode.focusedContext = nil return .init( - systemPrompt: [ + systemPrompt: "", + retrievedContent: [ .init( content: extractSystemPrompt(removedCode), priority: .high @@ -70,7 +71,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { } return .init( - systemPrompt: [ + systemPrompt: "", + retrievedContent: [ .init(content: extractSystemPrompt(context), priority: .high) ], functions: functions diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift index 6629ab35..44387bbb 100644 --- a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift @@ -78,7 +78,8 @@ public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { }() return .init( - systemPrompt: [ + systemPrompt: "", + retrievedContent: [ .init(content: """ Active Document Context:### Document Relative Path: \(relativePath) diff --git a/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift b/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift index 061af519..795d14e0 100644 --- a/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift @@ -18,16 +18,12 @@ public final class SystemInfoChatContextCollector: ChatContextCollector { configuration: ChatGPTConfiguration ) -> ChatContext { return .init( - systemPrompt: [ - .init( - content: """ + systemPrompt: """ Current Time: \( Self.dateFormatter.string(from: Date()) ) (You can use it to calculate time in another time zone) """, - priority: .custom(999999) - ), - ], + retrievedContent: [], functions: [] ) } diff --git a/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift b/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift index 91b6b5bc..81d1b9fc 100644 --- a/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift @@ -21,12 +21,8 @@ public final class WebChatContextCollector: ChatContextCollector { links.isEmpty ? nil : QueryWebsiteFunction(), ] return .init( - systemPrompt: [ - .init( - content: "You prefer to answer questions with latest content on the internet.", - priority: .low - ), - ], + systemPrompt: "You prefer to answer questions with latest content on the internet.", + retrievedContent: [], functions: functions.compactMap { $0 } ) } diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index fa6ab549..17c876b8 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -66,14 +66,19 @@ final class DynamicContextController { return contexts } + let extraSystemPrompt = contexts + .map(\.systemPrompt) + .filter { !$0.isEmpty } + .joined(separator: "\n") + let contextPrompts = contexts - .flatMap(\.systemPrompt) + .flatMap(\.retrievedContent) .filter { !$0.content.isEmpty } .sorted { $0.priority > $1.priority } let contextualSystemPrompt = """ \(language.isEmpty ? "" : "You must always reply in \(language)") - \(systemPrompt) + \(systemPrompt)\(extraSystemPrompt.isEmpty ? "" : "\n\(extraSystemPrompt)") """ await memory.mutateSystemPrompt(contextualSystemPrompt) await memory.mutateRetrievedContent(contextPrompts.map(\.content)) diff --git a/Pro b/Pro index 9b5986f3..8dc92443 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 9b5986f3a300cea117c3d80abbd4e0dd3927332d +Subproject commit 8dc92443efa0cd53b8e120b93af18f04793c0483 diff --git a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift index 69d356b7..0c1c7b34 100644 --- a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift @@ -2,7 +2,7 @@ import Foundation import OpenAIService public struct ChatContext { - public struct RetrievedPrompt { + public struct RetrievedContent { public enum Priority: Equatable, Comparable { case low case medium @@ -40,29 +40,35 @@ public struct ChatContext { } } - public var systemPrompt: [RetrievedPrompt] + public var systemPrompt: String + public var retrievedContent: [RetrievedContent] public var functions: [any ChatGPTFunction] - public init(systemPrompt: [RetrievedPrompt], functions: [any ChatGPTFunction]) { + public init( + systemPrompt: String, + retrievedContent: [RetrievedContent], + functions: [any ChatGPTFunction] + ) { self.systemPrompt = systemPrompt + self.retrievedContent = retrievedContent self.functions = functions } - + public static var empty: Self { - .init(systemPrompt: [], functions: []) + .init(systemPrompt: "", retrievedContent: [], functions: []) } } public func + ( - lhs: ChatContext.RetrievedPrompt.Priority, + lhs: ChatContext.RetrievedContent.Priority, rhs: Int -) -> ChatContext.RetrievedPrompt.Priority { +) -> ChatContext.RetrievedContent.Priority { .custom(lhs.rawValue + rhs) } public func - ( - lhs: ChatContext.RetrievedPrompt.Priority, + lhs: ChatContext.RetrievedContent.Priority, rhs: Int -) -> ChatContext.RetrievedPrompt.Priority { +) -> ChatContext.RetrievedContent.Priority { .custom(lhs.rawValue - rhs) } From e2dea79ec7676c6e4ec31ee0fc00434ab55a64ad Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 9 Oct 2023 22:50:16 +0800 Subject: [PATCH 21/37] Move ActiveDocumentChatContextCollector to Tool --- Core/Package.swift | 18 -------- .../ChatService/AllContextCollector.swift | 1 - Pro | 2 +- Tool/Package.swift | 42 ++++++++++++++----- .../ChatContextCollector.swift | 12 ++++-- .../ActiveDocumentChatContextCollector.swift | 2 +- .../Functions/ExpandFocusRangeFunction.swift | 0 .../MoveToCodeAroundLineFunction.swift | 0 .../Functions/MoveToFocusedCodeFunction.swift | 0 .../GetEditorInfo.swift | 0 ...cyActiveDocumentChatContextCollector.swift | 0 .../ReadableCursorRange.swift | 0 .../SwiftFocusedCodeFinderTests.swift | 0 ...nknownLanguageFocusedCodeFinderTests.swift | 0 14 files changed, 43 insertions(+), 34 deletions(-) rename {Core => Tool}/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift (99%) rename {Core => Tool}/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift (100%) rename {Core => Tool}/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift (100%) rename {Core => Tool}/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift (100%) rename {Core => Tool}/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/GetEditorInfo.swift (100%) rename {Core => Tool}/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift (100%) rename {Core => Tool}/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ReadableCursorRange.swift (100%) rename {Core => Tool}/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift (100%) rename {Core => Tool}/Tests/ActiveDocumentChatContextCollectorTests/UnknownLanguageFocusedCodeFinderTests.swift (100%) diff --git a/Core/Package.swift b/Core/Package.swift index aa895eee..c5b6db41 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -210,7 +210,6 @@ let package = Package( // context collectors "WebChatContextCollector", - "ActiveDocumentChatContextCollector", "SystemInfoChatContextCollector", .product(name: "ChatContextCollector", package: "Tool"), @@ -354,23 +353,6 @@ let package = Package( ], path: "Sources/ChatContextCollectors/SystemInfoChatContextCollector" ), - - .target( - name: "ActiveDocumentChatContextCollector", - dependencies: [ - .product(name: "ChatContextCollector", package: "Tool"), - .product(name: "OpenAIService", package: "Tool"), - .product(name: "Preferences", package: "Tool"), - .product(name: "FocusedCodeFinder", package: "Tool"), - .product(name: "AppMonitoring", package: "Tool"), - ], - path: "Sources/ChatContextCollectors/ActiveDocumentChatContextCollector" - ), - - .testTarget( - name: "ActiveDocumentChatContextCollectorTests", - dependencies: ["ActiveDocumentChatContextCollector"] - ), ] ) diff --git a/Core/Sources/ChatService/AllContextCollector.swift b/Core/Sources/ChatService/AllContextCollector.swift index ec365e9d..c13aa612 100644 --- a/Core/Sources/ChatService/AllContextCollector.swift +++ b/Core/Sources/ChatService/AllContextCollector.swift @@ -6,7 +6,6 @@ import WebChatContextCollector import ProChatContextCollectors let allContextCollectors: [any ChatContextCollector] = [ SystemInfoChatContextCollector(), - ActiveDocumentChatContextCollector(), WebChatContextCollector(), ProChatContextCollectors(), ] diff --git a/Pro b/Pro index 8dc92443..52befe02 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 8dc92443efa0cd53b8e120b93af18f04793c0483 +Subproject commit 52befe02afe733da1be8cf6072f118da04fa8035 diff --git a/Tool/Package.swift b/Tool/Package.swift index cbbe1a9e..d8af318b 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -15,7 +15,10 @@ let package = Package( .library(name: "Logger", targets: ["Logger"]), .library(name: "OpenAIService", targets: ["OpenAIService"]), .library(name: "ChatTab", targets: ["ChatTab"]), - .library(name: "ChatContextCollector", targets: ["ChatContextCollector"]), + .library( + name: "ChatContextCollector", + targets: ["ChatContextCollector", "ActiveDocumentChatContextCollector"] + ), .library(name: "Environment", targets: ["Environment"]), .library(name: "SuggestionModel", targets: ["SuggestionModel"]), .library(name: "ASTParser", targets: ["ASTParser"]), @@ -70,7 +73,7 @@ let package = Package( // MARK: - Helpers .target(name: "XPCShared", dependencies: ["SuggestionModel"]), - + .target(name: "Configs"), .target(name: "Preferences", dependencies: ["Configs", "AIModel"]), @@ -234,14 +237,6 @@ let package = Package( ] ), - .target( - name: "ChatContextCollector", - dependencies: [ - "SuggestionModel", - "OpenAIService", - ] - ), - .target(name: "BingSearchService"), .target(name: "SuggestionService", dependencies: [ @@ -310,6 +305,33 @@ let package = Package( )] ), + // MARK: - Chat Context Collector + + .target( + name: "ChatContextCollector", + dependencies: [ + "SuggestionModel", + "OpenAIService", + ] + ), + + .target( + name: "ActiveDocumentChatContextCollector", + dependencies: [ + "ChatContextCollector", + "OpenAIService", + "Preferences", + "FocusedCodeFinder", + "XcodeInspector", + ], + path: "Sources/ChatContextCollectors/ActiveDocumentChatContextCollector" + ), + + .testTarget( + name: "ActiveDocumentChatContextCollectorTests", + dependencies: ["ActiveDocumentChatContextCollector"] + ), + // MARK: - Tests .testTarget( diff --git a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift index 0c1c7b34..8ab52f06 100644 --- a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift @@ -4,19 +4,25 @@ import OpenAIService public struct ChatContext { public struct RetrievedContent { public enum Priority: Equatable, Comparable { + case bottom case low case medium case high + case top case custom(Int) public var rawValue: Int { switch self { + case .bottom: + return 0 case .low: - return 20 + return 400 case .medium: - return 60 + return 600 case .high: - return 80 + return 800 + case .top: + return 1_000_000_000 case let .custom(value): return value } diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift similarity index 99% rename from Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift rename to Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index be28668e..52c79015 100644 --- a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -10,7 +10,7 @@ import XcodeInspector public final class ActiveDocumentChatContextCollector: ChatContextCollector { public init() {} - var activeDocumentContext: ActiveDocumentContext? + public var activeDocumentContext: ActiveDocumentContext? public func generateContext( history: [ChatMessage], diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift similarity index 100% rename from Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift rename to Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/ExpandFocusRangeFunction.swift diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift similarity index 100% rename from Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift rename to Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToCodeAroundLineFunction.swift diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift similarity index 100% rename from Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift rename to Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/Functions/MoveToFocusedCodeFunction.swift diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/GetEditorInfo.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/GetEditorInfo.swift similarity index 100% rename from Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/GetEditorInfo.swift rename to Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/GetEditorInfo.swift diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift similarity index 100% rename from Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift rename to Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift diff --git a/Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ReadableCursorRange.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ReadableCursorRange.swift similarity index 100% rename from Core/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ReadableCursorRange.swift rename to Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ReadableCursorRange.swift diff --git a/Core/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift b/Tool/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift similarity index 100% rename from Core/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift rename to Tool/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift diff --git a/Core/Tests/ActiveDocumentChatContextCollectorTests/UnknownLanguageFocusedCodeFinderTests.swift b/Tool/Tests/ActiveDocumentChatContextCollectorTests/UnknownLanguageFocusedCodeFinderTests.swift similarity index 100% rename from Core/Tests/ActiveDocumentChatContextCollectorTests/UnknownLanguageFocusedCodeFinderTests.swift rename to Tool/Tests/ActiveDocumentChatContextCollectorTests/UnknownLanguageFocusedCodeFinderTests.swift From fd5c4aa30f24dfe7b1868f44fbf8a573753b106f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 10 Oct 2023 00:10:18 +0800 Subject: [PATCH 22/37] Update --- .../Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift index 0c2ea908..52c4181b 100644 --- a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift @@ -1,9 +1,9 @@ import ASTParser import Foundation +import Preferences import SuggestionModel import SwiftParser import SwiftSyntax -import Preferences public struct SwiftFocusedCodeFinder: FocusedCodeFinder { public let maxFocusedCodeLineCount: Int @@ -61,7 +61,10 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinder { } } guard let focusedNode else { - var result = UnknownLanguageFocusedCodeFinder(proposedSearchRange: 5) + var result = + UnknownLanguageFocusedCodeFinder( + proposedSearchRange: maxFocusedCodeLineCount / 2 + ) .findFocusedCode( containingRange: range, activeDocumentContext: activeDocumentContext From acf14f102bca58f4f647339b210521d2dc4aac45 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 10 Oct 2023 16:02:24 +0800 Subject: [PATCH 23/37] Adjust interface --- Pro | 2 +- .../ChatContextCollector.swift | 52 +------------------ .../ActiveDocumentChatContextCollector.swift | 15 ++---- ...cyActiveDocumentChatContextCollector.swift | 8 ++- 4 files changed, 10 insertions(+), 67 deletions(-) diff --git a/Pro b/Pro index 52befe02..e959ffcb 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 52befe02afe733da1be8cf6072f118da04fa8035 +Subproject commit e959ffcb6fd2ed105bfc6fda01d4e0c86b6fbacf diff --git a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift index 8ab52f06..05a06da6 100644 --- a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift @@ -3,44 +3,10 @@ import OpenAIService public struct ChatContext { public struct RetrievedContent { - public enum Priority: Equatable, Comparable { - case bottom - case low - case medium - case high - case top - case custom(Int) - - public var rawValue: Int { - switch self { - case .bottom: - return 0 - case .low: - return 400 - case .medium: - return 600 - case .high: - return 800 - case .top: - return 1_000_000_000 - case let .custom(value): - return value - } - } - - public static func < (lhs: Self, rhs: Self) -> Bool { - lhs.rawValue < rhs.rawValue - } - - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.rawValue == rhs.rawValue - } - } - public var content: String - public var priority: Priority + public var priority: Int - public init(content: String, priority: Priority) { + public init(content: String, priority: Int) { self.content = content self.priority = priority } @@ -64,20 +30,6 @@ public struct ChatContext { } } -public func + ( - lhs: ChatContext.RetrievedContent.Priority, - rhs: Int -) -> ChatContext.RetrievedContent.Priority { - .custom(lhs.rawValue + rhs) -} - -public func - ( - lhs: ChatContext.RetrievedContent.Priority, - rhs: Int -) -> ChatContext.RetrievedContent.Priority { - .custom(lhs.rawValue - rhs) -} - public protocol ChatContextCollector { func generateContext( history: [ChatMessage], diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index 52c79015..bf297931 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -27,13 +27,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { var removedCode = context removedCode.focusedContext = nil return .init( - systemPrompt: "", - retrievedContent: [ - .init( - content: extractSystemPrompt(removedCode), - priority: .high - ), - ], + systemPrompt: extractSystemPrompt(removedCode), + retrievedContent: [], functions: [] ) } @@ -71,10 +66,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { } return .init( - systemPrompt: "", - retrievedContent: [ - .init(content: extractSystemPrompt(context), priority: .high) - ], + systemPrompt: extractSystemPrompt(context), + retrievedContent: [], functions: functions ) } diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift index 44387bbb..45cc414c 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift @@ -78,9 +78,7 @@ public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { }() return .init( - systemPrompt: "", - retrievedContent: [ - .init(content: """ + systemPrompt: """ Active Document Context:### Document Relative Path: \(relativePath) Selection Range Start: \ @@ -100,8 +98,8 @@ public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { .joined(separator: "\n") ?? "N/A" ) ### - """, priority: .high), - ], + """, + retrievedContent: [], functions: [] ) } From 8f7f16a65a2bb25af081ced5d45c6ad1296a49d4 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 15:07:55 +0800 Subject: [PATCH 24/37] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index e959ffcb..86de017f 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit e959ffcb6fd2ed105bfc6fda01d4e0c86b6fbacf +Subproject commit 86de017f85a6c55e1114ac2c134a305c28ed43be From b088572e0ae3482eb37afba0c8dfcce158fc52c2 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 15:08:28 +0800 Subject: [PATCH 25/37] Add hard limit for retrieved content to use no more than half of max tokens --- .../Memory/AutoManagedChatGPTMemory.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift index 2066a6ea..8462c5ac 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -113,8 +113,10 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { } /// the available tokens count for retrieved content - let availableTokenCountForRetrievedContent = availableTokenCountForMessages - - messageTokenCount + let availableTokenCountForRetrievedContent = min( + availableTokenCountForMessages - messageTokenCount, + configuration.maxTokens / 2 + ) var retrievedContentTokenCount = 0 let separator = String(repeating: "=", count: 32) // only 1 token @@ -138,7 +140,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { """) { break } } else { - if !appendToSystemPrompt(separator) { break } + if !appendToSystemPrompt("\n\(separator)\n") { break } } if !appendToSystemPrompt(content) { break } @@ -152,7 +154,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { #if DEBUG Logger.service.info(""" Sending tokens count - - system prompt: \(smallestSystemPromptMessage) + - system prompt: \(smallestSystemMessageTokenCount) - functions: \(functionTokenCount) - messages: \(messageTokenCount) - retrieved content: \(retrievedContentTokenCount) @@ -162,7 +164,6 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { + messageTokenCount + retrievedContentTokenCount ) - """) #endif From 85e6cf838550e2e296e37a08e0275707f909ddd1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 15:10:56 +0800 Subject: [PATCH 26/37] Adjust chat panel --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 38 ++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index e8efcf41..c7a9d482 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -58,7 +58,7 @@ struct ChatPanelMessages: View { ChatHistory(chat: chat) .listItemTint(.clear) - + WithViewStore(chat, observe: \.isReceivingMessage) { viewStore in if viewStore.state { Spacer(minLength: 12) @@ -231,35 +231,35 @@ private struct Instruction: View { Group { Markdown( """ - You can use scopes to give the bot extra abilities. + You can use plugins to perform various tasks. - | Scope Name | Abilities | + | Plugin Name | Description | | --- | --- | - | `@file` | Read the metadata of the editing file | - | `@code` | Read the code and metadata in the editing file | - | `@web` (beta) | Search on Bing or query from a web page | - | `@project` | Experimental. Access content of the project | - - To use scopes, you can prefix a message with `@code`. + | `/run` | Runs a command under the project root | + | `/math` | Solves a math problem in natural language | + | `/search` | Searches on Bing and summarizes the results | + | `/shortcut(name)` | Runs a shortcut from the Shortcuts.app, with the previous message as input | + | `/shortcutInput(name)` | Runs a shortcut and uses its result as a new message | - You can use shorthand to represent a scope, such as `@c`, and enable multiple scopes with `@c+web`. + To use plugins, you can prefix a message with `/pluginName`. """ ) .modifier(InstructionModifier()) Markdown( """ - You can use plugins to perform various tasks. + You can use scopes to give the bot extra abilities. - | Plugin Name | Description | + | Scope Name | Abilities | | --- | --- | - | `/run` | Runs a command under the project root | - | `/math` | Solves a math problem in natural language | - | `/search` | Searches on Bing and summarizes the results | - | `/shortcut(name)` | Runs a shortcut from the Shortcuts.app, with the previous message as input | - | `/shortcutInput(name)` | Runs a shortcut and uses its result as a new message | + | `@file` | Read the metadata of the editing file | + | `@code` | Read the code and metadata in the editing file | + | `@web` (beta) | Search on Bing or query from a web page | + | `@project` | Experimental. Access content of the project | - To use plugins, you can prefix a message with `/pluginName`. + To use scopes, you can prefix a message with `@code`. + + You can use shorthand to represent a scope, such as `@c`, and enable multiple scopes with `@c+web`. """ ) .modifier(InstructionModifier()) @@ -537,7 +537,7 @@ struct ChatPanelInputArea: View { let availableFeatures = plugins + [ "/exit", "@code", - "@file", + "@project", "@web", ] From d50642908539faef4d195662a3a274e90f377153 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 15:15:57 +0800 Subject: [PATCH 27/37] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 86de017f..9e61bb2b 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 86de017f85a6c55e1114ac2c134a305c28ed43be +Subproject commit 9e61bb2b24cbc4210d5433e79d8268fc6d581e01 From 3c6648048fac8553d3ed04f08b01dc7160293cfe Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 15:17:57 +0800 Subject: [PATCH 28/37] Make 2 toggles default to on --- Tool/Sources/Preferences/Keys.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index 37d9344d..276f1e3d 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -173,7 +173,7 @@ public extension UserDefaultPreferenceKeys { } var gitHubCopilotIgnoreTrailingNewLines: PreferenceKey { - .init(defaultValue: false, key: "GitHubCopilotIgnoreTrailingNewLines") + .init(defaultValue: true, key: "GitHubCopilotIgnoreTrailingNewLines") } } @@ -318,7 +318,7 @@ public extension UserDefaultPreferenceKeys { } var acceptSuggestionWithTab: PreferenceKey { - .init(defaultValue: false, key: "AcceptSuggestionWithTab") + .init(defaultValue: true, key: "AcceptSuggestionWithTab") } } From 4f51c848954efa84386d3c1fd5636802ef6176ed Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 15:18:51 +0800 Subject: [PATCH 29/37] Update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index a8b6fd3c..bdeb59bb 100644 --- a/LICENSE +++ b/LICENSE @@ -9,7 +9,7 @@ a. may be subject to a more permissive open-source license in the future. b. can be used for commercial purposes. With the GPLv3 and these supplementary agreements, anyone can freely use, modify, and distribute the project, provided that: -- For commercial use or commercial forks of this project, please contact us for authorization. +- For commercial redistribution or commercial forks of this project, please contact us for authorization. Copyright (c) 2023 Shangxin Guo From 00ff5457e4d2809f644c5aa6cdac87e214b7b85f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 15:25:27 +0800 Subject: [PATCH 30/37] Adjust UI --- Core/Sources/SuggestionWidget/ChatWindowView.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Core/Sources/SuggestionWidget/ChatWindowView.swift b/Core/Sources/SuggestionWidget/ChatWindowView.swift index 370bccab..f82d7a89 100644 --- a/Core/Sources/SuggestionWidget/ChatWindowView.swift +++ b/Core/Sources/SuggestionWidget/ChatWindowView.swift @@ -66,9 +66,7 @@ struct ChatTitleBar: View { : Color(nsColor: .disabledControlTextColor) ) .frame(width: 10, height: 10) - .overlay { - Circle().strokeBorder(.black.opacity(0.3), lineWidth: 1) - } + .shadow(radius: 0.5) .overlay { if isHovering { Image(systemName: "minus") @@ -91,9 +89,7 @@ struct ChatTitleBar: View { : Color(nsColor: .disabledControlTextColor) ) .frame(width: 10, height: 10) - .overlay { - Circle().strokeBorder(.black.opacity(0.3), lineWidth: 1) - } + .shadow(radius: 0.5) .disabled(!viewStore.state) .overlay { if isHovering { From a00dd49edd059b30b805f25694884a5e9aefcf39 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 15:26:31 +0800 Subject: [PATCH 31/37] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 9e61bb2b..02010b2d 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 9e61bb2b24cbc4210d5433e79d8268fc6d581e01 +Subproject commit 02010b2d8142d5880fba84352b81d2ad355a5725 From c1edf6df0f1d8905994f38caa497b15d28918e96 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 22:21:41 +0800 Subject: [PATCH 32/37] Adjust minimum reply token count to avoid cut off when RAG is used --- .../Configuration/UserPreferenceChatGPTConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift index 4b83895b..81fc28a1 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift @@ -33,7 +33,7 @@ public struct UserPreferenceChatGPTConfiguration: ChatGPTConfiguration { } public var minimumReplyTokens: Int { - 300 + maxTokens / 5 } public var runFunctionsAutomatically: Bool { From e2adfe1e222751aea51c0c61e5342dc2d878318c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 23:44:56 +0800 Subject: [PATCH 33/37] Update --- Core/Package.resolved | 18 ------------------ Pro | 2 +- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/Core/Package.resolved b/Core/Package.resolved index 020dcdc9..47ef43c2 100644 --- a/Core/Package.resolved +++ b/Core/Package.resolved @@ -1,14 +1,5 @@ { "pins" : [ - { - "identity" : "cgeventoverride", - "kind" : "remoteSourceControl", - "location" : "https://github.com/intitni/CGEventOverride", - "state" : { - "revision" : "ae83f8ef1de2ad4c1a1473a0453b9675281d0d2c", - "version" : "1.2.1" - } - }, { "identity" : "codablewrappers", "kind" : "remoteSourceControl", @@ -54,15 +45,6 @@ "revision" : "4ffbb1b0b721378263297cafea6f2838044eb1eb" } }, - { - "identity" : "indexstore-db", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/indexstore-db.git", - "state" : { - "branch" : "release/5.9", - "revision" : "89ec16c2ac1bb271614e734a2ee792224809eb20" - } - }, { "identity" : "jsonrpc", "kind" : "remoteSourceControl", diff --git a/Pro b/Pro index 02010b2d..72fd9411 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 02010b2d8142d5880fba84352b81d2ad355a5725 +Subproject commit 72fd9411fb566b45e8dbb2df418cb5ef7a99d5cd From 05e22418b260db829c0647d79e0c30064332f570 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 23:45:05 +0800 Subject: [PATCH 34/37] Update TestPlan --- TestPlan.xctestplan | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/TestPlan.xctestplan b/TestPlan.xctestplan index 77034ff3..15a5c4cf 100644 --- a/TestPlan.xctestplan +++ b/TestPlan.xctestplan @@ -50,13 +50,6 @@ "name" : "PromptToCodeServiceTests" } }, - { - "target" : { - "containerPath" : "container:Core", - "identifier" : "GitHubCopilotServiceTests", - "name" : "GitHubCopilotServiceTests" - } - }, { "target" : { "containerPath" : "container:Tool", @@ -99,13 +92,6 @@ "name" : "SharedUIComponentsTests" } }, - { - "target" : { - "containerPath" : "container:Core", - "identifier" : "ActiveDocumentChatContextCollectorTests", - "name" : "ActiveDocumentChatContextCollectorTests" - } - }, { "target" : { "containerPath" : "container:Tool", @@ -126,6 +112,20 @@ "identifier" : "KeychainTests", "name" : "KeychainTests" } + }, + { + "target" : { + "containerPath" : "container:Tool", + "identifier" : "ActiveDocumentChatContextCollectorTests", + "name" : "ActiveDocumentChatContextCollectorTests" + } + }, + { + "target" : { + "containerPath" : "container:Tool", + "identifier" : "GitHubCopilotServiceTests", + "name" : "GitHubCopilotServiceTests" + } } ], "version" : 1 From f59100e5ad3e7e6e055c12867b829a2867cebe36 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 23:49:24 +0800 Subject: [PATCH 35/37] Update --- Core/Package.swift | 11 +++++++---- .../GUI/GraphicalUserInterfaceController.swift | 8 +++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Core/Package.swift b/Core/Package.swift index c5b6db41..e3f59806 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -38,12 +38,15 @@ let isProIncluded: Bool = { return false } do { - let content = String( + if let content = String( data: try Data(contentsOf: confURL), encoding: .utf8 - ) - print("") - return content?.hasPrefix("YES") ?? false + ) { + if content.hasPrefix("YES") { + return true + } + } + return false } catch { return false } diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index 343b48f0..bad5aca2 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -65,7 +65,7 @@ struct GUI: ReducerProtocol { } @Dependency(\.chatTabPool) var chatTabPool: ChatTabPool - + public enum Debounce: Hashable { case updateChatTabOrder } @@ -224,11 +224,13 @@ struct GUI: ReducerProtocol { guard old.map(\.id) != new.map(\.id) else { return .none } + #if canImport(ChatTabPersistent) return .run { send in - #if canImport(ChatTabPersistent) await send(.persistent(.chatOrderChanged)) - #endif }.debounce(id: Debounce.updateChatTabOrder, for: 1, scheduler: DispatchQueue.main) + #else + return .none + #endif } } } From 28153968376f4699b5c91084d5d63e92ece4db6d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 23:49:48 +0800 Subject: [PATCH 36/37] Bump version to 0.25.0 --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index 36b91970..35911ea7 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.24.1 -APP_BUILD = 251 +APP_VERSION = 0.25.0 +APP_BUILD = 261 From 2627b41a513ee36ff7cd73373e272e5917f5bb97 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 11 Oct 2023 23:52:59 +0800 Subject: [PATCH 37/37] Update appcast.xml --- appcast.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/appcast.xml b/appcast.xml index f75f5043..ae698149 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.25.0 + Wed, 11 Oct 2023 23:08:08 +0800 + 261 + 0.25.0 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.25.0 + + + + 0.24.1 Fri, 29 Sep 2023 14:35:35 +0800