Repository navigation
Pinpoint 0.5.0 — native iOS, behaviour notes, published
Install: npx -y pinpoint-bridge mcp (MCP Registry: io.github.gowtham012/pinpoint) — full setup in the README.
Found by installing Pinpoint and using it as a first-time user, then reading the code for causes.
Added
- Notes can be about behaviour, not only looks. Every channel used to call a note a "UI change
request" and the MCP server pointed agents at "UI or styling" tasks, so "this should save the draft
first" got the button restyled. Now the agent is told a note may be about how the element looks or
what it does, that the comment in the developer's own words is the whole instruction, and that for a
behaviour change the element is the way in — follow it into the handlers, state, API calls and
backend behind it. The comment box says so too ("how it looks or what it does?"). setupasks whether the app is web or native iOS. Web gets the browser extension as before;
iOS skips the browser step, checks for Xcode, Maestro and Java (saying how to get whichever is
missing, installing nothing), and opens the Simulator picker.--for web|ios|bothanswers it
without asking.- Native iOS apps, from the Simulator. A native app has no DOM, so the bridge now serves a picker
at/iosthat shows the booted Simulator's screen (xcrun simctl) over its accessibility tree
(Maestro, kept running as onemaestro mcpprocess so a refresh takes ~1s rather than the ~14s a
maestro hierarchyrun costs to boot its JVM and driver). Hover to see each element, click one, say what should change — it becomes an
ordinary annotation with the accessibility identifier, label, tree path, frame and a crop, so MCP,
hooks andpending.mdneed no changes. Works for SwiftUI, UIKit, React Native and Flutter with
nothing added to the app. No source-file hint (the tree has none) and norecheck_annotationyet. docs/stale-demo.gif, andtools/make-stale-demo.mjsthat records it: two notes on a panel
that rebuilds itself, so one pin re-finds its element among new nodes while the other says it is
not on this view — and the verdicts an agent gets for both. The recorder waits on the states it
narrates rather than on timers, so it cannot film a claim that is not true.- A pin can tell you it has gone stale. The browser already re-identified elements after a
re-render — a pin either finds its own element again or hides rather than sit on a stranger — but
that knowledge never left the page. Now the bridge is told, and the agent reads it: an annotation
whose element went missing, or whose selector stopped matching it, arrives marked possibly stale
with what to do about it.recheck_annotationdoes that on demand: it asks the open tab to
find the element again, and answers with a verdict (gone / moved / changed / unchanged), the
specific differences — text, size, the computed properties that changed by name — and a fresh crop
next to the one taken when it was marked. With no tab open it says it could not look, which is
never the same answer as "nothing changed". Resolving something the page had moved under still
works, and now says so. Asked for by a reader, and tested the way they framed it: a page that
rebuilds its DOM between the pin and the edit. node bridge/cli.js setup— the whole install in one command. Withgit clonein front of it
that is two commands and nothing else: it installs the bridge's dependencies itself (it is written
to run beforenpm install), then asks, with a default on every question — which project to
annotate, which of Claude Code, Cursor and Codex to register (merging their config rather than
replacing it, and backing up Codex's TOML), whether to install the hooks, and which of your
installed browsers to load the extension into. It opens that browser's extensions page, puts the
extension/path on your clipboard, and then confirms the extension connected — the bridge now
recordsextensionSeenAtand reports it on/health.--yesruns it unattended; a pipe or a CI
runner is treated the same way rather than hanging on a prompt.setup.shis now a shim for it.- PRIVACY.md and docs/chrome-web-store.md. What the
extension handles, where it is stored, and the one place it leaves by design — the agent you
connect; plus the packaging, permission justifications and listing copy needed to put the extension
on the Chrome Web Store, so "Load unpacked" stops being the only way in. The README also says
plainly what Pinpoint is not: a browser-driving agent. - Start and restart the bridge from the browser.
node cli.js install-native-hostregisters a
small native messaging launcher with every Chromium-based browser on the machine; after that the
popup's Start bridge button really starts the process, and while the bridge is running that
button and the new ↻ in the on-page bar restart it — so picking up a new build no longer
means going back to a terminal. The launcher can run exactly one thing, this repo's owncli.js,
on a port taken from the popup's own setting: no path, command or project directory crosses that
boundary. Restart is plain HTTP to the bridge itself, so it needs no launcher and works in Safari.
macOS and Linux only; on Windows the bridge still starts in a terminal. /healthreportscliPath, so the popup can print the real command to run instead of a
<pinpoint>placeholder nobody can copy.- Agents are told apart. The MCP handshake carries each client's own name, so with more than one
connected the bar names the agent that is working (claude-code,cursor-vscode,codex) rather
than saying "your agent", and each reply in the notes panel is attributed to whoever wrote it
(resolvedBy).
Changed
- The extension now pins its id with a manifest
key, because native messaging must whitelist
an exact id and an unpinned one is derived from the install path. If you already have Pinpoint
loaded, reload it once — the id changes, so the popup's port and corner reset to their defaults
and, if you had turned on "Allow access to file URLs", you must turn it back on. Remove the old
entry fromchrome://extensionsafterwards. (If Pinpoint is ever submitted to the Chrome Web
Store, this key must be stripped from the uploaded zip.)
Fixed
- The iOS picker recovers when another Maestro restarts the Simulator driver. Every Maestro on a
machine shares one on-device driver; when another one (an agent's Maestro MCP server, a test run)
restarted it, the bridge's Maestro kept talking to the dead one and every refresh failed with
"Device became unreachable" until the bridge was restarted. A failed read now starts a fresh Maestro
and retries once, and a cold start may take up to two minutes rather than timing out at one. - Two agents watching at once both got the same note.
wait_for_annotationwoke every blocked
waiter with the same annotation, so they duplicated the work and the secondresolve_annotation
silently replaced the first agent's reply. Each new note now goes to exactly one waiter, and
resolving something another agent already finished says so instead of overwriting it quietly. - Restarting the bridge froze the pins for 25 seconds at a time. The extension long-polls
/events?since=<n>, and a restarted bridge starts counting from 1 again — so every poll from a
client that was ahead sat out the full timeout before returning. A client from a previous life is
answered at once. - The agent's replies were unreachable, so a finished note looked deleted. The bar's counter is
the only way into the notes panel, and it hid itself whenever nothing was pending — so the moment
an agent finished the last note, the reply it had just written could never be opened. The counter
now stays while there is anything to read and says "1 done" rather than "1 note" when nothing is
outstanding. - The counter was inert while picking. The whole bar is click-through so it can never block the
element you are aiming at, and only Stop was exempt — but checking what you have already marked
is exactly a mid-picking act. It takes clicks now, like Stop. - A screenshot could show a different part of the page, with nothing saying so. The crop was
measured in viewport coordinates when Send was pressed but taken moments later by the worker, so
scrolling in between left those coordinates pointing at whatever had moved into that spot — and
screenshotSkippedstayed unset, so the agent received a confident picture of something the
developer never marked. Worse than no picture. The rect is now also sent in document coordinates
and re-derived against the page's actual scroll at capture time, so a small scroll still gets a
correct crop and scrolling away is refused with a reason. (The README already promised this
behaviour; it was not true.) - Clicking Stop also opened a comment box on the bar itself.
isOurs()asked
el.closest("pinpoint-root"), andclosest()does not cross shadow boundaries — so from inside
our own shadow root every control Pinpoint owns read as page content, and the document's
capture-phase click handler tried to annotate the Stop button. It only ever showed on Stop
because that is the one control clickable while picking. - The MCP server announced itself as
0.1.0while the package was0.4.0— the string every MCP
client shows in its server list. It readspackage.jsonnow, so the two cannot drift. - A region anchored to the whole page instead of the thing you drew around. Nobody drags
pixel-perfect: a box drawn generously around a card is contained by no card, so a rule asking for
the element that contains the box reached<main>. Asking instead for the outermost mostly-
covered element fails the same way, because a short page container is itself mostly covered and,
being outermost, swallows the card inside it. The rule now is the smallest element that
accounts for at least half the box: share is measured against the box, so a candidate must
already be about the size of what was drawn, and the smallest such element is the most specific
one. Span three cards and no single card reaches half, so their grid wins instead. file://pages were promised but dead. Chrome keeps Allow access to file URLs off per
extension, so the content script never injected — whileisLocalDev()returned true forfile:,
so the popup offered to annotate and then reported a message assertingfile://pages work.install-hooksbroke on any path containing a space, because the quotes that made the path
safe were stripped. The install reported success and every prompt failed afterwards.- Hook merging deleted unrelated hooks — ownership was a substring match on
"pinpoint", so a
user's own hook mentioning it, or any repo in apinpoint/folder, was dropped. - A flag before a subcommand started a daemon:
cli.js --port 7332 statusanswered by running
a server instead. install-hookserrors reached the user as raw stack traces instead of the sentences it writes.- The origin guard admitted
http://localhost— the dev site being annotated.SECURITY.mdand
the README both said otherwise; the code now matches. setup.shdefaulted the project to a personal path (~/Desktop/markus) in a public repo.content.jscalledcaptureScreenshot(), which is defined nowhere. It never fired because
nothing passes{screenshot: true}, but it would have thrown for whoever did. Removed.- Windows: the installed hook carried a mangled path. The path was JSON-escaped rather than
quoted, soC:\Users\dev\...reached the shell asC:\\Users\\dev\\.... Windows often
tolerates doubled separators, but when it does not the only symptom is "Cannot find module" on
every prompt. - The browser suite could not run on a fresh clone. Playwright ≥1.49 launches
chrome-headless-shellforheadless: true, which cannot load extensions. npm testhad never worked — it hands both files to onenode --test, which runs them in
parallel, and both bind port 7399. It is sequential now, matching CI.- The bar could land in the wrong place on pages whose
<html>carries a transform.
keepDockOnScreen()setstransform: noneand measures on the next line, but the bar also
transitionedtransformover 140ms — so the rect was read mid-animation and the correction was
computed from the wrong position. It self-corrected on the next pass, which is why it only
showed up intermittently. The transform is only ever a position fix, never an effect, so the
transition is gone.
Changed
- New logo — a click: an open ring, the burst of the press, and the cursor coming through the
gap. Drawn as SVG rather than traced from a raster, so it carries transparency and stays crisp.
Two cuts on purpose: at 16px the burst strokes and the ring merge into an unreadable blob, so the
toolbar size drops the burst, thickens the ring and enlarges the cursor. Sources and a
regeneration script live intools/logo/andtools/make-icons.sh. It is violet rather than
black because a black icon disappears on a dark browser toolbar. - Note numbers are per page. One global counter meant the first note on a new site could read
"#14", which tells the person looking at it nothing. Each page now numbers from 1, matching the
pins on screen, the panel titled "Notes on this page", and the counter. Ids stay globally unique
and remain what an agent resolves by; a number still works as a convenience while it names one
note, and resolves nothing when it does not —DELETE /annotations/1would previously have
deleted #1 from every page in the store. - The bar now opens in the top-right corner rather than bottom-left, and the popup's corner
picker opens on the same default. - Two hues in the bar instead of three. "You" was
#a8577f, a mauve unrelated to anything
else, so the bar carried violet (h248), mauve (h330) and orange (h18) — three corners of the
wheel, which is why it read as muddy. "You" is now the same violet as your pins and the brand
mark; orange stays the agent. - The idle agent avatar was an unreadable blob. Greyscaling the orange fill at 38% produced
about#cacaca, leaving its white glyph at 1.64:1. It is now drawn as an outline — absent
rather than muddy — at 5.26:1. - One optical row. The bar's children padded 5/10, 5/8, 5/10 and 0/4/0/6 — four rhythms in a
200px bar. Every control is now 24px tall on a single horizontal rhythm, the separators are
centred, and the⌥⇧Akeycap has the size and tracking it needs to be legible at all. - Your avatar now stacks in front of the agent's, which is what stacked presence means.
- The on-page bar is no longer dimmed. Fading it (
.58at rest,.4minified) put the label
at 4.4:1, the shortcut keycap at 2.34:1 and the minified bar at 2.55:1 against a white page —
below the 4.5:1 AA floor. Discretion now comes from size and position. - Pins, note numbers and the picked-element chip are no longer error red. White on
#e5484d
measured 3.91:1 at 11px bold, and a pending note is not a fault. Red now means only error or
delete. - Stop is clickable while picking. The bar stays inert so it can never block the element you
are aiming at, but clicking Stop no longer falls through and annotates the page behind it.
(Its first cut was white-on-white — 1.00:1, invisible — because the mockup had a violet bar while
only the toggle pill actually turns violet. The test now measures the control's contrast, not
just that it responds.) - The popup honours
prefers-color-scheme; it was a white flash in a dark browser.
Added
- Safari support.
tools/make-safari.shconverts the extension with Apple's
safari-web-extension-converterand builds it — verified end to end here,BUILD SUCCEEDED.
The script bakes in the argument that otherwise wastes an afternoon: the converter derives the
app id from the last component of--bundle-identifier, so if that component does not match the
app name the extension's id is not a prefix of the app's and Xcode refuses to embed it.
Safari does not support"world": "MAIN"content scripts, so the React/Vue component chain and
source-file hint are unavailable there; everything else behaves the same. - Cross-browser namespace shim: Safari and Firefox expose the promise API as
browser, Chrome as
chrome. Each script now prefers whichever exists, so the ~30await chrome.…calls work on all
three.chrome.extension.isAllowedFileSchemeAccessis Chrome-only and is probed before use. - You can see what your agent did. A finished note used to disappear, leaving you with a
vanished pin and no idea what changed. It now stays in the notes panel with the agent's reply
underneath it. That reply is thenoteonresolve_annotation, which was optional and so was
simply skipped — every resolved annotation came back withresolution: null. It is required now,
and the tool says plainly that the developer reads it in their browser and that "done" is not an
answer.cli.js resolvegained--noteto match; it accepted one internally but never passed it. demo/index.html— a self-contained demo site for trying Pinpoint and filming it: card grid,
pricing tiers, near-identical siblings, a tab panel that rebuilds itself, a dense table, a form.- Region selection. Drag instead of clicking to mark an area rather than a single element.
A box has no element of its own, so it is anchored to the deepest element that fully contains
it — which is what lets a region pin re-find itself after a re-render exactly as an element pin
does, and gives the agent a container to edit plus the list of what the box held. The screenshot
crops to the box. - README screenshots — the bar idle and picking, the popover, a pin, and a hero shot. Generated
from the shipped extension by a script that runs its own bridge on a scratch port. prefers-reduced-motionsupport — seven animations ran unconditionally, two of them forever.- README: prerequisites, corrected step order (the bridge blocks the terminal — you need a second
one), thefile://permission step,setup.sh, and a Troubleshooting section. - Three tests, taking the suite from 79 to 82: a flag before a subcommand, hooks surviving a path
with a space (and leaving unrelated hooks alone), and Stop ending a pick with a real mouse click
while the rest of the bar stays inert.