Skip to content

Commit 14f64e9

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents f3011e4 + 64a0691 commit 14f64e9

File tree

84 files changed

+3389
-728
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+3389
-728
lines changed

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## 0.37.0 - June 18, 2025
9+
### Added
10+
- **Advanced** settings: Added option to configure **Custom Instructions** for GitHub Copilot during chat sessions.
11+
- **Advanced** settings: Added option to keep the chat window automatically attached to Xcode.
12+
13+
### Changed
14+
- Enabled support for dragging-and-dropping files into the chat panel to provide context.
15+
16+
### Fixed
17+
- "Add Context" menu didn’t show files in workspaces organized with Xcode’s group feature.
18+
- Chat didn’t respond when the workspace was in a system folder (like Desktop, Downloads, or Documents) and access permission hadn’t been granted.
19+
20+
## 0.36.0 - June 4, 2025
21+
### Added
22+
- Introduced a new chat setting "**Response Language**" under **Advanced** settings to customize the natural language used in chat replies.
23+
- Enabled support for custom instructions defined in _.github/copilot-instructions.md_ within your workspace.
24+
- Added support for premium request handling.
25+
26+
### Fixed
27+
- Performance: Improved UI responsiveness by lazily restoring chat history.
28+
- Performance: Fixed lagging issue when pasting large text into the chat input.
29+
- Performance: Improved project indexing performance.
30+
- Don't trigger / (slash) commands when pasting a file path into the chat input.
31+
- Adjusted terminal text styling to align with Xcode’s theme.
32+
833
## 0.35.0 - May 19, 2025
934
### Added
1035
- Launched Agent Mode. Copilot will automatically use multiple requests to edit files, run terminal commands, and fix errors.

