@@ -14,6 +14,7 @@ import Logger
1414import Workspace
1515import XcodeInspector
1616import OrderedCollections
17+ import SystemUtils
1718
1819public protocol ChatServiceType {
1920var memory : ContextAwareAutoManagedChatMemory { get set }
@@ -330,22 +331,42 @@ public final class ChatService: ChatServiceType, ObservableObject {
330331let 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
342350if 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+ }
343364await memory. appendMessage ( chatMessage)
344365}
345366
346367 // reset file edits
347368self . resetFileEdits ( )
348-
369+
349370 // persist
350371saveChatMessageToStorage ( chatMessage)
351372
@@ -370,32 +391,68 @@ public final class ChatService: ChatServiceType, ObservableObject {
370391return
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]
374435let supportedSkills : [ String ] = skillSet. map { $0. id}
375436let ignoredSkills : [ String ] = skillCapabilities. filter {
376437 !supportedSkills. contains ( $0)
377438}
378- let currentEditorSkill = skillSet. first { $0. id== CurrentEditorSkill . ID}
379- let activeDoc : Doc ? = ( currentEditorSkillas? CurrentEditorSkill ) . map { Doc ( uri: $0. currentFile. url. absoluteString) }
380439
381440 /// replace the `@workspace` to `@project`
382441let 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
401458public 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
570623Task {
571624if 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 { historyin
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
573639saveChatMessageToStorage ( lastUserMessage)
574640}
@@ -653,14 +719,9 @@ public final class ChatService: ChatServiceType, ObservableObject {
653719Task {
654720await 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()
665726await 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 " ) {
685746Task {
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)
693751await memory. appendMessage ( errorMessage)
694752resetOngoingRequest ( )
695753return
696754}
697755} else {
698756Task {
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()
708759await memory. appendMessage ( errorMessage)
709760resetOngoingRequest ( )
@@ -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+
731798private func resetOngoingRequest( ) {
732799 activeRequestId= nil
733800 isReceivingMessage= false