@@ -14,10 +14,11 @@ import Logger
1414import Workspace
1515import XcodeInspector
1616import OrderedCollections
17+ import SystemUtils
1718
1819public 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
0 commit comments