Skip to content

Commit d3cd006

Browse files
committed
Pre-release 0.36.123
1 parent 2e8e989 commit d3cd006

34 files changed

+1036
-285
lines changed

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: 122 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ 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 }
@@ -330,22 +331,42 @@ public final class ChatService: ChatServiceType, ObservableObject {
330331
let workDoneToken = UUID().uuidString
331332
activeRequestId = workDoneToken
332333

333-
let chatMessage = ChatMessage(
334+
var chatMessage = ChatMessage(
334335
id: id,
335336
chatTabID: self.chatTabInfo.id,
336337
role: .user,
337338
content: content,
338339
references: references.toConversationReferences()
339340
)
340341

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
341349
// If turnId is provided, it is used to update the existing message, no need to append the user message
342350
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+
}
343364
await memory.appendMessage(chatMessage)
344365
}
345366

346367
// reset file edits
347368
self.resetFileEdits()
348-
369+
349370
// persist
350371
saveChatMessageToStorage(chatMessage)
351372

@@ -370,32 +391,68 @@ public final class ChatService: ChatServiceType, ObservableObject {
370391
return
371392
}
372393

373-
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]
374435
let supportedSkills: [String] = skillSet.map { $0.id }
375436
let ignoredSkills: [String] = skillCapabilities.filter {
376437
!supportedSkills.contains($0)
377438
}
378-
let currentEditorSkill = skillSet.first { $0.id == CurrentEditorSkill.ID }
379-
let activeDoc: Doc? = (currentEditorSkill as? CurrentEditorSkill).map { Doc(uri: $0.currentFile.url.absoluteString) }
380439

381440
/// replace the `@workspace` to `@project`
382441
let newContent = replaceFirstWord(in: content, from: "@workspace", to: "@project")
383442

384-
let request = ConversationRequest(workDoneToken: workDoneToken,
385-
content: newContent,
386-
workspaceFolder: "",
387-
activeDoc: activeDoc,
388-
skills: skillCapabilities,
389-
ignoredSkills: ignoredSkills,
390-
references: references,
391-
model: model,
392-
agentMode: agentMode,
393-
userLanguage: userLanguage,
394-
turnId: turnId
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
395455
)
396-
self.lastUserRequest = request
397-
self.skillSet = skillSet
398-
try await send(request)
399456
}
400457

401458
public func sendAndWait(_ id: String, content: String) async throws -> String {
@@ -444,20 +501,16 @@ public final class ChatService: ChatServiceType, ObservableObject {
444501
{
445502
// TODO: clean up contents for resend message
446503
activeRequestId = nil
447-
do {
448-
try await send(
449-
id,
450-
content: lastUserRequest.content,
451-
skillSet: skillSet,
452-
references: lastUserRequest.references ?? [],
453-
model: model != nil ? model : lastUserRequest.model,
454-
agentMode: lastUserRequest.agentMode,
455-
userLanguage: lastUserRequest.userLanguage,
456-
turnId: id
457-
)
458-
} catch {
459-
print("Failed to resend message")
460-
}
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+
)
461514
}
462515
}
463516

