npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

getobsrv

v0.62.1

Published

See your site the way 1x screens see it

Readme

Obsrv

CI npm license: MIT

See your site the way 1x screens see it.

Designers and developers work on HiDPI (2x–3x) monitors. A large share of users are on 1x screens — 1080p desktops, 1366×768 laptops, cheap TN panels. On those screens, thin font weights go weak, 0.5px hairlines vanish, low-contrast grey text becomes illegible, gradients band and P3 colours clip. Browser dev tools emulate the viewport of a 1x screen but still rasterise on your monitor's physical pixels, so you never see any of it.

Obsrv shows any URL (or a dropped 2x design export) two ways, side by side:

  • Native — rendered at your host DPR, the way you normally see it.
  • Target — rendered at a true 1x by an offscreen Chromium surface (deviceScaleFactor: 1), displayed at the physical size it would have on a chosen target monitor, with optional cheap-panel simulation (contrast floor, sRGB coverage, 6-bit + FRC dithering, brightness) applied in a WebGL2 shader.

Both panes stay in lock-step (scroll, navigation) and the 1x pane is fully interactive.

  • Mobile presets — phone and tablet presets (iPhone 6.1" @3x, budget Android @2x, …) rasterise at the device's true 2x/3x DPR, wear a mobile user agent, get real mobile viewport semantics (a page without a viewport meta lays out at 980px and shrinks to fit), and are shown at true physical size — usually a small, dense render on a desktop monitor, exactly like the phone in your hand.

macOS only, and the app is not yet signed — macOS will call the DMG "damaged" on first launch until you clear its quarantine flag (below). What Obsrv cannot do is the full boundary: what the measurement does not see, where the numbers stop being exact, and how it behaves on a page that keeps moving. Worth two minutes before you install.

Quickstart

The desktop app — download the DMG for your chip from Releases, drag Obsrv.app to Applications, then clear the quarantine flag once (the build is not yet notarised, so macOS falsely reports it as "damaged"):

xattr -cr /Applications/Obsrv.app

Open it and set your monitor's diagonal in Settings — that one number is what makes the target pane render at true physical size.

The CLI, and Claude Code / MCP clients:

npm i -g getobsrv                                          # or use npx -y getobsrv
obsrv install-skill                                        # teach Claude Code when to use it
claude mcp add --scope user obsrv -- npx -y getobsrv mcp   # give it the tools

install-skill copies the obsrv-screens skill into ~/.claude/skills/ (--dest for elsewhere, --print to pipe it into another agent framework). The skill is what makes an agent reach for Obsrv on its own when frontend work needs checking; the MCP registration is what gives it the tools to do so. New sessions pick both up.

As a Claude Code plugin — the repo is one: it carries the skill, and its .mcp.json registers the MCP server.

claude plugin marketplace add vibesyemmy/obsrv
claude plugin install obsrv@obsrv

That gives an agent the obsrv-screens skill and the seven-plus tools in one step; obsrv install-skill remains for the skill alone. The marketplace entry points at the release tag over https, so what installs is a release, never main between releases, and cloning it needs no git credentials. marketplace add does not refresh a marketplace you already have, so to move to a newer release:

claude plugin marketplace update obsrv
claude plugin update obsrv@obsrv

Both together — install the app and the tools, then flip Agent control on in the app's toolbar: agent testing now drives the window you are watching instead of rendering invisibly.

Use

npm install
npm run dev

Type a URL (localhost is fine), pick a target screen preset (1080p 24", 1366×768 laptop, iPhone 6.1" @3x, …), pick a panel profile (Reference, Office IPS, Budget TN, Old laptop) or open the side panel (the sliders icon) for the rest: text scale, throttle, the onion skin — the page's HiDPI render ghosted over the 1x raster, what the cheap raster moved, in one place (docs/onion-skin.md) — the panel sliders and colour vision. A condition in force is named in the target pane's footer, and clicking it opens the panel at its control. Enter your own monitor's diagonal in Settings once so the target pane renders at true physical size. Drop a 2x/3x PNG or JPG export to check a design before it's built.

The target pane opens in Fit, a fit-to-pane view of the whole render. Fit never enlarges past 1:1, so a render that already fits is shown at true size; a larger one is smoothly minified to fit, which is not pixel-exact — the footer says so, and names the magnification. Fit is fully interactive: clicks, drags, the wheel and the keyboard all reach the page.

Switch the toolbar's 1:1 / Fit control to 1:1 for true magnification, which usually overflows the pane: pan with a middle-button drag, Option+drag or Option+wheel. From fit, Option+click jumps straight to 1:1 with the clicked spot centred (plain clicks belong to the page).

The Both / Target control beside it hides the native pane so the target render takes the whole window — useful for a small mobile preset that would otherwise sit in half a window, and for agent captures. The native pane stays loaded while hidden, so the URL bar, back/forward and link clicks keep working exactly as they do side by side.

Tabs

The strip above the URL bar holds several independent sessions at once. A tab is its own URL, its own screen preset and panel profile, its own view mode and pan, and its own page state — so the same page on a 1366×768 laptop and on a budget Android sit in two tabs and you flip between them without either one reloading.

Cmd+T opens a tab, Cmd+W closes one, Cmd+1–Cmd+8 select by position and Cmd+9 selects the last. Closing the last tab leaves a fresh blank one rather than closing the window. Tabs come back on relaunch — their URLs, their screens and which one was in front.

Every tab is two Chromium renderers (a native pane and an offscreen 1x surface), so the count is capped: 12 by default, settable from 2 to 32 in Settings. At the cap the new-tab button dims and says why. Background tabs stay loaded and keep their scroll position, but stop rasterising until you return to them — the cost of a background tab is memory, not GPU.

Agent & CI use

The same rendering pipeline runs headless — no window, JSON on stdout, humans on stderr — so agents (Claude Code) and CI can ask "how does this URL look on a 1366×768 laptop / budget Android?" without the GUI. Build first (npm run build; the CLI runs the built out/), then:

# One PNG at a preset's true raster density (+ metadata JSON on stdout):
npx -y getobsrv snap http://localhost:5173 --preset laptop-768 --out shot.png

# A matrix of screens, cheap-panel simulation, full-page capture:
npx -y getobsrv snap http://localhost:5173 --matrix laptop-768,android-65,1080p-24 --out shots/
npx -y getobsrv snap http://localhost:5173 --preset laptop-768 --profile budget-tn --out tn.png
npx -y getobsrv snap http://localhost:5173 --preset laptop-768 --full-page --out full.png

# The whole page: captured a screenful at a time at the screen's own viewport and stitched, so a
# 100vh section keeps the height the screen gives it and an inner scroller is followed.
npx -y getobsrv snap http://localhost:5173 --preset laptop-768 --full-page --out full.png

# Machine-readable 1x-vs-2x comparison (ink coverage, row ratios, band deltas):
npx -y getobsrv diff http://localhost:5173 --preset laptop-768 --out-dir diffout

# Tap targets and text measured in millimetres on the chosen screen — a 24 CSS px
# control is 6.6 mm on a 24" 1080p and 4.5 mm on a 6.5" phone, and only one of
# those is a thumb's worth. Findings under 7 mm / 2 mm (provisional, tunable):
npx -y getobsrv audit http://localhost:5173 --preset android-65

# The elements a 1x screen and a cheap panel break, named: edges under a device
# pixel, light text too small for its weight, contrast that fails as stated or
# only once the panel lifts the blacks, images upscaled or oversized. Each finding
# carries a page rect and one sentence with the figures (docs/lint.md).
npx -y getobsrv lint http://localhost:5173 --preset 1080p-24 --profile budget-tn

# All of the above on one self-contained HTML page — two laptops/desktops and two
# phones by default: each render, the audit in millimetres, the lint grouped by
# cause, the 1x-vs-2x diff, and the worst findings pinned and cropped on the page.
# The thing to attach to a PR.
npx -y getobsrv report http://localhost:5173 --out obsrv-report.html

# The page as a user at 150% sees it: browser zoom as reflow, on any command.
# The PNG stays the screen's size; the page lays out in two thirds of it at
# 1.5x the density, and the audit's millimetres grow to match.
npx -y getobsrv snap http://localhost:5173 --preset laptop-768 --text-scale 1.5 --out zoomed.png

# The screen turned a quarter turn (0.61.0+): a phone held landscape, or a monitor stood on end.
# --orientation is deprecated and keeps its meaning, relative to how the preset is stored: portrait is
# the preset as stored, landscape is it turned, so landscape on a desktop preset (stored landscape) is a
# portrait screen. A --rotate and --orientation that disagree are refused.
npx -y getobsrv snap http://localhost:5173 --preset iphone-61 --rotate --out landscape.png

# How the page feels there: Chrome DevTools' network and CPU presets, and the time
# to paint-quiet in the JSON (settledMs). Compare against --throttle none.
npx -y getobsrv snap http://localhost:5173 --preset android-65 --throttle budget-phone --out slow.png

# One element, measured: font in millimetres on that screen, colours, and contrast
# as stated and on the panel, against the WCAG threshold for text that size.
npx -y getobsrv inspect http://localhost:5173 --preset android-65 --profile budget-tn --selector '#cta'

npx -y getobsrv --help (or node bin/obsrv.js --help in a checkout) lists every preset, profile and flag; npx -y getobsrv --version prints the installed version, and needs neither a build nor Electron. Diff findings are informational (exit 0); CI thresholds are the caller's job. A ready-made Claude Code skill that wraps the loop (snap matrix → read the PNGs → diff → fix → re-snap) lives at skills/obsrv-screens/SKILL.md; obsrv install-skill copies it into ~/.claude/skills/ so agents find it.

MCP server

The same CLI is also wrapped as an MCP server (stdio, stateless) so MCP clients get the tools natively: obsrv_snap (render a URL at a preset's true raster density — the PNG comes back as an inline image up to 1.5 MiB; inlined says whether it did, and past the cap a warning names the path), obsrv_diff (the 1x-vs-2x metrics as structured output), obsrv_audit and obsrv_inspect (millimetres and contrast on a chosen screen), obsrv_lint (the elements a 1x screen and a cheap panel break, named), obsrv_report (a whole matrix of screens as one HTML page) and obsrv_presets (every preset and panel profile, no render).

The MCP tools launch the app when it is not running; if it is open with agent control off, the app asks — Allow for this session, or Not now. Once it is live, obsrv_snap drives the visible window instead: you watch the URL load and the preset flip, and the agent gets back a capture of the app exactly as you see it (plus obsrv_drive to flip URL/preset/profile/panes directly — panes: 'target' gives the target render the whole window, which is usually what you want before a capture). Agents can also scroll, click, pan and highlight while you watch — a drive session works as a guided demo. A scroll reports the offset it actually reached (scrolled / scroller), finds the inner scroll container on pages whose root cannot scroll, and takes a scrollSelector when you need to name the container yourself. obsrv_inspect, obsrv_audit and obsrv_lint follow the app the same way: they measure the page in front, on the screen, text scale and panel in force, after whatever the drive did to it; obsrv_audit and obsrv_lint walk the page to the end and back first, so you see it looked at — and so do the headless audit, lint and report, so lazy images are judged by the file that arrived rather than its placeholder (--no-walk or walk: false measures the page as it first shows); obsrv_report is the exception to the live part and stays headless, being a batch over a matrix of screens. With no app running, mode: 'auto' (the default) launches one; mode: 'headless' never touches it.

To photograph a scrolled or panned state, pass capture: 'window' | 'pane' to obsrv_drive: it captures after its commands run, and nothing in that tool navigates unless you pass url, so the scroll survives the shutter. A live obsrv_snap only navigates when the app is showing a different URL — its navigated field says which happened — and navigating is a fresh load, which starts at the top of the page.

Tabs and the agent. obsrv_drive and a live obsrv_snap act on the tab that is in front, resolved per command rather than fixed when the drive starts — a command that quietly succeeded on a tab you could not see would not surface until the drive ended. Both report tabId and tabIndex, so an agent that cares can compare them across calls and notice you switched tabs under it. While Agent control is on, the driven tab carries a neutral rule on its leading edge that brightens for ~3 s on each command, so it is visible which session is being driven. An agent opens, fronts, switches and closes tabs itself, through obsrv_drive's tab and closeTab.

A headless snap returns settled: true when the page went paint-quiet and every pixel painted. settled: false is still a usable capture, not a failure — a page that kept animating, or one whose repaint never completed, comes back as-is (exit code 0) with a warning saying what was missing and an unsettledReason: animating (captured after ~2 s of steady painting; waiting longer would not help), timeout (still painting at the budget), uncovered (part of the frame never painted) or blank (the frame went quiet one colour end to end and stayed that way for 3 s: the page's background with nothing on it yet, or a page that really is empty — pass --wait for a page that paints late). Only a render that painted nothing at all is an error.

Build first, then register:

claude mcp add --scope user obsrv -- npx -y getobsrv mcp

In a checkout of this repository, test the local build as obsrv-dev, through the dev lane (see Develop), and leave obsrv to the plugin or the published package.

npx getobsrv run inside the repository is the checkout, not the release: npm links a project into the npx cache when the requested name is the project's own, so an obsrv registered as npx -y getobsrv mcp there ran whatever out/ held. The manifest (.mcp.json, the plugin's and the project's) therefore pins npx -y --prefix / getobsrv@<version> mcp: with its project at the filesystem root npm has none, and fetches the pinned release wherever the session runs. A result from obsrv-dev is the local build; one from obsrv is the release.

Develop

npm run typecheck     # tsc, both processes
npm test              # unit (Vitest, node)
npm run test:browser  # shader parity vs the TS reference (Vitest browser mode)
npm run test:e2e      # Playwright driving the real Electron app
npm run dist          # build a macOS DMG (unsigned without a Developer ID identity)

The dev lane

Testing a change through the MCP tools used to mean a release: publish, install the DMG, update the plugin, restart the session. The dev lane runs a checkout's own build instead, headless and live, and follows every rebuild.

npm run lane                 # point the lane at this checkout, build it, (re)launch its app
npm run lane -- --no-build   # point and relaunch without building
npm run lane -- --no-app     # point and build; the next live call relaunches the app
npm run lane -- --status     # where the lane points, and how fresh its builds and app are
  • One lane per machine, at ~/.obsrv-dev (OBSRV_DEV_HOME overrides): a pointer to the checkout it runs, the dev app's own profile, and the proxy. npm run lane in any checkout or worktree points it there.
  • The dev app runs beside an installed Obsrv, never instead of it: its own profile, so its own single-instance lock and control.json, and a title bar that says Obsrv — dev lane (<branch> @ <commit>).
  • obsrv-dev is a proxy (scripts/dev-mcp.js, installed into ~/.obsrv-dev/bin). It runs the lane's own MCP server in dev mode and starts it again when out/mcp/server.js changes or the lane moves, so the call after a build runs the build on the same session. A call in flight finishes on the build it started on.
  • Every result says which build answered. The lane is one pointer shared by every session, so npm run lane in one session moves every other session's obsrv-dev. Each tool result's notes (or warnings, for a tool without notes) end with a line naming the branch, commit, uncommitted changes, build time and checkout; obsrv_presets, which has neither, reads the preset table alone. The proxy also appends that line as a text block, and after a move says the lane moved, for clients that show text blocks.
  • Every call names the checkout it means to test. The stamp still leaves the comparison to whoever reads it, and the proxy cannot make it: it starts in the project the session opened, whichever worktree the session works in after that. So every obsrv-dev tool takes a required tree, the top of the caller's working tree. A call naming a checkout other than the lane's is not run, and says which checkout the lane serves and how to point it there. tree: "any" runs on whatever the lane serves, and that answer's stamp says it was not compared with your checkout.
  • In dev mode the server drives the lane's app: it discovers the lane profile's control.json, launches the checkout's out/main/index.js rather than /Applications/Obsrv.app, and relaunches a dev app that started before the latest build, saying so in the result.

Register it once, then restart the session once; after that nothing about a rebuild needs a restart:

claude mcp add --scope local --transport stdio obsrv-dev -- node ~/.obsrv-dev/bin/dev-mcp.js

A change to the proxy itself is the exception: it reaches a session once npm run lane has copied it and that session's MCP connection has restarted.

The loop is then: work on a branch in a worktree, npm run lane there, test through the obsrv-dev tools, adjust, npm run build, test again. obsrv stays the pinned release throughout. A release is still proven by the packed tarball's own CLI, since the lane by definition runs local code. Before removing a worktree the lane points at, point it elsewhere (npm run lane in the shared checkout); a lane that points at nothing answers every call with that sentence rather than failing to start.

Architecture, decisions and the full spec live in docs/superpowers/specs/2026-08-22-obsrv-design.md; the UI style rationale (why the chrome is strictly neutral) is in docs/superpowers/specs/2026-08-23-obsrv-ui-style.md.

Install (desktop app)

Grab the DMG for your chip from Releases, drag Obsrv.app to Applications, then clear the quarantine flag once (the build is not yet notarised, so macOS falsely reports it as "damaged"):

xattr -cr /Applications/Obsrv.app

Obsrv checks GitHub for a newer release once a day and, when there is one, shows it in the toolbar; clicking opens the release page. It is a single unauthenticated request carrying no identifiers, and Settings → Updates turns it off.

Distribution

Publish via a packed tarball, never bare npm publish: npm publish snapshots package.json before lifecycle hooks run, which silently skips the prepack electron dev→prod dependency swap (this shipped a broken 0.4.0). The flow is:

npm run release:pack
npm publish ./getobsrv-<version>.tgz

npm version also rewrites the plugin manifests: their version, the marketplace entry's source, which is pinned to the tag plugin-v<version> that npm run plugin:branch creates, and the MCP registration, pinned to getobsrv@<version>. Push the tags after main is green, and push plugin-v<version> only once npm has that version: the marketplace catalog points at the plugin tag, and a plugin fetched before the publish starts an MCP that npm cannot install. So the order is: main green, npm publish, then v<version>, plugin and plugin-v<version> together.

Obsrv publishes to npm as getobsrv (the installed commands remain obsrv and obsrv-mcp; the app's display name remains Obsrv). The bare obsrv npm name belongs to an unrelated package.

Privacy and files

Nothing is uploaded. The app makes exactly one outbound request of its own: a version check to api.github.com once a day, which asks for the latest release and sends nothing about you (src/main/updateCheck.ts). The npm package makes one more, once: when its Electron binary is missing, it downloads it — about 120 MB, from Electron's GitHub releases unless ELECTRON_MIRROR points elsewhere — as the MCP server starts, or on a CLI run's first use (bin/electronPath.js). Everything else that touches the network is the page you asked it to render, fetched by Chromium the way any browser would.

Everything it produces is a local file. PNGs and report HTML go where you point --out / --out-dir. The MCP tools don't take an output path: every capture, diff and report they make is written into a fresh obsrv-mcp-* directory under os.tmpdir() and handed back as a path — and the MCP server prunes those directories, older than a day, at startup (src/shared/pruneTemp.ts). Copy anything you want to keep, a report included. Headless CLI runs use a throwaway Electron profile under os.tmpdir() and remove it on exit.

The app keeps its own state in Electron's application-support directory for Obsrv: settings.json, history.json (the addresses you have visited in the app), tabs.json (the session it restores), and — only while agent control is on — control.json, mode 0600, holding the loopback port and token.

And it keeps Chromium's caches for every page it has rendered, in the same directory. Obsrv renders arbitrary third-party pages by design, so this grows with use in a way an ordinary app's does not — and faster for the people who use it most. A working profile was measured at 1.3 GB, 915 MB of it Cache, with nothing pruning it. The disk cache is now capped at 256 MiB; Code Cache is Chromium's own and is not, so the directory can still grow, just not without limit at the part that dominated it. Headless CLI runs are not part of this at all: each takes a throwaway profile and deletes it.

A warm cache is worth about 14 ms of load time on a small page and changes nothing Obsrv measures — five cold/warm pairs moved zero result fields — so the cap costs latency and not correctness.

Removing Obsrv does not remove any of that. Deleting Obsrv.app — dragging it to the Trash — removes the app and nothing else; npm rm -g getobsrv removes the CLI and nothing else. Measured on a fresh install: one page visited, then the app deleted, and every file below was still there.

obsrv uninstall will tell you what is actually on your machine — which of these paths exist, how big each one is, what is in it, and what is deliberately left alone (Electron's shared cache is not Obsrv's to delete). It lists only: it removes nothing, and prints the commands below with your own paths filled in. On macOS, to remove it by hand:

rm -rf ~/Library/Application\ Support/Obsrv   # settings, history, tabs, the Chromium profile
rm -rf ~/Library/Logs/Obsrv                    # obsrv.log
rm -rf ~/Library/Caches/electron               # the downloaded Electron runtimes

The first line is the one that matters for privacy: history.json lives there and holds the addresses you visited in the app. The third is the one that matters for disk — it is Electron itself, one copy per version Obsrv has used, kept outside node_modules and left behind by npm rm; it reached 477 MB on the machine this was measured on. Quit Obsrv before removing these, and check no other Electron app of yours relies on that cache.

If you used the MCP tools from the npm package before 0.61.0, with no Obsrv.app installed, one more place holds Obsrv's data. The app those tools launched named itself "Electron", not "Obsrv". Its files went into ~/Library/Application Support/Electron, and its log into ~/Library/Logs/Electron. Every other unnamed Electron app writes there too, and most of what is there can't be told apart by name:

  • Obsrv's own files are history.json (the addresses you opened in Obsrv), tabs.json (the tabs it restored), settings.json (with hostDiagonalInches in it), control.json (a port and token, or "enabled": false, with a pid), and the log's obsrv.log. Check each one is Obsrv's, then remove it:

    cd ~/Library/Application\ Support/Electron && rm -i history.json settings.json tabs.json control.json
    rm -i ~/Library/Logs/Electron/obsrv.log
  • The Chromium profile is there too: Cache, Code Cache, cookies, Local Storage and IndexedDB for every page that app rendered. It's usually most of the disk use, and it carries the sites' cookies. It uses the same names every Electron app uses. If nothing else on this Mac ever ran as "Electron" (you don't build or run unpackaged Electron apps), the whole directory is Obsrv's and rm -rf ~/Library/Application\ Support/Electron ~/Library/Logs/Electron removes all of it. If you aren't sure, leave the directory. Removing another app's profile would take its data with it.

The log records what breaks, not where you went. obsrv.log, in Electron's logs directory, is a few lines an hour about GPU processes dying, crashed targets and lost WebGL contexts, so a bug report arrives with evidence (src/shared/logFile.ts). It is not a record of the URLs you visit: an address appears in only two warnings, both written when something breaks — the page a crashed target was showing (src/main/targetSource.ts), and the address at which a navigation loop between the two panes was stopped (src/main/syncBus.ts).

The version a bug report needs is obsrv --version for the CLI (no build or Electron required), Settings → Updates in the app, and version in any live status reply.

See what an agent can do to your machine for the agent-control surface in full, what Obsrv cannot do for the measurement's limits, the thresholds for where every judged number comes from, and breaking changes for anything that has moved or been renamed.

Want to help? The board is the open work, each card carrying the criterion it closes and the file or commit that defines done. The cards themselves live in board/ — one markdown file each, and the only source of truth. Cards in Next are the ones worth starting.

The board itself is board/ — one markdown file per card. Claim one by editing its file: set owner: and column: doing, run npm run board, and open a pull request with both changes. There is no separate tracker to update and nobody to ask for access.

CONTRIBUTING.md is worth ten minutes first. It is not a style guide — it is the list of traps that have actually cost this project time, including the two that produce false test failures and the one rule that matters most here: a check nobody has watched refuse is a claim, not a check. npm run board regenerates two views from those cards, and CI fails if either has drifted from them:

| | | |---|---| | vibesyemmy.github.io/obsrv | the Kanban view — columns, owners, click a card for its full evidence. Rebuilt from board/ on every push to main, so it is never a stale copy of anything | | board/ | the cards themselves, one markdown file each — the source the views are built from, and the only thing committed |

Known v1 limits

  • Rendering truth is the host OS's 1x rasteriser (macOS today). Windows ClearType at 1x looks different again; a Windows build would show Windows truth natively.
  • Panel simulation is an approximation, not colourimetric.
  • Non-ASCII text input does not type into the target pane (Electron sendInputEvent limitation).
  • Inner-scroller reporting is one-way. An agent scroll finds the page's real scroll host — the app-shell pattern (html, body { overflow: hidden } with an inner overflow-y: auto container) is handled, and the result reports the offset actually reached — but scrolling a nested container by hand in the native pane is not mirrored to the target: element scroll events don't bubble to window, so the report side never sees them. Dragging the page itself still syncs both ways.
  • A stuck side rail still repeats in a full-page capture. --full-page captures the page a screenful at a time at the screen's own viewport and stitches the bands, so anything stuck to the viewport is painted into every one. Full-bleed chrome — a header, a cookie bar — is now hidden for the bands after the first, so it appears once and the page rows behind it are not lost (measured: tailwindcss.com/docs at laptop-768 stuck its 57 px header with position: fixed, developer.mozilla.org sticks its 98 px header and both side rails with position: sticky, so detection is by measurement — an element whose viewport rect is unchanged between two scroll offsets — rather than by reading position). An app shell gets the same treatment against its own scroller: the chrome around the scroller is sliced out of those bands already, so what repeats there is a sticky toolbar or table header inside it, and a bar spanning a scroller inset from the window is not full-bleed against the viewport at all — the frame for the whole measurement is the element the capture scrolls. A rail is left alone in both, page or panel: it covers no page content, so hiding it would only leave a blank column down every band but the first. --keep-stuck-chrome leaves all of it, as the capture used to; --single-surface avoids bands entirely, at the price of a page sized against the viewport laying out differently and an inner scroller not being followed at all.
  • Scroll targeting stops at the top-level document. The automatic detection finds a scroller inside an open shadow root, but not one inside an iframe or a closed root, and scrollSelector can't name a scroller inside any root or iframe (document.querySelector doesn't cross either boundary).
  • Frame delivery has no renderer-side backpressure mailbox (see plan header); at 30 fps with dirty rects it has not been needed.
  • Tabs are a first cut. They cannot be reordered, dragged out into another window, or reopened after closing (no Cmd+Shift+T), and there is no tab overflow menu — a strip longer than the window scrolls. An agent opens, fronts and closes tabs through obsrv_drive (tab, closeTab) and reads them from tabs. The URL bar's visited-URL suggestions are one window-wide list rather than one per tab (back/forward are per tab), and a restored tab comes back at the top of its page — the scroll position is not persisted.