# AGENTS.md Prompt is a native macOS app written in Swift/SwiftUI. Single executable SwiftPM package, no external dependencies (all Apple frameworks). It is a Copper-style (shadcn) capture panel: prompts and photos live in sections, clicking an item copies it back to the clipboard. ## Architecture - `Sources/Prompt/App.swift` — `@main` entry using a bare `Settings { EmptyView() }` scene; the app owns its `NSStatusItem` manually (same reason as SimpleTopBar: placement control on notched Macs, seeded via `NSStatusItem Preferred Position Prompt`). Left click toggles the panel, right click shows the menu (accessibility state, launch at login, open data folder, quit). Contains the single-instance guard and `LoginItem` (`SMAppService`). - `Sources/Prompt/Store.swift` — `@MainActor` `ObservableObject` singleton. Data model: `Section` → `[PromptItem]`, item content is `.text(String)` or `.image(String)` (filename). Persists to `~/Library/Application Support/Prompt/store.json`; images are PNG files in `Images/` next to it, cached in an `NSCache`. Owns all mutations (add, toggle done, edit, delete, move, merge, clear done) and clipboard writes (`copy`, `copyAsList`). Saves on every mutation. - `Sources/Prompt/PanelView.swift` — the panel UI. Header (section switcher, search, add photos, close), item list, composer. Composer `# Name` creates/ switches sections. Click = copy; Cmd+click = multi-select; right-click = context menu (Copy, Copy as List, Done, Edit, Open, Merge, Move To, Delete). Drop target for images/files. - `Sources/Prompt/PanelController.swift` — owns the floating `NSPanel` (`.nonactivatingPanel`, `.floating`, right-edge docked, frame autosaved as `PromptPanel`), the Cmd+V image-paste monitor, and `EditWindowController` (separate edit window with a `TextEditor`). - `Sources/Prompt/HotkeyManager.swift` — double-Shift detection via a CGEvent tap on `flagsChanged` (left Shift ×2 = capture, right Shift ×2 = toggle panel), plus Carbon `RegisterEventHotKey` fallbacks (Cmd+Shift+Space toggle, Cmd+Shift+C capture) that work without Accessibility. - `Sources/Prompt/Capture.swift` — selection capture: backs up every pasteboard item, simulates Cmd+C, polls `changeCount` for ≤0.5s, reads the string, restores the backup. Runs off the main thread (hotkey dispatches to a global queue); only the store append hops back to main. ## Commands ```bash swift build # debug build swift run Prompt # dev run (menu bar icon appears) ./build-app.sh 0.1.0 # release build + .app bundle + icon + codesign ./release.sh 0.1.0 # build-app.sh + dist/Prompt.zip + notarize + sha256 scripts/make-icon.sh # regenerate Prompt.app/Contents/Resources/Icon.icns ``` There is no test target and no linter/formatter configured. Verification = `swift build` and manual run. ## Bundle build & signing `build-app.sh` does: release build → handwritten `Info.plist` (NOT generated from `Package.swift`) → `make-icon.sh` → `codesign`. It resolves a `Developer ID Application` identity from the login keychain and falls back to ad-hoc signing (`-`) if none is present. `LSUIElement` is true → no Dock icon. Bundle id is `dev.tzgyn.prompt`. ## Release process 1. `./release.sh ` — builds, zips `Prompt.app` to `dist/Prompt.zip`, and prints its sha256 (it does not tag or upload). Notarizes when the `PROMPT_NOTARY_*` env vars (or the shared Kapture App Store Connect key at `~/.config/kapture/AuthKey_K5KXMMSB28.p8`) are present; set `PROMPT_SKIP_NOTARY=1` to skip. 2. Commit and push `main`. 3. `tea releases create --tag v --asset dist/Prompt.zip --repo TZGyn/prompt --login gitea.tzgyn.dev`. 4. Bump `version`/`sha256` in `~/.cache/homebrew-tap/Casks/prompt.rb`, push, then `git -C /opt/homebrew/Library/Taps/tzgyn/homebrew-tap pull --ff-only`. **Always print the upgrade script after any release or version bump:** ```bash brew update brew upgrade --cask prompt ``` ## Gotchas - **Never bind bare keys as `.keyboardShortcut` in the panel** (Space, Delete, plain Cmd+C). SwiftUI key equivalents are matched before text input and would steal characters from the composer TextField. Only Cmd-modified shortcuts that TextFields don't use are bound (Cmd+F/K/N, Shift+Cmd+C). This is why Copy has no in-panel shortcut: click-to-copy is the primary gesture, matching Copper's intent. - The panel is a `.nonactivatingPanel`; its `canBecomeKey` override is what lets the composer TextField take focus. Don't replace it with a plain NSWindow config without rechecking that typing works without the app becoming active. - The Cmd+V paste monitor only intercepts when the pasteboard has an image AND no string, so pasting text into the composer is never swallowed. - CGEvent tap callbacks run off-main; `HotkeyManager` hops to main before firing. The tap is re-enabled on `tapDisabledByTimeout`. - `Store` is `@MainActor`; anything reaching it from monitors/callbacks must go through `Task { @MainActor in }`. - `captureSelection()` blocks up to 0.5s polling the pasteboard — keep it off the main thread (the hotkey handler already dispatches to a global queue). - The double-Shift tap needs Accessibility; without it the event tap silently fails and only the Carbon fallbacks work. The panel banner and the status menu both surface the permission state. - `.gitignore` covers `.build/`, `Prompt.app/`, `dist/` — build artifacts should never be committed.