Imported from watzon/pindrop (
AGENTS.md). Install upstream withnpx skills add watzon/pindrop. Copyright stays with the author.
Repository Guidelines
Last updated: 2026-03-24
Project Snapshot
- App:
Pindrop(menu bar macOS app,LSUIElementbehavior) - Stack: Swift 5.9+, SwiftUI, SwiftData, Swift Testing, XCTest UI tests
- Platform target: macOS 14+
- Main dependency path:
Pindrop.xcodeproj+ SwiftPM - Entry points:
Pindrop/PindropApp.swift,Pindrop/AppCoordinator.swift
Source Layout
- App code:
Pindrop/ - Services:
Pindrop/Services/ - UI:
Pindrop/UI/ - Persistence models:
Pindrop/Models/ - Utilities/logging:
Pindrop/Utils/ - Tests:
PindropTests/ - Test doubles:
PindropTests/TestHelpers/ - Build automation:
justfile,scripts/,.github/workflows/
Required Local Tooling
- Xcode with command-line tools (
xcodebuild) justfor all routine workflows:brew install just- Optional:
swiftlint,swiftformat,create-dmg - Apple Developer signing configured in Xcode for signed local/release builds; CI recipes use explicit unsigned overrides
Build and Run Commands
Prefer just recipes over ad-hoc shell commands.
just build # Debug build (ALWAYS use this when testing builds)
just build-release # Release build
just export-app # Developer ID export for distribution
just dmg # Signed DMG for distribution
just test # Unit test plan
just test-integration # Integration test plan (opt-in)
just test-ui # UI test plan
just test-all # Unit + integration + UI
just test-coverage # Unit tests with coverage
just dev # clean + build + test
just ci # clean + unsigned build + unsigned test + unsigned release build
just xcode # open Xcode project
Direct focused test commands:
xcodebuild test -project Pindrop.xcodeproj -scheme Pindrop -testPlan Unit -destination 'platform=macOS'
xcodebuild test -project Pindrop.xcodeproj -scheme Pindrop -testPlan UI -destination 'platform=macOS'
xcodebuild test -project Pindrop.xcodeproj -scheme Pindrop -destination 'platform=macOS' -only-testing:PindropTests/AudioRecorderTests
xcodebuild test -project Pindrop.xcodeproj -scheme Pindrop -destination 'platform=macOS' -only-testing:PindropTests/AudioRecorderTests/testStartRecordingRequestsPermission
Coding Conventions
- Follow existing file header style (
Created on YYYY-MM-DD) - Use
final classfor services and most concrete implementations - Actor isolation pattern: services are usually
@MainActor - Known exception: hotkey internals with Carbon/event constraints
- Use
@Observablefor reactive services where compatible SettingsStoreintentionally usesObservableObject+@AppStorage- Keep import groups consistent with existing files
Service Patterns
- Dependency injection via initializer arguments (avoid hidden globals)
- Protocol abstractions for hardware/system boundaries
- Example protocol seam:
AudioCaptureBackendinPindrop/Services/AudioRecorder.swift - Keep async boundaries explicit (
async/async throws) - Avoid fire-and-forget tasks unless they are UI/lifecycle orchestration
Error Handling
- Define domain errors as
enum ...: Error, LocalizedError - Keep user-facing messaging in
errorDescription - Catch at boundaries, log with context, then rethrow typed errors when possible
- Do not swallow errors with empty catch blocks
Localization
- String Catalogs:
Pindrop/Localization/Localizable.xcstrings(in-app copy) andPindrop/Localization/InfoPlist.xcstrings(privacy strings, bundle display name). Both are in the app target’s Copy Bundle Resources. The top-levelLocalization/tree is the source of truth for the YAML-first pipeline. - Runtime API:
localized("English key", locale: locale)inPindrop/AppLocalization.swiftnow resolves through generated stable-key metadata before falling back toBundle;SettingsStore.selectedAppLocaledrives UI locale andSettingsStore.selectedAppLanguagedrives dictation/transcription language. - New user-facing strings: Add an entry to the YAML source tree under
Localization/, then runjust l10n-syncso the catalogs and generated Swift stay in sync. - New language (locale): Add the locale with
just l10n-add-locale <locale>(or editLocalization/locales.yml), then populate the relevantLocalization/app/*.ymlandLocalization/infoplist/*.ymlfiles before syncing. - Interface vs dictation language: The General settings UI now separates interface language from dictation language. Keep
AppLocale-driven UI locale changes away fromAppLanguage/transcription behavior. - AI enhancement prompts:
AIEnhancementSettingsViewlocalizes default prompts for display; if the user saves without editing, the localized prompt text can be persisted and sent to the API—expect models to follow non-English system prompts, or keep defaults in English if you change that flow. - Localization tooling: Use
just l10n-import-current,just l10n-sync, andjust l10n-lint. The oldscripts/translate_xcstrings.pyhelper is obsolete.
Logging
- Use
Logcategories fromPindrop/Utils/Logger.swift - Categories include:
audio,transcription,model,output,hotkey,app,ui,update,aiEnhancement,context - Log intent and failure context; avoid noisy per-frame spam
SwiftData and Persistence
- Models use SwiftData macros (
@Model,@Attribute(.unique)) - Keep schema-related changes coordinated with schema files under
Pindrop/Models/ - Use in-memory model containers for unit tests when testing store logic
Testing Conventions
- Test files:
*Tests.swift - Unit tests use Swift Testing with
@Suite/@Test; macOS UI coverage stays inPindropUITests/with XCTest UI APIs - Standard naming:
sutfor system under test - Prefer local fixture builders over shared
setUp/tearDown; usePindropTests/TestSupport.swiftfor reusable test helpers - Use protocol mocks from
PindropTests/TestHelpers/for hardware/system APIs - Integration tests are gated (see
PINDROP_RUN_INTEGRATION_TESTSpattern) - Test mode signal exists in runtime (
PINDROP_TEST_MODE) - UI tests run through
PINDROP_UI_TEST_MODEand deterministic fixture surfaces inPindrop/AppTestMode.swift
Change Scope Rules
- Keep fixes minimal and local; do not refactor unrelated code in bugfixes
- Preserve architecture boundaries (UI -> coordinator -> services -> models)
- Do not introduce alternate command systems when
justrecipes already exist - Prefer extending existing services over adding parallel duplicate services
Release and Distribution
- Local release helpers:
just build-release,just export-app,just dmg,just dmg-self-signed(fallback only) - Manual release flow is
just release <X.Y.Z>(local execution, not CI-driven)- Create/edit contextual release notes (
release-notes/vX.Y.Z.md) - For feature releases (X.Y.0): update the in-app What's New announcement —
AnnouncementCataloginPindrop/Models/Announcement.swift(new id +Pindrop X.Y.0 · <Month Year>header + feature items) plus thewhatsnew:strings inLocalization/app/*.ymlfor all locales, thenjust l10n-sync(just releaseenforces this) - Run tests
- Build signed release DMG (
just dmgexports a Developer ID-signed app first) - Generate
appcast.xml - Create + push tag
- Create GitHub release via
ghwith notes + DMG +appcast.xml - Sync release notes to the website changelog:
just sync-website-changelog <X.Y.Z>runs automatically at the end ofjust release(best-effort). It copiesrelease-notes/vX.Y.Z.mdwith version/date frontmatter into../pindrop-website/src/content/changelog/(override location withPINDROP_WEBSITE_DIR), commits that one file in the website repo, and pushes so the site redeploys. If it fails, run it manually after the release.
- Create/edit contextual release notes (
- CI workflows under
.github/workflows/are for build/test validation; release publishing is manual - Sparkle appcast generation is scripted via
just appcast <dmg-path> - Keep
just build-self-signed/just dmg-self-signedonly as a fallback when Apple signing is unavailable
Quick PR Checklist
- Build passes:
just build - Relevant tests pass:
just test(and integration when touched) - No new warnings from your change scope
- Docs/comments updated only when behavior changes
- Keep diffs focused; avoid opportunistic formatting-only churn
Important Paths
- App lifecycle:
Pindrop/PindropApp.swift - Service composition:
Pindrop/AppCoordinator.swift - Settings and keychain:
Pindrop/Services/SettingsStore.swift - Audio capture core:
Pindrop/Services/AudioRecorder.swift - Transcription orchestration:
Pindrop/Services/TranscriptionService.swift - Logging facade:
Pindrop/Utils/Logger.swift - Localization:
Pindrop/AppLocalization.swift,Pindrop/Generated/LocalizationMetadata.swift,Pindrop/Generated/L10nKeys.swift,Pindrop/Localization/Localizable.xcstrings,Pindrop/Localization/InfoPlist.xcstrings,Localization/ - Localization tooling:
scripts/localization.py,justfile - Build recipes:
justfile - Contributor docs:
README.md,CONTRIBUTING.md,BUILD.md
Notes for Agents
- Use
justcommands in examples unless a directxcodebuildform is required - When adding tests, mirror structure from the nearest existing test file first
- Prefer Swift Testing assertions (
#expect,#require,Issue.record) for unit tests; keep XCTest only for UI automation - When touching settings, verify both app behavior and test-mode behavior
- When touching model or schema code, verify migration and read/write behavior
- When adding or changing user-visible strings, update both Swift
localized(...)keys andLocalizable.xcstrings(andInfoPlist.xcstringsfor permission / bundle strings) for all shipped locales