diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..a5692cc --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,18 @@ +# This is a comment. Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @global-owner1 and @global-owner2 will be requested for +# review when someone opens a pull request. +* @shanthaneddula + +# Order is important; the last matching pattern takes the most +# precedence. When someone opens a pull request that only +# modifies JS files, only @js-owner and not the global +# owner(s) will be requested for a review. +*.swift @shanthaneddula + +# You can also use email addresses if you prefer. They'll be +# used to look up users just like we do for commit author +# emails. +# *.swift user@example.com \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d963052 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: 🐛 Bug Report +about: Create a report to help us improve +title: '[BUG] ' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment:** + - OS: [e.g. macOS 14.0] + - Swift Version: [e.g. 5.9] + - Xcode Version: [e.g. 15.0] + - Device: [e.g. MacBook Pro 2023] + +**Additional context** +Add any other context about the problem here. + +**Logs** +If applicable, please share relevant logs or error messages. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..beb08d7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: GitHub Community Support + url: https://github.com/orgs/community/discussions + about: Please ask and answer questions here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..1f792d8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: ✨ Feature Request +about: Suggest an idea for this project +title: '[FEATURE] ' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. + +**Implementation Notes** +If you have any specific implementation ideas or requirements, please describe them here. \ No newline at end of file diff --git a/.github/branch-protection.yml b/.github/branch-protection.yml new file mode 100644 index 0000000..66cf7d7 --- /dev/null +++ b/.github/branch-protection.yml @@ -0,0 +1,38 @@ +# Branch Protection Rules + +branches: + - name: main + protection: + required_status_checks: + strict: true + contexts: + - "Build and Test" + - "Code Coverage" + required_pull_request_reviews: + required_approving_review_count: 1 + dismiss_stale_reviews: true + require_code_owner_reviews: true + enforce_admins: true + required_linear_history: true + allow_force_pushes: false + allow_deletions: false + block_creations: true + required_conversation_resolution: true + + - name: dev + protection: + required_status_checks: + strict: true + contexts: + - "Build and Test" + - "Code Coverage" + required_pull_request_reviews: + required_approving_review_count: 1 + dismiss_stale_reviews: true + require_code_owner_reviews: false + enforce_admins: true + required_linear_history: true + allow_force_pushes: false + allow_deletions: false + block_creations: true + required_conversation_resolution: true \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..4f62161 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,27 @@ +## Description +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. + +Fixes # (issue) + +## Type of change +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +## How Has This Been Tested? +Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. + +## Checklist: +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes + +## Additional Notes +Add any additional notes or screenshots here. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e69de29..7094124 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +jobs: + build: + name: Build and Test + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Swift + uses: fwal/setup-swift@v1 + with: + swift-version: "5.9" + + - name: Build + run: swift build -v + + - name: Run Tests + run: swift test -v + + - name: Run SwiftLint + run: | + brew install swiftlint + swiftlint lint --reporter codeclimate-logger | tee swiftlint-report.json + + - name: Upload SwiftLint Report + uses: actions/upload-artifact@v4 + with: + name: swiftlint-report + path: swiftlint-report.json + retention-days: 7 + + codecov: + name: Upload Coverage + needs: build + runs-on: macos-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Swift + uses: fwal/setup-swift@v1 + with: + swift-version: "5.9" + + - name: Install Codecov + run: | + brew install codecov + + - name: Generate Coverage + run: | + swift test --enable-code-coverage + + - name: Upload Coverage + run: | + xcrun llvm-cov export -format="lcov" .build/debug/MinimalAIChatPackageTests.xctest/Contents/MacOS/MinimalAIChatPackageTests > coverage.lcov + codecov -f coverage.lcov -B main diff --git a/.github/workflows/dependency-update.yml b/.github/workflows/dependency-update.yml new file mode 100644 index 0000000..219f404 --- /dev/null +++ b/.github/workflows/dependency-update.yml @@ -0,0 +1,41 @@ +name: Dependency Update + +on: + schedule: + - cron: '0 0 * * 0' # Run weekly on Sunday at midnight + +jobs: + update: + name: Update Dependencies + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Swift + uses: fwal/setup-swift@v1 + with: + swift-version: "5.9" + + - name: Update Dependencies + run: | + swift package update + if git diff --quiet Package.resolved; then + echo "No dependency updates available" + else + echo "Dependencies have updates available" + git diff Package.resolved + fi + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + commit-message: "chore: update dependencies" + title: "chore: update dependencies" + body: | + Automated dependency update. + + This PR updates the project dependencies to their latest versions. + + Please review the changes and merge if appropriate. + branch: chore/dependency-update + delete-branch: true \ No newline at end of file diff --git a/.github/workflows/dependency-updates.yml b/.github/workflows/dependency-updates.yml new file mode 100644 index 0000000..5077c3c --- /dev/null +++ b/.github/workflows/dependency-updates.yml @@ -0,0 +1,47 @@ +name: Dependency Updates + +on: + schedule: + - cron: '0 0 * * 0' # Run weekly on Sunday + +jobs: + update-dependencies: + name: Update Dependencies + runs-on: macos-14 + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up Swift + uses: fwal/setup-swift@v1 + with: + swift-version: "5.9" + + - name: Update Dependencies + run: | + swift package update + if git diff --quiet Package.resolved; then + echo "No dependency updates available" + else + echo "Dependencies have updates available" + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add Package.resolved + git commit -m "Update dependencies" + git push + fi + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + commit-message: "Update dependencies" + title: "Update dependencies" + body: | + Automated dependency updates. + + This PR was created automatically by the dependency update workflow. + branch: dependency-updates + delete-branch: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e69de29..db313dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: Release + +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +jobs: + build: + name: Build Release + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Swift + uses: fwal/setup-swift@v1 + with: + swift-version: "5.9" + + - name: Build + run: swift build -c release + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref_name }} + draft: false + prerelease: false + + - name: Upload Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: .build/release/MinimalAIChat + asset_name: MinimalAIChat-${{ github.ref_name }} + asset_content_type: application/octet-stream diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..4408def --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +5.9 \ No newline at end of file diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..ffe05bf --- /dev/null +++ b/.swiftformat @@ -0,0 +1,14 @@ +--indent 4 +--allman false +--wraparguments before-first +--wrapcollections before-first +--closingparen same-line +--commas always +--comments indent +--semicolons never +--trimwhitespace always +--header strip +--maxwidth 120 +--wrapparameters before-first +--importgrouping testable-bottom +--xcodeindentation enabled \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index d5fd886..56f7f76 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,74 +1,81 @@ disabled_rules: - trailing_whitespace - - line_length + - function_body_length + - file_length + - type_body_length + - cyclomatic_complexity + - force_cast + - force_try + - force_unwrapping + - todo + opt_in_rules: - empty_count - missing_docs - force_unwrapping - - force_cast - - force_try - - todo - - notification_center_detachment - - legacy_random - - legacy_cg_graphics_functions - - legacy_constant - - legacy_nsgeometry_functions - - yoda_condition - - nimble_operator - - operator_usage_whitespace + - empty_string + - closure_spacing + - explicit_init - overridden_super_call - - prohibited_super_call - redundant_nil_coalescing - private_outlet - - prohibited_iboutlet - - custom_rules + - prohibited_super_call + - redundant_type_annotation + - sorted_imports + - strict_fileprivate + - toggle_bool + - unowned_variable_capture + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - yoda_condition + +included: + - Sources + - Tests + +excluded: + - Resources + - Documentation + - Archived line_length: warning: 120 error: 200 -type_body_length: - warning: 300 - error: 400 +type_name: + min_length: 3 + max_length: 50 -file_length: - warning: 500 - error: 1000 +identifier_name: + min_length: 2 + max_length: 40 function_body_length: warning: 50 error: 100 +file_length: + warning: 500 + error: 1000 + cyclomatic_complexity: warning: 10 error: 20 -reporter: "xcode" +function_parameter_count: + warning: 6 + error: 8 -included: - - App - - Tests - -excluded: - - Pods - - Tests/Performance - - Tests/UI - - Tests/Integration +large_tuple: + warning: 3 + error: 4 -line_length: - ignores_comments: true - ignores_urls: true - ignores_function_declarations: true - ignores_interpolated_strings: true +type_body_length: + warning: 300 + error: 400 -custom_rules: - no_direct_standard_out_logs: - name: "Print Usage" - regex: "(print|NSLog)\\(" - message: "Prefer using a logging framework over print or NSLog" - severity: warning - comments_space: - name: "Space After Comment" - regex: "//[^\\s]" - message: "There should be a space after //" - severity: warning \ No newline at end of file +nesting: + type_level: + warning: 3 + error: 4 \ No newline at end of file diff --git a/App/Core/AppDelegate.swift b/Archived/v1/code/App/Core/AppDelegate.swift similarity index 87% rename from App/Core/AppDelegate.swift rename to Archived/v1/code/App/Core/AppDelegate.swift index d0432cd..3d90812 100644 --- a/App/Core/AppDelegate.swift +++ b/Archived/v1/code/App/Core/AppDelegate.swift @@ -7,28 +7,28 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var spotlightIndexer: SpotlightIndexer? private var universalLinkRouter: UniversalLinkRouter? private var hotkeyManager: HotkeyManager? - - func applicationDidFinishLaunching(_ notification: Notification) { + + func applicationDidFinishLaunching(_: Notification) { // Initialize components deepLinkHandler = DeepLinkHandler() spotlightIndexer = SpotlightIndexer() universalLinkRouter = UniversalLinkRouter() hotkeyManager = HotkeyManager() - + // Setup hotkey setupGlobalHotkey() - + // Setup memory optimization setupMemoryOptimization() } - - func applicationWillTerminate(_ notification: Notification) { + + func applicationWillTerminate(_: Notification) { // Clean up resources hotkeyManager?.unregisterAllHotkeys() } - + // Handle URL schemes - func application(_ application: NSApplication, open urls: [URL]) { + func application(_: NSApplication, open urls: [URL]) { for url in urls { if url.scheme == Constants.appURLScheme { deepLinkHandler?.handleURL(url) @@ -37,9 +37,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } } - + // MARK: - Private Methods - + private func setupGlobalHotkey() { // Register default hotkey let defaultKeyCombo = KeyCombo(keyCode: 49, modifiers: [.command, .shift]) // Space + Cmd + Shift @@ -47,7 +47,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { self?.toggleMainWindow() } } - + private func setupMemoryOptimization() { // Setup memory pressure observer let memoryOptimizer = MemoryOptimizer() @@ -58,7 +58,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } pressureObserver.startObserving() } - + private func toggleMainWindow() { // Toggle main window visibility WindowManager.shared.toggleMainWindow() diff --git a/App/Core/AppMain.swift b/Archived/v1/code/App/Core/AppMain.swift similarity index 99% rename from App/Core/AppMain.swift rename to Archived/v1/code/App/Core/AppMain.swift index 116919c..c0288bd 100644 --- a/App/Core/AppMain.swift +++ b/Archived/v1/code/App/Core/AppMain.swift @@ -2,7 +2,7 @@ import SwiftUI struct MinimalAIChatApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - + var body: some Scene { WindowGroup { MainChatView() diff --git a/App/Core/Constants.swift b/Archived/v1/code/App/Core/Constants.swift similarity index 86% rename from App/Core/Constants.swift rename to Archived/v1/code/App/Core/Constants.swift index 52cf144..f486416 100644 --- a/App/Core/Constants.swift +++ b/Archived/v1/code/App/Core/Constants.swift @@ -1,50 +1,50 @@ import Foundation -struct Constants { +enum Constants { // App information static let appName = "MinimalAIChat" static let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" static let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1" - + // URL schemes static let appURLScheme = "minimalai" static let appUniversalLinkDomain = "app.minimalai.chat" - + // API endpoints static let apiBaseURL = "https://api.minimalai.chat" static let subscriptionValidationURL = "\(apiBaseURL)/validate-receipt" - + // Feature flags static let isDebugMode = false #if DEBUG - static let isTestEnvironment = true + static let isTestEnvironment = true #else - static let isTestEnvironment = false + static let isTestEnvironment = false #endif - + // Default settings - struct Defaults { + enum Defaults { static let launchAtLogin = true static let memoryOptimizationEnabled = true static let privacyConsentRequired = true } - + // Notification names - struct Notifications { + enum Notifications { static let subscriptionStatusChanged = Notification.Name("com.minimalai.subscriptionStatusChanged") static let memoryPressureWarning = Notification.Name("com.minimalai.memoryPressureWarning") } - + // UserDefaults keys - struct UserDefaultsKeys { + enum UserDefaultsKeys { static let hasCompletedOnboarding = "hasCompletedOnboarding" static let hasAcceptedPrivacyPolicy = "hasAcceptedPrivacyPolicy" static let customHotkeyCombo = "customHotkeyCombo" static let subscriptionTier = "subscriptionTier" } - + // App Store - struct AppStore { + enum AppStore { static let appID = "1234567890" static let monthlySubscriptionID = "com.minimalai.subscription.monthly" static let yearlySubscriptionID = "com.minimalai.subscription.yearly" diff --git a/App/Core/Handlers/DeepLinkHandler.swift b/Archived/v1/code/App/Core/Handlers/DeepLinkHandler.swift similarity index 93% rename from App/Core/Handlers/DeepLinkHandler.swift rename to Archived/v1/code/App/Core/Handlers/DeepLinkHandler.swift index 2a964fb..16a017e 100644 --- a/App/Core/Handlers/DeepLinkHandler.swift +++ b/Archived/v1/code/App/Core/Handlers/DeepLinkHandler.swift @@ -4,9 +4,9 @@ import Foundation @MainActor public final class DeepLinkHandler: Sendable { private let logger = Logger(label: "com.minimalaichat.deeplink") - + public init() {} - + /// Handles a deep link URL /// - Parameter url: The URL to handle public func handleURL(_ url: URL) async { @@ -14,10 +14,10 @@ public final class DeepLinkHandler: Sendable { logger.error("Invalid URL: \(url)") return } - + // Parse the path components let pathComponents = components.path.split(separator: "/").map(String.init) - + // Handle different deep link paths switch pathComponents.first { case "chat": @@ -28,8 +28,8 @@ public final class DeepLinkHandler: Sendable { logger.warning("Unknown deep link path: \(pathComponents.first ?? "nil")") } } - - private func handleChatDeepLink(pathComponents: [String], queryItems: [URLQueryItem]?) async { + + private func handleChatDeepLink(pathComponents: [String], queryItems _: [URLQueryItem]?) async { // Handle chat-specific deep links if pathComponents.count > 1 { let chatId = pathComponents[1] @@ -37,8 +37,8 @@ public final class DeepLinkHandler: Sendable { logger.info("Opening chat with ID: \(chatId)") } } - - private func handleSettingsDeepLink(pathComponents: [String], queryItems: [URLQueryItem]?) async { + + private func handleSettingsDeepLink(pathComponents: [String], queryItems _: [URLQueryItem]?) async { // Handle settings-specific deep links if pathComponents.count > 1 { let section = pathComponents[1] @@ -46,4 +46,4 @@ public final class DeepLinkHandler: Sendable { logger.info("Opening settings section: \(section)") } } -} \ No newline at end of file +} diff --git a/App/Core/HotkeyUIAlerts.swift b/Archived/v1/code/App/Core/HotkeyUIAlerts.swift similarity index 98% rename from App/Core/HotkeyUIAlerts.swift rename to Archived/v1/code/App/Core/HotkeyUIAlerts.swift index 291df42..1ff5c8b 100644 --- a/App/Core/HotkeyUIAlerts.swift +++ b/Archived/v1/code/App/Core/HotkeyUIAlerts.swift @@ -1,13 +1,13 @@ -import Cocoa -import Carbon import AppKit +import Carbon +import Cocoa /// Extension for NSEvent.ModifierFlags to add Carbon flags support extension NSEvent.ModifierFlags { /// Convert to Carbon modifier flags var carbonFlags: UInt32 { var carbonFlags: UInt32 = 0 - + if contains(.command) { carbonFlags |= UInt32(cmdKey) } @@ -20,7 +20,7 @@ extension NSEvent.ModifierFlags { if contains(.shift) { carbonFlags |= UInt32(shiftKey) } - + return carbonFlags } } @@ -37,7 +37,7 @@ class HotkeyUIAlerts { alert.addButton(withTitle: "OK") alert.runModal() } - + /// Show an alert to guide the user to grant accessibility permissions static func showAccessibilityPermissionsNeeded() { let alert = NSAlert() @@ -46,7 +46,7 @@ class HotkeyUIAlerts { alert.alertStyle = .informational alert.addButton(withTitle: "Open System Preferences") alert.addButton(withTitle: "Later") - + if alert.runModal() == .alertFirstButtonReturn { let prefpaneURL = URL(fileURLWithPath: "/System/Library/PreferencePanes/Security.prefPane") NSWorkspace.shared.open(prefpaneURL) diff --git a/App/Core/LaunchAgentService.swift b/Archived/v1/code/App/Core/LaunchAgentService.swift similarity index 73% rename from App/Core/LaunchAgentService.swift rename to Archived/v1/code/App/Core/LaunchAgentService.swift index 99d8c68..c368731 100644 --- a/App/Core/LaunchAgentService.swift +++ b/Archived/v1/code/App/Core/LaunchAgentService.swift @@ -4,21 +4,21 @@ import Foundation @MainActor class LaunchAgentService { static let shared = LaunchAgentService() - + private let launchAgentFileName = "com.minimalai.chat.plist" private var launchAgentFileURL: URL? { let libraryURL = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first return libraryURL?.appendingPathComponent("LaunchAgents").appendingPathComponent(launchAgentFileName) } - + private init() {} - + /// Check if the app is set to launch at login var isLaunchAtLoginEnabled: Bool { guard let launchAgentFileURL = launchAgentFileURL else { return false } return FileManager.default.fileExists(atPath: launchAgentFileURL.path) } - + /// Enable or disable launch at login func setLaunchAtLogin(enabled: Bool) -> Bool { if enabled { @@ -27,14 +27,15 @@ class LaunchAgentService { return disableLaunchAtLogin() } } - + /// Enable launch at login by creating a launch agent plist private func enableLaunchAtLogin() -> Bool { guard let launchAgentFileURL = launchAgentFileURL, - let appPath = Bundle.main.bundleURL.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { + let appPath = Bundle.main.bundleURL.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) + else { return false } - + // Create LaunchAgents directory if it doesn't exist let launchAgentsDirURL = launchAgentFileURL.deletingLastPathComponent() if !FileManager.default.fileExists(atPath: launchAgentsDirURL.path) { @@ -45,25 +46,25 @@ class LaunchAgentService { return false } } - + // Create launch agent plist content let plistContent = "\n" + - "\n" + - "\n" + - "\n" + - "\tLabel\n" + - "\tcom.minimalai.chat\n" + - "\tProgramArguments\n" + - "\t\n" + - "\t\t\(appPath)\n" + - "\t\n" + - "\tRunAtLoad\n" + - "\t\n" + - "\tKeepAlive\n" + - "\t\n" + - "\n" + - "" - + "\n" + + "\n" + + "\n" + + "\tLabel\n" + + "\tcom.minimalai.chat\n" + + "\tProgramArguments\n" + + "\t\n" + + "\t\t\(appPath)\n" + + "\t\n" + + "\tRunAtLoad\n" + + "\t\n" + + "\tKeepAlive\n" + + "\t\n" + + "\n" + + "" + do { try plistContent.write(to: launchAgentFileURL, atomically: true, encoding: .utf8) return true @@ -72,14 +73,15 @@ class LaunchAgentService { return false } } - + /// Disable launch at login by removing the launch agent plist private func disableLaunchAtLogin() -> Bool { guard let launchAgentFileURL = launchAgentFileURL, - FileManager.default.fileExists(atPath: launchAgentFileURL.path) else { + FileManager.default.fileExists(atPath: launchAgentFileURL.path) + else { return true // Already disabled } - + do { try FileManager.default.removeItem(at: launchAgentFileURL) return true diff --git a/App/Core/Managers/Hotkey/HotKey.swift b/Archived/v1/code/App/Core/Managers/Hotkey/HotKey.swift similarity index 89% rename from App/Core/Managers/Hotkey/HotKey.swift rename to Archived/v1/code/App/Core/Managers/Hotkey/HotKey.swift index 78dcbbb..4bb83a4 100644 --- a/App/Core/Managers/Hotkey/HotKey.swift +++ b/Archived/v1/code/App/Core/Managers/Hotkey/HotKey.swift @@ -1,5 +1,5 @@ -import Foundation import Carbon +import Foundation /// A class that manages a global hotkey @MainActor @@ -8,15 +8,15 @@ public final class HotKey: Sendable { private let handler: @Sendable () -> Void private var hotKeyRef: EventHotKeyRef? private let hotKeyID: EventHotKeyID - + public init(keyCombo: KeyCombo, handler: @Sendable @escaping () -> Void) { self.keyCombo = keyCombo self.handler = handler - self.hotKeyID = EventHotKeyID() - self.hotKeyID.signature = OSType(fourCharCode("MACH")) - self.hotKeyID.id = UInt32.random(in: 1...UInt32.max) + hotKeyID = EventHotKeyID() + hotKeyID.signature = OSType(fourCharCode("MACH")) + hotKeyID.id = UInt32.random(in: 1 ... UInt32.max) } - + public func register() throws { // Register the hotkey with Carbon let status = RegisterEventHotKey( @@ -27,11 +27,11 @@ public final class HotKey: Sendable { 0, &hotKeyRef ) - + guard status == noErr else { throw HotKeyError.registrationFailed } - + // Register the event handler try HotKeysController.shared.registerHandler(for: hotKeyID) { [weak self] in Task { @MainActor in @@ -39,7 +39,7 @@ public final class HotKey: Sendable { } } } - + public func unregister() { if let hotKeyRef = hotKeyRef { UnregisterEventHotKey(hotKeyRef) @@ -47,7 +47,7 @@ public final class HotKey: Sendable { } HotKeysController.shared.unregisterHandler(for: hotKeyID) } - + deinit { unregister() } @@ -56,7 +56,7 @@ public final class HotKey: Sendable { /// Errors that can occur during hotkey operations public enum HotKeyError: LocalizedError { case registrationFailed - + public var errorDescription: String? { switch self { case .registrationFailed: @@ -66,10 +66,11 @@ public enum HotKeyError: LocalizedError { } // MARK: - String Extension for OSType + private extension String { var fourCharCodeValue: UInt32 { var result: UInt32 = 0 - let chars = self.utf8 + let chars = utf8 var index = 0 for char in chars { guard index < 4 else { break } @@ -78,4 +79,4 @@ private extension String { } return result } -} \ No newline at end of file +} diff --git a/App/Core/Managers/Hotkey/HotKeysController.swift b/Archived/v1/code/App/Core/Managers/Hotkey/HotKeysController.swift similarity index 93% rename from App/Core/Managers/Hotkey/HotKeysController.swift rename to Archived/v1/code/App/Core/Managers/Hotkey/HotKeysController.swift index 7512656..5cd8175 100644 --- a/App/Core/Managers/Hotkey/HotKeysController.swift +++ b/Archived/v1/code/App/Core/Managers/Hotkey/HotKeysController.swift @@ -1,27 +1,27 @@ -import Foundation import Carbon +import Foundation /// A controller that manages global hotkeys @MainActor public final class HotKeysController: Sendable { public static let shared = HotKeysController() - + private var eventHandlerRef: EventHandlerRef? private var handlers: [EventHotKeyID: @Sendable () -> Void] = [:] - + private init() { setupEventHandler() } - + private func setupEventHandler() { var eventType = EventTypeSpec( eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed) ) - + let status = InstallEventHandler( GetApplicationEventTarget(), - { (_, event, _) -> OSStatus in + { _, event, _ -> OSStatus in var hotkeyID = EventHotKeyID() let err = GetEventParameter( event, @@ -32,9 +32,9 @@ public final class HotKeysController: Sendable { nil, &hotkeyID ) - + guard err == noErr else { return err } - + HotKeysController.shared.handleHotKey(hotkeyID) return noErr }, @@ -43,29 +43,29 @@ public final class HotKeysController: Sendable { nil, &eventHandlerRef ) - + guard status == noErr else { fatalError("Failed to install event handler") } } - + func registerHandler(for hotKeyID: EventHotKeyID, handler: @Sendable @escaping () -> Void) { handlers[hotKeyID] = handler } - + func unregisterHandler(for hotKeyID: EventHotKeyID) { handlers.removeValue(forKey: hotKeyID) } - + private func handleHotKey(_ hotKeyID: EventHotKeyID) { if let handler = handlers[hotKeyID] { handler() } } - + deinit { if let eventHandlerRef = eventHandlerRef { RemoveEventHandler(eventHandlerRef) } } -} \ No newline at end of file +} diff --git a/App/Core/Managers/Hotkey/KeyCombo.swift b/Archived/v1/code/App/Core/Managers/Hotkey/KeyCombo.swift similarity index 97% rename from App/Core/Managers/Hotkey/KeyCombo.swift rename to Archived/v1/code/App/Core/Managers/Hotkey/KeyCombo.swift index 8476b95..3f00aa7 100644 --- a/App/Core/Managers/Hotkey/KeyCombo.swift +++ b/Archived/v1/code/App/Core/Managers/Hotkey/KeyCombo.swift @@ -1,20 +1,20 @@ -import Foundation import Carbon +import Foundation /// Represents a key combination for global hotkeys public struct KeyCombo: Codable, Equatable { public let key: KeyCode public let modifiers: Set - + public init(key: KeyCode, modifiers: Set = []) { self.key = key self.modifiers = modifiers } - + var carbonKeyCode: UInt32 { switch key { case .space: return 0x31 - case .`return`: return 0x24 + case .return: return 0x24 case .tab: return 0x30 case .escape: return 0x35 case .delete: return 0x33 @@ -44,7 +44,7 @@ public struct KeyCombo: Codable, Equatable { case .f20: return 0x5A } } - + var carbonModifiers: UInt32 { var modifiers: UInt32 = 0 for modifier in self.modifiers { @@ -84,4 +84,4 @@ public enum KeyModifier: String, Codable, Hashable { case shift case option case control -} \ No newline at end of file +} diff --git a/App/Core/Managers/KeychainManager.swift b/Archived/v1/code/App/Core/Managers/KeychainManager.swift similarity index 88% rename from App/Core/Managers/KeychainManager.swift rename to Archived/v1/code/App/Core/Managers/KeychainManager.swift index 356f2e5..db0d7ff 100644 --- a/App/Core/Managers/KeychainManager.swift +++ b/Archived/v1/code/App/Core/Managers/KeychainManager.swift @@ -4,9 +4,9 @@ import Security /// Manages secure storage of API keys in the keychain public actor KeychainManager { private let service = "com.minimalaichat.keychain" - + public init() {} - + /// Retrieves an API key for the specified service /// - Parameter service: The AI service type /// - Returns: The stored API key @@ -16,21 +16,22 @@ public actor KeychainManager { kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.service, kSecAttrAccount as String: service.rawValue, - kSecReturnData as String: true + kSecReturnData as String: true, ] - + var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) - + guard status == errSecSuccess, let data = result as? Data, - let key = String(data: data, encoding: .utf8) else { + let key = String(data: data, encoding: .utf8) + else { throw KeychainError.keyNotFound } - + return key } - + /// Stores an API key for the specified service /// - Parameters: /// - key: The API key to store @@ -40,28 +41,28 @@ public actor KeychainManager { guard let data = key.data(using: .utf8) else { throw KeychainError.invalidData } - + let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.service, kSecAttrAccount as String: service.rawValue, - kSecValueData as String: data + kSecValueData as String: data, ] - + let status = SecItemAdd(query as CFDictionary, nil) - + if status == errSecDuplicateItem { // Update existing item let updateQuery: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.service, - kSecAttrAccount as String: service.rawValue + kSecAttrAccount as String: service.rawValue, ] - + let attributes: [String: Any] = [ - kSecValueData as String: data + kSecValueData as String: data, ] - + let updateStatus = SecItemUpdate(updateQuery as CFDictionary, attributes as CFDictionary) guard updateStatus == errSecSuccess else { throw KeychainError.saveFailed @@ -70,7 +71,7 @@ public actor KeychainManager { throw KeychainError.saveFailed } } - + /// Deletes an API key for the specified service /// - Parameter service: The AI service type /// - Throws: KeychainError if the key cannot be deleted @@ -78,9 +79,9 @@ public actor KeychainManager { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: self.service, - kSecAttrAccount as String: service.rawValue + kSecAttrAccount as String: service.rawValue, ] - + let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.deleteFailed @@ -94,7 +95,7 @@ public enum KeychainError: LocalizedError { case invalidData case saveFailed case deleteFailed - + public var errorDescription: String? { switch self { case .keyNotFound: @@ -107,4 +108,4 @@ public enum KeychainError: LocalizedError { return "Failed to delete API key from keychain" } } -} \ No newline at end of file +} diff --git a/App/Core/Managers/SessionManager.swift b/Archived/v1/code/App/Core/Managers/SessionManager.swift similarity index 74% rename from App/Core/Managers/SessionManager.swift rename to Archived/v1/code/App/Core/Managers/SessionManager.swift index 204fe91..968139e 100644 --- a/App/Core/Managers/SessionManager.swift +++ b/Archived/v1/code/App/Core/Managers/SessionManager.swift @@ -1,7 +1,7 @@ import Foundation -import WebKit import os.log import Security +import WebKit /// A manager class that handles session management and authentication for AI services /// @@ -40,19 +40,19 @@ final class SessionManager { private let service: WebViewManager.AIService private let keychain: KeychainManager private let logger = Logger(subsystem: "com.minimalaichat", category: "SessionManager") - + var isSessionValid: Bool { get async { - await validateSession(for: self.service) + await validateSession(for: service) } } - + init(service: WebViewManager.AIService) { self.service = service - self.keychain = KeychainManager() - self.logger.debug("Initialized SessionManager for service: \(service.name)") + keychain = KeychainManager() + logger.debug("Initialized SessionManager for service: \(service.name)") } - + /// Validates the current session for the specified service /// /// This method checks if the current session is valid by examining @@ -61,15 +61,15 @@ final class SessionManager { /// - Parameter service: The service to validate the session for /// - Returns: Whether the session is valid func validateSession(for service: WebViewManager.AIService) async -> Bool { - self.logger.debug("Validating session for service: \(service.name)") - + logger.debug("Validating session for service: \(service.name)") + do { // Check for stored session data - guard let sessionData = try? await self.keychain.getData(for: "session.\(service.rawValue)") else { - self.logger.info("No session data found for service: \(service.name)") + guard let sessionData = try? await keychain.getData(for: "session.\(service.rawValue)") else { + logger.info("No session data found for service: \(service.name)") return false } - + // Validate session data based on service let isValid = switch service { case .claude: @@ -79,78 +79,78 @@ final class SessionManager { case .deepSeek: validateDeepSeekSession(sessionData) default: - self.logger.warning("Unknown service: \(service.name)") + logger.warning("Unknown service: \(service.name)") return false } - - self.logger.debug("Session validation result for \(service.name): \(isValid)") + + logger.debug("Session validation result for \(service.name): \(isValid)") return isValid } catch { - self.logger.error("Failed to validate session: \(error.localizedDescription)") + logger.error("Failed to validate session: \(error.localizedDescription)") return false } } - + /// Stores session data for the current service /// /// This method securely stores session data in the keychain. /// /// - Parameter data: The session data to store func storeSession(_ data: Data) async throws { - self.logger.debug("Storing session data for service: \(self.service.name)") - + logger.debug("Storing session data for service: \(service.name)") + do { - try await self.keychain.save(data, for: "session.\(self.service.rawValue)") - self.logger.info("Successfully stored session data for service: \(self.service.name)") + try await keychain.save(data, for: "session.\(service.rawValue)") + logger.info("Successfully stored session data for service: \(service.name)") } catch { - self.logger.error("Failed to store session data: \(error.localizedDescription)") + logger.error("Failed to store session data: \(error.localizedDescription)") throw error } } - + /// Clears the current session /// /// This method removes the stored session data and resets /// the authentication state. func clearSession() async throws { - self.logger.debug("Clearing session for service: \(self.service.name)") - + logger.debug("Clearing session for service: \(service.name)") + do { - try await self.keychain.delete(for: "session.\(self.service.rawValue)") - self.logger.info("Successfully cleared session for service: \(self.service.name)") + try await keychain.delete(for: "session.\(service.rawValue)") + logger.info("Successfully cleared session for service: \(service.name)") } catch { - self.logger.error("Failed to clear session: \(error.localizedDescription)") + logger.error("Failed to clear session: \(error.localizedDescription)") throw error } } - + // MARK: - Service-Specific Validation - - private func validateOpenAISession(_ data: Data) -> Bool { - self.logger.debug("Validating OpenAI session") + + private func validateOpenAISession(_: Data) -> Bool { + logger.debug("Validating OpenAI session") // Implement OpenAI-specific session validation // This is a placeholder - actual implementation would depend on OpenAI's session structure return true } - - private func validateClaudeSession(_ data: Data) -> Bool { - self.logger.debug("Validating Claude session") + + private func validateClaudeSession(_: Data) -> Bool { + logger.debug("Validating Claude session") // Implement Claude-specific session validation // This is a placeholder - actual implementation would depend on Claude's session structure return true } - - private func validateDeepSeekSession(_ data: Data) -> Bool { - self.logger.debug("Validating DeepSeek session") + + private func validateDeepSeekSession(_: Data) -> Bool { + logger.debug("Validating DeepSeek session") // Implement DeepSeek-specific session validation // This is a placeholder - actual implementation would depend on DeepSeek's session structure return true } - + func saveSession(_ session: String) async throws { try await keychain.save(session.data(using: .utf8)!, for: "session.\(service.rawValue)") } - + func getSession() async throws -> String? { guard let data = try await keychain.getData(for: "session.\(service.rawValue)") else { return nil @@ -166,47 +166,48 @@ final class SessionManager { @MainActor private class KeychainManager { private let service = "com.minimalaichat.sessions" - + func save(_ data: Data, for key: String) async throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, - kSecValueData as String: data + kSecValueData as String: data, ] - + let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecSuccess else { throw KeychainError.saveFailed(status) } } - + func getData(for key: String) async throws -> Data { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, - kSecReturnData as String: true + kSecReturnData as String: true, ] - + var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) - + guard status == errSecSuccess, - let data = result as? Data else { + let data = result as? Data + else { throw KeychainError.retrieveFailed(status) } - + return data } - + func delete(for key: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, - kSecAttrAccount as String: key + kSecAttrAccount as String: key, ] - + let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.deleteFailed(status) @@ -219,4 +220,4 @@ enum KeychainError: Error { case saveFailed(OSStatus) case retrieveFailed(OSStatus) case deleteFailed(OSStatus) -} \ No newline at end of file +} diff --git a/App/Core/Managers/SettingsManager.swift b/Archived/v1/code/App/Core/Managers/SettingsManager.swift similarity index 74% rename from App/Core/Managers/SettingsManager.swift rename to Archived/v1/code/App/Core/Managers/SettingsManager.swift index c3165b7..1b43f73 100644 --- a/App/Core/Managers/SettingsManager.swift +++ b/Archived/v1/code/App/Core/Managers/SettingsManager.swift @@ -1,8 +1,8 @@ -import Foundation -import SwiftUI import AppKit import Combine +import Foundation import Security +import SwiftUI /// A manager class that handles user preferences and API key management /// @@ -39,98 +39,98 @@ import Security @MainActor public class SettingsManager: ObservableObject { // MARK: - Published Properties - + @Published public var selectedAIService: AIService = .openAI @Published public var selectedTheme: Theme = .system @Published public var errorMessage: String? @Published public var isShowingError: Bool = false - + @Published var hotkeyEnabled: Bool { didSet { savePreference(.hotkeyEnabled, value: hotkeyEnabled) } } - + @Published var hotkeyModifiers: NSEvent.ModifierFlags { didSet { savePreference(.hotkeyModifiers, value: hotkeyModifiers.rawValue) } } - + @Published var hotkeyKey: Key { didSet { savePreference(.hotkeyKey, value: hotkeyKey.rawValue) } } - + @Published var darkMode: Bool { didSet { savePreference(.darkMode, value: darkMode) } } - + @Published var fontSize: CGFloat { didSet { savePreference(.fontSize, value: fontSize) } } - + @Published var appearance: Appearance { didSet { savePreference(.appearance, value: appearance.rawValue) } } - + @Published var startAtLogin: Bool { didSet { savePreference(.startAtLogin, value: startAtLogin) } } - + @Published var showInMenuBar: Bool { didSet { savePreference(.showInMenuBar, value: showInMenuBar) } } - + @Published var showInDock: Bool { didSet { savePreference(.showInDock, value: showInDock) } } - + @Published var globalHotkeyEnabled: Bool { didSet { savePreference(.globalHotkeyEnabled, value: globalHotkeyEnabled) } } - + // MARK: - Private Properties - + private let defaults = UserDefaults.standard private let keychain: KeychainManager - + // MARK: - Initialization - + public init() { - self.keychain = KeychainManager() + keychain = KeychainManager() // Load saved preferences or use defaults - self.selectedAIService = AIService(rawValue: loadPreference(.selectedService) ?? "OpenAI") ?? .openAI - self.selectedTheme = Theme(rawValue: loadPreference(.theme) ?? "system") ?? .system - self.hotkeyEnabled = loadPreference(.hotkeyEnabled) ?? true - self.hotkeyModifiers = NSEvent.ModifierFlags(rawValue: loadPreference(.hotkeyModifiers) ?? 0) - self.hotkeyKey = Key(rawValue: loadPreference(.hotkeyKey) ?? "space") ?? .space - self.darkMode = loadPreference(.darkMode) ?? false - self.fontSize = loadPreference(.fontSize) ?? 14.0 - self.appearance = Appearance(rawValue: loadPreference(.appearance) ?? "system") ?? .system - self.startAtLogin = loadPreference(.startAtLogin) ?? false - self.showInMenuBar = loadPreference(.showInMenuBar) ?? true - self.showInDock = loadPreference(.showInDock) ?? true - self.globalHotkeyEnabled = loadPreference(.globalHotkeyEnabled) ?? true + selectedAIService = AIService(rawValue: loadPreference(.selectedService) ?? "OpenAI") ?? .openAI + selectedTheme = Theme(rawValue: loadPreference(.theme) ?? "system") ?? .system + hotkeyEnabled = loadPreference(.hotkeyEnabled) ?? true + hotkeyModifiers = NSEvent.ModifierFlags(rawValue: loadPreference(.hotkeyModifiers) ?? 0) + hotkeyKey = Key(rawValue: loadPreference(.hotkeyKey) ?? "space") ?? .space + darkMode = loadPreference(.darkMode) ?? false + fontSize = loadPreference(.fontSize) ?? 14.0 + appearance = Appearance(rawValue: loadPreference(.appearance) ?? "system") ?? .system + startAtLogin = loadPreference(.startAtLogin) ?? false + showInMenuBar = loadPreference(.showInMenuBar) ?? true + showInDock = loadPreference(.showInDock) ?? true + globalHotkeyEnabled = loadPreference(.globalHotkeyEnabled) ?? true } - + // MARK: - API Key Management - + /// Stores an API key securely in the keychain /// /// - Parameters: @@ -145,7 +145,7 @@ public class SettingsManager: ObservableObject { throw error } } - + /// Retrieves an API key from the keychain /// /// - Parameter service: The service to get the key for @@ -162,7 +162,7 @@ public class SettingsManager: ObservableObject { throw error } } - + /// Removes an API key from the keychain /// /// - Parameter service: The service to remove the key for @@ -175,17 +175,17 @@ public class SettingsManager: ObservableObject { throw error } } - + // MARK: - Private Methods - + private func savePreference(_ key: PreferenceKey, value: Any) { defaults.set(value, forKey: key.rawValue) } - + private func loadPreference(_ key: PreferenceKey) -> T? { defaults.object(forKey: key.rawValue) as? T } - + @MainActor func resetToDefaults() { // Reset all settings to their default values @@ -207,56 +207,56 @@ public class SettingsManager: ObservableObject { extension SettingsManager { enum Appearance: String, CaseIterable, Identifiable { - case light = "light" - case dark = "dark" - case system = "system" - - var id: String { self.rawValue } + case light + case dark + case system + + var id: String { rawValue } } - + /// Preference keys for UserDefaults private enum PreferenceKey: String { case selectedService = "selectedAIService" - case theme = "theme" - case hotkeyEnabled = "hotkeyEnabled" - case hotkeyModifiers = "hotkeyModifiers" - case hotkeyKey = "hotkeyKey" - case darkMode = "darkMode" - case fontSize = "fontSize" - case appearance = "appearance" - case startAtLogin = "startAtLogin" - case showInMenuBar = "showInMenuBar" - case showInDock = "showInDock" - case globalHotkeyEnabled = "globalHotkeyEnabled" + case theme + case hotkeyEnabled + case hotkeyModifiers + case hotkeyKey + case darkMode + case fontSize + case appearance + case startAtLogin + case showInMenuBar + case showInDock + case globalHotkeyEnabled } - + /// Available hotkey keys enum Key: String, CaseIterable, Identifiable { - case space = "space" + case space case return_ = "return" - case tab = "tab" - case escape = "escape" - case delete = "delete" - case forwardDelete = "forwardDelete" - case upArrow = "upArrow" - case downArrow = "downArrow" - case leftArrow = "leftArrow" - case rightArrow = "rightArrow" - case f1 = "f1" - case f2 = "f2" - case f3 = "f3" - case f4 = "f4" - case f5 = "f5" - case f6 = "f6" - case f7 = "f7" - case f8 = "f8" - case f9 = "f9" - case f10 = "f10" - case f11 = "f11" - case f12 = "f12" - - var id: String { self.rawValue } - + case tab + case escape + case delete + case forwardDelete + case upArrow + case downArrow + case leftArrow + case rightArrow + case f1 + case f2 + case f3 + case f4 + case f5 + case f6 + case f7 + case f8 + case f9 + case f10 + case f11 + case f12 + + var id: String { rawValue } + var displayName: String { switch self { case .space: return "Space" @@ -284,4 +284,4 @@ extension SettingsManager { } } } -} \ No newline at end of file +} diff --git a/App/Core/Managers/StorageManager.swift b/Archived/v1/code/App/Core/Managers/StorageManager.swift similarity index 92% rename from App/Core/Managers/StorageManager.swift rename to Archived/v1/code/App/Core/Managers/StorageManager.swift index 5e9da7f..bea84d5 100644 --- a/App/Core/Managers/StorageManager.swift +++ b/Archived/v1/code/App/Core/Managers/StorageManager.swift @@ -3,22 +3,23 @@ import Foundation class StorageManager { private let messagesKey = "chat_messages" private let defaults = UserDefaults.standard - + func saveMessages(_ messages: [MinimalAIChatMessage]) { if let encoded = try? JSONEncoder().encode(messages) { defaults.set(encoded, forKey: messagesKey) } } - + func loadMessages() -> [MinimalAIChatMessage] { guard let data = defaults.data(forKey: messagesKey), - let messages = try? JSONDecoder().decode([MinimalAIChatMessage].self, from: data) else { + let messages = try? JSONDecoder().decode([MinimalAIChatMessage].self, from: data) + else { return [] } return messages } - + func clearMessages() { defaults.removeObject(forKey: messagesKey) } -} \ No newline at end of file +} diff --git a/App/Core/Managers/WebViewManager.swift b/Archived/v1/code/App/Core/Managers/WebViewManager.swift similarity index 91% rename from App/Core/Managers/WebViewManager.swift rename to Archived/v1/code/App/Core/Managers/WebViewManager.swift index 4350661..2d6c31b 100644 --- a/App/Core/Managers/WebViewManager.swift +++ b/Archived/v1/code/App/Core/Managers/WebViewManager.swift @@ -1,5 +1,5 @@ -import WebKit import SwiftUI +import WebKit /// A manager class that handles WebView interactions with AI services /// @@ -38,19 +38,19 @@ class WebViewManager: NSObject, ObservableObject { private var webView: WKWebView? private let configuration: WKWebViewConfiguration private var sessionManager: SessionManager? - + @Published var isLoading = false @Published var error: Error? @Published var isAuthenticated = false @Published var currentService: AIService? - + override init() { configuration = WKWebViewConfiguration() configuration.defaultWebpagePreferences.allowsContentJavaScript = true configuration.websiteDataStore = .nonPersistent() super.init() } - + /// Creates and configures a new WebView instance /// /// This method sets up a new WebView with the appropriate configuration @@ -65,7 +65,7 @@ class WebViewManager: NSObject, ObservableObject { self.webView = webView return webView } - + /// Loads the specified AI service into the WebView /// /// This method handles loading the AI service URL and initializing @@ -75,7 +75,7 @@ class WebViewManager: NSObject, ObservableObject { func loadAIService(url: URL) { guard let webView = webView else { return } isLoading = true - + // Determine the service from the URL let host = url.host?.lowercased() ?? "" if host.contains("claude") { @@ -87,13 +87,13 @@ class WebViewManager: NSObject, ObservableObject { } else { currentService = .claude // Default to Claude } - + sessionManager = SessionManager(service: currentService!) - + let request = URLRequest(url: url) webView.load(request) } - + /// Injects a message into the current AI service /// /// This method handles sending messages to the AI service by injecting @@ -102,10 +102,10 @@ class WebViewManager: NSObject, ObservableObject { /// - Parameter message: The message to send func injectMessage(_ message: String) { guard let webView = webView else { return } - + // Escape special characters in the message let escapedMessage = message.replacingOccurrences(of: "\"", with: "\\\"") - + let javascript = """ (function() { const input = document.querySelector('textarea'); @@ -121,14 +121,14 @@ class WebViewManager: NSObject, ObservableObject { } })(); """ - - webView.evaluateJavaScript(javascript) { [weak self] result, error in + + webView.evaluateJavaScript(javascript) { [weak self] _, error in if let error = error { self?.error = error } } } - + /// Clears the WebView and resets its state /// /// This method cleans up the WebView by clearing its contents @@ -139,7 +139,7 @@ class WebViewManager: NSObject, ObservableObject { isAuthenticated = false currentService = nil } - + /// Handles authentication state changes /// /// This method updates the authentication state based on the @@ -152,11 +152,12 @@ class WebViewManager: NSObject, ObservableObject { } // MARK: - WKNavigationDelegate + extension WebViewManager: WKNavigationDelegate { - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + func webView(_: WKWebView, didFinish _: WKNavigation!) { isLoading = false updateAuthState() - + // Check for authentication status if let service = currentService { Task { @@ -164,19 +165,20 @@ extension WebViewManager: WKNavigationDelegate { } } } - - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + + func webView(_: WKWebView, didFail _: WKNavigation!, withError error: Error) { self.error = error isLoading = false } } // MARK: - WKUIDelegate + extension WebViewManager: WKUIDelegate { - func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + func webView(_ webView: WKWebView, createWebViewWith _: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures _: WKWindowFeatures) -> WKWebView? { if navigationAction.targetFrame == nil { webView.load(navigationAction.request) } return nil } -} \ No newline at end of file +} diff --git a/App/Core/MemoryPressureLevel.swift b/Archived/v1/code/App/Core/MemoryPressureLevel.swift similarity index 98% rename from App/Core/MemoryPressureLevel.swift rename to Archived/v1/code/App/Core/MemoryPressureLevel.swift index 899365f..451b890 100644 --- a/App/Core/MemoryPressureLevel.swift +++ b/Archived/v1/code/App/Core/MemoryPressureLevel.swift @@ -3,7 +3,7 @@ enum MemoryPressureLevel: Comparable { case warning case critical case terminal - + static func < (lhs: MemoryPressureLevel, rhs: MemoryPressureLevel) -> Bool { switch (lhs, rhs) { case (.normal, .warning), (.normal, .critical), (.normal, .terminal), @@ -14,4 +14,4 @@ enum MemoryPressureLevel: Comparable { return false } } -} \ No newline at end of file +} diff --git a/App/Core/Models/AIService.swift b/Archived/v1/code/App/Core/Models/AIService.swift similarity index 92% rename from App/Core/Models/AIService.swift rename to Archived/v1/code/App/Core/Models/AIService.swift index 97642c8..5e29aef 100644 --- a/App/Core/Models/AIService.swift +++ b/Archived/v1/code/App/Core/Models/AIService.swift @@ -2,9 +2,9 @@ public enum AIService: String, Codable, CaseIterable, Identifiable { case openAI = "OpenAI" case claude = "Claude" case deepSeek = "DeepSeek" - - public var id: String { self.rawValue } - + + public var id: String { rawValue } + public var displayName: String { switch self { case .openAI: return "OpenAI" @@ -12,7 +12,7 @@ public enum AIService: String, Codable, CaseIterable, Identifiable { case .deepSeek: return "DeepSeek" } } - + public var url: URL { switch self { case .openAI: @@ -23,7 +23,7 @@ public enum AIService: String, Codable, CaseIterable, Identifiable { return URL(string: "https://chat.deepseek.com")! } } - + public var icon: String { switch self { case .openAI: return "openai-icon" @@ -31,4 +31,4 @@ public enum AIService: String, Codable, CaseIterable, Identifiable { case .deepSeek: return "deepseek-icon" } } -} \ No newline at end of file +} diff --git a/App/Core/Models/ChatMessage.swift b/Archived/v1/code/App/Core/Models/ChatMessage.swift similarity index 84% rename from App/Core/Models/ChatMessage.swift rename to Archived/v1/code/App/Core/Models/ChatMessage.swift index b8ac529..6e86543 100644 --- a/App/Core/Models/ChatMessage.swift +++ b/Archived/v1/code/App/Core/Models/ChatMessage.swift @@ -6,9 +6,9 @@ public struct MinimalAIChatMessage: Codable, Identifiable { public let content: String public let isUser: Bool public let timestamp: Date - + public init(content: String, isUser: Bool, timestamp: Date = Date()) { - self.id = UUID() + id = UUID() self.content = content self.isUser = isUser self.timestamp = timestamp @@ -16,4 +16,4 @@ public struct MinimalAIChatMessage: Codable, Identifiable { } // Type alias for backward compatibility -public typealias ChatMessage = MinimalAIChatMessage \ No newline at end of file +public typealias ChatMessage = MinimalAIChatMessage diff --git a/App/Core/Models/SettingsModels.swift b/Archived/v1/code/App/Core/Models/SettingsModels.swift similarity index 92% rename from App/Core/Models/SettingsModels.swift rename to Archived/v1/code/App/Core/Models/SettingsModels.swift index 29b04dd..22590be 100644 --- a/App/Core/Models/SettingsModels.swift +++ b/Archived/v1/code/App/Core/Models/SettingsModels.swift @@ -1,18 +1,21 @@ import SwiftUI // MARK: - Service Types + enum AIServiceType: String, Codable { case directAPI case webWrapper } // MARK: - AI Models + enum AIModel: String, Codable { case gpt35 = "gpt-3.5-turbo" case gpt4 = "gpt-4" } // MARK: - Theme + enum Theme: String, Codable { case system case light @@ -20,10 +23,11 @@ enum Theme: String, Codable { } // MARK: - Hotkey + struct Hotkey: Codable, Equatable { let key: KeyCode let modifiers: Set - + var isValid: Bool { !modifiers.isEmpty } @@ -32,27 +36,29 @@ struct Hotkey: Codable, Equatable { // Using KeyCode and KeyModifier from KeyCombo.swift // MARK: - Settings Error + enum SettingsError: LocalizedError { case invalidAPIKey case invalidHotkey case keychainError(Error) case persistenceError(Error) - + var errorDescription: String? { switch self { case .invalidAPIKey: return "Invalid API key format" case .invalidHotkey: return "Invalid hotkey combination" - case .keychainError(let error): + case let .keychainError(error): return "Keychain error: \(error.localizedDescription)" - case .persistenceError(let error): + case let .persistenceError(error): return "Failed to save settings: \(error.localizedDescription)" } } } // MARK: - Keychain Protocol + protocol KeychainManagerProtocol { func store(_ value: String, for key: String) throws func retrieve(for key: String) throws -> String @@ -64,7 +70,7 @@ enum KeychainError: LocalizedError { case duplicateItem case invalidItemFormat case unhandledError(Error) - + var errorDescription: String? { switch self { case .itemNotFound: @@ -73,8 +79,8 @@ enum KeychainError: LocalizedError { return "Item already exists in keychain" case .invalidItemFormat: return "Invalid item format" - case .unhandledError(let error): + case let .unhandledError(error): return "Unhandled keychain error: \(error.localizedDescription)" } } -} \ No newline at end of file +} diff --git a/App/Modules/Discovery/SpotlightIndexer.swift b/Archived/v1/code/App/Modules/Discovery/SpotlightIndexer.swift similarity index 97% rename from App/Modules/Discovery/SpotlightIndexer.swift rename to Archived/v1/code/App/Modules/Discovery/SpotlightIndexer.swift index 16bc511..7d24769 100644 --- a/App/Modules/Discovery/SpotlightIndexer.swift +++ b/Archived/v1/code/App/Modules/Discovery/SpotlightIndexer.swift @@ -1,16 +1,16 @@ -import Foundation import CoreServices import CoreSpotlight +import Foundation /// Handles Spotlight indexing for the app class SpotlightIndexer { private let searchableIndex: CSSearchableIndex - + init() { // Initialize Spotlight index searchableIndex = CSSearchableIndex(name: "com.minimalaichat.index") } - + /// Index a chat message for Spotlight search func indexMessage(_ message: ChatMessage) { let attributeSet = CSSearchableItemAttributeSet(contentType: UTType.text) @@ -18,20 +18,20 @@ class SpotlightIndexer { attributeSet.contentDescription = message.isUser ? "Your message" : "AI response" attributeSet.addedDate = message.timestamp attributeSet.contentModificationDate = message.timestamp - + let item = CSSearchableItem( uniqueIdentifier: message.id.uuidString, domainIdentifier: "chat", attributeSet: attributeSet ) - + searchableIndex.indexSearchableItems([item]) { error in if let error = error { NSLog("Failed to index message: \(error.localizedDescription)") } } } - + /// Remove a message from the Spotlight index func removeMessage(_ messageId: String) { searchableIndex.deleteSearchableItems(withIdentifiers: [messageId]) { error in @@ -40,7 +40,7 @@ class SpotlightIndexer { } } } - + /// Clear all indexed items func clearIndex() { searchableIndex.deleteAllSearchableItems { error in diff --git a/App/Modules/Hotkey/HotkeyManager.swift b/Archived/v1/code/App/Modules/Hotkey/HotkeyManager.swift similarity index 94% rename from App/Modules/Hotkey/HotkeyManager.swift rename to Archived/v1/code/App/Modules/Hotkey/HotkeyManager.swift index 78fc49f..15be954 100644 --- a/App/Modules/Hotkey/HotkeyManager.swift +++ b/Archived/v1/code/App/Modules/Hotkey/HotkeyManager.swift @@ -4,23 +4,23 @@ import Cocoa class HotkeyManager { private var hotkeys: [UInt32: HotKey] = [:] private var nextHotkeyID: UInt32 = 1 - + init() { // Initialize hotkey manager } - + deinit { unregisterAllHotkeys() } - + func registerHotkey(keyCombo: KeyCombo, action: @escaping () -> Void) -> UInt32? { let hotkeyID = nextHotkeyID nextHotkeyID += 1 - + // Create Carbon event hotkey var eventHotKey: EventHotKeyRef? let gMyHotKeyID = EventHotKeyID(signature: OSType(hotkeyID), id: UInt32(hotkeyID)) - + let registerError = RegisterEventHotKey( UInt32(keyCombo.keyCode), UInt32(keyCombo.modifiers.carbonFlags), @@ -29,37 +29,37 @@ class HotkeyManager { 0, &eventHotKey ) - + guard registerError == noErr, let eventHotKey = eventHotKey else { NSLog("Failed to register hotkey with error: \(registerError)") return nil } - + let hotKey = HotKey(id: hotkeyID, keyCombo: keyCombo, carbonHotKey: eventHotKey, action: action) hotkeys[hotkeyID] = hotKey - + return hotkeyID } - + func unregisterHotkey(id: UInt32) { guard let hotkey = hotkeys[id] else { return } - + let unregisterError = UnregisterEventHotKey(hotkey.carbonHotKey) if unregisterError != noErr { NSLog("Failed to unregister hotkey with error: \(unregisterError)") } - + hotkeys.removeValue(forKey: id) } - + func unregisterAllHotkeys() { for (id, _) in hotkeys { unregisterHotkey(id: id) } } - + // MARK: - Private Types - + private struct HotKey { let id: UInt32 let keyCombo: KeyCombo diff --git a/App/Modules/Navigation/UniversalLinkRouter.swift b/Archived/v1/code/App/Modules/Navigation/UniversalLinkRouter.swift similarity index 95% rename from App/Modules/Navigation/UniversalLinkRouter.swift rename to Archived/v1/code/App/Modules/Navigation/UniversalLinkRouter.swift index 3bc581d..dab29cf 100644 --- a/App/Modules/Navigation/UniversalLinkRouter.swift +++ b/Archived/v1/code/App/Modules/Navigation/UniversalLinkRouter.swift @@ -9,16 +9,16 @@ class UniversalLinkRouter { NSLog("Invalid universal link domain: \(url.host ?? "none")") return } - + // Extract path components let pathComponents = url.pathComponents.filter { $0 != "/" } - + guard !pathComponents.isEmpty else { // Default action for domain root WindowManager.shared.showMainWindow() return } - + // Route based on first path component switch pathComponents[0] { case "chat": @@ -32,11 +32,11 @@ class UniversalLinkRouter { WindowManager.shared.showMainWindow() } } - + /// Handle chat-related universal links - private func handleChatLink(url: URL, pathComponents: [String]) { + private func handleChatLink(url: URL, pathComponents _: [String]) { WindowManager.shared.showMainWindow() - + // Extract query if present if let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems { // Process query parameters @@ -53,13 +53,13 @@ class UniversalLinkRouter { } } } - + /// Handle service-related universal links private func handleServiceLink(url: URL, pathComponents: [String]) { // Check if we have a service name in the path if pathComponents.count > 1 { let serviceName = pathComponents[1] - + // Process service-specific parameters if let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems { for item in queryItems { @@ -74,7 +74,7 @@ class UniversalLinkRouter { } } } - + // Show the main window WindowManager.shared.showMainWindow() } diff --git a/App/Modules/Navigation/WindowManager.swift b/Archived/v1/code/App/Modules/Navigation/WindowManager.swift similarity index 96% rename from App/Modules/Navigation/WindowManager.swift rename to Archived/v1/code/App/Modules/Navigation/WindowManager.swift index 5bfd2ad..4f6cade 100644 --- a/App/Modules/Navigation/WindowManager.swift +++ b/Archived/v1/code/App/Modules/Navigation/WindowManager.swift @@ -5,20 +5,20 @@ import SwiftUI @MainActor class WindowManager { static let shared = WindowManager() - + private var mainWindow: NSWindow? private var preferencesWindow: NSWindow? private var statusBarController: StatusBarController? private var popover: NSPopover? - + private init() {} - + /// Initialize the window manager with a popover for status bar integration func initialize(with popover: NSPopover) { self.popover = popover statusBarController = StatusBarController(popover: popover) } - + /// Create and show the main application window func showMainWindow() { // If we're showing in the popover, just show that @@ -26,7 +26,7 @@ class WindowManager { statusBarController.showPopover() return } - + // Otherwise create and show a standard window if mainWindow == nil { let window = NSWindow( @@ -40,15 +40,15 @@ class WindowManager { window.contentView = NSHostingView(rootView: MainChatView()) window.title = Constants.appName window.makeKeyAndOrderFront(nil) - + mainWindow = window } else { mainWindow?.makeKeyAndOrderFront(nil) } - + NSApp.activate(ignoringOtherApps: true) } - + /// Toggle the main window visibility func toggleMainWindow() { if let popover = popover, let statusBarController = statusBarController { @@ -59,14 +59,14 @@ class WindowManager { } return } - + if let window = mainWindow, window.isVisible { window.close() } else { showMainWindow() } } - + /// Show the preferences window func showPreferencesWindow() { if preferencesWindow == nil { @@ -81,10 +81,10 @@ class WindowManager { // Replace with your actual preferences view window.contentView = NSHostingView(rootView: Text("Preferences")) window.title = "Preferences" - + preferencesWindow = window } - + preferencesWindow?.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) } diff --git a/App/Modules/WebView/WebViewCleaner.swift b/Archived/v1/code/App/Modules/WebView/WebViewCleaner.swift similarity index 99% rename from App/Modules/WebView/WebViewCleaner.swift rename to Archived/v1/code/App/Modules/WebView/WebViewCleaner.swift index 1ebf70c..5a53a30 100644 --- a/App/Modules/WebView/WebViewCleaner.swift +++ b/Archived/v1/code/App/Modules/WebView/WebViewCleaner.swift @@ -23,4 +23,4 @@ import WebKit /// a custom version of the cleanup methods that avoids Objective-C bridging issues. class WebViewCleaner: WebViewCleanupable { // Uses default implementation from WebViewCleanupable protocol -} \ No newline at end of file +} diff --git a/App/Modules/WebView/WebViewCleanupActor.swift b/Archived/v1/code/App/Modules/WebView/WebViewCleanupActor.swift similarity index 95% rename from App/Modules/WebView/WebViewCleanupActor.swift rename to Archived/v1/code/App/Modules/WebView/WebViewCleanupActor.swift index 922d949..9051d67 100644 --- a/App/Modules/WebView/WebViewCleanupActor.swift +++ b/Archived/v1/code/App/Modules/WebView/WebViewCleanupActor.swift @@ -49,50 +49,50 @@ import WebKit public actor WebViewCleanupActor: WebViewCleanupable { private let dataStore: WKWebsiteDataStore private var cleanupTasks: [Task] = [] - + public init(dataStore: WKWebsiteDataStore = .default()) { self.dataStore = dataStore } - + func cleanup() async throws { // Cancel any existing cleanup tasks for task in cleanupTasks { task.cancel() } cleanupTasks.removeAll() - + // Create a new cleanup task let task = Task { try await cleanupWebViewData() try await cleanupWebViewCookies() } - + cleanupTasks.append(task) - + // Wait for the task to complete try await task.value } - + /// Cleans up all WebView data func cleanupWebViewData() async throws { let dataTypes = WKWebsiteDataStore.allWebsiteDataTypes() let date = Date(timeIntervalSince1970: 0) - + try await dataStore.removeData(ofTypes: dataTypes, modifiedSince: date) } - + /// Cleans up WebView cookies func cleanupWebViewCookies() async throws { let cookieStore = dataStore.httpCookieStore let cookies = try await cookieStore.allCookies() - + for cookie in cookies { try await cookieStore.delete(cookie) } } /// Performs WebKit data removal with improved error handling - /// + /// /// This method handles the actual removal of WebKit data with proper error handling /// and actor isolation. It ensures that all operations are performed on the main actor /// and properly handles completion callbacks. @@ -102,15 +102,15 @@ public actor WebViewCleanupActor: WebViewCleanupable { /// - types: The types of data to remove /// - Throws: Any errors encountered during the cleanup process private func removeWebKitData( - _ dataStore: WKWebsiteDataStore, + _ dataStore: WKWebsiteDataStore, types: Set ) async throws { let stringTypes = Set(types.map { $0.rawValue() }) - + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in Task { @MainActor in dataStore.removeData( - ofTypes: stringTypes, + ofTypes: stringTypes, modifiedSince: .distantPast ) { error in if let error = error { @@ -122,7 +122,7 @@ public actor WebViewCleanupActor: WebViewCleanupable { } } } - + /// Cleans up WebKit caches with improved concurrency and error handling /// /// This method removes all cached data from the default WebKit data store. @@ -132,13 +132,13 @@ public actor WebViewCleanupActor: WebViewCleanupable { /// - Throws: Any errors encountered during the cleanup process func cleanupWebKitCaches() async throws { let dataStore = await WKWebsiteDataStore.default() - + try await removeWebKitData( - dataStore, + dataStore, types: [.memoryCache, .diskCache, .offlineWebApplicationCache, .allWebsiteData] ) } - + /// Cleans up WebKit data stores with improved concurrency and error handling /// /// This method removes all data from the default WebKit data store, including @@ -148,9 +148,9 @@ public actor WebViewCleanupActor: WebViewCleanupable { /// - Throws: Any errors encountered during the cleanup process func cleanupWebKitDataStores() async throws { let dataStore = await WKWebsiteDataStore.default() - + try await removeWebKitData( - dataStore, + dataStore, types: [.memoryCache, .diskCache, .offlineWebApplicationCache, .allWebsiteData] ) } @@ -165,10 +165,11 @@ public actor WebViewCleanupActor: WebViewCleanupable { guard let url = item as? URL, let resourceValues = try? url.resourceValues(forKeys: [.isRegularFileKey]), resourceValues.isRegularFile == true, - types.contains(url.pathExtension) else { + types.contains(url.pathExtension) + else { return } - + do { try FileManager.default.removeItem(at: url) } catch { @@ -176,8 +177,8 @@ public actor WebViewCleanupActor: WebViewCleanupable { return } } - + continuation.resume() } } -} \ No newline at end of file +} diff --git a/App/Modules/WebView/WebViewCleanupable.swift b/Archived/v1/code/App/Modules/WebView/WebViewCleanupable.swift similarity index 98% rename from App/Modules/WebView/WebViewCleanupable.swift rename to Archived/v1/code/App/Modules/WebView/WebViewCleanupable.swift index 75ef419..2142619 100644 --- a/App/Modules/WebView/WebViewCleanupable.swift +++ b/Archived/v1/code/App/Modules/WebView/WebViewCleanupable.swift @@ -2,7 +2,7 @@ import Foundation import WebKit /// Protocol defining WebView cleanup operations for memory optimization -/// +/// /// This protocol provides a standardized interface for cleaning up WebKit resources /// such as caches and data stores. It's designed to be used in conjunction with /// memory pressure monitoring to optimize memory usage in WebView-heavy applications. @@ -20,15 +20,15 @@ protocol WebViewCleanupable { /// Cleans up WebKit caches by removing all cached data /// - Throws: Any errors that occur during the cleanup process func cleanupWebKitCaches() async throws - + /// Cleans up WebKit data stores by removing all stored data /// - Throws: Any errors that occur during the cleanup process func cleanupWebKitDataStores() async throws - + /// Cleans up WebView data by removing all stored data /// - Throws: Any errors that occur during the cleanup process func cleanupWebViewData() async throws - + /// Cleans up WebView cookies by removing all stored cookies /// - Throws: Any errors that occur during the cleanup process func cleanupWebViewCookies() async throws @@ -37,7 +37,7 @@ protocol WebViewCleanupable { /// Default implementation of WebView cleanup operations extension WebViewCleanupable { /// Improved implementation for cleaning up WebKit caches - /// + /// /// This implementation addresses previous data race warnings by: /// 1. Creating a local, stable copy of website data types /// 2. Using non-bridged, local variables @@ -47,7 +47,7 @@ extension WebViewCleanupable { [.memoryCache, .diskCache, .offlineWebApplicationCache, .allWebsiteData] .map { $0.rawValue() } ) - + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in WKWebsiteDataStore.default().removeData( ofTypes: dataTypesToRemove, @@ -61,9 +61,9 @@ extension WebViewCleanupable { } } } - + /// Improved implementation for cleaning up WebKit data stores - /// + /// /// This implementation follows the same pattern as cleanupWebKitCaches /// to minimize data race and bridging issues func cleanupWebKitDataStores() async throws { @@ -71,7 +71,7 @@ extension WebViewCleanupable { [.memoryCache, .diskCache, .offlineWebApplicationCache, .allWebsiteData] .map { $0.rawValue() } ) - + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in WKWebsiteDataStore.default().removeData( ofTypes: dataTypesToRemove, @@ -85,7 +85,7 @@ extension WebViewCleanupable { } } } - + /// Cleans up WebView data by removing all stored data /// - Throws: Any errors that occur during the cleanup process func cleanupWebViewData() async throws { @@ -93,18 +93,18 @@ extension WebViewCleanupable { [.cookies, .localStorage, .sessionStorage, .webSQLDatabases] .map { $0.rawValue() } ) - + try await WKWebsiteDataStore.default().removeData( ofTypes: dataTypes, modifiedSince: .distantPast ) } - + /// Cleans up WebView cookies by removing all stored cookies /// - Throws: Any errors that occur during the cleanup process func cleanupWebViewCookies() async throws { let dataTypes: Set = Set([.cookies].map { $0.rawValue() }) - + try await WKWebsiteDataStore.default().removeData( ofTypes: dataTypes, modifiedSince: .distantPast diff --git a/App/Modules/WebView/WebViewModel.swift b/Archived/v1/code/App/Modules/WebView/WebViewModel.swift similarity index 93% rename from App/Modules/WebView/WebViewModel.swift rename to Archived/v1/code/App/Modules/WebView/WebViewModel.swift index d8b59a0..8a84ab3 100644 --- a/App/Modules/WebView/WebViewModel.swift +++ b/Archived/v1/code/App/Modules/WebView/WebViewModel.swift @@ -7,26 +7,26 @@ class WebViewModel: ObservableObject { @Published var isLoading: Bool = false @Published var title: String = "" @Published var selectedService: AIService = .openAI - + init() { loadSelectedService() } - + /// Load the currently selected AI service func loadSelectedService() { isLoading = true currentURL = selectedService.url } - + /// Switch to a different AI service func switchService(to service: AIService) { selectedService = service loadSelectedService() } - + /// Handle navigation completion func handleNavigationFinished(url: URL) { isLoading = false - self.currentURL = url + currentURL = url } } diff --git a/App/Modules/WebView/WebsiteDataType.swift b/Archived/v1/code/App/Modules/WebView/WebsiteDataType.swift similarity index 97% rename from App/Modules/WebView/WebsiteDataType.swift rename to Archived/v1/code/App/Modules/WebView/WebsiteDataType.swift index 5c69420..d1b5751 100644 --- a/App/Modules/WebView/WebsiteDataType.swift +++ b/Archived/v1/code/App/Modules/WebView/WebsiteDataType.swift @@ -19,7 +19,7 @@ enum WebsiteDataType { case webSQLDatabases case indexedDBDatabases case allWebsiteData - + /// Provides the raw WebKit data type string for each case /// - Returns: The corresponding WebKit data type string func rawValue() -> String { @@ -44,7 +44,7 @@ enum WebsiteDataType { return WKWebsiteDataTypeAllWebsiteData } } - + /// Defines all available WebKit data types static let allTypes: Set = [ .memoryCache, @@ -54,6 +54,6 @@ enum WebsiteDataType { .sessionStorage, .localStorage, .webSQLDatabases, - .indexedDBDatabases + .indexedDBDatabases, ] -} \ No newline at end of file +} diff --git a/App/Services/AI/AIServiceClient.swift b/Archived/v1/code/App/Services/AI/AIServiceClient.swift similarity index 94% rename from App/Services/AI/AIServiceClient.swift rename to Archived/v1/code/App/Services/AI/AIServiceClient.swift index 02a6bc5..1087491 100644 --- a/App/Services/AI/AIServiceClient.swift +++ b/Archived/v1/code/App/Services/AI/AIServiceClient.swift @@ -1,6 +1,6 @@ +import Crypto import Foundation import Logging -import Crypto /// A service that handles communication with AI services /// @@ -35,7 +35,7 @@ public class AIServiceClient { private let sessionManager: SessionManager private let settingsManager: SettingsManager private let keychainManager: KeychainManager - + public init( sessionManager: SessionManager? = nil, settingsManager: SettingsManager? = nil, @@ -45,7 +45,7 @@ public class AIServiceClient { self.settingsManager = settingsManager ?? SettingsManager() self.keychainManager = keychainManager ?? KeychainManager() } - + /// Sends a message to the configured AI service /// - Parameter message: The message to send /// - Returns: The AI service's response @@ -53,10 +53,10 @@ public class AIServiceClient { public func sendMessage(_ message: String) async throws -> String { // Get current settings let settings = try await settingsManager.getSettings() - + // Validate session try await sessionManager.validateSession() - + // Select appropriate service based on settings switch settings.selectedService { case .claude: @@ -67,7 +67,7 @@ public class AIServiceClient { return try await sendToDeepSeek(message) } } - + /// Sends a message to Claude /// - Parameter message: The message to send /// - Returns: Claude's response @@ -75,26 +75,26 @@ public class AIServiceClient { private func sendToClaude(_ message: String) async throws -> String { let apiKey = try await keychainManager.getAPIKey(for: .claude) let url = URL(string: APIConfig.Claude.messagesEndpoint)! - + var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "x-api-key") request.setValue("application/json", forHTTPHeaderField: "Content-Type") - + let body: [String: Any] = [ "model": APIConfig.Claude.defaultModel, "max_tokens": APIConfig.Claude.maxTokens, "temperature": APIConfig.Claude.temperature, "messages": [ - ["role": "user", "content": message] - ] + ["role": "user", "content": message], + ], ] - + request.httpBody = try JSONSerialization.data(withJSONObject: body) - + return try await performRequest(request) } - + /// Sends a message to OpenAI /// - Parameter message: The message to send /// - Returns: OpenAI's response @@ -102,26 +102,26 @@ public class AIServiceClient { private func sendToOpenAI(_ message: String) async throws -> String { let apiKey = try await keychainManager.getAPIKey(for: .openAI) let url = URL(string: APIConfig.OpenAI.chatEndpoint)! - + var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") - + let body: [String: Any] = [ "model": APIConfig.OpenAI.defaultModel, "max_tokens": APIConfig.OpenAI.maxTokens, "temperature": APIConfig.OpenAI.temperature, "messages": [ - ["role": "user", "content": message] - ] + ["role": "user", "content": message], + ], ] - + request.httpBody = try JSONSerialization.data(withJSONObject: body) - + return try await performRequest(request) } - + /// Sends a message to DeepSeek /// - Parameter message: The message to send /// - Returns: DeepSeek's response @@ -129,26 +129,26 @@ public class AIServiceClient { private func sendToDeepSeek(_ message: String) async throws -> String { let apiKey = try await keychainManager.getAPIKey(for: .deepSeek) let url = URL(string: APIConfig.DeepSeek.chatEndpoint)! - + var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") - + let body: [String: Any] = [ "model": APIConfig.DeepSeek.defaultModel, "max_tokens": APIConfig.DeepSeek.maxTokens, "temperature": APIConfig.DeepSeek.temperature, "messages": [ - ["role": "user", "content": message] - ] + ["role": "user", "content": message], + ], ] - + request.httpBody = try JSONSerialization.data(withJSONObject: body) - + return try await performRequest(request) } - + /// Performs an API request with retry logic and error handling /// - Parameter request: The URL request to perform /// - Returns: The response string @@ -156,33 +156,33 @@ public class AIServiceClient { private func performRequest(_ request: URLRequest) async throws -> String { var currentRetry = 0 var lastError: Error? - + while currentRetry < APIConfig.Common.maxRetries { do { let (data, response) = try await URLSession.shared.data(for: request) - + guard let httpResponse = response as? HTTPURLResponse else { throw AIServiceError.invalidResponse } - + switch httpResponse.statusCode { case 200: let result = try JSONDecoder().decode(APIResponse.self, from: data) return result.choices.first?.message.content ?? "" - + case 401: throw AIServiceError.invalidSession - + case 429: throw AIServiceError.rateLimitExceeded - + default: throw AIServiceError.unknown } } catch { lastError = error currentRetry += 1 - + if currentRetry < APIConfig.Common.maxRetries { let delay = APIConfig.Common.retryDelay * pow(APIConfig.Common.exponentialBackoffFactor, Double(currentRetry - 1)) try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) @@ -190,7 +190,7 @@ public class AIServiceClient { } } } - + throw lastError ?? AIServiceError.unknown } } @@ -202,7 +202,7 @@ enum AIServiceError: LocalizedError { case networkError case invalidResponse case unknown - + var errorDescription: String? { switch self { case .invalidSession: @@ -220,6 +220,7 @@ enum AIServiceError: LocalizedError { } // MARK: - API Response Models + private struct APIResponse: Codable { let choices: [Choice] } @@ -230,4 +231,4 @@ private struct Choice: Codable { private struct Message: Codable { let content: String -} \ No newline at end of file +} diff --git a/App/UI/Localization/Localizable.xcstrings b/Archived/v1/code/App/UI/Localization/Localizable.xcstrings similarity index 100% rename from App/UI/Localization/Localizable.xcstrings rename to Archived/v1/code/App/UI/Localization/Localizable.xcstrings diff --git a/App/UI/Localization/String+Localization.swift b/Archived/v1/code/App/UI/Localization/String+Localization.swift similarity index 72% rename from App/UI/Localization/String+Localization.swift rename to Archived/v1/code/App/UI/Localization/String+Localization.swift index 00b45f2..ec6d89d 100644 --- a/App/UI/Localization/String+Localization.swift +++ b/Archived/v1/code/App/UI/Localization/String+Localization.swift @@ -4,8 +4,8 @@ extension String { var localized: String { NSLocalizedString(self, comment: "") } - + func localized(with arguments: CVarArg...) -> String { - String(format: self.localized, arguments: arguments) + String(format: localized, arguments: arguments) } -} \ No newline at end of file +} diff --git a/App/UI/Views/Chat/ChatView.swift b/Archived/v1/code/App/UI/Views/Chat/ChatView.swift similarity index 96% rename from App/UI/Views/Chat/ChatView.swift rename to Archived/v1/code/App/UI/Views/Chat/ChatView.swift index 431eb9c..0e8a9ee 100644 --- a/App/UI/Views/Chat/ChatView.swift +++ b/Archived/v1/code/App/UI/Views/Chat/ChatView.swift @@ -4,7 +4,7 @@ import SwiftUI struct ChatView: View { @StateObject private var viewModel = ChatViewModel() @FocusState private var isInputFocused: Bool - + var body: some View { VStack(spacing: 0) { // Message List @@ -26,7 +26,7 @@ struct ChatView: View { } } } - + // Input Field VStack(spacing: 0) { Divider() @@ -34,12 +34,12 @@ struct ChatView: View { TextField("Type a message...", text: $viewModel.inputText, axis: .vertical) .textFieldStyle(.plain) .focused($isInputFocused) - .lineLimit(1...5) + .lineLimit(1 ... 5) .padding(.horizontal, 12) .padding(.vertical, 8) .background(Color(.textBackgroundColor)) .cornerRadius(8) - + Button(action: { Task { await viewModel.sendMessage() @@ -64,25 +64,25 @@ struct ChatView: View { /// A view that displays a single message bubble struct MessageBubble: View { let message: ChatMessage - + var body: some View { HStack { if message.isUser { Spacer() } - + VStack(alignment: message.isUser ? .trailing : .leading) { Text(message.content) .padding(12) .background(message.isUser ? Color.accentColor : Color(.textBackgroundColor)) .foregroundColor(message.isUser ? .white : .primary) .cornerRadius(16) - + Text(message.timestamp.formatted(.dateTime.hour().minute())) .font(.caption2) .foregroundColor(.secondary) } - + if !message.isUser { Spacer() } @@ -92,4 +92,4 @@ struct MessageBubble: View { #Preview { ChatView() -} \ No newline at end of file +} diff --git a/App/UI/Views/Chat/ChatViewModel.swift b/Archived/v1/code/App/UI/Views/Chat/ChatViewModel.swift similarity index 96% rename from App/UI/Views/Chat/ChatViewModel.swift rename to Archived/v1/code/App/UI/Views/Chat/ChatViewModel.swift index 7f130f5..e7991b6 100644 --- a/App/UI/Views/Chat/ChatViewModel.swift +++ b/Archived/v1/code/App/UI/Views/Chat/ChatViewModel.swift @@ -6,19 +6,19 @@ import SwiftUI class ChatViewModel: ObservableObject { /// The current list of messages in the chat @Published private(set) var messages: [ChatMessage] = [] - + /// The current input text in the message field @Published var inputText: String = "" - + /// Whether the chat is currently processing a message @Published private(set) var isProcessing: Bool = false - + /// The current error state, if any @Published private(set) var error: Error? - + private let aiService: AIService private let storageManager: StorageManager - + init(aiService: AIService = AIService(), storageManager: StorageManager = StorageManager()) { self.aiService = aiService self.storageManager = storageManager @@ -26,13 +26,13 @@ class ChatViewModel: ObservableObject { await loadMessages() } } - + /// Sends the current input text as a message /// - Returns: Void func sendMessage() async { let trimmedText = inputText.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedText.isEmpty else { return } - + // Create and add user message let userMessage = ChatMessage( content: trimmedText, @@ -40,15 +40,15 @@ class ChatViewModel: ObservableObject { timestamp: Date() ) messages.append(userMessage) - + // Clear input inputText = "" - + // Process with AI isProcessing = true do { let response = try await aiService.sendMessage(trimmedText) - + // Create and add AI response let aiMessage = ChatMessage( content: response, @@ -56,7 +56,7 @@ class ChatViewModel: ObservableObject { timestamp: Date() ) messages.append(aiMessage) - + // Save messages try await storageManager.saveMessages(messages) } catch { @@ -71,7 +71,7 @@ class ChatViewModel: ObservableObject { } isProcessing = false } - + /// Loads saved messages from storage private func loadMessages() async { do { @@ -80,7 +80,7 @@ class ChatViewModel: ObservableObject { self.error = error } } - + /// Clears all messages from the chat func clearMessages() async { messages.removeAll() @@ -98,11 +98,11 @@ struct ChatMessage: Identifiable, Codable { let content: String let isUser: Bool let timestamp: Date - + init(content: String, isUser: Bool, timestamp: Date = Date()) { - self.id = UUID() + id = UUID() self.content = content self.isUser = isUser self.timestamp = timestamp } -} \ No newline at end of file +} diff --git a/App/UI/Views/Main/MainChatView.swift b/Archived/v1/code/App/UI/Views/Main/MainChatView.swift similarity index 97% rename from App/UI/Views/Main/MainChatView.swift rename to Archived/v1/code/App/UI/Views/Main/MainChatView.swift index a1fca3e..6d9c121 100644 --- a/App/UI/Views/Main/MainChatView.swift +++ b/Archived/v1/code/App/UI/Views/Main/MainChatView.swift @@ -4,7 +4,7 @@ import SwiftUI struct MainChatView: View { @StateObject private var viewModel = WebViewModel() @State private var isShowingServiceSelector = false - + var body: some View { VStack(spacing: 0) { // Top toolbar @@ -28,9 +28,9 @@ struct MainChatView: View { .popover(isPresented: $isShowingServiceSelector) { ServiceSelectorView(viewModel: viewModel) } - + Spacer() - + // Refresh button Button(action: { viewModel.loadSelectedService() @@ -50,13 +50,13 @@ struct MainChatView: View { .foregroundColor(Color.gray.opacity(0.2)), alignment: .bottom ) - + // Web view container ZStack { WebViewWrapper(url: $viewModel.currentURL) { url in viewModel.handleNavigationFinished(url: url) } - + if viewModel.isLoading { ProgressView() .scaleEffect(1.5) @@ -73,7 +73,7 @@ struct MainChatView: View { struct ServiceSelectorView: View { @ObservedObject var viewModel: WebViewModel @Environment(\.presentationMode) var presentationMode - + var body: some View { VStack(alignment: .leading, spacing: 0) { ForEach(WebViewModel.AIService.allCases) { service in @@ -95,7 +95,7 @@ struct ServiceSelectorView: View { .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) - + if service != WebViewModel.AIService.allCases.last { Divider() .padding(.leading, 16) diff --git a/App/UI/Views/Main/StatusBarView.swift b/Archived/v1/code/App/UI/Views/Main/StatusBarView.swift similarity index 97% rename from App/UI/Views/Main/StatusBarView.swift rename to Archived/v1/code/App/UI/Views/Main/StatusBarView.swift index 52ade4d..7eb37bb 100644 --- a/App/UI/Views/Main/StatusBarView.swift +++ b/Archived/v1/code/App/UI/Views/Main/StatusBarView.swift @@ -1,5 +1,5 @@ -import SwiftUI import AppKit +import SwiftUI /// Status bar controller for the app @MainActor @@ -7,19 +7,19 @@ class StatusBarController { private var statusBar: NSStatusBar private var statusItem: NSStatusItem private var popover: NSPopover - + init(popover: NSPopover) { self.popover = popover statusBar = NSStatusBar.system statusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength) - + if let statusBarButton = statusItem.button { statusBarButton.image = NSImage(systemSymbolName: "bubble.left.fill", accessibilityDescription: "MinimalAIChat") statusBarButton.action = #selector(togglePopover) statusBarButton.target = self } } - + @objc func togglePopover() { if popover.isShown { hidePopover() @@ -27,13 +27,13 @@ class StatusBarController { showPopover() } } - + func showPopover() { if let statusBarButton = statusItem.button { popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.minY) } } - + func hidePopover() { popover.performClose(nil) } diff --git a/App/UI/Views/Main/WebViewWrapper.swift b/Archived/v1/code/App/UI/Views/Main/WebViewWrapper.swift similarity index 81% rename from App/UI/Views/Main/WebViewWrapper.swift rename to Archived/v1/code/App/UI/Views/Main/WebViewWrapper.swift index c30affc..6c9f0f8 100644 --- a/App/UI/Views/Main/WebViewWrapper.swift +++ b/Archived/v1/code/App/UI/Views/Main/WebViewWrapper.swift @@ -5,33 +5,33 @@ import WebKit struct WebViewWrapper: NSViewRepresentable { @Binding var url: URL var onNavigationFinished: ((URL) -> Void)? - + func makeNSView(context: Context) -> WKWebView { let webView = WKWebView() webView.navigationDelegate = context.coordinator return webView } - - func updateNSView(_ webView: WKWebView, context: Context) { + + func updateNSView(_ webView: WKWebView, context _: Context) { let request = URLRequest(url: url) webView.load(request) } - + func makeCoordinator() -> Coordinator { Coordinator(self) } - + class Coordinator: NSObject, WKNavigationDelegate { var parent: WebViewWrapper - + init(_ parent: WebViewWrapper) { self.parent = parent } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + + func webView(_ webView: WKWebView, didFinish _: WKNavigation!) { if let url = webView.url { parent.onNavigationFinished?(url) } } } -} \ No newline at end of file +} diff --git a/App/Utilities/MemoryOptimizer.swift b/Archived/v1/code/App/Utilities/MemoryOptimizer.swift similarity index 96% rename from App/Utilities/MemoryOptimizer.swift rename to Archived/v1/code/App/Utilities/MemoryOptimizer.swift index f5695de..40db2fb 100644 --- a/App/Utilities/MemoryOptimizer.swift +++ b/Archived/v1/code/App/Utilities/MemoryOptimizer.swift @@ -1,5 +1,5 @@ -import Foundation import AppKit +import Foundation import WebKit /// A class that handles memory optimization for the application @@ -63,7 +63,7 @@ public final class MemoryOptimizer: Sendable { private let webViewCleanupActor: WebViewCleanupActor private var pressureObserver: MemoryPressureObserver? private let logger = Logger(label: "com.minimalaichat.memoryoptimizer") - + public init(webViewCleanupActor: WebViewCleanupActor = WebViewCleanupActor()) { self.webViewCleanupActor = webViewCleanupActor let handler: (MemoryPressureLevel) -> Void = { [weak self] level in @@ -71,19 +71,19 @@ public final class MemoryOptimizer: Sendable { await self?.handleMemoryPressure(level) } } - self.pressureObserver = MemoryPressureObserver(handler: handler) + pressureObserver = MemoryPressureObserver(handler: handler) } - + /// Starts monitoring memory pressure func startMonitoring() { pressureObserver?.startObserving() } - + /// Stops monitoring memory pressure func stopMonitoring() { pressureObserver?.stopObserving() } - + /// Handles memory pressure events by performing appropriate cleanup operations /// - Parameter level: The current memory pressure level private func handleMemoryPressure(_ level: MemoryPressureLevel) async { @@ -101,44 +101,44 @@ public final class MemoryOptimizer: Sendable { break } } - + /// Optimizes memory usage by cleaning up resources public func optimizeMemoryUsage() async { do { // Clean up WebKit resources using the actor try await webViewCleanupActor.cleanup() - + // Clear image caches clearImageCaches() - + // Clear temporary files clearTemporaryFiles() - + logger.info("Memory optimization completed successfully") } catch { logger.error("Failed to optimize memory: \(error.localizedDescription)") } } - + private func clearImageCaches() { // Clear NSCache instances URLCache.shared.removeAllCachedResponses() - + // Clear any custom image caches // Add your custom image cache clearing logic here } - + private func clearTemporaryFiles() { let fileManager = FileManager.default let tempDirectory = fileManager.temporaryDirectory - + do { let tempFiles = try fileManager.contentsOfDirectory( at: tempDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles] ) - + for file in tempFiles { try? fileManager.removeItem(at: file) } diff --git a/App/Utilities/MemoryPressureObserver.swift b/Archived/v1/code/App/Utilities/MemoryPressureObserver.swift similarity index 98% rename from App/Utilities/MemoryPressureObserver.swift rename to Archived/v1/code/App/Utilities/MemoryPressureObserver.swift index 1d420b2..4982635 100644 --- a/App/Utilities/MemoryPressureObserver.swift +++ b/Archived/v1/code/App/Utilities/MemoryPressureObserver.swift @@ -1,5 +1,5 @@ -import Foundation import AppKit +import Foundation /// A class that observes system memory pressure and notifies when it changes /// @@ -15,11 +15,11 @@ class MemoryPressureObserver { private var timer: Timer? private let handler: (MemoryPressureLevel) -> Void private let checkInterval: TimeInterval = 5.0 // Check every 5 seconds - + init(handler: @escaping (MemoryPressureLevel) -> Void) { self.handler = handler } - + /// Starts observing memory pressure /// /// This method initializes the timer and performs an initial check. @@ -31,13 +31,13 @@ class MemoryPressureObserver { await self?.checkMemoryPressure() } } - + // Initial check Task { await checkMemoryPressure() } } - + /// Stops observing memory pressure. /// /// This method invalidates the timer and stops all memory pressure checks. @@ -47,20 +47,20 @@ class MemoryPressureObserver { timer = nil } } - + /// Checks the current memory pressure level and calls the handler if it has changed private func checkMemoryPressure() async { let level = await determineMemoryPressureLevel() handler(level) } - + /// Determines the current memory pressure level based on system metrics /// /// Returns: The current MemoryPressureLevel private func determineMemoryPressureLevel() async -> MemoryPressureLevel { let processInfo = ProcessInfo.processInfo let isOperatingSystemAtLeast = processInfo.isOperatingSystemAtLeast - + if isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 10, minorVersion: 10, patchVersion: 0)) { switch processInfo.thermalState { case .nominal: @@ -78,7 +78,7 @@ class MemoryPressureObserver { // Fallback for older OS versions let memoryPressure = Double(processInfo.physicalMemory) let totalMemory = Double(ProcessInfo.processInfo.physicalMemory) - + if memoryPressure < totalMemory * 0.7 { return .normal } else if memoryPressure < totalMemory * 0.85 { @@ -90,7 +90,7 @@ class MemoryPressureObserver { } } } - + deinit { stopObserving() } diff --git a/App/Views/Settings/SettingsView.swift b/Archived/v1/code/App/Views/Settings/SettingsView.swift similarity index 97% rename from App/Views/Settings/SettingsView.swift rename to Archived/v1/code/App/Views/Settings/SettingsView.swift index 5447366..600f6b2 100644 --- a/App/Views/Settings/SettingsView.swift +++ b/Archived/v1/code/App/Views/Settings/SettingsView.swift @@ -5,7 +5,7 @@ struct SettingsView: View { @State private var apiKey: String = "" @State private var selectedKey: KeyCode = .space @State private var selectedModifiers: Set = [.command] - + var body: some View { Form { Group { @@ -18,7 +18,7 @@ struct SettingsView: View { } header: { Text("Service Type") } - + Section { SecureField("API Key", text: $apiKey) .textFieldStyle(.roundedBorder) @@ -34,7 +34,7 @@ struct SettingsView: View { } header: { Text("API Key") } - + Section { Picker("Theme", selection: $settingsManager.selectedTheme) { ForEach(Theme.allCases, id: \.self) { theme in @@ -44,14 +44,14 @@ struct SettingsView: View { } header: { Text("Theme") } - + Section { Picker("Key", selection: $selectedKey) { ForEach([KeyCode.space, .return, .tab, .escape], id: \.self) { key in Text(key.rawValue).tag(key) } } - + Toggle("Command", isOn: Binding( get: { selectedModifiers.contains(.command) }, set: { toggleModifier(.command, $0) } @@ -79,7 +79,7 @@ struct SettingsView: View { Text(settingsManager.errorMessage ?? "Unknown error") } } - + private func toggleModifier(_ modifier: KeyModifier, _ isOn: Bool) { if isOn { selectedModifiers.insert(modifier) @@ -90,9 +90,10 @@ struct SettingsView: View { } // MARK: - General Settings + private struct GeneralSettingsView: View { @ObservedObject var settingsManager: SettingsManager - + var body: some View { Form { Section { @@ -104,7 +105,7 @@ private struct GeneralSettingsView: View { } header: { Text("Service Type") } - + Section { Picker("Model", selection: Binding( get: { settingsManager.getModel() }, @@ -122,11 +123,12 @@ private struct GeneralSettingsView: View { } // MARK: - API Settings + private struct APISettingsView: View { @ObservedObject var settingsManager: SettingsManager @Binding var apiKey: String @Binding var showingError: Bool - + var body: some View { Form { Section { @@ -135,7 +137,7 @@ private struct APISettingsView: View { } header: { Text("API Key") } - + Section { Button("Save API Key") { do { @@ -157,9 +159,10 @@ private struct APISettingsView: View { } // MARK: - Appearance Settings + private struct AppearanceSettingsView: View { @ObservedObject var settingsManager: SettingsManager - + var body: some View { Form { Section { @@ -171,7 +174,7 @@ private struct AppearanceSettingsView: View { } header: { Text("Theme") } - + Section { ColorPicker("Accent Color", selection: Binding( get: { settingsManager.getAccentColor() }, @@ -186,12 +189,13 @@ private struct AppearanceSettingsView: View { } // MARK: - Hotkey Settings + private struct HotkeySettingsView: View { @ObservedObject var settingsManager: SettingsManager @Binding var showingError: Bool @State private var selectedKey: KeyCode = .space @State private var selectedModifiers: Set = [.command] - + var body: some View { Form { Section("Global Hotkey") { @@ -200,27 +204,27 @@ private struct HotkeySettingsView: View { Text(key.rawValue.capitalized).tag(key) } } - + Toggle("Command", isOn: Binding( get: { selectedModifiers.contains(.command) }, set: { toggleModifier(.command, $0) } )) - + Toggle("Shift", isOn: Binding( get: { selectedModifiers.contains(.shift) }, set: { toggleModifier(.shift, $0) } )) - + Toggle("Option", isOn: Binding( get: { selectedModifiers.contains(.option) }, set: { toggleModifier(.option, $0) } )) - + Toggle("Control", isOn: Binding( get: { selectedModifiers.contains(.control) }, set: { toggleModifier(.control, $0) } )) - + Button("Save Hotkey") { do { try settingsManager.setGlobalHotkey(Hotkey(key: selectedKey, modifiers: selectedModifiers)) @@ -238,7 +242,7 @@ private struct HotkeySettingsView: View { Text(settingsManager.error ?? "Unknown error") } } - + private func toggleModifier(_ modifier: KeyModifier, _ isOn: Bool) { if isOn { selectedModifiers.insert(modifier) @@ -250,4 +254,4 @@ private struct HotkeySettingsView: View { #Preview { SettingsView(settingsManager: SettingsManager()) -} \ No newline at end of file +} diff --git a/App/main.swift b/Archived/v1/code/App/main.swift similarity index 100% rename from App/main.swift rename to Archived/v1/code/App/main.swift diff --git a/Sources/Keychain/Keychain-Swift.h b/Archived/v1/code/Sources/Keychain/Keychain-Swift.h similarity index 100% rename from Sources/Keychain/Keychain-Swift.h rename to Archived/v1/code/Sources/Keychain/Keychain-Swift.h diff --git a/Sources/Keychain/Keychain.h b/Archived/v1/code/Sources/Keychain/Keychain.h similarity index 100% rename from Sources/Keychain/Keychain.h rename to Archived/v1/code/Sources/Keychain/Keychain.h diff --git a/Sources/Keychain/Keychain.swift b/Archived/v1/code/Sources/Keychain/Keychain.swift similarity index 99% rename from Sources/Keychain/Keychain.swift rename to Archived/v1/code/Sources/Keychain/Keychain.swift index 90e9a83..a5e6095 100644 --- a/Sources/Keychain/Keychain.swift +++ b/Archived/v1/code/Sources/Keychain/Keychain.swift @@ -41,7 +41,7 @@ public enum Keychain { /// The service identifier for the keychain public static let service = "com.minimalaichat.keychain" - + /// The default accessibility setting for keychain items public static let defaultAccessibility = kSecAttrAccessibleAfterFirstUnlock -} \ No newline at end of file +} diff --git a/Sources/Keychain/KeychainError.swift b/Archived/v1/code/Sources/Keychain/KeychainError.swift similarity index 79% rename from Sources/Keychain/KeychainError.swift rename to Archived/v1/code/Sources/Keychain/KeychainError.swift index 857821d..dea35d9 100644 --- a/Sources/Keychain/KeychainError.swift +++ b/Archived/v1/code/Sources/Keychain/KeychainError.swift @@ -34,26 +34,26 @@ public enum KeychainError: LocalizedError { case readError(status: OSStatus) case updateError(status: OSStatus) case deleteError(status: OSStatus) - + public var errorDescription: String? { switch self { - case .saveError(let status): + case let .saveError(status): return "Failed to save to keychain: \(status)" - case .readError(let status): + case let .readError(status): return "Failed to read from keychain: \(status)" - case .updateError(let status): + case let .updateError(status): return "Failed to update keychain item: \(status)" - case .deleteError(let status): + case let .deleteError(status): return "Failed to delete from keychain: \(status)" } } - + public var errorCode: Int { switch self { - case .saveError(let status): return Int(status) - case .readError(let status): return Int(status) - case .updateError(let status): return Int(status) - case .deleteError(let status): return Int(status) + case let .saveError(status): return Int(status) + case let .readError(status): return Int(status) + case let .updateError(status): return Int(status) + case let .deleteError(status): return Int(status) } } -} \ No newline at end of file +} diff --git a/Sources/Keychain/KeychainManager.swift b/Archived/v1/code/Sources/Keychain/KeychainManager.swift similarity index 91% rename from Sources/Keychain/KeychainManager.swift rename to Archived/v1/code/Sources/Keychain/KeychainManager.swift index 79ad59c..a86478e 100644 --- a/Sources/Keychain/KeychainManager.swift +++ b/Archived/v1/code/Sources/Keychain/KeychainManager.swift @@ -1,6 +1,6 @@ import Foundation -import Security import os.log +import Security /// A class that manages secure storage operations using the system keychain /// @@ -40,9 +40,9 @@ import os.log public final class KeychainManager { private let service = Keychain.service private let logger = Logger(subsystem: "com.minimalaichat", category: "KeychainManager") - + public init() {} - + /// Saves data to the keychain /// /// - Parameters: @@ -55,11 +55,11 @@ public final class KeychainManager { kSecAttrService as String: service, kSecAttrAccount as String: key, kSecValueData as String: data, - kSecAttrAccessible as String: Keychain.defaultAccessibility + kSecAttrAccessible as String: Keychain.defaultAccessibility, ] - + let status = SecItemAdd(query as CFDictionary, nil) - + if status == errSecDuplicateItem { try update(data, for: key) } else if status != errSecSuccess { @@ -67,7 +67,7 @@ public final class KeychainManager { throw KeychainError.saveError(status: status) } } - + /// Retrieves data from the keychain /// /// - Parameter key: The key associated with the data @@ -78,21 +78,22 @@ public final class KeychainManager { kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, - kSecReturnData as String: true + kSecReturnData as String: true, ] - + var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) - + guard status == errSecSuccess, - let data = result as? Data else { + let data = result as? Data + else { logger.error("Failed to get data: \(status, privacy: .public)") throw KeychainError.readError(status: status) } - + return data } - + /// Updates data in the keychain /// /// - Parameters: @@ -103,21 +104,21 @@ public final class KeychainManager { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, - kSecAttrAccount as String: key + kSecAttrAccount as String: key, ] - + let attributes: [String: Any] = [ - kSecValueData as String: data + kSecValueData as String: data, ] - + let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) - + if status != errSecSuccess { logger.error("Failed to update data: \(status, privacy: .public)") throw KeychainError.updateError(status: status) } } - + /// Deletes data from the keychain /// /// - Parameter key: The key associated with the data @@ -126,14 +127,14 @@ public final class KeychainManager { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, - kSecAttrAccount as String: key + kSecAttrAccount as String: key, ] - + let status = SecItemDelete(query as CFDictionary) - - if status != errSecSuccess && status != errSecItemNotFound { + + if status != errSecSuccess, status != errSecItemNotFound { logger.error("Failed to delete data: \(status, privacy: .public)") throw KeychainError.deleteError(status: status) } } -} \ No newline at end of file +} diff --git a/Sources/Keychain/module.modulemap b/Archived/v1/code/Sources/Keychain/module.modulemap similarity index 100% rename from Sources/Keychain/module.modulemap rename to Archived/v1/code/Sources/Keychain/module.modulemap diff --git a/Sources/MinimalAIChat/Core/Services/KeychainManager.swift b/Archived/v1/code/Sources/MinimalAIChat/Core/Services/KeychainManager.swift similarity index 84% rename from Sources/MinimalAIChat/Core/Services/KeychainManager.swift rename to Archived/v1/code/Sources/MinimalAIChat/Core/Services/KeychainManager.swift index e72c66b..363613f 100644 --- a/Sources/MinimalAIChat/Core/Services/KeychainManager.swift +++ b/Archived/v1/code/Sources/MinimalAIChat/Core/Services/KeychainManager.swift @@ -1,34 +1,34 @@ - import Foundation +import Foundation import Security class KeychainManager: KeychainManagerProtocol { private let service = Bundle.main.bundleIdentifier ?? "com.minimalai.chat" - + func store(_ value: String, for key: String) throws { let data = value.data(using: .utf8)! - + let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, - kSecValueData as String: data + kSecValueData as String: data, ] - + let status = SecItemAdd(query as CFDictionary, nil) - + if status == errSecDuplicateItem { let updateQuery: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, - kSecAttrAccount as String: key + kSecAttrAccount as String: key, ] - + let attributesToUpdate: [String: Any] = [ - kSecValueData as String: data + kSecValueData as String: data, ] - + let updateStatus = SecItemUpdate(updateQuery as CFDictionary, attributesToUpdate as CFDictionary) - + guard updateStatus == errSecSuccess else { throw KeychainError.unhandledError(NSError(domain: NSOSStatusErrorDomain, code: Int(updateStatus))) } @@ -36,44 +36,45 @@ class KeychainManager: KeychainManagerProtocol { throw KeychainError.unhandledError(NSError(domain: NSOSStatusErrorDomain, code: Int(status))) } } - + func retrieve(for key: String) throws -> String { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: key, - kSecReturnData as String: true + kSecReturnData as String: true, ] - + var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) - + guard status == errSecSuccess else { if status == errSecItemNotFound { throw KeychainError.itemNotFound } throw KeychainError.unhandledError(NSError(domain: NSOSStatusErrorDomain, code: Int(status))) } - + guard let data = result as? Data, - let string = String(data: data, encoding: .utf8) else { + let string = String(data: data, encoding: .utf8) + else { throw KeychainError.invalidItemFormat } - + return string } - + func delete(for key: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, - kSecAttrAccount as String: key + kSecAttrAccount as String: key, ] - + let status = SecItemDelete(query as CFDictionary) - + guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.unhandledError(NSError(domain: NSOSStatusErrorDomain, code: Int(status))) } } -} \ No newline at end of file +} diff --git a/Sources/MinimalAIChat/Core/Services/StorageManager.swift b/Archived/v1/code/Sources/MinimalAIChat/Core/Services/StorageManager.swift similarity index 97% rename from Sources/MinimalAIChat/Core/Services/StorageManager.swift rename to Archived/v1/code/Sources/MinimalAIChat/Core/Services/StorageManager.swift index 1797849..6c95331 100644 --- a/Sources/MinimalAIChat/Core/Services/StorageManager.swift +++ b/Archived/v1/code/Sources/MinimalAIChat/Core/Services/StorageManager.swift @@ -6,36 +6,36 @@ import MinimalAIChat class StorageManager { private let fileManager = FileManager.default private let documentsPath: URL - + init() { documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] } - + private var chatHistoryURL: URL { documentsPath.appendingPathComponent("chat_history.json") } - + func saveMessages(_ messages: [ChatMessage]) throws { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 let data = try encoder.encode(messages) try data.write(to: chatHistoryURL) } - + func loadMessages() throws -> [ChatMessage] { guard fileManager.fileExists(atPath: chatHistoryURL.path) else { return [] } - + let data = try Data(contentsOf: chatHistoryURL) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return try decoder.decode([ChatMessage].self, from: data) } - + func clearMessages() throws { if fileManager.fileExists(atPath: chatHistoryURL.path) { try fileManager.removeItem(at: chatHistoryURL) } } -} \ No newline at end of file +} diff --git a/Tests/Integration/Hotkey/HotkeyIntegrationTests.swift b/Archived/v1/code/Tests/Integration/Hotkey/HotkeyIntegrationTests.swift similarity index 96% rename from Tests/Integration/Hotkey/HotkeyIntegrationTests.swift rename to Archived/v1/code/Tests/Integration/Hotkey/HotkeyIntegrationTests.swift index f7840ac..72a42ce 100644 --- a/Tests/Integration/Hotkey/HotkeyIntegrationTests.swift +++ b/Archived/v1/code/Tests/Integration/Hotkey/HotkeyIntegrationTests.swift @@ -1,79 +1,79 @@ -import XCTest @testable import MinimalAIChat +import XCTest class HotkeyIntegrationTests: XCTestCase { var settingsManager: SettingsManager! var hotKeysController: HotKeysController! - + override func setUp() { super.setUp() settingsManager = SettingsManager.shared hotKeysController = HotKeysController.shared } - + override func tearDown() { settingsManager = nil hotKeysController = nil super.tearDown() } - + func testHotkeyRegistrationThroughSettings() { // Set up a test hotkey in settings let testHotkey = Hotkey(keyCode: .space, modifiers: [.command]) settingsManager.setGlobalHotkey(testHotkey) - + // Verify the hotkey is registered XCTAssertTrue(hotKeysController.isHotkeyRegistered(testHotkey.keyCombo)) - + // Change the hotkey in settings let newHotkey = Hotkey(keyCode: .return, modifiers: [.command]) settingsManager.setGlobalHotkey(newHotkey) - + // Verify old hotkey is unregistered and new one is registered XCTAssertFalse(hotKeysController.isHotkeyRegistered(testHotkey.keyCombo)) XCTAssertTrue(hotKeysController.isHotkeyRegistered(newHotkey.keyCombo)) } - + func testHotkeyPersistence() { // Set up a test hotkey let testHotkey = Hotkey(keyCode: .space, modifiers: [.command]) settingsManager.setGlobalHotkey(testHotkey) - + // Create a new instance of SettingsManager to simulate app restart let newSettingsManager = SettingsManager.shared - + // Verify the hotkey is still registered XCTAssertTrue(hotKeysController.isHotkeyRegistered(testHotkey.keyCombo)) - + // Verify the hotkey is still in settings let savedHotkey = newSettingsManager.getGlobalHotkey() XCTAssertEqual(savedHotkey?.keyCode, testHotkey.keyCode) XCTAssertEqual(savedHotkey?.modifiers, testHotkey.modifiers) } - + func testInvalidHotkeyHandling() { // Try to register an invalid hotkey let invalidHotkey = Hotkey(keyCode: .space, modifiers: []) settingsManager.setGlobalHotkey(invalidHotkey) - + // Verify the hotkey is not registered XCTAssertFalse(hotKeysController.isHotkeyRegistered(invalidHotkey.keyCombo)) - + // Verify the settings still have the previous hotkey (if any) let savedHotkey = settingsManager.getGlobalHotkey() XCTAssertNotEqual(savedHotkey?.keyCombo, invalidHotkey.keyCombo) } - + func testHotkeyConflictHandling() { // Register a hotkey let hotkey1 = Hotkey(keyCode: .space, modifiers: [.command]) settingsManager.setGlobalHotkey(hotkey1) - + // Try to register the same hotkey again settingsManager.setGlobalHotkey(hotkey1) - + // Verify only one instance is registered let registeredCount = hotKeysController.registeredHotKeys.filter { $0.combo == hotkey1.keyCombo }.count XCTAssertEqual(registeredCount, 1) } -} \ No newline at end of file +} diff --git a/Tests/Integration/Settings/SettingsIntegrationTests.swift b/Archived/v1/code/Tests/Integration/Settings/SettingsIntegrationTests.swift similarity index 92% rename from Tests/Integration/Settings/SettingsIntegrationTests.swift rename to Archived/v1/code/Tests/Integration/Settings/SettingsIntegrationTests.swift index b0fe4c0..4851115 100644 --- a/Tests/Integration/Settings/SettingsIntegrationTests.swift +++ b/Archived/v1/code/Tests/Integration/Settings/SettingsIntegrationTests.swift @@ -1,6 +1,6 @@ -import Quick -import Nimble @testable import MinimalAIChat +import Nimble +import Quick class SettingsIntegrationTests: QuickSpec { override func spec() { @@ -8,100 +8,100 @@ class SettingsIntegrationTests: QuickSpec { var settingsManager: SettingsManager! var keychainManager: KeychainManager! var userDefaults: UserDefaults! - + beforeEach { // Use a separate UserDefaults suite for testing userDefaults = UserDefaults(suiteName: "com.minimalaichat.test") keychainManager = KeychainManager() settingsManager = SettingsManager(keychainManager: keychainManager) - + // Clear test data userDefaults.removePersistentDomain(forName: "com.minimalaichat.test") try? keychainManager.delete(for: "apiKey") } - + afterEach { // Clean up test data userDefaults.removePersistentDomain(forName: "com.minimalaichat.test") try? keychainManager.delete(for: "apiKey") } - + context("API Key Integration") { it("should persist API key across app launches") { let apiKey = "test-api-key" try? settingsManager.setAPIKey(apiKey) - + // Simulate app relaunch let newSettingsManager = SettingsManager(keychainManager: keychainManager) let retrievedKey = try? newSettingsManager.getAPIKey() - + expect(retrievedKey).to(equal(apiKey)) } - + it("should handle API key rotation") { let oldKey = "old-api-key" let newKey = "new-api-key" - + try? settingsManager.setAPIKey(oldKey) try? settingsManager.setAPIKey(newKey) - + let retrievedKey = try? settingsManager.getAPIKey() expect(retrievedKey).to(equal(newKey)) } } - + context("Service Selection Integration") { it("should persist service type selection") { settingsManager.setServiceType(.webWrapper) - + // Simulate app relaunch let newSettingsManager = SettingsManager(keychainManager: keychainManager) expect(newSettingsManager.getServiceType()).to(equal(.webWrapper)) } - + it("should update AI service based on selection") { settingsManager.setServiceType(.directAPI) let aiService = AIService(serviceType: settingsManager.getServiceType()) expect(aiService.serviceType).to(equal(.directAPI)) } } - + context("Theme Integration") { it("should apply theme changes immediately") { settingsManager.setTheme(.dark) let theme = settingsManager.getTheme() expect(theme).to(equal(.dark)) } - + it("should persist theme selection") { settingsManager.setTheme(.light) - + // Simulate app relaunch let newSettingsManager = SettingsManager(keychainManager: keychainManager) expect(newSettingsManager.getTheme()).to(equal(.light)) } } - + context("Hotkey Integration") { it("should register global hotkey") { let hotkey = Hotkey(key: .space, modifiers: [.command]) try? settingsManager.setGlobalHotkey(hotkey) - + // Verify hotkey registration let registeredHotkey = settingsManager.getGlobalHotkey() expect(registeredHotkey).to(equal(hotkey)) } - + it("should handle hotkey conflicts") { let hotkey1 = Hotkey(key: .space, modifiers: [.command]) let hotkey2 = Hotkey(key: .space, modifiers: [.command, .shift]) - + try? settingsManager.setGlobalHotkey(hotkey1) try? settingsManager.setGlobalHotkey(hotkey2) - + expect(settingsManager.getGlobalHotkey()).to(equal(hotkey2)) } } } } -} \ No newline at end of file +} diff --git a/Tests/MinimalAIChatTests/ChatViewModelTests.swift b/Archived/v1/code/Tests/MinimalAIChatTests/ChatViewModelTests.swift similarity index 95% rename from Tests/MinimalAIChatTests/ChatViewModelTests.swift rename to Archived/v1/code/Tests/MinimalAIChatTests/ChatViewModelTests.swift index 2e74df0..90f8a21 100644 --- a/Tests/MinimalAIChatTests/ChatViewModelTests.swift +++ b/Archived/v1/code/Tests/MinimalAIChatTests/ChatViewModelTests.swift @@ -1,3 +1,7 @@ +import Combine +@testable import MinimalAIChat +import WebKit + /// Tests for the ChatViewModel class that manages chat interface and WebView interactions /// /// This test suite verifies the functionality of ChatViewModel, including: @@ -42,28 +46,25 @@ /// try await testSuite.tearDown() /// ``` import XCTest -import WebKit -import Combine -@testable import MinimalAIChat @MainActor final class ChatViewModelTests: XCTestCase { // MARK: - Properties - + /// The view model being tested var viewModel: ChatViewModel! - + /// Mock WebView manager for testing WebView interactions var mockWebViewManager: MockWebViewManager! - + /// Mock storage manager for testing persistence var mockStorageManager: MockStorageManager! - + /// Mock settings manager for testing configuration var mockSettingsManager: MockSettingsManager! - + // MARK: - Setup and Teardown - + override func setUp() async throws { try await super.setUp() mockWebViewManager = MockWebViewManager() @@ -75,7 +76,7 @@ final class ChatViewModelTests: XCTestCase { settingsManager: mockSettingsManager ) } - + override func tearDown() async throws { viewModel = nil mockWebViewManager = nil @@ -83,9 +84,9 @@ final class ChatViewModelTests: XCTestCase { mockSettingsManager = nil try await super.tearDown() } - + // MARK: - Message Tests - + /// Tests the message sending functionality /// /// Verifies that: @@ -95,17 +96,17 @@ final class ChatViewModelTests: XCTestCase { func testSendMessage() async throws { // Given let message = "Test message" - + // When viewModel.sendMessage(message) - + // Then XCTAssertEqual(viewModel.messages.count, 1) XCTAssertEqual(viewModel.messages.first?.content, message) XCTAssertTrue(viewModel.isLoading) XCTAssertTrue(mockWebViewManager.injectMessageCalled) } - + /// Tests the chat clearing functionality /// /// Verifies that: @@ -115,18 +116,18 @@ final class ChatViewModelTests: XCTestCase { func testClearChat() async throws { // Given viewModel.sendMessage("Test message") - + // When viewModel.clearChat() - + // Then XCTAssertTrue(viewModel.messages.isEmpty) XCTAssertTrue(mockStorageManager.clearMessagesCalled) XCTAssertTrue(mockWebViewManager.clearWebViewCalled) } - + // MARK: - WebView Tests - + /// Tests the WebView initialization /// /// Verifies that: @@ -136,14 +137,14 @@ final class ChatViewModelTests: XCTestCase { func testInitializeWebView() async throws { // When viewModel.initializeWebView() - + // Then XCTAssertTrue(mockWebViewManager.createWebViewCalled) XCTAssertTrue(mockWebViewManager.loadAIServiceCalled) } - + // MARK: - Error Handling Tests - + /// Tests error handling through WebView manager /// /// Verifies that: @@ -154,16 +155,16 @@ final class ChatViewModelTests: XCTestCase { // Given let message = "Test message" let error = NSError(domain: "test", code: -1) - + // When viewModel.sendMessage(message) mockWebViewManager.simulateError(error) - + // Then XCTAssertTrue(viewModel.showError) XCTAssertEqual(viewModel.error?.localizedDescription, error.localizedDescription) } - + /// Tests the retry mechanism for failed messages /// /// Verifies that: @@ -175,15 +176,15 @@ final class ChatViewModelTests: XCTestCase { let message = "Test message" viewModel.sendMessage(message) mockWebViewManager.simulateError(NSError(domain: "test", code: -1)) - + // When viewModel.retryLastMessage() - + // Then XCTAssertTrue(mockWebViewManager.clearWebViewCalled) XCTAssertTrue(viewModel.isLoading) } - + /// Tests message persistence /// /// Verifies that: @@ -193,10 +194,10 @@ final class ChatViewModelTests: XCTestCase { func testMessagePersistence() async throws { // Given let message = "Test message" - + // When viewModel.sendMessage(message) - + // Then XCTAssertTrue(mockStorageManager.saveMessagesCalled) } @@ -212,38 +213,38 @@ final class ChatViewModelTests: XCTestCase { /// - Simplified WebView behavior class MockWebViewManager: WebViewManager { // MARK: - Properties - + var createWebViewCalled = false var loadAIServiceCalled = false var injectMessageCalled = false var clearWebViewCalled = false private var errorSubject = PassthroughSubject() - + // MARK: - WebViewManager Overrides - + override var error: AnyPublisher { errorSubject.eraseToAnyPublisher() } - + override func createWebView() -> WKWebView { createWebViewCalled = true return WKWebView() } - - override func loadAIService(url: URL) { + + override func loadAIService(url _: URL) { loadAIServiceCalled = true } - - override func injectMessage(_ message: String) { + + override func injectMessage(_: String) { injectMessageCalled = true } - + override func clearWebView() { clearWebViewCalled = true } - + // MARK: - Mock Methods - + /// Simulates an error in the WebView manager /// /// - Parameter error: The error to simulate @@ -260,17 +261,17 @@ class MockWebViewManager: WebViewManager { /// - No actual persistence class MockStorageManager: StorageManager { // MARK: - Properties - + var clearMessagesCalled = false var saveMessagesCalled = false - + // MARK: - StorageManager Overrides - + override func clearMessages() { clearMessagesCalled = true } - - override func saveMessages(_ messages: [ChatMessage]) { + + override func saveMessages(_: [ChatMessage]) { saveMessagesCalled = true } } @@ -283,13 +284,13 @@ class MockStorageManager: StorageManager { /// - No actual persistence class MockSettingsManager: SettingsManager { // MARK: - Properties - + private var _selectedAIService: AIService = .chatGPT - + // MARK: - SettingsManager Overrides - + override var selectedAIService: AIService { get { _selectedAIService } set { _selectedAIService = newValue } } -} \ No newline at end of file +} diff --git a/Tests/Performance/ChatPerformanceTests.swift b/Archived/v1/code/Tests/Performance/ChatPerformanceTests.swift similarity index 90% rename from Tests/Performance/ChatPerformanceTests.swift rename to Archived/v1/code/Tests/Performance/ChatPerformanceTests.swift index d989155..0027192 100644 --- a/Tests/Performance/ChatPerformanceTests.swift +++ b/Archived/v1/code/Tests/Performance/ChatPerformanceTests.swift @@ -1,126 +1,126 @@ -import XCTest @testable import MinimalAIChat +import XCTest final class ChatPerformanceTests: XCTestCase { var aiService: AIService! var chatViewModel: ChatViewModel! var storageManager: StorageManager! - + override func setUp() { super.setUp() aiService = AIService() storageManager = StorageManager() chatViewModel = ChatViewModel(aiService: aiService, storageManager: storageManager) } - + override func tearDown() { aiService = nil chatViewModel = nil storageManager = nil super.tearDown() } - + func testMessageSendingPerformance() throws { measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) { let expectation = XCTestExpectation(description: "Message sending") - + Task { await chatViewModel.sendMessage("Test message") expectation.fulfill() } - + wait(for: [expectation], timeout: 5.0) } } - + func testMessageLoadingPerformance() throws { // Create test messages - let messages = (0..<100).map { i in + let messages = (0 ..< 100).map { i in ChatMessage( content: "Test message \(i)", isUser: i % 2 == 0, timestamp: Date() ) } - + measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) { let expectation = XCTestExpectation(description: "Message loading") - + Task { try? await storageManager.saveMessages(messages) await chatViewModel.loadMessages() expectation.fulfill() } - + wait(for: [expectation], timeout: 5.0) } } - + func testMessageRenderingPerformance() throws { // Create a large number of messages - let messages = (0..<1000).map { i in + let messages = (0 ..< 1000).map { i in ChatMessage( content: "Test message \(i) with some longer content to test rendering performance", isUser: i % 2 == 0, timestamp: Date() ) } - + measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) { let expectation = XCTestExpectation(description: "Message rendering") - + Task { await chatViewModel.messages = messages expectation.fulfill() } - + wait(for: [expectation], timeout: 5.0) } } - + func testMessageStoragePerformance() throws { // Create test messages - let messages = (0..<1000).map { i in + let messages = (0 ..< 1000).map { i in ChatMessage( content: "Test message \(i)", isUser: i % 2 == 0, timestamp: Date() ) } - + measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) { let expectation = XCTestExpectation(description: "Message storage") - + Task { try? await storageManager.saveMessages(messages) expectation.fulfill() } - + wait(for: [expectation], timeout: 5.0) } } - + func testMessageRetrievalPerformance() throws { // Create and save test messages - let messages = (0..<1000).map { i in + let messages = (0 ..< 1000).map { i in ChatMessage( content: "Test message \(i)", isUser: i % 2 == 0, timestamp: Date() ) } - + try await storageManager.saveMessages(messages) - + measure(metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()]) { let expectation = XCTestExpectation(description: "Message retrieval") - + Task { _ = try? await storageManager.loadMessages() expectation.fulfill() } - + wait(for: [expectation], timeout: 5.0) } } -} \ No newline at end of file +} diff --git a/Tests/Performance/Hotkey/HotkeyPerformanceTests.swift b/Archived/v1/code/Tests/Performance/Hotkey/HotkeyPerformanceTests.swift similarity index 87% rename from Tests/Performance/Hotkey/HotkeyPerformanceTests.swift rename to Archived/v1/code/Tests/Performance/Hotkey/HotkeyPerformanceTests.swift index b08e6ca..48c4f2c 100644 --- a/Tests/Performance/Hotkey/HotkeyPerformanceTests.swift +++ b/Archived/v1/code/Tests/Performance/Hotkey/HotkeyPerformanceTests.swift @@ -1,104 +1,104 @@ -import XCTest @testable import MinimalAIChat +import XCTest class HotkeyPerformanceTests: XCTestCase { var hotKeysController: HotKeysController! var settingsManager: SettingsManager! - + override func setUp() { super.setUp() hotKeysController = HotKeysController.shared settingsManager = SettingsManager.shared } - + override func tearDown() { hotKeysController = nil settingsManager = nil super.tearDown() } - + func testHotkeyRegistrationPerformance() { measure { // Register 100 hotkeys - for i in 0..<100 { + for i in 0 ..< 100 { let hotkey = HotKey(keyCombo: KeyCombo(keyCode: .space, modifiers: [.command])) hotKeysController.registerHotKey(hotkey) } - + // Clean up hotKeysController.registeredHotKeys.removeAll() } } - + func testHotkeyLookupPerformance() { // Set up test data - let hotkeys = (0..<1000).map { _ in + let hotkeys = (0 ..< 1000).map { _ in HotKey(keyCombo: KeyCombo(keyCode: .space, modifiers: [.command])) } hotkeys.forEach { hotKeysController.registerHotKey($0) } - + measure { // Look up 1000 hotkeys - for _ in 0..<1000 { + for _ in 0 ..< 1000 { _ = hotKeysController.isHotkeyRegistered(KeyCombo(keyCode: .space, modifiers: [.command])) } } - + // Clean up hotKeysController.registeredHotKeys.removeAll() } - + func testHotkeyEventHandlingPerformance() { // Set up test data let hotkey = HotKey(keyCombo: KeyCombo(keyCode: .space, modifiers: [.command])) hotKeysController.registerHotKey(hotkey) - + measure { // Simulate 1000 hotkey events - for _ in 0..<1000 { + for _ in 0 ..< 1000 { hotkey.handleEvent() } } - + // Clean up hotKeysController.registeredHotKeys.removeAll() } - + func testSettingsHotkeyPersistencePerformance() { measure { // Save and load hotkey settings 100 times - for i in 0..<100 { + for i in 0 ..< 100 { let hotkey = Hotkey(keyCode: .space, modifiers: [.command]) settingsManager.setGlobalHotkey(hotkey) _ = settingsManager.getGlobalHotkey() } } } - + func testHotkeyConflictDetectionPerformance() { // Set up test data - let hotkeys = (0..<100).map { _ in + let hotkeys = (0 ..< 100).map { _ in HotKey(keyCombo: KeyCombo(keyCode: .space, modifiers: [.command])) } - + measure { // Check for conflicts 1000 times - for _ in 0..<1000 { + for _ in 0 ..< 1000 { _ = hotKeysController.isHotkeyRegistered(KeyCombo(keyCode: .space, modifiers: [.command])) } } - + // Clean up hotKeysController.registeredHotKeys.removeAll() } - + func testHotkeyUnregistrationPerformance() { // Set up test data - let hotkeys = (0..<1000).map { _ in + let hotkeys = (0 ..< 1000).map { _ in HotKey(keyCombo: KeyCombo(keyCode: .space, modifiers: [.command])) } hotkeys.forEach { hotKeysController.registerHotKey($0) } - + measure { // Unregister 1000 hotkeys for hotkey in hotkeys { @@ -106,4 +106,4 @@ class HotkeyPerformanceTests: XCTestCase { } } } -} \ No newline at end of file +} diff --git a/Tests/Performance/MemoryTests.swift b/Archived/v1/code/Tests/Performance/MemoryTests.swift similarity index 100% rename from Tests/Performance/MemoryTests.swift rename to Archived/v1/code/Tests/Performance/MemoryTests.swift diff --git a/Tests/Performance/Settings/SettingsPerformanceTests.swift b/Archived/v1/code/Tests/Performance/Settings/SettingsPerformanceTests.swift similarity index 86% rename from Tests/Performance/Settings/SettingsPerformanceTests.swift rename to Archived/v1/code/Tests/Performance/Settings/SettingsPerformanceTests.swift index 079d76d..627c0e7 100644 --- a/Tests/Performance/Settings/SettingsPerformanceTests.swift +++ b/Archived/v1/code/Tests/Performance/Settings/SettingsPerformanceTests.swift @@ -1,70 +1,70 @@ -import Quick -import Nimble @testable import MinimalAIChat +import Nimble +import Quick class SettingsPerformanceTests: QuickSpec { override func spec() { describe("Settings Performance") { var settingsManager: SettingsManager! var keychainManager: KeychainManager! - + beforeEach { keychainManager = KeychainManager() settingsManager = SettingsManager(keychainManager: keychainManager) } - + context("API Key Operations") { it("should handle rapid API key updates efficiently") { measure { - for i in 0..<100 { + for i in 0 ..< 100 { try? settingsManager.setAPIKey("test-key-\(i)") } } } - + it("should retrieve API key quickly") { try? settingsManager.setAPIKey("test-key") - + measure { - for _ in 0..<1000 { + for _ in 0 ..< 1000 { _ = try? settingsManager.getAPIKey() } } } } - + context("Service Type Operations") { it("should handle rapid service type changes") { measure { - for _ in 0..<1000 { + for _ in 0 ..< 1000 { settingsManager.setServiceType(.directAPI) settingsManager.setServiceType(.webWrapper) } } } - + it("should retrieve service type quickly") { measure { - for _ in 0..<1000 { + for _ in 0 ..< 1000 { _ = settingsManager.getServiceType() } } } } - + context("Theme Operations") { it("should handle rapid theme changes") { measure { - for _ in 0..<1000 { + for _ in 0 ..< 1000 { settingsManager.setTheme(.light) settingsManager.setTheme(.dark) } } } - + it("should apply theme changes efficiently") { measure { - for _ in 0..<100 { + for _ in 0 ..< 100 { settingsManager.setTheme(.light) settingsManager.setAccentColor(.blue) settingsManager.setTheme(.dark) @@ -73,29 +73,29 @@ class SettingsPerformanceTests: QuickSpec { } } } - + context("Hotkey Operations") { it("should handle rapid hotkey updates") { measure { - for i in 0..<100 { + for i in 0 ..< 100 { try? settingsManager.setGlobalHotkey(Hotkey(key: .space, modifiers: [.command, .shift])) } } } - + it("should validate hotkeys efficiently") { measure { - for _ in 0..<1000 { + for _ in 0 ..< 1000 { _ = try? settingsManager.setGlobalHotkey(Hotkey(key: .space, modifiers: [.command])) } } } } - + context("Memory Usage") { it("should maintain stable memory usage with many operations") { measure { - for i in 0..<1000 { + for i in 0 ..< 1000 { settingsManager.setServiceType(.directAPI) try? settingsManager.setAPIKey("test-key-\(i)") settingsManager.setTheme(.dark) @@ -103,14 +103,14 @@ class SettingsPerformanceTests: QuickSpec { } } } - + it("should clean up resources efficiently") { // Setup - for i in 0..<1000 { + for i in 0 ..< 1000 { settingsManager.setServiceType(.directAPI) try? settingsManager.setAPIKey("test-key-\(i)") } - + measure { // Cleanup try? keychainManager.delete(for: "apiKey") @@ -120,4 +120,4 @@ class SettingsPerformanceTests: QuickSpec { } } } -} \ No newline at end of file +} diff --git a/Tests/Performance/ThreadingTests.swift b/Archived/v1/code/Tests/Performance/ThreadingTests.swift similarity index 100% rename from Tests/Performance/ThreadingTests.swift rename to Archived/v1/code/Tests/Performance/ThreadingTests.swift diff --git a/Tests/TestConfiguration.swift b/Archived/v1/code/Tests/TestConfiguration.swift similarity index 97% rename from Tests/TestConfiguration.swift rename to Archived/v1/code/Tests/TestConfiguration.swift index a85c5b1..4ea1518 100644 --- a/Tests/TestConfiguration.swift +++ b/Archived/v1/code/Tests/TestConfiguration.swift @@ -1,6 +1,6 @@ import Foundation -import Quick import Nimble +import Quick class TestConfiguration: QuickConfiguration { override class func configure(_ configuration: Configuration) { @@ -9,7 +9,7 @@ class TestConfiguration: QuickConfiguration { // Global setup before all tests // Initialize test environment, load test data, etc. } - + configuration.afterSuite { // Global cleanup after all tests // Clean up resources, reset state, etc. @@ -18,32 +18,34 @@ class TestConfiguration: QuickConfiguration { } // MARK: - Test Helpers + extension TestConfiguration { static func setupTestEnvironment() { // Set up test environment variables ProcessInfo.processInfo.environment["TESTING"] = "1" - + // Configure test-specific settings UserDefaults.standard.set(true, forKey: "isTesting") } - + static func cleanupTestEnvironment() { // Reset environment variables ProcessInfo.processInfo.environment.removeValue(forKey: "TESTING") - + // Clean up test-specific settings UserDefaults.standard.removeObject(forKey: "isTesting") } } // MARK: - Performance Testing Configuration + extension TestConfiguration { static func configurePerformanceTests() { // Set up performance testing environment // Configure memory limits, timeouts, etc. } - + static func measurePerformance(_ block: @escaping () -> Void) { measure(block) } -} \ No newline at end of file +} diff --git a/Tests/UI/AccessibilityTests.swift b/Archived/v1/code/Tests/UI/AccessibilityTests.swift similarity index 100% rename from Tests/UI/AccessibilityTests.swift rename to Archived/v1/code/Tests/UI/AccessibilityTests.swift diff --git a/Tests/UI/ChatUITests.swift b/Archived/v1/code/Tests/UI/ChatUITests.swift similarity index 93% rename from Tests/UI/ChatUITests.swift rename to Archived/v1/code/Tests/UI/ChatUITests.swift index 0f26bef..e5e25e7 100644 --- a/Tests/UI/ChatUITests.swift +++ b/Archived/v1/code/Tests/UI/ChatUITests.swift @@ -1,12 +1,12 @@ -import XCTest -import SnapshotTesting @testable import MinimalAIChat +import SnapshotTesting +import XCTest class ChatUITests: XCTestCase { var view: ChatView! var viewModel: ChatViewModel! var app: XCUIApplication! - + override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() @@ -16,138 +16,138 @@ class ChatUITests: XCTestCase { viewModel = ChatViewModel() view = ChatView(viewModel: viewModel) } - + func testEmptyChatView() { let hostingController = UIHostingController(rootView: view) hostingController.view.frame = CGRect(x: 0, y: 0, width: 400, height: 600) - + assertSnapshot(matching: hostingController, as: .image) } - + func testChatViewWithMessages() { // Add some test messages viewModel.messages = [ ChatMessage(content: "Hello!", isUser: true), ChatMessage(content: "Hi there!", isUser: false), ChatMessage(content: "How are you?", isUser: true), - ChatMessage(content: "I'm doing great, thanks!", isUser: false) + ChatMessage(content: "I'm doing great, thanks!", isUser: false), ] - + let hostingController = UIHostingController(rootView: view) hostingController.view.frame = CGRect(x: 0, y: 0, width: 400, height: 600) - + assertSnapshot(matching: hostingController, as: .image) } - + func testChatViewWithLoadingState() { viewModel.isLoading = true - + let hostingController = UIHostingController(rootView: view) hostingController.view.frame = CGRect(x: 0, y: 0, width: 400, height: 600) - + assertSnapshot(matching: hostingController, as: .image) } - + func testChatViewWithLongMessages() { let longMessage = String(repeating: "This is a very long message that should wrap to multiple lines. ", count: 5) - + viewModel.messages = [ ChatMessage(content: longMessage, isUser: true), - ChatMessage(content: "This is a response to the long message.", isUser: false) + ChatMessage(content: "This is a response to the long message.", isUser: false), ] - + let hostingController = UIHostingController(rootView: view) hostingController.view.frame = CGRect(x: 0, y: 0, width: 400, height: 600) - + assertSnapshot(matching: hostingController, as: .image) } - + func testChatViewWithManyMessages() { // Add 20 messages to test scrolling - for i in 0..<20 { + for i in 0 ..< 20 { viewModel.messages.append(ChatMessage(content: "Message \(i)", isUser: i % 2 == 0)) } - + let hostingController = UIHostingController(rootView: view) hostingController.view.frame = CGRect(x: 0, y: 0, width: 400, height: 600) - + assertSnapshot(matching: hostingController, as: .image) } - + func testSendMessage() throws { // Given let messageTextField = app.textFields["Type a message..."] let sendButton = app.buttons["Send Message"] - + // When messageTextField.tap() messageTextField.typeText("Hello, AI!") sendButton.tap() - + // Then let messageBubble = app.staticTexts["Hello, AI!"] XCTAssertTrue(messageBubble.waitForExistence(timeout: 5)) } - + func testEmptyMessageCannotBeSent() throws { // Given let messageTextField = app.textFields["Type a message..."] let sendButton = app.buttons["Send Message"] - + // When messageTextField.tap() messageTextField.typeText(" ") - + // Then XCTAssertFalse(sendButton.isEnabled) } - + func testMessageListScrollsToBottom() throws { // Given let messageTextField = app.textFields["Type a message..."] let sendButton = app.buttons["Send Message"] - + // When - for i in 1...10 { + for i in 1 ... 10 { messageTextField.tap() messageTextField.typeText("Message \(i)\n") sendButton.tap() } - + // Then let lastMessage = app.staticTexts["Message 10"] XCTAssertTrue(lastMessage.waitForExistence(timeout: 5)) } - + func testErrorHandling() throws { // Given let messageTextField = app.textFields["Type a message..."] let sendButton = app.buttons["Send Message"] - + // When messageTextField.tap() messageTextField.typeText("Error Test") sendButton.tap() - + // Then let errorMessage = app.staticTexts["Sorry, I encountered an error. Please try again."] XCTAssertTrue(errorMessage.waitForExistence(timeout: 5)) } - + func testClearMessages() throws { // Given let messageTextField = app.textFields["Type a message..."] let sendButton = app.buttons["Send Message"] let clearButton = app.buttons["Clear Messages"] - + // When messageTextField.tap() messageTextField.typeText("Test Message") sendButton.tap() clearButton.tap() - + // Then let messageBubble = app.staticTexts["Test Message"] XCTAssertFalse(messageBubble.exists) } -} \ No newline at end of file +} diff --git a/Tests/UI/Hotkey/HotkeyUITests.swift b/Archived/v1/code/Tests/UI/Hotkey/HotkeyUITests.swift similarity index 94% rename from Tests/UI/Hotkey/HotkeyUITests.swift rename to Archived/v1/code/Tests/UI/Hotkey/HotkeyUITests.swift index 936b177..f85adf9 100644 --- a/Tests/UI/Hotkey/HotkeyUITests.swift +++ b/Archived/v1/code/Tests/UI/Hotkey/HotkeyUITests.swift @@ -1,106 +1,106 @@ -import XCTest @testable import MinimalAIChat +import XCTest class HotkeyUITests: XCTestCase { var app: XCUIApplication! - + override func setUp() { super.setUp() app = XCUIApplication() app.launch() } - + override func tearDown() { app = nil super.tearDown() } - + func testHotkeySettingsUI() { // Navigate to settings app.menuBars.buttons["Settings"].click() - + // Switch to hotkey tab app.tabBars.buttons["Hotkeys"].click() - + // Verify hotkey settings UI elements XCTAssertTrue(app.staticTexts["Global Hotkey"].exists) XCTAssertTrue(app.buttons["Record Hotkey"].exists) - + // Test hotkey recording app.buttons["Record Hotkey"].click() XCTAssertTrue(app.staticTexts["Press keys..."].exists) - + // Simulate key press (Command + Space) app.typeKey(.command, modifierFlags: .command) app.typeKey(.space, modifierFlags: .command) - + // Verify hotkey is displayed XCTAssertTrue(app.staticTexts["⌘ Space"].exists) - + // Test hotkey clearing app.buttons["Clear"].click() XCTAssertFalse(app.staticTexts["⌘ Space"].exists) } - + func testHotkeyValidationUI() { // Navigate to settings app.menuBars.buttons["Settings"].click() app.tabBars.buttons["Hotkeys"].click() - + // Try to record invalid hotkey (no modifiers) app.buttons["Record Hotkey"].click() app.typeKey(.space, modifierFlags: []) - + // Verify error alert XCTAssertTrue(app.alerts["Invalid Hotkey"].exists) XCTAssertTrue(app.alerts["Invalid Hotkey"].staticTexts["Hotkey must include at least one modifier key"].exists) - + // Dismiss alert app.alerts["Invalid Hotkey"].buttons["OK"].click() } - + func testHotkeyConflictUI() { // Navigate to settings app.menuBars.buttons["Settings"].click() app.tabBars.buttons["Hotkeys"].click() - + // Record first hotkey app.buttons["Record Hotkey"].click() app.typeKey(.command, modifierFlags: .command) app.typeKey(.space, modifierFlags: .command) - + // Try to record same hotkey again app.buttons["Record Hotkey"].click() app.typeKey(.command, modifierFlags: .command) app.typeKey(.space, modifierFlags: .command) - + // Verify conflict alert XCTAssertTrue(app.alerts["Hotkey Conflict"].exists) XCTAssertTrue(app.alerts["Hotkey Conflict"].staticTexts["This hotkey is already in use"].exists) - + // Dismiss alert app.alerts["Hotkey Conflict"].buttons["Cancel"].click() } - + func testHotkeyPersistenceUI() { // Navigate to settings app.menuBars.buttons["Settings"].click() app.tabBars.buttons["Hotkeys"].click() - + // Record a hotkey app.buttons["Record Hotkey"].click() app.typeKey(.command, modifierFlags: .command) app.typeKey(.return, modifierFlags: .command) - + // Quit and relaunch app app.terminate() app.launch() - + // Navigate back to settings app.menuBars.buttons["Settings"].click() app.tabBars.buttons["Hotkeys"].click() - + // Verify hotkey is still displayed XCTAssertTrue(app.staticTexts["⌘ Return"].exists) } -} \ No newline at end of file +} diff --git a/Tests/UI/NavigationTests.swift b/Archived/v1/code/Tests/UI/NavigationTests.swift similarity index 100% rename from Tests/UI/NavigationTests.swift rename to Archived/v1/code/Tests/UI/NavigationTests.swift diff --git a/Tests/UI/Settings/SettingsUITests.swift b/Archived/v1/code/Tests/UI/Settings/SettingsUITests.swift similarity index 96% rename from Tests/UI/Settings/SettingsUITests.swift rename to Archived/v1/code/Tests/UI/Settings/SettingsUITests.swift index 1d6d090..ae3ab87 100644 --- a/Tests/UI/Settings/SettingsUITests.swift +++ b/Archived/v1/code/Tests/UI/Settings/SettingsUITests.swift @@ -1,70 +1,70 @@ -import XCTest -import SnapshotTesting @testable import MinimalAIChat +import SnapshotTesting +import XCTest class SettingsUITests: XCTestCase { var settingsView: SettingsView! var settingsManager: SettingsManager! - + override func setUp() { super.setUp() settingsManager = SettingsManager(keychainManager: KeychainManager()) settingsView = SettingsView(settingsManager: settingsManager) } - + func testDefaultSettingsView() { let hostingController = NSHostingController(rootView: settingsView) hostingController.view.frame = CGRect(x: 0, y: 0, width: 600, height: 400) - + assertSnapshot(matching: hostingController, as: .image) } - + func testSettingsViewWithAPIKey() { try? settingsManager.setAPIKey("test-api-key") let hostingController = NSHostingController(rootView: settingsView) hostingController.view.frame = CGRect(x: 0, y: 0, width: 600, height: 400) - + assertSnapshot(matching: hostingController, as: .image) } - + func testSettingsViewWithWebWrapperSelected() { settingsManager.setServiceType(.webWrapper) let hostingController = NSHostingController(rootView: settingsView) hostingController.view.frame = CGRect(x: 0, y: 0, width: 600, height: 400) - + assertSnapshot(matching: hostingController, as: .image) } - + func testSettingsViewWithDarkTheme() { settingsManager.setTheme(.dark) let hostingController = NSHostingController(rootView: settingsView) hostingController.view.frame = CGRect(x: 0, y: 0, width: 600, height: 400) - + assertSnapshot(matching: hostingController, as: .image) } - + func testSettingsViewWithCustomAccentColor() { settingsManager.setAccentColor(.purple) let hostingController = NSHostingController(rootView: settingsView) hostingController.view.frame = CGRect(x: 0, y: 0, width: 600, height: 400) - + assertSnapshot(matching: hostingController, as: .image) } - + func testSettingsViewWithHotkeyConfigured() { try? settingsManager.setGlobalHotkey(Hotkey(key: .space, modifiers: [.command])) let hostingController = NSHostingController(rootView: settingsView) hostingController.view.frame = CGRect(x: 0, y: 0, width: 600, height: 400) - + assertSnapshot(matching: hostingController, as: .image) } - + func testSettingsViewWithErrorState() { // Simulate an error state settingsManager.setError("Invalid API Key") let hostingController = NSHostingController(rootView: settingsView) hostingController.view.frame = CGRect(x: 0, y: 0, width: 600, height: 400) - + assertSnapshot(matching: hostingController, as: .image) } -} \ No newline at end of file +} diff --git a/Tests/UI/Snapshot/PaywallLayoutTests.swift b/Archived/v1/code/Tests/UI/Snapshot/PaywallLayoutTests.swift similarity index 100% rename from Tests/UI/Snapshot/PaywallLayoutTests.swift rename to Archived/v1/code/Tests/UI/Snapshot/PaywallLayoutTests.swift diff --git a/Tests/UI/Snapshot/RTLSupportTests.swift b/Archived/v1/code/Tests/UI/Snapshot/RTLSupportTests.swift similarity index 100% rename from Tests/UI/Snapshot/RTLSupportTests.swift rename to Archived/v1/code/Tests/UI/Snapshot/RTLSupportTests.swift diff --git a/Tests/Unit/AIServiceTests.swift b/Archived/v1/code/Tests/Unit/AIServiceTests.swift similarity index 96% rename from Tests/Unit/AIServiceTests.swift rename to Archived/v1/code/Tests/Unit/AIServiceTests.swift index 9165ef9..fffccd3 100644 --- a/Tests/Unit/AIServiceTests.swift +++ b/Archived/v1/code/Tests/Unit/AIServiceTests.swift @@ -1,25 +1,25 @@ -import XCTest @testable import MinimalAIChat +import XCTest final class AIServiceTests: XCTestCase { var aiService: AIService! var mockSessionManager: MockSessionManager! var mockSettingsManager: MockSettingsManager! var mockKeychainManager: MockKeychainManager! - + override func setUp() { super.setUp() mockSessionManager = MockSessionManager() mockSettingsManager = MockSettingsManager() mockKeychainManager = MockKeychainManager() - + aiService = AIService( sessionManager: mockSessionManager, settingsManager: mockSettingsManager, keychainManager: mockKeychainManager ) } - + override func tearDown() { aiService = nil mockSessionManager = nil @@ -27,54 +27,54 @@ final class AIServiceTests: XCTestCase { mockKeychainManager = nil super.tearDown() } - + func testSendMessageToClaude() async throws { // Given let message = "Hello, Claude!" mockSettingsManager.mockSettings = Settings(selectedService: .claude) mockKeychainManager.mockAPIKey = "test-claude-key" - + // When let response = try await aiService.sendMessage(message) - + // Then XCTAssertFalse(response.isEmpty) XCTAssertEqual(mockKeychainManager.lastService, .claude) } - + func testSendMessageToOpenAI() async throws { // Given let message = "Hello, OpenAI!" mockSettingsManager.mockSettings = Settings(selectedService: .openAI) mockKeychainManager.mockAPIKey = "test-openai-key" - + // When let response = try await aiService.sendMessage(message) - + // Then XCTAssertFalse(response.isEmpty) XCTAssertEqual(mockKeychainManager.lastService, .openAI) } - + func testSendMessageToDeepSeek() async throws { // Given let message = "Hello, DeepSeek!" mockSettingsManager.mockSettings = Settings(selectedService: .deepSeek) mockKeychainManager.mockAPIKey = "test-deepseek-key" - + // When let response = try await aiService.sendMessage(message) - + // Then XCTAssertFalse(response.isEmpty) XCTAssertEqual(mockKeychainManager.lastService, .deepSeek) } - + func testInvalidSessionError() async { // Given let message = "Hello!" mockSessionManager.shouldThrowError = true - + // When/Then do { _ = try await aiService.sendMessage(message) @@ -85,12 +85,12 @@ final class AIServiceTests: XCTestCase { XCTFail("Unexpected error: \(error)") } } - + func testRateLimitError() async { // Given let message = "Hello!" mockKeychainManager.shouldSimulateRateLimit = true - + // When/Then do { _ = try await aiService.sendMessage(message) @@ -104,9 +104,10 @@ final class AIServiceTests: XCTestCase { } // MARK: - Mock Classes + class MockSessionManager: SessionManager { var shouldThrowError = false - + override func validateSession() async throws { if shouldThrowError { throw AIServiceError.invalidSession @@ -116,7 +117,7 @@ class MockSessionManager: SessionManager { class MockSettingsManager: SettingsManager { var mockSettings = Settings(selectedService: .openAI) - + override func getSettings() async throws -> Settings { return mockSettings } @@ -126,7 +127,7 @@ class MockKeychainManager: KeychainManager { var mockAPIKey = "test-key" var lastService: AIServiceType? var shouldSimulateRateLimit = false - + override func getAPIKey(for service: AIServiceType) async throws -> String { lastService = service if shouldSimulateRateLimit { @@ -134,4 +135,4 @@ class MockKeychainManager: KeychainManager { } return mockAPIKey } -} \ No newline at end of file +} diff --git a/Tests/Unit/ChatViewTests.swift b/Archived/v1/code/Tests/Unit/ChatViewTests.swift similarity index 91% rename from Tests/Unit/ChatViewTests.swift rename to Archived/v1/code/Tests/Unit/ChatViewTests.swift index 2fb231e..aef134e 100644 --- a/Tests/Unit/ChatViewTests.swift +++ b/Archived/v1/code/Tests/Unit/ChatViewTests.swift @@ -1,60 +1,60 @@ -import Quick +@testable import MinimalAIChat import Nimble +import Quick import SwiftUI -@testable import MinimalAIChat class ChatViewTests: QuickSpec { override func spec() { describe("ChatView") { var view: ChatView! var viewModel: ChatViewModel! - + beforeEach { viewModel = ChatViewModel() view = ChatView(viewModel: viewModel) } - + context("when initialized") { it("should have an empty message list") { expect(viewModel.messages).to(beEmpty()) } - + it("should have an empty input text") { expect(viewModel.inputText).to(equal("")) } } - + context("when sending a message") { it("should add the message to the list") { let message = "Hello, AI!" viewModel.inputText = message viewModel.sendMessage() - + expect(viewModel.messages).to(haveCount(1)) expect(viewModel.messages.first?.content).to(equal(message)) } - + it("should clear the input text after sending") { viewModel.inputText = "Test message" viewModel.sendMessage() - + expect(viewModel.inputText).to(equal("")) } } - + context("when receiving an AI response") { it("should add the response to the message list") { let userMessage = "Hello" let aiResponse = "Hi there!" - + viewModel.inputText = userMessage viewModel.sendMessage() viewModel.receiveAIResponse(aiResponse) - + expect(viewModel.messages).to(haveCount(2)) expect(viewModel.messages.last?.content).to(equal(aiResponse)) } } } } -} \ No newline at end of file +} diff --git a/Tests/Unit/DeepLinkHandlerTests.swift b/Archived/v1/code/Tests/Unit/DeepLinkHandlerTests.swift similarity index 98% rename from Tests/Unit/DeepLinkHandlerTests.swift rename to Archived/v1/code/Tests/Unit/DeepLinkHandlerTests.swift index d2474ca..793cf9a 100644 --- a/Tests/Unit/DeepLinkHandlerTests.swift +++ b/Archived/v1/code/Tests/Unit/DeepLinkHandlerTests.swift @@ -1,22 +1,22 @@ -import XCTest @testable import MinimalAIChat +import XCTest final class DeepLinkHandlerTests: XCTestCase { var deepLinkHandler: DeepLinkHandler! var expectation: XCTestExpectation! - + override func setUp() { super.setUp() deepLinkHandler = DeepLinkHandler() expectation = XCTestExpectation(description: "Deep link handled") } - + override func tearDown() { deepLinkHandler = nil expectation = nil super.tearDown() } - + func testValidChatDeepLink() async { let url = URL(string: "minimalaichat://chat/123")! await deepLinkHandler.handleURL(url) @@ -24,7 +24,7 @@ final class DeepLinkHandlerTests: XCTestCase { // as it requires UI interaction // This test just verifies that the URL is parsed correctly } - + func testValidSettingsDeepLink() async { let url = URL(string: "minimalaichat://settings/preferences")! await deepLinkHandler.handleURL(url) @@ -32,7 +32,7 @@ final class DeepLinkHandlerTests: XCTestCase { // as it requires UI interaction // This test just verifies that the URL is parsed correctly } - + func testInvalidDeepLink() async { let url = URL(string: "minimalaichat://invalid/path")! await deepLinkHandler.handleURL(url) @@ -40,7 +40,7 @@ final class DeepLinkHandlerTests: XCTestCase { // as it requires UI interaction // This test just verifies that the URL is parsed correctly } - + func testDeepLinkWithQueryParameters() async { let url = URL(string: "minimalaichat://chat/123?message=hello")! await deepLinkHandler.handleURL(url) @@ -48,4 +48,4 @@ final class DeepLinkHandlerTests: XCTestCase { // as it requires UI interaction // This test just verifies that the URL is parsed correctly } -} \ No newline at end of file +} diff --git a/Tests/Unit/HotKeyTests.swift b/Archived/v1/code/Tests/Unit/HotKeyTests.swift similarity index 97% rename from Tests/Unit/HotKeyTests.swift rename to Archived/v1/code/Tests/Unit/HotKeyTests.swift index 02ff760..5114825 100644 --- a/Tests/Unit/HotKeyTests.swift +++ b/Archived/v1/code/Tests/Unit/HotKeyTests.swift @@ -1,10 +1,10 @@ -import XCTest @testable import MinimalAIChat +import XCTest final class HotKeyTests: XCTestCase { var hotKey: HotKey! var expectation: XCTestExpectation! - + override func setUp() { super.setUp() expectation = XCTestExpectation(description: "HotKey handler called") @@ -12,45 +12,45 @@ final class HotKeyTests: XCTestCase { self?.expectation.fulfill() } } - + override func tearDown() { hotKey = nil expectation = nil super.tearDown() } - + func testKeyComboInitialization() { let combo = KeyCombo(key: .space, modifiers: [.command]) XCTAssertEqual(combo.key, .space) XCTAssertEqual(combo.modifiers, [.command]) } - + func testCarbonKeyCodeConversion() { let combo = KeyCombo(key: .space) XCTAssertEqual(combo.carbonKeyCode, 0x31) - + let returnCombo = KeyCombo(key: .return) XCTAssertEqual(returnCombo.carbonKeyCode, 0x24) } - + func testCarbonModifiersConversion() { let combo = KeyCombo(key: .space, modifiers: [.command, .shift]) let modifiers = combo.carbonModifiers - + // Check if command and shift modifiers are set XCTAssertTrue((modifiers & UInt32(cmdKey)) != 0) XCTAssertTrue((modifiers & UInt32(shiftKey)) != 0) XCTAssertFalse((modifiers & UInt32(optionKey)) != 0) XCTAssertFalse((modifiers & UInt32(controlKey)) != 0) } - + func testHotKeyRegistration() async throws { try await hotKey.register() // Note: We can't actually test the hotkey triggering in unit tests // as it requires system-level keyboard events // This test just verifies that registration doesn't throw } - + func testHotKeyUnregistration() async throws { try await hotKey.register() hotKey.unregister() @@ -58,7 +58,7 @@ final class HotKeyTests: XCTestCase { // as it requires system-level keyboard events // This test just verifies that unregistration doesn't crash } - + func testHotKeyDeinitialization() async throws { try await hotKey.register() hotKey = nil // This should trigger deinit and unregister @@ -66,4 +66,4 @@ final class HotKeyTests: XCTestCase { // as it requires system-level keyboard events // This test just verifies that deinitialization doesn't crash } -} \ No newline at end of file +} diff --git a/Tests/Unit/Hotkey/HotKeysControllerTests.swift b/Archived/v1/code/Tests/Unit/Hotkey/HotKeysControllerTests.swift similarity index 96% rename from Tests/Unit/Hotkey/HotKeysControllerTests.swift rename to Archived/v1/code/Tests/Unit/Hotkey/HotKeysControllerTests.swift index 665c9fb..4059115 100644 --- a/Tests/Unit/Hotkey/HotKeysControllerTests.swift +++ b/Archived/v1/code/Tests/Unit/Hotkey/HotKeysControllerTests.swift @@ -1,61 +1,61 @@ -import XCTest import Carbon @testable import MinimalAIChat +import XCTest class HotKeysControllerTests: XCTestCase { var controller: HotKeysController! var mockHotKey: HotKey! - + override func setUp() { super.setUp() controller = HotKeysController.shared mockHotKey = HotKey(keyCombo: KeyCombo(keyCode: .space, modifiers: [.command])) } - + override func tearDown() { controller = nil mockHotKey = nil super.tearDown() } - + func testSingletonInstance() { let instance1 = HotKeysController.shared let instance2 = HotKeysController.shared XCTAssertTrue(instance1 === instance2, "HotKeysController should be a singleton") } - + func testRegisterHotKey() { controller.registerHotKey(mockHotKey) XCTAssertTrue(controller.isHotkeyRegistered(mockHotKey.combo)) } - + func testUnregisterHotKey() { controller.registerHotKey(mockHotKey) controller.unregisterHotKey(mockHotKey) XCTAssertFalse(controller.isHotkeyRegistered(mockHotKey.combo)) } - + func testLaunchAgentInstallation() { controller.installLaunchAgent() let agentPath = (("~/Library/LaunchAgents/com.minimalaichat.hotkey.plist" as NSString).expandingTildeInPath) XCTAssertTrue(FileManager.default.fileExists(atPath: agentPath)) - + controller.uninstallLaunchAgent() XCTAssertFalse(FileManager.default.fileExists(atPath: agentPath)) } - + func testMultipleHotKeys() { let hotKey1 = HotKey(keyCombo: KeyCombo(keyCode: .space, modifiers: [.command])) let hotKey2 = HotKey(keyCombo: KeyCombo(keyCode: .return, modifiers: [.command])) - + controller.registerHotKey(hotKey1) controller.registerHotKey(hotKey2) - + XCTAssertTrue(controller.isHotkeyRegistered(hotKey1.combo)) XCTAssertTrue(controller.isHotkeyRegistered(hotKey2.combo)) - + controller.unregisterHotKey(hotKey1) XCTAssertFalse(controller.isHotkeyRegistered(hotKey1.combo)) XCTAssertTrue(controller.isHotkeyRegistered(hotKey2.combo)) } -} \ No newline at end of file +} diff --git a/Tests/Unit/Hotkey/HotkeyManagerTests.swift b/Archived/v1/code/Tests/Unit/Hotkey/HotkeyManagerTests.swift similarity index 100% rename from Tests/Unit/Hotkey/HotkeyManagerTests.swift rename to Archived/v1/code/Tests/Unit/Hotkey/HotkeyManagerTests.swift diff --git a/Tests/Unit/MemoryOptimizerTests.swift b/Archived/v1/code/Tests/Unit/MemoryOptimizerTests.swift similarity index 96% rename from Tests/Unit/MemoryOptimizerTests.swift rename to Archived/v1/code/Tests/Unit/MemoryOptimizerTests.swift index 075765c..ad2345f 100644 --- a/Tests/Unit/MemoryOptimizerTests.swift +++ b/Archived/v1/code/Tests/Unit/MemoryOptimizerTests.swift @@ -1,63 +1,64 @@ -import XCTest @testable import MinimalAIChat +import XCTest final class MemoryOptimizerTests: XCTestCase { var memoryOptimizer: MemoryOptimizer! var mockWebViewCleanupActor: MockWebViewCleanupActor! - + override func setUp() { super.setUp() mockWebViewCleanupActor = MockWebViewCleanupActor() memoryOptimizer = MemoryOptimizer(webViewCleanupActor: mockWebViewCleanupActor) } - + override func tearDown() { memoryOptimizer = nil mockWebViewCleanupActor = nil super.tearDown() } - + func testMemoryOptimization() async throws { // Test successful optimization try await memoryOptimizer.optimizeMemoryUsage() - + // Verify WebView cleanup was called XCTAssertTrue(mockWebViewCleanupActor.cleanupCalled) - + // Verify URL cache was cleared let cache = URLCache.shared let request = URLRequest(url: URL(string: "https://example.com")!) let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! let data = "test".data(using: .utf8)! cache.storeCachedResponse(CachedURLResponse(response: response, data: data), for: request) - + try await memoryOptimizer.optimizeMemoryUsage() - + // Verify cache was cleared XCTAssertNil(cache.cachedResponse(for: request)) } - + func testMemoryOptimizationWithError() async throws { // Configure mock to throw an error mockWebViewCleanupActor.shouldThrowError = true - + // Test optimization with error try await memoryOptimizer.optimizeMemoryUsage() - + // Verify cleanup was attempted XCTAssertTrue(mockWebViewCleanupActor.cleanupCalled) } } // MARK: - Mock WebViewCleanupActor + private class MockWebViewCleanupActor: WebViewCleanupable { var cleanupCalled = false var shouldThrowError = false - + func cleanup() async throws { cleanupCalled = true if shouldThrowError { throw NSError(domain: "test", code: -1) } } -} \ No newline at end of file +} diff --git a/Tests/Unit/Settings/SettingsManagerTests.swift b/Archived/v1/code/Tests/Unit/Settings/SettingsManagerTests.swift similarity index 93% rename from Tests/Unit/Settings/SettingsManagerTests.swift rename to Archived/v1/code/Tests/Unit/Settings/SettingsManagerTests.swift index b8219f6..b78dc68 100644 --- a/Tests/Unit/Settings/SettingsManagerTests.swift +++ b/Archived/v1/code/Tests/Unit/Settings/SettingsManagerTests.swift @@ -1,81 +1,81 @@ -import Quick -import Nimble @testable import MinimalAIChat +import Nimble +import Quick class SettingsManagerTests: QuickSpec { override func spec() { describe("SettingsManager") { var settingsManager: SettingsManager! var keychainManager: MockKeychainManager! - + beforeEach { keychainManager = MockKeychainManager() settingsManager = SettingsManager(keychainManager: keychainManager) } - + context("API Key Management") { it("should store API key securely") { let apiKey = "test-api-key" try? settingsManager.setAPIKey(apiKey) - + expect(keychainManager.storedKeys["apiKey"]).to(equal(apiKey)) } - + it("should retrieve API key") { let apiKey = "test-api-key" keychainManager.storedKeys["apiKey"] = apiKey - + let retrievedKey = try? settingsManager.getAPIKey() expect(retrievedKey).to(equal(apiKey)) } - + it("should validate API key format") { let invalidKey = "invalid-key" expect { try settingsManager.setAPIKey(invalidKey) }.to(throwError()) } } - + context("Service Selection") { it("should store and retrieve service type") { settingsManager.setServiceType(.webWrapper) expect(settingsManager.getServiceType()).to(equal(.webWrapper)) - + settingsManager.setServiceType(.directAPI) expect(settingsManager.getServiceType()).to(equal(.directAPI)) } - + it("should store and retrieve model selection") { settingsManager.setModel(.gpt4) expect(settingsManager.getModel()).to(equal(.gpt4)) - + settingsManager.setModel(.gpt35) expect(settingsManager.getModel()).to(equal(.gpt35)) } } - + context("Theme Settings") { it("should store and retrieve theme preference") { settingsManager.setTheme(.dark) expect(settingsManager.getTheme()).to(equal(.dark)) - + settingsManager.setTheme(.light) expect(settingsManager.getTheme()).to(equal(.light)) } - + it("should store and retrieve accent color") { let color = Color.blue settingsManager.setAccentColor(color) expect(settingsManager.getAccentColor()).to(equal(color)) } } - + context("Hotkey Configuration") { it("should store and retrieve hotkey settings") { let hotkey = Hotkey(key: .space, modifiers: [.command]) settingsManager.setGlobalHotkey(hotkey) expect(settingsManager.getGlobalHotkey()).to(equal(hotkey)) } - + it("should validate hotkey combinations") { let invalidHotkey = Hotkey(key: .space, modifiers: []) expect { try settingsManager.setGlobalHotkey(invalidHotkey) }.to(throwError()) @@ -86,21 +86,22 @@ class SettingsManagerTests: QuickSpec { } // MARK: - Mock Keychain Manager + class MockKeychainManager: KeychainManagerProtocol { var storedKeys: [String: String] = [:] - + func store(_ value: String, for key: String) throws { storedKeys[key] = value } - + func retrieve(for key: String) throws -> String { guard let value = storedKeys[key] else { throw KeychainError.itemNotFound } return value } - + func delete(for key: String) throws { storedKeys.removeValue(forKey: key) } -} \ No newline at end of file +} diff --git a/Tests/Unit/Subscription/PurchaseManagerTests.swift b/Archived/v1/code/Tests/Unit/Subscription/PurchaseManagerTests.swift similarity index 100% rename from Tests/Unit/Subscription/PurchaseManagerTests.swift rename to Archived/v1/code/Tests/Unit/Subscription/PurchaseManagerTests.swift diff --git a/Tests/Unit/WebView/WebViewCleanupTests.swift b/Archived/v1/code/Tests/Unit/WebView/WebViewCleanupTests.swift similarity index 100% rename from Tests/Unit/WebView/WebViewCleanupTests.swift rename to Archived/v1/code/Tests/Unit/WebView/WebViewCleanupTests.swift diff --git a/Archived/v1/config/.swiftlint.yml b/Archived/v1/config/.swiftlint.yml new file mode 100644 index 0000000..d5fd886 --- /dev/null +++ b/Archived/v1/config/.swiftlint.yml @@ -0,0 +1,74 @@ +disabled_rules: + - trailing_whitespace + - line_length +opt_in_rules: + - empty_count + - missing_docs + - force_unwrapping + - force_cast + - force_try + - todo + - notification_center_detachment + - legacy_random + - legacy_cg_graphics_functions + - legacy_constant + - legacy_nsgeometry_functions + - yoda_condition + - nimble_operator + - operator_usage_whitespace + - overridden_super_call + - prohibited_super_call + - redundant_nil_coalescing + - private_outlet + - prohibited_iboutlet + - custom_rules + +line_length: + warning: 120 + error: 200 + +type_body_length: + warning: 300 + error: 400 + +file_length: + warning: 500 + error: 1000 + +function_body_length: + warning: 50 + error: 100 + +cyclomatic_complexity: + warning: 10 + error: 20 + +reporter: "xcode" + +included: + - App + - Tests + +excluded: + - Pods + - Tests/Performance + - Tests/UI + - Tests/Integration + +line_length: + ignores_comments: true + ignores_urls: true + ignores_function_declarations: true + ignores_interpolated_strings: true + +custom_rules: + no_direct_standard_out_logs: + name: "Print Usage" + regex: "(print|NSLog)\\(" + message: "Prefer using a logging framework over print or NSLog" + severity: warning + comments_space: + name: "Space After Comment" + regex: "//[^\\s]" + message: "There should be a space after //" + severity: warning \ No newline at end of file diff --git a/Archived/v1/config/Package.swift b/Archived/v1/config/Package.swift new file mode 100644 index 0000000..7441919 --- /dev/null +++ b/Archived/v1/config/Package.swift @@ -0,0 +1,75 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MinimalAIChat", + platforms: [ + .macOS(.v13), + ], + products: [ + .executable( + name: "MinimalAIChat", + targets: ["MinimalAIChat"] + ), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"), + .package(url: "https://github.com/apple/swift-async-algorithms.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), + .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), + .package(url: "https://github.com/apple/swift-asn1.git", from: "0.10.0"), + .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.12.0"), + .package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"), + .package(url: "https://github.com/apple/swift-numerics.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-atomics.git", from: "1.1.0"), + .package(url: "https://github.com/Quick/Quick.git", from: "7.3.0"), + .package(url: "https://github.com/Quick/Nimble.git", from: "13.2.0"), + ], + targets: [ + .executableTarget( + name: "MinimalAIChat", + dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), + .product(name: "Collections", package: "swift-collections"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Algorithms", package: "swift-algorithms"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftASN1", package: "swift-asn1"), + .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), + .product(name: "Crypto", package: "swift-crypto"), + .product(name: "Numerics", package: "swift-numerics"), + .product(name: "Atomics", package: "swift-atomics"), + .product(name: "Quick", package: "Quick"), + .product(name: "Nimble", package: "Nimble"), + ], + path: "App", + resources: [ + .process("UI/Localization"), + ], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ] + ), + .testTarget( + name: "MinimalAIChatTests", + dependencies: ["MinimalAIChat"], + path: "Tests/Unit", + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ] + ), + .testTarget( + name: "MinimalAIChatUITests", + dependencies: ["MinimalAIChat"], + path: "Tests/UI", + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency"), + ] + ), + ] +) diff --git a/Discussions.md b/Archived/v1/docs/Discussions.md similarity index 100% rename from Discussions.md rename to Archived/v1/docs/Discussions.md diff --git a/FeatureRefinement.md b/Archived/v1/docs/FeatureRefinement.md similarity index 100% rename from FeatureRefinement.md rename to Archived/v1/docs/FeatureRefinement.md diff --git a/ProgressTracker.md b/Archived/v1/docs/ProgressTracker.md similarity index 100% rename from ProgressTracker.md rename to Archived/v1/docs/ProgressTracker.md diff --git a/ProjectJourney.md b/Archived/v1/docs/ProjectJourney.md similarity index 100% rename from ProjectJourney.md rename to Archived/v1/docs/ProjectJourney.md diff --git a/Config/APIConfig.example.swift b/Config/APIConfig.example.swift index 9a30580..7f117a9 100644 --- a/Config/APIConfig.example.swift +++ b/Config/APIConfig.example.swift @@ -2,17 +2,17 @@ import Foundation /// This is an example configuration file. /// Copy this to APIConfig.swift and fill in your actual API keys -struct APIConfig { +enum APIConfig { // In the initial version, these are placeholders for future API integration static let openAIKey = "YOUR_OPENAI_API_KEY" static let anthropicKey = "YOUR_ANTHROPIC_API_KEY" static let deepSeekKey = "YOUR_DEEPSEEK_API_KEY" - + // Future configuration options static let organizationID = "YOUR_ORGANIZATION_ID" // Optional for some services - + // Feature flags static let useDirectAPI = false // Set to false for initial web-based version - + // Add additional configuration as needed } diff --git a/Config/APIConfig.swift b/Config/APIConfig.swift index a1adc66..07124a3 100644 --- a/Config/APIConfig.swift +++ b/Config/APIConfig.swift @@ -9,13 +9,13 @@ enum APIConfig { static let models = [ "gpt-4": "gpt-4", "gpt-4-turbo": "gpt-4-1106-preview", - "gpt-3.5-turbo": "gpt-3.5-turbo" + "gpt-3.5-turbo": "gpt-3.5-turbo", ] static let defaultModel = "gpt-3.5-turbo" static let maxTokens = 1000 static let temperature = 0.7 } - + /// Anthropic Claude API configuration enum Claude { static let baseURL = "https://api.anthropic.com/v1" @@ -23,26 +23,26 @@ enum APIConfig { static let models = [ "claude-3-opus": "claude-3-opus-20240229", "claude-3-sonnet": "claude-3-sonnet-20240229", - "claude-2.1": "claude-2.1" + "claude-2.1": "claude-2.1", ] static let defaultModel = "claude-3-sonnet" static let maxTokens = 4096 static let temperature = 0.7 } - + /// DeepSeek API configuration enum DeepSeek { static let baseURL = "https://api.deepseek.com/v1" static let chatEndpoint = "\(baseURL)/chat/completions" static let models = [ "deepseek-chat": "deepseek-chat", - "deepseek-coder": "deepseek-coder" + "deepseek-coder": "deepseek-coder", ] static let defaultModel = "deepseek-chat" static let maxTokens = 1000 static let temperature = 0.7 } - + /// Common API configuration enum Common { static let timeoutInterval: TimeInterval = 30 diff --git a/Package.swift b/Package.swift index 9e5b5c1..f8c936d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,75 +1,60 @@ -// swift-tools-version: 5.9 -// The swift-tools-version declares the minimum version of Swift required to build this package. - +// swift-tools-version:5.9 import PackageDescription let package = Package( name: "MinimalAIChat", platforms: [ - .macOS(.v13) + .macOS(.v13), ], products: [ .executable( name: "MinimalAIChat", targets: ["MinimalAIChat"] - ) + ), + .library( + name: "MinimalAIChatCore", + targets: ["MinimalAIChatCore"] + ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"), - .package(url: "https://github.com/apple/swift-async-algorithms.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "0.5.0"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), - .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), - .package(url: "https://github.com/apple/swift-asn1.git", from: "0.10.0"), - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.12.0"), - .package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"), - .package(url: "https://github.com/apple/swift-numerics.git", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-atomics.git", from: "1.1.0"), - .package(url: "https://github.com/Quick/Quick.git", from: "7.3.0"), - .package(url: "https://github.com/Quick/Nimble.git", from: "13.2.0") + .package(url: "https://github.com/apple/swift-async-algorithms.git", from: "1.0.0"), ], targets: [ .executableTarget( name: "MinimalAIChat", dependencies: [ + "MinimalAIChatCore", .product(name: "Logging", package: "swift-log"), - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), .product(name: "Collections", package: "swift-collections"), - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "Algorithms", package: "swift-algorithms"), - .product(name: "SwiftSyntax", package: "swift-syntax"), - .product(name: "SwiftASN1", package: "swift-asn1"), - .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), - .product(name: "Crypto", package: "swift-crypto"), - .product(name: "Numerics", package: "swift-numerics"), - .product(name: "Atomics", package: "swift-atomics"), - .product(name: "Quick", package: "Quick"), - .product(name: "Nimble", package: "Nimble") + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), ], - path: "App", - resources: [ - .process("UI/Localization") + path: "Sources/MinimalAIChat" + ), + .target( + name: "MinimalAIChatCore", + dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "Collections", package: "swift-collections"), + .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), ], - swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency") - ] + path: "Sources/MinimalAIChatCore" ), .testTarget( name: "MinimalAIChatTests", - dependencies: ["MinimalAIChat"], - path: "Tests/Unit", - swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency") - ] + dependencies: ["MinimalAIChatCore"], + path: "Tests/Unit" ), .testTarget( - name: "MinimalAIChatUITests", - dependencies: ["MinimalAIChat"], - path: "Tests/UI", - swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency") - ] - ) + name: "MinimalAIChatIntegrationTests", + dependencies: ["MinimalAIChatCore"], + path: "Tests/Integration" + ), + .testTarget( + name: "MinimalAIChatPerformanceTests", + dependencies: ["MinimalAIChatCore"], + path: "Tests/Performance" + ), ] ) diff --git a/README.md b/README.md index 22d7e79..2e9cfd6 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,84 @@ -# MinimalAIChat +# MinimalAIChat V2 -A native macOS application providing instant, privacy-focused access to AI chat through a minimal interface with global hotkey activation. +A privacy-focused, high-performance AI chat application for macOS. ## Features -- **Global Hotkey Access**: Summon the app from anywhere with a keyboard shortcut -- **Minimal Memory Footprint**: Only 45MB vs 200MB+ for web alternatives -- **Privacy-Focused**: Minimal data collection, local processing where possible -- **Native macOS Integration**: Spotlight search, Universal Links, and more -- **Optimized Performance**: Fast, responsive UI built with Swift and SwiftUI - -## Current Status (v1.0) - -In the initial version, MinimalAIChat focuses on: -- Wrapping web interfaces for top AI services -- Providing a unified chat experience -- Supporting platforms like ChatGPT, Claude AI, and DeepSeek - -## Future Plans (v2.0) - -In version 2.0 (planned for Q3-Q4 2024), we will implement direct API integration with: -- Native API support for all major AI platforms -- Seamless switching between web and API-based interactions -- Advanced configuration and management of AI service connections -- Secure API key storage in macOS Keychain - -For more details, see the [API Key Implementation Roadmap](docs/api-key-future-implementation-doc.md). - -## Getting Started - -### Prerequisites - -- macOS 12.0+ (Monterey or later) -- Xcode 14.0+ -- Swift 5.7+ - -### Setup Guide - -1. **Clone the repository** - ```bash - git clone https://github.com/your-username/MinimalAIChat.git - cd MinimalAIChat - ``` - -2. **Initialize Git repository** (if not already cloned) - ```bash - git init - git add . - git commit -m "Initial commit" - ``` - -3. **Set up project structure** - ```bash - mkdir -p App/{Core,Modules,UI,Utilities} - mkdir -p App/Modules/{Hotkey,WebView,Subscription,Security,Navigation,Discovery} - mkdir -p App/UI/{Views,Localization,Accessibility} - mkdir -p Resources/{Assets.xcassets,Entitlements} - mkdir -p Tests/{Unit,UI,Performance} - mkdir -p docs - ``` - -4. **Create Xcode project** - ```bash - # Open Xcode and create a new macOS app project - # Choose SwiftUI App template - # Name it MinimalAIChat - # Save it in the repository folder - ``` - -5. **Configure project** - - Set minimum deployment target to macOS 12.0 - - Enable App Sandbox with network access - - Add necessary entitlements for hotkey monitoring - - Set up Info.plist with proper App Transport Security settings - -6. **Install dependencies** (if using Swift Package Manager) - - Open the project in Xcode - - File > Swift Packages > Add Package Dependency - - Add essential packages (if needed) - -7. **Configure the project for development** - ```bash - # Copy example configuration files - cp Config/APIConfig.example.swift Config/APIConfig.swift - # Edit APIConfig.swift with your development settings - ``` - -## Documentation - -Comprehensive documentation is available in the `docs/` folder: - -- [Product Requirements](docs/01-product-requirements.md) -- [Technical Specification](docs/02-technical-specification.md) -- [Project Structure](docs/03-project-structure.md) -- [Project Overview](docs/04-project-overview.md) -- [Implementation Guide](docs/05-implementation-guide.md) -- [Core Component Implementation](docs/06-core-component-implementation.md) -- [Performance Optimization](docs/07-performance-optimization.md) -- [Testing Plan](docs/08-testing-plan.md) -- [Accessibility Testing](docs/09-accessibility-testing.md) -- [Security & Compliance](docs/10-security-compliance.md) -- [SEO & App Store Optimization](docs/11-seo-workflow.md) -- [Deployment Guide](docs/12-deployment-guide.md) -- [Glossary](docs/13-glossary.md) -- [Contributing Guide](docs/14-contributing-guide.md) +- Privacy-first approach with local processing capabilities +- High-performance architecture with efficient resource management +- Seamless macOS integration +- Extensible plugin system +- Advanced session management +- Secure authentication and data handling -## Contributing +## Requirements -Please read our [Contributing Guide](docs/14-contributing-guide.md) for details on our code of conduct and the process for submitting pull requests. +- macOS 13.0 or later +- Xcode 15.0 or later +- Swift 5.9 or later -## License +## Installation + +1. Clone the repository: +```bash +git clone https://github.com/yourusername/MinimalAIChat.git +cd MinimalAIChat +``` + +2. Build the project: +```bash +swift build +``` + +3. Run the application: +```bash +swift run +``` + +## Development + +### Project Structure + +``` +Sources/ +├── MinimalAIChat/ # Main application target +│ ├── Core/ # Core application logic +│ ├── Features/ # Feature modules +│ ├── Infrastructure/ # Infrastructure services +│ └── UI/ # User interface components +└── MinimalAIChatCore/ # Core library target + +Tests/ +├── Unit/ # Unit tests +├── Integration/ # Integration tests +└── Performance/ # Performance tests +``` -This project is licensed under the MIT License - see the LICENSE file for details. +### Building -## Acknowledgments +```bash +# Build the project +swift build + +# Run tests +swift test + +# Run specific test target +swift test --filter MinimalAIChatTests +``` + +### Code Style + +This project uses SwiftLint for code style enforcement. The configuration can be found in `.swiftlint.yml`. + +## Contributing + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## License -- [OpenAI](https://openai.com/) for ChatGPT -- [Anthropic](https://www.anthropic.com/) for Claude -- [DeepSeek](https://deepseek.ai/) for DeepSeek AI \ No newline at end of file +This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/Sources/MinimalAIChat/Core/AppDelegate.swift b/Sources/MinimalAIChat/Core/AppDelegate.swift new file mode 100644 index 0000000..3eb6412 --- /dev/null +++ b/Sources/MinimalAIChat/Core/AppDelegate.swift @@ -0,0 +1,57 @@ +import Logging +import SwiftUI + +/// The main application delegate responsible for managing the application lifecycle +/// and core services. +@main +final class AppDelegate: NSObject, NSApplicationDelegate { + // MARK: - Properties + + private let logger = Logger(label: "com.minimalaichat.app") + private var window: NSWindow? + + // MARK: - NSApplicationDelegate + + func applicationDidFinishLaunching(_: Notification) { + logger.info("Application launching...") + setupWindow() + setupServices() + } + + func applicationWillTerminate(_: Notification) { + logger.info("Application terminating...") + cleanupServices() + } + + // MARK: - Private Methods + + private func setupWindow() { + let contentView = ContentView() + window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [ + .titled, + .closable, + .miniaturizable, + .resizable + ], + backing: .buffered, + defer: false + ) + + window?.center() + window?.setFrameAutosaveName("Main Window") + window?.contentView = NSHostingView(rootView: contentView) + window?.makeKeyAndOrderFront(nil) + } + + private func setupServices() { + // Initialize core services here + logger.info("Setting up core services...") + } + + private func cleanupServices() { + // Cleanup core services here + logger.info("Cleaning up core services...") + } +} diff --git a/Sources/MinimalAIChat/UI/ContentView.swift b/Sources/MinimalAIChat/UI/ContentView.swift new file mode 100644 index 0000000..44a5fbb --- /dev/null +++ b/Sources/MinimalAIChat/UI/ContentView.swift @@ -0,0 +1,51 @@ +import SwiftUI + +/// The main content view of the application. +struct ContentView: View { + // MARK: - Properties + + @State private var selectedTab: Tab = .chat + + // MARK: - Body + + var body: some View { + NavigationView { + List { + NavigationLink( + destination: ChatView(), + tag: Tab.chat, + selection: $selectedTab + ) { + Label("Chat", systemImage: "message") + } + + NavigationLink( + destination: SettingsView(), + tag: Tab.settings, + selection: $selectedTab + ) { + Label("Settings", systemImage: "gear") + } + } + .listStyle(SidebarListStyle()) + .frame(minWidth: 200) + + Text("Select a tab") + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .frame(minWidth: 800, minHeight: 600) + } +} + +// MARK: - Tab Enum + +private enum Tab { + case chat + case settings +} + +// MARK: - Preview + +#Preview { + ContentView() +} diff --git a/Sources/MinimalAIChat/UI/Views/ChatView.swift b/Sources/MinimalAIChat/UI/Views/ChatView.swift new file mode 100644 index 0000000..12ff046 --- /dev/null +++ b/Sources/MinimalAIChat/UI/Views/ChatView.swift @@ -0,0 +1,68 @@ +import SwiftUI + +/// The main chat interface view. +struct ChatView: View { + // MARK: - Properties + + @State private var messageText = "" + + // MARK: - Body + + var body: some View { + VStack { + ScrollView { + LazyVStack(spacing: 12) { + ForEach(0 ..< 5) { _ in + MessageBubble(isUser: Bool.random()) + } + } + .padding() + } + + HStack { + TextField("Type a message...", text: $messageText) + .textFieldStyle(RoundedBorderTextFieldStyle()) + + Button(action: sendMessage) { + Image(systemName: "arrow.up.circle.fill") + .font(.title2) + } + .disabled(messageText.isEmpty) + } + .padding() + } + } + + // MARK: - Private Methods + + private func sendMessage() { + // TODO: Implement message sending + messageText = "" + } +} + +// MARK: - MessageBubble + +private struct MessageBubble: View { + let isUser: Bool + + var body: some View { + HStack { + if isUser { Spacer() } + + Text(isUser ? "User message" : "AI response") + .padding() + .background(isUser ? Color.blue : Color.gray.opacity(0.2)) + .foregroundColor(isUser ? .white : .primary) + .cornerRadius(12) + + if !isUser { Spacer() } + } + } +} + +// MARK: - Preview + +#Preview { + ChatView() +} diff --git a/Sources/MinimalAIChat/UI/Views/SettingsView.swift b/Sources/MinimalAIChat/UI/Views/SettingsView.swift new file mode 100644 index 0000000..4634b90 --- /dev/null +++ b/Sources/MinimalAIChat/UI/Views/SettingsView.swift @@ -0,0 +1,46 @@ +import SwiftUI + +/// The settings interface view. +struct SettingsView: View { + // MARK: - Properties + + @State private var apiKey = "" + @State private var selectedModel = "GPT-4" + @State private var enableLocalProcessing = false + + // MARK: - Body + + var body: some View { + Form { + Section("API Configuration") { + SecureField("API Key", text: $apiKey) + Picker("Model", selection: $selectedModel) { + Text("GPT-4").tag("GPT-4") + Text("GPT-3.5").tag("GPT-3.5") + Text("Claude").tag("Claude") + } + } + + Section("Processing") { + Toggle("Enable Local Processing", isOn: $enableLocalProcessing) + } + + Section("About") { + HStack { + Text("Version") + Spacer() + Text("1.0.0") + .foregroundColor(.secondary) + } + } + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +// MARK: - Preview + +#Preview { + SettingsView() +} diff --git a/Sources/MinimalAIChat/main.swift b/Sources/MinimalAIChat/main.swift new file mode 100644 index 0000000..0652690 --- /dev/null +++ b/Sources/MinimalAIChat/main.swift @@ -0,0 +1,12 @@ +import Logging +import MinimalAIChatCore + +let logger = Logger(label: "com.minimalaichat.app") + +@main +struct MinimalAIChat { + static func main() { + logger.info("Starting MinimalAIChat application") + // TODO: Initialize and run the application + } +} \ No newline at end of file diff --git a/Sources/MinimalAIChatCore/Core.swift b/Sources/MinimalAIChatCore/Core.swift new file mode 100644 index 0000000..eb30c1c --- /dev/null +++ b/Sources/MinimalAIChatCore/Core.swift @@ -0,0 +1,19 @@ +import Foundation +import Logging + +/// Core module for MinimalAIChat application +public struct Core { + private let logger: Logger + + /// Initialize a new Core instance + /// - Parameter logger: The logger instance to use + public init(logger: Logger) { + self.logger = logger + } + + /// Initialize the core functionality + public func initialize() { + logger.info("Initializing MinimalAIChatCore") + // TODO: Add core initialization logic + } +} \ No newline at end of file diff --git a/Tests/Unit/CoreTests.swift b/Tests/Unit/CoreTests.swift new file mode 100644 index 0000000..2ba9a9b --- /dev/null +++ b/Tests/Unit/CoreTests.swift @@ -0,0 +1,12 @@ +import Logging +import XCTest +@testable import MinimalAIChatCore + +final class CoreTests: XCTestCase { + func testCoreInitialization() { + let logger = Logger(label: "com.minimalaichat.test") + let core = Core(logger: logger) + core.initialize() + // Add more assertions as we implement functionality + } +} \ No newline at end of file diff --git a/docs/V1_ARCHIVE.md b/docs/V1_ARCHIVE.md new file mode 100644 index 0000000..4f38ca2 --- /dev/null +++ b/docs/V1_ARCHIVE.md @@ -0,0 +1,61 @@ +# MinimalAIChat V1 Archive + +## Overview +This document tracks the archive of MinimalAIChat V1 codebase and related documentation. + +## Archive Structure +``` +Archived/ +└── v1/ + ├── code/ + │ ├── App/ + │ ├── Sources/ + │ └── Tests/ + ├── docs/ + │ ├── ARCHIVE.md + │ ├── Discussions.md + │ ├── FeatureRefinement.md + │ ├── ProjectJourney.md + │ └── ProgressTracker.md + └── config/ + ├── Package.swift + └── .swiftlint.yml +``` + +## Archived Components + +### Core Features +- WebView Integration +- Chat Interface +- Settings Management +- Authentication System +- Session Management + +### Documentation +- Project Journey +- Feature Refinement +- Progress Tracking +- Architecture Decisions +- Implementation Notes + +### Configuration +- Swift Package Manager Setup +- SwiftLint Configuration +- Build Settings +- Development Environment + +## Archive Status +- [ ] Core codebase archived +- [ ] Documentation archived +- [ ] Configuration files archived +- [ ] Build artifacts cleaned up + +## Migration Notes +- V1 codebase will be preserved for reference +- Key learnings documented in V2 architecture +- Performance issues and solutions tracked +- Security considerations documented + +## Archive History +- Created: March 29, 2024 +- Purpose: Preserve V1 codebase for reference while moving to V2 \ No newline at end of file diff --git a/setgit.md b/setgit.md new file mode 100644 index 0000000..e69de29