850 lines · 24.8 KB · Markdown
RawDownload

Quotio - Codebase Structure, Architecture, and Code Standards

Last Updated: December 27, 2024
Swift Version: 6.0
Minimum macOS: 15.0 (Sequoia)


Table of Contents

  1. Directory Structure
  2. Architecture Patterns
  3. Code Style Guidelines
  4. Key Implementation Patterns

Directory Structure

Quotio/
├── Config/
│   ├── Debug.xcconfig           # Debug build configuration
│   ├── Release.xcconfig         # Release build configuration
│   └── Local.xcconfig.example   # Template for local overrides
│
├── Quotio/
│   ├── Assets.xcassets/
│   │   ├── AccentColor.colorset/    # Accent color definition
│   │   ├── AppIcon.appiconset/      # Production app icons
│   │   ├── AppIconDev.appiconset/   # Development app icons
│   │   ├── MenuBarIcons/            # Provider icons for menu bar
│   │   │   ├── claude-menubar.imageset/
│   │   │   ├── gemini-menubar.imageset/
│   │   │   └── ... (other providers)
│   │   └── ProviderIcons/           # Provider logos for UI
│   │       ├── claude.imageset/
│   │       ├── gemini.imageset/
│   │       └── ... (other providers)
│   │
│   ├── Models/
│   │   ├── Models.swift             # Core data types
│   │   ├── AgentModels.swift        # CLI agent types
│   │   ├── AppMode.swift            # App mode management
│   │   └── MenuBarSettings.swift    # Menu bar settings
│   │
│   ├── Services/
│   │   ├── CLIProxyManager.swift        # Proxy lifecycle management
│   │   ├── ManagementAPIClient.swift    # HTTP client (actor)
│   │   ├── StatusBarManager.swift       # Menu bar management
│   │   ├── NotificationManager.swift    # User notifications
│   │   ├── UpdaterService.swift         # Sparkle integration
│   │   ├── AgentDetectionService.swift  # Agent detection
│   │   ├── AgentConfigurationService.swift # Config generation
│   │   ├── ShellProfileManager.swift    # Shell profile updates
│   │   ├── DirectAuthFileService.swift  # Direct file scanning
│   │   ├── CLIExecutor.swift            # CLI execution
│   │   ├── LanguageManager.swift        # Localization
│   │   ├── AntigravityQuotaFetcher.swift
│   │   ├── OpenAIQuotaFetcher.swift
│   │   ├── CopilotQuotaFetcher.swift
│   │   ├── ClaudeCodeQuotaFetcher.swift
│   │   ├── CursorQuotaFetcher.swift
│   │   ├── CodexCLIQuotaFetcher.swift
│   │   └── GeminiCLIQuotaFetcher.swift
│   │
│   ├── ViewModels/
│   │   ├── QuotaViewModel.swift         # Main app state
│   │   └── AgentSetupViewModel.swift    # Agent setup state
│   │
│   ├── Views/
│   │   ├── Components/
│   │   │   ├── AgentCard.swift          # Agent display card
│   │   │   ├── AgentConfigSheet.swift   # Configuration sheet
│   │   │   ├── MenuBarView.swift        # Menu bar popover
│   │   │   ├── ProviderIcon.swift       # Provider icon
│   │   │   ├── QuotaCard.swift          # Quota display card
│   │   │   ├── QuotaProgressBar.swift   # Progress bar
│   │   │   └── SidebarView.swift        # Navigation sidebar
│   │   ├── Onboarding/
│   │   │   └── ModePickerView.swift     # Mode selection
│   │   └── Screens/
│   │       ├── DashboardScreen.swift
│   │       ├── QuotaScreen.swift
│   │       ├── ProvidersScreen.swift
│   │       ├── AgentSetupScreen.swift
│   │       ├── APIKeysScreen.swift
│   │       ├── LogsScreen.swift
│   │       └── SettingsScreen.swift
│   │
│   ├── Info.plist                   # App metadata
│   ├── Quotio.entitlements          # App entitlements
│   └── QuotioApp.swift              # App entry point
│
├── Quotio.xcodeproj/
│   ├── project.pbxproj              # Project configuration
│   └── xcshareddata/
│       └── xcschemes/
│           └── Quotio.xcscheme      # Build scheme
│
├── scripts/
│   ├── build.sh                     # Build script
│   ├── release.sh                   # Release workflow
│   ├── bump-version.sh              # Version management
│   ├── notarize.sh                  # Apple notarization
│   ├── package.sh                   # DMG packaging
│   ├── generate-appcast.sh          # Sparkle appcast
│   ├── config.sh                    # Shared configuration
│   └── ExportOptions.plist          # Archive export options
│
├── screenshots/                     # Documentation screenshots
│
├── AGENTS.md                        # Development guidelines
├── CHANGELOG.md                     # Version history
├── README.md                        # Project readme (English)
├── README.vi.md                     # Project readme (Vietnamese)
└── RELEASE.md                       # Release documentation

