@@ -17,7 +17,7 @@ import OrderedCollections
1717
1818public protocol ChatServiceType {
1919 var memory : ContextAwareAutoManagedChatMemory { get set }
20- func send( _ id: String , content: String , skillSet: [ ConversationSkill ] , references: [ FileReference ] , model: String ? , agentMode: Bool ) async throws
20+ func send( _ id: String , content: String , skillSet: [ ConversationSkill ] , references: [ FileReference ] , model: String ? , agentMode: Bool , userLanguage : String ? , turnId : String ? ) async throws
2121 func stopReceivingMessage( ) async
2222 func upvote( _ id: String , _ rating: ConversationRating ) async
2323 func downvote( _ id: String , _ rating: ConversationRating ) async
@@ -79,6 +79,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
7979 private var activeRequestId : String ?
8080 private( set) public var conversationId : String ?
8181 private var skillSet : [ ConversationSkill ] = [ ]
82+ private var lastUserRequest : ConversationRequest ?
8283 private var isRestored : Bool = false
8384 private var pendingToolCallRequests : [ String : ToolCallRequest ] = [ : ]
8485 init ( provider: any ConversationServiceProvider ,
@@ -98,6 +99,18 @@ public final class ChatService: ChatServiceType, ObservableObject {
9899 subscribeToClientToolConfirmationEvent ( )
99100 }
100101
102+ deinit {
103+ Task { [ weak self] in
104+ await self ? . stopReceivingMessage ( )
105+ }
106+
107+ // Clear all subscriptions
108+ cancellables. forEach { $0. cancel ( ) }
109+ cancellables. removeAll ( )
110+
111+ // Memory will be deallocated automatically
112+ }
113+
101114 private func subscribeToNotifications( ) {
102115 memory. observeHistoryChange { [ weak self] in
103116 Task { [ weak self] in
@@ -303,7 +316,16 @@ public final class ChatService: ChatServiceType, ObservableObject {
303316 }
304317 }
305318
306- public func send( _ id: String , content: String , skillSet: Array < ConversationSkill > , references: Array < FileReference > , model: String ? = nil , agentMode: Bool = false ) async throws {
319+ public func send(
320+ _ id: String ,
321+ content: String ,
322+ skillSet: Array < ConversationSkill > ,
323+ references: Array < FileReference > ,
324+ model: String ? = nil ,
325+ agentMode: Bool = false ,
326+ userLanguage: String ? = nil ,
327+ turnId: String ? = nil
328+ ) async throws {
307329 guard activeRequestId == nil else { return }
308330 let workDoneToken = UUID ( ) . uuidString
309331 activeRequestId = workDoneToken
@@ -315,11 +337,15 @@ public final class ChatService: ChatServiceType, ObservableObject {
315337 content: content,
316338 references: references. toConversationReferences ( )
317339 )
318- await memory. appendMessage ( chatMessage)
340+
341+ // If turnId is provided, it is used to update the existing message, no need to append the user message
342+ if turnId == nil {
343+ await memory. appendMessage ( chatMessage)
344+ }
319345
320346 // reset file edits
321347 self . resetFileEdits ( )
322-
348+
323349 // persist
324350 saveChatMessageToStorage ( chatMessage)
325351
@@ -363,7 +389,11 @@ public final class ChatService: ChatServiceType, ObservableObject {
363389 ignoredSkills: ignoredSkills,
364390 references: references,
365391 model: model,
366- agentMode: agentMode)
392+ agentMode: agentMode,
393+ userLanguage: userLanguage,
394+ turnId: turnId
395+ )
396+ self . lastUserRequest = request
367397 self . skillSet = skillSet
368398 try await send ( request)
369399 }
@@ -408,12 +438,23 @@ public final class ChatService: ChatServiceType, ObservableObject {
408438 deleteChatMessageFromStorage ( id)
409439 }
410440
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 } )
441+ public func resendMessage ( id : String , model : String ? = nil ) async throws {
442+ if let _ = ( await memory . history ) . first ( where : { $0 . id == id } ) ,
443+ let lastUserRequest
414444 {
445+ // TODO: clean up contents for resend message
446+ activeRequestId = nil
415447 do {
416- try await send ( id, content: message. content, skillSet: [ ] , references: [ ] )
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+ )
417458 } catch {
418459 print ( " Failed to resend message " )
419460 }
@@ -611,17 +652,34 @@ public final class ChatService: ChatServiceType, ObservableObject {
611652 if CLSError . code == 402 {
612653 Task {
613654 await Status . shared
614- . updateCLSStatus ( . error , busy: false , message: CLSError . message)
655+ . updateCLSStatus ( . warning , busy: false , message: CLSError . message)
615656 let errorMessage = ChatMessage (
616657 id: progress. turnId,
617658 chatTabID: self . chatTabInfo. id,
618659 clsTurnID: progress. turnId,
619- role: . system,
620- content: CLSError . message
660+ role: . assistant,
661+ content: " " ,
662+ panelMessages: [ . init( type: . error, title: String ( CLSError . code ?? 0 ) , message: CLSError . message, location: . Panel) ]
621663 )
622664 // will persist in resetongoingRequest()
623- await memory. removeMessage ( progress. turnId)
624665 await memory. appendMessage ( errorMessage)
666+
667+ if let lastUserRequest {
668+ guard let fallbackModel = CopilotModelManager . getFallbackLLM (
669+ scope: lastUserRequest. agentMode ? . agentPanel : . chatPanel
670+ ) else {
671+ resetOngoingRequest ( )
672+ return
673+ }
674+ do {
675+ CopilotModelManager . switchToFallbackModel ( )
676+ try await resendMessage ( id: progress. turnId, model: fallbackModel. id)
677+ } catch {
678+ Logger . gitHubCopilot. error ( error)
679+ resetOngoingRequest ( )
680+ }
681+ return
682+ }
625683 }
626684 } else if CLSError . code == 400 && CLSError . message. contains ( " model is not supported " ) {
627685 Task {
@@ -633,6 +691,8 @@ public final class ChatService: ChatServiceType, ObservableObject {
633691 errorMessage: " Oops, the model is not supported. Please enable it first in [GitHub Copilot settings](https://github.com/settings/copilot). "
634692 )
635693 await memory. appendMessage ( errorMessage)
694+ resetOngoingRequest ( )
695+ return
636696 }
637697 } else {
638698 Task {
@@ -646,10 +706,10 @@ public final class ChatService: ChatServiceType, ObservableObject {
646706 )
647707 // will persist in resetOngoingRequest()
648708 await memory. appendMessage ( errorMessage)
709+ resetOngoingRequest ( )
710+ return
649711 }
650712 }
651- resetOngoingRequest ( )
652- return
653713 }
654714
655715 Task {
@@ -664,9 +724,8 @@ public final class ChatService: ChatServiceType, ObservableObject {
664724 )
665725 // will persist in resetOngoingRequest()
666726 await memory. appendMessage ( message)
727+ resetOngoingRequest ( )
667728 }
668-
669- resetOngoingRequest ( )
670729 }
671730
672731 private func resetOngoingRequest( ) {
@@ -732,7 +791,12 @@ public final class ChatService: ChatServiceType, ObservableObject {
732791
733792 do {
734793 if let conversationId = conversationId {
735- try await conversationProvider? . createTurn ( with: conversationId, request: request, workspaceURL: getWorkspaceURL ( ) )
794+ try await conversationProvider?
795+ . createTurn (
796+ with: conversationId,
797+ request: request,
798+ workspaceURL: getWorkspaceURL ( )
799+ )
736800 } else {
737801 var requestWithTurns = request
738802
0 commit comments