Skip to content

Plannotator Snapshots: native macOS capture HUD (launch) - #1762

Draft
backnotprop wants to merge 27 commits into
mainfrom
feat/shots-macos
Draft

backnotprop wants to merge 27 commits into
mainfrom
feat/shots-macos

Conversation

@backnotprop

@backnotprop backnotprop commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Plannotator Snapshots for macOS, the launch PR. Press ⌥⇧⌘4 (Screen Capture). The screen freezes; drag a box, mark it with numbered boxes and comments, and press ⌘↩. The agent session gets one message with each image's path, your notes and each box's rectangle and comment. Nothing blocks the session, and Ask (⌘J) gets an answer from the session itself, streamed into the HUD. ⌥⇧⌘5 takes an App Capture: the frontmost window plus its accessibility text.

Merging this ships it: plannotator snapshot is out of HIDDEN_SUBCOMMANDS (the set is empty again) and documented.

What ships

  • The native app (apps/snapshots-macos, "Plannotator Snapshots.app", Swift/AppKit, macOS 14+, universal). It is embedded in the darwin binaries (index-darwin.ts). The first plannotator snapshot installs it into ~/Applications and opens it through LaunchServices, so the app holds Screen Recording (and, for App Capture, Accessibility) itself. Nothing in your terminal inherits those permissions. Carbon hotkeys need no permission. The URL scheme carries only an action and a capture kind.
  • The hub (packages/server/snapshots, plannotator snapshot hub): the store under <data dir>/snapshots/, routing (the summoning session, then the one you typed into in the last 15 minutes, then the only one), delivery and Ask, over the pull-bridge protocol.
  • The HUD page (packages/snapshots-hud): the strip, the panel, the marking canvas, View text, the Ask pane, the ⌘K picker and the no-session fallbacks.
  • The CLI: plannotator snapshot [--app] [--wait] [--session] [--no-capture], plus add, open, status, stop, install-app and hub. Capture is macOS only. The other subcommands run anywhere.
  • /plannotator-snapshot: a core skill (plannotator snapshot --wait) with command stubs for the other hosts. The installer ships it.

Defaults and hosts

On by default for every agent. One switch turns it off: PLANNOTATOR_SNAPSHOTS=0 or { "snapshots": false } in config.json. It is read once per session start. The plannotator snapshot CLI is unaffected.

Host /plannotator-snapshot The Send Ask
Claude Code with the mod answered by the mod, returns at once one plugin turn, once across processes a real turn
Pi native command, returns at once one followUp message a real turn
OpenCode 2 native command, returns at once queued into the root session a real turn
OpenCode 1, Codex, Gemini, Copilot, Vibe, Kiro, Claude Code without the mod plannotator snapshot --wait printed on stdout none

Signing

  • The release workflow's snapshots-macos job builds the app on macOS. On v* tag pushes it signs with the Developer ID, enables the hardened runtime, then notarizes and staples the app. Other runs build it ad-hoc signed.
  • The Apple secrets live in the protected GitHub environment apple-signing, which admits only main and v* tags. The job enters it only after release-eligibility passes, so pull request and branch runs reference no secret:
    • APPLE_DEVELOPER_ID_P12
    • APPLE_DEVELOPER_ID_P12_PASSWORD
    • APPLE_DEVELOPER_ID_IDENTITY
    • APPLE_NOTARY_KEY_P8
    • APPLE_NOTARY_KEY_ID
    • APPLE_NOTARY_ISSUER_ID
  • Snapshots sign check (snapshots-sign-check.yml, run it by hand) proves the secrets without making a release. It builds, signs, notarizes, staples and verifies. Its default ref is now main. Its last run passed.

Messages a host shows

  • Off macOS, all three hosts give the same macOS-only text. The mod now checks macOS before it runs the CLI.
  • A plannotator built without the Mac app: the session is linked, so the message says to reinstall with the install script, or use plannotator snapshot open. install-app cannot help in that case.