Architecture Patterns

MVVM (Model-View-ViewModel)

Quotio follows the MVVM architectural pattern with SwiftUI:

┌─────────────────────────────────────────────────────────────┐
│                          View Layer                          │
│  (SwiftUI Views in Views/Screens/ and Views/Components/)    │
└─────────────────────────────┬───────────────────────────────┘
                              │ @Environment(ViewModel.self)
                              │ @Bindable var vm = viewModel
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                       ViewModel Layer                        │
│  (QuotaViewModel, AgentSetupViewModel in ViewModels/)       │
│  - @Observable macro                                         │
│  - @MainActor for UI thread safety                          │
└─────────────────────────────┬───────────────────────────────┘
                              │ async/await calls
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                       Service Layer                          │
│  (CLIProxyManager, ManagementAPIClient, *Fetcher in Services/)│
│  - Singleton pattern for managers                            │
│  - Actor isolation for API clients                           │
└─────────────────────────────┬───────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                        Model Layer                           │
│  (Data types in Models/)                                     │
│  - Codable for JSON serialization                            │
│  - Sendable for cross-actor transfer                         │
└─────────────────────────────────────────────────────────────┘

Observable Pattern (Swift 6)

The app uses the new @Observable macro instead of ObservableObject:

// ViewModel Declaration
@MainActor
@Observable
final class QuotaViewModel {
    var isLoading = false
    var authFiles: [AuthFile] = []
    // Properties are automatically observed
}

// View Usage
struct DashboardScreen: View {
    @Environment(QuotaViewModel.self) private var viewModel
    
    var body: some View {
        @Bindable var vm = viewModel  // For two-way bindings
        
        List(selection: $vm.currentPage) {
            // ...
        }
    }
}

Actor-Based Concurrency

Thread-safe services use the actor keyword:

actor ManagementAPIClient {
    private let baseURL: String
    private let authKey: String
    private let session: URLSession
    
    func fetchAuthFiles() async throws -> [AuthFile] {
        // Actor isolation ensures thread safety
        let data = try await makeRequest("/auth-files")
        return try JSONDecoder().decode(AuthFilesResponse.self, from: data).files
    }
}

Singleton Pattern for Managers

Long-lived services use the singleton pattern:

@MainActor
@Observable
final class StatusBarManager {
    static let shared = StatusBarManager()
    
    private var statusItem: NSStatusItem?
    
    private init() {}  // Private initializer
    
    func updateStatusBar(...) {
        // ...
    }
}

Code Style Guidelines

Swift Version and Concurrency

Swift 6 with strict concurrency checking is required:

// All UI-related classes must be MainActor
@MainActor
@Observable
final class QuotaViewModel {
    // UI state
}

// Thread-safe services use actor
actor ManagementAPIClient {
    // Network operations
}

// Data types crossing actor boundaries must be Sendable
struct AuthFile: Codable, Identifiable, Hashable, Sendable {
    let id: String
    // ...
}

// Async operations use async/await
func refreshData() async {
    let files = try await client.fetchAuthFiles()
    self.authFiles = files
}

Observable Pattern

Use @Observable macro (not ObservableObject):

// Declaration
@MainActor
@Observable
final class QuotaViewModel {
    var isLoading = false
    var authFiles: [AuthFile] = []
}

// View access via @Environment
@Environment(QuotaViewModel.self) private var viewModel

// Two-way bindings via @Bindable
var body: some View {
    @Bindable var vm = viewModel
    Toggle("Auto Start", isOn: $vm.autoStart)
}

Naming Conventions

Element Convention Examples
Types PascalCase AIProvider, QuotaViewModel, StatusBarManager
Properties camelCase authFiles, isLoading, proxyStatus
Methods camelCase refreshData(), startProxy(), toggleItem(_:)
Constants camelCase managementKey, warningThreshold
Enum Types PascalCase AIProvider, AppMode, NavigationPage
Enum Cases camelCase case gemini, case claude, case quotaOnly
File Names Match primary type QuotaViewModel.swift, CLIProxyManager.swift

