11import BuiltinExtension
2+ import ChatBasic
23import CopilotForXcodeKit
34import Foundation
5+ import LanguageServerProtocol
46import XcodeInspector
57
68public final class GitHubCopilotChatService : BuiltinExtensionChatServiceType {
@@ -22,90 +24,131 @@ public final class GitHubCopilotChatService: BuiltinExtensionChatServiceType {
2224 else { return . finished( throwing: CancellationError ( ) ) }
2325 let id = UUID ( ) . uuidString
2426 let editorContent = await XcodeInspector . shared. getFocusedEditorContent ( )
25- do {
26- let createResponse = try await service. server
27- . sendRequest ( GitHubCopilotRequest . ConversationCreate ( requestBody: . init(
28- workDoneToken: " " ,
29- turns: convertHistory ( history: history) ,
30- capabilities: [
31- . init( allSkills: true , skills: [ ] ) ,
32- ] ,
33- doc: . init(
34- source: editorContent? . editorContent? . content ?? " " ,
35- tabSize: 1 ,
36- indentSize: 4 ,
37- insertSpaces: true ,
38- path: editorContent? . documentURL. path ?? " " ,
39- uri: editorContent? . documentURL. path ?? " " ,
40- relativePath: editorContent? . relativePath ?? " " ,
41- languageId: editorContent? . language. rawValue ?? " plaintext " ,
42- position: . zero
43- ) ,
44- source: . panel,
45- workspaceFolder: workspace. projectURL. path
46- ) ) )
47-
48- let stream = AsyncThrowingStream < String , Error > . init { continuation in
49- service. registerNotificationHandler ( id: id) { notification in
50- if notification. method. rawValue == " " {
51- return true
52- }
27+ let workDoneToken = UUID ( ) . uuidString
28+ let turns = convertHistory ( history: history, message: message)
29+ let request = GitHubCopilotRequest . ConversationCreate ( requestBody: . init(
30+ workDoneToken: workDoneToken,
31+ turns: turns,
32+ capabilities: . init( allSkills: true , skills: [ ] ) ,
33+ doc: . init(
34+ source: editorContent? . editorContent? . content ?? " " ,
35+ tabSize: 1 ,
36+ indentSize: 4 ,
37+ insertSpaces: true ,
38+ path: editorContent? . documentURL. path ?? " " ,
39+ uri: editorContent? . documentURL. path ?? " " ,
40+ relativePath: editorContent? . relativePath ?? " " ,
41+ languageId: editorContent? . language ?? . plaintext,
42+ position: editorContent? . editorContent? . cursorPosition ?? . zero
43+ ) ,
44+ source: . panel,
45+ workspaceFolder: workspace. projectURL. path
46+ ) )
47+
48+ var cont : AsyncThrowingStream < String , Error > . Continuation ? = nil
49+ let stream = AsyncThrowingStream < String , Error > { continuation in
50+ cont = continuation
51+ let startTimestamp = Date ( )
5352
53+ service. registerNotificationHandler ( id: id) { notification, data in
54+ // just incase the conversation is stuck, we will cancel it after timeout
55+ if Date ( ) . timeIntervalSince ( startTimestamp) > 60 * 30 {
56+ continuation. finish ( throwing: CancellationError ( ) )
5457 return false
5558 }
5659
57- continuation. onTermination = { _ in
58- Task {
59- try await service. server. sendRequest (
60- GitHubCopilotRequest . ConversationDestroy ( requestBody: . init(
61- conversationId: createResponse. conversationId
62- ) )
60+ switch notification. method {
61+ case " $/progress " :
62+ do {
63+ let progress = try JSONDecoder ( ) . decode (
64+ JSONRPC< StreamProgressParams> . self ,
65+ from: data
66+ ) . params
67+ guard progress. token == workDoneToken else { return false }
68+ if let reply = progress. value. reply, progress. value. kind == " report " {
69+ continuation. yield ( reply)
70+ } else if progress. value. kind == " end " {
71+ if let error = progress. value. error,
72+ progress. value. cancellationReason == nil
73+ {
74+ continuation. finish ( throwing: ServerError . serverError (
75+ code: 0 ,
76+ message: error,
77+ data: nil
78+ ) )
79+ } else {
80+ continuation. finish ( )
81+ }
82+ }
83+ return true
84+ } catch {
85+ return false
86+ }
87+ case " conversation/context " :
88+ do {
89+ _ = try JSONDecoder ( ) . decode (
90+ JSONRPC< ConversationContextParams> . self ,
91+ from: data
6392 )
93+ throw ServerError . clientDataUnavailable ( CancellationError ( ) )
94+ } catch {
95+ return false
6496 }
97+
98+ default :
99+ return false
65100 }
66101 }
102+ }
67103
68- _ = try await service. server
69- . sendRequest ( GitHubCopilotRequest . ConversationTurn ( requestBody: . init(
70- workDoneToken: " " ,
71- conversationId: createResponse. conversationId,
72- message: message
73- ) ) )
74-
75- return stream
104+ do {
105+ let createResponse = try await service. server. sendRequest ( request)
106+ cont? . onTermination = { _ in
107+ Task {
108+ service. unregisterNotificationHandler ( id: id)
109+ _ = try await service. server. sendRequest (
110+ GitHubCopilotRequest . ConversationDestroy ( requestBody: . init(
111+ conversationId: createResponse. conversationId
112+ ) )
113+ )
114+ }
115+ }
76116 } catch {
77- return . finished ( throwing: error)
117+ cont ? . finish ( throwing: error)
78118 }
119+
120+ return stream
79121 }
80122}
81123
82124extension GitHubCopilotChatService {
83125 typealias Turn = GitHubCopilotRequest . ConversationCreate . RequestBody . Turn
84- func convertHistory( history: [ Message ] ) -> [ Turn ] {
126+ func convertHistory( history: [ Message ] , message : String ) -> [ Turn ] {
85127 guard let firstIndexOfUserMessage = history. firstIndex ( where: { $0. role == . user } )
86- else { return [ ] }
128+ else { return [ . init ( request : message , response : nil ) ] }
87129
88130 var currentTurn = Turn ( request: " " , response: nil )
89131 var turns : [ Turn ] = [ ]
90132 for i in firstIndexOfUserMessage..< history. endIndex {
91133 let message = history [ i]
134+ let text = message. content ?? " "
92135 switch message. role {
93136 case . user:
94137 if currentTurn. response == nil {
95138 if currentTurn. request. isEmpty {
96- currentTurn. request = message . text
139+ currentTurn. request = text
97140 } else {
98- currentTurn. request += " \n \n \( message . text) "
141+ currentTurn. request += " \n \n \( text) "
99142 }
100143 } else { // a valid turn is created
101144 turns. append ( currentTurn)
102- currentTurn = Turn ( request: message . text, response: nil )
145+ currentTurn = Turn ( request: text, response: nil )
103146 }
104147 case . assistant:
105148 if let response = currentTurn. response {
106- currentTurn. response = " \( response) \n \n \( message . text) "
149+ currentTurn. response = " \( response) \n \n \( text) "
107150 } else {
108- currentTurn. response = message . text
151+ currentTurn. response = text
109152 }
110153 default :
111154 break
@@ -117,12 +160,67 @@ extension GitHubCopilotChatService {
117160 }
118161
119162 turns. append ( currentTurn)
163+ turns. append ( . init( request: message, response: nil ) )
120164
121165 return turns
122166 }
123167
124168 func createNewMessage( references: [ RetrievedContent ] , message: String ) -> String {
125169 return message
126170 }
171+
172+ struct JSONRPC < Params: Decodable > : Decodable {
173+ var jsonrpc : String
174+ var method : String
175+ var params : Params
176+ }
177+
178+ struct StreamProgressParams : Decodable {
179+ struct Value : Decodable {
180+ struct Step : Decodable {
181+ var id : String
182+ var title : String
183+ var status : String
184+ }
185+
186+ struct FollowUp : Decodable {
187+ var id : String
188+ var type : String
189+ var message : String
190+ }
191+
192+ var kind : String
193+ var title : String ?
194+ var conversationId : String
195+ var turnId : String
196+ var steps : [ Step ] ?
197+ var followUp : FollowUp ?
198+ var suggestedTitle : String ?
199+ var reply : String ?
200+ var annotations : [ String ] ?
201+ var hideText : Bool ?
202+ var cancellationReason : String ?
203+ var error : String ?
204+ }
205+
206+ var token : String
207+ var value : Value
208+ }
209+
210+ struct ConversationContextParams : Decodable {
211+ enum SkillID : String , Decodable {
212+ case currentEditor = " current-editor "
213+ case projectLabels = " project-labels "
214+ case recentFiles = " recent-files "
215+ case references
216+ case problemsInActiveDocument = " problems-in-active-document "
217+ }
218+
219+ var conversationId : String
220+ var turnId : String
221+ var skillId : String
222+ }
223+
224+ struct ConversationContextResponseBody : Encodable { }
127225}
128226
0 commit comments