diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 755a5464e..b8f56ec91 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,8 +6,8 @@ name: MacOS App on: push: branches: [ "main" ] - pull_request: - branches: [ "main" ] +# pull_request: +# branches: [ "main" ] jobs: build-release-app: diff --git a/app/cmd.sh b/app/cmd.sh index ba052ca6f..85a2bbadf 100755 --- a/app/cmd.sh +++ b/app/cmd.sh @@ -11,7 +11,7 @@ sync_dependencies_command() { } close_xcode() { - if pgrep -x "Xcode" > /dev/null; then + if pgrep -x "Xcode" >/dev/null; then # Kill Xcode. pkill -x "Xcode" fi @@ -40,7 +40,7 @@ clean_command() { while read file; do rm -rf "$file"; done # Reset xcode state cd "$(git rev-parse --show-toplevel)/app" && - find . -path '*.xcuserstate' 2>/dev/null | git check-ignore --stdin | xargs -I{} rm {} + find . -path '*.xcuserstate' 2>/dev/null | git check-ignore --stdin | xargs -I{} rm {} } test_swift_command() { diff --git a/app/modules/coreui/DLS/Sources/CoreUI/buttons/IconsLabelButton.swift b/app/modules/coreui/DLS/Sources/CoreUI/buttons/IconsLabelButton.swift new file mode 100644 index 000000000..c9833d287 --- /dev/null +++ b/app/modules/coreui/DLS/Sources/CoreUI/buttons/IconsLabelButton.swift @@ -0,0 +1,66 @@ +// Copyright command. All rights reserved. +// Licensed under the XXX License. See License.txt in the project root for license information. + +import Foundation +import SwiftUI + +// MARK: - IconsLabelButton + +public struct IconsLabelButton: View { + + public init( + action: @escaping () -> Void, + systemNames: [String], + label: String, + onHoverColor: Color = Color.primary.opacity(0.1), + padding: CGFloat = 4, + cornerRadius: CGFloat = 4) + { + self.action = action + self.systemNames = systemNames + self.label = label + self.onHoverColor = onHoverColor + self.padding = padding + self.cornerRadius = cornerRadius + } + + public var body: some View { + HoveredButton( + action: action, + onHoverColor: onHoverColor, + padding: padding, + cornerRadius: cornerRadius) + { + HStack(spacing: 2) { + Text(label) + ForEach(Array(systemNames.enumerated()), id: \.offset) { _, systemName in + Image(systemName: systemName) + } + } + } + } + + let action: () -> Void + let systemNames: [String] + let label: String + let onHoverColor: Color + let padding: CGFloat + let cornerRadius: CGFloat +} + +#Preview { + VStack { + IconsLabelButton( + action: { }, + systemNames: ["command", "return"], + label: "Accept") + IconsLabelButton( + action: { }, + systemNames: ["return"], + label: "Send") + IconsLabelButton( + action: { }, + systemNames: ["shift", "command", "delete.left"], + label: "Reject") + } +} diff --git a/app/modules/features/Chat/Sources/ChatTabViewModel.swift b/app/modules/features/Chat/Sources/ChatTabViewModel.swift index e8b0160a7..23ee2bb59 100644 --- a/app/modules/features/Chat/Sources/ChatTabViewModel.swift +++ b/app/modules/features/Chat/Sources/ChatTabViewModel.swift @@ -94,6 +94,7 @@ final class ChatTabViewModel: Identifiable, Equatable { func cancelCurrentMessage() { streamingTask?.cancel() streamingTask = nil + input.cancelAllPendingToolApprovalRequests() } /// Are we queing too much on the main thread? @@ -105,6 +106,10 @@ final class ChatTabViewModel: Identifiable, Equatable { defaultLogger.error("not sending as already streaming") return } + + // Cancel any pending tool approvals from previous messages + input.cancelAllPendingToolApprovalRequests() + guard let selectedModel = input.selectedModel else { defaultLogger.error("not sending as no model selected") return @@ -139,6 +144,9 @@ final class ChatTabViewModel: Identifiable, Equatable { project: projectInfo?.path, projectRoot: projectInfo?.dirPath, prepareForWriteToolUse: { [weak self] in await self?.handlePrepareForWriteToolUse() }, + requestToolApproval: { [weak self] toolUse in + try await self?.handleToolApproval(for: toolUse) + }, chatMode: input.mode), handleUpdateStream: { newMessages in Task { @MainActor [weak self] in @@ -189,6 +197,8 @@ final class ChatTabViewModel: Identifiable, Equatable { } } + private static let userDefaultsAlwaysApproveKey = "alwaysApprove_" + @ObservationIgnored private var workspaceRootObservation: AnyCancellable? @ObservationIgnored @@ -214,6 +224,28 @@ final class ChatTabViewModel: Identifiable, Equatable { } } + private func handleToolApproval(for toolUse: any ToolUse) async throws { + // Check if user has already approved this tool type + if shouldAlwaysApprove(toolName: toolUse.toolName) { + return // Skip approval for this tool + } + + let approvalResult = await input.requestApproval( + for: toolUse) + + switch approvalResult { + case .denied: + throw LLMServiceError.toolUsageDenied + case .approved: + break // Continue execution + case .alwaysApprove(let toolName): + // Store preference and continue + storeAlwaysApprovePreference(for: toolName) + case .cancelled: + throw CancellationError() + } + } + private func handlePrepareForWriteToolUse() async { guard let projectInfo = updateProjectInfo() else { return @@ -242,6 +274,14 @@ final class ChatTabViewModel: Identifiable, Equatable { } } + private func storeAlwaysApprovePreference(for toolName: String) { + userDefaults.set(true, forKey: "\(Self.userDefaultsAlwaysApproveKey)\(toolName)") + } + + private func shouldAlwaysApprove(toolName: String) -> Bool { + userDefaults.bool(forKey: "\(Self.userDefaultsAlwaysApproveKey)\(toolName)") + } + private func updateProjectInfo() -> SelectedProjectInfo? { if let projectInfo { return projectInfo @@ -274,17 +314,20 @@ struct DefaultChatContext: ChatContext { project: URL?, projectRoot: URL?, prepareForWriteToolUse: @escaping @Sendable () async -> Void, + requestToolApproval: @escaping @Sendable (any ToolUse) async throws -> Void, chatMode: ChatMode) { self.project = project self.projectRoot = projectRoot self.prepareForWriteToolUse = prepareForWriteToolUse + self.requestToolApproval = requestToolApproval self.chatMode = chatMode } let project: URL? let projectRoot: URL? let prepareForWriteToolUse: @Sendable () async -> Void + let requestToolApproval: @Sendable (any ToolUse) async throws -> Void let chatMode: ChatMode } diff --git a/app/modules/features/Chat/Sources/Input/ChatInputView.swift b/app/modules/features/Chat/Sources/Input/ChatInputView.swift index 6a8d7719e..edbb48e9b 100644 --- a/app/modules/features/Chat/Sources/Input/ChatInputView.swift +++ b/app/modules/features/Chat/Sources/Input/ChatInputView.swift @@ -56,6 +56,23 @@ struct ChatInputView: View { var body: some View { VStack(spacing: 0) { + if let pendingApproval = inputViewModel.pendingApproval { + ToolApprovalView( + request: pendingApproval, + onApprove: { + inputViewModel.handleApproval(of: pendingApproval, result: .approved) + }, + onDeny: { + inputViewModel.handleApproval(of: pendingApproval, result: .denied) + }, + onAlwaysApprove: { + inputViewModel.handleApproval(of: pendingApproval, result: .alwaysApprove(toolName: pendingApproval.displayName)) + }) + .transition( + .asymmetric( + insertion: .move(edge: .bottom).combined(with: .opacity), + removal: .move(edge: .bottom).combined(with: .opacity))) + } VStack(alignment: .leading, spacing: 0) { HStack(spacing: 8) { AttachmentsView( @@ -102,6 +119,7 @@ struct ChatInputView: View { .offset(y: -30) } } + .animation(.easeInOut, value: inputViewModel.pendingApproval != nil) .onTapGesture { inputViewModel.textInputNeedsFocus = true } diff --git a/app/modules/features/Chat/Sources/Input/ChatInputViewModel.swift b/app/modules/features/Chat/Sources/Input/ChatInputViewModel.swift index 49ebd2398..5d9a6f41d 100644 --- a/app/modules/features/Chat/Sources/Input/ChatInputViewModel.swift +++ b/app/modules/features/Chat/Sources/Input/ChatInputViewModel.swift @@ -15,9 +15,34 @@ import LoggingServiceInterface import PDFKit import SettingsServiceInterface import SwiftUI +import ToolFoundation import UniformTypeIdentifiers import XcodeObserverServiceInterface +// MARK: - ToolApprovalRequest + +struct ToolApprovalRequest: Identifiable { + let id = UUID() + let displayName: String +} + +// MARK: - ToolApprovalResult + +enum ToolApprovalResult { + case approved + case denied + case alwaysApprove(toolName: String) + case cancelled +} + +// MARK: - PendingToolApproval + +/// The current tool approvals request pending user response. +private struct PendingToolApproval { + let request: ToolApprovalRequest + let continuation: CheckedContinuation +} + // MARK: - ChatInputViewModel @Observable @MainActor @@ -109,6 +134,9 @@ final class ChatInputViewModel { /// When searching for references, the index of the selected search result (at this point the selection has not yet been confirmed). var selectedSearchResultIndex = 0 + /// The current tool approval request pending user response. + var pendingApproval: ToolApprovalRequest? { toolCallsPendingApproval.first?.request } + /// Which LLM model is selected to respond to the next message. var selectedModel: LLMModel? { didSet { @@ -277,9 +305,36 @@ final class ChatInputViewModel { textInput = TextInput(str) } + /// Request approval for a tool use operation. + func requestApproval(for toolUse: any ToolUse) async -> ToolApprovalResult { + await withCheckedContinuation { continuation in + let request = ToolApprovalRequest( + displayName: toolUse.toolDisplayName) + self.toolCallsPendingApproval.append(PendingToolApproval(request: request, continuation: continuation)) + } + } + + func cancelAllPendingToolApprovalRequests() { + for item in toolCallsPendingApproval { item.continuation.resume(returning: .cancelled) } + } + + /// Handle the user's approval response. + func handleApproval(of request: ToolApprovalRequest, result: ToolApprovalResult) { + guard let index = toolCallsPendingApproval.firstIndex(where: { $0.request.id == request.id }) else { + defaultLogger.error("Could not find pending tool approval request with ID: \(request.id)") + return + } + let pendingApproval = toolCallsPendingApproval.remove(at: index) + pendingApproval.continuation.resume(returning: result) + } + private static let userDefaultsSelectLLMModelKey = "selectedLLMModel" private static let userDefaultsChatModeKey = "chatMode" + /// Queue of tool approval requests waiting for user response. + /// Each entry contains both the request details and the continuation that will receive the user's decision. + private var toolCallsPendingApproval: [PendingToolApproval] = [] + /// References to attachments within the text input. @ObservationIgnored private var inlineReferences = [String: Attachment]() @@ -356,7 +411,6 @@ final class ChatInputViewModel { } selectedModel = activeModels.first } - } // MARK: - TextInput diff --git a/app/modules/features/Chat/Sources/Input/ToolApprovalView.swift b/app/modules/features/Chat/Sources/Input/ToolApprovalView.swift new file mode 100644 index 000000000..d0e0cd68a --- /dev/null +++ b/app/modules/features/Chat/Sources/Input/ToolApprovalView.swift @@ -0,0 +1,54 @@ +// Copyright command. All rights reserved. +// Licensed under the XXX License. See License.txt in the project root for license information. + +import DLS +import SwiftUI +import ToolFoundation + +// MARK: - ToolApprovalView + +struct ToolApprovalView: View { + + let request: ToolApprovalRequest + let onApprove: () -> Void + let onDeny: () -> Void + let onAlwaysApprove: () -> Void + + var body: some View { + VStack(alignment: .leading) { + Text("**cmd** wants to use the *\(request.displayName)* tool") + .frame(maxWidth: .infinity, alignment: .leading) + HStack(spacing: 12) { + IconsLabelButton( + action: onAlwaysApprove, + systemNames: ["command", "shift", "return"], + label: "Always Allow") + .keyboardShortcut(.return, modifiers: [.shift, .command]) + + IconsLabelButton( + action: onApprove, + systemNames: ["command", "return"], + label: "Allow Once") + .keyboardShortcut(.return, modifiers: .command) + + IconsLabelButton( + action: onDeny, + systemNames: ["command", "shift", "delete.left"], + label: "Reject") + .keyboardShortcut(.delete, modifiers: [.shift, .command]) + } + } + .padding(12) + } +} + +// MARK: - Previews + +#Preview { + ToolApprovalView( + request: ToolApprovalRequest( + displayName: "get_workspace_info"), + onApprove: { }, + onDeny: { }, + onAlwaysApprove: { }) +} diff --git a/app/modules/features/Chat/Sources/Message/ChatMessageView+Previews.swift b/app/modules/features/Chat/Sources/Message/ChatMessageView+Previews.swift index 25b8a8095..4cd355dcb 100644 --- a/app/modules/features/Chat/Sources/Message/ChatMessageView+Previews.swift +++ b/app/modules/features/Chat/Sources/Message/ChatMessageView+Previews.swift @@ -39,6 +39,7 @@ struct TestTool: NonStreamableTool { var inputSchema = JSON.object([:]) var name: String { "TestTool" } + var displayName: String { "Test Tool" } var description: String { "A tool for testing." } func use(toolUseId _: String, input _: String, context _: ToolExecutionContext) -> Use { diff --git a/app/modules/foundations/ToolFoundation/Sources/Tool.swift b/app/modules/foundations/ToolFoundation/Sources/Tool.swift index 196b8870d..5f1cba96e 100644 --- a/app/modules/foundations/ToolFoundation/Sources/Tool.swift +++ b/app/modules/foundations/ToolFoundation/Sources/Tool.swift @@ -28,6 +28,8 @@ public protocol Tool: Sendable { func isAvailable(in mode: ChatMode) -> Bool /// Whether this tool expect to receive the input as it is being streamed, or only once it is received entirely. var canInputBeStreamed: Bool { get } + /// The tool display name + var displayName: String { get } } extension Tool { @@ -79,6 +81,8 @@ extension ToolUse { public var toolName: String { callingTool.name } + public var toolDisplayName: String { callingTool.displayName } + public var result: Output { get async throws { // TODO: check why the iterator doesn't work nor completes if the value has already been set. diff --git a/app/modules/plugins/tools/AskFollowUpTool/Sources/AskFollowUpTool.swift b/app/modules/plugins/tools/AskFollowUpTool/Sources/AskFollowUpTool.swift index 3b43cfcee..8c9e1d36e 100644 --- a/app/modules/plugins/tools/AskFollowUpTool/Sources/AskFollowUpTool.swift +++ b/app/modules/plugins/tools/AskFollowUpTool/Sources/AskFollowUpTool.swift @@ -62,6 +62,10 @@ public final class AskFollowUpTool: NonStreamableTool { Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. """ + public var displayName: String { + "Follow Up" + } + public var inputSchema: JSON { .object([ "type": .string("object"), diff --git a/app/modules/plugins/tools/BuildTool/Sources/BuildTool.swift b/app/modules/plugins/tools/BuildTool/Sources/BuildTool.swift index 728c4fe1c..c2da731b5 100644 --- a/app/modules/plugins/tools/BuildTool/Sources/BuildTool.swift +++ b/app/modules/plugins/tools/BuildTool/Sources/BuildTool.swift @@ -88,6 +88,10 @@ public final class BuildTool: NonStreamableTool { Request to trigger a build action in Xcode. This tool allows you to build for testing or running and to get the output in case of failure. """ + public var displayName: String { + "Build" + } + public var inputSchema: JSON { .object([ "type": .string("object"), diff --git a/app/modules/plugins/tools/EditFilesTool/Sources/EditFilesTool.swift b/app/modules/plugins/tools/EditFilesTool/Sources/EditFilesTool.swift index c35e448ad..b2e669270 100644 --- a/app/modules/plugins/tools/EditFilesTool/Sources/EditFilesTool.swift +++ b/app/modules/plugins/tools/EditFilesTool/Sources/EditFilesTool.swift @@ -210,6 +210,10 @@ public final class EditFilesTool: Tool { } } + public var displayName: String { + "Edit Files" + } + public var description: String { """ \(shortDescription) This tool allows for precise, surgical replaces to files by specifying exactly what content to search for and what to replace it with. diff --git a/app/modules/plugins/tools/ExecuteCommandTool/Sources/ExecuteCommandTool.swift b/app/modules/plugins/tools/ExecuteCommandTool/Sources/ExecuteCommandTool.swift index 510c9ea7e..23f2f7a74 100644 --- a/app/modules/plugins/tools/ExecuteCommandTool/Sources/ExecuteCommandTool.swift +++ b/app/modules/plugins/tools/ExecuteCommandTool/Sources/ExecuteCommandTool.swift @@ -112,6 +112,10 @@ public final class ExecuteCommandTool: NonStreamableTool { DO NOT use this to create or update files. Instead describe them as code suggestions, and wait for the users to approve the changes. """ + public var displayName: String { + "Execute Command" + } + public var inputSchema: JSON { .object([ "type": .string("object"), diff --git a/app/modules/plugins/tools/LSTool/Sources/LSTool.swift b/app/modules/plugins/tools/LSTool/Sources/LSTool.swift index 3a6a4141d..bed3c7690 100644 --- a/app/modules/plugins/tools/LSTool/Sources/LSTool.swift +++ b/app/modules/plugins/tools/LSTool/Sources/LSTool.swift @@ -97,6 +97,10 @@ public final class LSTool: NonStreamableTool { Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. """ + public var displayName: String { + "List Files" + } + public var inputSchema: JSON { .object([ "type": .string("object"), diff --git a/app/modules/plugins/tools/ReadFileTool/Sources/ReadFileTool.swift b/app/modules/plugins/tools/ReadFileTool/Sources/ReadFileTool.swift index c4cdc7fa5..6ddb4b8ec 100644 --- a/app/modules/plugins/tools/ReadFileTool/Sources/ReadFileTool.swift +++ b/app/modules/plugins/tools/ReadFileTool/Sources/ReadFileTool.swift @@ -89,6 +89,10 @@ public final class ReadFileTool: NonStreamableTool { - limit: (optional) Maximum number of lines to read. If not provided, the tool will attempt to read the entire file. """ + public var displayName: String { + "Read File" + } + public var inputSchema: JSON { .object([ "type": .string("object"), diff --git a/app/modules/plugins/tools/SearchFilesTool/Sources/SearchFilesTool.swift b/app/modules/plugins/tools/SearchFilesTool/Sources/SearchFilesTool.swift index 2b3d603c8..128d34392 100644 --- a/app/modules/plugins/tools/SearchFilesTool/Sources/SearchFilesTool.swift +++ b/app/modules/plugins/tools/SearchFilesTool/Sources/SearchFilesTool.swift @@ -93,6 +93,10 @@ public final class SearchFilesTool: NonStreamableTool { Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. """ + public var displayName: String { + "Search Files" + } + public var inputSchema: JSON { .object([ "type": .string("object"), diff --git a/app/modules/serviceInterfaces/LLMServiceInterface/Sources/LLMService.swift b/app/modules/serviceInterfaces/LLMServiceInterface/Sources/LLMService.swift index 81167046f..28f788452 100644 --- a/app/modules/serviceInterfaces/LLMServiceInterface/Sources/LLMService.swift +++ b/app/modules/serviceInterfaces/LLMServiceInterface/Sources/LLMService.swift @@ -21,6 +21,8 @@ public protocol ChatContext: Sendable { /// When a tool that is not read-only runs, this function will be called before. var prepareForWriteToolUse: @Sendable () async -> Void { get } + /// Request user approval before executing a tool. + var requestToolApproval: @Sendable (any ToolUse) async throws -> Void { get } /// Which chat mode applies to the current context. var chatMode: ChatMode { get } } @@ -37,6 +39,23 @@ public protocol LLMService: Sendable { async throws -> [AssistantMessage] } +// MARK: - LLMServiceError + +public enum LLMServiceError: Error { + case toolUsageDenied +} + +// MARK: LocalizedError + +extension LLMServiceError: LocalizedError { + public var errorDescription: String? { + switch self { + case .toolUsageDenied: + "User denied permission to execute this tool. Please suggest an alternative approach or ask for clarification." + } + } +} + #if DEBUG // TODO: Remove this once tests have been migrated to use the new API. extension LLMService { diff --git a/app/modules/services/LLMService/Sources/FailedTool.swift b/app/modules/services/LLMService/Sources/FailedTool.swift index 714e86b87..71eea14f4 100644 --- a/app/modules/services/LLMService/Sources/FailedTool.swift +++ b/app/modules/services/LLMService/Sources/FailedTool.swift @@ -38,21 +38,27 @@ struct FailedToolUse: ToolUse { // MARK: - FailedTool struct FailedTool: NonStreamableTool { + init(name: String) { + self.name = name + } + typealias Use = FailedToolUse - func isAvailable(in _: ChatMode) -> Bool { - true + let name: String + + var displayName: String { + "Failed tool" } - init(name: String) { - self.name = name + var description: String { "Failed tool" } + var inputSchema: JSON { .object([:]) } + + func isAvailable(in _: ChatMode) -> Bool { + true } func use(toolUseId _: String, input _: EmptyObject, context _: ToolExecutionContext) -> FailedToolUse { fatalError("Should not be called") } - let name: String - var description: String { "Failed tool" } - var inputSchema: JSON { .object([:]) } } diff --git a/app/modules/services/LLMService/Sources/RequestStreamingHelper.swift b/app/modules/services/LLMService/Sources/RequestStreamingHelper.swift index f78a726d7..12c6837e4 100644 --- a/app/modules/services/LLMService/Sources/RequestStreamingHelper.swift +++ b/app/modules/services/LLMService/Sources/RequestStreamingHelper.swift @@ -212,9 +212,30 @@ final class RequestStreamingHelper: Sendable { } if isInputComplete { - // TODO: move to handle(toolUseReque to help ensure that we execute the tool use with an input that has no change of having missing data. - streamingToolUse?.startExecuting() - endStreamedToolUse() + // Request approval before executing the tool + if let toolUse = streamingToolUse { + endStreamedToolUse() + Task { [weak self] in + guard let self else { return } + do { + try await context.requestToolApproval(toolUse) + toolUse.startExecuting() + } catch { + defaultLogger.error("Tool approval denied or cancelled: \(error)") + // Replace the tool use with a failed one + var updatedContent = result.content + if let index = updatedContent.firstIndex(where: { $0.asToolUseRequest?.toolUse.toolUseId == toolUse.toolUseId }) { + updatedContent[index] = .tool(ToolUseMessage(toolUse: FailedToolUse( + toolUseId: toolUse.toolUseId, + toolName: toolUse.toolName, + error: error))) + result.update(with: AssistantMessage(content: updatedContent)) + } + } + } + } else { + endStreamedToolUse() + } } } catch { // If the above fails, this is because the input could not be parsed by the tool. @@ -266,7 +287,20 @@ final class RequestStreamingHelper: Sendable { await context.prepareForWriteToolUse() } content.append(toolUse: toolUse) - toolUse.startExecuting() + + // Request approval before executing the tool + do { + try await context.requestToolApproval(toolUse) + toolUse.startExecuting() + } catch { + defaultLogger.error("Tool approval denied or cancelled: \(error)") + // Replace the tool use with a failed one + content.removeLast() + content.append(toolUse: FailedToolUse( + toolUseId: toolUse.toolUseId, + toolName: toolUse.toolName, + error: error)) + } } catch { // If the above fails, this is because the input could not be parsed by the tool. diff --git a/app/tools/dependencies/focus.sh b/app/tools/dependencies/focus.sh index fdd913d97..765b1f572 100755 --- a/app/tools/dependencies/focus.sh +++ b/app/tools/dependencies/focus.sh @@ -18,13 +18,13 @@ fi # If --list is provided, list the packages and exit if [[ "$*" == *"--list"* ]]; then - ./tmp/bin/sync-package-dependencies focus --path "${ROOT_DIR}/app/modules/Package.swift" "$@" + ./tmp/bin/sync-package-dependencies focus --path "${ROOT_DIR}/app/modules/Package.swift" "$@" - # lint Package.swift / Module.swift - cd "${ROOT_DIR}/app" - mkdir -p .build/caches/swiftformat - swiftformat --config rules.swiftformat ./**/Module.swift --cache .build/caches/swiftformat --quiet - swiftformat --config rules.swiftformat ./**/Package.swift --cache .build/caches/swiftformat --quiet + # lint Package.swift / Module.swift + cd "${ROOT_DIR}/app" + mkdir -p .build/caches/swiftformat + swiftformat --config rules.swiftformat ./**/Module.swift --cache .build/caches/swiftformat --quiet + swiftformat --config rules.swiftformat ./**/Package.swift --cache .build/caches/swiftformat --quiet exit 0 fi @@ -44,4 +44,3 @@ echo "Opening package at: ${package_path_for_module}" # Tell Xcode to not re-open windows that were open when it quit, # to avoid having several windows using the same files which Xcode doesn't support. open -a "$xcode_path" "$package_path_for_module" --args -ApplePersistenceIgnoreState YES - diff --git a/app/tools/sync-js-bundle-to-app.sh b/app/tools/sync-js-bundle-to-app.sh index 3186959c6..71778b5b9 100755 --- a/app/tools/sync-js-bundle-to-app.sh +++ b/app/tools/sync-js-bundle-to-app.sh @@ -64,7 +64,7 @@ build_and_copy() { # So we need to exit with a non-zero exit code to indicate that the build needs to be restarted with the correct files. echo "The local server has been updated. Please restart the build." >&2 exit 1 - fi + fi } # Sync the schema diff --git a/local-server/package.json b/local-server/package.json index 4e87d199e..c8cc4794e 100644 --- a/local-server/package.json +++ b/local-server/package.json @@ -89,5 +89,6 @@ "typescript": "^5.7.3", "typescript-eslint": "^8.22.0", "yargs": "^17.7.2" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/local-server/tools/parseAnthropicResponse.test.ts b/local-server/tools/parseAnthropicResponse.test.ts index c2b44eaf5..a02fc347e 100644 --- a/local-server/tools/parseAnthropicResponse.test.ts +++ b/local-server/tools/parseAnthropicResponse.test.ts @@ -238,7 +238,7 @@ data: {"type":"message_stop" }` { name: "Bash", parameters: { - command: "cd /Users/me/command/app/modules && swift test 2>&1 | head -100", + command: "cd /Users/guigui/dev/Xcompanion/app/modules && swift test 2>&1 | head -100", description: "Run tests again after fixing ChatContext", }, }, @@ -444,20 +444,22 @@ data: {"type":"message_stop" } { name: "Read", parameters: { - file_path: "/Users/me/command/app/modules/coreui/CodePreview/Tests/DiffViewModelTests.swift", + file_path: + "/Users/guigui/dev/Xcompanion/app/modules/coreui/CodePreview/Tests/DiffViewModelTests.swift", }, }, { name: "Read", parameters: { file_path: - "/Users/me/command/app/modules/plugins/tools/EditFilesTool/Tests/EditFileToolTests.swift", + "/Users/guigui/dev/Xcompanion/app/modules/plugins/tools/EditFilesTool/Tests/EditFileToolTests.swift", }, }, { name: "Read", parameters: { - file_path: "/Users/me/command/app/modules/foundations/SwiftTesting/Sources/Expectation.swift", + file_path: + "/Users/guigui/dev/Xcompanion/app/modules/foundations/SwiftTesting/Sources/Expectation.swift", }, }, ])