Import Order

Organize imports in this order with blank lines between groups:

// 1. System frameworks
import Foundation
import SwiftUI
import AppKit
import ServiceManagement

// 2. Conditional imports for optional frameworks
#if canImport(Sparkle)
import Sparkle
#endif

// 3. Local modules (if any)
// (Currently none in this project)

Type Definitions

Enums with Raw Values for API Compatibility:

enum AIProvider: String, CaseIterable, Codable, Identifiable {
    case gemini = "gemini-cli"
    case claude = "claude"
    case codex = "codex"
    case copilot = "github-copilot"
    
    var id: String { rawValue }
    
    var displayName: String {
        switch self {
        case .gemini: return "Gemini CLI"
        case .claude: return "Claude Code"
        case .codex: return "Codex (OpenAI)"
        case .copilot: return "GitHub Copilot"
        }
    }
}

Codable Structs with CodingKeys for snake_case APIs:

struct AuthFile: Codable, Identifiable, Hashable, Sendable {
    let id: String
    let name: String
    let provider: String
    let statusMessage: String?
    let runtimeOnly: Bool?
    let accountType: String?
    let createdAt: String?
    
    enum CodingKeys: String, CodingKey {
        case id, name, provider
        case statusMessage = "status_message"
        case runtimeOnly = "runtime_only"
        case accountType = "account_type"
        case createdAt = "created_at"
    }
}

View Structure

Organize views with MARK: - comments:

struct DashboardScreen: View {
    @Environment(QuotaViewModel.self) private var viewModel
    
    // MARK: - Computed Properties
    
    private var isSetupComplete: Bool {
        viewModel.proxyManager.isBinaryInstalled &&
        viewModel.proxyManager.proxyStatus.running
    }
    
    private var totalAccounts: Int {
        viewModel.authFiles.count
    }
    
    // MARK: - Body
    
    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                headerSection
                statsSection
                quickActionsSection
            }
            .padding()
        }
    }
    
    // MARK: - Subviews
    
    private var headerSection: some View {
        HStack {
            Text("Dashboard")
                .font(.title)
            Spacer()
        }
    }
    
    private var statsSection: some View {
        // ...
    }
    
    private var quickActionsSection: some View {
        // ...
    }
}

Error Handling

Use custom error enums conforming to LocalizedError:

enum APIError: LocalizedError {
    case invalidURL
    case invalidResponse
    case httpError(Int)
    case decodingError(String)
    
    var errorDescription: String? {
        switch self {
        case .invalidURL: return "Invalid URL"
        case .invalidResponse: return "Invalid response"
        case .httpError(let code): return "HTTP error: \(code)"
        case .decodingError(let msg): return "Decoding error: \(msg)"
        }
    }
}

// Usage in ViewModel
func refreshData() async {
    do {
        self.authFiles = try await client.fetchAuthFiles()
    } catch {
        // Store for UI display
        self.errorMessage = error.localizedDescription
    }
}

Localization

All user-facing strings must use localization:

// Extension method usage
Text("nav.dashboard".localized())
Label("action.refresh".localized(), systemImage: "arrow.clockwise")

// Key format: category.subcategory.item
// Examples:
// - nav.dashboard
// - action.startProxy
// - status.running
// - settings.port
// - error.networkError

UserDefaults Usage

Use @AppStorage in views, UserDefaults.standard in services:

// In Views - for reactive properties
struct SettingsScreen: View {
    @AppStorage("autoStartProxy") private var autoStartProxy = false
    @AppStorage("loggingToFile") private var loggingToFile = true
    
    var body: some View {
        Toggle("Auto Start", isOn: $autoStartProxy)
    }
}

// In Services - for direct access
final class CLIProxyManager {
    init() {
        let savedPort = UserDefaults.standard.integer(forKey: "proxyPort")
        if savedPort > 0 && savedPort < 65536 {
            self.proxyStatus.port = UInt16(savedPort)
        }
    }
    
    var port: UInt16 {
        didSet {
            UserDefaults.standard.set(Int(newValue), forKey: "proxyPort")
        }
    }
}

Color Handling

Use hex color initializer from Models.swift:

extension Color {

Lines 1–500 of 850.