diff --git a/Copilot for Xcode.xcodeproj/project.pbxproj b/Copilot for Xcode.xcodeproj/project.pbxproj
index d5f6f3f2..a2e4ba8a 100644
--- a/Copilot for Xcode.xcodeproj/project.pbxproj
+++ b/Copilot for Xcode.xcodeproj/project.pbxproj
@@ -958,6 +958,7 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Copilot-for-Xcode-Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = "";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
@@ -991,6 +992,7 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Copilot-for-Xcode-Info.plist";
+ INFOPLIST_KEY_CFBundleDisplayName = "";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
diff --git a/Copilot-for-Xcode-Info.plist b/Copilot-for-Xcode-Info.plist
index 07a19a85..9f9fdd6e 100644
--- a/Copilot-for-Xcode-Info.plist
+++ b/Copilot-for-Xcode-Info.plist
@@ -12,6 +12,11 @@
$(EXTENSION_BUNDLE_NAME)
HOST_APP_NAME
$(HOST_APP_NAME)
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
SUEnableJavaScript
YES
SUFeedURL
diff --git a/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift b/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift
index d4291d2f..9114a5dd 100644
--- a/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift
+++ b/Core/Sources/ChatGPTChatTab/ChatContextMenu.swift
@@ -72,7 +72,7 @@ struct ChatContextMenu: View {
var chatModel: some View {
let allModels = chatModels + [.init(
id: "com.github.copilot",
- name: "GitHub Copilot as chat model",
+ name: "GitHub Copilot Language Server",
format: .openAI,
info: .init()
)]
diff --git a/Core/Sources/ChatService/AllPlugins.swift b/Core/Sources/ChatService/AllPlugins.swift
index be6c9846..82e756ec 100644
--- a/Core/Sources/ChatService/AllPlugins.swift
+++ b/Core/Sources/ChatService/AllPlugins.swift
@@ -94,6 +94,8 @@ final class LegacyChatPluginWrapper: LegacyChatPlugin {
break
case .startNewMessage:
break
+ case .reasoning:
+ break
}
await chatGPTService.memory.mutateHistory { history in
diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift
index f1407c31..0f5e2d1f 100644
--- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift
+++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEdit.swift
@@ -33,6 +33,7 @@ struct ChatModelEdit {
var openAIProjectID: String = ""
var customHeaders: [ChatModel.Info.CustomHeaderInfo.HeaderField] = []
var openAICompatibleSupportsMultipartMessageContent = true
+ var requiresBeginWithUserMessage = false
}
enum Action: Equatable, BindableAction {
@@ -45,10 +46,44 @@ struct ChatModelEdit {
case testSucceeded(String)
case testFailed(String)
case checkSuggestedMaxTokens
+ case selectModelFormat(ModelFormat)
case apiKeySelection(APIKeySelection.Action)
case baseURLSelection(BaseURLSelection.Action)
}
+ enum ModelFormat: CaseIterable {
+ case openAI
+ case azureOpenAI
+ case googleAI
+ case ollama
+ case claude
+ case gitHubCopilot
+ case openAICompatible
+ case deepSeekOpenAICompatible
+ case openRouterOpenAICompatible
+ case grokOpenAICompatible
+ case mistralOpenAICompatible
+
+ init(_ format: ChatModel.Format) {
+ switch format {
+ case .openAI:
+ self = .openAI
+ case .azureOpenAI:
+ self = .azureOpenAI
+ case .googleAI:
+ self = .googleAI
+ case .ollama:
+ self = .ollama
+ case .claude:
+ self = .claude
+ case .openAICompatible:
+ self = .openAICompatible
+ case .gitHubCopilot:
+ self = .gitHubCopilot
+ }
+ }
+ }
+
var toast: (String, ToastType) -> Void {
@Dependency(\.namespacedToast) var toast
return {
@@ -164,11 +199,53 @@ struct ChatModelEdit {
state.suggestedMaxTokens = nil
}
return .none
+ case .gitHubCopilot:
+ if let knownModel = AvailableGitHubCopilotModel(rawValue: state.modelName) {
+ state.suggestedMaxTokens = knownModel.contextWindow
+ } else {
+ state.suggestedMaxTokens = nil
+ }
+ return .none
default:
state.suggestedMaxTokens = nil
return .none
}
+ case let .selectModelFormat(format):
+ switch format {
+ case .openAI:
+ state.format = .openAI
+ case .azureOpenAI:
+ state.format = .azureOpenAI
+ case .googleAI:
+ state.format = .googleAI
+ case .ollama:
+ state.format = .ollama
+ case .claude:
+ state.format = .claude
+ case .gitHubCopilot:
+ state.format = .gitHubCopilot
+ case .openAICompatible:
+ state.format = .openAICompatible
+ case .deepSeekOpenAICompatible:
+ state.format = .openAICompatible
+ state.baseURLSelection.baseURL = "https://api.deepseek.com"
+ state.baseURLSelection.isFullURL = false
+ case .openRouterOpenAICompatible:
+ state.format = .openAICompatible
+ state.baseURLSelection.baseURL = "https://openrouter.ai"
+ state.baseURLSelection.isFullURL = false
+ case .grokOpenAICompatible:
+ state.format = .openAICompatible
+ state.baseURLSelection.baseURL = "https://api.x.ai"
+ state.baseURLSelection.isFullURL = false
+ case .mistralOpenAICompatible:
+ state.format = .openAICompatible
+ state.baseURLSelection.baseURL = "https://api.mistral.ai"
+ state.baseURLSelection.isFullURL = false
+ }
+ return .none
+
case .apiKeySelection:
return .none
@@ -208,7 +285,7 @@ extension ChatModel {
switch state.format {
case .googleAI, .ollama, .claude:
return false
- case .azureOpenAI, .openAI, .openAICompatible:
+ case .azureOpenAI, .openAI, .openAICompatible, .gitHubCopilot:
return state.supportsFunctionCalling
}
}(),
@@ -222,7 +299,8 @@ extension ChatModel {
openAICompatibleInfo: .init(
enforceMessageOrder: state.enforceMessageOrder,
supportsMultipartMessageContent: state
- .openAICompatibleSupportsMultipartMessageContent
+ .openAICompatibleSupportsMultipartMessageContent,
+ requiresBeginWithUserMessage: state.requiresBeginWithUserMessage
),
customHeaderInfo: .init(headers: state.customHeaders)
)
@@ -249,7 +327,8 @@ extension ChatModel {
openAIProjectID: info.openAIInfo.projectID,
customHeaders: info.customHeaderInfo.headers,
openAICompatibleSupportsMultipartMessageContent: info.openAICompatibleInfo
- .supportsMultipartMessageContent
+ .supportsMultipartMessageContent,
+ requiresBeginWithUserMessage: info.openAICompatibleInfo.requiresBeginWithUserMessage
)
}
}
diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift
index c6b74281..4d3941be 100644
--- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift
+++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelEditView.swift
@@ -29,6 +29,8 @@ struct ChatModelEditView: View {
OllamaForm(store: store)
case .claude:
ClaudeForm(store: store)
+ case .gitHubCopilot:
+ GitHubCopilotForm(store: store)
}
}
.padding()
@@ -86,31 +88,44 @@ struct ChatModelEditView: View {
var body: some View {
WithPerceptionTracking {
Picker(
- selection: $store.format,
+ selection: Binding(
+ get: { .init(store.format) },
+ set: { store.send(.selectModelFormat($0)) }
+ ),
content: {
ForEach(
- ChatModel.Format.allCases,
- id: \.rawValue
+ ChatModelEdit.ModelFormat.allCases,
+ id: \.self
) { format in
switch format {
case .openAI:
- Text("OpenAI").tag(format)
+ Text("OpenAI")
case .azureOpenAI:
- Text("Azure OpenAI").tag(format)
+ Text("Azure OpenAI")
case .openAICompatible:
- Text("OpenAI Compatible").tag(format)
+ Text("OpenAI Compatible")
case .googleAI:
- Text("Google Generative AI").tag(format)
+ Text("Google AI")
case .ollama:
- Text("Ollama").tag(format)
+ Text("Ollama")
case .claude:
- Text("Claude").tag(format)
+ Text("Claude")
+ case .gitHubCopilot:
+ Text("GitHub Copilot")
+ case .deepSeekOpenAICompatible:
+ Text("DeepSeek (OpenAI Compatible)")
+ case .openRouterOpenAICompatible:
+ Text("OpenRouter (OpenAI Compatible)")
+ case .grokOpenAICompatible:
+ Text("Grok (OpenAI Compatible)")
+ case .mistralOpenAICompatible:
+ Text("Mistral (OpenAI Compatible)")
}
}
},
label: { Text("Format") }
)
- .pickerStyle(.segmented)
+ .pickerStyle(.menu)
}
}
}
@@ -243,7 +258,7 @@ struct ChatModelEditView: View {
MaxTokensTextField(store: store)
SupportsFunctionCallingToggle(store: store)
-
+
TextField(text: $store.openAIOrganizationID, prompt: Text("Optional")) {
Text("Organization ID")
}
@@ -321,11 +336,15 @@ struct ChatModelEditView: View {
Toggle(isOn: $store.enforceMessageOrder) {
Text("Enforce message order to be user/assistant alternated")
}
-
+
Toggle(isOn: $store.openAICompatibleSupportsMultipartMessageContent) {
Text("Support multi-part message content")
}
+ Toggle(isOn: $store.requiresBeginWithUserMessage) {
+ Text("Requires the first message to be from the user")
+ }
+
Button("Custom Headers") {
isEditingCustomHeader.toggle()
}
@@ -375,12 +394,16 @@ struct ChatModelEditView: View {
struct OllamaForm: View {
@Perception.Bindable var store: StoreOf
+ @State var isEditingCustomHeader = false
+
var body: some View {
WithPerceptionTracking {
BaseURLTextField(store: store, prompt: Text("http://127.0.0.1:11434")) {
Text("/api/chat")
}
+ ApiKeyNamePicker(store: store)
+
TextField("Model Name", text: $store.modelName)
MaxTokensTextField(store: store)
@@ -389,12 +412,19 @@ struct ChatModelEditView: View {
Text("Keep Alive")
}
+ Button("Custom Headers") {
+ isEditingCustomHeader.toggle()
+ }
+
VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" For more details, please visit [https://ollama.com](https://ollama.com)."
)
}
.padding(.vertical)
+
+ }.sheet(isPresented: $isEditingCustomHeader) {
+ CustomHeaderSettingsView(headers: $store.customHeaders)
}
}
}
@@ -442,6 +472,61 @@ struct ChatModelEditView: View {
}
}
}
+
+ struct GitHubCopilotForm: View {
+ @Perception.Bindable var store: StoreOf
+ @State var isEditingCustomHeader = false
+
+ var body: some View {
+ WithPerceptionTracking {
+ TextField("Model Name", text: $store.modelName)
+ .overlay(alignment: .trailing) {
+ Picker(
+ "",
+ selection: $store.modelName,
+ content: {
+ if AvailableGitHubCopilotModel(rawValue: store.modelName) == nil {
+ Text("Custom Model").tag(store.modelName)
+ }
+ ForEach(AvailableGitHubCopilotModel.allCases, id: \.self) { model in
+ Text(model.rawValue).tag(model.rawValue)
+ }
+ }
+ )
+ .frame(width: 20)
+ }
+
+ MaxTokensTextField(store: store)
+ SupportsFunctionCallingToggle(store: store)
+
+ Toggle(isOn: $store.enforceMessageOrder) {
+ Text("Enforce message order to be user/assistant alternated")
+ }
+
+ Toggle(isOn: $store.openAICompatibleSupportsMultipartMessageContent) {
+ Text("Support multi-part message content")
+ }
+
+ Button("Custom Headers") {
+ isEditingCustomHeader.toggle()
+ }
+
+ VStack(alignment: .leading, spacing: 8) {
+ Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
+ " Please login in the GitHub Copilot settings to use the model."
+ )
+
+ Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
+ " This will call the APIs directly, which may not be allowed by GitHub. But it's used in other popular apps like Zed."
+ )
+ }
+ .dynamicHeightTextInFormWorkaround()
+ .padding(.vertical)
+ }.sheet(isPresented: $isEditingCustomHeader) {
+ CustomHeaderSettingsView(headers: $store.customHeaders)
+ }
+ }
+ }
}
#Preview("OpenAI") {
diff --git a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelManagement.swift b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelManagement.swift
index 4dc46630..64eadd57 100644
--- a/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelManagement.swift
+++ b/Core/Sources/HostApp/AccountSettings/ChatModelManagement/ChatModelManagement.swift
@@ -13,6 +13,7 @@ extension ChatModel: ManageableAIModel {
case .googleAI: return "Google Generative AI"
case .ollama: return "Ollama"
case .claude: return "Claude"
+ case .gitHubCopilot: return "GitHub Copilot"
}
}
diff --git a/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelEdit.swift b/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelEdit.swift
index 4e36a583..f057be21 100644
--- a/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelEdit.swift
+++ b/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelEdit.swift
@@ -41,9 +41,35 @@ struct EmbeddingModelEdit {
case testFailed(String)
case fixDimensions(Int)
case checkSuggestedMaxTokens
+ case selectModelFormat(ModelFormat)
case apiKeySelection(APIKeySelection.Action)
case baseURLSelection(BaseURLSelection.Action)
}
+
+ enum ModelFormat: CaseIterable {
+ case openAI
+ case azureOpenAI
+ case ollama
+ case gitHubCopilot
+ case openAICompatible
+ case mistralOpenAICompatible
+ case voyageAIOpenAICompatible
+
+ init(_ format: EmbeddingModel.Format) {
+ switch format {
+ case .openAI:
+ self = .openAI
+ case .azureOpenAI:
+ self = .azureOpenAI
+ case .ollama:
+ self = .ollama
+ case .openAICompatible:
+ self = .openAICompatible
+ case .gitHubCopilot:
+ self = .gitHubCopilot
+ }
+ }
+ }
var toast: (String, ToastType) -> Void {
@Dependency(\.namespacedToast) var toast
@@ -155,6 +181,29 @@ struct EmbeddingModelEdit {
case let .fixDimensions(value):
state.dimensions = value
return .none
+
+ case let .selectModelFormat(format):
+ switch format {
+ case .openAI:
+ state.format = .openAI
+ case .azureOpenAI:
+ state.format = .azureOpenAI
+ case .ollama:
+ state.format = .ollama
+ case .openAICompatible:
+ state.format = .openAICompatible
+ case .gitHubCopilot:
+ state.format = .gitHubCopilot
+ case .mistralOpenAICompatible:
+ state.format = .openAICompatible
+ state.baseURLSelection.baseURL = "https://api.mistral.ai"
+ state.baseURLSelection.isFullURL = false
+ case .voyageAIOpenAICompatible:
+ state.format = .openAICompatible
+ state.baseURLSelection.baseURL = "https://api.voyage.ai"
+ state.baseURLSelection.isFullURL = false
+ }
+ return .none
case .apiKeySelection:
return .none
diff --git a/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelEditView.swift b/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelEditView.swift
index 45014756..46f4effd 100644
--- a/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelEditView.swift
+++ b/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelEditView.swift
@@ -24,6 +24,8 @@ struct EmbeddingModelEditView: View {
OpenAICompatibleForm(store: store)
case .ollama:
OllamaForm(store: store)
+ case .gitHubCopilot:
+ GitHubCopilotForm(store: store)
}
}
.padding()
@@ -81,27 +83,36 @@ struct EmbeddingModelEditView: View {
var body: some View {
WithPerceptionTracking {
Picker(
- selection: $store.format,
+ selection: Binding(
+ get: { .init(store.format) },
+ set: { store.send(.selectModelFormat($0)) }
+ ),
content: {
ForEach(
- EmbeddingModel.Format.allCases,
- id: \.rawValue
+ EmbeddingModelEdit.ModelFormat.allCases,
+ id: \.self
) { format in
switch format {
case .openAI:
- Text("OpenAI").tag(format)
+ Text("OpenAI")
case .azureOpenAI:
- Text("Azure OpenAI").tag(format)
- case .openAICompatible:
- Text("OpenAI Compatible").tag(format)
+ Text("Azure OpenAI")
case .ollama:
- Text("Ollama").tag(format)
+ Text("Ollama")
+ case .openAICompatible:
+ Text("OpenAI Compatible")
+ case .mistralOpenAICompatible:
+ Text("Mistral (OpenAI Compatible)")
+ case .voyageAIOpenAICompatible:
+ Text("Voyage (OpenAI Compatible)")
+ case .gitHubCopilot:
+ Text("GitHub Copilot")
}
}
},
label: { Text("Format") }
)
- .pickerStyle(.segmented)
+ .pickerStyle(.menu)
}
}
}
@@ -174,7 +185,7 @@ struct EmbeddingModelEditView: View {
}
}
}
-
+
struct DimensionsTextField: View {
@Perception.Bindable var store: StoreOf
@@ -212,7 +223,7 @@ struct EmbeddingModelEditView: View {
return .primary
}() as Color)
}
-
+
Text("If you are not sure, run test to get the correct value.")
.font(.caption)
.dynamicHeightTextInFormWorkaround()
@@ -327,7 +338,7 @@ struct EmbeddingModelEditView: View {
MaxTokensTextField(store: store)
DimensionsTextField(store: store)
-
+
Button("Custom Headers") {
isEditingCustomHeader.toggle()
}
@@ -339,11 +350,16 @@ struct EmbeddingModelEditView: View {
struct OllamaForm: View {
@Perception.Bindable var store: StoreOf
+ @State var isEditingCustomHeader = false
+
var body: some View {
WithPerceptionTracking {
BaseURLTextField(store: store, prompt: Text("http://127.0.0.1:11434")) {
Text("/api/embeddings")
}
+
+ ApiKeyNamePicker(store: store)
+
TextField("Model Name", text: $store.modelName)
MaxTokensTextField(store: store)
@@ -355,12 +371,66 @@ struct EmbeddingModelEditView: View {
}
}
+ Button("Custom Headers") {
+ isEditingCustomHeader.toggle()
+ }
+
VStack(alignment: .leading, spacing: 8) {
Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
" For more details, please visit [https://ollama.com](https://ollama.com)."
)
}
.padding(.vertical)
+
+ }.sheet(isPresented: $isEditingCustomHeader) {
+ CustomHeaderSettingsView(headers: $store.customHeaders)
+ }
+ }
+ }
+
+ struct GitHubCopilotForm: View {
+ @Perception.Bindable var store: StoreOf
+ @State var isEditingCustomHeader = false
+
+ var body: some View {
+ WithPerceptionTracking {
+ TextField("Model Name", text: $store.modelName)
+ .overlay(alignment: .trailing) {
+ Picker(
+ "",
+ selection: $store.modelName,
+ content: {
+ if OpenAIEmbeddingModel(rawValue: store.modelName) == nil {
+ Text("Custom Model").tag(store.modelName)
+ }
+ ForEach(OpenAIEmbeddingModel.allCases, id: \.self) { model in
+ Text(model.rawValue).tag(model.rawValue)
+ }
+ }
+ )
+ .frame(width: 20)
+ }
+
+ MaxTokensTextField(store: store)
+ DimensionsTextField(store: store)
+
+ Button("Custom Headers") {
+ isEditingCustomHeader.toggle()
+ }
+
+ VStack(alignment: .leading, spacing: 8) {
+ Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
+ " Please login in the GitHub Copilot settings to use the model."
+ )
+
+ Text(Image(systemName: "exclamationmark.triangle.fill")) + Text(
+ " This will call the APIs directly, which may not be allowed by GitHub. But it's used in other popular apps like Zed."
+ )
+ }
+ .dynamicHeightTextInFormWorkaround()
+ .padding(.vertical)
+ }.sheet(isPresented: $isEditingCustomHeader) {
+ CustomHeaderSettingsView(headers: $store.customHeaders)
}
}
}
diff --git a/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelManagement.swift b/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelManagement.swift
index 294ca401..156f58ac 100644
--- a/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelManagement.swift
+++ b/Core/Sources/HostApp/AccountSettings/EmbeddingModelManagement/EmbeddingModelManagement.swift
@@ -11,6 +11,7 @@ extension EmbeddingModel: ManageableAIModel {
case .azureOpenAI: return "Azure OpenAI"
case .openAICompatible: return "OpenAI Compatible"
case .ollama: return "Ollama"
+ case .gitHubCopilot: return "GitHub Copilot"
}
}
diff --git a/Core/Sources/HostApp/CustomCommandSettings/EditCustomCommandView.swift b/Core/Sources/HostApp/CustomCommandSettings/EditCustomCommandView.swift
index d42cac83..e2304f8b 100644
--- a/Core/Sources/HostApp/CustomCommandSettings/EditCustomCommandView.swift
+++ b/Core/Sources/HostApp/CustomCommandSettings/EditCustomCommandView.swift
@@ -263,7 +263,7 @@ extension CustomCommand.Attachment.Kind {
case .senseScope: return "Sense Scope"
case .projectScope: return "Project Scope"
case .webScope: return "Web Scope"
- case .gitStatus: return "Git Status"
+ case .gitStatus: return "Git Status and Diff"
case .gitLog: return "Git Log"
case .file: return "File"
}
diff --git a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift
index b78d00a4..93316505 100644
--- a/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift
+++ b/Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift
@@ -155,7 +155,7 @@ struct ChatSettingsGeneralSectionView: View {
) {
let allModels = settings.chatModels + [.init(
id: "com.github.copilot",
- name: "GitHub Copilot as chat model",
+ name: "GitHub Copilot Language Server",
format: .openAI,
info: .init()
)]
diff --git a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift
index d2062450..4965efbf 100644
--- a/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift
+++ b/Core/Sources/KeyBindingManager/TabToAcceptSuggestion.swift
@@ -103,8 +103,6 @@ final class TabToAcceptSuggestion {
let tab = 48
let esc = 53
- Logger.service.info("TabToAcceptSuggestion: \(keycode)")
-
switch keycode {
case tab:
Logger.service.info("TabToAcceptSuggestion: Tab")
diff --git a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift
index e0e25de4..d2a2b2b3 100644
--- a/Core/Sources/SuggestionWidget/WidgetWindowsController.swift
+++ b/Core/Sources/SuggestionWidget/WidgetWindowsController.swift
@@ -880,7 +880,11 @@ class WidgetWindow: CanBecomeKeyWindow {
func widgetLevel(_ addition: Int) -> NSWindow.Level {
let minimumWidgetLevel: Int
+ #if DEBUG
+ minimumWidgetLevel = NSWindow.Level.floating.rawValue + 1
+ #else
minimumWidgetLevel = NSWindow.Level.floating.rawValue
+ #endif
return .init(minimumWidgetLevel + addition)
}
diff --git a/ExtensionService/Info.plist b/ExtensionService/Info.plist
index 94c867be..9faed878 100644
--- a/ExtensionService/Info.plist
+++ b/ExtensionService/Info.plist
@@ -12,6 +12,11 @@
$(EXTENSION_BUNDLE_NAME)
HOST_APP_NAME
$(HOST_APP_NAME)
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
TEAM_ID_PREFIX
$(TeamIdentifierPrefix)
XPCService
diff --git a/Tool/Package.swift b/Tool/Package.swift
index b5f93686..7d356d08 100644
--- a/Tool/Package.swift
+++ b/Tool/Package.swift
@@ -453,6 +453,7 @@ let package = Package(
"Keychain",
"BuiltinExtension",
"ChatBasic",
+ "GitHubCopilotService",
.product(name: "JSONRPC", package: "JSONRPC"),
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
.product(name: "GoogleGenerativeAI", package: "generative-ai-swift"),
diff --git a/Tool/Sources/AIModel/ChatModel.swift b/Tool/Sources/AIModel/ChatModel.swift
index c98658c7..7ae666e3 100644
--- a/Tool/Sources/AIModel/ChatModel.swift
+++ b/Tool/Sources/AIModel/ChatModel.swift
@@ -23,6 +23,7 @@ public struct ChatModel: Codable, Equatable, Identifiable {
case googleAI
case ollama
case claude
+ case gitHubCopilot
}
public struct Info: Codable, Equatable {
@@ -52,13 +53,17 @@ public struct ChatModel: Codable, Equatable, Identifiable {
public var enforceMessageOrder: Bool
@FallbackDecoding
public var supportsMultipartMessageContent: Bool
+ @FallbackDecoding
+ public var requiresBeginWithUserMessage: Bool
public init(
enforceMessageOrder: Bool = false,
- supportsMultipartMessageContent: Bool = true
+ supportsMultipartMessageContent: Bool = true,
+ requiresBeginWithUserMessage: Bool = false
) {
self.enforceMessageOrder = enforceMessageOrder
self.supportsMultipartMessageContent = supportsMultipartMessageContent
+ self.requiresBeginWithUserMessage = requiresBeginWithUserMessage
}
}
@@ -178,6 +183,8 @@ public struct ChatModel: Codable, Equatable, Identifiable {
let baseURL = info.baseURL
if baseURL.isEmpty { return "https://api.anthropic.com/v1/messages" }
return "\(baseURL)/v1/messages"
+ case .gitHubCopilot:
+ return "https://api.githubcopilot.com/chat/completions"
}
}
}
diff --git a/Tool/Sources/AIModel/EmbeddingModel.swift b/Tool/Sources/AIModel/EmbeddingModel.swift
index ad77cd9a..4e192dda 100644
--- a/Tool/Sources/AIModel/EmbeddingModel.swift
+++ b/Tool/Sources/AIModel/EmbeddingModel.swift
@@ -21,6 +21,7 @@ public struct EmbeddingModel: Codable, Equatable, Identifiable {
case azureOpenAI
case openAICompatible
case ollama
+ case gitHubCopilot
}
public struct Info: Codable, Equatable {
@@ -92,6 +93,8 @@ public struct EmbeddingModel: Codable, Equatable, Identifiable {
let baseURL = info.baseURL
if baseURL.isEmpty { return "http://localhost:11434/api/embeddings" }
return "\(baseURL)/api/embeddings"
+ case .gitHubCopilot:
+ return "https://api.githubcopilot.com/embeddings"
}
}
}
diff --git a/Tool/Sources/ChatBasic/ChatAgent.swift b/Tool/Sources/ChatBasic/ChatAgent.swift
index 7f464881..763233b9 100644
--- a/Tool/Sources/ChatBasic/ChatAgent.swift
+++ b/Tool/Sources/ChatBasic/ChatAgent.swift
@@ -4,7 +4,7 @@ public enum ChatAgentResponse {
public enum Content {
case text(String)
}
-
+
public enum ActionResult {
case success(String)
case failure(String)
@@ -24,6 +24,8 @@ public enum ChatAgentResponse {
case references([ChatMessage.Reference])
/// End the current message. The next contents will be sent as a new message.
case startNewMessage
+ /// Reasoning
+ case reasoning(String)
}
public struct ChatAgentRequest {
diff --git a/Tool/Sources/ChatBasic/ChatMessage.swift b/Tool/Sources/ChatBasic/ChatMessage.swift
index a0f7d432..23e57000 100644
--- a/Tool/Sources/ChatBasic/ChatMessage.swift
+++ b/Tool/Sources/ChatBasic/ChatMessage.swift
@@ -59,7 +59,7 @@ public struct ChatMessage: Equatable, Codable {
}
/// A reference to include in a chat message.
- public struct Reference: Codable, Equatable {
+ public struct Reference: Codable, Equatable, Identifiable {
/// The kind of reference.
public enum Kind: Codable, Equatable {
public enum Symbol: String, Codable {
@@ -89,6 +89,8 @@ public struct ChatMessage: Equatable, Codable {
case error
}
+ @FallbackDecoding
+ public var id: String
/// The title of the reference.
public var title: String
/// The content of the reference.
@@ -98,10 +100,12 @@ public struct ChatMessage: Equatable, Codable {
public var kind: Kind
public init(
+ id: String = UUID().uuidString,
title: String,
content: String,
kind: Kind
) {
+ self.id = id
self.title = title
self.content = content
self.kind = kind
@@ -188,6 +192,10 @@ public struct ReferenceKindFallback: FallbackValueProvider {
public static var defaultValue: ChatMessage.Reference.Kind { .other(kind: "Unknown") }
}
+public struct ReferenceIDFallback: FallbackValueProvider {
+ public static var defaultValue: String { UUID().uuidString }
+}
+
public struct ChatMessageRoleFallback: FallbackValueProvider {
public static var defaultValue: ChatMessage.Role { .user }
}
diff --git a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift
index a556e8ff..f18ac2be 100644
--- a/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift
+++ b/Tool/Sources/GitHubCopilotService/GitHubCopilotExtension.swift
@@ -139,7 +139,6 @@ protocol ServiceLocatorType {
class ServiceLocator: ServiceLocatorType {
let workspacePool: WorkspacePool
-
init(workspacePool: WorkspacePool) {
self.workspacePool = workspacePool
}
@@ -152,3 +151,109 @@ class ServiceLocator: ServiceLocatorType {
}
}
+extension GitHubCopilotExtension {
+ public struct Token: Codable {
+// let codesearch: Bool
+ public let individual: Bool
+ public let endpoints: Endpoints
+ public let chat_enabled: Bool
+// public let sku: String
+// public let copilotignore_enabled: Bool
+// public let limited_user_quotas: String?
+// public let tracking_id: String
+// public let xcode: Bool
+// public let limited_user_reset_date: String?
+// public let telemetry: String
+// public let prompt_8k: Bool
+ public let token: String
+// public let nes_enabled: Bool
+// public let vsc_electron_fetcher_v2: Bool
+// public let code_review_enabled: Bool
+// public let annotations_enabled: Bool
+// public let chat_jetbrains_enabled: Bool
+// public let xcode_chat: Bool
+// public let refresh_in: Int
+// public let snippy_load_test_enabled: Bool
+// public let trigger_completion_after_accept: Bool
+ public let expires_at: Int
+// public let public_suggestions: String
+// public let code_quote_enabled: Bool
+
+ public struct Endpoints: Codable {
+ public let api: String
+ public let proxy: String
+ public let telemetry: String
+// public let origin-tracker: String
+ }
+ }
+
+ struct AuthInfo: Codable {
+ public let user: String
+ public let oauth_token: String
+ public let githubAppId: String
+ }
+
+ static var authInfo: AuthInfo? {
+ guard let urls = try? GitHubCopilotBaseService.createFoldersIfNeeded()
+ else { return nil }
+ let path = urls.supportURL
+ .appendingPathComponent("undefined")
+ .appendingPathComponent(".config")
+ .appendingPathComponent("github-copilot")
+ .appendingPathComponent("apps.json").path
+ guard FileManager.default.fileExists(atPath: path) else { return nil }
+
+ do {
+ let data = try Data(contentsOf: URL(fileURLWithPath: path))
+ let json = try JSONSerialization
+ .jsonObject(with: data, options: []) as? [String: [String: String]]
+ guard let firstEntry = json?.values.first else { return nil }
+ let jsonData = try JSONSerialization.data(withJSONObject: firstEntry, options: [])
+ return try JSONDecoder().decode(AuthInfo.self, from: jsonData)
+ } catch {
+ Logger.gitHubCopilot.error(error.localizedDescription)
+ return nil
+ }
+ }
+
+ @MainActor
+ static var cachedToken: Token?
+
+ public static func fetchToken() async throws -> Token {
+ guard let authToken = authInfo?.oauth_token
+ else { throw GitHubCopilotError.notLoggedIn }
+
+ let oldToken = await MainActor.run { cachedToken }
+ if let oldToken {
+ let expiresAt = Date(timeIntervalSince1970: TimeInterval(oldToken.expires_at))
+ if expiresAt > Date() {
+ return oldToken
+ }
+ }
+
+ let url = URL(string: "https://api.github.com/copilot_internal/v2/token")!
+ var request = URLRequest(url: url)
+ request.httpMethod = "GET"
+ request.setValue("token \(authToken)", forHTTPHeaderField: "authorization")
+ request.setValue("unknown-editor/0", forHTTPHeaderField: "editor-version")
+ request.setValue("unknown-editor-plugin/0", forHTTPHeaderField: "editor-plugin-version")
+ request.setValue("1.236.0", forHTTPHeaderField: "copilot-language-server-version")
+ request.setValue("GithubCopilot/1.236.0", forHTTPHeaderField: "user-agent")
+ request.setValue("*/*", forHTTPHeaderField: "accept")
+ request.setValue("gzip,deflate,br", forHTTPHeaderField: "accept-encoding")
+
+ do {
+ let (data, _) = try await URLSession.shared.data(for: request)
+ if let jsonString = String(data: data, encoding: .utf8) {
+ print(jsonString)
+ }
+ let newToken = try JSONDecoder().decode(Token.self, from: data)
+ await MainActor.run { cachedToken = newToken }
+ return newToken
+ } catch {
+ Logger.service.error(error.localizedDescription)
+ throw error
+ }
+ }
+}
+
diff --git a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift
index 590cbbe4..7979c76e 100644
--- a/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift
+++ b/Tool/Sources/GitHubCopilotService/LanguageServer/GitHubCopilotService.swift
@@ -53,6 +53,7 @@ extension GitHubCopilotLSP {
}
enum GitHubCopilotError: Error, LocalizedError {
+ case notLoggedIn
case languageServerNotInstalled
case languageServerError(ServerError)
case failedToInstallStartScript
@@ -60,6 +61,8 @@ enum GitHubCopilotError: Error, LocalizedError {
var errorDescription: String? {
switch self {
+ case .notLoggedIn:
+ return "Not logged in."
case .languageServerNotInstalled:
return "Language server is not installed."
case .failedToInstallStartScript:
diff --git a/Tool/Sources/LangChain/DocumentTransformer/TextSplitter.swift b/Tool/Sources/LangChain/DocumentTransformer/TextSplitter.swift
index 4e467106..5880616c 100644
--- a/Tool/Sources/LangChain/DocumentTransformer/TextSplitter.swift
+++ b/Tool/Sources/LangChain/DocumentTransformer/TextSplitter.swift
@@ -26,6 +26,9 @@ public extension TextSplitter {
for (text, metadata) in zip(texts, metadata) {
let chunks = try await split(text: text)
for chunk in chunks {
+ var metadata = metadata
+ metadata["startUTF16Offset"] = .number(Double(chunk.startUTF16Offset))
+ metadata["endUTF16Offset"] = .number(Double(chunk.endUTF16Offset))
let document = Document(pageContent: chunk.text, metadata: metadata)
documents.append(document)
}
@@ -48,6 +51,41 @@ public extension TextSplitter {
func transformDocuments(_ documents: [Document]) async throws -> [Document] {
return try await splitDocuments(documents)
}
+
+ func joinDocuments(_ documents: [Document]) -> Document {
+ let textChunks: [TextChunk] = documents.compactMap { document in
+ func extract(_ key: String) -> Int? {
+ if case let .number(d) = document.metadata[key] {
+ return Int(d)
+ }
+ return nil
+ }
+ guard let start = extract("startUTF16Offset"),
+ let end = extract("endUTF16Offset")
+ else { return nil }
+ return TextChunk(
+ text: document.pageContent,
+ startUTF16Offset: start,
+ endUTF16Offset: end
+ )
+ }.sorted(by: { $0.startUTF16Offset < $1.startUTF16Offset })
+ var sumChunk: TextChunk?
+ for chunk in textChunks {
+ if let current = sumChunk {
+ if let merged = current.merged(with: chunk, force: true) {
+ sumChunk = merged
+ }
+ } else {
+ sumChunk = chunk
+ }
+ }
+ let pageContent = sumChunk?.text ?? ""
+ var metadata = documents.first?.metadata ?? [String: JSONValue]()
+ metadata["startUTF16Offset"] = nil
+ metadata["endUTF16Offset"] = nil
+
+ return Document(pageContent: pageContent, metadata: metadata)
+ }
}
public struct TextChunk: Equatable {
@@ -83,14 +121,14 @@ public extension TextSplitter {
let text = (a + b).map(\.text).joined()
var l = Int.max
var u = 0
-
+
for chunk in a + b {
l = min(l, chunk.startUTF16Offset)
u = max(u, chunk.endUTF16Offset)
}
-
+
guard l < u else { return nil }
-
+
return .init(text: text, startUTF16Offset: l, endUTF16Offset: u)
}
diff --git a/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIBuilder.swift b/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIBuilder.swift
index f114b32d..8f8e3feb 100644
--- a/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIBuilder.swift
+++ b/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIBuilder.swift
@@ -62,6 +62,11 @@ struct DefaultChatCompletionsAPIBuilder: ChatCompletionsAPIBuilder {
endpoint: endpoint,
requestBody: requestBody
)
+ case .gitHubCopilot:
+ return GitHubCopilotChatCompletionsService(
+ model: model,
+ requestBody: requestBody
+ )
}
}
@@ -107,6 +112,11 @@ struct DefaultChatCompletionsAPIBuilder: ChatCompletionsAPIBuilder {
endpoint: endpoint,
requestBody: requestBody
)
+ case .gitHubCopilot:
+ return GitHubCopilotChatCompletionsService(
+ model: model,
+ requestBody: requestBody
+ )
}
}
}
@@ -121,3 +131,4 @@ extension DependencyValues {
set { self[ChatCompletionsAPIBuilderDependencyKey.self] = newValue }
}
}
+
diff --git a/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIDefinition.swift b/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIDefinition.swift
index f8f1a5ff..87a5e73e 100644
--- a/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIDefinition.swift
+++ b/Tool/Sources/OpenAIService/APIs/ChatCompletionsAPIDefinition.swift
@@ -172,6 +172,23 @@ protocol ChatCompletionsStreamAPI {
func callAsFunction() async throws -> AsyncThrowingStream
}
+extension ChatCompletionsStreamAPI {
+ static func setupExtraHeaderFields(
+ _ request: inout URLRequest,
+ model: ChatModel,
+ apiKey: String
+ ) async {
+ let parser = HeaderValueParser()
+ for field in model.info.customHeaderInfo.headers where !field.key.isEmpty {
+ let value = await parser.parse(
+ field.value,
+ context: .init(modelName: model.info.modelName, apiKey: apiKey)
+ )
+ request.setValue(value, forHTTPHeaderField: field.key)
+ }
+ }
+}
+
extension AsyncSequence {
func toStream() -> AsyncThrowingStream {
AsyncThrowingStream { continuation in
@@ -209,6 +226,7 @@ struct ChatCompletionsStreamDataChunk {
var role: ChatCompletionsRequestBody.Message.Role?
var content: String?
+ var reasoningContent: String?
var toolCalls: [ToolCall]?
}
@@ -243,6 +261,8 @@ struct ChatCompletionResponseBody: Equatable {
var role: Role
/// The content of the message.
var content: String?
+ /// The reasoning content of the message.
+ var reasoningContent: String?
/// When we want to reply to a function call with the result, we have to provide the
/// name of the function call, and include the result in `content`.
///
diff --git a/Tool/Sources/OpenAIService/APIs/EmbeddingAPIDefinitions.swift b/Tool/Sources/OpenAIService/APIs/EmbeddingAPIDefinitions.swift
index 2d099a0b..6a63ee7b 100644
--- a/Tool/Sources/OpenAIService/APIs/EmbeddingAPIDefinitions.swift
+++ b/Tool/Sources/OpenAIService/APIs/EmbeddingAPIDefinitions.swift
@@ -9,6 +9,23 @@ protocol EmbeddingAPI {
func embed(tokens: [[Int]]) async throws -> EmbeddingResponse
}
+extension EmbeddingAPI {
+ static func setupExtraHeaderFields(
+ _ request: inout URLRequest,
+ model: EmbeddingModel,
+ apiKey: String
+ ) async {
+ let parser = HeaderValueParser()
+ for field in model.info.customHeaderInfo.headers where !field.key.isEmpty {
+ let value = await parser.parse(
+ field.value,
+ context: .init(modelName: model.info.modelName, apiKey: apiKey)
+ )
+ request.setValue(value, forHTTPHeaderField: field.key)
+ }
+ }
+}
+
public struct EmbeddingResponse: Decodable {
public struct Object: Decodable {
public var embedding: [Float]
diff --git a/Tool/Sources/OpenAIService/APIs/GitHubCopilotChatCompletionsService.swift b/Tool/Sources/OpenAIService/APIs/GitHubCopilotChatCompletionsService.swift
new file mode 100644
index 00000000..d9b2a51b
--- /dev/null
+++ b/Tool/Sources/OpenAIService/APIs/GitHubCopilotChatCompletionsService.swift
@@ -0,0 +1,112 @@
+import AIModel
+import AsyncAlgorithms
+import ChatBasic
+import Foundation
+import GitHubCopilotService
+import Logger
+import Preferences
+
+public enum AvailableGitHubCopilotModel: String, CaseIterable {
+ case claude35sonnet = "claude-3.5-sonnet"
+ case o1Mini = "o1-mini"
+ case o1 = "o1"
+ case gpt4Turbo = "gpt-4-turbo"
+ case gpt4oMini = "gpt-4o-mini"
+ case gpt4o = "gpt-4o"
+ case gpt4 = "gpt-4"
+ case gpt35Turbo = "gpt-3.5-turbo"
+
+ public var contextWindow: Int {
+ switch self {
+ case .claude35sonnet:
+ return 200_000
+ case .o1Mini:
+ return 128_000
+ case .o1:
+ return 128_000
+ case .gpt4Turbo:
+ return 128_000
+ case .gpt4oMini:
+ return 128_000
+ case .gpt4o:
+ return 128_000
+ case .gpt4:
+ return 32_768
+ case .gpt35Turbo:
+ return 16_384
+ }
+ }
+}
+
+/// Looks like it's used in many other popular repositories so maybe it's safe.
+actor GitHubCopilotChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI {
+
+ let chatModel: ChatModel
+ let requestBody: ChatCompletionsRequestBody
+
+ init(
+ model: ChatModel,
+ requestBody: ChatCompletionsRequestBody
+ ) {
+ var model = model
+ model.format = .openAICompatible
+ chatModel = model
+ self.requestBody = requestBody
+ }
+
+ func callAsFunction() async throws
+ -> AsyncThrowingStream
+ {
+ let service = try await buildService()
+ return try await service()
+ }
+
+ func callAsFunction() async throws -> ChatCompletionResponseBody {
+ let service = try await buildService()
+ return try await service()
+ }
+
+ private func buildService() async throws -> OpenAIChatCompletionsService {
+ let token = try await GitHubCopilotExtension.fetchToken()
+
+ guard let endpoint = URL(string: token.endpoints.api + "/chat/completions") else {
+ throw ChatGPTServiceError.endpointIncorrect
+ }
+
+ return OpenAIChatCompletionsService(
+ apiKey: token.token,
+ model: chatModel,
+ endpoint: endpoint,
+ requestBody: requestBody
+ ) { request in
+
+// POST /chat/completions HTTP/2
+// :authority: api.individual.githubcopilot.com
+// authorization: Bearer *
+// x-request-id: *
+// openai-organization: github-copilot
+// vscode-sessionid: *
+// vscode-machineid: *
+// editor-version: vscode/1.89.1
+// editor-plugin-version: Copilot for Xcode/0.35.5
+// copilot-language-server-version: 1.236.0
+// x-github-api-version: 2023-07-07
+// openai-intent: conversation-panel
+// content-type: application/json
+// user-agent: GithubCopilot/1.236.0
+// content-length: 9061
+// accept: */*
+// accept-encoding: gzip,deflate,br
+
+ request.setValue(
+ "Copilot for Xcode/\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown")",
+ forHTTPHeaderField: "Editor-Version"
+ )
+ request.setValue("Bearer \(token.token)", forHTTPHeaderField: "Authorization")
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue("vscode-chat", forHTTPHeaderField: "Copilot-Integration-Id")
+ request.setValue("2023-07-07", forHTTPHeaderField: "X-Github-Api-Version")
+ }
+ }
+}
+
diff --git a/Tool/Sources/OpenAIService/APIs/GitHubCopilotEmbeddingService.swift b/Tool/Sources/OpenAIService/APIs/GitHubCopilotEmbeddingService.swift
new file mode 100644
index 00000000..627694a4
--- /dev/null
+++ b/Tool/Sources/OpenAIService/APIs/GitHubCopilotEmbeddingService.swift
@@ -0,0 +1,72 @@
+import AIModel
+import AsyncAlgorithms
+import ChatBasic
+import Foundation
+import GitHubCopilotService
+import Logger
+import Preferences
+
+/// Looks like it's used in many other popular repositories so maybe it's safe.
+actor GitHubCopilotEmbeddingService: EmbeddingAPI {
+ let chatModel: EmbeddingModel
+
+ init(model: EmbeddingModel) {
+ var model = model
+ model.format = .openAICompatible
+ chatModel = model
+ }
+
+ func embed(text: String) async throws -> EmbeddingResponse {
+ let service = try await buildService()
+ return try await service.embed(text: text)
+ }
+
+ func embed(texts: [String]) async throws -> EmbeddingResponse {
+ let service = try await buildService()
+ return try await service.embed(texts: texts)
+ }
+
+ func embed(tokens: [[Int]]) async throws -> EmbeddingResponse {
+ let service = try await buildService()
+ return try await service.embed(tokens: tokens)
+ }
+
+ private func buildService() async throws -> OpenAIEmbeddingService {
+ let token = try await GitHubCopilotExtension.fetchToken()
+
+ return OpenAIEmbeddingService(
+ apiKey: token.token,
+ model: chatModel,
+ endpoint: token.endpoints.api + "/embeddings"
+ ) { request in
+
+// POST /chat/completions HTTP/2
+// :authority: api.individual.githubcopilot.com
+// authorization: Bearer *
+// x-request-id: *
+// openai-organization: github-copilot
+// vscode-sessionid: *
+// vscode-machineid: *
+// editor-version: vscode/1.89.1
+// editor-plugin-version: Copilot for Xcode/0.35.5
+// copilot-language-server-version: 1.236.0
+// x-github-api-version: 2023-07-07
+// openai-intent: conversation-panel
+// content-type: application/json
+// user-agent: GithubCopilot/1.236.0
+// content-length: 9061
+// accept: */*
+// accept-encoding: gzip,deflate,br
+
+ request.setValue(
+ "Copilot for Xcode/\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown")",
+ forHTTPHeaderField: "Editor-Version"
+ )
+ request.setValue("Bearer \(token.token)", forHTTPHeaderField: "Authorization")
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue("vscode-chat", forHTTPHeaderField: "Copilot-Integration-Id")
+ request.setValue("2023-07-07", forHTTPHeaderField: "X-Github-Api-Version")
+ }
+ }
+}
+
diff --git a/Tool/Sources/OpenAIService/APIs/OlamaChatCompletionsService.swift b/Tool/Sources/OpenAIService/APIs/OlamaChatCompletionsService.swift
index f95b2c74..9ac6e0dd 100644
--- a/Tool/Sources/OpenAIService/APIs/OlamaChatCompletionsService.swift
+++ b/Tool/Sources/OpenAIService/APIs/OlamaChatCompletionsService.swift
@@ -59,6 +59,13 @@ extension OllamaChatCompletionsService: ChatCompletionsAPI {
let encoder = JSONEncoder()
request.httpBody = try encoder.encode(requestBody)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+
+ if !apiKey.isEmpty {
+ request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
+ }
+
+ await Self.setupExtraHeaderFields(&request, model: model, apiKey: apiKey)
+
let (result, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
@@ -135,6 +142,13 @@ extension OllamaChatCompletionsService: ChatCompletionsStreamAPI {
let encoder = JSONEncoder()
request.httpBody = try encoder.encode(requestBody)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+
+ if !apiKey.isEmpty {
+ request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
+ }
+
+ await Self.setupExtraHeaderFields(&request, model: model, apiKey: apiKey)
+
let (result, response) = try await URLSession.shared.bytes(for: request)
guard let response = response as? HTTPURLResponse else {
diff --git a/Tool/Sources/OpenAIService/APIs/OllamaEmbeddingService.swift b/Tool/Sources/OpenAIService/APIs/OllamaEmbeddingService.swift
index dfd170cc..1e0f2933 100644
--- a/Tool/Sources/OpenAIService/APIs/OllamaEmbeddingService.swift
+++ b/Tool/Sources/OpenAIService/APIs/OllamaEmbeddingService.swift
@@ -12,6 +12,7 @@ struct OllamaEmbeddingService: EmbeddingAPI {
var embedding: [Float]
}
+ let apiKey: String
let model: EmbeddingModel
let endpoint: String
@@ -25,6 +26,14 @@ struct OllamaEmbeddingService: EmbeddingAPI {
model: model.info.modelName
))
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+
+ if !apiKey.isEmpty {
+ request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
+ }
+
+ for field in model.info.customHeaderInfo.headers where !field.key.isEmpty {
+ request.setValue(field.value, forHTTPHeaderField: field.key)
+ }
let (result, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
diff --git a/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift b/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift
index 1ad0c4d9..b614f775 100644
--- a/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift
+++ b/Tool/Sources/OpenAIService/APIs/OpenAIChatCompletionsService.swift
@@ -100,6 +100,8 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI
struct Delta: Codable {
var role: MessageRole?
var content: String?
+ var reasoning_content: String?
+ var reasoning: String?
var function_call: RequestBody.MessageFunctionCall?
var tool_calls: [RequestBody.MessageToolCall]?
}
@@ -112,6 +114,8 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI
var role: MessageRole
/// The content of the message.
var content: String?
+ var reasoning_content: String?
+ var reasoning: String?
/// When we want to reply to a function call with the result, we have to provide the
/// name of the function call, and include the result in `content`.
///
@@ -286,12 +290,14 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI
var endpoint: URL
var requestBody: RequestBody
var model: ChatModel
+ let requestModifier: ((inout URLRequest) -> Void)?
init(
apiKey: String,
model: ChatModel,
endpoint: URL,
- requestBody: ChatCompletionsRequestBody
+ requestBody: ChatCompletionsRequestBody,
+ requestModifier: ((inout URLRequest) -> Void)? = nil
) {
self.apiKey = apiKey
self.endpoint = endpoint
@@ -301,11 +307,14 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI
enforceMessageOrder: model.info.openAICompatibleInfo.enforceMessageOrder,
supportsMultipartMessageContent: model.info.openAICompatibleInfo
.supportsMultipartMessageContent,
+ requiresBeginWithUserMessage: model.info.openAICompatibleInfo
+ .requiresBeginWithUserMessage,
canUseTool: model.info.supportsFunctionCalling,
supportsImage: model.info.supportsImage,
supportsAudio: model.info.supportsAudio
)
self.model = model
+ self.requestModifier = requestModifier
}
func callAsFunction() async throws
@@ -320,7 +329,8 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI
Self.setupAppInformation(&request)
Self.setupAPIKey(&request, model: model, apiKey: apiKey)
- Self.setupExtraHeaderFields(&request, model: model)
+ await Self.setupExtraHeaderFields(&request, model: model, apiKey: apiKey)
+ requestModifier?(&request)
let (result, response) = try await URLSession.shared.bytes(for: request)
guard let response = response as? HTTPURLResponse else {
@@ -456,6 +466,8 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
case .azureOpenAI:
request.setValue(apiKey, forHTTPHeaderField: "api-key")
+ case .gitHubCopilot:
+ break
case .googleAI:
assertionFailure("Unsupported")
case .ollama:
@@ -465,12 +477,6 @@ actor OpenAIChatCompletionsService: ChatCompletionsStreamAPI, ChatCompletionsAPI
}
}
}
-
- static func setupExtraHeaderFields(_ request: inout URLRequest, model: ChatModel) {
- for field in model.info.customHeaderInfo.headers where !field.key.isEmpty {
- request.setValue(field.value, forHTTPHeaderField: field.key)
- }
- }
}
extension OpenAIChatCompletionsService.ResponseBody {
@@ -482,6 +488,7 @@ extension OpenAIChatCompletionsService.ResponseBody {
.init(
role: message.role.formalized,
content: message.content ?? "",
+ reasoningContent: message.reasoning_content ?? message.reasoning ?? "",
toolCalls: {
if let toolCalls = message.tool_calls {
return toolCalls.map { toolCall in
@@ -553,6 +560,8 @@ extension OpenAIChatCompletionsService.StreamDataChunk {
return .init(
role: choice.delta?.role?.formalized,
content: choice.delta?.content,
+ reasoningContent: choice.delta?.reasoning_content
+ ?? choice.delta?.reasoning,
toolCalls: {
if let toolCalls = choice.delta?.tool_calls {
return toolCalls.map {
@@ -702,6 +711,7 @@ extension OpenAIChatCompletionsService.RequestBody {
endpoint: URL,
enforceMessageOrder: Bool,
supportsMultipartMessageContent: Bool,
+ requiresBeginWithUserMessage: Bool,
canUseTool: Bool,
supportsImage: Bool,
supportsAudio: Bool
@@ -725,10 +735,21 @@ extension OpenAIChatCompletionsService.RequestBody {
model = body.model
+ var body = body
+
+ if requiresBeginWithUserMessage {
+ let firstUserIndex = body.messages.firstIndex(where: { $0.role == .user }) ?? 0
+ let endIndex = firstUserIndex
+ for i in stride(from: endIndex - 1, to: 0, by: -1)
+ where i >= 0 && body.messages.endIndex > i
+ {
+ body.messages.remove(at: i)
+ }
+ }
+
// Special case for Claude through OpenRouter
if endpoint.absoluteString.contains("openrouter.ai"), model.hasPrefix("anthropic/") {
- var body = body
body.model = model.replacingOccurrences(of: "anthropic/", with: "")
let claudeRequestBody = ClaudeChatCompletionsService.RequestBody(body)
messages = claudeRequestBody.system.map {
diff --git a/Tool/Sources/OpenAIService/APIs/OpenAIEmbeddingService.swift b/Tool/Sources/OpenAIService/APIs/OpenAIEmbeddingService.swift
index d6fe2780..f6edf3b7 100644
--- a/Tool/Sources/OpenAIService/APIs/OpenAIEmbeddingService.swift
+++ b/Tool/Sources/OpenAIService/APIs/OpenAIEmbeddingService.swift
@@ -16,6 +16,7 @@ struct OpenAIEmbeddingService: EmbeddingAPI {
let apiKey: String
let model: EmbeddingModel
let endpoint: String
+ var requestModifier: ((inout URLRequest) -> Void)? = nil
public func embed(text: String) async throws -> EmbeddingResponse {
return try await embed(texts: [text])
@@ -23,6 +24,13 @@ struct OpenAIEmbeddingService: EmbeddingAPI {
public func embed(texts text: [String]) async throws -> EmbeddingResponse {
guard let url = URL(string: endpoint) else { throw ChatGPTServiceError.endpointIncorrect }
+ if text.isEmpty {
+ return .init(
+ data: [],
+ model: model.info.modelName,
+ usage: .init(prompt_tokens: 0, total_tokens: 0)
+ )
+ }
var request = URLRequest(url: url)
request.httpMethod = "POST"
let encoder = JSONEncoder()
@@ -34,6 +42,8 @@ struct OpenAIEmbeddingService: EmbeddingAPI {
Self.setupAppInformation(&request)
Self.setupAPIKey(&request, model: model, apiKey: apiKey)
+ await Self.setupExtraHeaderFields(&request, model: model, apiKey: apiKey)
+ requestModifier?(&request)
let (result, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
@@ -55,6 +65,13 @@ struct OpenAIEmbeddingService: EmbeddingAPI {
public func embed(tokens: [[Int]]) async throws -> EmbeddingResponse {
guard let url = URL(string: endpoint) else { throw ChatGPTServiceError.endpointIncorrect }
+ if tokens.isEmpty {
+ return .init(
+ data: [],
+ model: model.info.modelName,
+ usage: .init(prompt_tokens: 0, total_tokens: 0)
+ )
+ }
var request = URLRequest(url: url)
request.httpMethod = "POST"
let encoder = JSONEncoder()
@@ -66,7 +83,8 @@ struct OpenAIEmbeddingService: EmbeddingAPI {
Self.setupAppInformation(&request)
Self.setupAPIKey(&request, model: model, apiKey: apiKey)
- Self.setupExtraHeaderFields(&request, model: model)
+ await Self.setupExtraHeaderFields(&request, model: model, apiKey: apiKey)
+ requestModifier?(&request)
let (result, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
@@ -121,16 +139,12 @@ struct OpenAIEmbeddingService: EmbeddingAPI {
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
case .azureOpenAI:
request.setValue(apiKey, forHTTPHeaderField: "api-key")
+ case .gitHubCopilot:
+ break
case .ollama:
assertionFailure("Unsupported")
}
}
}
-
- static func setupExtraHeaderFields(_ request: inout URLRequest, model: EmbeddingModel) {
- for field in model.info.customHeaderInfo.headers where !field.key.isEmpty {
- request.setValue(field.value, forHTTPHeaderField: field.key)
- }
- }
}
diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift
index c10cb01c..25ba3929 100644
--- a/Tool/Sources/OpenAIService/ChatGPTService.swift
+++ b/Tool/Sources/OpenAIService/ChatGPTService.swift
@@ -66,6 +66,7 @@ public struct ChatGPTError: Error, Codable, LocalizedError {
public enum ChatGPTResponse: Equatable {
case status([String])
case partialText(String)
+ case partialReasoning(String)
case toolCalls([ChatMessage.ToolCall])
}
@@ -195,6 +196,9 @@ public class ChatGPTService: ChatGPTServiceType {
switch content {
case let .partialText(text):
continuation.yield(ChatGPTResponse.partialText(text))
+
+ case let .partialReasoning(text):
+ continuation.yield(ChatGPTResponse.partialReasoning(text))
case let .partialToolCalls(toolCalls):
guard configuration.runFunctionsAutomatically else { break }
@@ -250,6 +254,7 @@ public class ChatGPTService: ChatGPTServiceType {
extension ChatGPTService {
enum StreamContent {
+ case partialReasoning(String)
case partialText(String)
case partialToolCalls([Int: ChatMessage.ToolCall])
}
@@ -340,6 +345,10 @@ extension ChatGPTService {
if let content = delta.content {
continuation.yield(.partialText(content))
}
+
+ if let reasoning = delta.reasoningContent {
+ continuation.yield(.partialReasoning(reasoning))
+ }
}
Logger.service.info("ChatGPT usage: \(usage)")
@@ -531,7 +540,7 @@ extension ChatGPTService {
stream: Bool
) -> ChatCompletionsRequestBody {
let serviceSupportsFunctionCalling = switch model.format {
- case .openAI, .openAICompatible, .azureOpenAI:
+ case .openAI, .openAICompatible, .azureOpenAI, .gitHubCopilot:
model.info.supportsFunctionCalling
case .ollama, .googleAI, .claude:
false
diff --git a/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift
index 3b0bd896..3f6c0afe 100644
--- a/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift
+++ b/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift
@@ -120,7 +120,7 @@ public class OverridingChatGPTConfiguration: ChatGPTConfiguration {
public var apiKey: String {
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
+ return (try? Keychain.apiKey.get(name)) ?? ""
}
public var shouldEndTextWindow: (String) -> Bool {
diff --git a/Tool/Sources/OpenAIService/EmbeddingService.swift b/Tool/Sources/OpenAIService/EmbeddingService.swift
index 0e54d3ac..d0bf1116 100644
--- a/Tool/Sources/OpenAIService/EmbeddingService.swift
+++ b/Tool/Sources/OpenAIService/EmbeddingService.swift
@@ -23,9 +23,14 @@ public struct EmbeddingService {
).embed(text: text)
case .ollama:
embeddingResponse = try await OllamaEmbeddingService(
+ apiKey: configuration.apiKey,
model: model,
endpoint: configuration.endpoint
).embed(text: text)
+ case .gitHubCopilot:
+ embeddingResponse = try await GitHubCopilotEmbeddingService(
+ model: model
+ ).embed(text: text)
}
#if DEBUG
@@ -54,9 +59,14 @@ public struct EmbeddingService {
).embed(texts: text)
case .ollama:
embeddingResponse = try await OllamaEmbeddingService(
+ apiKey: configuration.apiKey,
model: model,
endpoint: configuration.endpoint
).embed(texts: text)
+ case .gitHubCopilot:
+ embeddingResponse = try await GitHubCopilotEmbeddingService(
+ model: model
+ ).embed(texts: text)
}
#if DEBUG
@@ -85,9 +95,14 @@ public struct EmbeddingService {
).embed(tokens: tokens)
case .ollama:
embeddingResponse = try await OllamaEmbeddingService(
+ apiKey: configuration.apiKey,
model: model,
endpoint: configuration.endpoint
).embed(tokens: tokens)
+ case .gitHubCopilot:
+ embeddingResponse = try await GitHubCopilotEmbeddingService(
+ model: model
+ ).embed(tokens: tokens)
}
#if DEBUG
diff --git a/Tool/Sources/OpenAIService/HeaderValueParser.swift b/Tool/Sources/OpenAIService/HeaderValueParser.swift
new file mode 100644
index 00000000..0042ea75
--- /dev/null
+++ b/Tool/Sources/OpenAIService/HeaderValueParser.swift
@@ -0,0 +1,104 @@
+import Foundation
+import GitHubCopilotService
+import Logger
+import Terminal
+
+public struct HeaderValueParser {
+ public enum Placeholder: String {
+ case gitHubCopilotOBearerToken = "github_copilot_bearer_token"
+ case apiKey = "api_key"
+ case modelName = "model_name"
+ }
+
+ public struct Context {
+ public var modelName: String
+ public var apiKey: String
+ public var gitHubCopilotToken: () async -> GitHubCopilotExtension.Token?
+ public var shellEnvironmentVariable: (_ key: String) async -> String?
+
+ public init(
+ modelName: String,
+ apiKey: String,
+ gitHubCopilotToken: (() async -> GitHubCopilotExtension.Token?)? = nil,
+ shellEnvironmentVariable: ((_: String) async -> String?)? = nil
+ ) {
+ self.modelName = modelName
+ self.apiKey = apiKey
+ self.gitHubCopilotToken = gitHubCopilotToken ?? {
+ try? await GitHubCopilotExtension.fetchToken()
+ }
+ self.shellEnvironmentVariable = shellEnvironmentVariable ?? { p in
+ let shell = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/bash"
+ let terminal = Terminal()
+ return try? await terminal.runCommand(
+ shell,
+ arguments: ["-i", "-l", "-c", "echo $\(p)"],
+ environment: [:]
+ )
+ }
+ }
+ }
+
+ public init() {}
+
+ /// Replace `{{PlaceHolder}}` with exact values.
+ public func parse(_ value: String, context: Context) async -> String {
+ var parsedValue = value
+ let placeholderRanges = findPlaceholderRanges(in: parsedValue)
+
+ for (range, placeholderText) in placeholderRanges.reversed() {
+ let cleanPlaceholder = placeholderText
+ .trimmingCharacters(in: CharacterSet(charactersIn: "{}"))
+
+ var replacement: String?
+ if let knownPlaceholder = Placeholder(rawValue: cleanPlaceholder) {
+ async let token = context.gitHubCopilotToken()
+ switch knownPlaceholder {
+ case .gitHubCopilotOBearerToken:
+ replacement = await token?.token
+ case .apiKey:
+ replacement = context.apiKey
+ case .modelName:
+ replacement = context.modelName
+ }
+ } else {
+ replacement = await context.shellEnvironmentVariable(cleanPlaceholder)
+ }
+
+ if let replacement {
+ parsedValue.replaceSubrange(
+ range,
+ with: replacement.trimmingCharacters(in: .whitespacesAndNewlines)
+ )
+ } else {
+ parsedValue.replaceSubrange(range, with: "none")
+ }
+ }
+
+ return parsedValue
+ }
+
+ private func findPlaceholderRanges(in string: String) -> [(Range, String)] {
+ var ranges: [(Range, String)] = []
+ let pattern = #"\{\{[^}]+\}\}"#
+
+ do {
+ let regex = try NSRegularExpression(pattern: pattern)
+ let matches = regex.matches(
+ in: string,
+ range: NSRange(string.startIndex..., in: string)
+ )
+
+ for match in matches {
+ if let range = Range(match.range, in: string) {
+ ranges.append((range, String(string[range])))
+ }
+ }
+ } catch {
+ Logger.service.error("Failed to find placeholders in string: \(string)")
+ }
+
+ return ranges
+ }
+}
+
diff --git a/Tool/Sources/Preferences/Types/ChatGPTModel.swift b/Tool/Sources/Preferences/Types/ChatGPTModel.swift
index 8ac04faf..ae199b75 100644
--- a/Tool/Sources/Preferences/Types/ChatGPTModel.swift
+++ b/Tool/Sources/Preferences/Types/ChatGPTModel.swift
@@ -19,10 +19,12 @@ public enum ChatGPTModel: String, CaseIterable {
case gpt432k0314 = "gpt-4-32k-0314"
case gpt432k0613 = "gpt-4-32k-0613"
case gpt40125 = "gpt-4-0125-preview"
+ case o1 = "o1"
case o1Preview = "o1-preview"
case o1Preview20240912 = "o1-preview-2024-09-12"
case o1Mini = "o1-mini"
case o1Mini20240912 = "o1-mini-2024-09-12"
+ case o3Mini = "o3-mini"
}
public extension ChatGPTModel {
@@ -68,13 +70,17 @@ public extension ChatGPTModel {
return 128_000
case .o1Mini, .o1Mini20240912:
return 128_000
+ case .o1:
+ return 200_000
+ case .o3Mini:
+ return 200_000
}
}
var supportsImages: Bool {
switch self {
case .gpt4VisionPreview, .gpt4Turbo, .gpt4Turbo20240409, .gpt4o, .gpt4oMini, .o1Preview,
- .o1Preview20240912, .o1Mini, .o1Mini20240912:
+ .o1Preview20240912, .o1Mini, .o1Mini20240912, .o1, .o3Mini:
return true
default:
return false
diff --git a/Tool/Sources/SuggestionBasic/EditorInformation.swift b/Tool/Sources/SuggestionBasic/EditorInformation.swift
index 8518b8b0..d9979890 100644
--- a/Tool/Sources/SuggestionBasic/EditorInformation.swift
+++ b/Tool/Sources/SuggestionBasic/EditorInformation.swift
@@ -24,6 +24,7 @@ public struct EditorInformation {
public var selectedContent: String {
if let range = selections.first {
+ if range.isEmpty { return "" }
let startIndex = min(
max(0, range.start.line),
lines.endIndex - 1
@@ -103,7 +104,7 @@ public struct EditorInformation {
inside range: CursorRange,
ignoreColumns: Bool = false
) -> (code: String, lines: [String]) {
- guard range.start <= range.end else { return ("", []) }
+ guard range.start < range.end else { return ("", []) }
let rangeLines = lines(in: code, containing: range)
if ignoreColumns {
diff --git a/Version.xcconfig b/Version.xcconfig
index 44c36d4a..bc3af414 100644
--- a/Version.xcconfig
+++ b/Version.xcconfig
@@ -1,4 +1,4 @@
-APP_VERSION = 0.35.4
-APP_BUILD = 437
+APP_VERSION = 0.35.5
+APP_BUILD = 443
RELEASE_CHANNEL =
RELEASE_NUMBER = 1