From 62a6e85aaf84d789d6d6af6a40ef27cdaf5ceca8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sun, 22 Oct 2023 22:01:29 +0800 Subject: [PATCH 01/49] Disable hit test when a chat panel tab is not active --- Core/Sources/SuggestionWidget/ChatWindowView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/Sources/SuggestionWidget/ChatWindowView.swift b/Core/Sources/SuggestionWidget/ChatWindowView.swift index 955e73fa..9037cf51 100644 --- a/Core/Sources/SuggestionWidget/ChatWindowView.swift +++ b/Core/Sources/SuggestionWidget/ChatWindowView.swift @@ -377,6 +377,7 @@ struct ChatTabContainer: View { tab.body .opacity(isActive ? 1 : 0) .disabled(!isActive) + .allowsHitTesting(isActive) .frame(maxWidth: .infinity, maxHeight: .infinity) } else { EmptyView() From 27ad97cf3e4f8d0d34a8ead1a1071a8aa51aee55 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 01:27:30 +0800 Subject: [PATCH 02/49] Update --- Pro | 2 +- .../UserPreferenceChatGPTConfiguration.swift | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Pro b/Pro index 5feae55e..4b8d5f98 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 5feae55e1e9cb9d60166d9126419bf23c25a995d +Subproject commit 4b8d5f98f0a5ed673021a09e9a9973e372fa697d diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift index 81fc28a1..8d75df8a 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift @@ -54,6 +54,7 @@ public class OverridingChatGPTConfiguration: ChatGPTConfiguration { public var maxTokens: Int? public var minimumReplyTokens: Int? public var runFunctionsAutomatically: Bool? + public var apiKey: String? public init( temperature: Double? = nil, @@ -62,7 +63,8 @@ public class OverridingChatGPTConfiguration: ChatGPTConfiguration { stop: [String]? = nil, maxTokens: Int? = nil, minimumReplyTokens: Int? = nil, - runFunctionsAutomatically: Bool? = nil + runFunctionsAutomatically: Bool? = nil, + apiKey: String? = nil ) { self.temperature = temperature self.modelId = modelId @@ -71,6 +73,7 @@ public class OverridingChatGPTConfiguration: ChatGPTConfiguration { self.maxTokens = maxTokens self.minimumReplyTokens = minimumReplyTokens self.runFunctionsAutomatically = runFunctionsAutomatically + self.apiKey = apiKey } } @@ -113,5 +116,9 @@ public class OverridingChatGPTConfiguration: ChatGPTConfiguration { public var runFunctionsAutomatically: Bool { overriding.runFunctionsAutomatically ?? configuration.runFunctionsAutomatically } + + public var apiKey: String { + overriding.apiKey ?? configuration.apiKey + } } From ad9c0081a7b303ccff776c5de520b6a9fb615bd9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 01:41:42 +0800 Subject: [PATCH 03/49] Add new scope @sense --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 3 ++- Pro | 2 +- README.md | 12 +++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index 2b28793c..240499bc 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -255,8 +255,9 @@ private struct Instruction: View { | --- | --- | | `@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 | + | `@sense`| Experimental. Read the relevant information of the focused code | | `@project` | Experimental. Access content of the project | + | `@web` (beta) | Search on Bing or query from a web page | To use scopes, you can prefix a message with `@code`. diff --git a/Pro b/Pro index 4b8d5f98..8a609853 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 4b8d5f98f0a5ed673021a09e9a9973e372fa697d +Subproject commit 8a6098537db2eccb4fc422cc5088d9bf52a30d0d diff --git a/README.md b/README.md index cf82fa66..cd764b8d 100644 --- a/README.md +++ b/README.md @@ -249,11 +249,13 @@ You can detach the chat panel by simply dragging it away. Once detached, the cha The chat panel allows for chat scope to temporarily control the context of the conversation for the latest message. To use a scope, simply prefix the message with `@scope`. -| Scope | Description | -| :-----: | ---------------------------------------------------------------------------------------- | -| `@file` | Includes the metadata of the editing document and line annotations in the system prompt. | -| `@code` | Includes the focused/selected code and everything from `@file` in the system prompt. | -| `@web` | Allow the bot to search on Bing or query from a web page | +| Scope | Description | +| :--------: | ---------------------------------------------------------------------------------------- | +| `@file` | Includes the metadata of the editing document and line annotations in the system prompt. | +| `@code` | Includes the focused/selected code and everything from `@file` in the system prompt. | +| `@sense` | Experimental. Read the relevant information of the focused code | +| `@project` | Experimental. Access content of the project | +| `@web` | Allow the bot to search on Bing or query from a web page | `@code` is on by default, if `Use @code scope by default in chat context.` is on. Otherwise, `@file` will be on by default. From 5bdaf24cba46bb6b3d0b03fb5f08be35643864cf Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 01:44:47 +0800 Subject: [PATCH 04/49] Update instruction --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 2 +- Pro | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index 240499bc..474c4eb4 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -255,7 +255,7 @@ private struct Instruction: View { | --- | --- | | `@file` | Read the metadata of the editing file | | `@code` | Read the code and metadata in the editing file | - | `@sense`| Experimental. Read the relevant information of the focused code | + | `@sense`| Experimental. Read the relevant code of the focused editor | | `@project` | Experimental. Access content of the project | | `@web` (beta) | Search on Bing or query from a web page | diff --git a/Pro b/Pro index 8a609853..7fe0d037 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 8a6098537db2eccb4fc422cc5088d9bf52a30d0d +Subproject commit 7fe0d0372d964e2dab6fb526929c559b8e988a12 From ddbfece996bf2068e142470ea6f180d5fa6f91e2 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 10:04:30 +0800 Subject: [PATCH 05/49] Add settings for scopes --- .../FeatureSettings/ChatSettingsView.swift | 217 +++++++++++++++++- Pro | 2 +- Tool/Sources/Preferences/Keys.swift | 44 +++- .../SharedUIComponents/SettingsDivider.swift | 42 ++++ 4 files changed, 286 insertions(+), 19 deletions(-) create mode 100644 Tool/Sources/SharedUIComponents/SettingsDivider.swift diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index 7f903728..e8669f3e 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -1,6 +1,11 @@ import Preferences +import SharedUIComponents import SwiftUI +#if canImport(ProHostApp) +import ProHostApp +#endif + struct ChatSettingsView: View { class Settings: ObservableObject { static let availableLocalizedLocales = Locale.availableLocalizedLocales @@ -11,8 +16,6 @@ struct ChatSettingsView: View { @AppStorage(\.chatCodeFontSize) var chatCodeFontSize @AppStorage(\.maxFocusedCodeLineCount) var maxFocusedCodeLineCount - @AppStorage(\.useCodeScopeByDefaultInChatContext) - var useCodeScopeByDefaultInChatContext @AppStorage(\.defaultChatFeatureChatModelId) var defaultChatFeatureChatModelId @AppStorage(\.defaultChatSystemPrompt) var defaultChatSystemPrompt @AppStorage(\.chatSearchPluginMaxIterations) var chatSearchPluginMaxIterations @@ -32,12 +35,13 @@ struct ChatSettingsView: View { var body: some View { VStack { chatSettingsForm - Divider() + SettingsDivider("UI") uiForm - Divider() + SettingsDivider("Context") contextForm - Divider() + SettingsDivider("Plugin") pluginForm + ScopeForm() } } @@ -160,7 +164,7 @@ struct ChatSettingsView: View { Text("pt") } - + Toggle(isOn: $settings.wrapCodeInCodeBlock) { Text("Wrap code in code block") } @@ -170,10 +174,6 @@ struct ChatSettingsView: View { @ViewBuilder var contextForm: some View { Form { - Toggle(isOn: $settings.useCodeScopeByDefaultInChatContext) { - Text("Use @code scope by default in chat context.") - } - HStack { TextField(text: .init(get: { "\(Int(settings.maxFocusedCodeLineCount))" @@ -235,13 +235,206 @@ struct ChatSettingsView: View { ) } } + + struct ScopeForm: View { + class Settings: ObservableObject { + @AppStorage(\.enableFileScopeByDefaultInChatContext) + var enableFileScopeByDefaultInChatContext: Bool + @AppStorage(\.enableCodeScopeByDefaultInChatContext) + var enableCodeScopeByDefaultInChatContext: Bool + @AppStorage(\.enableSenseScopeByDefaultInChatContext) + var enableSenseScopeByDefaultInChatContext: Bool + @AppStorage(\.enableProjectScopeByDefaultInChatContext) + var enableProjectScopeByDefaultInChatContext: Bool + @AppStorage(\.enableWebScopeByDefaultInChatContext) + var enableWebScopeByDefaultInChatContext: Bool + @AppStorage(\.preferredChatModelIdForSenseScope) + var preferredChatModelIdForSenseScope: String + @AppStorage(\.preferredChatModelIdForProjectScope) + var preferredChatModelIdForProjectScope: String + @AppStorage(\.preferredChatModelIdForWebScope) + var preferredChatModelIdForWebScope: String + @AppStorage(\.chatModels) var chatModels + + init() {} + } + + @StateObject var settings = Settings() + + var body: some View { + VStack { + Scope( + title: Text("File Scope"), + description: "Enable the bot to read the metadata of the editing file." + ) { + Form { + Toggle(isOn: $settings.enableFileScopeByDefaultInChatContext) { + Text("Enable @file scope by default in chat context.") + } + } + } + + Scope( + title: Text("Code Scope"), + description: "Enable the bot to read the code and metadata in the editing file." + ) { + Form { + Toggle(isOn: $settings.enableCodeScopeByDefaultInChatContext) { + Text("Enable @code scope by default in chat context.") + } + } + } + + #if canImport(ProHostApp) + + Scope( + title: WithFeatureEnabled(\.projectScopeInChat) { Text("Sense Scope") }, + description: "Experimental. Enable the bot to read the relevant code of the editing file in the project, third party packages and the SDK." + ) { + WithFeatureEnabled(\.projectScopeInChat, alignment: .hidden) { + Form { + Toggle(isOn: $settings.enableSenseScopeByDefaultInChatContext) { + Text("Enable @sense scope by default in chat context.") + } + + Picker( + "Preferred Chat Model", + selection: $settings.preferredChatModelIdForSenseScope + ) { + Text("None").tag("") + + if !settings.chatModels + .contains(where: { + $0.id == settings.preferredChatModelIdForSenseScope + }), + !settings.preferredChatModelIdForSenseScope.isEmpty + { + Text( + (settings.chatModels.first?.name).map { "\($0) (Default)" } + ?? "No Model Found" + ) + .tag(settings.preferredChatModelIdForSenseScope) + } + + ForEach(settings.chatModels, id: \.id) { chatModel in + Text(chatModel.name).tag(chatModel.id) + } + } + } + } + .padding(.top, 20) + } + + Scope( + title: WithFeatureEnabled(\.projectScopeInChat) { Text("Project Scope") }, + description: "Experimental. Enable the bot to search code symbols in the project, third party packages and the SDK." + ) { + WithFeatureEnabled(\.projectScopeInChat, alignment: .hidden) { + Form { + Toggle(isOn: $settings.enableProjectScopeByDefaultInChatContext) { + Text("Enable @project scope by default in chat context.") + } + + Picker( + "Preferred Chat Model", + selection: $settings.preferredChatModelIdForProjectScope + ) { + Text("None").tag("") + + if !settings.chatModels + .contains(where: { + $0.id == settings.preferredChatModelIdForProjectScope + }), + !settings.preferredChatModelIdForProjectScope.isEmpty + { + Text( + (settings.chatModels.first?.name).map { "\($0) (Default)" } + ?? "No Model Found" + ) + .tag(settings.preferredChatModelIdForProjectScope) + } + + ForEach(settings.chatModels, id: \.id) { chatModel in + Text(chatModel.name).tag(chatModel.id) + } + } + } + } + } + + #endif + + Scope( + title: Text("Web Scope"), + description: "Allow the bot to search on Bing or read a web page." + ) { + Form { + Toggle(isOn: $settings.enableWebScopeByDefaultInChatContext) { + Text("Enable @web scope by default in chat context.") + } + + Picker( + "Preferred Chat Model", + selection: $settings.preferredChatModelIdForWebScope + ) { + Text("None").tag("") + + if !settings.chatModels + .contains(where: { + $0.id == settings.preferredChatModelIdForWebScope + }), + !settings.preferredChatModelIdForWebScope.isEmpty + { + Text( + (settings.chatModels.first?.name).map { "\($0) (Default)" } + ?? "No Model Found" + ) + .tag(settings.preferredChatModelIdForWebScope) + } + + ForEach(settings.chatModels, id: \.id) { chatModel in + Text(chatModel.name).tag(chatModel.id) + } + } + } + } + } + } + + struct Scope: View { + let title: Title + let description: String + let content: () -> Content + + var body: some View { + SettingsDivider(title) + VStack { + Text(description) + .multilineTextAlignment(.center) + .foregroundStyle(.secondary) + .padding(8) + .background { + RoundedRectangle(cornerRadius: 8) + .fill(Color.secondary.opacity(0.1)) + } + .frame(maxWidth: 500) + Form { + content() + } + } + } + } + } } // MARK: - Preview -struct ChatSettingsView_Previews: PreviewProvider { - static var previews: some View { +#Preview { + ScrollView { ChatSettingsView() + .padding() } + .frame(height: 800) + .environment(\.overrideFeatureFlag, \.never) } diff --git a/Pro b/Pro index 7fe0d037..e6d0cac8 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 7fe0d0372d964e2dab6fb526929c559b8e988a12 +Subproject commit e6d0cac8671d660f5a0546191101035d01412a69 diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index dd7e3f24..aac8fd99 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -88,7 +88,7 @@ public struct UserDefaultPreferenceKeys { defaultValue: false, key: "HideCircularWidget" ) - + public let showHideWidgetShortcutGlobally = PreferenceKey( defaultValue: false, key: "ShowHideWidgetShortcutGlobally" @@ -269,11 +269,11 @@ public extension UserDefaultPreferenceKeys { var promptToCodeGenerateDescriptionInUserPreferredLanguage: PreferenceKey { .init(defaultValue: true, key: "PromptToCodeGenerateDescriptionInUserPreferredLanguage") } - + var promptToCodeChatModelId: PreferenceKey { .init(defaultValue: "", key: "PromptToCodeChatModelId") } - + var promptToCodeEmbeddingModelId: PreferenceKey { .init(defaultValue: "", key: "PromptToCodeEmbeddingModelId") } @@ -366,7 +366,7 @@ public extension UserDefaultPreferenceKeys { .init(defaultValue: 100, key: "MaxEmbeddableFileInChatContextLineCount") } - var useCodeScopeByDefaultInChatContext: PreferenceKey { + var useCodeScopeByDefaultInChatContext: DeprecatedPreferenceKey { .init(defaultValue: true, key: "UseSelectionScopeByDefaultInChatContext") } @@ -389,10 +389,42 @@ public extension UserDefaultPreferenceKeys { var chatSearchPluginMaxIterations: PreferenceKey { .init(defaultValue: 3, key: "ChatSearchPluginMaxIterations") } - + var wrapCodeInChatCodeBlock: PreferenceKey { .init(defaultValue: true, key: "WrapCodeInChatCodeBlock") } + + var enableFileScopeByDefaultInChatContext: PreferenceKey { + .init(defaultValue: true, key: "EnableFileScopeByDefaultInChatContext") + } + + var enableCodeScopeByDefaultInChatContext: PreferenceKey { + .init(defaultValue: true, key: "UseSelectionScopeByDefaultInChatContext") + } + + var enableSenseScopeByDefaultInChatContext: PreferenceKey { + .init(defaultValue: false, key: "EnableSenseScopeByDefaultInChatContext") + } + + var enableProjectScopeByDefaultInChatContext: PreferenceKey { + .init(defaultValue: false, key: "EnableProjectScopeByDefaultInChatContext") + } + + var enableWebScopeByDefaultInChatContext: PreferenceKey { + .init(defaultValue: false, key: "EnableWebScopeByDefaultInChatContext") + } + + var preferredChatModelIdForSenseScope: PreferenceKey { + .init(defaultValue: "", key: "PreferredChatModelIdForSenseScope") + } + + var preferredChatModelIdForProjectScope: PreferenceKey { + .init(defaultValue: "", key: "PreferredChatModelIdForProjectScope") + } + + var preferredChatModelIdForWebScope: PreferenceKey { + .init(defaultValue: "", key: "PreferredChatModelIdForWebScope") + } } // MARK: - Bing Search @@ -508,7 +540,7 @@ public extension UserDefaultPreferenceKeys { key: "FeatureFlag-DisableGitHubCopilotSettingsAutoRefreshOnAppear" ) } - + var disableEnhancedWorkspace: FeatureFlag { .init( defaultValue: false, diff --git a/Tool/Sources/SharedUIComponents/SettingsDivider.swift b/Tool/Sources/SharedUIComponents/SettingsDivider.swift new file mode 100644 index 00000000..15db820d --- /dev/null +++ b/Tool/Sources/SharedUIComponents/SettingsDivider.swift @@ -0,0 +1,42 @@ +import SwiftUI + +public struct SettingsDivider: View { + let title: Title? + + public init(_ title: Title) { + self.title = title + } + + public var body: some View { + if let title { + HStack { + VStack { + Divider() + } + title + .foregroundStyle(.secondary) + .font(.subheadline) + .zIndex(2) + VStack { + Divider() + } + } + .padding(.vertical, 8) + } else { + Divider() + .padding(.vertical, 8) + } + } +} + +extension SettingsDivider where Title == Text { + public init(_ title: String) { + self.title = Text(title) + } +} + +extension SettingsDivider where Title == EmptyView { + public init() { + self.title = nil + } +} From 3f25cc5f739d29507050aaa34ed2aed811ff4ebb Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 10:23:51 +0800 Subject: [PATCH 06/49] Adjust UI --- .../HostApp/AccountSettings/CodeiumView.swift | 3 +- .../AccountSettings/GitHubCopilotView.swift | 4 +- .../FeatureSettings/ChatSettingsView.swift | 49 +++--- .../PromptToCodeSettingsView.swift | 16 +- .../SuggestionSettingsView.swift | 166 +++++++++--------- Core/Sources/HostApp/GeneralView.swift | 7 +- 6 files changed, 114 insertions(+), 131 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/CodeiumView.swift b/Core/Sources/HostApp/AccountSettings/CodeiumView.swift index 7c7b12cb..37a1fd16 100644 --- a/Core/Sources/HostApp/AccountSettings/CodeiumView.swift +++ b/Core/Sources/HostApp/AccountSettings/CodeiumView.swift @@ -1,5 +1,6 @@ import CodeiumService import Foundation +import SharedUIComponents import SwiftUI struct CodeiumView: View { @@ -220,7 +221,7 @@ struct CodeiumView: View { .stroke(Color(nsColor: .separatorColor), style: .init(lineWidth: 1)) } - Divider() + SettingsDivider("Advanced") Form { Toggle("Verbose Log", isOn: $viewModel.codeiumVerboseLog) diff --git a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift index d31b1fb8..39828096 100644 --- a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift +++ b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift @@ -270,7 +270,7 @@ struct GitHubCopilotView: View { .stroke(Color(nsColor: .separatorColor), style: .init(lineWidth: 1)) } - Divider() + SettingsDivider("Advanced") Form { Toggle( @@ -287,7 +287,7 @@ struct GitHubCopilotView: View { Toggle("Verbose Log", isOn: $settings.gitHubCopilotVerboseLog) } - Divider() + SettingsDivider("Proxy") Form { TextField( diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index e8669f3e..6ab291cf 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -14,8 +14,7 @@ struct ChatSettingsView: View { @AppStorage(\.chatGPTMaxMessageCount) var chatGPTMaxMessageCount @AppStorage(\.chatFontSize) var chatFontSize @AppStorage(\.chatCodeFontSize) var chatCodeFontSize - @AppStorage(\.maxFocusedCodeLineCount) - var maxFocusedCodeLineCount + @AppStorage(\.defaultChatFeatureChatModelId) var defaultChatFeatureChatModelId @AppStorage(\.defaultChatSystemPrompt) var defaultChatSystemPrompt @AppStorage(\.chatSearchPluginMaxIterations) var chatSearchPluginMaxIterations @@ -37,8 +36,6 @@ struct ChatSettingsView: View { chatSettingsForm SettingsDivider("UI") uiForm - SettingsDivider("Context") - contextForm SettingsDivider("Plugin") pluginForm ScopeForm() @@ -171,24 +168,6 @@ struct ChatSettingsView: View { } } - @ViewBuilder - var contextForm: some View { - Form { - HStack { - TextField(text: .init(get: { - "\(Int(settings.maxFocusedCodeLineCount))" - }, set: { - settings.maxFocusedCodeLineCount = Int($0) ?? 0 - })) { - Text("Max focused code line count in chat context") - } - .textFieldStyle(.roundedBorder) - - Text("lines") - } - } - } - @ViewBuilder var pluginForm: some View { Form { @@ -255,6 +234,8 @@ struct ChatSettingsView: View { @AppStorage(\.preferredChatModelIdForWebScope) var preferredChatModelIdForWebScope: String @AppStorage(\.chatModels) var chatModels + @AppStorage(\.maxFocusedCodeLineCount) + var maxFocusedCodeLineCount init() {} } @@ -282,6 +263,19 @@ struct ChatSettingsView: View { Toggle(isOn: $settings.enableCodeScopeByDefaultInChatContext) { Text("Enable @code scope by default in chat context.") } + + HStack { + TextField(text: .init(get: { + "\(Int(settings.maxFocusedCodeLineCount))" + }, set: { + settings.maxFocusedCodeLineCount = Int($0) ?? 0 + })) { + Text("Max focused code") + } + .textFieldStyle(.roundedBorder) + + Text("lines") + } } } @@ -322,7 +316,6 @@ struct ChatSettingsView: View { } } } - .padding(.top, 20) } Scope( @@ -334,7 +327,7 @@ struct ChatSettingsView: View { Toggle(isOn: $settings.enableProjectScopeByDefaultInChatContext) { Text("Enable @project scope by default in chat context.") } - + Picker( "Preferred Chat Model", selection: $settings.preferredChatModelIdForProjectScope @@ -372,7 +365,7 @@ struct ChatSettingsView: View { Toggle(isOn: $settings.enableWebScopeByDefaultInChatContext) { Text("Enable @web scope by default in chat context.") } - + Picker( "Preferred Chat Model", selection: $settings.preferredChatModelIdForWebScope @@ -412,15 +405,13 @@ struct ChatSettingsView: View { Text(description) .multilineTextAlignment(.center) .foregroundStyle(.secondary) + .frame(maxWidth: .infinity) .padding(8) .background { RoundedRectangle(cornerRadius: 8) .fill(Color.secondary.opacity(0.1)) } - .frame(maxWidth: 500) - Form { - content() - } + content() } } } diff --git a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift index de42264b..d06fdf7e 100644 --- a/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/PromptToCodeSettingsView.swift @@ -1,3 +1,4 @@ +import SharedUIComponents import SwiftUI struct PromptToCodeSettingsView: View { @@ -30,7 +31,7 @@ struct PromptToCodeSettingsView: View { selection: $settings.promptToCodeChatModelId ) { Text("Same as Chat Feature").tag("") - + if !settings.chatModels .contains(where: { $0.id == settings.promptToCodeChatModelId }), !settings.promptToCodeChatModelId.isEmpty @@ -52,7 +53,7 @@ struct PromptToCodeSettingsView: View { selection: $settings.promptToCodeEmbeddingModelId ) { Text("Same as Chat Feature").tag("") - + if !settings.embeddingModels .contains(where: { $0.id == settings.promptToCodeEmbeddingModelId }), !settings.promptToCodeEmbeddingModelId.isEmpty @@ -78,16 +79,7 @@ struct PromptToCodeSettingsView: View { } } - Divider() - - Text("Mirroring Settings of Suggestion Feature") - .foregroundColor(.white) - .padding(.vertical, 2) - .padding(.horizontal, 8) - .background( - Color.accentColor, - in: RoundedRectangle(cornerRadius: 20) - ) + SettingsDivider("Mirroring Settings of Suggestion Feature") Form { Toggle(isOn: $settings.hideCommonPrecedingSpacesInSuggestion) { diff --git a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift index 8218f95a..d63a819d 100644 --- a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift @@ -1,4 +1,5 @@ import Preferences +import SharedUIComponents import SwiftUI #if canImport(ProHostApp) @@ -36,112 +37,109 @@ struct SuggestionSettingsView: View { var body: some View { Form { - Group { - Picker(selection: $settings.suggestionPresentationMode) { - ForEach(PresentationMode.allCases, id: \.rawValue) { - switch $0 { - case .nearbyTextCursor: - Text("Nearby Text Cursor").tag($0) - case .floatingWidget: - Text("Floating Widget").tag($0) - } + Picker(selection: $settings.suggestionPresentationMode) { + ForEach(PresentationMode.allCases, id: \.rawValue) { + switch $0 { + case .nearbyTextCursor: + Text("Nearby Text Cursor").tag($0) + case .floatingWidget: + Text("Floating Widget").tag($0) } - } label: { - Text("Presentation") } + } label: { + Text("Presentation") + } - Picker(selection: $settings.suggestionFeatureProvider) { - ForEach(SuggestionFeatureProvider.allCases, id: \.rawValue) { - switch $0 { - case .gitHubCopilot: - Text("GitHub Copilot").tag($0) - case .codeium: - Text("Codeium").tag($0) - } + Picker(selection: $settings.suggestionFeatureProvider) { + ForEach(SuggestionFeatureProvider.allCases, id: \.rawValue) { + switch $0 { + case .gitHubCopilot: + Text("GitHub Copilot").tag($0) + case .codeium: + Text("Codeium").tag($0) } - } label: { - Text("Feature Provider") } + } label: { + Text("Feature Provider") + } - Toggle(isOn: $settings.realtimeSuggestionToggle) { - Text("Real-time Suggestion") - } + Toggle(isOn: $settings.realtimeSuggestionToggle) { + Text("Real-time Suggestion") + } - #if canImport(ProHostApp) - WithFeatureEnabled(\.tabToAcceptSuggestion) { - Toggle(isOn: $settings.acceptSuggestionWithTab) { - Text("Accept Suggestion with Tab") - } + #if canImport(ProHostApp) + WithFeatureEnabled(\.tabToAcceptSuggestion) { + Toggle(isOn: $settings.acceptSuggestionWithTab) { + Text("Accept Suggestion with Tab") } - #endif - - HStack { - Toggle(isOn: $settings.disableSuggestionFeatureGlobally) { - Text("Disable Suggestion Feature Globally") - } + } + #endif - Button("Exception List") { - isSuggestionFeatureEnabledListPickerOpen = true - } - }.sheet(isPresented: $isSuggestionFeatureEnabledListPickerOpen) { - SuggestionFeatureEnabledProjectListView( - isOpen: $isSuggestionFeatureEnabledListPickerOpen - ) + HStack { + Toggle(isOn: $settings.disableSuggestionFeatureGlobally) { + Text("Disable Suggestion Feature Globally") } - HStack { - Button("Disabled Language List") { - isSuggestionFeatureDisabledLanguageListViewOpen = true - } - }.sheet(isPresented: $isSuggestionFeatureDisabledLanguageListViewOpen) { - SuggestionFeatureDisabledLanguageListView( - isOpen: $isSuggestionFeatureDisabledLanguageListViewOpen - ) + Button("Exception List") { + isSuggestionFeatureEnabledListPickerOpen = true } + }.sheet(isPresented: $isSuggestionFeatureEnabledListPickerOpen) { + SuggestionFeatureEnabledProjectListView( + isOpen: $isSuggestionFeatureEnabledListPickerOpen + ) + } - HStack { - Slider(value: $settings.realtimeSuggestionDebounce, in: 0...2, step: 0.1) { - Text("Real-time Suggestion Debounce") - } + HStack { + Button("Disabled Language List") { + isSuggestionFeatureDisabledLanguageListViewOpen = true + } + }.sheet(isPresented: $isSuggestionFeatureDisabledLanguageListViewOpen) { + SuggestionFeatureDisabledLanguageListView( + isOpen: $isSuggestionFeatureDisabledLanguageListViewOpen + ) + } - Text( - "\(settings.realtimeSuggestionDebounce.formatted(.number.precision(.fractionLength(2))))s" - ) - .font(.body) - .monospacedDigit() - .padding(.vertical, 2) - .padding(.horizontal, 6) - .background( - RoundedRectangle(cornerRadius: 4, style: .continuous) - .fill(Color.primary.opacity(0.1)) - ) + HStack { + Slider(value: $settings.realtimeSuggestionDebounce, in: 0...2, step: 0.1) { + Text("Real-time Suggestion Debounce") } - Divider() + Text( + "\(settings.realtimeSuggestionDebounce.formatted(.number.precision(.fractionLength(2))))s" + ) + .font(.body) + .monospacedDigit() + .padding(.vertical, 2) + .padding(.horizontal, 6) + .background( + RoundedRectangle(cornerRadius: 4, style: .continuous) + .fill(Color.primary.opacity(0.1)) + ) } + } - Group { - Toggle(isOn: $settings.suggestionDisplayCompactMode) { - Text("Hide Buttons") - } + SettingsDivider("UI") - Toggle(isOn: $settings.hideCommonPrecedingSpacesInSuggestion) { - Text("Hide Common Preceding Spaces") - } + Form { + Toggle(isOn: $settings.suggestionDisplayCompactMode) { + Text("Hide Buttons") + } - HStack { - TextField(text: .init(get: { - "\(Int(settings.suggestionCodeFontSize))" - }, set: { - settings.suggestionCodeFontSize = Double(Int($0) ?? 0) - })) { - Text("Font size of suggestion code") - } - .textFieldStyle(.roundedBorder) + Toggle(isOn: $settings.hideCommonPrecedingSpacesInSuggestion) { + Text("Hide Common Preceding Spaces") + } - Text("pt") + HStack { + TextField(text: .init(get: { + "\(Int(settings.suggestionCodeFontSize))" + }, set: { + settings.suggestionCodeFontSize = Double(Int($0) ?? 0) + })) { + Text("Font size of suggestion code") } - Divider() + .textFieldStyle(.roundedBorder) + + Text("pt") } } } diff --git a/Core/Sources/HostApp/GeneralView.swift b/Core/Sources/HostApp/GeneralView.swift index c011de4a..90e352ca 100644 --- a/Core/Sources/HostApp/GeneralView.swift +++ b/Core/Sources/HostApp/GeneralView.swift @@ -3,6 +3,7 @@ import ComposableArchitecture import KeyboardShortcuts import LaunchAgentManager import Preferences +import SharedUIComponents import SwiftUI struct GeneralView: View { @@ -12,11 +13,11 @@ struct GeneralView: View { ScrollView { VStack(alignment: .leading, spacing: 0) { AppInfoView() - Divider() + SettingsDivider() ExtensionServiceView(store: store) - Divider() + SettingsDivider() LaunchAgentView() - Divider() + SettingsDivider() GeneralSettingsView() } } From e1f4b54e3449376d7c1d56d373d2fbd1d7abee89 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 10:26:22 +0800 Subject: [PATCH 07/49] Update UI --- Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index 6ab291cf..412a809f 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -103,6 +103,7 @@ struct ChatSettingsView: View { "\(settings.chatGPTTemperature.formatted(.number.precision(.fractionLength(1))))" ) .font(.body) + .foregroundColor(settings.chatGPTTemperature >= 1 ? .red : .secondary) .monospacedDigit() .padding(.vertical, 2) .padding(.horizontal, 6) From 2b5d950da9133297dce26336bdc0447c1554f558 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 10:29:06 +0800 Subject: [PATCH 08/49] Update to setup scope according to settings --- Core/Sources/ChatService/ChatService.swift | 26 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 2be2c2f8..85296a82 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -62,11 +62,29 @@ public final class ChatService: ObservableObject { } public func resetDefaultScopes() { - if UserDefaults.shared.value(for: \.useCodeScopeByDefaultInChatContext) { - memory.contextController.defaultScopes = ["code"] - } else { - memory.contextController.defaultScopes = ["file"] + var scopes = Set() + + if UserDefaults.shared.value(for: \.enableFileScopeByDefaultInChatContext) { + scopes.insert("file") + } + + if UserDefaults.shared.value(for: \.enableCodeScopeByDefaultInChatContext) { + scopes.insert("code") + } + + if UserDefaults.shared.value(for: \.enableSenseScopeByDefaultInChatContext) { + scopes.insert("sense") } + + if UserDefaults.shared.value(for: \.enableProjectScopeByDefaultInChatContext) { + scopes.insert("project") + } + + if UserDefaults.shared.value(for: \.enableWebScopeByDefaultInChatContext) { + scopes.insert("web") + } + + memory.contextController.defaultScopes = scopes } public func send(content: String) async throws { From f528d9adc6ab03b16a9f8a8b37b72c37e11e2f98 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 11:14:45 +0800 Subject: [PATCH 09/49] Support changing default scopes in chat panel --- Core/Sources/ChatGPTChatTab/Chat.swift | 36 +++++++++++++++ .../ChatGPTChatTab/ChatContextMenu.swift | 30 +++++++++++++ Core/Sources/ChatGPTChatTab/ChatPanel.swift | 31 +++++++------ Core/Sources/ChatService/ChatService.swift | 44 +++++++++++-------- 4 files changed, 110 insertions(+), 31 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index 72d00503..d5ec598b 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -51,11 +51,13 @@ struct Chat: ReducerProtocol { case observeIsReceivingMessageChange case observeSystemPromptChange case observeExtraSystemPromptChange + case observeDefaultScopesChange case historyChanged case isReceivingMessageChanged case systemPromptChanged case extraSystemPromptChanged + case defaultScopesChanged case chatMenu(ChatMenu.Action) } @@ -68,6 +70,7 @@ struct Chat: ReducerProtocol { case observeIsReceivingMessageChange(UUID) case observeSystemPromptChange(UUID) case observeExtraSystemPromptChange(UUID) + case observeDefaultScopesChange(UUID) } var body: some ReducerProtocol { @@ -131,6 +134,7 @@ struct Chat: ReducerProtocol { await send(.observeIsReceivingMessageChange) await send(.observeSystemPromptChange) await send(.observeExtraSystemPromptChange) + await send(.observeDefaultScopesChange) } case .observeHistoryChange: @@ -198,6 +202,22 @@ struct Chat: ReducerProtocol { } }.cancellable(id: CancelID.observeExtraSystemPromptChange(id), cancelInFlight: true) + case .observeDefaultScopesChange: + return .run { send in + let stream = AsyncStream { continuation in + let cancellable = service.$defaultScopes + .sink { _ in + continuation.yield() + } + continuation.onTermination = { _ in + cancellable.cancel() + } + } + for await _ in stream { + await send(.defaultScopesChanged) + } + }.cancellable(id: CancelID.observeDefaultScopesChange(id), cancelInFlight: true) + case .historyChanged: state.history = service.chatHistory.map { message in .init( @@ -250,6 +270,10 @@ struct Chat: ReducerProtocol { state.chatMenu.extraSystemPrompt = service.extraSystemPrompt return .none + case .defaultScopesChanged: + state.chatMenu.defaultScopes = service.defaultScopes + return .none + case .binding: return .none @@ -266,6 +290,7 @@ struct ChatMenu: ReducerProtocol { var extraSystemPrompt: String = "" var temperatureOverride: Double? = nil var chatModelIdOverride: String? = nil + var defaultScopes: Set = [] } enum Action: Equatable { @@ -274,6 +299,8 @@ struct ChatMenu: ReducerProtocol { case temperatureOverrideSelected(Double?) case chatModelIdOverrideSelected(String?) case customCommandButtonTapped(CustomCommand) + case resetDefaultScopesButtonTapped + case toggleScope(ChatService.Scope) } let service: ChatService @@ -304,6 +331,15 @@ struct ChatMenu: ReducerProtocol { return .run { _ in try await service.handleCustomCommand(command) } + + case .resetDefaultScopesButtonTapped: + return .run { _ in + service.resetDefaultScopes() + } + case let .toggleScope(scope): + return .run { _ in + service.defaultScopes.formSymmetricDifference([scope]) + } } } } diff --git a/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift b/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift index bff64f4e..e6a3b2c4 100644 --- a/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift +++ b/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift @@ -1,4 +1,5 @@ import AppKit +import ChatService import ComposableArchitecture import SharedUIComponents import SwiftUI @@ -30,6 +31,7 @@ struct ChatContextMenu: View { chatModel temperature + defaultScopes Divider() @@ -153,6 +155,34 @@ struct ChatContextMenu: View { } } + @ViewBuilder + var defaultScopes: some View { + Menu("Default Scopes") { + WithViewStore(store, observe: \.defaultScopes) { viewStore in + Button(action: { + store.send(.resetDefaultScopesButtonTapped) + }) { + Text("Reset Default Scopes") + } + + Divider() + + ForEach(ChatService.Scope.allCases, id: \.rawValue) { value in + Button(action: { + viewStore.send(.toggleScope(value)) + }) { + HStack { + Text("@" + value.rawValue) + if viewStore.state.contains(value) { + Image(systemName: "checkmark") + } + } + } + } + } + } + } + var customCommandMenu: some View { Menu("Custom Commands") { ForEach( diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index 474c4eb4..f8d12923 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -54,7 +54,7 @@ struct ChatPanelMessages: View { Group { Spacer(minLength: 12) - Instruction() + Instruction(chat: chat) ChatHistory(chat: chat) .listItemTint(.clear) @@ -225,8 +225,7 @@ private struct StopRespondingButton: View { } private struct Instruction: View { - @AppStorage(\.useCodeScopeByDefaultInChatContext) - var useCodeScopeByDefaultInChatContext + let chat: StoreOf var body: some View { Group { @@ -266,18 +265,24 @@ private struct Instruction: View { ) .modifier(InstructionModifier()) - Markdown( - """ - Hello, I am your AI programming assistant. I can identify issues, explain and even improve code. + WithViewStore(chat, observe: \.chatMenu.defaultScopes) { viewStore in + 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." + \({ + if viewStore.state.isEmpty { + return "No scope is enabled by default" + } else { + let scopes = viewStore.state.map(\.rawValue).sorted() + .joined(separator: ", ") + return "Default scopes: `\(scopes)`" + } + }()) + """ ) - """ - ) - .modifier(InstructionModifier()) + .modifier(InstructionModifier()) + } } } diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 85296a82..372afcc6 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -6,6 +6,14 @@ import OpenAIService import Preferences public final class ChatService: ObservableObject { + public enum Scope: String, Equatable, CaseIterable { + case file + case code + case sense + case project + case web + } + public let memory: ContextAwareAutoManagedChatGPTMemory public let configuration: OverridingChatGPTConfiguration public let chatGPTService: any ChatGPTServiceType @@ -15,6 +23,7 @@ public final class ChatService: ObservableObject { @Published public internal(set) var systemPrompt = UserDefaults.shared .value(for: \.defaultChatSystemPrompt) @Published public internal(set) var extraSystemPrompt = "" + @Published public var defaultScopes = Set() let pluginController: ChatPluginController var cancellable = Set() @@ -50,7 +59,7 @@ public final class ChatService: ObservableObject { functionProvider: memory.functionProvider ) ) - + resetDefaultScopes() memory.chatService = self @@ -60,38 +69,38 @@ public final class ChatService: ObservableObject { } } } - + public func resetDefaultScopes() { - var scopes = Set() - + var scopes = Set() if UserDefaults.shared.value(for: \.enableFileScopeByDefaultInChatContext) { - scopes.insert("file") + scopes.insert(.file) } - + if UserDefaults.shared.value(for: \.enableCodeScopeByDefaultInChatContext) { - scopes.insert("code") + scopes.insert(.code) } - - if UserDefaults.shared.value(for: \.enableSenseScopeByDefaultInChatContext) { - scopes.insert("sense") - } - + if UserDefaults.shared.value(for: \.enableProjectScopeByDefaultInChatContext) { - scopes.insert("project") + scopes.insert(.project) } - + + if UserDefaults.shared.value(for: \.enableSenseScopeByDefaultInChatContext) { + scopes.insert(.sense) + } + if UserDefaults.shared.value(for: \.enableWebScopeByDefaultInChatContext) { - scopes.insert("web") + scopes.insert(.web) } - memory.contextController.defaultScopes = scopes + defaultScopes = scopes } public func send(content: String) async throws { + memory.contextController.defaultScopes = Set(defaultScopes.map(\.rawValue)) guard !isReceivingMessage else { throw CancellationError() } let handledInPlugin = try await pluginController.handleContent(content) if handledInPlugin { return } - + let stream = try await chatGPTService.send(content: content, summary: nil) isReceivingMessage = true do { @@ -133,7 +142,6 @@ public final class ChatService: ObservableObject { public func resetPrompt() async { systemPrompt = UserDefaults.shared.value(for: \.defaultChatSystemPrompt) extraSystemPrompt = "" - resetDefaultScopes() } public func deleteMessage(id: String) async { From 956f71295670e11cde7b89b9e5a31ef9ad91b2c1 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 11:38:20 +0800 Subject: [PATCH 10/49] Add Scope enum to represent scopes --- .../SystemInfoChatContextCollector.swift | 2 +- .../WebChatContextCollector.swift | 4 +-- Core/Sources/ChatService/ChatService.swift | 10 ++----- ...ContextAwareAutoManagedChatGPTMemory.swift | 2 +- .../DynamicContextController.swift | 10 +++---- Pro | 2 +- .../ChatContextCollector.swift | 28 ++++++++++++++++--- .../ActiveDocumentChatContextCollector.swift | 6 ++-- ...cyActiveDocumentChatContextCollector.swift | 8 +++--- 9 files changed, 43 insertions(+), 29 deletions(-) diff --git a/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift b/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift index d5563b51..9686ca85 100644 --- a/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/SystemInfoChatContextCollector/SystemInfoChatContextCollector.swift @@ -13,7 +13,7 @@ public final class SystemInfoChatContextCollector: ChatContextCollector { public func generateContext( history: [ChatMessage], - scopes: Set, + scopes: Set, content: String, configuration: ChatGPTConfiguration ) -> ChatContext { diff --git a/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift b/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift index 81d1b9fc..29327cea 100644 --- a/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift +++ b/Core/Sources/ChatContextCollectors/WebChatContextCollector/WebChatContextCollector.swift @@ -9,11 +9,11 @@ public final class WebChatContextCollector: ChatContextCollector { public func generateContext( history: [ChatMessage], - scopes: Set, + scopes: Set, content: String, configuration: ChatGPTConfiguration ) -> ChatContext { - guard scopes.contains("web") || scopes.contains("w") else { return .empty } + guard scopes.contains(.web) else { return .empty } let links = Self.detectLinks(from: history) + Self.detectLinks(from: content) let functions: [(any ChatGPTFunction)?] = [ SearchFunction(maxTokens: configuration.maxTokens), diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 372afcc6..10c29411 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -6,13 +6,7 @@ import OpenAIService import Preferences public final class ChatService: ObservableObject { - public enum Scope: String, Equatable, CaseIterable { - case file - case code - case sense - case project - case web - } + public typealias Scope = ChatContext.Scope public let memory: ContextAwareAutoManagedChatGPTMemory public let configuration: OverridingChatGPTConfiguration @@ -96,7 +90,7 @@ public final class ChatService: ObservableObject { } public func send(content: String) async throws { - memory.contextController.defaultScopes = Set(defaultScopes.map(\.rawValue)) + memory.contextController.defaultScopes = defaultScopes guard !isReceivingMessage else { throw CancellationError() } let handledInPlugin = try await pluginController.handleContent(content) if handledInPlugin { return } diff --git a/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift b/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift index 0a22051c..6406df5c 100644 --- a/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift +++ b/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift @@ -24,7 +24,7 @@ public final class ContextAwareAutoManagedChatGPTMemory: ChatGPTMemory { } init( - configuration: ChatGPTConfiguration, + configuration: OverridingChatGPTConfiguration, functionProvider: ChatFunctionProvider ) { memory = AutoManagedChatGPTMemory( diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index 4757fbec..e73ea0c6 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -8,13 +8,13 @@ final class DynamicContextController { let contextCollectors: [ChatContextCollector] let memory: AutoManagedChatGPTMemory let functionProvider: ChatFunctionProvider - let configuration: ChatGPTConfiguration - var defaultScopes = [] as Set + let configuration: OverridingChatGPTConfiguration + var defaultScopes = [] as Set convenience init( memory: AutoManagedChatGPTMemory, functionProvider: ChatFunctionProvider, - configuration: ChatGPTConfiguration, + configuration: OverridingChatGPTConfiguration, contextCollectors: ChatContextCollector... ) { self.init( @@ -28,7 +28,7 @@ final class DynamicContextController { init( memory: AutoManagedChatGPTMemory, functionProvider: ChatFunctionProvider, - configuration: ChatGPTConfiguration, + configuration: OverridingChatGPTConfiguration, contextCollectors: [ChatContextCollector] ) { self.memory = memory @@ -86,7 +86,7 @@ final class DynamicContextController { } extension DynamicContextController { - static func parseScopes(_ prompt: inout String) -> Set { + static func parseScopes(_ prompt: inout String) -> Set { let parser = MessageScopeParser() return parser(&prompt) } diff --git a/Pro b/Pro index e6d0cac8..0747cfb6 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit e6d0cac8671d660f5a0546191101035d01412a69 +Subproject commit 0747cfb6c32f77f6d37f9caa950be976edd6156a diff --git a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift index 5b2d0cc0..996c3a52 100644 --- a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift @@ -3,6 +3,14 @@ import OpenAIService import Parsing public struct ChatContext { + public enum Scope: String, Equatable, CaseIterable { + case file + case code + case sense + case project + case web + } + public struct RetrievedContent { public var content: String public var priority: Int @@ -31,10 +39,22 @@ public struct ChatContext { } } +public extension ChatContext.Scope { + init?(text: String) { + for scope in Self.allCases { + if scope.rawValue.hasPrefix(text.lowercased()) { + self = scope + return + } + } + return nil + } +} + public protocol ChatContextCollector { func generateContext( history: [ChatMessage], - scopes: Set, + scopes: Set, content: String, configuration: ChatGPTConfiguration ) async -> ChatContext @@ -43,11 +63,11 @@ public protocol ChatContextCollector { public struct MessageScopeParser { public init() {} - public func callAsFunction(_ content: inout String) -> Set { + public func callAsFunction(_ content: inout String) -> Set { return parseScopes(&content) } - func parseScopes(_ prompt: inout String) -> Set { + func parseScopes(_ prompt: inout String) -> Set { guard !prompt.isEmpty else { return [] } do { let parser = Parse { @@ -68,7 +88,7 @@ public struct MessageScopeParser { } let (scopes, rest) = try parser.parse(prompt) prompt = String(rest) - return Set(scopes.map(String.init)) + return Set(scopes.map(String.init).compactMap(ChatContext.Scope.init(text:))) } catch { return [] } diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index d1d90264..0c55f851 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -14,7 +14,7 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { public func generateContext( history: [ChatMessage], - scopes: Set, + scopes: Set, content: String, configuration: ChatGPTConfiguration ) -> ChatContext { @@ -22,8 +22,8 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { let context = getActiveDocumentContext(info) activeDocumentContext = context - guard scopes.contains("code") || scopes.contains("c") else { - if scopes.contains("file") || scopes.contains("f") { + guard scopes.contains(.code) else { + if scopes.contains(.file) { var removedCode = context removedCode.focusedContext = nil return .init( diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift index 45cc414c..c7c4d20f 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/LegacyActiveDocumentChatContextCollector.swift @@ -10,7 +10,7 @@ public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { public func generateContext( history: [ChatMessage], - scopes: Set, + scopes: Set, content: String, configuration: ChatGPTConfiguration ) -> ChatContext { @@ -18,7 +18,7 @@ public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { let relativePath = content.relativePath let selectionRange = content.editorContent?.selections.first ?? .outOfScope let editorContent = { - if scopes.contains("file") || scopes.contains("f") { + if scopes.contains(.file) { return """ File Content:```\(content.language.rawValue) \(content.editorContent?.content ?? "") @@ -49,7 +49,7 @@ public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { } } - if UserDefaults.shared.value(for: \.useCodeScopeByDefaultInChatContext) { + if UserDefaults.shared.value(for: \.enableCodeScopeByDefaultInChatContext) { return """ Selected Code \ (start from line \(selectionRange.start.line)):```\(content.language.rawValue) @@ -58,7 +58,7 @@ public struct LegacyActiveDocumentChatContextCollector: ChatContextCollector { """ } - if scopes.contains("selection") || scopes.contains("s") { + if scopes.contains(.code) { return """ Selected Code \ (start from line \(selectionRange.start.line)):```\(content.language.rawValue) From 39b387dd4c7e11439127e065f143c69832c3392f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 12:32:41 +0800 Subject: [PATCH 11/49] Support changing model according to scopes --- Core/Sources/ChatService/ChatService.swift | 6 ++-- ...ContextAwareAutoManagedChatGPTMemory.swift | 15 ++++++---- .../DynamicContextController.swift | 29 ++++++++++++++++++- .../OpenAIService/ChatGPTService.swift | 8 ++--- .../UserPreferenceChatGPTConfiguration.swift | 13 +++++---- 5 files changed, 53 insertions(+), 18 deletions(-) diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 10c29411..30a49b6f 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -40,8 +40,10 @@ public final class ChatService: ObservableObject { public convenience init() { let configuration = UserPreferenceChatGPTConfiguration().overriding() + /// Used by context collector + let extraConfiguration = configuration.overriding() let memory = ContextAwareAutoManagedChatGPTMemory( - configuration: configuration, + configuration: extraConfiguration, functionProvider: ChatFunctionProvider() ) self.init( @@ -49,7 +51,7 @@ public final class ChatService: ObservableObject { configuration: configuration, chatGPTService: ChatGPTService( memory: memory, - configuration: configuration, + configuration: extraConfiguration, functionProvider: memory.functionProvider ) ) diff --git a/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift b/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift index 6406df5c..f4600a79 100644 --- a/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift +++ b/Core/Sources/ChatService/ContextAwareAutoManagedChatGPTMemory.swift @@ -14,11 +14,11 @@ public final class ContextAwareAutoManagedChatGPTMemory: ChatGPTMemory { public var remainingTokens: Int? { get async { await memory.remainingTokens } } - + public var history: [ChatMessage] { get async { await memory.history } } - + func observeHistoryChange(_ observer: @escaping () -> Void) { memory.observeHistoryChange(observer) } @@ -48,10 +48,13 @@ public final class ContextAwareAutoManagedChatGPTMemory: ChatGPTMemory { public func refresh() async { let content = (await memory.history) .last(where: { $0.role == .user || $0.role == .function })?.content - try? await contextController.updatePromptToMatchContent(systemPrompt: """ - \(chatService?.systemPrompt ?? "") - \(chatService?.extraSystemPrompt ?? "") - """, content: content ?? "") + try? await contextController.collectContextInformation( + systemPrompt: """ + \(chatService?.systemPrompt ?? "") + \(chatService?.extraSystemPrompt ?? "") + """, + content: content ?? "" + ) await memory.refresh() } } diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index e73ea0c6..2f905999 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -37,10 +37,37 @@ final class DynamicContextController { self.contextCollectors = contextCollectors } - func updatePromptToMatchContent(systemPrompt: String, content: String) async throws { + func collectContextInformation(systemPrompt: String, content: String) async throws { var content = content var scopes = Self.parseScopes(&content) scopes.formUnion(defaultScopes) + + let overridingChatModelId = { + var ids = [String]() + if scopes.contains(.sense) { + ids.append(UserDefaults.shared.value(for: \.preferredChatModelIdForSenseScope)) + } + + if scopes.contains(.project) { + ids.append(UserDefaults.shared.value(for: \.preferredChatModelIdForProjectScope)) + } + + if scopes.contains(.web) { + ids.append(UserDefaults.shared.value(for: \.preferredChatModelIdForWebScope)) + } + + let chatModels = UserDefaults.shared.value(for: \.chatModels) + let idIndexMap = chatModels.enumerated().reduce(into: [String: Int]()) { + $0[$1.element.id] = $1.offset + } + return ids.sorted(by: { + let lhs = idIndexMap[$0] ?? Int.max + let rhs = idIndexMap[$1] ?? Int.max + return lhs < rhs + }).first + }() + + configuration.overriding.modelId = overridingChatModelId functionProvider.removeAll() let language = UserDefaults.shared.value(for: \.chatGPTLanguage) diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index a1ef374f..640bf73a 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -212,6 +212,8 @@ extension ChatGPTService { /// Send the memory as prompt to ChatGPT, with stream enabled. func sendMemory() async throws -> AsyncThrowingStream { + await memory.refresh() + guard let model = configuration.model else { throw ChatGPTServiceError.chatModelNotAvailable } @@ -219,8 +221,6 @@ extension ChatGPTService { throw ChatGPTServiceError.endpointIncorrect } - await memory.refresh() - let messages = await memory.messages.map { CompletionRequestBody.Message( role: $0.role, @@ -325,6 +325,8 @@ extension ChatGPTService { /// Send the memory as prompt to ChatGPT, with stream disabled. func sendMemoryAndWait() async throws -> ChatMessage? { + await memory.refresh() + guard let model = configuration.model else { throw ChatGPTServiceError.chatModelNotAvailable } @@ -332,8 +334,6 @@ extension ChatGPTService { throw ChatGPTServiceError.endpointIncorrect } - await memory.refresh() - let messages = await memory.messages.map { CompletionRequestBody.Message( role: $0.role, diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift index 8d75df8a..b457c1ee 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift @@ -1,24 +1,25 @@ import AIModel import Foundation +import Keychain import Preferences public struct UserPreferenceChatGPTConfiguration: ChatGPTConfiguration { public var chatModelKey: KeyPath>? - + public var temperature: Double { min(max(0, UserDefaults.shared.value(for: \.chatGPTTemperature)), 2) } public var model: ChatModel? { let models = UserDefaults.shared.value(for: \.chatModels) - + if let chatModelKey { let id = UserDefaults.shared.value(for: chatModelKey) if let model = models.first(where: { $0.id == id }) { return model } } - + let id = UserDefaults.shared.value(for: \.defaultChatFeatureChatModelId) return models.first { $0.id == id } ?? models.first @@ -116,9 +117,11 @@ public class OverridingChatGPTConfiguration: ChatGPTConfiguration { public var runFunctionsAutomatically: Bool { overriding.runFunctionsAutomatically ?? configuration.runFunctionsAutomatically } - + public var apiKey: String { - overriding.apiKey ?? configuration.apiKey + if let apiKey = overriding.apiKey { return apiKey } + guard let name = model?.info.apiKeyName else { return configuration.apiKey } + return (try? Keychain.apiKey.get(name)) ?? configuration.apiKey } } From d55b05c2a8c10ae9b3b27c30b5edcc82794faeac Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 14:25:47 +0800 Subject: [PATCH 12/49] Support persisting scopes of chat panel --- Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift | 9 ++++++++- .../ChatContextCollector/ChatContextCollector.swift | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift index 17babd6d..ffde45a9 100644 --- a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift +++ b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift @@ -1,5 +1,7 @@ +import ChatContextCollector import ChatService import ChatTab +import CodableWrappers import Combine import ComposableArchitecture import Foundation @@ -21,6 +23,7 @@ public class ChatGPTChatTab: ChatTab { var configuration: OverridingChatGPTConfiguration.Overriding var systemPrompt: String var extraSystemPrompt: String + var defaultScopes: Set? } struct Builder: ChatTabBuilder { @@ -65,7 +68,8 @@ public class ChatGPTChatTab: ChatTab { history: await service.memory.history, configuration: service.configuration.overriding, systemPrompt: service.systemPrompt, - extraSystemPrompt: service.extraSystemPrompt + extraSystemPrompt: service.extraSystemPrompt, + defaultScopes: service.defaultScopes ) return (try? JSONEncoder().encode(state)) ?? Data() } @@ -79,6 +83,9 @@ public class ChatGPTChatTab: ChatTab { tab.service.configuration.overriding = state.configuration tab.service.mutateSystemPrompt(state.systemPrompt) tab.service.mutateExtraSystemPrompt(state.extraSystemPrompt) + if let scopes = state.defaultScopes { + tab.service.defaultScopes = scopes + } await tab.service.memory.mutateHistory { history in history = state.history } diff --git a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift index 996c3a52..eccd7d03 100644 --- a/Tool/Sources/ChatContextCollector/ChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollector/ChatContextCollector.swift @@ -3,7 +3,7 @@ import OpenAIService import Parsing public struct ChatContext { - public enum Scope: String, Equatable, CaseIterable { + public enum Scope: String, Equatable, CaseIterable, Codable { case file case code case sense From 5f71a9725fbd3ea9818385a950d7a8891dfbfc17 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Tue, 31 Oct 2023 14:26:16 +0800 Subject: [PATCH 13/49] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 0747cfb6..ba91f5c9 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 0747cfb6c32f77f6d37f9caa950be976edd6156a +Subproject commit ba91f5c9624929af756000464b58b04c4ac77631 From 4b34f20be6ed3747dd77f9df4c28c9a581c35873 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 2 Nov 2023 15:51:11 +0800 Subject: [PATCH 14/49] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index ba91f5c9..a66b238d 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit ba91f5c9624929af756000464b58b04c4ac77631 +Subproject commit a66b238d6d00f2a5573f41b06a0d86dd3b7655e4 From 403a91817019afe53928514793230dc776eb56b6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 2 Nov 2023 15:51:19 +0800 Subject: [PATCH 15/49] Add new logger --- Tool/Sources/Logger/Logger.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tool/Sources/Logger/Logger.swift b/Tool/Sources/Logger/Logger.swift index f4d97d1e..8b740ddc 100644 --- a/Tool/Sources/Logger/Logger.swift +++ b/Tool/Sources/Logger/Logger.swift @@ -19,6 +19,7 @@ public final class Logger { public static let gitHubCopilot = Logger(category: "GitHubCopilot") public static let codeium = Logger(category: "Codeium") public static let langchain = Logger(category: "LangChain") + public static let retrieval = Logger(category: "Retrieval") #if DEBUG /// Use a temp logger to log something temporary. I won't be available in release builds. public static let temp = Logger(category: "Temp") From c03ce88468eb7ace61ea33323544627d93a72713 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 2 Nov 2023 15:51:27 +0800 Subject: [PATCH 16/49] Fix tests --- .../Tests/ChatServiceTests/ParseScopesTests.swift | 15 +++++++++++---- .../SwiftFocusedCodeFinderTests.swift | 5 ++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Core/Tests/ChatServiceTests/ParseScopesTests.swift b/Core/Tests/ChatServiceTests/ParseScopesTests.swift index 78c6634f..ebfeb83c 100644 --- a/Core/Tests/ChatServiceTests/ParseScopesTests.swift +++ b/Core/Tests/ChatServiceTests/ParseScopesTests.swift @@ -8,14 +8,21 @@ final class ParseScopesTests: XCTestCase { func test_parse_single_scope() async throws { var prompt = "@web hello" let scopes = parse(&prompt) - XCTAssertEqual(scopes, ["web"]) + XCTAssertEqual(scopes, [.web]) + XCTAssertEqual(prompt, "hello") + } + + func test_parse_single_scope_with_prefix() async throws { + var prompt = "@w hello" + let scopes = parse(&prompt) + XCTAssertEqual(scopes, [.web]) XCTAssertEqual(prompt, "hello") } func test_parse_multiple_spaces() async throws { var prompt = "@web hello" let scopes = parse(&prompt) - XCTAssertEqual(scopes, ["web"]) + XCTAssertEqual(scopes, [.web]) XCTAssertEqual(prompt, "hello") } @@ -27,9 +34,9 @@ final class ParseScopesTests: XCTestCase { } func test_parse_multiple_scopes() async throws { - var prompt = "@web+file+selection hello" + var prompt = "@web+file+c+s+project hello" let scopes = parse(&prompt) - XCTAssertEqual(scopes, ["web", "file", "selection"]) + XCTAssertEqual(scopes, [.web, .code, .sense, .project, .file]) XCTAssertEqual(prompt, "hello") } } diff --git a/Tool/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift b/Tool/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift index c145c5f6..88355c9c 100644 --- a/Tool/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift +++ b/Tool/Tests/ActiveDocumentChatContextCollectorTests/SwiftFocusedCodeFinderTests.swift @@ -355,7 +355,7 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { XCTAssertEqual(context, .init( scope: .top, contextRange: .init(startPair: (0, 0), endPair: (13, 2)), - focusedRange: .init(startPair: (0, 0), endPair: (10, 15)), + focusedRange: .init(startPair: (0, 0), endPair: (13, 2)), focusedCode: """ @MainActor public @@ -368,6 +368,9 @@ final class SwiftFocusedCodeFinder_FocusedCode_Tests: XCTestCase { } func hello() { + print("hello") + print("hello") + } """, imports: [] From 4158c6b31504f27c05b68c359c21dfa0a6964c6c Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 3 Nov 2023 17:05:32 +0800 Subject: [PATCH 17/49] Add ContextAwarePromptToCodeService --- Core/Package.swift | 4 +- .../OpenAIPromptToCodeService.swift | 13 +++- .../PromptToCodeServiceType.swift | 59 +++++++++++++++++-- Pro | 2 +- 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/Core/Package.swift b/Core/Package.swift index 10d744d4..5e55ae02 100644 --- a/Core/Package.swift +++ b/Core/Package.swift @@ -200,7 +200,9 @@ let package = Package( .product(name: "OpenAIService", package: "Tool"), .product(name: "AppMonitoring", package: "Tool"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), - ] + ].pro([ + "ProService", + ]) ), .testTarget(name: "PromptToCodeServiceTests", dependencies: ["PromptToCodeService"]), diff --git a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift index 57c48195..b229cf79 100644 --- a/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift +++ b/Core/Sources/PromptToCodeService/OpenAIPromptToCodeService.swift @@ -166,7 +166,7 @@ public final class OpenAIPromptToCodeService: PromptToCodeServiceType { let secondMessage = """ I will update the code you just provided. - It looks like every line has an indentation of \(indentation) spaces, I will keep that. + Every line has an indentation of \(indentation) spaces, I will keep that. What is your requirement? """ @@ -212,8 +212,14 @@ public final class OpenAIPromptToCodeService: PromptToCodeServiceType { } } } +} + +// MAKR: - Internal - func extractCodeAndDescription(from content: String) -> (code: String, description: String) { +extension OpenAIPromptToCodeService { + func extractCodeAndDescription(from content: String) + -> (code: String, description: String) + { func extractCodeFromMarkdown(_ markdown: String) -> (code: String, endIndex: Int)? { let codeBlockRegex = try! NSRegularExpression( pattern: #"```(?:\w+)?[\n]([\s\S]+?)[\n]```"#, @@ -275,7 +281,8 @@ public final class OpenAIPromptToCodeService: PromptToCodeServiceType { editorInformation: EditorInformation, source: PromptToCodeSource ) -> String { - guard let annotations = editorInformation.editorContent?.lineAnnotations else { return "" } + guard let annotations = editorInformation.editorContent?.lineAnnotations + else { return "" } let all = annotations .lazy .filter { annotation in diff --git a/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift b/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift index 01519d18..6e75002b 100644 --- a/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift +++ b/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift @@ -42,11 +42,6 @@ public struct PromptToCodeServiceDependencyKey: DependencyKey { public static let previewValue: PromptToCodeServiceType = PreviewPromptToCodeService() } -public struct PromptToCodeServiceFactoryDependencyKey: DependencyKey { - public static let liveValue: () -> PromptToCodeServiceType = { OpenAIPromptToCodeService() } - public static let previewValue: () -> PromptToCodeServiceType = { PreviewPromptToCodeService() } -} - public extension DependencyValues { var promptToCodeService: PromptToCodeServiceType { get { self[PromptToCodeServiceDependencyKey.self] } @@ -59,3 +54,57 @@ public extension DependencyValues { } } +#if canImport(ContextAwarePromptToCodeService) + +import ContextAwarePromptToCodeService + +extension ContextAwarePromptToCodeService: PromptToCodeServiceType { + public func modifyCode( + code: String, + requirement: String, + source: PromptToCodeSource, + isDetached: Bool, + extraSystemPrompt: String?, + generateDescriptionRequirement: Bool? + ) async throws -> AsyncThrowingStream<(code: String, description: String), Error> { + try await modifyCode( + code: code, + requirement: requirement, + source: ContextAwarePromptToCodeService.Source( + language: source.language, + documentURL: source.documentURL, + projectRootURL: source.projectRootURL, + allCode: source.allCode, + range: source.range + ), + isDetached: isDetached, + extraSystemPrompt: extraSystemPrompt, + generateDescriptionRequirement: generateDescriptionRequirement + ) + } +} + +public struct PromptToCodeServiceFactoryDependencyKey: DependencyKey { + public static let liveValue: () -> PromptToCodeServiceType = { + ContextAwarePromptToCodeService() + } + + public static let previewValue: () -> PromptToCodeServiceType = { + PreviewPromptToCodeService() + } +} + +#else + +public struct PromptToCodeServiceFactoryDependencyKey: DependencyKey { + public static let liveValue: () -> PromptToCodeServiceType = { + OpenAIPromptToCodeService() + } + + public static let previewValue: () -> PromptToCodeServiceType = { + PreviewPromptToCodeService() + } +} + +#endif + diff --git a/Pro b/Pro index a66b238d..ed56bcae 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit a66b238d6d00f2a5573f41b06a0d86dd3b7655e4 +Subproject commit ed56bcae30b60a181042b466f3142dffc05e79c9 From f47d2d39c0b67bdcf79345803f3a0e810eacfe2f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 6 Nov 2023 17:31:11 +0800 Subject: [PATCH 18/49] Update --- Pro | 2 +- Tool/Sources/XcodeInspector/XcodeInspector.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Pro b/Pro index ed56bcae..0bcafcce 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit ed56bcae30b60a181042b466f3142dffc05e79c9 +Subproject commit 0bcafccef09f79a90ea4f4f330f176d18391dcac diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index c218f021..cd3f54fd 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -141,6 +141,8 @@ public final class XcodeInspector: ObservableObject { } } + #warning("TODO: Double check before releasing 0.27.0") + @MainActor func setActiveXcode(_ xcode: XcodeAppInstanceInspector) { activeApplication = xcode From 582bdc4b35f0414169c8b2f724e834d0aa7317f9 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Mon, 6 Nov 2023 17:31:39 +0800 Subject: [PATCH 19/49] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 0bcafcce..e35f5388 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 0bcafccef09f79a90ea4f4f330f176d18391dcac +Subproject commit e35f53889ee2abad8777f53547deeb1974afcec7 From 4209f86656a404a5f8a8536a3d5035ba7afdc760 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 2 Nov 2023 16:27:37 +0800 Subject: [PATCH 20/49] Make chat window title bar and traffic light button bigger --- .../SuggestionWidget/ChatWindowView.swift | 150 ++++++++++-------- Core/Sources/SuggestionWidget/Styles.swift | 2 + .../SuggestionWidgetController.swift | 4 +- 3 files changed, 87 insertions(+), 69 deletions(-) diff --git a/Core/Sources/SuggestionWidget/ChatWindowView.swift b/Core/Sources/SuggestionWidget/ChatWindowView.swift index 9037cf51..00fce575 100644 --- a/Core/Sources/SuggestionWidget/ChatWindowView.swift +++ b/Core/Sources/SuggestionWidget/ChatWindowView.swift @@ -52,56 +52,36 @@ struct ChatWindowView: View { struct ChatTitleBar: View { let store: StoreOf @State var isHovering = false - @Environment(\.controlActiveState) var controlActiveState var body: some View { - HStack(spacing: 4) { - Button(action: { - store.send(.hideButtonClicked) - }) { - Circle() - .fill( - controlActiveState == .key - ? Color(nsColor: .systemOrange) - : Color(nsColor: .disabledControlTextColor) - ) - .frame(width: 10, height: 10) - .shadow(radius: 0.5) - .overlay { - if isHovering { - Image(systemName: "minus") - .resizable() - .foregroundStyle(.black.opacity(0.7)) - .font(Font.title.weight(.heavy)) - .frame(width: 5, height: 1) - } - } + HStack(spacing: 6) { + TrafficLightButton( + isHovering: isHovering, + isActive: true, + color: Color(nsColor: .systemOrange), + action: { + store.send(.hideButtonClicked) + } + ) { + Image(systemName: "minus") + .foregroundStyle(.black.opacity(0.5)) + .font(Font.system(size: 8).weight(.heavy)) } .keyboardShortcut("m", modifiers: [.command]) WithViewStore(store, observe: { $0.chatPanelInASeparateWindow }) { viewStore in - Button(action: { - store.send(.toggleChatPanelDetachedButtonClicked) - }) { - Circle() - .fill( - controlActiveState == .key && viewStore.state - ? Color(nsColor: .systemCyan) - : Color(nsColor: .disabledControlTextColor) - ) - .frame(width: 10, height: 10) - .shadow(radius: 0.5) - .disabled(!viewStore.state) - .overlay { - if isHovering { - Image(systemName: "pin") - .resizable() - .foregroundStyle(.black.opacity(0.7)) - .font(Font.title.weight(.heavy)) - .frame(width: 4, height: 6) - .transformEffect(.init(translationX: 0, y: 0.5)) - } - } + TrafficLightButton( + isHovering: isHovering, + isActive: viewStore.state, + color: Color(nsColor: .systemCyan), + action: { + store.send(.toggleChatPanelDetachedButtonClicked) + } + ) { + Image(systemName: "pin.fill") + .foregroundStyle(.black.opacity(0.5)) + .font(Font.system(size: 6).weight(.black)) + .transformEffect(.init(translationX: 0, y: 0.5)) } } @@ -131,11 +111,47 @@ struct ChatTitleBar: View { .padding(.horizontal, 6) .padding(.top, 1) .frame(maxWidth: .infinity) - .frame(height: 16) + .frame(height: Style.chatWindowTitleBarHeight) .onHover(perform: { hovering in isHovering = hovering }) } + + struct TrafficLightButton: View { + let isHovering: Bool + let isActive: Bool + let color: Color + let action: () -> Void + let icon: () -> Icon + + @Environment(\.controlActiveState) var controlActiveState + + var body: some View { + Button(action: { + action() + }) { + Circle() + .fill( + controlActiveState == .key && isActive + ? color + : Color(nsColor: .separatorColor) + ) + .frame( + width: Style.trafficLightButtonSize, + height: Style.trafficLightButtonSize + ) + .overlay{ + Circle().stroke(lineWidth: 0.5).foregroundColor(.black.opacity(0.2)) + } + .overlay { + if isHovering { + icon() + } + } + } + .focusable(false) + } + } } private extension View { @@ -315,29 +331,29 @@ struct ChatTabBarButton: View { icon().foregroundColor(.secondary) content() } - .font(.callout) - .lineLimit(1) - .frame(maxWidth: 120) - .padding(.horizontal, 28) - .contentShape(Rectangle()) - .onTapGesture { - store.send(.tabClicked(id: info.id)) - } - .overlay(alignment: .leading) { - Button(action: { - store.send(.closeTabButtonClicked(id: info.id)) - }) { - Image(systemName: "xmark") - .foregroundColor(.secondary) - } - .buttonStyle(.plain) - .padding(2) - .padding(.leading, 8) - .opacity(isHovered ? 1 : 0) + .font(.callout) + .lineLimit(1) + .frame(maxWidth: 120) + .padding(.horizontal, 28) + .contentShape(Rectangle()) + .onTapGesture { + store.send(.tabClicked(id: info.id)) + } + .overlay(alignment: .leading) { + Button(action: { + store.send(.closeTabButtonClicked(id: info.id)) + }) { + Image(systemName: "xmark") + .foregroundColor(.secondary) } - .onHover { isHovered = $0 } - .animation(.linear(duration: 0.1), value: isHovered) - .animation(.linear(duration: 0.1), value: isSelected) + .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/Styles.swift b/Core/Sources/SuggestionWidget/Styles.swift index d520702b..d1e0e102 100644 --- a/Core/Sources/SuggestionWidget/Styles.swift +++ b/Core/Sources/SuggestionWidget/Styles.swift @@ -11,6 +11,8 @@ enum Style { static let widgetHeight: Double = 20 static var widgetWidth: Double { widgetHeight } static let widgetPadding: Double = 4 + static let chatWindowTitleBarHeight: Double = 24 + static let trafficLightButtonSize: Double = 12 } extension Color { diff --git a/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift b/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift index 4918b128..cc252db4 100644 --- a/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift +++ b/Core/Sources/SuggestionWidget/SuggestionWidgetController.swift @@ -256,7 +256,7 @@ extension SuggestionWidgetController: NSWindowDelegate { .frame ?? .zero var mouseLocation = NSEvent.mouseLocation let windowFrame = chatPanelWindow.frame - if mouseLocation.y > windowFrame.maxY - 16, + if mouseLocation.y > windowFrame.maxY - Style.chatWindowTitleBarHeight, mouseLocation.y < windowFrame.maxY, mouseLocation.x > windowFrame.minX, mouseLocation.x < windowFrame.maxX @@ -291,7 +291,7 @@ class ChatWindow: NSWindow { override func mouseDown(with event: NSEvent) { let windowFrame = frame let currentLocation = event.locationInWindow - if currentLocation.y > windowFrame.size.height - 16, + if currentLocation.y > windowFrame.size.height - Style.chatWindowTitleBarHeight, currentLocation.y < windowFrame.size.height, currentLocation.x > 0, currentLocation.x < windowFrame.width From 01677349262bd38bda090aa454c3233e9723352d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 8 Nov 2023 11:51:21 +0800 Subject: [PATCH 21/49] Update focus code finder to return more info about contexts --- Pro | 2 +- .../ActiveDocumentChatContextCollector.swift | 2 +- .../ActiveDocumentContext.swift | 18 ++++++++++--- .../FocusedCodeFinder/FocusedCodeFinder.swift | 12 +++++---- .../SwiftFocusedCodeFinder.swift | 27 ++++++++++++++++--- .../SuggestionModel/ExportedFromLSP.swift | 1 + 6 files changed, 48 insertions(+), 14 deletions(-) diff --git a/Pro b/Pro index e35f5388..970fa1b4 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit e35f53889ee2abad8777f53547deeb1974afcec7 +Subproject commit 970fa1b437254ac0c0578ae9c5d8ad8315141a7e diff --git a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift index 0c55f851..d1cdeb3c 100644 --- a/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift +++ b/Tool/Sources/ChatContextCollectors/ActiveDocumentChatContextCollector/ActiveDocumentChatContextCollector.swift @@ -112,7 +112,7 @@ public final class ActiveDocumentChatContextCollector: ChatContextCollector { : """ Focused Context: ``` - \(focusedContext.context.joined(separator: "\n")) + \(focusedContext.context.map(\.signature).joined(separator: "\n")) ``` """ diff --git a/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift b/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift index 6b4d9525..f10977d4 100644 --- a/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift +++ b/Tool/Sources/FocusedCodeFinder/ActiveDocumentContext.swift @@ -13,7 +13,19 @@ public struct ActiveDocumentContext { public var imports: [String] public struct FocusedContext { - public var context: [String] + public struct Context: Equatable { + public var signature: String + public var name: String + public var range: CursorRange + + public init(signature: String, name: String, range: CursorRange) { + self.signature = signature + self.name = name + self.range = range + } + } + + public var context: [Context] public var contextRange: CursorRange public var codeRange: CursorRange public var code: String @@ -21,7 +33,7 @@ public struct ActiveDocumentContext { public var otherLineAnnotations: [EditorInformation.LineAnnotation] public init( - context: [String], + context: [Context], contextRange: CursorRange, codeRange: CursorRange, code: String, @@ -109,7 +121,7 @@ public struct ActiveDocumentContext { } focusedContext = .init( - context: codeContext.scopeSignatures, + context: codeContext.scopeContexts, contextRange: codeContext.contextRange, codeRange: codeContext.focusedRange, code: codeContext.focusedCode, diff --git a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift index a571da5d..ee37adea 100644 --- a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift @@ -2,20 +2,22 @@ import Foundation import SuggestionModel public struct CodeContext: Equatable { + public typealias ScopeContext = ActiveDocumentContext.FocusedContext.Context + public enum Scope: Equatable { case file case top - case scope(signature: [String]) + case scope(signature: [ScopeContext]) } - public var scopeSignatures: [String] { + public var scopeContexts: [ScopeContext] { switch scope { case .file: return [] case .top: - return ["Top level of the file"] - case let .scope(signature): - return signature + return [] + case let .scope(contexts): + return contexts } } diff --git a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift index 53387616..9f5d6327 100644 --- a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift @@ -100,7 +100,7 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinder { } var contextRange = CursorRange.zero - var signature = [String]() + var signature = [CodeContext.ScopeContext]() while let node = nodes.first { nodes.removeFirst() @@ -114,7 +114,11 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinder { if let context { contextRange = context.contextRange - signature.insert(context.signature, at: 0) + signature.insert(.init( + signature: context.signature, + name: context.name, + range: context.contextRange + ), at: 0) } if !more { @@ -135,6 +139,7 @@ public struct SwiftFocusedCodeFinder: FocusedCodeFinder { extension SwiftFocusedCodeFinder { struct ContextInfo { var signature: String + var name: String var contextRange: CursorRange var canBeUsedAsCodeRange: Bool = true } @@ -163,6 +168,7 @@ extension SwiftFocusedCodeFinder { .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), + name: name, contextRange: convertRange(node) ), false) @@ -174,6 +180,7 @@ extension SwiftFocusedCodeFinder { .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), + name: name, contextRange: convertRange(node) ), false) @@ -185,6 +192,7 @@ extension SwiftFocusedCodeFinder { .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), + name: name, contextRange: convertRange(node) ), false) @@ -196,6 +204,7 @@ extension SwiftFocusedCodeFinder { .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: ""), + name: name, contextRange: convertRange(node) ), false) @@ -206,6 +215,7 @@ extension SwiftFocusedCodeFinder { signature: "\(type) \(name)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), + name: name, contextRange: convertRange(node) ), false) @@ -217,6 +227,7 @@ extension SwiftFocusedCodeFinder { .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), + name: name, contextRange: convertRange(node) ), false) @@ -228,6 +239,7 @@ extension SwiftFocusedCodeFinder { .prefixedModifiers(node.modifierAndAttributeText(extractText)) .suffixedInheritance(node.inheritanceClauseTexts(extractText)) .replacingOccurrences(of: "\n", with: " "), + name: name, contextRange: convertRange(node) ), false) @@ -242,6 +254,7 @@ extension SwiftFocusedCodeFinder { return (.init( signature: "\(type) \(name)\(signature)" .prefixedModifiers(node.modifierAndAttributeText(extractText)), + name: name, contextRange: convertRange(node) ), true) @@ -254,6 +267,7 @@ extension SwiftFocusedCodeFinder { signature: "\(type) \(name)\(signature.isEmpty ? "" : "\(signature)")" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), + name: name, contextRange: convertRange(node) ), true) @@ -265,6 +279,7 @@ extension SwiftFocusedCodeFinder { signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), + name: keyword, contextRange: convertRange(node) ), true) @@ -278,6 +293,7 @@ extension SwiftFocusedCodeFinder { signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), + name: "subscript", contextRange: convertRange(node) ), true) @@ -288,7 +304,7 @@ extension SwiftFocusedCodeFinder { signature: "\(signature)" .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), - + name: "init", contextRange: convertRange(node) ), true) @@ -299,7 +315,7 @@ extension SwiftFocusedCodeFinder { signature: signature .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), - + name: "deinit", contextRange: convertRange(node) ), true) @@ -308,6 +324,7 @@ extension SwiftFocusedCodeFinder { return (.init( signature: signature.replacingOccurrences(of: "\n", with: " "), + name: "closure", contextRange: convertRange(node) ), true) @@ -316,6 +333,7 @@ extension SwiftFocusedCodeFinder { return (.init( signature: signature.replacingOccurrences(of: "\n", with: " "), + name: "function call", contextRange: convertRange(node), canBeUsedAsCodeRange: false ), true) @@ -323,6 +341,7 @@ extension SwiftFocusedCodeFinder { case let node as SwitchCaseSyntax: return (.init( signature: node.trimmedDescription.replacingOccurrences(of: "\n", with: " "), + name: "switch", contextRange: convertRange(node) ), true) diff --git a/Tool/Sources/SuggestionModel/ExportedFromLSP.swift b/Tool/Sources/SuggestionModel/ExportedFromLSP.swift index 9d7d364b..6bad79d5 100644 --- a/Tool/Sources/SuggestionModel/ExportedFromLSP.swift +++ b/Tool/Sources/SuggestionModel/ExportedFromLSP.swift @@ -1,5 +1,6 @@ import LanguageServerProtocol +/// Line starts at 0. public typealias CursorPosition = LanguageServerProtocol.Position public extension CursorPosition { From 8923f3966ec89b34e38f5a3fdf7f0d651048c4b3 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 8 Nov 2023 12:33:56 +0800 Subject: [PATCH 22/49] Prevent variable declaration being used as code range --- Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift index 9f5d6327..0da2abe7 100644 --- a/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/SwiftFocusedCodeFinder.swift @@ -268,7 +268,8 @@ extension SwiftFocusedCodeFinder { .prefixedModifiers(node.modifierAndAttributeText(extractText)) .replacingOccurrences(of: "\n", with: " "), name: name, - contextRange: convertRange(node) + contextRange: convertRange(node), + canBeUsedAsCodeRange: false ), true) case let node as AccessorDeclSyntax: From 585df5653e7206412e45c7715037e90b4c25c44d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 8 Nov 2023 17:23:05 +0800 Subject: [PATCH 23/49] Support proService clean up --- Core/Sources/Service/ScheduledCleaner.swift | 28 ++++++++++----------- Core/Sources/Service/Service.swift | 4 ++- Pro | 2 +- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Core/Sources/Service/ScheduledCleaner.swift b/Core/Sources/Service/ScheduledCleaner.swift index 66f1d438..c6c24a86 100644 --- a/Core/Sources/Service/ScheduledCleaner.swift +++ b/Core/Sources/Service/ScheduledCleaner.swift @@ -7,16 +7,9 @@ import Workspace import XcodeInspector public final class ScheduledCleaner { - let workspacePool: WorkspacePool - let guiController: GraphicalUserInterfaceController + weak var service: Service? - init( - workspacePool: WorkspacePool, - guiController: GraphicalUserInterfaceController - ) { - self.workspacePool = workspacePool - self.guiController = guiController - } + init() {} func start() { Task { @ServiceActor in @@ -38,6 +31,8 @@ public final class ScheduledCleaner { @ServiceActor func cleanUp() async { + guard let service else { return } + let workspaceInfos = XcodeInspector.shared.xcodes.reduce( into: [ XcodeAppInstanceInspector.WorkspaceIdentifier: @@ -53,18 +48,18 @@ public final class ScheduledCleaner { } } } - for (url, workspace) in workspacePool.workspaces { + for (url, workspace) in service.workspacePool.workspaces { if workspace.isExpired, workspaceInfos[.url(url)] == nil { Logger.service.info("Remove idle workspace") _ = await Task { @MainActor in - guiController.viewStore.send( + service.guiController.viewStore.send( .promptToCodeGroup(.discardExpiredPromptToCode(documentURLs: Array( workspace.filespaces.keys ))) ) }.result await workspace.cleanUp(availableTabs: []) - workspacePool.removeWorkspace(url: url) + service.workspacePool.removeWorkspace(url: url) } else { let tabs = (workspaceInfos[.url(url)]?.tabs ?? []) .union(workspaceInfos[.unknown]?.tabs ?? []) @@ -77,7 +72,7 @@ public final class ScheduledCleaner { ) { Logger.service.info("Remove idle filespace") _ = await Task { @MainActor in - guiController.viewStore.send( + service.guiController.viewStore.send( .promptToCodeGroup(.discardExpiredPromptToCode(documentURLs: [url])) ) }.result @@ -87,11 +82,16 @@ public final class ScheduledCleaner { await workspace.cleanUp(availableTabs: tabs) } } + + #if canImport(ProService) + await service.proService.cleanUp(workspaceInfos: workspaceInfos) + #endif } @ServiceActor public func closeAllChildProcesses() async { - for (_, workspace) in workspacePool.workspaces { + guard let service else { return } + for (_, workspace) in service.workspacePool.workspaces { await workspace.terminateSuggestionService() } } diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index c5903ad4..6fe76e40 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -38,7 +38,7 @@ public final class Service { private init() { @Dependency(\.workspacePool) var workspacePool - scheduledCleaner = .init(workspacePool: workspacePool, guiController: guiController) + scheduledCleaner = .init() workspacePool.registerPlugin { SuggestionServiceWorkspacePlugin(workspace: $0) } self.workspacePool = workspacePool globalShortcutManager = .init(guiController: guiController) @@ -52,6 +52,8 @@ public final class Service { ProService() } #endif + + scheduledCleaner.service = self } @MainActor diff --git a/Pro b/Pro index 970fa1b4..b72cf6f6 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 970fa1b437254ac0c0578ae9c5d8ad8315141a7e +Subproject commit b72cf6f6a481fe30f20bb5f1de66b35d19b6574c From 4740bae9a7656d9395b5b6cae2a4f5ca90ebf726 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Wed, 8 Nov 2023 17:23:26 +0800 Subject: [PATCH 24/49] Add keys --- .../FeatureSettings/SuggestionSettingsView.swift | 10 ++++++++++ Tool/Sources/Preferences/Keys.swift | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift index d63a819d..f2896a61 100644 --- a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift @@ -28,6 +28,8 @@ struct SuggestionSettingsView: View { var suggestionDisplayCompactMode @AppStorage(\.acceptSuggestionWithTab) var acceptSuggestionWithTab + @AppStorage(\.isSuggestionSenseEnabled) + var isSuggestionSenseEnabled init() {} } @@ -67,6 +69,14 @@ struct SuggestionSettingsView: View { Text("Real-time Suggestion") } + #if canImport(ProHostApp) + WithFeatureEnabled(\.suggestionSense) { + Toggle(isOn: $settings.isSuggestionSenseEnabled) { + Text("Suggestion Sense") + } + } + #endif + #if canImport(ProHostApp) WithFeatureEnabled(\.tabToAcceptSuggestion) { Toggle(isOn: $settings.acceptSuggestionWithTab) { diff --git a/Tool/Sources/Preferences/Keys.swift b/Tool/Sources/Preferences/Keys.swift index aac8fd99..7f49a153 100644 --- a/Tool/Sources/Preferences/Keys.swift +++ b/Tool/Sources/Preferences/Keys.swift @@ -325,6 +325,10 @@ public extension UserDefaultPreferenceKeys { var acceptSuggestionWithTab: PreferenceKey { .init(defaultValue: true, key: "AcceptSuggestionWithTab") } + + var isSuggestionSenseEnabled: PreferenceKey { + .init(defaultValue: false, key: "IsSuggestionSenseEnabled") + } } // MARK: - Chat From 1f3650a130a86de12069ff7a6d55fd6f950e59de Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 00:22:47 +0800 Subject: [PATCH 25/49] Reset to use old prompt to code service --- Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift b/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift index 6e75002b..1716eb12 100644 --- a/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift +++ b/Core/Sources/PromptToCodeService/PromptToCodeServiceType.swift @@ -86,7 +86,7 @@ extension ContextAwarePromptToCodeService: PromptToCodeServiceType { public struct PromptToCodeServiceFactoryDependencyKey: DependencyKey { public static let liveValue: () -> PromptToCodeServiceType = { - ContextAwarePromptToCodeService() + OpenAIPromptToCodeService() } public static let previewValue: () -> PromptToCodeServiceType = { From fd84744802f05542add46f271780bc1a86397028 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 00:22:54 +0800 Subject: [PATCH 26/49] Update --- Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift | 4 +++- .../HostApp/FeatureSettings/SuggestionSettingsView.swift | 2 +- Pro | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index 412a809f..7700c857 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -320,7 +320,9 @@ struct ChatSettingsView: View { } Scope( - title: WithFeatureEnabled(\.projectScopeInChat) { Text("Project Scope") }, + title: WithFeatureEnabled(\.projectScopeInChat) { + Text("Project Scope (Experimental)") + }, description: "Experimental. Enable the bot to search code symbols in the project, third party packages and the SDK." ) { WithFeatureEnabled(\.projectScopeInChat, alignment: .hidden) { diff --git a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift index f2896a61..7094ac3f 100644 --- a/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/SuggestionSettingsView.swift @@ -72,7 +72,7 @@ struct SuggestionSettingsView: View { #if canImport(ProHostApp) WithFeatureEnabled(\.suggestionSense) { Toggle(isOn: $settings.isSuggestionSenseEnabled) { - Text("Suggestion Sense") + Text("Suggestion Cheatsheet (Experimental)") } } #endif diff --git a/Pro b/Pro index b72cf6f6..feddadd7 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit b72cf6f6a481fe30f20bb5f1de66b35d19b6574c +Subproject commit feddadd7f1baf34bc15d0d17c4b396363473fa48 From 4a3959168f83580a748769965e84cbfcb1f0ae8d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 00:38:00 +0800 Subject: [PATCH 27/49] Add new OpenAI model --- Tool/Sources/AIModel/ChatModel.swift | 4 ++++ .../Preferences/Types/ChatGPTModel.swift | 22 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/AIModel/ChatModel.swift b/Tool/Sources/AIModel/ChatModel.swift index d132b5dd..03aee079 100644 --- a/Tool/Sources/AIModel/ChatModel.swift +++ b/Tool/Sources/AIModel/ChatModel.swift @@ -31,6 +31,8 @@ public struct ChatModel: Codable, Equatable, Identifiable { public var maxTokens: Int @FallbackDecoding public var supportsFunctionCalling: Bool + @FallbackDecoding + public var supportsOpenAIAPI2023_11: Bool @FallbackDecoding public var modelName: String public var azureOpenAIDeploymentName: String { @@ -43,12 +45,14 @@ public struct ChatModel: Codable, Equatable, Identifiable { baseURL: String = "", maxTokens: Int = 4000, supportsFunctionCalling: Bool = true, + supportsOpenAIAPI2023_11: Bool = false, modelName: String = "" ) { self.apiKeyName = apiKeyName self.baseURL = baseURL self.maxTokens = maxTokens self.supportsFunctionCalling = supportsFunctionCalling + self.supportsOpenAIAPI2023_11 = supportsOpenAIAPI2023_11 self.modelName = modelName } } diff --git a/Tool/Sources/Preferences/Types/ChatGPTModel.swift b/Tool/Sources/Preferences/Types/ChatGPTModel.swift index 3f454240..9531b55d 100644 --- a/Tool/Sources/Preferences/Types/ChatGPTModel.swift +++ b/Tool/Sources/Preferences/Types/ChatGPTModel.swift @@ -7,8 +7,11 @@ public enum ChatGPTModel: String { case gpt432k = "gpt-4-32k" case gpt40314 = "gpt-4-0314" case gpt40613 = "gpt-4-0613" + case gpt41106Preview = "gpt-4-1106-preview" + case gpt4VisionPreview = "gpt-4-vision-preview" case gpt35Turbo0301 = "gpt-3.5-turbo-0301" case gpt35Turbo0613 = "gpt-3.5-turbo-0613" + case gpt35Turbo1106 = "gpt-3.5-turbo-1106" case gpt35Turbo16k0613 = "gpt-3.5-turbo-16k-0613" case gpt432k0314 = "gpt-4-32k-0314" case gpt432k0613 = "gpt-4-32k-0613" @@ -31,14 +34,29 @@ public extension ChatGPTModel { return 4096 case .gpt35Turbo0613: return 4096 + case .gpt35Turbo1106: + return 16385 case .gpt35Turbo16k: - return 16384 + return 16385 case .gpt35Turbo16k0613: - return 16384 + return 16385 case .gpt40613: return 8192 case .gpt432k0613: return 32768 + case .gpt41106Preview: + return 128000 + case .gpt4VisionPreview: + return 128000 + } + } + + var supportsImages: Bool { + switch self { + case .gpt4VisionPreview: + return true + default: + return false } } } From 18a5b34a2a678f21ec26ddc2150da2bc4e76c027 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 12:22:38 +0800 Subject: [PATCH 28/49] Adjust implementation of chat panel --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 100 ++++++++++---------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index f8d12923..970ef5ec 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -66,19 +66,16 @@ struct ChatPanelMessages: View { } Spacer(minLength: 12) - .onAppear { - withAnimation { - proxy.scrollTo(bottomID, anchor: .bottom) - } - } .id(bottomID) + .task { + proxy.scrollTo(bottomID, anchor: .bottom) + } .background(GeometryReader { geo in let offset = geo.frame(in: .named(scrollSpace)).minY - Color.clear - .preference( - key: ScrollViewOffsetPreferenceKey.self, - value: offset - ) + Color.clear.preference( + key: ScrollViewOffsetPreferenceKey.self, + value: offset + ) }) .preference( key: ListHeightPreferenceKey.self, @@ -96,12 +93,16 @@ struct ChatPanelMessages: View { .listStyle(.plain) .coordinateSpace(name: scrollSpace) .onPreferenceChange(ListHeightPreferenceKey.self) { value in - listHeight = value - updatePinningState() + Task { @MainActor in + listHeight = value + updatePinningState() + } } .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in - scrollOffset = value - updatePinningState() + Task { @MainActor in + scrollOffset = value + updatePinningState() + } } .overlay(alignment: .bottom) { WithViewStore(chat, observe: \.isReceivingMessage) { viewStore in @@ -114,51 +115,54 @@ struct ChatPanelMessages: View { } } .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) - .shadow(color: .black.opacity(0.2), radius: 2) - } - .overlay { - Circle().stroke(Color(nsColor: .separatorColor), lineWidth: 1) - } - .foregroundStyle(.secondary) - .padding(4) - } - .keyboardShortcut(.downArrow, modifiers: [.command]) - .opacity(pinnedToBottom ? 0 : 1) - .buttonStyle(.plain) - .onChange(of: viewStore.state) { _ in - if pinnedToBottom || isInitialLoad { - if isInitialLoad { - isInitialLoad = false - } - withAnimation { - proxy.scrollTo(bottomID, anchor: .bottom) - } - } - } - } + scrollToBottomButton(proxy: proxy) } } } } func updatePinningState() { - if scrollOffset > listHeight + 24 + 100 || scrollOffset <= 0 { + if scrollOffset > listHeight + 30 + 100 || scrollOffset <= 0 { pinnedToBottom = false } else { pinnedToBottom = true } } + + @ViewBuilder + func scrollToBottomButton(proxy: ScrollViewProxy) -> some View { + 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) + .shadow(color: .black.opacity(0.2), radius: 2) + } + .overlay { + Circle().stroke(Color(nsColor: .separatorColor), lineWidth: 1) + } + .foregroundStyle(.secondary) + .padding(4) + } + .keyboardShortcut(.downArrow, modifiers: [.command]) + .opacity(pinnedToBottom ? 0 : 1) + .buttonStyle(.plain) + .onChange(of: viewStore.state) { _ in + if pinnedToBottom || isInitialLoad { + if isInitialLoad { + isInitialLoad = false + } + proxy.scrollTo(bottomID, anchor: .bottom) + } + } + } + } } struct ChatHistory: View { From ab97bad7bf3428e9d99dc5bcfc32f46bd329b256 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 15:33:52 +0800 Subject: [PATCH 29/49] Tweak pin to bottom behavior of chat panel --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 121 +++++++++++++------- 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index 970ef5ec..feb99e51 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -40,12 +40,12 @@ private struct ListHeightPreferenceKey: PreferenceKey { struct ChatPanelMessages: View { let chat: StoreOf - @State var pinnedToBottom = true + @State var isScrollToBottomButtonDisplayed = true + @State var isPinnedToBottom = 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 { ScrollViewReader { proxy in @@ -67,6 +67,9 @@ struct ChatPanelMessages: View { Spacer(minLength: 12) .id(bottomID) + .onAppear { + proxy.scrollTo(bottomID, anchor: .bottom) + } .task { proxy.scrollTo(bottomID, anchor: .bottom) } @@ -93,16 +96,23 @@ struct ChatPanelMessages: View { .listStyle(.plain) .coordinateSpace(name: scrollSpace) .onPreferenceChange(ListHeightPreferenceKey.self) { value in - Task { @MainActor in - listHeight = value - updatePinningState() - } + listHeight = value + updatePinningState() } .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in - Task { @MainActor in - scrollOffset = value - updatePinningState() + /// I don't know if there is a way to detect that a scroll is triggered by user + let scrollUpToThreshold = listHeight > 0 // sometimes it can suddenly become 0 + && value > listHeight + 32 + 20 // scroll up to a threshold + && value > scrollOffset // it's scroll up + && value - scrollOffset < 100 // it's not some mystery jump + /// Scroll up too much and the tracker is lost + let checkerOutOfScope = value <= 0 + if checkerOutOfScope || scrollUpToThreshold { + isPinnedToBottom = false } + + scrollOffset = value + updatePinningState() } .overlay(alignment: .bottom) { WithViewStore(chat, observe: \.isReceivingMessage) { viewStore in @@ -117,49 +127,82 @@ struct ChatPanelMessages: View { .overlay(alignment: .bottomTrailing) { scrollToBottomButton(proxy: proxy) } + .background { + PinToBottomHandler(chat: chat, pinnedToBottom: $isPinnedToBottom) { + proxy.scrollTo(bottomID, anchor: .bottom) + } + } } } } + @MainActor func updatePinningState() { - if scrollOffset > listHeight + 30 + 100 || scrollOffset <= 0 { - pinnedToBottom = false - } else { - pinnedToBottom = true + // where does the 32 come from? + withAnimation { + isScrollToBottomButtonDisplayed = scrollOffset > listHeight + 32 + 20 + || scrollOffset <= 0 } } @ViewBuilder func scrollToBottomButton(proxy: ScrollViewProxy) -> some View { - WithViewStore(chat, observe: \.history.last) { viewStore in - Button(action: { - withAnimation(.easeInOut(duration: 0.1)) { - proxy.scrollTo(bottomID, anchor: .bottom) + Button(action: { + withAnimation(.easeInOut(duration: 0.1)) { + proxy.scrollTo(bottomID, anchor: .bottom) + } + }) { + Image(systemName: "arrow.down") + .padding(4) + .background { + Circle() + .fill(.thickMaterial) + .shadow(color: .black.opacity(0.2), radius: 2) } - }) { - Image(systemName: "arrow.down") - .padding(4) - .background { - Circle() - .fill(.thickMaterial) - .shadow(color: .black.opacity(0.2), radius: 2) - } - .overlay { - Circle().stroke(Color(nsColor: .separatorColor), lineWidth: 1) + .overlay { + Circle().stroke(Color(nsColor: .separatorColor), lineWidth: 1) + } + .foregroundStyle(.secondary) + .padding(4) + } + .keyboardShortcut(.downArrow, modifiers: [.command]) + .opacity(isScrollToBottomButtonDisplayed ? 1 : 0) + .buttonStyle(.plain) + } + + struct PinToBottomHandler: View { + let chat: StoreOf + @Binding var pinnedToBottom: Bool + let scrollToBottom: () -> Void + + @State var isInitialLoad = true + + struct PinToBottomRelatedState: Equatable { + var isReceivingMessage: Bool + var lastMessage: ChatMessage? + } + + var body: some View { + WithViewStore(chat, observe: { + PinToBottomRelatedState( + isReceivingMessage: $0.isReceivingMessage, + lastMessage: $0.history.last + ) + }) { viewStore in + EmptyView() + .onChange(of: viewStore.state.isReceivingMessage) { isReceiving in + if isReceiving { + pinnedToBottom = true + } } - .foregroundStyle(.secondary) - .padding(4) - } - .keyboardShortcut(.downArrow, modifiers: [.command]) - .opacity(pinnedToBottom ? 0 : 1) - .buttonStyle(.plain) - .onChange(of: viewStore.state) { _ in - if pinnedToBottom || isInitialLoad { - if isInitialLoad { - isInitialLoad = false + .onChange(of: viewStore.state.lastMessage) { _ in + if pinnedToBottom || isInitialLoad { + if isInitialLoad { + isInitialLoad = false + } + scrollToBottom() + } } - proxy.scrollTo(bottomID, anchor: .bottom) - } } } } From 8330640eb57dfcac8bebd2b71ddaf8ac2bf4d2b2 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 15:49:53 +0800 Subject: [PATCH 30/49] Move context system prompt to right next to the latest message --- .../DynamicContextController.swift | 5 ++-- .../Memory/AutoManagedChatGPTMemory.swift | 28 ++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Core/Sources/ChatService/DynamicContextController.swift b/Core/Sources/ChatService/DynamicContextController.swift index 2f905999..2f7cf014 100644 --- a/Core/Sources/ChatService/DynamicContextController.swift +++ b/Core/Sources/ChatService/DynamicContextController.swift @@ -92,7 +92,7 @@ final class DynamicContextController { return contexts } - let extraSystemPrompt = contexts + let contextSystemPrompt = contexts .map(\.systemPrompt) .filter { !$0.isEmpty } .joined(separator: "\n\n") @@ -104,9 +104,10 @@ final class DynamicContextController { let contextualSystemPrompt = """ \(language.isEmpty ? "" : "You must always reply in \(language)") - \(systemPrompt)\(extraSystemPrompt.isEmpty ? "" : "\n\(extraSystemPrompt)") + \(systemPrompt) """ await memory.mutateSystemPrompt(contextualSystemPrompt) + await memory.mutateContextSystemPrompt(contextSystemPrompt) 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 9a117d63..0a4f36e6 100644 --- a/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift +++ b/Tool/Sources/OpenAIService/Memory/AutoManagedChatGPTMemory.swift @@ -9,6 +9,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { public private(set) var remainingTokens: Int? public var systemPrompt: String + public var contextSystemPrompt: String public var retrievedContent: [String] = [] public var history: [ChatMessage] = [] { didSet { onHistoryChange() } @@ -27,6 +28,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { functionProvider: ChatGPTFunctionProvider ) { self.systemPrompt = systemPrompt + contextSystemPrompt = "" self.configuration = configuration self.functionProvider = functionProvider _ = Self.encoder // force pre-initialize @@ -40,6 +42,10 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { systemPrompt = newPrompt } + public func mutateContextSystemPrompt(_ newPrompt: String) { + contextSystemPrompt = newPrompt + } + public func mutateRetrievedContent(_ newContent: [String]) { retrievedContent = newContent } @@ -67,6 +73,8 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { /// [Retrieved Content B] /// [Functions] priority: high /// [Message History] priority: medium + /// [Context System Prompt] priority: high + /// [Latest Message] priority: high /// ``` func generateSendingHistory( maxNumberOfMessages: Int = UserDefaults.shared.value(for: \.chatGPTMaxMessageCount), @@ -80,7 +88,12 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { } var smallestSystemPromptMessage = ChatMessage(role: .system, content: systemPrompt) + var contextSystemPromptMessage = ChatMessage(role: .system, content: contextSystemPrompt) let smallestSystemMessageTokenCount = countToken(&smallestSystemPromptMessage) + let contextSystemPromptTokenCount = !contextSystemPrompt.isEmpty + ? countToken(&contextSystemPromptMessage) + : 0 + let functionTokenCount = functionProvider.functions.reduce(into: 0) { partial, function in var count = encoder.countToken(text: function.name) + encoder.countToken(text: function.description) @@ -92,6 +105,7 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { partial += count } let mandatoryContentTokensCount = smallestSystemMessageTokenCount + + contextSystemPromptTokenCount + functionTokenCount + 3 // every reply is primed with <|start|>assistant<|message|> @@ -135,13 +149,13 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { for (index, content) in retrievedContent.filter({ !$0.isEmpty }).enumerated() { if index == 0 { if !appendToSystemPrompt(""" - - + + ## Relevant Content - + Below are information related to the conversation, separated by \(separator) - + """) { break } } else { if !appendToSystemPrompt("\n\(separator)\n") { break } @@ -154,16 +168,22 @@ public actor AutoManagedChatGPTMemory: ChatGPTMemory { let message = ChatMessage(role: .system, content: systemPrompt) allMessages.append(message) } + + if !contextSystemPrompt.isEmpty { + allMessages.insert(contextSystemPromptMessage, at: 1) + } #if DEBUG Logger.service.info(""" Sending tokens count - system prompt: \(smallestSystemMessageTokenCount) + - context system prompt: \(contextSystemPromptTokenCount) - functions: \(functionTokenCount) - messages: \(messageTokenCount) - retrieved content: \(retrievedContentTokenCount) - total: \( smallestSystemMessageTokenCount + + contextSystemPromptTokenCount + functionTokenCount + messageTokenCount + retrievedContentTokenCount From 309c70f311f2f3f5616803d365bf5b1ae3caf585 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 16:45:56 +0800 Subject: [PATCH 31/49] Track scroll event to disable pin to bottom --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 85 ++++++++------------- 1 file changed, 33 insertions(+), 52 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index feb99e51..ae90a071 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -1,4 +1,5 @@ import AppKit +import Combine import ComposableArchitecture import MarkdownUI import OpenAIService @@ -40,12 +41,15 @@ private struct ListHeightPreferenceKey: PreferenceKey { struct ChatPanelMessages: View { let chat: StoreOf + @State var cancellable = Set() @State var isScrollToBottomButtonDisplayed = true @State var isPinnedToBottom = true @Namespace var bottomID @Namespace var scrollSpace @State var scrollOffset: Double = 0 @State var listHeight: Double = 0 + + @Environment(\.isEnabled) var isEnabled var body: some View { ScrollViewReader { proxy in @@ -80,10 +84,6 @@ struct ChatPanelMessages: View { value: offset ) }) - .preference( - key: ListHeightPreferenceKey.self, - value: listGeo.size.height - ) } .modify { view in if #available(macOS 13.0, *) { @@ -95,22 +95,15 @@ struct ChatPanelMessages: View { } .listStyle(.plain) .coordinateSpace(name: scrollSpace) + .preference( + key: ListHeightPreferenceKey.self, + value: listGeo.size.height + ) .onPreferenceChange(ListHeightPreferenceKey.self) { value in listHeight = value updatePinningState() } .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { value in - /// I don't know if there is a way to detect that a scroll is triggered by user - let scrollUpToThreshold = listHeight > 0 // sometimes it can suddenly become 0 - && value > listHeight + 32 + 20 // scroll up to a threshold - && value > scrollOffset // it's scroll up - && value - scrollOffset < 100 // it's not some mystery jump - /// Scroll up too much and the tracker is lost - let checkerOutOfScope = value <= 0 - if checkerOutOfScope || scrollUpToThreshold { - isPinnedToBottom = false - } - scrollOffset = value updatePinningState() } @@ -121,7 +114,6 @@ struct ChatPanelMessages: View { .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) { @@ -134,12 +126,34 @@ struct ChatPanelMessages: View { } } } + .onAppear { + trackScrollWheel() + } + .onDisappear { + cancellable.forEach { $0.cancel() } + cancellable = [] + } + } + + func trackScrollWheel() { + NSApplication.shared.publisher(for: \.currentEvent) + .filter { _ in isEnabled } + .filter { event in event?.type == .scrollWheel } + .sink { event in + guard isEnabled, isPinnedToBottom else { return } + let delta = event?.deltaY ?? 0 + let scrollUp = delta > 0 + if scrollUp { + isPinnedToBottom = false + } + } + .store(in: &cancellable) } @MainActor func updatePinningState() { // where does the 32 come from? - withAnimation { + withAnimation(.linear(duration: 0.1)) { isScrollToBottomButtonDisplayed = scrollOffset > listHeight + 32 + 20 || scrollOffset <= 0 } @@ -148,6 +162,7 @@ struct ChatPanelMessages: View { @ViewBuilder func scrollToBottomButton(proxy: ScrollViewProxy) -> some View { Button(action: { + isPinnedToBottom = true withAnimation(.easeInOut(duration: 0.1)) { proxy.scrollTo(bottomID, anchor: .bottom) } @@ -193,6 +208,7 @@ struct ChatPanelMessages: View { .onChange(of: viewStore.state.isReceivingMessage) { isReceiving in if isReceiving { pinnedToBottom = true + scrollToBottom() } } .onChange(of: viewStore.state.lastMessage) { _ in @@ -665,41 +681,6 @@ struct RoundedCorners: Shape { } } -struct GlobalChatSwitchToggleStyle: ToggleStyle { - func makeBody(configuration: Configuration) -> some View { - HStack(spacing: 4) { - Text(configuration.isOn ? "Shared Conversation" : "Local Conversation") - .foregroundStyle(.tertiary) - - RoundedRectangle(cornerRadius: 10, style: .circular) - .foregroundColor(configuration.isOn ? Color.indigo : .gray.opacity(0.5)) - .frame(width: 30, height: 20, alignment: .center) - .overlay( - Circle() - .fill(.regularMaterial) - .padding(.all, 2) - .overlay( - Image( - systemName: configuration - .isOn ? "globe" : "doc.circle" - ) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12, height: 12, alignment: .center) - .foregroundStyle(.secondary) - ) - .offset(x: configuration.isOn ? 5 : -5, y: 0) - .animation(.linear(duration: 0.1), value: configuration.isOn) - ) - .onTapGesture { configuration.isOn.toggle() } - .overlay { - RoundedRectangle(cornerRadius: 10, style: .circular) - .stroke(.black.opacity(0.2), lineWidth: 1) - } - } - } -} - // MARK: - Previews struct ChatPanel_Preview: PreviewProvider { From 651d29b367b79d751aa36e34d347282f4d0fd261 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 16:55:51 +0800 Subject: [PATCH 32/49] Bump Github Copilot to 1.11.4 --- Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift | 2 +- .../GitHubCopilotService/GitHubCopilotInstallationManager.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift index 39828096..1874ddd7 100644 --- a/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift +++ b/Core/Sources/HostApp/AccountSettings/GitHubCopilotView.swift @@ -149,7 +149,7 @@ struct GitHubCopilotView: View { "node" ) ) { - Text("Path to Node (v17+)") + Text("Path to Node (v18+)") } Text( diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift index f2837db6..051a3425 100644 --- a/Tool/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift +++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotInstallationManager.swift @@ -10,7 +10,7 @@ public struct GitHubCopilotInstallationManager { return URL(string: link)! } - static let latestSupportedVersion = "1.10.3" + static let latestSupportedVersion = "1.11.4" public init() {} From 1cf27811a009967304c0a78cae2ff83dca117671 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 16:58:50 +0800 Subject: [PATCH 33/49] Bump Codeium to 1.4.15 --- Tool/Sources/CodeiumService/CodeiumInstallationManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/CodeiumService/CodeiumInstallationManager.swift b/Tool/Sources/CodeiumService/CodeiumInstallationManager.swift index 27d15ad0..808aeabc 100644 --- a/Tool/Sources/CodeiumService/CodeiumInstallationManager.swift +++ b/Tool/Sources/CodeiumService/CodeiumInstallationManager.swift @@ -3,7 +3,7 @@ import Terminal public struct CodeiumInstallationManager { private static var isInstalling = false - static let latestSupportedVersion = "1.2.93" + static let latestSupportedVersion = "1.4.15" public init() {} @@ -62,7 +62,7 @@ public struct CodeiumInstallationManager { continuation.yield(.downloading) let urls = try CodeiumSuggestionService.createFoldersIfNeeded() let urlString = - "https://github.com/Exafunction/codeium/releases/download/language-server-v\(Self.latestSupportedVersion)/language_server_macos_\(isAppleSilicon() ? "arm" : "x64").gz" + "https://github.com/Exafunction/codeium/releases/download/language-server-v\(Self.latestSupportedVersion)/language_server_macos_\(isAppleSilicon() ? "arm" : "x64").gz" guard let url = URL(string: urlString) else { return } // download From 93c54046d0f18bf6b4c9eaa52d64444a61ba4865 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 17:04:03 +0800 Subject: [PATCH 34/49] Fix a crash that calls dropLast with a negative number --- Tool/Sources/SuggestionModel/EditorInformation.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tool/Sources/SuggestionModel/EditorInformation.swift b/Tool/Sources/SuggestionModel/EditorInformation.swift index c0964f8d..21b73cd0 100644 --- a/Tool/Sources/SuggestionModel/EditorInformation.swift +++ b/Tool/Sources/SuggestionModel/EditorInformation.swift @@ -102,12 +102,12 @@ public struct EditorInformation { } var content = rangeLines if !content.isEmpty { + let dropLastCount = max(0, content[content.endIndex - 1].count - range.end.character) content[content.endIndex - 1] = String( - content[content.endIndex - 1].dropLast( - content[content.endIndex - 1].count - range.end.character - ) + content[content.endIndex - 1].dropLast(dropLastCount) ) - content[0] = String(content[0].dropFirst(range.start.character)) + let dropFirstCount = max(0, range.start.character) + content[0] = String(content[0].dropFirst(dropFirstCount)) } return (content.joined(), rangeLines) } From 228b2b2d2c2eae264740d1b5c2615dff96d031be Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 17:06:31 +0800 Subject: [PATCH 35/49] Update --- Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index 7700c857..304ad83c 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -283,7 +283,7 @@ struct ChatSettingsView: View { #if canImport(ProHostApp) Scope( - title: WithFeatureEnabled(\.projectScopeInChat) { Text("Sense Scope") }, + title: WithFeatureEnabled(\.projectScopeInChat) { Text("Sense Scope (Experimental)") }, description: "Experimental. Enable the bot to read the relevant code of the editing file in the project, third party packages and the SDK." ) { WithFeatureEnabled(\.projectScopeInChat, alignment: .hidden) { From ee2aefbab6872d503316b5cb9f00379bfe2b771a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 17:28:14 +0800 Subject: [PATCH 36/49] Update --- Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift index 304ad83c..ab2879a2 100644 --- a/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift +++ b/Core/Sources/HostApp/FeatureSettings/ChatSettingsView.swift @@ -283,7 +283,9 @@ struct ChatSettingsView: View { #if canImport(ProHostApp) Scope( - title: WithFeatureEnabled(\.projectScopeInChat) { Text("Sense Scope (Experimental)") }, + title: WithFeatureEnabled(\.projectScopeInChat) { + Text("Sense Scope (Experimental)") + }, description: "Experimental. Enable the bot to read the relevant code of the editing file in the project, third party packages and the SDK." ) { WithFeatureEnabled(\.projectScopeInChat, alignment: .hidden) { From 9b0b4bc931856e2711eabf7f1c13b7f034028705 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 17:29:42 +0800 Subject: [PATCH 37/49] Fix chat panel interfere --- Core/Sources/SuggestionWidget/ChatWindowView.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Core/Sources/SuggestionWidget/ChatWindowView.swift b/Core/Sources/SuggestionWidget/ChatWindowView.swift index 00fce575..12e11915 100644 --- a/Core/Sources/SuggestionWidget/ChatWindowView.swift +++ b/Core/Sources/SuggestionWidget/ChatWindowView.swift @@ -140,7 +140,7 @@ struct ChatTitleBar: View { width: Style.trafficLightButtonSize, height: Style.trafficLightButtonSize ) - .overlay{ + .overlay { Circle().stroke(lineWidth: 0.5).foregroundColor(.black.opacity(0.2)) } .overlay { @@ -395,6 +395,11 @@ struct ChatTabContainer: View { .disabled(!isActive) .allowsHitTesting(isActive) .frame(maxWidth: .infinity, maxHeight: .infinity) + // move it out of window + .rotationEffect( + isActive ? .zero : .degrees(90), + anchor: .topLeading + ) } else { EmptyView() } From 65e603e98c07152768435c1123304e618a35ee39 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 17:54:18 +0800 Subject: [PATCH 38/49] Select the first chat tab after restoring --- .../Service/GUI/GraphicalUserInterfaceController.swift | 4 +++- Pro | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index b253f89b..a1a80b00 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -36,12 +36,14 @@ struct GUI: ReducerProtocol { get { .init( chatTabInfo: chatTabGroup.tabInfo, - isRestoreFinished: isChatTabRestoreFinished + isRestoreFinished: isChatTabRestoreFinished, + selectedChatTapId: chatTabGroup.selectedTabId ) } set { chatTabGroup.tabInfo = newValue.chatTabInfo isChatTabRestoreFinished = newValue.isRestoreFinished + chatTabGroup.selectedTabId = newValue.selectedChatTapId } } #endif diff --git a/Pro b/Pro index feddadd7..a6cef6f1 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit feddadd7f1baf34bc15d0d17c4b396363473fa48 +Subproject commit a6cef6f128929dffb55e06f46b0a36eb0f9de930 From ab3be0db225360476479d63840b2bbf2e6889710 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 21:33:18 +0800 Subject: [PATCH 39/49] Fix that prompt to code can open at launch --- .../FeatureReducers/PromptToCodeGroup.swift | 6 ++++-- .../SuggestionWidget/FeatureReducers/WidgetFeature.swift | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift index 0680030c..9012535a 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift @@ -1,13 +1,15 @@ import ComposableArchitecture +import Environment import Foundation import PromptToCodeService import SuggestionModel -import Environment +import XcodeInspector public struct PromptToCodeGroup: ReducerProtocol { public struct State: Equatable { public var promptToCodes: IdentifiedArrayOf = [] - public var activeDocumentURL: PromptToCode.State.ID? + public var activeDocumentURL: PromptToCode.State.ID? = XcodeInspector.shared + .realtimeActiveDocumentURL public var activePromptToCode: PromptToCode.State? { get { if let detached = promptToCodes.first(where: { !$0.isAttachedToSelectionRange }) { diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index bee2092e..225c6d93 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -331,6 +331,7 @@ public struct WidgetFeature: ReducerProtocol { return .run { send in await send(.observeEditorChange) + await send(.panel(.switchToAnotherEditorAndUpdateContent)) for await notification in notifications { try Task.checkCancellation() From 78809ef0867c441c711114d192d0bccc7788ab80 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 22:21:49 +0800 Subject: [PATCH 40/49] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index a6cef6f1..5e0a1458 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit a6cef6f128929dffb55e06f46b0a36eb0f9de930 +Subproject commit 5e0a1458f175c8009cbec0cfa849d4c45b2a3518 From 02ae983b1e23fa9cb9787c13ecbbe91f009d77c6 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 22:30:02 +0800 Subject: [PATCH 41/49] Bump version to 0.27.0 --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index b0ca9172..7664fb23 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.26.0 -APP_BUILD = 272 +APP_VERSION = 0.27.0 +APP_BUILD = 280 From aac6509f157dde2951d7fe287e61d6e543211c56 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 23:23:16 +0800 Subject: [PATCH 42/49] Add sense scope to auto completion --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index ae90a071..dd3b4e8a 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -607,6 +607,7 @@ struct ChatPanelInputArea: View { let availableFeatures = plugins + [ "/exit", "@code", + "@sense", "@project", "@web", ] From d0910a29d31be534eafa88c96bf58a5b38fca4dc Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 23:31:13 +0800 Subject: [PATCH 43/49] Simplify implementation --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index dd3b4e8a..e216c2ce 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -137,11 +137,11 @@ struct ChatPanelMessages: View { func trackScrollWheel() { NSApplication.shared.publisher(for: \.currentEvent) - .filter { _ in isEnabled } - .filter { event in event?.type == .scrollWheel } + .compactMap { $0 } + .filter { isEnabled && $0.type == .scrollWheel } .sink { event in - guard isEnabled, isPinnedToBottom else { return } - let delta = event?.deltaY ?? 0 + guard isPinnedToBottom else { return } + let delta = event.deltaY let scrollUp = delta > 0 if scrollUp { isPinnedToBottom = false From 17ce1158f075958f29c1e07182fb72b377a764ea Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Thu, 9 Nov 2023 23:52:52 +0800 Subject: [PATCH 44/49] Fix that overriding model not overriding max token and min reply token --- .../Configuration/UserPreferenceChatGPTConfiguration.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift index b457c1ee..54b7fbf2 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift @@ -107,11 +107,14 @@ public class OverridingChatGPTConfiguration: ChatGPTConfiguration { } public var maxTokens: Int { - overriding.maxTokens ?? configuration.maxTokens + if let maxTokens = overriding.maxTokens { return maxTokens } + if let model { return model.info.maxTokens } + return configuration.maxTokens } public var minimumReplyTokens: Int { - overriding.minimumReplyTokens ?? configuration.minimumReplyTokens + if let minimumReplyTokens = overriding.minimumReplyTokens { return minimumReplyTokens } + return maxTokens / 5 } public var runFunctionsAutomatically: Bool { From bb3d534ec13777bd03ddf1f2f404555b531fcbfa Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 10 Nov 2023 00:16:26 +0800 Subject: [PATCH 45/49] Update --- Core/Sources/ChatGPTChatTab/ChatPanel.swift | 6 ++++-- Pro | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index e216c2ce..d6e7dc5b 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -48,7 +48,6 @@ struct ChatPanelMessages: View { @Namespace var scrollSpace @State var scrollOffset: Double = 0 @State var listHeight: Double = 0 - @Environment(\.isEnabled) var isEnabled var body: some View { @@ -137,8 +136,11 @@ struct ChatPanelMessages: View { func trackScrollWheel() { NSApplication.shared.publisher(for: \.currentEvent) + .filter { + if !isEnabled { return false } + return $0?.type == .scrollWheel + } .compactMap { $0 } - .filter { isEnabled && $0.type == .scrollWheel } .sink { event in guard isPinnedToBottom else { return } let delta = event.deltaY diff --git a/Pro b/Pro index 5e0a1458..26724b24 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 5e0a1458f175c8009cbec0cfa849d4c45b2a3518 +Subproject commit 26724b240b5f3281b07e60f8029bed5a54da9836 From f0b2a24114d30076527e90bb9c064e15ed73e0e7 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 10 Nov 2023 00:33:47 +0800 Subject: [PATCH 46/49] Update --- .../SuggestionWidget/FeatureReducers/WidgetFeature.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index 225c6d93..7e5510c0 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -454,6 +454,7 @@ public struct WidgetFeature: ReducerProtocol { state.focusingDocumentURL = xcodeInspector.realtimeActiveDocumentURL return .none + #warning("TODO: use function instead of action for high rate actions like this") case let .updateWindowLocation(animated): guard let widgetLocation = generateWidgetLocation() else { return .none } state.panelState.sharedPanelState.alignTopToAnchor = widgetLocation @@ -523,6 +524,7 @@ public struct WidgetFeature: ReducerProtocol { if shouldDebounce { try await mainQueue.sleep(for: .seconds(0.2)) } + try Task.checkCancellation() let task = Task { @MainActor in if let app = activeApplicationMonitor.activeApplication, app.isXcode { let application = AXUIElementCreateApplication(app.processIdentifier) From e54c85f87eafcf8346ba095e5a6affd65716a23d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 10 Nov 2023 01:32:43 +0800 Subject: [PATCH 47/49] Update --- Core/Sources/Service/Service.swift | 13 ++++++++++--- .../FeatureReducers/ChatPanelFeature.swift | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index 6fe76e40..0582aed4 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -52,7 +52,7 @@ public final class Service { ProService() } #endif - + scheduledCleaner.service = self } @@ -83,9 +83,16 @@ final class GlobalShortcutManager { setupShortcutIfNeeded() KeyboardShortcuts.onKeyUp(for: .showHideWidget) { [guiController] in - guiController.viewStore.send(.suggestionWidget(.circularWidget(.widgetClicked))) + if XcodeInspector.shared.activeXcode == nil, + !guiController.viewStore.state.suggestionWidgetState.chatPanelState.isPanelDisplayed, + UserDefaults.shared.value(for: \.showHideWidgetShortcutGlobally) + { + guiController.viewStore.send(.openChatPanel(forceDetach: true)) + } else { + guiController.viewStore.send(.suggestionWidget(.circularWidget(.widgetClicked))) + } } - + XcodeInspector.shared.$activeApplication.sink { app in if !UserDefaults.shared.value(for: \.showHideWidgetShortcutGlobally) { let shouldBeEnabled = if let app, app.isXcode || app.isExtensionService { diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift index 47018a0e..a62c5d8a 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift @@ -47,7 +47,7 @@ public struct ChatPanelFeature: ReducerProtocol { public struct State: Equatable { public var chatTabGroup = ChatTabGroup() var colorScheme: ColorScheme = .light - var isPanelDisplayed = false + public internal(set) var isPanelDisplayed = false var chatPanelInASeparateWindow = false } From 12bc66b02ad666f04df72e744aa7db2a0d4867b8 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 10 Nov 2023 02:19:08 +0800 Subject: [PATCH 48/49] Update --- Pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pro b/Pro index 26724b24..ce6f1630 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit 26724b240b5f3281b07e60f8029bed5a54da9836 +Subproject commit ce6f1630793d46564d658d511d423048edc443f3 From 21985b9213030e6f1d68b1e68d694a6ef234047d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 10 Nov 2023 02:37:23 +0800 Subject: [PATCH 49/49] Update appcast.xml --- appcast.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/appcast.xml b/appcast.xml index 07f4f9e8..19c2b094 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.27.0 + Fri, 10 Nov 2023 02:34:25 +0800 + 280 + 0.27.0 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.27.0 + + + + 0.26.0 Sun, 22 Oct 2023 18:58:49 +0800