@@ -569,6 +622,19 @@ public final class ChatService: ChatServiceType, ObservableObject {
569622

570623
Task {
571624
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+
572638
lastUserMessage.clsTurnID = progress.turnId
573639
saveChatMessageToStorage(lastUserMessage)
574640
}
@@ -653,14 +719,9 @@ public final class ChatService: ChatServiceType, ObservableObject {
653719
Task {
654720
await Status.shared
655721
.updateCLSStatus(.warning, busy: false, message: CLSError.message)
656-
let errorMessage = ChatMessage(
657-
id: progress.turnId,
658-
chatTabID: self.chatTabInfo.id,
659-
clsTurnID: progress.turnId,
660-
role: .assistant,
661-
content: "",
662-
panelMessages: [.init(type: .error, title: String(CLSError.code ?? 0), message: CLSError.message, location: .Panel)]
663-
)
722+
let errorMessage = buildErrorMessage(
723+
turnId: progress.turnId,
724+
panelMessages: [.init(type: .error, title: String(CLSError.code ?? 0), message: CLSError.message, location: .Panel)])
664725
// will persist in resetongoingRequest()
665726
await memory.appendMessage(errorMessage)
666727

@@ -683,27 +744,17 @@ public final class ChatService: ChatServiceType, ObservableObject {
683744
}
684745
} else if CLSError.code == 400 && CLSError.message.contains("model is not supported") {
685746
Task {
686-
let errorMessage = ChatMessage(
687-
id: progress.turnId,
688-
chatTabID: self.chatTabInfo.id,
689-
role: .assistant,
690-
content: "",
691-
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)."]
692750
)
693751
await memory.appendMessage(errorMessage)
694752
resetOngoingRequest()
695753
return
696754
}
697755
} else {
698756
Task {
699-
let errorMessage = ChatMessage(
700-
id: progress.turnId,
701-
chatTabID: self.chatTabInfo.id,
702-
clsTurnID: progress.turnId,
703-
role: .assistant,
704-
content: "",
705-
errorMessage: CLSError.message
706-
)
757+
let errorMessage = buildErrorMessage(turnId: progress.turnId, errorMessages: [CLSError.message])
707758
// will persist in resetOngoingRequest()
708759
await memory.appendMessage(errorMessage)
709760
resetOngoingRequest()
@@ -728,6 +779,22 @@ public final class ChatService: ChatServiceType, ObservableObject {
728779
}
729780
}
730781

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+
)
796+
}
797+
731798
private func resetOngoingRequest() {
732799
activeRequestId = nil
733800
isReceivingMessage = false

Core/Sources/ChatService/Skills/CurrentEditorSkill.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import ConversationServiceProvider
22
import Foundation
33
import GitHubCopilotService
44
import JSONRPC
5+
import SystemUtils
56

67
public class CurrentEditorSkill: ConversationSkill {
78
public static let ID = "current-editor"
89
public let currentFile: FileReference
910
public var id: String {
1011
return CurrentEditorSkill.ID
1112
}
13+
public var currentFilePath: String { currentFile.url.path }
1214

1315
public init(
1416
currentFile: FileReference
@@ -20,6 +22,17 @@ public class CurrentEditorSkill: ConversationSkill {
2022
return params.skillId == self.id
2123
}
2224

25+
public static let readabilityErrorMessageProvider: FileUtils.ReadabilityErrorMessageProvider = { status in
26+
switch status {
27+
case .readable:
28+
return nil
29+
case .notFound:
30+
return "Copilot can’t find the current file, so it's not included."
31+
case .permissionDenied:
32+
return "Copilot can't access the current file. Enable \"Files & Folders\" access in [System Settings](x-apple.systempreferences:com.apple.preference.security?Privacy_FilesAndFolders)."
33+
}
34+
}
35+
2336
public func resolveSkill(request: ConversationContextRequest, completion: JSONRPCResponseHandler){
2437
let uri: String? = self.currentFile.url.absoluteString
2538
completion(

Core/Sources/ConversationTab/Chat.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public struct DisplayedChatMessage: Equatable {
2424
public var references: [ConversationReference] = []
2525
public var followUp: ConversationFollowUp? = nil
2626
public var suggestedTitle: String? = nil
27-
public var errorMessage: String? = nil
27+
public var errorMessages: [String] = []
2828
public var steps: [ConversationProgressStep] = []
2929
public var editAgentRounds: [AgentRound] = []
3030
public var panelMessages: [CopilotShowMessageParams] = []
@@ -36,7 +36,7 @@ public struct DisplayedChatMessage: Equatable {
3636
references: [ConversationReference] = [],
3737
followUp: ConversationFollowUp? = nil,
3838
suggestedTitle: String? = nil,
39-
errorMessage: String? = nil,
39+
errorMessages: [String] = [],
4040
steps: [ConversationProgressStep] = [],
4141
editAgentRounds: [AgentRound] = [],
4242
panelMessages: [CopilotShowMessageParams] = []
@@ -47,7 +47,7 @@ public struct DisplayedChatMessage: Equatable {
4747
self.references = references
4848
self.followUp = followUp
4949
self.suggestedTitle = suggestedTitle
50-
self.errorMessage = errorMessage
50+
self.errorMessages = errorMessages
5151
self.steps = steps
5252
self.editAgentRounds = editAgentRounds
5353
self.panelMessages = panelMessages
@@ -371,7 +371,7 @@ struct Chat {
371371
},
372372
followUp: message.followUp,
373373
suggestedTitle: message.suggestedTitle,
374-
errorMessage: message.errorMessage,
374+
errorMessages: message.errorMessages,
375375
steps: message.steps,
376376
editAgentRounds: message.editAgentRounds,
377377
panelMessages: message.panelMessages

0 commit comments

Comments
 (0)