Skip to content

Commit f8bea4e

Browse files
committed
Pre-release 0.45.155
1 parent 82b562b commit f8bea4e

File tree

34 files changed

+1336
-404
lines changed

34 files changed

+1336
-404
lines changed

Core/Sources/ChatService/ChatService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ public final class ChatService: ChatServiceType, ObservableObject {
162162
AgentRound(roundId: params.roundId,
163163
reply: "",
164164
toolCalls: [
165-
AgentToolCall(id: params.toolCallId, name: params.name, status: .waitForConfirmation, invokeParams: params)
165+
AgentToolCall(id: params.toolCallId, name: params.name, status: .waitForConfirmation, invokeParams: params, title: params.title)
166166
]
167167
)
168168
]

Core/Sources/ChatService/ToolCalls/InsertEditIntoFileTool.swift

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@ import Logger
88
import XcodeInspector
99
import ChatAPIService
1010

11+
public enum InsertEditError: LocalizedError {
12+
case missingEditorElement(file: URL)
13+
case openingApplicationUnavailable
14+
case fileNotOpenedInXcode
15+
case fileURLMismatch(expected: URL, actual: URL?)
16+
17+
public var errorDescription: String? {
18+
switch self {
19+
case .missingEditorElement(let file):
20+
return "Could not find source editor element for file \(file.lastPathComponent)."
21+
case .openingApplicationUnavailable:
22+
return "Failed to get the application that opened the file."
23+
case .fileNotOpenedInXcode:
24+
return "The file is not currently opened in Xcode."
25+
case .fileURLMismatch(let expected, let actual):
26+
return "The currently focused file URL \(actual?.lastPathComponent ?? "unknown") does not match the expected file URL \(expected.lastPathComponent)."
27+
}
28+
}
29+
}
30+
1131
public class InsertEditIntoFileTool: ICopilotTool {
1232
public static let name = ToolName.insertEditIntoFile
1333

@@ -89,16 +109,9 @@ public class InsertEditIntoFileTool: ICopilotTool {
89109
contextProvider: any ToolContextProvider,
90110
xcodeInstance: AppInstanceInspector
91111
) throws -> String {
92-
Thread.sleep(forTimeInterval: 0.5)
93-
// Get the focused element directly from the app (like XcodeInspector does)
94-
guard let focusedElement: AXUIElement = xcodeInstance.appElement.focusedElement
112+
guard let editorElement = Self.getEditorElement(by: xcodeInstance, for: fileURL)
95113
else {
96-
throw NSError(domain: "Failed to access xcode element", code: 0)
97-
}
98-
99-
// Find the source editor element using XcodeInspector's logic
100-
guard let editorElement = focusedElement.findSourceEditorElement() else {
101-
throw NSError(domain: "Could not find source editor element", code: 0)
114+
throw InsertEditError.missingEditorElement(file: fileURL)
102115
}
103116

104117
// Check if element supports kAXValueAttribute before reading
@@ -115,6 +128,8 @@ public class InsertEditIntoFileTool: ICopilotTool {
115128
let lines = value.components(separatedBy: .newlines)
116129

117130
do {
131+
try Self.checkOpenedFileURL(for: fileURL, xcodeInstance: xcodeInstance)
132+
118133
try AXHelper().injectUpdatedCodeWithAccessibilityAPI(
119134
.init(
120135
content: content,
@@ -133,25 +148,8 @@ public class InsertEditIntoFileTool: ICopilotTool {
133148
throw error
134149
}
135150

136-
guard let refreshedFocusedElement: AXUIElement = xcodeInstance.appElement.focusedElement,
137-
let refreshedEditorElement = refreshedFocusedElement.findSourceEditorElement()
138-
else {
139-
throw NSError(domain: "Failed to access xcode element", code: 0)
140-
}
141-
142151
// Verify the content was applied by reading it back
143-
do {
144-
let newContent: String = try refreshedEditorElement.copyValue(key: kAXValueAttribute)
145-
Logger.client.info("Successfully read back new content, length: \(newContent.count)")
146-
147-
return newContent
148-
} catch {
149-
Logger.client.error("Failed to read back new content: \(error)")
150-
if let axError = error as? AXError {
151-
Logger.client.error("AX Error code when reading back: \(axError.rawValue)")
152-
}
153-
throw error
154-
}
152+
return try Self.getCurrentEditorContent(for: fileURL, by: xcodeInstance)
155153
}
156154

157155
public static func applyEdit(
@@ -166,15 +164,15 @@ public class InsertEditIntoFileTool: ICopilotTool {
166164

167165
guard let app = app
168166
else {
169-
throw NSError(domain: "Failed to get the app that opens file.", code: 0)
167+
throw InsertEditError.openingApplicationUnavailable
170168
}
171169

172170
let appInstanceInspector = AppInstanceInspector(runningApplication: app)
173171
guard appInstanceInspector.isXcode
174172
else {
175-
throw NSError(domain: "The file is not opened in Xcode.", code: 0)
173+
throw InsertEditError.fileNotOpenedInXcode
176174
}
177-
175+
178176
let newContent = try applyEdit(
179177
for: fileURL,
180178
content: content,
@@ -192,4 +190,61 @@ public class InsertEditIntoFileTool: ICopilotTool {
192190
}
193191
}
194192
}
193+
194+
/// Get the source editor element with retries for specific file URL
195+
private static func getEditorElement(
196+
by xcodeInstance: AppInstanceInspector,
197+
for fileURL: URL,
198+
retryTimes: Int = 6,
199+
delay: TimeInterval = 0.5
200+
) -> AXUIElement? {
201+
var remainingAttempts = max(1, retryTimes)
202+
203+
while remainingAttempts > 0 {
204+
guard let realtimeURL = xcodeInstance.appElement.realtimeDocumentURL,
205+
realtimeURL == fileURL,
206+
let focusedElement = xcodeInstance.appElement.focusedElement,
207+
let editorElement = focusedElement.findSourceEditorElement()
208+
else {
209+
if remainingAttempts > 1 {
210+
Thread.sleep(forTimeInterval: delay)
211+
}
212+
213+
remainingAttempts -= 1
214+
continue
215+
}
216+
217+
return editorElement
218+
}
219+
220+
Logger.client.error("Editor element not found for \(fileURL.lastPathComponent) after \(retryTimes) attempts.")
221+
return nil
222+
}
223+
224+
// Check if current opened file is the target URL
225+
private static func checkOpenedFileURL(
226+
for fileURL: URL,
227+
xcodeInstance: AppInstanceInspector
228+
) throws {
229+
let realtimeDocumentURL = xcodeInstance.realtimeDocumentURL
230+
231+
if realtimeDocumentURL != fileURL {
232+
throw InsertEditError.fileURLMismatch(expected: fileURL, actual: realtimeDocumentURL)
233+
}
234+
}
235+
236+
private static func getCurrentEditorContent(for fileURL: URL, by xcodeInstance: AppInstanceInspector) throws -> String {
237+
guard let editorElement = getEditorElement(by: xcodeInstance, for: fileURL, retryTimes: 1)
238+
else {
239+
throw InsertEditError.missingEditorElement(file: fileURL)
240+
}
241+
242+
return try editorElement.copyValue(key: kAXValueAttribute)
243+
}
244+
}
245+
246+
private extension AppInstanceInspector {
247+
var realtimeDocumentURL: URL? {
248+
appElement.realtimeDocumentURL
249+
}
195250
}

Core/Sources/ConversationTab/FilePicker.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,8 @@ struct FileRowView: View {
203203

204204
var body: some View {
205205
WithPerceptionTracking {
206-
HStack {
206+
HStack(alignment: .center) {
207207
drawFileIcon(ref.url, isDirectory: ref.isDirectory)
208-
.resizable()
209208
.scaledToFit()
210209
.scaledFrame(width: 16, height: 16)
211210
.hoverSecondaryForeground(isHovered: selectedId == id)

Core/Sources/ConversationTab/ModeAndModelPicker/ModeAndModelPickerPicker.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ struct ModeAndModelPicker: View {
116116

117117
for model in models {
118118
let multiplierText = ModelMenuItemFormatter.getMultiplierText(for: model)
119-
newCache[model.modelName.appending(model.providerName ?? "")] = multiplierText
120-
119+
newCache[model.id.appending(model.providerName ?? "")] = multiplierText
120+
121121
let displayName = "\(model.displayName ?? model.modelName)"
122122
let displayNameWidth = displayName.size(withAttributes: attributes).width
123123
let multiplierWidth = multiplierText.isEmpty ? 0 : multiplierText.size(withAttributes: attributes).width
@@ -328,7 +328,7 @@ struct ModeAndModelPicker: View {
328328
Text(createModelMenuItemAttributedString(
329329
modelName: model.displayName ?? model.modelName,
330330
isSelected: selectedModel == model,
331-
cachedMultiplierText: currentCache.modelMultiplierCache[model.modelName.appending(model.providerName ?? "")] ?? ""
331+
cachedMultiplierText: currentCache.modelMultiplierCache[model.id.appending(model.providerName ?? "")] ?? ""
332332
))
333333
}
334334
.help(

Core/Sources/ConversationTab/ModeAndModelPicker/ModelManagerUtils.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public extension AppState {
2626
}
2727

2828
guard let modelName = savedModel["modelName"]?.stringValue,
29-
let modelFamily = savedModel["modelFamily"]?.stringValue else {
29+
let modelFamily = savedModel["modelFamily"]?.stringValue,
30+
let id = savedModel["id"]?.stringValue else {
3031
return nil
3132
}
3233

@@ -48,6 +49,7 @@ public extension AppState {
4849
displayName: displayName,
4950
modelName: modelName,
5051
modelFamily: modelFamily,
52+
id: id,
5153
billing: billing,
5254
providerName: providerName,
5355
supportVision: supportVision
@@ -149,7 +151,8 @@ public class CopilotModelManagerObservable: ObservableObject {
149151
AppState.shared.setSelectedModel(
150152
.init(
151153
modelName: fallbackModel.modelName,
152-
modelFamily: fallbackModel.id,
154+
modelFamily: fallbackModel.modelFamily,
155+
id: fallbackModel.id,
153156
billing: fallbackModel.billing,
154157
supportVision: fallbackModel.capabilities.supports.vision
155158
)
@@ -170,6 +173,7 @@ public extension CopilotModelManager {
170173
return LLMModel(
171174
modelName: $0.modelName,
172175
modelFamily: $0.isChatFallback ? $0.id : $0.modelFamily,
176+
id: $0.id,
173177
billing: $0.billing,
174178
supportVision: $0.capabilities.supports.vision
175179
)
@@ -185,6 +189,7 @@ public extension CopilotModelManager {
185189
return LLMModel(
186190
modelName: defaultModel.modelName,
187191
modelFamily: defaultModel.modelFamily,
192+
id: defaultModel.id,
188193
billing: defaultModel.billing,
189194
supportVision: defaultModel.capabilities.supports.vision
190195
)
@@ -196,6 +201,7 @@ public extension CopilotModelManager {
196201
return LLMModel(
197202
modelName: gpt4_1.modelName,
198203
modelFamily: gpt4_1.modelFamily,
204+
id: gpt4_1.id,
199205
billing: gpt4_1.billing,
200206
supportVision: gpt4_1.capabilities.supports.vision
201207
)
@@ -206,6 +212,7 @@ public extension CopilotModelManager {
206212
return LLMModel(
207213
modelName: firstModel.modelName,
208214
modelFamily: firstModel.modelFamily,
215+
id: firstModel.id,
209216
billing: firstModel.billing,
210217
supportVision: firstModel.capabilities.supports.vision
211218
)
@@ -229,6 +236,7 @@ public extension BYOKModelManager {
229236
displayName: $0.modelCapabilities?.name,
230237
modelName: $0.modelId,
231238
modelFamily: $0.modelId,
239+
id: $0.modelId,
232240
billing: nil,
233241
providerName: $0.providerName.rawValue,
234242
supportVision: $0.modelCapabilities?.vision ?? false
@@ -241,6 +249,7 @@ public struct LLMModel: Codable, Hashable, Equatable {
241249
public let displayName: String?
242250
public let modelName: String
243251
public let modelFamily: String
252+
public let id: String
244253
public let billing: CopilotModelBilling?
245254
public let providerName: String?
246255
public let supportVision: Bool
@@ -249,13 +258,15 @@ public struct LLMModel: Codable, Hashable, Equatable {
249258
displayName: String? = nil,
250259
modelName: String,
251260
modelFamily: String,
261+
id: String,
252262
billing: CopilotModelBilling?,
253263
providerName: String? = nil,
254264
supportVision: Bool
255265
) {
256266
self.displayName = displayName
257267
self.modelName = modelName
258268
self.modelFamily = modelFamily
269+
self.id = id
259270
self.billing = billing
260271
self.providerName = providerName
261272
self.supportVision = supportVision

0 commit comments

Comments
 (0)