Core/Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ let package = Package(
180180
.product(name: "ConversationServiceProvider", package: "Tool"),
181181
.product(name: "GitHubCopilotService", package: "Tool"),
182182
.product(name: "Workspace", package: "Tool"),
183-
.product(name: "Terminal", package: "Tool")
183+
.product(name: "Terminal", package: "Tool"),
184+
.product(name: "SystemUtils", package: "Tool")
184185
]),
185186
.testTarget(
186187
name: "ChatServiceTests",

Core/Sources/ChatService/ChatService.swift

Lines changed: 185 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ import Logger
1414
import Workspace
1515
import XcodeInspector
1616
import OrderedCollections
17+
import SystemUtils
1718

1819
public protocol ChatServiceType {
1920
var memory: ContextAwareAutoManagedChatMemory { get set }
20-
func send(_ id: String, content: String, skillSet: [ConversationSkill], references: [FileReference], model: String?, agentMode: Bool) async throws
21+
func send(_ id: String, content: String, skillSet: [ConversationSkill], references: [FileReference], model: String?, agentMode: Bool, userLanguage: String?, turnId: String?) async throws
2122
func stopReceivingMessage() async
2223
func upvote(_ id: String, _ rating: ConversationRating) async
2324
func downvote(_ id: String, _ rating: ConversationRating) async
@@ -79,6 +80,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
7980
private var activeRequestId: String?
8081
private(set) public var conversationId: String?
8182
private var skillSet: [ConversationSkill] = []
83+
private var lastUserRequest: ConversationRequest?
8284
private var isRestored: Bool = false
8385
private var pendingToolCallRequests: [String: ToolCallRequest] = [:]
8486
init(provider: any ConversationServiceProvider,
@@ -98,6 +100,18 @@ public final class ChatService: ChatServiceType, ObservableObject {
98100
subscribeToClientToolConfirmationEvent()
99101
}
100102

103+
deinit {
104+
Task { [weak self] in
105+
await self?.stopReceivingMessage()
106+
}
107+
108+
// Clear all subscriptions
109+
cancellables.forEach { $0.cancel() }
110+
cancellables.removeAll()
111+
112+
// Memory will be deallocated automatically
113+
}
114+
101115
private func subscribeToNotifications() {
102116
memory.observeHistoryChange { [weak self] in
103117
Task { [weak self] in
@@ -303,19 +317,52 @@ public final class ChatService: ChatServiceType, ObservableObject {
303317
}
304318
}
305319

306-
public func send(_ id: String, content: String, skillSet: Array<ConversationSkill>, references: Array<FileReference>, model: String? = nil, agentMode: Bool = false) async throws {
320+
public func send(
321+
_ id: String,
322+
content: String,
323+
skillSet: Array<ConversationSkill>,
324+
references: Array<FileReference>,
325+
model: String? = nil,
326+
agentMode: Bool = false,
327+
userLanguage: String? = nil,
328+
turnId: String? = nil
329+
) async throws {
307330
guard activeRequestId == nil else { return }
308331
let workDoneToken = UUID().uuidString
309332
activeRequestId = workDoneToken
310333

311-
let chatMessage = ChatMessage(
334+
var chatMessage = ChatMessage(
312335
id: id,
313336
chatTabID: self.chatTabInfo.id,
314337
role: .user,
315338
content: content,
316339
references: references.toConversationReferences()
317340
)
318-
await memory.appendMessage(chatMessage)
341+
342+
let currentEditorSkill = skillSet.first(where: { $0.id == CurrentEditorSkill.ID }) as? CurrentEditorSkill
343+
let currentFileReadability = currentEditorSkill == nil
344+
? nil
345+
: FileUtils.checkFileReadability(at: currentEditorSkill!.currentFilePath)
346+
var errorMessage: ChatMessage?
347+
348+
var currentTurnId: String? = turnId
349+
// If turnId is provided, it is used to update the existing message, no need to append the user message
350+
if turnId == nil {
351+
if let currentFileReadability, !currentFileReadability.isReadable {
352+
// For associating error message with user message
353+
currentTurnId = UUID().uuidString
354+
chatMessage.clsTurnID = currentTurnId
355+
errorMessage = buildErrorMessage(
356+
turnId: currentTurnId!,
357+
errorMessages: [
358+
currentFileReadability.errorMessage(
359+
using: CurrentEditorSkill.readabilityErrorMessageProvider
360+
)
361+
].compactMap { $0 }.filter { !$0.isEmpty }
362+
)
363+
}
364+
await memory.appendMessage(chatMessage)
365+
}
319366

320367
// reset file edits
321368
self.resetFileEdits()
@@ -344,28 +391,68 @@ public final class ChatService: ChatServiceType, ObservableObject {
344391
return
345392
}
346393

347-
let skillCapabilities: [String] = [ CurrentEditorSkill.ID, ProblemsInActiveDocumentSkill.ID ]
394+
if let errorMessage {
395+
Task { await memory.appendMessage(errorMessage) }
396+
}
397+
398+
var activeDoc: Doc?
399+
var validSkillSet: [ConversationSkill] = skillSet
400+
if let currentEditorSkill, currentFileReadability?.isReadable == true {
401+
activeDoc = Doc(uri: currentEditorSkill.currentFile.url.absoluteString)
402+
} else {
403+
validSkillSet.removeAll(where: { $0.id == CurrentEditorSkill.ID || $0.id == ProblemsInActiveDocumentSkill.ID })
404+
}
405+
406+
let request = createConversationRequest(
407+
workDoneToken: workDoneToken,
408+
content: content,
409+
activeDoc: activeDoc,
410+
references: references,
411+
model: model,
412+
agentMode: agentMode,
413+
userLanguage: userLanguage,
414+
turnId: currentTurnId,
415+
skillSet: validSkillSet
416+
)
417+
418+
self.lastUserRequest = request
419+
self.skillSet = validSkillSet
420+
try await send(request)
421+
}
422+
423+
private func createConversationRequest(
424+
workDoneToken: String,
425+
content: String,
426+
activeDoc: Doc?,
427+
references: [FileReference],
428+
model: String? = nil,
429+
agentMode: Bool = false,
430+
userLanguage: String? = nil,
431+
turnId: String? = nil,
432+
skillSet: [ConversationSkill]
433+
) -> ConversationRequest {
434+
let skillCapabilities: [String] = [CurrentEditorSkill.ID, ProblemsInActiveDocumentSkill.ID]
348435
let supportedSkills: [String] = skillSet.map { $0.id }
349436
let ignoredSkills: [String] = skillCapabilities.filter {
350437
!supportedSkills.contains($0)
351438
}
352-
let currentEditorSkill = skillSet.first { $0.id == CurrentEditorSkill.ID }
353-
let activeDoc: Doc? = (currentEditorSkill as? CurrentEditorSkill).map { Doc(uri: $0.currentFile.url.absoluteString) }
354439

355440
/// replace the `@workspace` to `@project`
356441
let newContent = replaceFirstWord(in: content, from: "@workspace", to: "@project")
357442

358-
let request = ConversationRequest(workDoneToken: workDoneToken,
359-
content: newContent,
360-
workspaceFolder: "",
361-
activeDoc: activeDoc,
362-
skills: skillCapabilities,
363-
ignoredSkills: ignoredSkills,
364-
references: references,
365-
model: model,
366-
agentMode: agentMode)
367-
self.skillSet = skillSet
368-
try await send(request)
443+
return ConversationRequest(
444+
workDoneToken: workDoneToken,
445+
content: newContent,
446+
workspaceFolder: "",
447+
activeDoc: activeDoc,
448+
skills: skillCapabilities,
449+
ignoredSkills: ignoredSkills,
450+
references: references,
451+
model: model,
452+
agentMode: agentMode,
453+
userLanguage: userLanguage,
454+
turnId: turnId
455+
)
369456
}
370457

371458
public func sendAndWait(_ id: String, content: String) async throws -> String {
@@ -408,15 +495,22 @@ public final class ChatService: ChatServiceType, ObservableObject {
408495
deleteChatMessageFromStorage(id)
409496
}
410497

411-
// Not used for now
412-
public func resendMessage(id: String) async throws {
413-
if let message = (await memory.history).first(where: { $0.id == id })
498+
public func resendMessage(id: String, model: String? = nil) async throws {
499+
if let _ = (await memory.history).first(where: { $0.id == id }),
500+
let lastUserRequest
414501
{
415-
do {
416-
try await send(id, content: message.content, skillSet: [], references: [])
417-
} catch {
418-
print("Failed to resend message")
419-
}
502+
// TODO: clean up contents for resend message
503+
activeRequestId = nil
504+
try await send(
505+
id,
506+
content: lastUserRequest.content,
507+
skillSet: skillSet,
508+
references: lastUserRequest.references ?? [],
509+
model: model != nil ? model : lastUserRequest.model,
510+
agentMode: lastUserRequest.agentMode,
511+
userLanguage: lastUserRequest.userLanguage,
512+
turnId: id
513+
)
420514
}
421515
}
422516

@@ -528,6 +622,19 @@ public final class ChatService: ChatServiceType, ObservableObject {
528622

529623
Task {
530624
if var lastUserMessage = await memory.history.last(where: { $0.role == .user }) {
625+
626+
// Case: New conversation where error message was generated before CLS request
627+
// Using clsTurnId to associate this error message with the corresponding user message
628+
// When merging error messages with bot responses from CLS, these properties need to be updated
629+
await memory.mutateHistory { history in
630+
if let existingBotIndex = history.lastIndex(where: {
631+
$0.role == .assistant && $0.clsTurnID == lastUserMessage.clsTurnID
632+
}) {
633+
history[existingBotIndex].id = turnId
634+
history[existingBotIndex].clsTurnID = turnId
635+
}
636+
}
637+
531638
lastUserMessage.clsTurnID = progress.turnId
532639
saveChatMessageToStorage(lastUserMessage)
533640
}
@@ -611,45 +718,49 @@ public final class ChatService: ChatServiceType, ObservableObject {
611718
if CLSError.code == 402 {
612719
Task {
613720
await Status.shared
614-
.updateCLSStatus(.error, busy: false, message: CLSError.message)
615-
let errorMessage = ChatMessage(
616-
id: progress.turnId,
617-
chatTabID: self.chatTabInfo.id,
618-
clsTurnID: progress.turnId,
619-
role: .system,
620-
content: CLSError.message
621-
)
721+
.updateCLSStatus(.warning, busy: false, message: CLSError.message)
722+
let errorMessage = buildErrorMessage(
723+
turnId: progress.turnId,
724+
panelMessages: [.init(type: .error, title: String(CLSError.code ?? 0), message: CLSError.message, location: .Panel)])
622725
// will persist in resetongoingRequest()
623-
await memory.removeMessage(progress.turnId)
624726
await memory.appendMessage(errorMessage)
727+
728+
if let lastUserRequest {
729+
guard let fallbackModel = CopilotModelManager.getFallbackLLM(
730+
scope: lastUserRequest.agentMode ? .agentPanel : .chatPanel
731+
) else {
732+
resetOngoingRequest()
733+
return
734+
}
735+
do {
736+
CopilotModelManager.switchToFallbackModel()
737+
try await resendMessage(id: progress.turnId, model: fallbackModel.id)
738+
} catch {
739+
Logger.gitHubCopilot.error(error)
740+
resetOngoingRequest()
741+
}
742+
return
743+
}
625744
}
626745
} else if CLSError.code == 400 && CLSError.message.contains("model is not supported") {
627746
Task {
628-
let errorMessage = ChatMessage(
629-
id: progress.turnId,
630-
chatTabID: self.chatTabInfo.id,
631-
role: .assistant,
632-
content: "",
633-
errorMessage: "Oops, the model is not supported. Please enable it first in [GitHub Copilot settings](https://github.com/settings/copilot)."
747+
let errorMessage = buildErrorMessage(
748+
turnId: progress.turnId,
749+
errorMessages: ["Oops, the model is not supported. Please enable it first in [GitHub Copilot settings](https://github.com/settings/copilot)."]
634750
)
635751
await memory.appendMessage(errorMessage)
752+
resetOngoingRequest()
753+
return
636754
}
637755
} else {
638756
Task {
639-
let errorMessage = ChatMessage(
640-
id: progress.turnId,
641-
chatTabID: self.chatTabInfo.id,
642-
clsTurnID: progress.turnId,
643-
role: .assistant,
644-
content: "",
645-
errorMessage: CLSError.message
646-
)
757+
let errorMessage = buildErrorMessage(turnId: progress.turnId, errorMessages: [CLSError.message])
647758
// will persist in resetOngoingRequest()
648759
await memory.appendMessage(errorMessage)
760+
resetOngoingRequest()
761+
return
649762
}
650763
}
651-
resetOngoingRequest()
652-
return
653764
}
654765

655766
Task {
@@ -664,9 +775,24 @@ public final class ChatService: ChatServiceType, ObservableObject {
664775
)
665776
// will persist in resetOngoingRequest()
666777
await memory.appendMessage(message)
778+
resetOngoingRequest()
667779
}
668-
669-
resetOngoingRequest()
780+
}
781+
782+
private func buildErrorMessage(
783+
turnId: String,
784+
errorMessages: [String] = [],
785+
panelMessages: [CopilotShowMessageParams] = []
786+
) -> ChatMessage {
787+
return .init(
788+
id: turnId,
789+
chatTabID: chatTabInfo.id,
790+
clsTurnID: turnId,
791+
role: .assistant,
792+
content: "",
793+
errorMessages: errorMessages,
794+
panelMessages: panelMessages
795+
)
670796
}
671797

672798
private func resetOngoingRequest() {
@@ -732,7 +858,12 @@ public final class ChatService: ChatServiceType, ObservableObject {
732858

733859
do {
734860
if let conversationId = conversationId {
735-
try await conversationProvider?.createTurn(with: conversationId, request: request, workspaceURL: getWorkspaceURL())
861+
try await conversationProvider?
862+
.createTurn(
863+
with: conversationId,
864+
request: request,
865+
workspaceURL: getWorkspaceURL()
866+
)
736867
} else {
737868
var requestWithTurns = request
738869

Core/Sources/ChatService/ContextAwareAutoManagedChatMemory.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public final class ContextAwareAutoManagedChatMemory: ChatMemory {
1818
systemPrompt: ""
1919
)
2020
}
21+
22+
deinit { }
2123

2224
public func mutateHistory(_ update: (inout [ChatMessage]) -> Void) async {
2325
await memory.mutateHistory(update)

0 commit comments

Comments
 (0)