diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 30128e7..85458cc 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -938,6 +938,22 @@ } } }, + "Enable Debug Mode" : { + "localizations" : { + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activer le mode de débogage" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abilita la modalità di debug" + } + } + } + }, "Enable Dictation" : { "localizations" : { "fr" : { @@ -1034,6 +1050,16 @@ } } }, + "Failed to get screenshot boundaries" : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Non è stato possibile ottenere i confini dello screenshot" + } + } + } + }, "Failed to register" : { "localizations" : { "fr" : { diff --git a/TypeaheadAI.xcodeproj/project.pbxproj b/TypeaheadAI.xcodeproj/project.pbxproj index b1d2f5f..2300817 100644 --- a/TypeaheadAI.xcodeproj/project.pbxproj +++ b/TypeaheadAI.xcodeproj/project.pbxproj @@ -119,6 +119,7 @@ 2BCF84352A9DD90F00359841 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BCF84342A9DD90F00359841 /* HistoryManager.swift */; }; 2BCF843A2A9DE6DA00359841 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BCF84392A9DE6DA00359841 /* GeneralSettingsView.swift */; }; 2BD3821E2B2C4B2E00F96C19 /* CanSimulateEnter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD3821D2B2C4B2E00F96C19 /* CanSimulateEnter.swift */; }; + 2BD97C802B720496002570C1 /* ApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD97C7F2B720496002570C1 /* ApiError.swift */; }; 2BDA45C32ABEE840006128BC /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BDA45C22ABEE840006128BC /* MessageView.swift */; }; 2BDDB9892B27DDE100D52BF0 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 2BDDB9882B27DDE100D52BF0 /* SwiftSoup */; }; 2BDDB98B2B27DDFF00D52BF0 /* String+XMLMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BDDB98A2B27DDFF00D52BF0 /* String+XMLMarkdown.swift */; }; @@ -270,6 +271,7 @@ 2BCF84342A9DD90F00359841 /* HistoryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryManager.swift; sourceTree = ""; }; 2BCF84392A9DE6DA00359841 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; 2BD3821D2B2C4B2E00F96C19 /* CanSimulateEnter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanSimulateEnter.swift; sourceTree = ""; }; + 2BD97C7F2B720496002570C1 /* ApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiError.swift; sourceTree = ""; }; 2BDA45C22ABEE840006128BC /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 2BDDB98A2B27DDFF00D52BF0 /* String+XMLMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+XMLMarkdown.swift"; sourceTree = ""; }; 2BDDB98C2B282AFF00D52BF0 /* AppManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = ""; }; @@ -654,6 +656,7 @@ 2BE7BB8E2B258C7900164F88 /* UIElement.swift */, 2BDDB98E2B282B3000D52BF0 /* Application.swift */, 2B9A89902B44CBCC00041856 /* JSONAny.swift */, + 2BD97C7F2B720496002570C1 /* ApiError.swift */, ); path = Models; sourceTree = ""; @@ -847,6 +850,7 @@ 2BA7F0B52A9ABCD7003D38BA /* QuickActionManager.swift in Sources */, 2B2EF14E2AC17D4000EF2BD4 /* CustomTextField.swift in Sources */, 2B11FCF22B114C3F00325F38 /* Message.swift in Sources */, + 2BD97C802B720496002570C1 /* ApiError.swift in Sources */, 2B7669AA2B5EED910071D713 /* SpecialFocusActor.swift in Sources */, 2B8CD4BB2B0AAC09003E0589 /* CopyButtonView.swift in Sources */, 2B4BDB8B2ACC281E00E55D78 /* IntentManager.swift in Sources */, @@ -1098,7 +1102,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"TypeaheadAI/Preview Content\""; DEVELOPMENT_TEAM = TZA789GZFT; @@ -1116,7 +1120,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.1.27; + MARKETING_VERSION = 2.1.29; PRODUCT_BUNDLE_IDENTIFIER = ai.typeahead.TypeaheadAI; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1139,7 +1143,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"TypeaheadAI/Preview Content\""; DEVELOPMENT_TEAM = TZA789GZFT; @@ -1157,7 +1161,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.1.27; + MARKETING_VERSION = 2.1.29; PRODUCT_BUNDLE_IDENTIFIER = ai.typeahead.TypeaheadAI; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/TypeaheadAI/Actors/SpecialFocusActor.swift b/TypeaheadAI/Actors/SpecialFocusActor.swift index 2daa3b9..29c9d28 100644 --- a/TypeaheadAI/Actors/SpecialFocusActor.swift +++ b/TypeaheadAI/Actors/SpecialFocusActor.swift @@ -9,7 +9,7 @@ import Cocoa import Foundation import os.log -actor SpecialFocusActor: CanGetUIElements, CanExecuteScript { +actor SpecialFocusActor: CanGetUIElements { private let logger = Logger( subsystem: "ai.typeahead.TypeaheadAI", category: "SpecialFocusActor" diff --git a/TypeaheadAI/Actors/SpecialVisionActor.swift b/TypeaheadAI/Actors/SpecialVisionActor.swift index 5f9e9bd..e13ef6c 100644 --- a/TypeaheadAI/Actors/SpecialVisionActor.swift +++ b/TypeaheadAI/Actors/SpecialVisionActor.swift @@ -54,14 +54,19 @@ actor SpecialVisionActor: CanGetUIElements, CanExecuteScript { } func specialVision() async throws { + // Clear the current state + await self.modalManager.forceRefresh() + let appInfo = try await appContextManager.getActiveAppInfo() - guard let (point, size) = await getPointAndSize(appContext: appInfo.appContext) else { + guard let (point, size) = try await getPointAndSize(appContext: appInfo.appContext) else { + await self.modalManager.showModal() + await modalManager.setError(NSLocalizedString("Failed to get screenshot boundaries", comment: ""), appContext: appInfo.appContext) + + await NSApp.activate(ignoringOtherApps: true) return } if let image = captureScreen(point: point, size: size) { - // Clear the current state - await self.modalManager.forceRefresh() await self.modalManager.showModal() // Add user image @@ -72,15 +77,18 @@ actor SpecialVisionActor: CanGetUIElements, CanExecuteScript { try await modalManager.replyToUserMessage() } else { + await self.modalManager.showModal() await modalManager.setError(NSLocalizedString("Failed to get screenshot", comment: ""), appContext: appInfo.appContext) } + + await NSApp.activate(ignoringOtherApps: true) } - func getPointAndSize(appContext: AppContext?) async -> (CGPoint, CGSize)? { + func getPointAndSize(appContext: AppContext?) async throws -> (CGPoint, CGSize)? { if NSWorkspace.shared.isVoiceOverEnabled { /// If VoiceOver is enabled, then get the VO cursor bounds - if let serializedCursor = await executeScript(script: SpecialVisionActor.voCursorScript), - let data = serializedCursor.data(using: .utf8), + let serializedCursor = try await executeScript(script: SpecialVisionActor.voCursorScript) + if let data = serializedCursor.data(using: .utf8), let cursor = try? JSONDecoder().decode(VOCursor.self, from: data) { return (cursor.point, cursor.size) } else { diff --git a/TypeaheadAI/AppContextManager.swift b/TypeaheadAI/AppContextManager.swift index 2204664..df48e14 100644 --- a/TypeaheadAI/AppContextManager.swift +++ b/TypeaheadAI/AppContextManager.swift @@ -45,7 +45,7 @@ class AppContextManager: CanFetchAppContext, CanExecuteScript { private func getUrl(bundleIdentifier: String?) async -> URL? { if bundleIdentifier == "com.google.Chrome" { - if let urlString = await executeScript(script: AppContextManager.getActiveTabURLScript), + if let urlString = try? await executeScript(script: AppContextManager.getActiveTabURLScript), let url = URL(string: urlString), let strippedUrl = self.stripQueryParameters(from: url) { return strippedUrl diff --git a/TypeaheadAI/AppState.swift b/TypeaheadAI/AppState.swift index 6428778..9e05490 100644 --- a/TypeaheadAI/AppState.swift +++ b/TypeaheadAI/AppState.swift @@ -14,8 +14,7 @@ import UserNotifications @MainActor final class AppState: ObservableObject { - @Published var isLoading: Bool = false - @Published var isBlinking: Bool = false + // NOTE: This is needed for the Menu View @Published var isMenuVisible: Bool = false private var updateTimer: Timer? @@ -128,8 +127,6 @@ final class AppState: ObservableObject { self.settingsManager.quickActionManager = quickActionManager self.settingsManager.supabaseManager = supabaseManager - checkAndRequestNotificationPermissions() - KeyboardShortcuts.onKeyUp(for: .specialCopy) { [self] in Task { do { @@ -138,6 +135,7 @@ final class AppState: ObservableObject { NotificationCenter.default.post(name: .smartCopyPerformed, object: nil) } } catch { + try? await clientManager.sendFeedback(feedback: "Failed to smart-copy\n\(error.localizedDescription)") AudioServicesPlaySystemSoundWithCompletion(1103, nil) } } @@ -151,6 +149,7 @@ final class AppState: ObservableObject { NotificationCenter.default.post(name: .smartPastePerformed, object: nil) } } catch { + try? await clientManager.sendFeedback(feedback: "Failed to smart-paste\n\(error.localizedDescription)") AudioServicesPlaySystemSoundWithCompletion(1103, nil) } } @@ -161,7 +160,7 @@ final class AppState: ObservableObject { do { try await self.specialRecordActor?.specialRecord() } catch { - self.logger.error("\(error.localizedDescription)") + try? await clientManager.sendFeedback(feedback: "Failed to smart-record\n\(error.localizedDescription)") AudioServicesPlaySystemSoundWithCompletion(1103, nil) } } @@ -172,7 +171,7 @@ final class AppState: ObservableObject { do { try await self.specialVisionActor?.specialVision() } catch { - self.logger.error("\(error.localizedDescription)") + try? await clientManager.sendFeedback(feedback: "Failed to smart-vision\n\(error.localizedDescription)") AudioServicesPlaySystemSoundWithCompletion(1103, nil) } } @@ -183,7 +182,7 @@ final class AppState: ObservableObject { do { try await self.specialFocusActor?.specialFocus() } catch { - self.logger.error("\(error.localizedDescription)") + try? await clientManager.sendFeedback(feedback: "Failed to smart-focus\n\(error.localizedDescription)") AudioServicesPlaySystemSoundWithCompletion(1103, nil) } } @@ -194,7 +193,7 @@ final class AppState: ObservableObject { do { try await self.specialOpenActor?.specialOpen() } catch { - self.logger.error("\(error.localizedDescription)") + try? await clientManager.sendFeedback(feedback: "Failed to open chat\n\(error.localizedDescription)") AudioServicesPlaySystemSoundWithCompletion(1103, nil) } } @@ -205,7 +204,7 @@ final class AppState: ObservableObject { do { try await self.specialOpenActor?.specialOpen(forceRefresh: true) } catch { - self.logger.error("\(error.localizedDescription)") + try? await clientManager.sendFeedback(feedback: "Failed to open new chat\n\(error.localizedDescription)") AudioServicesPlaySystemSoundWithCompletion(1103, nil) } } @@ -215,14 +214,4 @@ final class AppState: ObservableObject { self.modalManager.cancelTasks() } } - - private func checkAndRequestNotificationPermissions() -> Void { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in - if granted { - self.logger.debug("Notification permission granted") - } else if let error = error { - self.logger.error("Notification permission error: \(error.localizedDescription)") - } - } - } } diff --git a/TypeaheadAI/ClientManager.swift b/TypeaheadAI/ClientManager.swift index 603194e..9b273e3 100644 --- a/TypeaheadAI/ClientManager.swift +++ b/TypeaheadAI/ClientManager.swift @@ -70,7 +70,7 @@ class ClientManager: CanGetUIElements { func sendFeedback(feedback: String, timeout: TimeInterval = 30) async throws { guard let uuid = try? await supabaseManager?.client.auth.session.user.id else { - throw ClientManagerError.signInRequired("Must be signed in to share feedback!") + throw ApiError.signInRequired("Must be signed in to share feedback!") } let payload = FeedbackPayload( @@ -81,7 +81,7 @@ class ClientManager: CanGetUIElements { ) guard let httpBody = try? JSONEncoder().encode(payload) else { - throw ClientManagerError.badRequest("Request was malformed...") + throw ApiError.badRequest("Request was malformed...") } var urlRequest = URLRequest(url: self.apiFeedback, timeoutInterval: timeout) @@ -92,14 +92,14 @@ class ClientManager: CanGetUIElements { let (data, resp) = try await self.session.data(for: urlRequest) guard let httpResponse = resp as? HTTPURLResponse else { - throw ClientManagerError.serverError("Something went wrong...") + throw ApiError.serverError("Something went wrong...") } guard 200...299 ~= httpResponse.statusCode else { if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) { - throw ClientManagerError.serverError(errorResponse.detail) + throw ApiError.serverError(errorResponse.detail) } else { - throw ClientManagerError.serverError("Something went wrong...") + throw ApiError.serverError("Something went wrong...") } } } @@ -125,7 +125,7 @@ class ClientManager: CanGetUIElements { } guard let uuid = try? await supabaseManager?.client.auth.session.user.id else { - throw ClientManagerError.signInRequired("Must be signed in.") + throw ApiError.signInRequired("Must be signed in.") } let payload = RequestPayload( @@ -145,7 +145,7 @@ class ClientManager: CanGetUIElements { ) guard let httpBody = try? JSONEncoder().encode(payload) else { - throw ClientManagerError.badRequest("Request was malformed...") + throw ApiError.badRequest("Request was malformed...") } var urlRequest = URLRequest(url: self.apiIntents, timeoutInterval: timeout) @@ -156,14 +156,14 @@ class ClientManager: CanGetUIElements { let (data, resp) = try await self.session.data(for: urlRequest) guard let httpResponse = resp as? HTTPURLResponse else { - throw ClientManagerError.serverError("Something went wrong...") + throw ApiError.serverError("Something went wrong...") } guard 200...299 ~= httpResponse.statusCode else { if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) { - throw ClientManagerError.serverError(errorResponse.detail) + throw ApiError.serverError(errorResponse.detail) } else { - throw ClientManagerError.serverError("Something went wrong...") + throw ApiError.serverError("Something went wrong...") } } @@ -176,11 +176,11 @@ class ClientManager: CanGetUIElements { ) async throws -> (FunctionCall, AppInfo?) { guard online else { // Incognito mode doesn't support this yet - throw ClientManagerError.signInRequired("Not supported in offline mode.") + throw ApiError.signInRequired("Not supported in offline mode.") } guard let uuid = try? await supabaseManager?.client.auth.session.user.id else { - throw ClientManagerError.signInRequired("Must be signed in.") + throw ApiError.signInRequired("Must be signed in.") } var appInfo = try await appContextManager?.getActiveAppInfo() @@ -211,7 +211,7 @@ class ClientManager: CanGetUIElements { ) guard let httpBody = try? JSONEncoder().encode(payload) else { - throw ClientManagerError.badRequest("Request was malformed...") + throw ApiError.badRequest("Request was malformed...") } var urlRequest = URLRequest(url: self.apiFocus, timeoutInterval: timeout) @@ -222,19 +222,19 @@ class ClientManager: CanGetUIElements { let (data, resp) = try await self.session.data(for: urlRequest) guard let httpResponse = resp as? HTTPURLResponse else { - throw ClientManagerError.serverError("Something went wrong...") + throw ApiError.serverError("Something went wrong...") } guard 200...299 ~= httpResponse.statusCode else { if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) { - throw ClientManagerError.serverError(errorResponse.detail) + throw ApiError.serverError(errorResponse.detail) } else { - throw ClientManagerError.serverError("Something went wrong...") + throw ApiError.serverError("Something went wrong...") } } guard let functionCall = try? JSONDecoder().decode(FunctionCall.self, from: data) else { - throw ClientManagerError.functionParsingError("Function could not be parsed: \(String(data: data, encoding: .utf8) ?? "")") + throw ApiError.functionParsingError("Function could not be parsed: \(String(data: data, encoding: .utf8) ?? "")") } return (functionCall, appInfo) @@ -441,7 +441,7 @@ class ClientManager: CanGetUIElements { var payloadCopy = payload payloadCopy.version = version guard let httpBody = try? JSONEncoder().encode(payloadCopy) else { - throw ClientManagerError.badRequest("Bad request format") + throw ApiError.badRequest("Bad request format") } var urlRequest = URLRequest(url: self.apiImage, timeoutInterval: timeout) @@ -452,14 +452,14 @@ class ClientManager: CanGetUIElements { let (data, resp) = try await self.session.data(for: urlRequest) guard let httpResponse = resp as? HTTPURLResponse else { - throw ClientManagerError.serverError("Something went wrong...") + throw ApiError.serverError("Something went wrong...") } guard 200...299 ~= httpResponse.statusCode else { if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: data) { - throw ClientManagerError.serverError(errorResponse.detail) + throw ApiError.serverError(errorResponse.detail) } else { - throw ClientManagerError.serverError("Something went wrong...") + throw ApiError.serverError("Something went wrong...") } } @@ -473,7 +473,7 @@ class ClientManager: CanGetUIElements { appInfo: AppInfo? ) throws -> AsyncThrowingStream { guard let httpBody = try? JSONEncoder().encode(payload) else { - throw ClientManagerError.badRequest("Encoding error") + throw ApiError.badRequest("Encoding error") } let decoder = JSONDecoder() @@ -491,25 +491,25 @@ class ClientManager: CanGetUIElements { do { (data, resp) = try await self.session.bytes(for: urlRequest) } catch { - let error = ClientManagerError.serverError(error.localizedDescription) + let error = ApiError.serverError(error.localizedDescription) continuation.finish(throwing: error) return } guard let httpResponse = resp as? HTTPURLResponse else { - let error = ClientManagerError.serverError("Something went wrong...") + let error = ApiError.serverError("Something went wrong...") continuation.finish(throwing: error) return } guard 200...299 ~= httpResponse.statusCode else { var buffer = Data() - var error = ClientManagerError.serverError("Something went wrong...") + var error = ApiError.serverError("Something went wrong...") for try await byte in data { buffer.append(byte) } if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: buffer) { - error = ClientManagerError.serverError(errorResponse.detail) + error = ApiError.serverError(errorResponse.detail) } continuation.finish(throwing: error) @@ -519,13 +519,13 @@ class ClientManager: CanGetUIElements { for try await line in data.lines { guard let data = line.data(using: .utf8), let chunk = try? decoder.decode(ChunkPayload.self, from: data) else { - let error = ClientManagerError.serverError("Failed to parse response...") + let error = ApiError.serverError("Failed to parse response...") continuation.finish(throwing: error) return } guard chunk.finishReason == nil || validFinishReasons.contains(chunk.finishReason!) else { - let error = ClientManagerError.serverError("Stream is incomplete. Finished with error: \"\(chunk.finishReason!)\"") + let error = ApiError.serverError("Stream is incomplete. Finished with error: \"\(chunk.finishReason!)\"") continuation.finish(throwing: error) return } @@ -647,54 +647,6 @@ struct ChunkPayload: Codable { let finishReason: String? } -enum ClientManagerError: LocalizedError { - case badRequest(_ message: String) - case retriesExceeded(_ message: String) - case clientError(_ message: String) - case serverError(_ message: String) - case appError(_ message: String) - case networkError(_ message: String) - case corruptedDataError(_ message: String) - case signInRequired(_ message: String) - - // LLaMA errors - case modelNotFound(_ message: String) - case modelNotLoaded(_ message: String) - case modelDirectoryNotAuthorized(_ message: String) - case modelFailed(_ message: String) - - // FunctionManager errors - case functionParsingError(_ message: String) - case functionArgParsingError(_ message: String) - case functionCallError(_ message: String, functionCall: FunctionCall, appContext: AppContext?) - case functionOnFocusError(_ message: String) - - var errorDescription: String { - switch self { - case .badRequest(let message): return message - case .retriesExceeded(let message): return message - case .clientError(let message): return message - case .serverError(let message): return message - case .appError(let message): return message - case .networkError(let message): return message - case .corruptedDataError(let message): return message - case .signInRequired(let message): return message - - // LLaMA model errors - case .modelNotFound(let message): return message - case .modelNotLoaded(let message): return message - case .modelDirectoryNotAuthorized(let message): return message - case .modelFailed(let message): return message - - // FunctionManager errors - case .functionParsingError(let message): return message - case .functionArgParsingError(let message): return message - case .functionCallError(let message, _, _): return message - case .functionOnFocusError(let message): return message - } - } -} - struct SuggestIntentsPayload: Codable { let intents: [String] } diff --git a/TypeaheadAI/Functions/FunctionManager+FocusUIElement.swift b/TypeaheadAI/Functions/FunctionManager+FocusUIElement.swift index f0f091e..e146b78 100644 --- a/TypeaheadAI/Functions/FunctionManager+FocusUIElement.swift +++ b/TypeaheadAI/Functions/FunctionManager+FocusUIElement.swift @@ -14,11 +14,11 @@ extension FunctionManager: CanSimulateControl { guard case .focusUIElement(let idOpt, let errorMessage) = try functionCall.parseArgs(), let elementMap = appInfo?.elementMap else { - throw ClientManagerError.appError("Invalid app state") + throw ApiError.appError("Invalid app state") } if let errorMessage = errorMessage { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( errorMessage, functionCall: functionCall, appContext: appInfo?.appContext @@ -26,7 +26,7 @@ extension FunctionManager: CanSimulateControl { } guard let elementId = idOpt else { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( "Missing element ID", functionCall: functionCall, appContext: appInfo?.appContext @@ -34,7 +34,7 @@ extension FunctionManager: CanSimulateControl { } guard let axElement = elementMap[elementId] else { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( "No such element \(elementId)", functionCall: functionCall, appContext: appInfo?.appContext diff --git a/TypeaheadAI/Functions/FunctionManager+OpenApplication.swift b/TypeaheadAI/Functions/FunctionManager+OpenApplication.swift index 5894875..6b3be48 100644 --- a/TypeaheadAI/Functions/FunctionManager+OpenApplication.swift +++ b/TypeaheadAI/Functions/FunctionManager+OpenApplication.swift @@ -11,11 +11,11 @@ import Foundation extension FunctionManager { func openApplication(_ functionCall: FunctionCall, appInfo: AppInfo?) async throws { guard case .openApplication(let bundleIdentifier) = try functionCall.parseArgs() else { - throw ClientManagerError.appError("Invalid app state") + throw ApiError.appError("Invalid app state") } guard appInfo?.apps[bundleIdentifier] != nil else { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( "This app cannot be opened by Typeahead", functionCall: functionCall, appContext: appInfo?.appContext @@ -23,7 +23,7 @@ extension FunctionManager { } guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) else { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( "Failed to open \(bundleIdentifier)", functionCall: functionCall, appContext: appInfo?.appContext diff --git a/TypeaheadAI/Functions/FunctionManager+OpenFile.swift b/TypeaheadAI/Functions/FunctionManager+OpenFile.swift index 07dc709..a9068a5 100644 --- a/TypeaheadAI/Functions/FunctionManager+OpenFile.swift +++ b/TypeaheadAI/Functions/FunctionManager+OpenFile.swift @@ -13,7 +13,7 @@ extension FunctionManager { let appContext = appInfo?.appContext guard case .openFile(let file) = try functionCall.parseArgs() else { - throw ClientManagerError.appError("Invalid app state") + throw ApiError.appError("Invalid app state") } // Activate relevant app diff --git a/TypeaheadAI/Functions/FunctionManager+OpenURL.swift b/TypeaheadAI/Functions/FunctionManager+OpenURL.swift index 1666091..eae2cdb 100644 --- a/TypeaheadAI/Functions/FunctionManager+OpenURL.swift +++ b/TypeaheadAI/Functions/FunctionManager+OpenURL.swift @@ -11,11 +11,11 @@ import Foundation extension FunctionManager { func openURL(_ functionCall: FunctionCall, appInfo: AppInfo?) async throws { guard case .openURL(let url) = try functionCall.parseArgs() else { - throw ClientManagerError.appError("Invalid app state") + throw ApiError.appError("Invalid app state") } guard let url = URL(string: url) else { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( "URL not found", functionCall: functionCall, appContext: appInfo?.appContext diff --git a/TypeaheadAI/Functions/FunctionManager+PerformUIAction.swift b/TypeaheadAI/Functions/FunctionManager+PerformUIAction.swift index c2709f8..2b9b98e 100644 --- a/TypeaheadAI/Functions/FunctionManager+PerformUIAction.swift +++ b/TypeaheadAI/Functions/FunctionManager+PerformUIAction.swift @@ -22,11 +22,11 @@ extension FunctionManager: CanSimulateEnter, CanGetUIElements { guard case .performUIAction(let action) = try functionCall.parseArgs(), let elementMap = appInfo?.elementMap else { - throw ClientManagerError.appError("Invalid app state") + throw ApiError.appError("Invalid app state") } guard let axElement = elementMap[action.id] else { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( "No such element \(action.id)", functionCall: functionCall, appContext: appInfo?.appContext diff --git a/TypeaheadAI/Functions/FunctionManager+SaveFile.swift b/TypeaheadAI/Functions/FunctionManager+SaveFile.swift index 4200d29..f25c277 100644 --- a/TypeaheadAI/Functions/FunctionManager+SaveFile.swift +++ b/TypeaheadAI/Functions/FunctionManager+SaveFile.swift @@ -13,12 +13,12 @@ extension FunctionManager { let appContext = appInfo?.appContext guard case .saveFile(let id, let file) = try functionCall.parseArgs() else { - throw ClientManagerError.appError("Invalid app state") + throw ApiError.appError("Invalid app state") } guard let elementMap = appInfo?.elementMap, let savePanel = elementMap[id] else { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( "Failed to save file", functionCall: functionCall, appContext: appInfo?.appContext diff --git a/TypeaheadAI/Functions/FunctionManager+SetVOCursor.swift b/TypeaheadAI/Functions/FunctionManager+SetVOCursor.swift index 03100e0..e0088b2 100644 --- a/TypeaheadAI/Functions/FunctionManager+SetVOCursor.swift +++ b/TypeaheadAI/Functions/FunctionManager+SetVOCursor.swift @@ -13,12 +13,12 @@ extension FunctionManager { let appContext = appInfo?.appContext guard case .saveFile(let id, let file) = try functionCall.parseArgs() else { - throw ClientManagerError.appError("Invalid app state") + throw ApiError.appError("Invalid app state") } guard let elementMap = appInfo?.elementMap, let savePanel = elementMap[id] else { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( "Failed to save file", functionCall: functionCall, appContext: appInfo?.appContext diff --git a/TypeaheadAI/Functions/FunctionManager.swift b/TypeaheadAI/Functions/FunctionManager.swift index fd06adf..312fc73 100644 --- a/TypeaheadAI/Functions/FunctionManager.swift +++ b/TypeaheadAI/Functions/FunctionManager.swift @@ -61,28 +61,28 @@ struct FunctionCall: Codable, Equatable { let errorOpt = self.stringArg("error") if idOpt == nil, errorOpt == nil { - throw ClientManagerError.functionArgParsingError("Function is missing ID but doesn't have an error message.") + throw ApiError.functionArgParsingError("Function is missing ID but doesn't have an error message.") } return .focusUIElement(id: self.stringArg("id"), errorMessage: self.stringArg("error")) case .openApplication: guard let bundleIdentifier = self.stringArg("bundleIdentifier") else { - throw ClientManagerError.functionArgParsingError("Failed to open application...") + throw ApiError.functionArgParsingError("Failed to open application...") } return .openApplication(bundleIdentifier: bundleIdentifier) case .openFile: guard let file = self.stringArg("file") else { - throw ClientManagerError.functionArgParsingError("Failed to open file...") + throw ApiError.functionArgParsingError("Failed to open file...") } return .openFile(file: file) case .openURL: guard let url = self.stringArg("url") else { - throw ClientManagerError.functionArgParsingError("Failed to open file...") + throw ApiError.functionArgParsingError("Failed to open file...") } return .openURL(url: url) @@ -90,7 +90,7 @@ struct FunctionCall: Codable, Equatable { case .performUIAction: guard let id = self.stringArg("id"), let narration = self.stringArg("narration") else { - throw ClientManagerError.functionArgParsingError("Failed to perform UI action...") + throw ApiError.functionArgParsingError("Failed to perform UI action...") } return .performUIAction(action: Action( @@ -103,7 +103,7 @@ struct FunctionCall: Codable, Equatable { case .saveFile: guard let id = self.stringArg("id"), let file = self.stringArg("file") else { - throw ClientManagerError.functionArgParsingError("Failed to save file...") + throw ApiError.functionArgParsingError("Failed to save file...") } return .saveFile(id: id, file: file) @@ -124,7 +124,7 @@ class FunctionManager: CanFetchAppContext, func parse(jsonString: String) async throws -> FunctionCall { guard let jsonData = jsonString.data(using: .utf8), let functionCall = try? JSONDecoder().decode(FunctionCall.self, from: jsonData) else { - throw ClientManagerError.functionParsingError("Function could not be parsed: \(jsonString)") + throw ApiError.functionParsingError("Function could not be parsed: \(jsonString)") } return functionCall @@ -152,7 +152,7 @@ class FunctionManager: CanFetchAppContext, try Task.checkCancellation() guard let serializedUIElement = newTree?.serializeWithContext(appContext: newAppContext) else { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( "Failed to serialize UI state", functionCall: functionCall, appContext: appInfo?.appContext diff --git a/TypeaheadAI/Info.plist b/TypeaheadAI/Info.plist index 9a554c3..d8ea624 100644 --- a/TypeaheadAI/Info.plist +++ b/TypeaheadAI/Info.plist @@ -3,9 +3,9 @@ CFBundleShortVersionString - 2.1.27 + 2.1.29 CFBundleVersion - 7 + 9 SUFeedURL https://pub-aad921bb9ea949159b00d32ac16f8df8.r2.dev/appcast.xml SUPublicEDKey diff --git a/TypeaheadAI/Llama/LlamaModelManager.swift b/TypeaheadAI/Llama/LlamaModelManager.swift index 18ecda3..08930d3 100644 --- a/TypeaheadAI/Llama/LlamaModelManager.swift +++ b/TypeaheadAI/Llama/LlamaModelManager.swift @@ -173,7 +173,7 @@ class LlamaModelManager: ObservableObject { self.model = LlamaWrapper(url) guard let _ = self.model?.isLoaded() else { - throw ClientManagerError.modelNotLoaded("Model could not be loaded") + throw ApiError.modelNotLoaded("Model could not be loaded") } self.logger.info("model loaded successfully: \(url.lastPathComponent)") @@ -234,14 +234,14 @@ class LlamaModelManager: ObservableObject { streamHandler: @escaping (Result, AppInfo?) async -> Void ) async throws { guard let model = model else { - throw ClientManagerError.modelNotLoaded("Open Settings > Offline Mode to select a model for offline mode.") + throw ApiError.modelNotLoaded("Open Settings > Offline Mode to select a model for offline mode.") } var payloadCopy = payload payloadCopy.messages = [] guard let jsonPayload = encodeToJSONString(from: payloadCopy) else { - throw ClientManagerError.badRequest("Encoding error") + throw ApiError.badRequest("Encoding error") } var refinements = "" @@ -281,7 +281,7 @@ class LlamaModelManager: ObservableObject { await streamHandler(.success(token), nil) } } catch { - throw ClientManagerError.modelFailed(error.localizedDescription) + throw ApiError.modelFailed(error.localizedDescription) } } diff --git a/TypeaheadAI/Models/ApiError.swift b/TypeaheadAI/Models/ApiError.swift new file mode 100644 index 0000000..a34c1cb --- /dev/null +++ b/TypeaheadAI/Models/ApiError.swift @@ -0,0 +1,56 @@ +// +// ApiError.swift +// TypeaheadAI +// +// Created by Jeff Hara on 2/5/24. +// + +import Foundation + +enum ApiError: LocalizedError { + case badRequest(_ message: String) + case retriesExceeded(_ message: String) + case clientError(_ message: String) + case serverError(_ message: String) + case appError(_ message: String) + case networkError(_ message: String) + case corruptedDataError(_ message: String) + case signInRequired(_ message: String) + + // LLaMA errors + case modelNotFound(_ message: String) + case modelNotLoaded(_ message: String) + case modelDirectoryNotAuthorized(_ message: String) + case modelFailed(_ message: String) + + // FunctionManager errors + case functionParsingError(_ message: String) + case functionArgParsingError(_ message: String) + case functionCallError(_ message: String, functionCall: FunctionCall, appContext: AppContext?) + case functionOnFocusError(_ message: String) + + var errorDescription: String { + switch self { + case .badRequest(let message): return message + case .retriesExceeded(let message): return message + case .clientError(let message): return message + case .serverError(let message): return message + case .appError(let message): return message + case .networkError(let message): return message + case .corruptedDataError(let message): return message + case .signInRequired(let message): return message + + // LLaMA model errors + case .modelNotFound(let message): return message + case .modelNotLoaded(let message): return message + case .modelDirectoryNotAuthorized(let message): return message + case .modelFailed(let message): return message + + // FunctionManager errors + case .functionParsingError(let message): return message + case .functionArgParsingError(let message): return message + case .functionCallError(let message, _, _): return message + case .functionOnFocusError(let message): return message + } + } +} diff --git a/TypeaheadAI/Traits/CanExecuteScript.swift b/TypeaheadAI/Traits/CanExecuteScript.swift index b93deec..56aedab 100644 --- a/TypeaheadAI/Traits/CanExecuteScript.swift +++ b/TypeaheadAI/Traits/CanExecuteScript.swift @@ -8,12 +8,12 @@ import Foundation protocol CanExecuteScript { - func executeScript(script: String) async -> String? + func executeScript(script: String) async throws -> String } extension CanExecuteScript { - func executeScript(script: String) async -> String? { - await withCheckedContinuation { continuation in + func executeScript(script: String) async throws -> String { + try await withCheckedThrowingContinuation { continuation in let task = Process() let pipe = Pipe() @@ -23,15 +23,17 @@ extension CanExecuteScript { task.terminationHandler = { _ in let data = pipe.fileHandleForReading.readDataToEndOfFile() - let output = String(data: data, encoding: .utf8) - continuation.resume(returning: output) + if let output = String(data: data, encoding: .utf8) { + continuation.resume(returning: output) + } else { + continuation.resume(throwing: ApiError.appError("Failed to execute script")) + } } do { try task.run() } catch { - print("An error occurred: \(error)") - continuation.resume(returning: nil) + continuation.resume(throwing: ApiError.appError(error.localizedDescription)) } } } diff --git a/TypeaheadAI/Traits/CanFocusOnElement.swift b/TypeaheadAI/Traits/CanFocusOnElement.swift index 0608c55..7219e90 100644 --- a/TypeaheadAI/Traits/CanFocusOnElement.swift +++ b/TypeaheadAI/Traits/CanFocusOnElement.swift @@ -32,7 +32,7 @@ extension CanFocusOnElement { try await Task.safeSleep(for: .milliseconds(100)) guard result == .success else { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( "Action failed (code: \(result?.rawValue ?? -1))", functionCall: functionCall, appContext: appContext diff --git a/TypeaheadAI/Traits/CanSetVOFocus.swift b/TypeaheadAI/Traits/CanSetVOFocus.swift index a364eb9..557c0da 100644 --- a/TypeaheadAI/Traits/CanSetVOFocus.swift +++ b/TypeaheadAI/Traits/CanSetVOFocus.swift @@ -17,14 +17,14 @@ extension CanSetVOFocus { func focusVO(on axElement: AXUIElement, functionCall: FunctionCall, appContext: AppContext?) async throws { guard NSWorkspace.shared.isVoiceOverEnabled else { - throw ClientManagerError.functionCallError("VoiceOver must be enabled", functionCall: functionCall, appContext: appContext) + throw ApiError.functionCallError("VoiceOver must be enabled", functionCall: functionCall, appContext: appContext) } _ = AXUIElementPerformAction(axElement, "AXScrollToVisible" as CFString) try await Task.safeSleep(for: .milliseconds(100)) guard let center = axElement.getCenter() else { - throw ClientManagerError.functionCallError("Failed to focus on element", functionCall: functionCall, appContext: appContext) + throw ApiError.functionCallError("Failed to focus on element", functionCall: functionCall, appContext: appContext) } // Move the mouse to the center of the element diff --git a/TypeaheadAI/Views/Settings/GeneralSettingsView.swift b/TypeaheadAI/Views/Settings/GeneralSettingsView.swift index 49d7481..d626a83 100644 --- a/TypeaheadAI/Views/Settings/GeneralSettingsView.swift +++ b/TypeaheadAI/Views/Settings/GeneralSettingsView.swift @@ -19,6 +19,7 @@ struct GeneralSettingsView: View { @AppStorage("isAutopilotEnabled") private var isAutopilotEnabled: Bool = true @AppStorage("hideModalDuringAutopilot") private var hideModalDuringAutopilot: Bool = true @AppStorage("isNarrateEnabled") private var isNarrateEnabled: Bool = true + @AppStorage("isDebugModeEnabled") private var isDebugModeEnabled: Bool = false var body: some View { VStack(alignment: .leading) { @@ -103,6 +104,10 @@ struct GeneralSettingsView: View { Text("Enable Dictation") } Spacer() + Toggle(isOn: $isDebugModeEnabled) { + Text("Enable Debug Mode") + } + Spacer() } HStack { diff --git a/TypeaheadAI/WindowManagers/ModalManager.swift b/TypeaheadAI/WindowManagers/ModalManager.swift index 490a0b3..0add6bc 100644 --- a/TypeaheadAI/WindowManagers/ModalManager.swift +++ b/TypeaheadAI/WindowManagers/ModalManager.swift @@ -671,27 +671,17 @@ class ModalManager: ObservableObject, CanSimulateDictation { currentTask = Task { do { try await reply(quickAction: quickAction) - } catch let error as ClientManagerError { + } catch let error as ApiError { switch error { - case .badRequest(let message): - self.setError(message, appContext: appContext) - case .serverError(let message): - self.setError(message, appContext: appContext) - case .clientError(let message): - self.setError(message, appContext: appContext) - case .modelNotFound(let message): - self.setError(message, appContext: appContext) - case .modelNotLoaded(let message): - self.setError(message, appContext: appContext) - case .functionParsingError(let message): - self.setError(message, appContext: appContext) - case .functionArgParsingError(let message): - self.setError(message, appContext: appContext) case .functionCallError(let message, let functionCall, let appContext): self.showModal() self.appendToolError(message, functionCall: functionCall, appContext: appContext) default: - self.setError("Something went wrong. \(error.errorDescription)", appContext: appContext) + if !self.isVisible { + self.showModal() + } + + self.setError(error.errorDescription, appContext: appContext) } } catch _ as CancellationError { if !self.isVisible { @@ -725,7 +715,7 @@ class ModalManager: ObservableObject, CanSimulateDictation { if case .focus = self.messages.first?.messageContext { let startTime = Date() guard let (functionCall, appInfo) = try await self.clientManager?.focus(messages: self.messages) else { - throw ClientManagerError.functionParsingError("Could not parse function payload.") + throw ApiError.functionParsingError("Could not parse function payload.") } if self.isVisible { @@ -761,7 +751,7 @@ class ModalManager: ObservableObject, CanSimulateDictation { // Handle Function payloads guard let jsonString = bufferedPayload.text, let functionCall = try await functionManager?.parse(jsonString: jsonString) else { - throw ClientManagerError.functionParsingError("Could not parse function payload.") + throw ApiError.functionParsingError("Could not parse function payload.") } // Parse arguments and update UI with human readable description. @@ -783,7 +773,7 @@ class ModalManager: ObservableObject, CanSimulateDictation { try Task.checkCancellation() guard let serialized = newAppInfo?.appContext?.serializedUIElement else { - throw ClientManagerError.functionCallError( + throw ApiError.functionCallError( "Failed to get updated state", functionCall: functionCall, appContext: appInfo?.appContext @@ -822,7 +812,7 @@ class ModalManager: ObservableObject, CanSimulateDictation { currentTask = Task { do { try await reply() - } catch let error as ClientManagerError { + } catch let error as ApiError { switch error { case .badRequest(let message): self.setError(message, appContext: nil) @@ -883,7 +873,7 @@ class ModalManager: ObservableObject, CanSimulateDictation { currentTask = Task { do { try await reply() - } catch let error as ClientManagerError { + } catch let error as ApiError { switch error { case .badRequest(let message): self.setError(message, appContext: nil)