Skip to content

Commit 46c3099

Browse files
committed
Pre-release 0.43.141
1 parent 75aa71a commit 46c3099

33 files changed

+845
-153
lines changed

Core/Sources/ChatService/ChatService.swift

Lines changed: 39 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -48,37 +48,6 @@ struct ToolCallRequest {
4848
let completion: (AnyJSONRPCResponse) -> Void
4949
}
5050

51-
public struct FileEdit: Equatable {
52-
53-
public enum Status: String {
54-
case none = "none"
55-
case kept = "kept"
56-
case undone = "undone"
57-
}
58-
59-
public let fileURL: URL
60-
public let originalContent: String
61-
public var modifiedContent: String
62-
public var status: Status
63-
64-
/// Different toolName, the different undo logic. Like `insert_edit_into_file` and `create_file`
65-
public var toolName: ToolName
66-
67-
public init(
68-
fileURL: URL,
69-
originalContent: String,
70-
modifiedContent: String,
71-
status: Status = .none,
72-
toolName: ToolName
73-
) {
74-
self.fileURL = fileURL
75-
self.originalContent = originalContent
76-
self.modifiedContent = modifiedContent
77-
self.status = status
78-
self.toolName = toolName
79-
}
80-
}
81-
8251
public final class ChatService: ChatServiceType, ObservableObject {
8352

8453
public enum RequestType: String, Equatable {
@@ -207,35 +176,23 @@ public final class ChatService: ChatServiceType, ObservableObject {
207176
return
208177
}
209178

210-
copilotTool.invokeTool(request, completion: completion, chatHistoryUpdater: self?.appendToolCallHistory, contextProvider: self)
179+
copilotTool.invokeTool(request, completion: completion, contextProvider: self)
211180
}).store(in: &cancellables)
212181
}
213182