Docs

  • plannotator --help and plannotator snapshot --help.
  • The plannotator skill has a ## plannotator snapshot section, kept fresh by plannotator-skill-reference.test.ts.
  • /docs/commands/snapshot/, plus the env var row and the quickstart and installation command lists.
  • The Pi and OpenCode READMEs.
  • AGENTS.md.

Proof

  • Live host checks, no paid model. Each host runs against a scripted model endpoint (tests/helpers/scripted-model.ts) and a real hub started by plannotator snapshot hub --background in a temp HOME and data dir (tests/helpers/snapshots-world.ts). The native app never runs. The test plays the HUD through the hub API: capture a PNG, add a note, Send, Ask.
    • Pi (apps/pi-extension/snapshots-live.test.ts, the real Pi in RPC mode): /plannotator-snapshot, then one Send delivered exactly once, still once after the hub's 5 s re-send, carrying the image path and the note. The hub reports it delivered. Ask runs as a turn and returns the model's text.
    • OpenCode 2 (apps/opencode-plugin/snapshots-v2.test.ts, the packed plugin in real opencode2 2.0.19): the native command links the session. One Send arrives as one queued turn, byte for byte the composed text, still once after the re-send. Ask runs as a turn.
    • Claude Code mod (apps/hook/hooks/mod/snapshots-live.test.ts, the mod's own code on real processes): /plannotator-snapshot through the mod. One plugin turn is byte-identical to the composed text, still once after 7 s. Ask runs as a turn.
    • CI runs all three against the compiled Linux binary: the Inbox Pi and Claude Code jobs, and the "OpenCode 2 installed package" job. All three passed there (off macOS, /plannotator-snapshot links the session on demand and the test summons it as the CLI would). Their path filters now cover the hub. They also passed locally on macOS with the CLI from source.
    • The vendor parity test now reads vendor.sh's snapshots/ loop. It had been failing since the Pi host landed, and became visible once the Pi job ran on this branch.
  • bun run typecheck, bun test, the mod engine harness, build:hook, the Pi parity gate (vendor.sh + bun pm pack --dry-run), check-release-version and actionlint all pass.
  • Earlier rounds (see the history of this PR): real Claude Code 2.1.291 on the fake Messages API, the HUD in Chromium and WebKit, native capture through --selftest, and the compiled darwin binary installing the embedded app.

How to try it

  1. Install a build from this branch (or the release), then in Claude Code (2.1.287+, with the mod), Pi or OpenCode 2 run /plannotator-snapshot. Anywhere else, run plannotator snapshot --wait.
  2. Allow Screen Recording for Plannotator Snapshots when macOS asks.
  3. Drag a box, mark it with a numbered box and a comment, and press ⌘↩. The message lands in the session.
  4. Optional: ⌥⇧⌘5 for an App Capture, ⌘J to ask the session about a snapshot, plannotator snapshot status to see the hub and sessions.

Not built yet

  • Dragging thumbnails out as files.
  • A login item.
  • Settings for hotkeys and exclusions.
  • The Windows installers. Capture is macOS only, so Windows users would only get the "macOS for now" message.
  • An Amp palette command.

backnotprop and others added 13 commits October 7, 2026 09:58
…s, Reopen only when needed, revoked states); icon slots
…ransparent) and the HUD's mark; icon scripts
The two capture modes are now Screenshot (⌥⇧⌘4, the picture) and Snapshot
(⌥⇧⌘5, the picture plus the window's accessibility text). Renamed in the
menu, HUD, permission card, CLI help and flag (plannotator screenshot
--snapshot, also the /plannotator-screenshot argument), docs, and the
internal identifiers: shot kind "snapshot", settings.snapshots, the URL
scheme kind=snapshot, Swift enum cases and selectors.

Also fixes 'plannotator screenshot --app' never reaching the Shots command:
the annotate live-app --app flag was stripped from argv before the
screenshot dispatch. --snapshot is not.
The URL command that launches the app is handled before
didFinishLaunching, so begin() saved pendingShot and resumeAfterLaunch()
then read that same entry as a shot from before a relaunch and switched
the card to Waiting: the Allow button was gone and macOS was never asked.
pendingShot now records the pid that saved it, and only an earlier
process's shot resumes. Found in the signed permission test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The URL command that launches the app is handled before
didFinishLaunching, so Permissions.begin() drew the card through a
show hook that was not wired yet and the card never appeared: the app
sat with no window. Permissions keeps the last card state and draws it
the moment the hook is set. The panel now logs page load, ready,
layout changes and a web-content crash, and has a navigation delegate
(its didFail handlers were never called). Found in the signed
permission test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ring had one side cut away, so it read as a "U" (and stood still under
reduced motion). It is now a faint full ring with one bright quarter turning
on it; under reduced motion it fades slowly instead of standing still.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The note on the whole image is now a field under the shot (N or the rail
button focuses it), saved with the shot, removable, and sent before the
numbered boxes ('Note on this image: …'); the headline and send summary
count notes apart from comments, and Ask includes the note.

The open-shot panel resizes from its top-left corner and top/left edges,
anchored bottom-right. The drag runs natively (PanelResizer over the web
view) so frames follow the pointer with no web round trip; the size is
kept in UserDefaults, clamped between 760x540 and the visible screen less
16 pt, and a double-click resets it.
…r-snapshot skill

Product: Plannotator Snapshots (app bundle, menu bar, permission cards, HUD,
docs). Command: plannotator snapshot, /plannotator-snapshot. Modes: Screen
Capture (opt-shift-cmd-4, a box, picture only) and App Capture
(opt-shift-cmd-5, a window plus its accessibility text, --app).

Internal names follow: bundle id ai.plannotator.snapshots, URL scheme
plannotator-snapshots://, data dir snapshots/, PLANNOTATOR_SNAPSHOTS and the
snapshots config key, the hub, folders, Swift target, release job.

--app and --static are taken from annotate's argv only (live-flags.ts), so
snapshot --app reaches its command.

New core skill plannotator-snapshot (core, Claude bang copy, OpenCode,
Gemini, Kiro, Vibe, Copilot and Droid stubs) runs plannotator snapshot --wait.
@backnotprop backnotprop changed the title Plannotator Shots: native macOS screenshot HUD (hidden until launch) Plannotator Snapshots: native macOS capture HUD (hidden until launch) Oct 8, 2026
backnotprop and others added 4 commits October 8, 2026 09:27
Granting Screen Recording often makes macOS quit and reopen the app. The
pending capture was cleared at the grant, so the reopened app sat silent
with nothing on screen and a waiting /plannotator-snapshot never returned.
The pending capture now stays saved until the capture actually opens, and a
relaunch that finds the permission already granted goes straight to the
capture. Found in the owner's first run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The capture overlay gets a toolbar like macOS's own ⌘⇧5 bar (Cancel,
Screen Capture, Window, Full Screen, App Capture, each with its key),
bottom center of the display with the pointer, in its own panel above
the overlay: never key, never in a capture, out of the way while a box
is dragged. App Capture there (or A) picks a window and takes it with
its text; without Accessibility it closes the overlay, shows the card,
and reopens on App Capture.

The HUD's strip and panel get a + beside a remembered Screen / App
toggle (hub setting captureMode), replacing the icon-only ◫ button;
"App Capture for ⌥⇧⌘4" stays in the menu bar.

Co-Authored-By: Claude <noreply@anthropic.com>
…pple-signing environment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	AGENTS.md
#	apps/hook/hooks/mod/controller.ts
#	apps/hook/hooks/mod/enabled.ts
#	apps/hook/hooks/mod/register.ts
#	apps/hook/package.json
#	apps/hook/server/cli.ts
#	package.json
#	packages/server/uninstall.ts
@backnotprop
backnotprop deployed to apple-signing October 8, 2026 19:26 — with GitHub Actions Active
…1803)

