Skip to content

Commit b79ae9e

Browse files
committed
allow edit tool to be used in different modes, improve UI
1 parent 0e59989 commit b79ae9e

File tree

14 files changed

+554
-186
lines changed

14 files changed

+554
-186
lines changed

app/modules/App/Sources/plugins/ToolsPlugin.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ extension ToolsPlugin {
1414
plugIn(tool: LSTool())
1515
plugIn(tool: ReadFileTool())
1616
plugIn(tool: SearchFilesTool())
17-
plugIn(tool: EditFilesTool())
17+
plugIn(tool: EditFilesTool(shouldAutoApply: true))
18+
// plugIn(tool: EditFilesTool(shouldAutoApply: false))
1819
plugIn(tool: ExecuteCommandTool())
1920
plugIn(tool: AskFollowUpTool())
2021
}

app/modules/Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,9 @@ targets.append(contentsOf: Target.module(
240240
"FoundationInterfaces",
241241
"JSONFoundation",
242242
"ServerServiceInterface",
243+
"ThreadSafe",
243244
"ToolFoundation",
244245
"XcodeControllerServiceInterface",
245-
"XcodeObserverServiceInterface",
246246
],
247247
testDependencies: [],
248248
path: "./plugins/tools/EditFilesTool"))
@@ -788,6 +788,7 @@ targets.append(contentsOf: Target.module(
788788
.product(name: "JSONScanner", package: "JSONScanner"),
789789
.product(name: "SwiftOpenAI", package: "SwiftOpenAI"),
790790
"AppFoundation",
791+
"ChatFoundation",
791792
"ConcurrencyFoundation",
792793
"DependencyFoundation",
793794
"JSONFoundation",

app/modules/coreui/CodePreview/Sources/FileDiffViewModel.swift

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,23 @@ public final class FileDiffViewModel: Sendable {
2424
filePath: String,
2525
llmDiff: String)
2626
{
27-
// Variables used to log debug info if something fails.
28-
var _oldContent: String?
27+
let fileContent: String
28+
do {
29+
fileContent = try Self.getCurrentContent(of: URL(fileURLWithPath: filePath))
30+
} catch {
31+
defaultLogger.error("Error reading file \(filePath)", error)
32+
return nil
33+
}
2934
do {
30-
let path = URL(fileURLWithPath: filePath)
31-
@Dependency(\.fileManager) var fileManager
32-
let fileContent = try fileManager.read(contentsOf: path)
33-
_oldContent = fileContent
3435
let changes = try FileDiff.parse(searchReplacePattern: llmDiff, for: fileContent)
35-
self.init(filePath: filePath, changes: changes)
36+
self.init(filePath: filePath, changes: changes, oldContent: fileContent)
3637
} catch {
3738
defaultLogger.error("""
3839
Could not format diff for \(filePath): \(error)
3940
-- Diff:
4041
\(llmDiff)
4142
-- Current content:
42-
\(_oldContent ?? "?")
43+
\(fileContent)
4344
--
4445
""")
4546
return nil
@@ -48,53 +49,53 @@ public final class FileDiffViewModel: Sendable {
4849

4950
public convenience init?(
5051
filePath: String,
51-
changes: [FileDiff.SearchReplace])
52+
changes: [FileDiff.SearchReplace],
53+
oldContent: String? = nil)
5254
{
53-
// Variables used to log debug info if something fails.
54-
var _oldContent: String?
55-
var _newContent: String?
56-
do {
57-
let path = URL(fileURLWithPath: filePath)
58-
@Dependency(\.fileManager) var fileManager
59-
guard let oldContent = try? fileManager.read(contentsOf: path) else {
60-
throw AppError(message: "File content not available for \(path)")
55+
let path = URL(fileURLWithPath: filePath)
56+
let fileContent: String
57+
if let oldContent {
58+
fileContent = oldContent
59+
} else {
60+
do {
61+
fileContent = try Self.getCurrentContent(of: path)
62+
} catch {
63+
defaultLogger.error("Error reading file \(filePath)", error)
64+
return nil
6165
}
62-
_oldContent = oldContent
63-
64-
let newContent = try FileDiff.apply(changes: changes, to: oldContent)
65-
_newContent = newContent
66+
}
6667

67-
if newContent == oldContent {
68+
do {
69+
let newContent = try FileDiff.apply(changes: changes, to: fileContent)
70+
if newContent == fileContent {
6871
return nil
6972
}
7073

71-
let gitDiff = try FileDiff.getGitDiff(oldContent: oldContent, newContent: newContent)
74+
let gitDiff = try FileDiff.getGitDiff(oldContent: fileContent, newContent: newContent)
7275

7376
self.init(
7477
filePath: path,
75-
baseLineContent: oldContent,
78+
baseLineContent: fileContent,
7679
targetContent: newContent,
7780
changes: changes,
7881
canBeApplied: true,
7982
formattedDiff: nil)
8083

8184
diffingTasks.queue {
8285
let formattedDiff = try await FileDiff.getColoredDiff(
83-
oldContent: oldContent,
86+
oldContent: fileContent,
8487
newContent: newContent,
8588
gitDiff: gitDiff,
8689
highlightColors: .dark(.xcode))
87-
return .init(canBeApplied: true, formattedDiff: formattedDiff, baseLineContent: oldContent)
90+
return .init(canBeApplied: true, formattedDiff: formattedDiff, baseLineContent: fileContent)
8891
}
8992
} catch {
9093
defaultLogger.error("""
9194
Could not format diff for \(filePath): \(error)
9295
-- Changes:
9396
\(changes.map { "replace:\n\($0.replace)\nwith:\n\($0.replace)" }.joined(separator: "\n-------\n"))
9497
-- Previous Content:
95-
\(_oldContent ?? "?")
96-
-- New Content:
97-
\(_newContent ?? "?")
98+
\(fileContent)
9899
--
99100
""")
100101
return nil
@@ -251,4 +252,20 @@ public final class FileDiffViewModel: Sendable {
251252
private let xcodeController: XcodeController
252253

253254
private let diffingTasks = ReplaceableTaskQueue<SuggestionUpdate?>()
255+
256+
/// Get the current content of the file. It is possible that the editor has content that is not yet saved to disk.
257+
private static func getCurrentContent(of file: URL) throws -> String {
258+
@Dependency(\.fileManager) var fileManager
259+
@Dependency(\.xcodeObserver) var xcodeObserver
260+
let editorContent = xcodeObserver.state.wrapped?.xcodesState.compactMap { xc in
261+
xc.workspaces.compactMap { ws in
262+
ws.tabs.compactMap { tab in
263+
tab.knownPath == file ? tab.lastKnownContent : nil
264+
}.first
265+
}.first
266+
}.first
267+
// TODO: is it fine to run on the main thread?
268+
return try editorContent ?? fileManager.read(contentsOf: file, encoding: .utf8)
269+
}
270+
254271
}

app/modules/features/Chat/Sources/ChatTabViewModel.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ final class ChatTabViewModel: Identifiable, Equatable {
123123
model: selectedModel,
124124
context: DefaultChatContext(
125125
projectRoot: projectRoot,
126-
prepareForWriteToolUse: { [weak self] in await self?.handlePrepareForWriteToolUse() }),
126+
prepareForWriteToolUse: { [weak self] in await self?.handlePrepareForWriteToolUse() },
127+
chatMode: input.mode),
127128
migrated: true,
128129
handleUpdateStream: { newMessages in
129130
Task { @MainActor [weak self] in
@@ -257,14 +258,17 @@ struct DefaultChatContext: ChatContext {
257258

258259
init(
259260
projectRoot: URL,
260-
prepareForWriteToolUse: @escaping @Sendable () async -> Void)
261+
prepareForWriteToolUse: @escaping @Sendable () async -> Void,
262+
chatMode: ChatMode)
261263
{
262264
self.projectRoot = projectRoot
263265
self.prepareForWriteToolUse = prepareForWriteToolUse
266+
self.chatMode = chatMode
264267
}
265268

266269
let projectRoot: URL
267270
let prepareForWriteToolUse: @Sendable () async -> Void
271+
let chatMode: ChatMode
268272
}
269273

270274
// MARK: - ChatEvent

app/modules/plugins/tools/EditFilesTool/Module.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ Target.module(
1010
"FoundationInterfaces",
1111
"JSONFoundation",
1212
"ServerServiceInterface",
13+
"ThreadSafe",
1314
"ToolFoundation",
1415
"XcodeControllerServiceInterface",
15-
"XcodeObserverServiceInterface",
1616
],
1717
testDependencies: [])

0 commit comments

Comments
 (0)