|
| 1 | +import Foundation |
| 2 | +import RxCodeCore |
| 3 | + |
| 4 | +/// Errors surfaced while starting a manual commit turn from a project, briefing, |
| 5 | +/// or thread menu. |
| 6 | +enum CommitFilesError: LocalizedError { |
| 7 | + case unknownThread |
| 8 | + case unknownProject |
| 9 | + case sendFailed(String) |
| 10 | + |
| 11 | + var errorDescription: String? { |
| 12 | + switch self { |
| 13 | + case .unknownThread: |
| 14 | + return "Couldn't find the thread to commit." |
| 15 | + case .unknownProject: |
| 16 | + return "Couldn't find the project to commit." |
| 17 | + case .sendFailed(let message): |
| 18 | + return "Couldn't start the commit.\n\n\(message)" |
| 19 | + } |
| 20 | + } |
| 21 | +} |
| 22 | + |
| 23 | +extension AppState { |
| 24 | + static let manualCommitLabel = "Commit" |
| 25 | + |
| 26 | + /// Send a commit-only follow-up into the selected thread. The prompt names |
| 27 | + /// the files recorded for that thread so the agent can avoid staging |
| 28 | + /// unrelated work. |
| 29 | + @discardableResult |
| 30 | + func commitFilesForThread(sessionId: String) async throws -> String { |
| 31 | + guard let summary = allSessionSummaries.first(where: { $0.id == sessionId }) |
| 32 | + ?? threadStore.fetch(id: sessionId)?.toSummary() else { |
| 33 | + throw CommitFilesError.unknownThread |
| 34 | + } |
| 35 | + guard projects.contains(where: { $0.id == summary.projectId }) else { |
| 36 | + throw CommitFilesError.unknownProject |
| 37 | + } |
| 38 | + |
| 39 | + var seen = Set<String>() |
| 40 | + let changedFiles = threadStore.fetchFileEdits(sessionId: sessionId).compactMap { edit in |
| 41 | + seen.insert(edit.path).inserted ? edit.path : nil |
| 42 | + } |
| 43 | + |
| 44 | + let result = try await sendCrossProject( |
| 45 | + projectId: nil, |
| 46 | + threadId: sessionId, |
| 47 | + prompt: Self.threadCommitPrompt(changedFiles: changedFiles), |
| 48 | + permissionMode: .auto, |
| 49 | + waitForResponse: false, |
| 50 | + timeoutSeconds: 600, |
| 51 | + setupKind: HookSetupKind.commitPush |
| 52 | + ) |
| 53 | + if let error = result.error { throw CommitFilesError.sendFailed(error) } |
| 54 | + return result.threadId |
| 55 | + } |
| 56 | + |
| 57 | + /// Start a commit-only thread for all current uncommitted project changes. |
| 58 | + /// Used by project rows and briefing cards. |
| 59 | + @discardableResult |
| 60 | + func commitAllChangesForProject(project: Project) async throws -> String { |
| 61 | + let result = try await sendCrossProject( |
| 62 | + projectId: project.id, |
| 63 | + threadId: nil, |
| 64 | + prompt: Self.projectCommitPrompt(projectName: project.name), |
| 65 | + permissionMode: .auto, |
| 66 | + waitForResponse: false, |
| 67 | + timeoutSeconds: 600, |
| 68 | + threadLabel: Self.manualCommitLabel, |
| 69 | + setupKind: HookSetupKind.commitPush |
| 70 | + ) |
| 71 | + if let error = result.error { throw CommitFilesError.sendFailed(error) } |
| 72 | + return result.threadId |
| 73 | + } |
| 74 | + |
| 75 | + static func threadCommitPrompt(changedFiles: [String]) -> String { |
| 76 | + let fileList = changedFiles.isEmpty |
| 77 | + ? "(no recorded file edits — inspect this thread and the working tree, then commit only the files that belong to this thread)" |
| 78 | + : changedFiles.map { "- \($0)" }.joined(separator: "\n") |
| 79 | + |
| 80 | + return """ |
| 81 | + Commit the files changed by this thread. |
| 82 | +
|
| 83 | + Files changed by this thread: |
| 84 | + \(fileList) |
| 85 | +
|
| 86 | + Steps: |
| 87 | + 1. Inspect `git status` and the relevant diffs. |
| 88 | + 2. Stage only the files that belong to this thread. Do not stage unrelated project changes. |
| 89 | + 3. Create a local commit with a clear Conventional Commit message. |
| 90 | + 4. Report the commit hash and the files committed. |
| 91 | +
|
| 92 | + Do not push the commit unless the user explicitly asks for a push. |
| 93 | + Do not make further code changes beyond what is needed to commit these files. |
| 94 | + """ |
| 95 | + } |
| 96 | + |
| 97 | + static func projectCommitPrompt(projectName: String) -> String { |
| 98 | + """ |
| 99 | + Commit all current uncommitted changes for project `\(projectName)`. |
| 100 | +
|
| 101 | + Steps: |
| 102 | + 1. Inspect `git status` and the relevant diffs. |
| 103 | + 2. Stage all modified, deleted, and untracked files that belong to the current project change set. |
| 104 | + 3. Create a local commit with a clear Conventional Commit message. |
| 105 | + 4. Report the commit hash and the files committed. |
| 106 | +
|
| 107 | + If there are no uncommitted changes, report that clearly and do not create an empty commit. |
| 108 | + Do not push the commit unless the user explicitly asks for a push. |
| 109 | + Do not make further code changes beyond what is needed to commit the current changes. |
| 110 | + """ |
| 111 | + } |
| 112 | +} |
0 commit comments