feat(annotations): comment picker + clipboard markdown bundle for agents#27
Open
malopezr7 wants to merge 5 commits into
Open
feat(annotations): comment picker + clipboard markdown bundle for agents#27malopezr7 wants to merge 5 commits into
malopezr7 wants to merge 5 commits into
Conversation
Bun's `promisify(execFile)` returns `stdout: undefined` even with an explicit `encoding`, so the AX snapshot poller would emit "undefined is not an object (evaluating 'stdout.toString')" any time `axe describe-ui` actually succeeded. The error was masked previously because the only path that reached `execFileText` in CI was the ENOENT path (axe not installed). Drop `promisify` and call `execFile` directly with a manual Promise wrapper. Works the same on Node and Bun.
Adds the on-disk persistence layer for the upcoming comment annotation
feature. Lives in `$TMPDIR/serve-sim/annotations/{udid}/` so it
follows the same per-device convention as the existing helper state
files, and JSON Lines (append-only) so concurrent writers can't corrupt
the log.
- `annotations-shared.ts` — types shared between server, middleware, and
client (Annotation, AnnotationCreateRequest, AnnotationContext, ...).
- `annotations.ts` — list / create / update / delete / clear / asset
resolver functions. Crops are written next to the JSONL as
`crops/anno-{id}.jpg`; full-frame captures (optional) go under
`frames/`. Path traversal guard on the asset resolver since IDs come
from disk and are reflected in HTTP responses.
Wires the storage layer behind the same `{base}/api` namespace the
middleware already exposes. Both the bundled CLI middleware and the
dev-server share the routes (the `Connect` and `Bun.serve` flavors
diverge only in their request/response shape — the data layer is the
same module).
Routes:
- `GET {base}/api/annotations` — list (filtered by ?device=)
- `POST {base}/api/annotations` — create from base64 cropDataUri
- `DELETE {base}/api/annotations` — clear all for device
- `GET {base}/api/annotations/:id` — fetch one
- `PATCH {base}/api/annotations/:id` — update comment / status
- `DELETE {base}/api/annotations/:id` — delete one
- `GET {base}/api/annotations/:id/crop` — JPEG of the saved crop
- `GET {base}/api/annotations/:id/frame` — JPEG of the full frame
- `OPTIONS {base}/api/annotations*` — CORS preflight
Body size cap of 16 MB on the Connect path (well above the ~30 KB
typical crop) so we don't OOM on a malicious client. CORS is permissive
to match the rest of the preview API.
Also publishes the new src files via package.json#files so the
shared types ship to consumers of `serve-sim/middleware`.
Adds the browser-side experience for picking elements on the simulator
preview, attaching a comment, and exporting the bundle as markdown for
agent tooling. The whole feature is gated behind a "Comments / Enable"
toggle in the Tools panel — the topbar buttons, picker overlay, and
dropdown only mount once the user opts in. Defaults are off so existing
users see no change.
Picker behavior:
- Cursor changes to crosshair while active.
- Hover hit-tests against the live AX snapshot (same SSE the AX overlay
already drives) and renders a bbox sized to the element plus a
`<label> <role>` floating badge.
- Click freezes the bbox, captures a JPEG crop from the visible MJPEG
`<img>` (cropped to the element when AX is available, falling back
to a 192 px square otherwise), and shows an inline composer.
- Composer accepts text; Enter submits, Escape cancels, Shift+click
quick-adds without opening the composer.
- Picker stays active after submit so the user can keep picking.
Comments dropdown:
- Anchored to the chat-bubble icon in the topbar with a circular pending
counter badge.
- Per-item delete on hover, confirm-twice clear-all, and a Copy button
that writes a markdown bundle to the clipboard. Each item links its
crop via an absolute URL to /api/annotations/{id}/crop, so an agent
with HTTP access to the preview can fetch the screenshot directly.
- Click an item to flash its bbox over the simulator (1.2 s pulse).
Tools panel section:
- "Comments / Enable" toggle flips the feature on; toggling it on also
closes the panel so the topbar change is visible.
- When AXe is missing, an inline amber callout points the user at
`brew install cameroncooke/axe/axe` — the picker still works without
it, but the bbox highlight degrades to the crop-only fallback.
- Click outside the panel closes it.
Wiring in `client.tsx`:
- Lift `pickerActive` to App so the existing AX endpoint subscription
can be conditioned on `(axOverlayEnabled || pickerActive)`. Picking
with AX off would otherwise fall back to plain coordinates.
- The HID-keyboard forwarder now bails when an INPUT/TEXTAREA/
contentEditable element is focused, so typing into the composer
doesn't leak into the iOS simulator.
c2e02c7 to
c836245
Compare
`serve-sim --kill` already tears down the helper and unlinks its
`server-{udid}.json` state file. Make it tear down the matching
`$TMPDIR/serve-sim/annotations/{udid}/` directory too — JSONL, crops,
optional frames, and the directory itself — so killing a device leaves
no leftover files behind.
`serve-sim --kill` (no udid) wipes the whole annotations root, matching
the way it clears every `server-*.json` state file in one go.
The dropdown's per-item delete and clear-all (and `DELETE
/api/annotations/...`) keep working unchanged; this only adds a fresh
cleanup hook on the way out.
Contributor
Author
|
Added a follow-up commit ( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an opt-in Comment annotations mode to the preview UI. Click the picker icon, tap an element on the simulator, write a comment, repeat. When you're done, copy a markdown bundle to the clipboard with one click and paste it into Claude Code, Cursor, Codex — any agent that lives next to your editor.
The feature is gated behind a new "Comments" entry in the Tools panel, so existing users see no UI change until they opt in.
accessibilityLabel/role/typeon the saved annotation.<img>, served back over HTTP so any agent with access to the preview port can pull the exact pixels of what the user pointed at.$TMPDIR/serve-sim/annotations/{udid}/, cleared on demand from the dropdown.Demo
Comments / Enabletoggle<label> <role>Entersubmits,EsccancelsCopyThe crop saved to disk for the example above (256 px on the long side, JPEG q70):
Output for agents
Copywrites a single markdown document to the clipboard:The screenshot URLs hit the new
/api/annotations/{id}/cropendpoint. An agent that runs locally next to the dev server can fetch the JPEG directly — no need to put base64 image data in the prompt. When the agent doesn't have HTTP access, the same bytes are also on disk under the device's annotations dir.The on-disk record (one line of
annotations.jsonl) carries everything the picker captured:{ "id": "02cae5aaf6ec", "createdAt": "2026-05-05T22:55:39.026Z", "device": { "udid": "8B3BDAE3-…", "name": "iPhone 17 Pro" }, "point": { "x": 200, "y": 352 }, "region": { "x": 36, "y": 334.33, "w": 330, "h": 34.33 }, "screenshotPath": "crops/anno-02cae5aaf6ec.jpg", "context": { "accessibilityLabel": "Personalizar la página principal", "accessibilityRole": "button", "accessibilityType": "Button", "accessibilityId": "0.0.2" }, "comment": "make this CTA bigger and more contrasted", "status": "pending" }How it works
Storage layer (
src/annotations.ts,src/annotations-shared.ts)$TMPDIR/serve-sim/annotations/{udid}/annotations.jsonl.crops/anno-{id}.jpg, optional full frames atframes/.listAnnotations,createAnnotation,updateAnnotation,deleteAnnotation,deleteAllAnnotations,resolveAnnotationAsset.HTTP API (mounted by
middleware.tsanddev.ts)The Connect-style middleware and the Bun dev server share the storage layer; only the request/response plumbing differs.
Picker UI (
src/client/annotations/index.tsx)annotationsEnabledis true. Toggling on auto-closes the panel so the toolbar change is visible. Toggling off resets all transient picker state./ax). The picker subscribes to that endpoint when active, regardless of whether the AX overlay is visible, so picking with the AX overlay off still gives you element-shaped bboxes.<img>onto a canvas, crops to the element bbox in physical pixels, scales the longest side down to 256 px, and posts a JPEG data URI toPOST /api/annotations.axeon PATH (brew install cameroncooke/axe/axe) the picker still captures point + crop, just without an element-fitting bbox. The Tools panel surfaces an inline amber callout linking to the install command.INPUT/TEXTAREA/contentEditableis focused, so typing in the composer doesn't leak into iOS.Future work (not in this PR)
The picker is currently AX-driven (Tier 3 + Accessibility tree). The same overlay can be powered by richer sources without changing the wire format —
Annotation.contextwas designed withcomponentName,sourceFile,sourceLine, etc. fields ready to fill:react-devtools-coreis embedded in any RN dev build; we can act as the headless client (the same mechanismreact-native-debuggerand Flipper use) and resolve the Fiber under the cursor to component name +file:line. That's the next PR — same UX, richer context.react-native-grabintegration. When the host app already runs grab (Fabric ShadowTree, native IDs), we can prefer its pipeline for native-side metadata. Optional.This PR is intentionally agnostic — no DevTools wiring, no
react-native-grabdependency — so it ships value for any sim, RN or not, today.Test plan
bun install --frozen-lockfilebun run --filter serve-sim buildbun test ./packages/serve-sim/src/__tests__bun test ./packages/serve-sim-client/src/__tests__/serve-simagainst a booted iOS simulator → toolbar is unchanged.COMMENTS / Enablerow.Enable→ panel closes, picker (crosshair) and comments (chat bubble) appear in the toolbar.axeon PATH: hover the simulator with picker active → bbox tracks the element under the cursor; badge shows<label> <role>.axe: open Tools while Comments is enabled → amber callout points at the install command.Escwhile typing → composer cancels.Copybutton → markdown bundle on the clipboard.curl http://localhost:3200/api/annotationsreturns the persisted list.curl http://localhost:3200/api/annotations/<id>/cropreturns a JPEG.udidon disk and over the API.