Skip to content

Commit 81fc588

Browse files
committed
Pre-release 0.37.126
1 parent 64a0691 commit 81fc588

38 files changed

+1209
-271
lines changed

Core/Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ let package = Package(
181181
.product(name: "GitHubCopilotService", package: "Tool"),
182182
.product(name: "Workspace", package: "Tool"),
183183
.product(name: "Terminal", package: "Tool"),
184-
.product(name: "SystemUtils", package: "Tool")
184+
.product(name: "SystemUtils", package: "Tool"),
185+
.product(name: "AppKitExtension", package: "Tool")
185186
]),
186187
.testTarget(
187188
name: "ChatServiceTests",

Core/Sources/ChatService/ChatService.swift

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import SystemUtils
1818

1919
public protocol ChatServiceType {
2020
var memory: ContextAwareAutoManagedChatMemory { get set }
21-
func send(_ id: String, content: String, skillSet: [ConversationSkill], references: [FileReference], model: String?, agentMode: Bool, userLanguage: String?, turnId: String?) async throws
21+
func send(_ id: String, content: String, contentImages: [ChatCompletionContentPartImage], contentImageReferences: [ImageReference], skillSet: [ConversationSkill], references: [FileReference], model: String?, agentMode: Bool, userLanguage: String?, turnId: String?) async throws
2222
func stopReceivingMessage() async
2323
func upvote(_ id: String, _ rating: ConversationRating) async
2424
func downvote(_ id: String, _ rating: ConversationRating) async
@@ -316,10 +316,23 @@ public final class ChatService: ChatServiceType, ObservableObject {
316316
}
317317
}
318318
}
319+
320+
public enum ChatServiceError: Error, LocalizedError {
321+
case conflictingImageFormats(String)
322+
323+
public var errorDescription: String? {
324+
switch self {
325+
case .conflictingImageFormats(let message):
326+
return message
327+
}
328+
}
329+
}
319330

