liltype_
← Back to Documentation

Contributing to liltype

Welcome! This guide covers everything you need to contribute to liltype, an offline-first macOS menubar dictation app built with Swift 6.2.

Development Setup

Prerequisites

  • macOS 14+ (Sonoma or later)
  • Xcode with Swift 6.2
  • Git

Getting Started

git clone https://github.com/jaeyunha/liltype.git
cd liltype

swift build                    # Compile the app
swift test                     # Run all tests
swift run liltype              # Launch the app

Before opening a PR, ensure everything builds and tests pass:

swift build && swift test

Project Structure

liltype/
├── Sources/liltype/            # Main app source code
│   ├── *Configuration.swift    # Settings bundles (protocol + value type + store)
│   ├── *Coordinator.swift      # Stateful session orchestrators
│   ├── *Client.swift           # External service clients
│   ├── *Permission.swift       # System permission checks + UI
│   ├── *View.swift             # SwiftUI components
│   └── DictationMenuController.swift   # Central orchestrator
├── Tests/liltypeTests/         # Unit & integration tests (mirrors Sources/)
├── web/                        # Next.js landing page (uses bun, not npm)
├── infra/                      # Backend: TypeScript/Hono.js, AWS ECS Fargate
│   ├── src/handlers/           # API handlers (Stripe checkout, webhooks)
│   ├── ecs/                    # ECS task definition
│   └── scripts/deploy.sh       # Backend deployment
├── scripts/                    # Build & deployment scripts
├── docs/                       # Developer & user documentation
├── agent_docs/                 # Architecture & testing patterns
└── CLAUDE.md                   # Project instructions & conventions

Architecture Overview

Protocol-Driven Dependency Injection

Every major concern is a protocol. The production implementation is injected at app startup. This design enables easy testing with mock implementations.

Key protocol families:

ConcernExample ProtocolsProduction Implementation
PermissionsMicrophonePermissionControlling, AccessibilityPermissionControllingAVFoundation, AX API, ScreenCaptureKit
HotkeysGlobalHotkeyRegistering, HotkeyConflictCheckingCarbon Events + NSEvent
TranscriptionTranscriptionJobScheduling, WhisperModelTranscribing, CloudSpeechToTextTranscribingWhisperKit, OpenAI/Deepgram/Groq/AssemblyAI
Settings*SettingsStoring (13 protocols)UserDefaults-backed stores
OutputTranscriptionOutputHandlingClipboard + CGEvent paste
ModelsDictationModelCatalogProviding, ModelDownloadingWhisperKit catalog, URLSession
StorageLocalStorageWriting, ClipboardHistoryStoringSQLite
CloudProviderAPIKeyStoring, CloudCostEstimatingKeychain, provider APIs
AuthAuthSessionManaging, SubscriptionStatusProvidingSupabase OAuth, UserDefaults cache
Meeting/SessionSessionRecordingManaging, SessionAudioCapturingLive mic+system audio, chunk transcription

DictationMenuController: The Central Orchestrator

Located at Sources/liltype/DictationMenuController.swift, this @MainActor ObservableObject is the heart of the app:

  • ~79 protocol-typed dependencies injected via constructor
  • ~40+ @Published properties for UI binding
  • Responsibilities: Dictation lifecycle, meeting detection, model management, auth, settings, error handling

Production wiring is in LiltypeApp.swift:66; tests inject stubs.

Core Flows

Dictation:

Hotkey → Permission gate → Recording (live waveform) → [Release/Escape]
  → Transcribing (offline or cloud) → Output (clipboard + paste) → Idle

Meeting Capture:

App detection → Permission gate → Simultaneous mic + system audio capture
  → Chunked transcription (8s chunks, 2s overlap) → Session notes + workspace sync

Agent Sessions:

Session create → ACP/CLI transport → Transcript syncing → Workspace management

File Naming Conventions

Follow these patterns consistently:

  • *Configuration.swift — Settings bundle containing:

    • Protocol defining the interface
    • Value types (request/response objects)
    • UserDefaults-backed store implementation
    • All in one file for one domain concern
  • *Coordinator.swift — Stateful session orchestrators (e.g., MeetingSessionCoordinator)

  • *Client.swift — External service clients (e.g., ACPClient for Agent Communication Protocol)

  • *Permission.swift — System permission checks + alert UI

  • *View.swift — SwiftUI UI components

  • Tests mirror sources: FileName.swiftFileNameTests.swift

