Skip to content

Commit a45336d

Browse files
committed
Pre-release 0.46.157
1 parent 53360f8 commit a45336d

File tree

12 files changed

+185
-80
lines changed

12 files changed

+185
-80
lines changed

Core/Sources/ChatService/Extensions/ChatService+FileEdit.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ extension ChatService {
2525

2626
switch fileEdit.toolName {
2727
case .insertEditIntoFile:
28-
InsertEditIntoFileTool.applyEdit(for: fileURL, content: fileEdit.originalContent, contextProvider: self)
28+
InsertEditIntoFileTool.applyEdit(for: fileURL, content: fileEdit.originalContent)
2929
case .createFile:
3030
try CreateFileTool.undo(for: fileURL)
3131
default:

Core/Sources/ChatService/ToolCalls/InsertEditIntoFileTool.swift

Lines changed: 91 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ import JSONRPC
77
import Logger
88
import XcodeInspector
99
import ChatAPIService
10+
import SystemUtils
11+
import Workspace
1012

1113
public enum InsertEditError: LocalizedError {
1214
case missingEditorElement(file: URL)
1315
case openingApplicationUnavailable
1416
case fileNotOpenedInXcode
1517
case fileURLMismatch(expected: URL, actual: URL?)
18+
case fileNotAccessible(URL)
19+
case fileHasUnsavedChanges(URL)
1620

1721
public var errorDescription: String? {
1822
switch self {
@@ -24,6 +28,10 @@ public enum InsertEditError: LocalizedError {
2428
return "The file is not currently opened in Xcode."
2529
case .fileURLMismatch(let expected, let actual):
2630
return "The currently focused file URL \(actual?.lastPathComponent ?? "unknown") does not match the expected file URL \(expected.lastPathComponent)."
31+
case .fileNotAccessible(let fileURL):
32+
return "The file \(fileURL.lastPathComponent) is not accessible."
33+
case .fileHasUnsavedChanges(let fileURL):
34+
return "The file \(fileURL.lastPathComponent) seems to have unsaved changes in Xcode. Please save the file and try again."
2735
}
2836
}
2937
}
@@ -50,7 +58,7 @@ public class InsertEditIntoFileTool: ICopilotTool {
5058
let fileURL = URL(fileURLWithPath: filePath)
5159
let originalContent = try String(contentsOf: fileURL, encoding: .utf8)
5260

53-
InsertEditIntoFileTool.applyEdit(for: fileURL, content: code, contextProvider: contextProvider) { newContent, error in
61+
InsertEditIntoFileTool.applyEdit(for: fileURL, content: code) { newContent, error in
5462
if let error = error {
5563
self.completeResponse(
5664
request,
@@ -106,7 +114,6 @@ public class InsertEditIntoFileTool: ICopilotTool {
106114
public static func applyEdit(
107115
for fileURL: URL,
108116
content: String,
109-
contextProvider: any ToolContextProvider,
110117
xcodeInstance: AppInstanceInspector
111118
) throws -> String {
112119
guard let editorElement = Self.getEditorElement(by: xcodeInstance, for: fileURL)
@@ -155,39 +162,21 @@ public class InsertEditIntoFileTool: ICopilotTool {
155162
public static func applyEdit(
156163
for fileURL: URL,
157164
content: String,
158-
contextProvider: any ToolContextProvider,
159165
completion: ((String?, Error?) -> Void)? = nil
160166
) {
161-
NSWorkspace.openFileInXcode(fileURL: fileURL) { app, error in
162-
do {
163-
if let error = error { throw error }
164-
165-
guard let app = app
166-
else {
167-
throw InsertEditError.openingApplicationUnavailable
168-
}
169-
170-
let appInstanceInspector = AppInstanceInspector(runningApplication: app)
171-
guard appInstanceInspector.isXcode
172-
else {
173-
throw InsertEditError.fileNotOpenedInXcode
174-
}
175-
176-
let newContent = try applyEdit(
177-
for: fileURL,
178-
content: content,
179-
contextProvider: contextProvider,
180-
xcodeInstance: appInstanceInspector
181-
)
182-
183-
Task {
184-
await WorkspaceInvocationCoordinator().invokeFilespaceUpdate(fileURL: fileURL, content: newContent)
185-
if let completion = completion { completion(newContent, nil) }
186-
}
187-
} catch {
188-
if let completion = completion { completion(nil, error) }
189-
Logger.client.info("Failed to apply edit for file at \(fileURL), \(error)")
190-
}
167+
if SystemUtils.isDeveloperMode || SystemUtils.isPrereleaseBuild {
168+
/// Experimental solution: Use file system write for better reliability. Only enable in dev mode or prerelease builds.
169+
Self.applyEditWithFileSystem(
170+
for: fileURL,
171+
content: content,
172+
completion: completion
173+
)
174+
} else {
175+
Self.applyEditWithAccessibilityAPI(
176+
for: fileURL,
177+
content: content,
178+
completion: completion
179+
)
191180
}
192181
}
193182

@@ -248,3 +237,72 @@ private extension AppInstanceInspector {
248237
appElement.realtimeDocumentURL
249238
}
250239
}
240+
241+
extension InsertEditIntoFileTool {
242+
static func applyEditWithFileSystem(
243+
for fileURL: URL,
244+
content: String,
245+
completion: ((String?, Error?) -> Void)? = nil
246+
) {
247+
do {
248+
guard let diskFileContent = try? String(contentsOf: fileURL) else {
249+
throw InsertEditError.fileNotAccessible(fileURL)
250+
}
251+
252+
if let focusedElement = XcodeInspector.shared.focusedElement,
253+
focusedElement.isSourceEditor,
254+
focusedElement.value != diskFileContent
255+
{
256+
throw InsertEditError.fileHasUnsavedChanges(fileURL)
257+
}
258+
259+
// write content to disk
260+
try content.write(to: fileURL, atomically: true, encoding: .utf8)
261+
262+
Task { @WorkspaceActor in
263+
await WorkspaceInvocationCoordinator().invokeFilespaceUpdate(fileURL: fileURL, content: content)
264+
if let completion = completion { completion(content, nil) }
265+
}
266+
} catch {
267+
if let completion = completion { completion(nil, error) }
268+
Logger.client.info("Failed to apply edit for file at \(fileURL), \(error)")
269+
}
270+
}
271+
272+
static func applyEditWithAccessibilityAPI(
273+
for fileURL: URL,
274+
content: String,
275+
completion: ((String?, Error?) -> Void)? = nil,
276+
) {
277+
NSWorkspace.openFileInXcode(fileURL: fileURL) { app, error in
278+
do {
279+
if let error = error { throw error }
280+
281+
guard let app = app
282+
else {
283+
throw InsertEditError.openingApplicationUnavailable
284+
}
285+
286+
let appInstanceInspector = AppInstanceInspector(runningApplication: app)
287+
guard appInstanceInspector.isXcode
288+
else {
289+
throw InsertEditError.fileNotOpenedInXcode
290+
}
291+
292+
let newContent = try applyEdit(
293+
for: fileURL,
294+
content: content,
295+
xcodeInstance: appInstanceInspector
296+
)
297+
298+
Task {
299+
await WorkspaceInvocationCoordinator().invokeFilespaceUpdate(fileURL: fileURL, content: newContent)
300+
if let completion = completion { completion(newContent, nil) }
301+
}
302+
} catch {
303+
if let completion = completion { completion(nil, error) }
304+
Logger.client.info("Failed to apply edit for file at \(fileURL), \(error)")
305+
}
306+
}
307+
}
308+
}

Core/Sources/ChatService/ToolCalls/ToolCallStatusUpdater.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ struct ToolCallStatusUpdater {
3939
AgentToolCall(
4040
id: toolCallId,
4141
name: toolCall.name,
42+
toolType: toolCall.toolType,
4243
status: newStatus
4344
),
4445
]
@@ -65,6 +66,7 @@ struct ToolCallStatusUpdater {
6566
AgentToolCall(
6667
id: toolCallId,
6768
name: toolCall.name,
69+
toolType: toolCall.toolType,
6870
status: newStatus
6971
),
7072
]

Core/Sources/ConversationTab/Views/BotMessage.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ struct BotMessage: View {
127127
HStack {
128128
if shouldShowTurnStatus() {
129129
TurnStatusView(message: message)
130+
.modify { view in
131+
if message.turnStatus == .inProgress {
132+
view
133+
.scaledPadding(.leading, 6)
134+
} else {
135+
view
136+
}
137+
}
130138
}
131139

132140
Spacer()
@@ -256,8 +264,8 @@ private struct TurnStatusView: View {
256264
HStack(spacing: 4) {
257265
ProgressView()
258266
.controlSize(.small)
259-
.scaledFont(size: chatFontSize - 1)
260-
.conditionalFontWeight(.medium)
267+
.scaledScaleEffect(0.7)
268+
.scaledFrame(width: 16, height: 16)
261269

262270
Text("Generating...")
263271
.scaledFont(size: chatFontSize - 1)

Core/Sources/ConversationTab/Views/ConversationAgentProgressView/ConversationAgentProgressView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,12 @@ struct GenericToolTitleView: View {
156156
HStack(spacing: 4) {
157157
Text(toolStatus)
158158
.textSelection(.enabled)
159-
.scaledFont(size: chatFontSize, weight: fontWeight)
159+
.scaledFont(size: chatFontSize - 1, weight: fontWeight)
160160
.foregroundStyle(.primary)
161161
.background(Color.clear)
162162
Text(toolName)
163163
.textSelection(.enabled)
164-
.scaledFont(size: chatFontSize, weight: fontWeight)
164+
.scaledFont(size: chatFontSize - 1, weight: fontWeight)
165165
.foregroundStyle(.primary)
166166
.scaledPadding(.vertical, 2)
167167
.scaledPadding(.horizontal, 4)

Core/Sources/ConversationTab/Views/ConversationAgentProgressView/ToolStatusItemView.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct ToolStatusItemView: View {
99
let tool: AgentToolCall
1010

1111
@AppStorage(\.chatFontSize) var chatFontSize
12+
@AppStorage(\.fontScale) var fontScale
1213

1314
@State private var isHoveringFileLink = false
1415

@@ -291,6 +292,31 @@ struct ToolStatusItemView: View {
291292
)
292293
}
293294

295+
@ViewBuilder
296+
func toolCallDetailSection(title: String, text: String) -> some View {
297+
VStack(alignment: .leading, spacing: 4) {
298+
Text(title)
299+
.scaledFont(size: chatFontSize - 1, weight: .medium)
300+
.foregroundColor(.secondary)
301+
markdownView(text: text)
302+
.toolCallDetailStyle(fontScale: fontScale)
303+
}
304+
}
305+
306+
var mcpDetailView: some View {
307+
VStack(alignment: .leading, spacing: 8) {
308+
if let inputMessage = tool.inputMessage, !inputMessage.isEmpty {
309+
toolCallDetailSection(title: "Input", text: inputMessage)
310+
}
311+
if let errorMessage = tool.error, !errorMessage.isEmpty {
312+
toolCallDetailSection(title: "Output", text: errorMessage)
313+
}
314+
if let result = tool.result, !result.isEmpty {
315+
toolCallDetailSection(title: "Output", text: toolResultText ?? "")
316+
}
317+
}
318+
}
319+
294320
var progress: some View {
295321
HStack(spacing: 4) {
296322
statusIcon
@@ -440,6 +466,11 @@ struct ToolStatusItemView: View {
440466
title: progress,
441467
content: markdownView(text: extractInsertEditContent(from: resultText))
442468
)
469+
} else if tool.toolType == .mcp {
470+
ToolStatusDetailsView(
471+
title: progress,
472+
content: mcpDetailView
473+
)
443474
} else if tool.status == .error {
444475
ToolStatusDetailsView(
445476
title: progress,
@@ -530,4 +561,20 @@ private extension View {
530561
}
531562
}
532563
}
564+
565+
func toolCallDetailStyle(fontScale: CGFloat) -> some View {
566+
/// Leverage the `modify` extension to avoid refreshing of chat panel `List` view
567+
self.modify { view in
568+
view
569+
.foregroundColor(.secondary)
570+
.scaledPadding(4)
571+
.frame(maxWidth: .infinity, alignment: .leading)
572+
.background(SecondarySystemFillColor)
573+
.clipShape(RoundedRectangle(cornerRadius: 6))
574+
.background(
575+
RoundedRectangle(cornerRadius: 6)
576+
.stroke(Color.agentToolStatusOutlineColor, lineWidth: 1 * fontScale)
577+
)
578+
}
579+
}
533580
}

Core/Sources/Service/RealtimeSuggestionController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ public actor RealtimeSuggestionController {
6969
let handler = { [weak self] in
7070
guard let self else { return }
7171
await cancelInFlightTasks()
72-
await self.triggerPrefetchDebounced()
7372
await self.notifyEditingFileChange(editor: sourceEditor.element)
73+
await self.triggerPrefetchDebounced()
7474
}
7575

7676
for await _ in valueChange {

Tool/Sources/ChatAPIService/Memory/ChatMemory.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,18 @@ public extension ChatMemory {
3535
for newToolCall in messageToolCalls {
3636
if let toolCallIndex = mergedToolCalls.firstIndex(where: { $0.id == newToolCall.id }) {
3737
mergedToolCalls[toolCallIndex].status = newToolCall.status
38+
if let toolType = newToolCall.toolType {
39+
mergedToolCalls[toolCallIndex].toolType = toolType
40+
}
3841
if let progressMessage = newToolCall.progressMessage, !progressMessage.isEmpty {
3942
mergedToolCalls[toolCallIndex].progressMessage = progressMessage
4043
}
44+
if let input = newToolCall.input, !input.isEmpty {
45+
mergedToolCalls[toolCallIndex].input = input
46+
}
47+
if let inputMessage = newToolCall.inputMessage, !inputMessage.isEmpty {
48+
mergedToolCalls[toolCallIndex].inputMessage = inputMessage
49+
}
4150
if let result = newToolCall.result, !result.isEmpty {
4251
mergedToolCalls[toolCallIndex].result = result
4352
}
@@ -163,9 +172,18 @@ extension ChatMessage {
163172
for newToolCall in newRound.toolCalls! {
164173
if let toolCallIndex = mergedToolCalls.firstIndex(where: { $0.id == newToolCall.id }) {
165174
mergedToolCalls[toolCallIndex].status = newToolCall.status
175+
if let toolType = newToolCall.toolType {
176+
mergedToolCalls[toolCallIndex].toolType = toolType
177+
}
166178
if let progressMessage = newToolCall.progressMessage, !progressMessage.isEmpty {
167179
mergedToolCalls[toolCallIndex].progressMessage = newToolCall.progressMessage
168180
}
181+
if let input = newToolCall.input, !input.isEmpty {
182+
mergedToolCalls[toolCallIndex].input = input
183+
}
184+
if let inputMessage = newToolCall.inputMessage, !inputMessage.isEmpty {
185+
mergedToolCalls[toolCallIndex].inputMessage = inputMessage
186+
}
169187
if let result = newToolCall.result, !result.isEmpty {
170188
mergedToolCalls[toolCallIndex].result = result
171189
}

Tool/Sources/ConversationServiceProvider/LSPTypes+AgentRound.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import CopilotForXcodeKit
22
import Foundation
33
import LanguageServerProtocol
44

5-
65
public struct AgentRound: Codable, Equatable {
76
public let roundId: Int
87
public var reply: String
@@ -20,10 +19,11 @@ public struct AgentRound: Codable, Equatable {
2019
public struct AgentToolCall: Codable, Equatable, Identifiable {
2120
public let id: String
2221
public let name: String
22+
public var toolType: ToolType?
2323
public var progressMessage: String?
2424
public var status: ToolCallStatus
2525
public var input: [String: AnyCodable]?
26-
public let inputMessage: String?
26+
public var inputMessage: String?
2727
public var error: String?
2828
public var result: [ToolCallResultData]?
2929
public var resultDetails: [ToolResultItem]?
@@ -37,6 +37,7 @@ public struct AgentToolCall: Codable, Equatable, Identifiable {
3737
public init(
3838
id: String,
3939
name: String,
40+
toolType: ToolType? = nil,
4041
progressMessage: String? = nil,
4142
status: ToolCallStatus,
4243
input: [String: AnyCodable]? = nil,
@@ -49,6 +50,7 @@ public struct AgentToolCall: Codable, Equatable, Identifiable {
4950
) {
5051
self.id = id
5152
self.name = name
53+
self.toolType = toolType
5254
self.progressMessage = progressMessage
5355
self.status = status
5456
self.input = input

0 commit comments

Comments
 (0)