Quotio - Codebase Structure, Architecture, and Code Standards
Last Updated: December 27, 2024
Swift Version: 6.0
Minimum macOS: 15.0 (Sequoia)
Table of Contents
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. Load more
# 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](#directory-structure) 2. [Architecture Patterns](#architecture-patterns) 3. [Code Style Guidelines](#code-style-guidelines) 4. [Key Implementation Patterns](#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`: ```swift // 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: ```swift 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: ```swift @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: ```swift // 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`): ```swift // 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: ```swift // 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**: ```swift 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**: ```swift 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: ```swift 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`: ```swift 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: ```swift // 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: ```swift // 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`: ```swift extension Color {