The Configuration Pattern

The Configuration pattern is how liltype manages settings. Here's how to add a new configuration concern:

Step-by-Step Example

Let's say you're adding "Focus Mode Behavior" settings:

// FocusModeConfiguration.swift

import Foundation

// 1. Define the protocol interface
protocol FocusModeSettingsStoring: Sendable {
    func loadFocusModeBehavior() -> FocusModeBehavior
    func saveFocusModeBehavior(_ behavior: FocusModeBehavior) async
}

// 2. Define value types (Codable, Equatable, Sendable)
struct FocusModeBehavior: Codable, Equatable, Sendable {
    let pauseDictationDuringFocus: Bool
    let muteNotifications: Bool
    let resumeOnExitFocus: Bool

    static let `default` = FocusModeBehavior(
        pauseDictationDuringFocus: true,
        muteNotifications: true,
        resumeOnExitFocus: true
    )
}

// 3. Create UserDefaults-backed store
final class UserDefaultsFocusModeStore: FocusModeSettingsStoring {
    private let userDefaults: UserDefaults
    private let key = "com.liltype.focusMode"

    init(userDefaults: UserDefaults = .standard) {
        self.userDefaults = userDefaults
    }

    func loadFocusModeBehavior() -> FocusModeBehavior {
        guard let data = userDefaults.data(forKey: key),
              let decoded = try? JSONDecoder().decode(FocusModeBehavior.self, from: data) else {
            return .default
        }
        return decoded
    }

    func saveFocusModeBehavior(_ behavior: FocusModeBehavior) async {
        if let encoded = try? JSONEncoder().encode(behavior) {
            userDefaults.set(encoded, forKey: key)
        }
    }
}

Integration Checklist

  1. Add the protocol to DictationMenuController dependencies
  2. Provide a default implementation in DictationMenuController.init
  3. Create a test file (FocusModeConfigurationTests.swift) with UUID-isolated UserDefaults
  4. Add a settings view if user-facing (FocusModeSettingsView.swift)

Testing Conventions

UserDefaults Isolation

Every settings store test creates an isolated suite with a UUID. See HotkeyConfigurationTests.swift:6 for the canonical pattern:

import XCTest

final class FocusModeConfigurationTests: XCTestCase {
    let suiteName = UUID().uuidString
    var userDefaults: UserDefaults!
    var store: UserDefaultsFocusModeStore!

    override func setUp() {
        super.setUp()
        userDefaults = UserDefaults(suiteName: suiteName)
        store = UserDefaultsFocusModeStore(userDefaults: userDefaults)
    }

    override func tearDown() {
        userDefaults.removePersistentDomain(forName: suiteName)
        super.tearDown()
    }

    func testDefaultBehavior() {
        let behavior = store.loadFocusModeBehavior()
        XCTAssertEqual(behavior, .default)
    }
}

Controller Testing

DictationMenuController takes ~79 dependencies. Use NoOp* stubs for irrelevant deps; create targeted mocks only for behavior under test:

let mockFocusMonitor = MockFocusMonitor()
let controller = DictationMenuController(
    focusMonitor: mockFocusMonitor,
    hotkeyRegistering: NoOpGlobalHotkeyRegistering(),
    transcriptionScheduling: NoOpTranscriptionJobScheduling(),
    // ... other deps with NoOp stubs
)

Test Coverage Expectations

  • Configuration stores: Comprehensive — load/save/defaults/edge cases
  • Controller: Permission gates, state transitions, success & failure paths
  • Session recording: Recording lifecycle, speaker diarization, chunk transcription
  • Recording window: Anchor positioning, result layout, style settings
  • Auth/Subscription: Session management, token lifecycle, subscription status caching

Code Style

Swift 6.2 Concurrency

  • Use @MainActor for UI code
  • Mark async functions clearly
  • Avoid @unchecked Sendable — refactor instead
  • All value types must be Sendable

Value Types

  • Persistence: Use Codable for UserDefaults/Keychain serialization
  • Equality: Use Equatable for testing
  • Thread-safety: Mark as Sendable
struct MySettings: Codable, Equatable, Sendable {
    let value: String

    static let `default` = MySettings(value: "default")
}

