Imported from lwouis/alt-tab-macos (
AGENTS.md). Install upstream withnpx skills add lwouis/alt-tab-macos. Copyright stays with the author.
macOS development
- Don't use xcode directly to develop
- Use pure swift 5.8 code to make the app. No interface builder. No SwiftUI.
- Aim for compact code. Within methods, don't have groups of statements separated with newlines. No inline comments for simple code. Instead, split statements into sub-methods.
- Use guard closes as much as possible to separate the happy-path under them
- Organize source files into folders. Folders should group files that change together, at the same pace (e.g. one feature)
- When possible, follow the triad pattern: specs in *Specs.md, unit-tests in *Tests.swift, and *swift for the implementation. Document features and their edge-cases this way
- Favor low latency and responsiveness. Reuse objects, avoid wasting memory or I/O. Use observer APIs; don't poll.
Main-thread IPC
Ordinary AppKit calls can be a synchronous XPC round trip, so they can hang for as long as the other process takes. searchField.stringValue = "" froze a user's main thread for 3.0s (#5981): it resigned first responder, which deactivates the system text-input context, which timed out. Assume any AppKit call that changes first responder, orders a window, or resizes one talks to another process.
- Everything under
NSResponderandNSCell(soNSWindow,NSView,NSControl,NSTextField,NSApplication) plusNSTextInputContextis main-thread-only, and AppKit asserts on it at runtime. For those the levers are ordering and deferral, not dispatch: the three off-main schedulers (AXCallScheduler,CGSCallScheduler,ProcessCallScheduler) can't take them. NSWorkspaceandNSRunningApplicationare the documented exception: their headers state they are thread safe, so LaunchServices reads (runningApplications,frontmostApplication,bundleURL,icon,activate) may run off-main, andApplication.fetchAppIconalready relies on that. Don't "fix" them back onto main.src/main-thread-ipc.mdhas the audited call sites and the evidence for both halves of this.- On the latency-critical paths (summoning, a keystroke while cycling or searching, dismissal with focus), nothing that can stall may run before the work the user is waiting for. Put the visible work first; put the bookkeeping after it.
- A CoreAnimation transaction commits when the runloop turn ENDS. Setting
alphaValueor callingorderOutdoes not reach the screen before that, so blocking later in the same turn keeps the old frame up. Defer withDispatchQueue.main.asyncand re-check the state in the block. MainThreadStall.step()at the top of a main-thread function names it in the log when it runs long. Use it to measure rather than guessing; temporarily lowerthresholdInMsand add marks around single statements to attribute a cost.
Comments
A wrong comment costs several times more than a missing one, for humans and agents alike. So write for low drift, not for low line count. A correct, non-obvious comment can be as long as it needs to be.
- Comment what the code cannot show: OS/API behaviour (macOS, CGS, SkyLight, AppKit), measured timings, private-API notes, invariants, and why a guard that looks removable isn't. Prefer measured evidence over recollection.
- Don't comment what the code already says. Don't narrate history: no "this used to be X", no reverted approaches, no commentary on earlier comments. If a past bug is the reason a constraint exists, name the test that pins it instead.
- State each rule once, at the widest scope it applies to. Restating it further down is what makes two copies drift apart.
- One intent per comment, and keep it short. Split a paragraph that mixes a rule, its history, and an aside.
- When you change code, re-read the comments around it. Updating them is part of the change, not a follow-up.
- Run
/rework-commentsto audit or rework comments across a file, a folder, or all ofsrc/.
Workflow
- Copy commands from ai/build.sh and run them, to confirm compilation works after you're done with implementing a change
- Run the unit tests with ai/test.sh. Don't use scripts/run_tests.sh: that's the CI entrypoint and its Release config rebuilds the whole test target on every edit (34s vs 1.3s)
- In a git worktree, copy
config/local.xcconfigover from the main checkout before building. It's gitignored, so a fresh worktree doesn't have it, anddebug.xcconfig/release.xcconfig#include?it. Without it the build dies at CodeSign ("no identity found") on a dependency framework, BEFORE the app target's own Swift compile — so it looks like the code compiled when nothing ofsrc/was checked.ai/test.shdoesn't cover that either: theunit-teststarget has its own source list and never builds the app target - Git commit messages must respect our pre-hook conventions, and must be clear and high-level, written for end-users (changelog)
License / Keychain invariant
- The app's Developer ID, TeamID, and bundle ID must remain stable across builds. Keychain items are tied to the code signature; changing any of these orphans every user's stored license key and forces mass re-activation. If a rotation is unavoidable, plan a migration first (e.g., a backup-restore handler, or
kSecAttrAccessGroupwith a stable group identifier). - Do not introduce legacy
SecKeychain*API orkSecAccessControl(biometric/PIN gating) into license code — both can trigger Keychain password prompts, which is bad UX for license activation.