* Snapshots: App Capture turns on Chromium/Electron accessibility trees itself

AXManualAccessibility is written on the application element before the walk
for Chromium browsers and Electron apps (after Codex's Appshots), once per
process, with a bounded 600 ms settle inside the 2 s budget. Other apps are
asked only when their tree comes back empty. AXEnhancedUserInterface only as a
per-capture fallback for a Chromium that refuses AXManualAccessibility.
No text: the picture goes on its own. Unit tests via swift test; --selftest
checks the decisions and gains --ax-app.

* Snapshots: review fixes for App Capture's accessibility enablement

Every AX message gets its own messaging timeout clamped to the remaining
budget (children did not inherit the app element's 0.25 s), and the work
stops 100 ms early so the AXEnhancedUserInterface reset fits in the 2 s.
That reset now always runs unless the attribute read true beforehand.
The walk is generic over a tree source; a fake tree tests the secure-field
skip and the deadline. Documents the persistent AXManualAccessibility cost.
)

* Snapshots for every agent: Pi and OpenCode 2 link to the hub, on by default

- packages/shared/snapshots/agent-link.ts: the Pi / OpenCode session link
  (summon, hub hello, pull-bridge poll, deliver claimed once across processes,
  Ask through the host's session bridge); vendored to Pi.
- packages/ai/session-bridge-pull-client.ts: custom poll/event paths, poll
  extras, extra commands with an outbox post, superseded back-off.
- Pi: native /plannotator-snapshot (non-blocking), Send as a followUp user
  message, Ask as a real turn through the Pi session bridge.
- OpenCode 2: native /plannotator-snapshot beside the review commands, Send as
  a queued session.prompt into the root session, Ask through the OpenCode
  session bridge; OpenCode 1 keeps the blocking stub.
- One switch, resolveSnapshotsEnabled in packages/shared/config.ts, default
  on; the mod's mirror defaults on too (enabled.test.ts parity).
- Tests against a real in-process hub; AGENTS.md Snapshots hosts section.

* Snapshots hosts: review fixes (lazy OpenCode, no lone notice, session lease)

- OpenCode: no event subscription, bridge or probe until a running hub is
  found (macOS 5 s check); the hub going away, a deleted session and the
  plugin's cleanup tear it down.
- OpenCode: a successful /plannotator-snapshot posts no notice; a failure's
  notice is tracked and a Send while it is pending is steered with it (#1515).
- Shared link: the Inbox wake's session lease; only the holder says hello,
  polls and delivers. Nothing spawned before a hub (link and mod).
- macOS only links at start; elsewhere the command links on demand.
- Pi: the Send is a followUp custom message confirmed by details.sendId.
- OpenCode: one question per session across bridges.
- An older plannotator answers 'update Plannotator' (link and mod).

* Snapshots link: a stale hub.json costs a pid check, not a fetch, lease write or git spawn
…very once, hub input) (#1805)

* Snapshots: fix the review findings (URL-scheme trust, signing secrets, delivery once, hub input)

- The app's URL scheme carries only an action and a capture kind; the data dir
  and the CLI argv come from the app's defaults (written by the CLI with
  `defaults write`), and the argv runs only when every element is a trusted
  file. The hub registry must name loopback, the HUD web view navigates only on
  the hub's origin, and the message handler takes the main frame on that origin
  only (SnapshotsSecurity, unit tested; --selftest checks it too).
- release.yml enters the apple-signing environment only on tag runs; PR and
  branch runs reference no secret.
- Delivery is one turn across two Claude Code processes on one session: the
  session lease plus a per-send claim (the Inbox link's rules); the hub stops
  re-sending once a send is accepted and hands it out again after a hello.
- `snapshot --wait` re-points an open collection that already has a destination.
- install-app compares build stamps by order, keeps a newer or same build, and
  waits for a running app to quit before replacing it.
- Hub input: send ids are plain names inside sends/, captures are verified as
  images by their bytes, window text only from incoming/, malformed marks are
  refused with 400, HUD tokens are bounded.
- The agent message puts window titles and URLs on their own JSON-quoted lines
  and drops URL queries.
- Tests for the hub, store, compose, connections, SnapshotsLink, install-app
  and the uninstall additions.

* Snapshots review minors: 127.0.0.1-only hub origin, defaults written only by the installed CLI, signing after the eligibility gate

- HubOrigin accepts only http://127.0.0.1:<port> (localhost can resolve to ::1).
- The CLI writes the app's dataDir/cli defaults only from the compiled binary
  the install script manages (__CLI_VERSION__ set and isManagedBinary); a run
  from source or a test never moves the installed app's data dir.
- release.yml: snapshots-macos needs release-eligibility and signs only after
  it passed; build and the jobs below it keep running when the gate is skipped
  (PRs, branches, dry runs) and stop when it failed.
- Remove snapshot from HIDDEN_SUBCOMMANDS (the set is empty again); the
  top-level --help lists plannotator snapshot, add and the other forms;
  snapshot --help describes stop, install-app, hub, --session, --no-capture
- plannotator skill: a ## plannotator snapshot section, a table row and the
  PLANNOTATOR_SNAPSHOTS env row; the freshness test pins it released
- Marketing: /docs/commands/snapshot, the env var row, quickstart and
  installation command lists
- Pi and OpenCode READMEs: /plannotator-snapshot, on by default,
  PLANNOTATOR_SNAPSHOTS=0
- AGENTS.md: the Snapshots section reads as shipped
- Snapshots sign check defaults to main
- tests/helpers/snapshots-world.ts: a real hub from plannotator snapshot hub
  --background in the world's temp HOME and data dir (no native app runs);
  snapshotsHubClient drives any hub as the HUD does
- Pi (real Pi in RPC mode), OpenCode 2 (the packed plugin in real opencode2)
  and the Claude Code mod (its own code on real processes): /plannotator-snapshot,
  one Send delivered exactly once with the image path and the note, past the
  hub's re-send; Ask from the hub answered as a turn. Scripted model only
- CI: the Inbox Pi / Claude Code jobs and the OpenCode 2 installed-package job
  run them against the compiled binary; path filters cover the hub
@backnotprop backnotprop changed the title Plannotator Snapshots: native macOS capture HUD (hidden until launch) Plannotator Snapshots: native macOS capture HUD (launch) Oct 8, 2026
…stall it

/plannotator-snapshot answered "could not start" when the CLI summoned the
session and then found no app, though the hub was up and the session linked.
The mod, Pi and OpenCode now say one sentence (SNAPSHOTS_APP_MISSING_TEXT,
the mod's copy pinned equal): linked, run `plannotator snapshot install-app`.
The CLI's own line names install-app too. A test per host, the live proofs
assert it on macOS, and a test ties the stub to the CLI's text.
…l; docs say add --screen needs macOS

- The mod's /plannotator-snapshot now checks macOS (the file only macOS has)
  before it runs the CLI, and off macOS gives the text Pi and OpenCode give
  (SNAPSHOTS_MACOS_ONLY_TEXT, copy pinned equal), runs nothing and links on
  demand. A 0.28.8 binary on Linux no longer reads as "update Plannotator".
- "Not installed" is reached only by a plannotator built without the
  embedded app, where install-app fails too: every host now says the session
  is linked and to reinstall with the install script, or use snapshot open.
  The CLI's line says the same.
- Skill and docs: add <file>, open, status, stop and hub run off macOS;
  add --screen uses screencapture and needs macOS.

This branch had an error being deployed

1 failed (outdated) deployment
apple-signing — 06cd6c84 Deployed Oct 8, 2026 by backnotprop via snapshots-macos #3317
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant