Architecture
September 17, 2026 · View on GitHub
Native Swift/SwiftUI menu-bar (accessory) app. SPM executable, no external app framework.
Data
Models.swift, CodableRepo,Agent,RunMode,Settings, seed agents (Claude Code, Codex). Tolerant decoders so adding fields never reseeds the store.LaunchDefaultsis the pure launch-resolution precedence: override → per-repo pin → last-used → agent default.Store.swift,AppStore(@MainActor ObservableObject), JSON at~/Library/Application Support/Tintpad/store.json. Frecency recording, background auto-discovery, debounced settings writes, the (tip-jar)allows(_:)entitlement check. A store that fails to load is preserved asstore.corrupt-<ts>.json, never overwritten by a reseed.SingleInstance.swiftflocks a sidecar so two instances can't clobber each other's writes.
Services
Frecency.swift,fre-style continuous half-life decay ranking.FuzzyMatch.swift, how a query finds a repo.FuzzyMatch.matchreturns a tier (exact, prefix, word boundary, infix of three letters or more, subsequence, path) plus the matched character offsets in the name, case- and diacritic-insensitive.RepoSearch.rankorders hits by (tier, frecency index), a strict weak ordering, so an exact name outranks a frequent fork and the list never reshuffles. The palette draws the offsets as match ink, and caches the ranking againstAppStore.searchRevision(bumped on any repo or half-life change).GitInfo.swift, parses.git/HEAD+.git/configdirectly (no subprocess).CommandTemplate.swift, variable substitution + login-shell binary resolution. All interpolated values are sanitized (control chars stripped) and single-quoted, the injection surface, unit-tested.RepoDiscovery.swift, scans root folders 1–2 levels deep for.git.ShellEnvironment.swift, resolves the interactive login-shell PATH (with a timeout, off the main thread) so a GUI-launched app findsclaude/codex/… (the #1 footgun).ProcessRunner.swift, the one way subprocesses run: hard timeout, concurrently drained pipes (no 64KB deadlock), SIGTERM then SIGKILL, plusspawnDetachedfor processes that are the thing being opened. Backs the terminal-adapter helper,osascript(itsinputwrites the script to stdin), worktree git, and the async GitHub clone.GitStatus.swift, "is this working tree dirty" via a boundedgit status --porcelain(first-byte read, never takes the index lock).RepoTint.swift, every repo's stable identity hue and short name.WorktreeService.swift,DispatchService.swift,GitHubService.swift,Keychain.swift,LaunchService.swift.
Terminal handoff
LaunchService.swift, the one launch path. No launch blocks the main thread:handOffruns the adapter'sprepareon the main actor (installed, TCC trust,NSRunningApplication), then theTerminalHandoff.blockinghalf on a serial GCDhandoffQueue(serial so two keystroke scripts never interleave, every step bounded), and answers through a main-actor completion, after recording frecency and the session on success.launchAgent,resumeLastandopenInEditorhave no synchronous variants. The palette holdslaunchInFlightuntil the answer, says "Still opening …" after 4s (LaunchStatusCopy), keeps Esc live, plays the launch exit on a focus loss, and routes each answer through the pureLaunchAnswerPolicy: its own drop lands it, a newer idle drop shows a failure at once, otherwise the next summon within 30s does, and Return retries it. Resumes from every surface share one in-flight guard (ResumeError.inFlight), so a double press never launches twice.TerminalAdapter.swift, aSendableprotocol + 7 adapters. Ghostty / kitty / Alacritty viaopenCLI flags (Ghostty needs an Accessibility keystroke, single-instance limitation), WezTerm via bundled binary, iTerm2 / Terminal.app via AppleScriptdo script(all AppleScript runs in a childosascriptthroughProcessRunner, never in process, andAppleScriptRunner.classifyturns its exit status and stderr into permission or launch errors), Warp via open-at-path + clipboard fallback (no command API). A missing TCC grant throwsTerminalLaunchError.permissionNeeded(summary:remedy:pane:), which carries the System Settings pane (PrivacyPane) so the palette can open it on ⏎ instead of only naming it in prose. See CONTRIBUTING to add one.
UI
CommandPanel.swift, a borderless.nonactivatingPanelNSPanelat.statusBarlevel (it draws over the menu bar strip to fuse with the camera housing), forced darkAqua, returns focus on Esc / focus-loss.dock()computes the summon screen's notch geometry per show (safeAreaInsets.top) and bridges it to SwiftUI viaNotchAnchor, notched screens dock flush with the screen top, plain screens just below the menu bar. The window draws no system shadow and carries no AppKit animation (animationBehavior = .none, so the order-out removes it in one transaction rather than fading it out under the drop's own scripted exit, andhide()defersNSApp.hidea runloop turn so a stranded frame can't be left composited). Dismissal is sequenced by the pureDismissSequencer(DismissSequencer.swift): blank the panel to alpha 0, order it out a runloop turn later, hide the app a turn after that, never from insideresignKey, and a summon mid-sequence cancels it. The drop casts its own shadow inside a transparent margin, andresize(toContentHeight:)still reads the real safe-area insets (zero today) so a future style change can't silently clip.PaletteView.swift, the palette: the drop, a pure-black capsule that forms under the notch (bead swells → expands in place → content follows, played byStepSequencerfrom theDropTimelinebeats, crossfade under Reduce Motion). Exits are requested throughPaletteModel.requestDismiss(_:)(DismissPolicypicks the exit, a focus loss during a launch plays the launch exit), the view plays them and callsexitDidFinish(), and only then does the controller'sDismissSequencerblank and order out the panel. Stark black and white: gray repo tokens, a white selection chip with black ink, and the contract as two etched instrument chips (AGENT and MODE, micro-label eyebrows, never truncated), with the permission-skipping mode red-etched and every dangerous path routed through one confirm gate (fireOrConfirm). A launch that fails on a missing TCC grant arms a pending state of the same shape (a short red line, ⏎ opens the exact System Settings pane, Esc cancels), because a permission error whose only affordance is prose reads as "nothing happened". Fully mute at rest: tokens only, the query materializes as you type. A scopedNSEventkey monitor drives navigation (.onKeyPresson aTextFieldswallows arrows), and ←/→ move through tokens only while the field is empty.KeyPolicydecides when Tab belongs to focus traversal instead of agent cycling.DropGeometry.swift, the pure geometry of the drop, resolved per summon fromNotchGeometry(housing depth and width, screen width) and the Dynamic Type scale: the 8pt gap below the housing or menu bar, the capsule height (the housing's depth clamped to 32 to 40, or 36 for the floating pill), chips 12pt shorter than the capsule, the minimum width (housing width plus one capsule height either side, or 280), width hugging in 8pt steps, the 20pt shadow margin and the window's height and width. The view holds no geometry literals.StepSequencer.swift, a@MainActorrunner for timed beats with an injectable scheduler and a generation counter, so a re-summon cancels every beat of the sequence it interrupts (nestedasyncAfterchains could not be cancelled).DropTimeline.swift, the beat tables for the drop's arrival and each exit (DismissReason: launch, escape, focus loss), plus the Reduce Motion crossfades, as pure data the sequencer plays.AgentMarks.swift, the agent's brand mark (shown beside its name in the launch pill and in Settings), rasterized on demand at the exact pixel size it will be drawn, with a per-brand optical correction so an airy mark and a dense one carry the same ink.Monogram.swiftassigns distinct letters across the whole agent set (AppStore.monogram(for:)is the single source of truth).Tokens.swift, spacing, radii, and the scalableTypeRampused by the resizable surfaces.SettingsView.swift(+ per-pane views), nativeNavigationSplitViewpreferences. Forced darkAqua and tinted gray at the root, because SwiftUI controls take the accent from the environment without naming it, so tinting only the sidebar left toggles, links, and list glyphs painting the user's macOS accent into a monochrome window. Two controls ignore the tint and are handled explicitly: aLinkkeeps the system link blue until asked for.buttonStyle(.plain)(linkStyle()), and aListstyles aLabel's icon itself, so rows that need an ink glyph are laid out as anHStack.HotkeyManager.swift, KeyboardShortcuts global summon.TintpadApp.swift,MenuBarExtraaccessory app.
Tests
swift test, pure-logic unit tests (frecency incl. clock-rollback clamping, command-template sanitization/injection, launch-default precedence, git parse + dirty detection, repo tints and short names, discovery, monogram assignment, Tab policy), plus the suites for the drop and the launch path: ContractPreviewTests, LaunchGateTests, OnboardingCopyTests, DismissSequencerTests, DropGeometryTests, DropSubjectTests, StepSequencerTests, DropTimelineTests, DismissPolicyTests, PaletteKeysTests, FuzzyMatchTests, AppleScriptClassifyTests, ProcessRunnerTests, LaunchStatusCopyTests, LaunchAnswerPolicyTests and PermissionEscalationTests. TESTING.md is the journey-based synthetic user testing plan. Scripts/uitest.sh, synthetic-input GUI smoke test (local only, needs Accessibility/Automation grants). TINTPAD_SHOWCASE=1 summons the palette at launch and holds it open for screenshots, see DEMO.md.
See also: AUDIT.md (security/quality), RELEASE.md, HOMEBREW.md.