Enums for UserDefaults Serialization

Use String raw values for enum serialization:

enum ProcessingMode: String, Codable, Sendable {
    case offline = "offline"
    case cloudSTT = "cloudSTT"
    case notes = "notes"
}

Secrets & Sensitive Data

  • Keychain: Use for API keys, auth tokens, credentials
  • Never: Store secrets in UserDefaults, plist, or source code
  • Environment variables: For build-time secrets only (signing, notarization)

Web Development (Landing Page)

The landing page is a Next.js app deployed to Cloudflare Pages.

Setup

cd web
bun install              # Use bun, not npm
bun run dev              # Local development
bun run build            # Production build

Package Manager

Use bun, not npm. The project uses bun.lock (not package-lock.json). bun is significantly faster and more reliable for this project.

Deployment

Deploy via the script:

scripts/deploy-web.sh    # Builds + deploys to Cloudflare Pages

Live at: https://liltype.com

Pages

  • / — Home
  • /pricing — Tier comparison & checkout links
  • /privacy — Privacy policy
  • /terms — Terms of service

Infrastructure

Backend (infra/)

  • Language: TypeScript (Hono.js framework)
  • Deployment: AWS ECS Fargate (256 CPU, 512 MB RAM)
  • Database: Supabase PostgreSQL
  • Port: 8080

API Handlers

  • create-checkout.ts — Stripe checkout session creation
  • stripe-webhook.ts — Stripe webhook handler for subscription events
  • subscription-status.ts — Fetch user subscription status
  • create-portal.ts — Stripe billing portal link

Deployment

infra/scripts/deploy.sh              # Deploy backend to ECS

Environment Variables

Set in AWS SSM Parameter Store or ECS task definition:

  • SUPABASE_URL — Supabase API endpoint
  • SUPABASE_SECRET_KEY — Admin secret
  • STRIPE_SECRET_KEY — Stripe API key
  • STRIPE_WEBHOOK_SECRET — Webhook verification
  • STRIPE_PLUS_PRICE_MONTHLY/YEARLY — Plus tier price IDs
  • STRIPE_PRO_PRICE_MONTHLY/YEARLY — Pro tier price IDs
  • DEEPGRAM_API_KEY — Deepgram fallback API key
  • PORT — Listen port (8080)

Scripts Reference

ScriptPurpose
scripts/build-dmg.shBuild release/debug DMG with signing & notarization
scripts/deploy-web.shDeploy landing page to Cloudflare Pages
infra/scripts/deploy.shDeploy backend to AWS ECS
infra/scripts/store-secrets.shStore secrets in AWS SSM Parameter Store

build-dmg.sh Options

scripts/build-dmg.sh --version 1.0.0 --dev --skip-build
  • --version <value> — Set app version
  • --bundle-id <value> — Override bundle ID
  • --dist-dir <path> — Output directory (default: dist/)
  • --dev — Separate dev build & DMG
  • --skip-build — Skip swift build step

Git Workflow

Committing

Commit and push directly to the current branch — no need to create a feature branch first. The project uses a simple workflow:

  1. Make changes
  2. Verify with swift build && swift test
  3. Commit: git commit -m "Clear, descriptive message"
  4. Push: git push origin main

Before Opening a PR

swift build && swift test    # Must pass before PR

Ensure:

  • All tests pass
  • No new compiler warnings
  • Code follows conventions in this guide
  • Commit messages are clear

Additional Documentation

For deeper dives, see:

  • agent_docs/architecture.md — Protocol families, state machines, offline/cloud modes, DictationMenuController dependency map
  • agent_docs/testing.md — UserDefaults isolation patterns, controller testing, coverage expectations, fixture conventions
  • CLAUDE.md — Project instructions, build commands, file patterns, key conventions

Summary

  • Build: swift build && swift test before every PR
  • Pattern: Protocol-driven DI with UserDefaults for settings, SQLite for persistence, Keychain for secrets
  • Testing: UUID-isolated UserDefaults, NoOp stubs, targeted mocks
  • Code style: Swift 6.2 concurrency, @MainActor for UI, Sendable/Codable value types
  • Deployment: DMG via build-dmg.sh, web via deploy-web.sh, backend via infra/scripts/deploy.sh
  • Questions? Check agent_docs/, CLAUDE.md, or ask in an issue

Happy contributing!