Files
prompt/AGENTS.md
T
TZGyn c074be3378 Initial commit: a Copper-style panel for prompts and photos
Keeps text prompts and photos in a floating always-on-top panel and
copies them back to the clipboard on click. Double-Shift captures the
selection from any app; sections, search, copy-as-list, merge, and
local JSON persistence mirror shadcn's Copper workflow.

💘 Generated with Crush

Assisted-by: Crush:kimi-k3
2026-10-07 20:54:49 +08:00

5.5 KiB
Raw Blame History

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

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 <version> — 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<version> --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:

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.