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