320331
public func send(
321332
_ id: String,
322333
content: String,
334+
contentImages: Array<ChatCompletionContentPartImage> = [],
335+
contentImageReferences: Array<ImageReference> = [],
323336
skillSet: Array<ConversationSkill>,
324337
references: Array<FileReference>,
325338
model: String? = nil,
@@ -331,11 +344,31 @@ public final class ChatService: ChatServiceType, ObservableObject {
331344
let workDoneToken = UUID().uuidString
332345
activeRequestId = workDoneToken
333346

347+
let finalImageReferences: [ImageReference]
348+
let finalContentImages: [ChatCompletionContentPartImage]
349+
350+
if !contentImageReferences.isEmpty {
351+
// User attached images are all parsed as ImageReference
352+
finalImageReferences = contentImageReferences
353+
finalContentImages = contentImageReferences
354+
.map {
355+
ChatCompletionContentPartImage(
356+
url: $0.dataURL(imageType: $0.source == .screenshot ? "png" : "")
357+
)
358+
}
359+
} else {
360+
// In current implementation, only resend message will have contentImageReferences
361+
// No need to convert ChatCompletionContentPartImage to ImageReference for persistence
362+
finalImageReferences = []
363+
finalContentImages = contentImages
364+
}
365+
334366
var chatMessage = ChatMessage(
335367
id: id,
336368
chatTabID: self.chatTabInfo.id,
337369
role: .user,
338370
content: content,
371+
contentImageReferences: finalImageReferences,
339372
references: references.toConversationReferences()
340373
)
341374

@@ -406,6 +439,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
406439
let request = createConversationRequest(
407440
workDoneToken: workDoneToken,
408441
content: content,
442+
contentImages: finalContentImages,
409443
activeDoc: activeDoc,
410444
references: references,
411445
model: model,
@@ -417,12 +451,13 @@ public final class ChatService: ChatServiceType, ObservableObject {
417451

418452
self.lastUserRequest = request
419453
self.skillSet = validSkillSet
420-
try await send(request)
454+
try await sendConversationRequest(request)
421455
}
422456

423457
private func createConversationRequest(
424458
workDoneToken: String,
425459
content: String,
460+
contentImages: [ChatCompletionContentPartImage] = [],
426461
activeDoc: Doc?,
427462
references: [FileReference],
428463
model: String? = nil,
@@ -443,6 +478,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
443478
return ConversationRequest(
444479
workDoneToken: workDoneToken,
445480
content: newContent,
481+
contentImages: contentImages,
446482
workspaceFolder: "",
447483
activeDoc: activeDoc,
448484
skills: skillCapabilities,
@@ -504,6 +540,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
504540
try await send(
505541
id,
506542
content: lastUserRequest.content,
543+
contentImages: lastUserRequest.contentImages,
507544
skillSet: skillSet,
508545
references: lastUserRequest.references ?? [],
509546
model: model != nil ? model : lastUserRequest.model,
@@ -720,12 +757,14 @@ public final class ChatService: ChatServiceType, ObservableObject {
720757
await Status.shared
721758
.updateCLSStatus(.warning, busy: false, message: CLSError.message)
722759
let errorMessage = buildErrorMessage(
723-
turnId: progress.turnId,
760+
turnId: progress.turnId,
724761
panelMessages: [.init(type: .error, title: String(CLSError.code ?? 0), message: CLSError.message, location: .Panel)])
725762
// will persist in resetongoingRequest()
726763
await memory.appendMessage(errorMessage)
727764

728-
if let lastUserRequest {
765+
if let lastUserRequest,
766+
let currentUserPlan = await Status.shared.currentUserPlan(),
767+
currentUserPlan != "free" {
729768
guard let fallbackModel = CopilotModelManager.getFallbackLLM(
730769
scope: lastUserRequest.agentMode ? .agentPanel : .chatPanel
731770
) else {
@@ -852,7 +891,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
852891
}
853892
}
854893

855-
private func send(_ request: ConversationRequest) async throws {
894+
private func sendConversationRequest(_ request: ConversationRequest) async throws {
856895
guard !isReceivingMessage else { throw CancellationError() }
857896
isReceivingMessage = true
858897

@@ -892,7 +931,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
892931

893932
switch fileEdit.toolName {
894933
case .insertEditIntoFile:
895-
try InsertEditIntoFileTool.applyEdit(for: fileURL, content: fileEdit.originalContent, contextProvider: self)
934+
InsertEditIntoFileTool.applyEdit(for: fileURL, content: fileEdit.originalContent, contextProvider: self)
896935
case .createFile:
897936
try CreateFileTool.undo(for: fileURL)
898937
default:

Core/Sources/ChatService/ToolCalls/CreateFileTool.swift

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,47 +17,43 @@ public class CreateFileTool: ICopilotTool {
1717
let filePath = input["filePath"]?.value as? String,
1818
let content = input["content"]?.value as? String
1919
else {
20-
completeResponse(request, response: "Invalid parameters", completion: completion)
20+
completeResponse(request, status: .error, response: "Invalid parameters", completion: completion)
2121
return true
2222
}
2323

2424
let fileURL = URL(fileURLWithPath: filePath)
2525

2626
guard !FileManager.default.fileExists(atPath: filePath)
2727
else {
28-
completeResponse(request, response: "File already exists at \(filePath)", completion: completion)
28+
completeResponse(request, status: .error, response: "File already exists at \(filePath)", completion: completion)
2929
return true
3030
}
3131

3232
do {
3333
try content.write(to: fileURL, atomically: true, encoding: .utf8)
3434
} catch {
35-
completeResponse(request, response: "Failed to write content to file: \(error)", completion: completion)
35+
completeResponse(request, status: .error, response: "Failed to write content to file: \(error)", completion: completion)
3636
return true
3737
}
3838

3939
guard FileManager.default.fileExists(atPath: filePath),
40-
let writtenContent = try? String(contentsOf: fileURL, encoding: .utf8),
41-
!writtenContent.isEmpty
40+
let writtenContent = try? String(contentsOf: fileURL, encoding: .utf8)
4241
else {
43-
completeResponse(request, response: "Failed to verify file creation.", completion: completion)
42+
completeResponse(request, status: .error, response: "Failed to verify file creation.", completion: completion)
4443
return true
4544
}
4645

4746
contextProvider?.updateFileEdits(by: .init(
4847
fileURL: URL(fileURLWithPath: filePath),
4948
originalContent: "",
50-
modifiedContent: content,
49+
modifiedContent: writtenContent,
5150
toolName: CreateFileTool.name
5251
))
5352

54-
do {
55-
if let workspacePath = contextProvider?.chatTabInfo.workspacePath,
56-
let xcodeIntance = Utils.getXcode(by: workspacePath) {
57-
try Utils.openFileInXcode(fileURL: URL(fileURLWithPath: filePath), xcodeInstance: xcodeIntance)
53+
Utils.openFileInXcode(fileURL: URL(fileURLWithPath: filePath)) { _, error in
54+
if let error = error {
55+
Logger.client.info("Failed to open file at \(filePath), \(error)")
5856
}
59-
} catch {
60-
Logger.client.info("Failed to open file in Xcode, \(error)")
6157
}
6258

6359
let editAgentRounds: [AgentRound] = [

Core/Sources/ChatService/ToolCalls/ICopilotTool.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import ConversationServiceProvider
22
import JSONRPC
33
import ChatTab
44

5+
enum ToolInvocationStatus: String {
6+
case success, error, cancelled
7+
}
8+
59
public protocol ToolContextProvider {
610
// MARK: insert_edit_into_file
711
var chatTabInfo: ChatTabInfo { get }
@@ -34,16 +38,21 @@ extension ICopilotTool {
3438
* Completes a tool response.
3539
* - Parameters:
3640
* - request: The original tool invocation request.
41+
* - status: The completion status of the tool execution (success, error, or cancelled).
3742
* - response: The string value to include in the response content.
3843
* - completion: The completion handler to call with the response.
3944
*/
4045
func completeResponse(
4146
_ request: InvokeClientToolRequest,
47+
status: ToolInvocationStatus = .success,
4248
response: String = "",
4349
completion: @escaping (AnyJSONRPCResponse) -> Void
4450
) {
4551
let result: JSONValue = .array([
46-
.hash(["content": .array([.hash(["value": .string(response)])])]),
52+
.hash([
53+
"status": .string(status.rawValue),
54+
"content": .array([.hash(["value": .string(response)])])
55+
]),
4756
.null
4857
])
4958
completion(AnyJSONRPCResponse(id: request.id, result: result))

0 commit comments

Comments
 (0)