214-
private func appendToolCallHistory(turnId: String, editAgentRounds: [AgentRound]) {
183+
func appendToolCallHistory(turnId: String, editAgentRounds: [AgentRound], fileEdits: [FileEdit] = []) {
215184
let chatTabId = self.chatTabInfo.id
216185
Task {
217186
let message = ChatMessage(
218187
assistantMessageWithId: turnId,
219188
chatTabID: chatTabId,
220-
editAgentRounds: editAgentRounds
189+
editAgentRounds: editAgentRounds,
190+
fileEdits: fileEdits
221191
)
222192

223193
await self.memory.appendMessage(message)
224194
}
225195
}
226-
227-
public func updateFileEdits(by fileEdit: FileEdit) {
228-
if let existingFileEdit = self.fileEditMap[fileEdit.fileURL] {
229-
self.fileEditMap[fileEdit.fileURL] = .init(
230-
fileURL: fileEdit.fileURL,
231-
originalContent: existingFileEdit.originalContent,
232-
modifiedContent: fileEdit.modifiedContent,
233-
toolName: existingFileEdit.toolName
234-
)
235-
} else {
236-
self.fileEditMap[fileEdit.fileURL] = fileEdit
237-
}
238-
}
239196

240197
public func notifyChangeTextDocument(fileURL: URL, content: String, version: Int) async throws {
241198
try await conversationProvider?.notifyChangeTextDocument(fileURL: fileURL, content: content, version: version, workspaceURL: getWorkspaceURL())
@@ -542,10 +499,17 @@ public final class ChatService: ChatServiceType, ObservableObject {
542499
deleteAllChatMessagesFromStorage(messageIds)
543500
resetOngoingRequest()
544501
}
545-
546-
public func deleteMessage(id: String) async {
547-
await memory.removeMessage(id)
548-
deleteChatMessageFromStorage(id)
502+
503+
public func deleteMessages(ids: [String]) async {
504+
let turnIdsFromMessages = await memory.history
505+
.filter { ids.contains($0.id) }
506+
.compactMap { $0.clsTurnID }
507+
.map { String($0) }
508+
let turnIds = Array(Set(turnIdsFromMessages))
509+
510+
await memory.removeMessages(ids)
511+
await deleteTurns(turnIds)
512+
deleteAllChatMessagesFromStorage(ids)
549513
}
550514

551515
public func resendMessage(id: String, model: String? = nil, modelProviderName: String? = nil) async throws {
@@ -772,12 +736,22 @@ public final class ChatService: ChatServiceType, ObservableObject {
772736
// CLS Error Code 402: reached monthly chat messages limit
773737
if CLSError.code == 402 {
774738
Task {
739+
let selectedModel = lastUserRequest?.model
740+
let selectedModelProviderName = lastUserRequest?.modelProviderName
741+
742+
var errorMessageText: String
743+
if let selectedModel = selectedModel, let selectedModelProviderName = selectedModelProviderName {
744+
errorMessageText = "You've reached your quota limit for your BYOK model \(selectedModel). Please check with \(selectedModelProviderName) for more information."
745+
} else {
746+
errorMessageText = CLSError.message
747+
}
748+
775749
await Status.shared
776-
.updateCLSStatus(.warning, busy: false, message: CLSError.message)
750+
.updateCLSStatus(.warning, busy: false, message: errorMessageText)
777751
let errorMessage = ChatMessage(
778752
errorMessageWithId: progress.turnId,
779753
chatTabID: chatTabInfo.id,
780-
panelMessages: [.init(type: .error, title: String(CLSError.code ?? 0), message: CLSError.message, location: .Panel)]
754+
panelMessages: [.init(type: .error, title: String(CLSError.code ?? 0), message: errorMessageText, location: .Panel)]
781755
)
782756
// will persist in resetongoingRequest()
783757
await memory.appendMessage(errorMessage)
@@ -944,37 +918,21 @@ public final class ChatService: ChatServiceType, ObservableObject {
944918
}
945919
}
946920

947-
// MARK: - File Edit
948-
public func undoFileEdit(for fileURL: URL) throws {
949-
guard let fileEdit = self.fileEditMap[fileURL],
950-
fileEdit.status == .none
951-
else { return }
952-
953-
switch fileEdit.toolName {
954-
case .insertEditIntoFile:
955-
InsertEditIntoFileTool.applyEdit(for: fileURL, content: fileEdit.originalContent, contextProvider: self)
956-
case .createFile:
957-
try CreateFileTool.undo(for: fileURL)
958-
default:
921+
private func deleteTurns(_ turnIds: [String]) async {
922+
guard !turnIds.isEmpty, let conversationId = conversationId else {
959923
return
960924
}
961925

962-
self.fileEditMap[fileURL]!.status = .undone
963-
}
964-
965-
public func keepFileEdit(for fileURL: URL) {
966-
guard let fileEdit = self.fileEditMap[fileURL], fileEdit.status == .none
967-
else { return }
968-
self.fileEditMap[fileURL]!.status = .kept
969-
}
970-
971-
public func resetFileEdits() {
972-
self.fileEditMap = [:]
973-
}
974-
975-
public func discardFileEdit(for fileURL: URL) throws {
976-
try self.undoFileEdit(for: fileURL)
977-
self.fileEditMap.removeValue(forKey: fileURL)
926+
let workspaceURL = getWorkspaceURL()
927+
928+
for turnId in turnIds {
929+
do {
930+
try await conversationProvider?
931+
.deleteTurn(with: conversationId, turnId: turnId, workspaceURL: workspaceURL)
932+
} catch {
933+
Logger.client.error("Failed to delete turn: \(error)")
934+
}
935+
}
978936
}
979937
}
980938

Core/Sources/ChatService/CodeReview/CodeReviewProvider.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,9 @@ public struct CodeReviewProvider {
4949
) async throws -> CodeReviewResult? {
5050
return try await context.conversationServiceProvider?
5151
.reviewChanges(
52-
.init(
53-
changes: changes.map {
54-
.init(uri: $0.uri, path: $0.path, baseContent: $0.baseContent, headContent: $0.headContent)
55-
}
56-
)
52+
changes.map {
53+
.init(uri: $0.uri, path: $0.path, baseContent: $0.baseContent, headContent: $0.headContent)
54+
}
5755
)
5856
}
5957
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Foundation
2+
import ConversationServiceProvider
3+
import ChatAPIService
4+
5+
extension ChatService {
6+
// MARK: - File Edit
7+
8+
public func updateFileEdits(by fileEdit: FileEdit) {
9+
if let existingFileEdit = self.fileEditMap[fileEdit.fileURL] {
10+
self.fileEditMap[fileEdit.fileURL] = .init(
11+
fileURL: fileEdit.fileURL,
12+
originalContent: existingFileEdit.originalContent,
13+
modifiedContent: fileEdit.modifiedContent,
14+
toolName: existingFileEdit.toolName
15+
)
16+
} else {
17+
self.fileEditMap[fileEdit.fileURL] = fileEdit
18+
}
19+
}
20+
21+
public func undoFileEdit(for fileURL: URL) throws {
22+
guard var fileEdit = self.fileEditMap[fileURL],
23+
fileEdit.status == .none
24+
else { return }
25+
26+
switch fileEdit.toolName {
27+
case .insertEditIntoFile:
28+
InsertEditIntoFileTool.applyEdit(for: fileURL, content: fileEdit.originalContent, contextProvider: self)
29+
case .createFile:
30+
try CreateFileTool.undo(for: fileURL)
31+
default:
32+
return
33+
}
34+
35+
fileEdit.status = .undone
36+
self.fileEditMap[fileURL] = fileEdit
37+
}
38+
39+
public func keepFileEdit(for fileURL: URL) {
40+
guard var fileEdit = self.fileEditMap[fileURL], fileEdit.status == .none
41+
else { return }
42+
43+
fileEdit.status = .kept
44+
self.fileEditMap[fileURL] = fileEdit
45+
}
46+
47+
public func resetFileEdits() {
48+
self.fileEditMap = [:]
49+
}
50+
51+
public func discardFileEdit(for fileURL: URL) throws {
52+
try self.undoFileEdit(for: fileURL)
53+
self.fileEditMap.removeValue(forKey: fileURL)
54+
}
55+
}

Core/Sources/ChatService/ToolCalls/CreateFileTool.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import AppKit
33
import ConversationServiceProvider
44
import Foundation
55
import Logger
6+
import ChatAPIService
67

78
public class CreateFileTool: ICopilotTool {
89
public static let name = ToolName.createFile
910

1011
public func invokeTool(
1112
_ request: InvokeClientToolRequest,
1213
completion: @escaping (AnyJSONRPCResponse) -> Void,
13-
chatHistoryUpdater: ChatHistoryUpdater?,
1414
contextProvider: (any ToolContextProvider)?
1515
) -> Bool {
1616
guard let params = request.params,
@@ -50,12 +50,14 @@ public class CreateFileTool: ICopilotTool {
5050
return true
5151
}
5252

53-
contextProvider?.updateFileEdits(by: .init(
53+
let fileEdit: FileEdit = .init(
5454
fileURL: URL(fileURLWithPath: filePath),
5555
originalContent: "",
5656
modifiedContent: writtenContent,
5757
toolName: CreateFileTool.name
58-
))
58+
)
59+
60+
contextProvider?.updateFileEdits(by: fileEdit)
5961

6062
NSWorkspace.openFileInXcode(fileURL: URL(fileURLWithPath: filePath)) { _, error in
6163
if let error = error {
@@ -78,9 +80,7 @@ public class CreateFileTool: ICopilotTool {
7880
)
7981
]
8082

81-
if let chatHistoryUpdater {
82-
chatHistoryUpdater(params.turnId, editAgentRounds)
83-
}
83+
contextProvider?.updateChatHistory(params.turnId, editAgentRounds: editAgentRounds, fileEdits: [fileEdit])
8484

8585
completeResponse(
8686
request,

Core/Sources/ChatService/ToolCalls/FetchWebPageTool.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ public class FetchWebPageTool: ICopilotTool {
1414
public func invokeTool(
1515
_ request: InvokeClientToolRequest,
1616
completion: @escaping (AnyJSONRPCResponse) -> Void,
17-
chatHistoryUpdater: ChatHistoryUpdater?,
1817
contextProvider: (any ToolContextProvider)?
1918
) -> Bool {
2019
guard let params = request.params,

Core/Sources/ChatService/ToolCalls/GetErrorsTool.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ public class GetErrorsTool: ICopilotTool {
88
public func invokeTool(
99
_ request: InvokeClientToolRequest,
1010
completion: @escaping (AnyJSONRPCResponse) -> Void,
11-
chatHistoryUpdater: ChatHistoryUpdater?,
1211
contextProvider: ToolContextProvider?
1312
) -> Bool {
1413
guard let params = request.params,

Core/Sources/ChatService/ToolCalls/GetTerminalOutputTool.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import JSONRPC
44
import Terminal
55

66
public class GetTerminalOutputTool: ICopilotTool {
7-
public func invokeTool(_ request: InvokeClientToolRequest, completion: @escaping (AnyJSONRPCResponse) -> Void, chatHistoryUpdater: ChatHistoryUpdater?, contextProvider: (any ToolContextProvider)?) -> Bool {
7+
public func invokeTool(_ request: InvokeClientToolRequest, completion: @escaping (AnyJSONRPCResponse) -> Void, contextProvider: (any ToolContextProvider)?) -> Bool {
88
var result: String = ""
99
if let input = request.params?.input as? [String: AnyCodable], let terminalId = input["id"]?.value as? String{
1010
let session = TerminalSessionManager.shared.getSession(for: terminalId)

Core/Sources/ChatService/ToolCalls/ICopilotTool.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,30 @@ import ChatTab
22
import ConversationServiceProvider
33
import Foundation
44
import JSONRPC
5+
import ChatAPIService
56

67
public protocol ToolContextProvider {
78
// MARK: insert_edit_into_file
89
var chatTabInfo: ChatTabInfo { get }
910
func updateFileEdits(by fileEdit: FileEdit) -> Void
1011
func notifyChangeTextDocument(fileURL: URL, content: String, version: Int) async throws
12+
func updateChatHistory(_ turnId: String, editAgentRounds: [AgentRound], fileEdits: [FileEdit])
1113
}
1214

13-
public typealias ChatHistoryUpdater = (String, [AgentRound]) -> Void
1415

1516
public protocol ICopilotTool {
1617
/**
1718
* Invokes the Copilot tool with the given request.
1819
* - Parameters:
1920
* - request: The tool invocation request.
2021
* - completion: Closure called with JSON-RPC response when tool execution completes.
21-
* - chatHistoryUpdater: Optional closure to update chat history during tool execution.
2222
* - contextProvider: Optional provider that supplies additional context information
2323
* needed for tool execution, such as chat tab data and file editing capabilities.
2424
* - Returns: Boolean indicating if the tool call has completed. True if the tool call is completed, false otherwise.
2525
*/
2626
func invokeTool(
2727
_ request: InvokeClientToolRequest,
2828
completion: @escaping (AnyJSONRPCResponse) -> Void,
29-
chatHistoryUpdater: ChatHistoryUpdater?,
3029
contextProvider: ToolContextProvider?
3130
) -> Bool
3231
}
@@ -85,4 +84,8 @@ extension ICopilotTool {
8584
}
8685
}
8786

88-
extension ChatService: ToolContextProvider { }
87+
extension ChatService: ToolContextProvider {
88+
public func updateChatHistory(_ turnId: String, editAgentRounds: [AgentRound], fileEdits: [FileEdit] = []) {
89+
appendToolCallHistory(turnId: turnId, editAgentRounds: editAgentRounds, fileEdits: fileEdits)
90+
}
91+
}

Core/Sources/ChatService/ToolCalls/InsertEditIntoFileTool.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import Foundation
66
import JSONRPC
77
import Logger
88
import XcodeInspector
9+
import ChatAPIService
910

1011
public class InsertEditIntoFileTool: ICopilotTool {
1112
public static let name = ToolName.insertEditIntoFile
1213

1314
public func invokeTool(
1415
_ request: InvokeClientToolRequest,
1516
completion: @escaping (AnyJSONRPCResponse) -> Void,
16-
chatHistoryUpdater: ChatHistoryUpdater?,
1717
contextProvider: (any ToolContextProvider)?
1818
) -> Bool {
1919
guard let params = request.params,
@@ -47,9 +47,8 @@ public class InsertEditIntoFileTool: ICopilotTool {
4747
return
4848
}
4949

50-
contextProvider.updateFileEdits(
51-
by: .init(fileURL: fileURL, originalContent: originalContent, modifiedContent: code, toolName: InsertEditIntoFileTool.name)
52-
)
50+
let fileEdit: FileEdit = .init(fileURL: fileURL, originalContent: originalContent, modifiedContent: code, toolName: InsertEditIntoFileTool.name)
51+
contextProvider.updateFileEdits(by: fileEdit)
5352

5453
let editAgentRounds: [AgentRound] = [
5554
.init(
@@ -66,9 +65,8 @@ public class InsertEditIntoFileTool: ICopilotTool {
6665
)
6766
]
6867

69-
if let chatHistoryUpdater {
70-
chatHistoryUpdater(params.turnId, editAgentRounds)
71-
}
68+
contextProvider
69+
.updateChatHistory(params.turnId, editAgentRounds: editAgentRounds, fileEdits: [fileEdit])
7270

7371
self.completeResponse(request, response: newContent, completion: completion)
7472
}

0 commit comments

Comments
 (0)