Skip to content

feat(snapshot): publish the viewport a snapshot's rects are measured in - #3210

Merged
thymikee merged 21 commits into
mainfrom
feat/snapshot-response-viewport
Oct 5, 2026
Merged

thymikee merged 21 commits into
mainfrom
feat/snapshot-response-viewport

Conversation

@thymikee

@thymikee thymikee commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Summary

A snapshot response now carries a response-level viewport: { width, height } — the box the node rects are measured in, in the same coordinate space and orientation as the nodes beside it — so consumers scale and clip against the screen they were shown instead of inferring one from the largest rect (Closes #3182).

Producers answer per their own surface: the Android helper reads the real display extent behind the tree (getMaximumWindowMetrics on API 30+, getRealMetrics below) around each dump and publishes it only when both reads agree; the Apple runner publishes the box the fold validated, which the engine now returns rather than each caller re-deriving; absent means the producer measured nothing, never a zero — snapshotViewportSizeFrom is the sole construction path and the type is branded. The daemon state, digest view, serialization, client, and remote-proxy parity set all carry it.

// snapshot --json
{ "viewport": { "width": 402, "height": 874 }, "nodes": [ ... ] }

45 files: kernel/contracts types, Android helper (Java DisplayExtent) + capture plumbing, Apple presentation, daemon/client/digest seams, tests per seam, docs.

Validation

Tested at 5a05a3ea4: pnpm check:affected --run green (1036 files / 8404 tests, plus helper JVM suite, format, lint, typecheck, layering, fallow, build). All CI lanes green on that head: CI, Android, iOS, macOS, Linux, Size.

Live evidence through the daemon CLI on an API 35 emulator with 3-button navigation (#issuecomment-5988412865): portrait viewport equals wm size and the PNG, with Back/Home/Recents inside (the old app-display read ended above the nav bar); landscape swap with sessionReused/transport=persistent-session; Android --raw publishes the viewport while Apple --raw omits it; empty-screen captures still report it on both.

The Android smoke failure was this PR's, not a flake: the state-seam spread dropped WeakMap-keyed clickability evidence, so Maestro's clickable-first ordering fell back and tapped an inert duplicate. Fixed at the seam with a regression test that fails without it.

Review in cubic

…in (#3182)

Carry a SnapshotViewportSize from each producer through the state, response,
serialization, digest view, and client reader, published once at response level
and only through the kernel guard that makes a zero-size viewport unrepresentable.

Android: the snapshot helper reports the DisplayMetrics extent beside the density
it already declares; the host converts the pair to a viewport at the helper-result
boundary and keeps only the response fact. Apple: the host publishes the viewport
evidence the presentation fold already resolves, with no new runner wire key.
Kernel: the construction guard and the wire re-read refuse zero, non-finite, and
the infinite sentinel as absence. Android: both helper transports read the display
pair, snapshotAndroid publishes the display beside an empty tree, the publication
adapter carries it, and the backend metadata keeps no second copy.
)

The capture publishes the fold's app-window evidence, follows the quality payload
the fold prefers, refuses zero/infinite boxes as absence, and the desktop runner
publishes none because window-space rects answer to no single box.
…t, client (#3182)

Runtime command publishes what its producer measured and omits what it didn't; the
session state carries the box so find-path consumers read the same one; the digest
survives the tree collapse; serialization emits it once; the client re-reads it
through the kernel guard so a broken payload stays absent.
#3182)

The proxied iOS capture now publishes the app-window box the presentation fold
resolves; the parity table names it rather than the guard loosening to a size.
…ture test (#3182)

parseAndroidSnapshotHelperOutput lives in snapshot-helper-capture.ts, so its viewport claim
follows the source topology instead of the legacy aggregation; the capture fixture gains result
lines rather than keeping a second output builder.
`--json` consumers now read the box the rects are measured in off the
response instead of inferring one from the largest rect on screen.
commands.md names each producer's answer, the absence rule, and the
separation from content-safe gesture bounds.
…y holds (#3182)

presentIosSnapshotAcquisition is the one seam that turns an acquisition into a snapshot result for
the simulator AX bridge, an Appium source, and a Limrun tree, and it had the viewport evidence in
hand while returning no viewport. That left the default local-iOS producer without the field the
issue's first Done-when line asks for. Like the runner fold, it publishes only the box the regular
projection measured against: raw validates no box at all.
The published shape carries no origin, so a wire payload with the CGRectInfinite extents and no
coordinates used to mint (0, 0) around them and pass the guard, handing the caller the largest
number on the wire as the screen every rect is measured in (#2891). Both viewport gates now read
the extents themselves. kernel/record keeps its inlined rect rule and points at the other half of
that seam.
…d it (#3182)

The runner fold published the largest root even on a raw projection, where the engine validates no
box at all — reinstating the largest-rect guess #3182 exists to retire under the name of a measured
fact, on a payload the regular projection refuses outright. Publication now follows the validated
projection, and the absence cases are pinned off the runnerFatal early return.
…question (#3182)

The contract and user docs claimed a web producer that does not exist: platform-web hands over a
tree and never reads a screen, and neither does Linux. They now name the two surfaces that answer
and the absences that are real, including raw, which validates no box. The Node client page gains
the field, following how readiness was documented.
…3182)

normalizeBackendSnapshot returned a backend-built state verbatim, so a producer that named its own
keyboard band or viewport box beside that state lost it on the one path where the backend was the
authority. The two Android rejection helpers advertise a capture they can never yield and now say
never, and the helper's one-read claim is scoped to what the code guarantees: density and extent
from one DisplayMetrics, not a metrics sample taken with the dump. Proves the display read answers
the geometry-free-tree case the issue came from.
…ndow (#3182)

The re-capture loop #3160 split out of helper capture declared the plain xml+metadata pair, so the
display read a usable attempt made was invisible to the caller while it travelled inside the object.
Declaring the capture type makes the viewport the loop already carries part of its contract.
…3182)

The issue's second Done-when line is a capture of an empty screen that still reports the box. On the
provider acquisition seam that is now a test: the fold runs with no nodes and the reported viewport
still reaches the result.
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-05 09:00 UTC

@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.03 MB 4.97 MB -56.6 kB
Package (unpacked) 5.03 MB 4.97 MB -56.6 kB
Package (download) 1.51 MB 1.49 MB -15.0 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 20.8 ms 21.0 ms +0.3 ms
CLI --help 62.7 ms 64.1 ms +1.5 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 41 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/kernel/src/snapshot.ts Outdated
Comment thread packages/capture-kit/src/snapshot/ios-snapshot-runtime.ts Outdated
Comment thread website/docs/docs/commands.md Outdated
@thymikee

thymikee commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

The Android viewport reads the wrong display size, so this needs a fix and a live Android run before merge (reviewed at a31e682).

The helper reads Resources.getSystem().getDisplayMetrics().widthPixels/heightPixels in https://github.com/callstack/agent-device/blob/a31e682/android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/SnapshotInstrumentation.java#L128. That is the app display size, so on devices with a persistent navigation bar (3-button nav) it leaves out the bar. The tree comes from getBoundsInScreen across interactive windows and includes the SystemUI nav-bar nodes, so those rects fall outside the published viewport. The same gap appears in landscape. Consumers such as the use case in #3182 would scale and clip against a box smaller than the screen, and nav-bar or edge taps would land outside it. Please make this rule hold: the Android viewport contains every getBoundsInScreen rect of a full-screen capture, in the current rotation. Read the real display extent (for example WindowManager.getMaximumWindowMetrics().getBounds() on API 30+, Display.getRealMetrics below) at the same point as the tree. Take it once per capture, including the session path that shares putCaptureMetadata at SnapshotInstrumentation.java:250. I also could not tell whether Resources.getSystem() refreshes on rotation inside a reused helper session. Does it, or can it stay stale?

This PR changes a device-facing producer (new helper metadata keys and host derivation at https://github.com/callstack/agent-device/blob/a31e682/packages/platform-android/src/snapshot-capture.ts#L383) with no live Android run, and it closes #3182. The PR body says Android evidence rides on CI, but no CI e2e reads viewport, and the iOS Preferences run does not cover Android. Please run this on an Android emulator with 3-button navigation through the daemon CLI. (a) In portrait, snapshot --json must report a viewport equal to adb shell wm size (override if set) and the screenshot PNG dimensions, and the Back and Home node rects must lie inside it. (b) Rotate to landscape and run a second snapshot --json in the same session, with diagnostics showing sessionReused or transport=session. The viewport must be swapped and match the landscape screenshot. (c) Run snapshot --json --raw on Android to settle what the docs claim. (d) Capture an empty screen on both the iOS simulator and the Android emulator, and show that viewport is still reported, as the done-when of #3182 asks.

Could this be simpler? The iOS engine already resolves the validated viewport (resolveIosViewport in invariants.ts, used at engine.ts:109) but does not return it. Both callers then re-derive "regular validates a box, raw does not", in snapshot-presentation.ts:734 with its viewportSize helper and inline in ios-snapshot-runtime.ts:46. If publishIosSnapshot and presentIosRunnerSnapshot returned the validated rect on the presentation result, each caller could pass it through snapshotViewportSizeFrom once, and the rule would live in its owner. On Android, the one-check DisplaySizeReader class could go, since the host guard snapshotViewportSizeFrom already refuses values <= 0. Deriving the viewport once in snapshotAndroid from helper metadata would also avoid threading a new AndroidUiHierarchyCapture sibling through six signatures. In the kernel, readSnapshotViewportSize could delegate to snapshotViewportSizeFrom, so there is one constructor, and a branded type would make the claimed invariant real. No ADR is needed, but the engine's presentation output type has to carry the resolved viewport first.

Open threads from the other review that still apply: the rotation window in the metrics read, #3210 (comment), and the plain structural SnapshotViewportSize type, #3210 (comment). The --raw docs thread, #3210 (comment), also applies: Android --raw does publish viewport, correctly, because raw rects are still in screen space, so commands.md:420 should limit the --raw absence to Apple. The thread at #3210 (comment) does not apply and can be resolved: resolveIosViewport uses isPositiveFiniteRect, which already rejects the infinite rect a failed XCTest read returns, and the result would be absence, which the contract allows. For the rotation window, a cheap fix is to read before and after the dump and omit viewport on mismatch. That is separate from which metrics are read, covered above.

The Android smoke job failed in the no-index Maestro tapOn duplicate step, where assertVisible 'Maestro selection: clickable' did not match after the tap. This looks unrelated. The diff adds no reader of viewport in tap, selector, Maestro or replay code, and in the same run 15 helper snapshots succeeded and the automation-system scenario passed. The diff does rebuild the helper APK, so please rerun the job to confirm. iOS smoke and Coverage were still running when I looked. I did not run any device or local test, and I did not check whether that Maestro step fails on main. The PR body's local gate and iOS simulator results are taken as quoted. There are no conflicts.

…ump (#3182)

Resources.getSystem() reports the app display, which excludes a persistent
navigation bar, while the tree's getBoundsInScreen rects span the full panel.
Read the real extent instead (WindowManager.getMaximumWindowMetrics on API 30+,
Display.getRealMetrics below) before and after each dump on both the one-shot
and the session path, and publish it only when the two reads agree so a
rotation mid-dump omits the viewport instead of pairing one rotation's bounds
with the other's dimensions.
A plain structural { width, height } accepted the zero the type claimed was
unrepresentable, and readSnapshotViewportSize stood as a second constructor
beside snapshotViewportSizeFrom. Brand the type with a token only kernel/rect
imports, so a literal with a zero in it stops being assignable, and let the
reader delegate to snapshotViewportSizeFrom so one guard decides what a usable
box is. Test fixtures now mint their viewport through the guard, which is the
mutation that proves the brand bites.
#3182)

The engine already resolves the box the regular projection validates against;
hand it over on the presentation result and publication instead of making
snapshot-presentation.ts and ios-snapshot-runtime.ts each re-derive the rule
that regular validates a box and raw does not. Callers now only pass the
returned rect through the shared construction guard, so the rule lives with
its owner.
…id (#3182)

The capture carrier held a pre-derived viewport beside its metadata, so every
re-capture hop had to thread the sibling through; a second copy of the display
pair also meant two lifetimes for one fact. Carry the raw helper metadata on
AndroidUiHierarchyCapture instead and answer the viewport question only in
snapshotAndroid, through the shared guard, so both the healthy and the
presentation-failed return publish the same box the tree was measured against.
)

Android publishes its viewport under --raw too: the display the helper reads is
the same screen the raw rects are measured on. Only the Apple projection
validates no box, so only there does --raw omit viewport.
@thymikee

thymikee commented Oct 5, 2026 •

Copy link
Copy Markdown
Member Author

Live Android evidence (3-button navigation) — head 8ff568ab4

Run through the daemon CLI on emulator-5554 (API 35, 1080x2400), 3-button nav enabled
(cmd overlay enable com.android.internal.systemui.navbar.threebutton, navigation_mode 0;
SystemUI nav-bar inset frame=[0,2274][1080,2400]). Build freshness per docs/agents/device-verification.md:
pnpm build && pnpm build:android && pnpm clean:daemon. Helper provenance on every capture:
androidSnapshot.backend=android-helper, helperVersion=0.21.20 (== package.json).

The whole point of the fix is visible in one number: the nav bar occupies y 2274..2400, so the
old Resources.getSystem() app-display read (1080x2274) left the Back/Home/Recents rects outside
the published viewport. They now sit inside it.

(a) Portrait: viewport == wm size == PNG dims, nav nodes inside

$ adb -s emulator-5554 shell wm size
Physical size: 1080x2400

$ agent-device snapshot --json --session vp3182 --platform android
viewport: {"width":1080,"height":2400}
backend: android-helper  helperVersion: 0.21.20  transport: persistent-session

Back     rect={"x":125,"y":2279,"width":207,"height":116}   # y+height = 2395 <= 2400
Home     rect={"x":434,"y":2279,"width":210,"height":116}
Recents  rect={"x":746,"y":2279,"width":207,"height":116}

nodes outside viewport: 0 of 45

$ adb -s emulator-5554 exec-out screencap -p   -> PNG 1080x2400
$ agent-device screenshot --out out.png --session vp3182 --platform android --fullscreen
/tmp/vp3182-portrait-ad.png (1080x2400)

All three nav-bar buttons now resolve inside the box, and the viewport matches both wm size and
the PNG the same screen produced.

(b) Landscape, same session: viewport swapped, session diagnostics

$ adb -s emulator-5554 shell cmd window user-rotation lock 1   # cur=2400x1080

$ agent-device snapshot --json --session vp3182 --platform android --force-full   # 1st
viewport: {"width":2400,"height":1080}  transport: persistent-session  sessionReused: false
Recents rect={"x":2279,"y":125,"width":116,"height":207}   # swapped axis
Home    rect={"x":2279,"y":434,"width":116,"height":210}
Back    rect={"x":2279,"y":746,"width":116,"height":207}
nodes outside viewport: 0 of 45

$ agent-device snapshot --json --session vp3182 --platform android --force-full   # 2nd
viewport: {"width":2400,"height":1080}  transport: persistent-session  sessionReused: true

$ adb -s emulator-5554 exec-out screencap -p   -> PNG 2400x1080

transport=persistent-session on every capture and sessionReused=true on the second one prove the
reused-session path (putCaptureMetadata via the socket loop) is the path that produced these
numbers, not the one-shot. The nav bar moved to the right edge and the box moved with it.

sessionReused: false on the first command of a session is expected: the helper is started by that
command and retired when it ends unless the session is app-bound. The two captures above are against
an app-bound session (open com.google.android.deskclock --session vp3182), which is what makes the
second one reuse.

(c) snapshot --json --raw on Android publishes viewport

$ agent-device snapshot --json --raw --session vp3182 --platform android --force-full
viewport: {"width":2400,"height":1080}  nodes: 101  androidSnapshot.backend: android-helper

Android publishes viewport under --raw, as the doc update now states: the display the helper
reads is the same screen the raw rects are measured on. On Apple the raw projection validates no box
and omits it (verified below), so the absence is Apple-only.

(d) Empty-screen captures still report viewport

Android (0 nodes presented, tree still on screen):

$ agent-device snapshot --json --session vp3182 --platform android --scope "zzz-no-such-surface-9x7"
viewport: {"width":2400,"height":1080}  nodes: 0

iOS 27.0 simulator (iPhone 18 Pro), Settings open:

$ agent-device snapshot --json --session vp3182ios --platform ios --force-full
viewport: {"width":402,"height":874}  nodes: 42

$ agent-device snapshot --json --session vp3182ios --platform ios --scope "zzz-no-such-surface-9x7"
viewport: {"width":402,"height":874}  nodes: 0

$ agent-device snapshot --json --raw --session vp3182ios --platform ios --force-full
"viewport" in payload: false   nodes: 123

Both producers answer the box without reading it out of the tree: Android from the display read,
iOS from the viewport the regular fold validated. The Apple --raw omission is confirmed on the same
device in the same session.

Resources.getSystem() on rotation in a reused helper session

The question is answered by the change rather than by measurement, which is the point. Two things
were wrong with it, and the second is exactly the staleness you asked about:

  1. It reports the app display, so its extent excludes a persistent nav bar. That is what put
    Back/Home outside the box; WindowManager.getMaximumWindowMetrics().getBounds() does not.
  2. It is a process-wide snapshot, not a per-window one. A helper that stays alive across a rotation
    keeps an ActivityThread s configuration that only advances when the framework delivers a config
    change to that process. An instrumentation process with no Activity and no registered
    ComponentCallbacks has nothing forcing that delivery, so Resources.getSystem() can keep
    answering the previous orientation's widthPixels/heightPixels for the whole life of a reused
    session — a wrong box, not merely a narrow one. DisplayExtent.read() asks the display each time
    through WindowManager, which is the live display object, so it cannot go stale the same way, and
    the before/after pair around the dump catches the remaining case where the display changes while
    the tree is being walked.

So: yes, it can stay stale in a reused session, and reading the display per capture is what makes the
question not need asking. DisplaySizeReader and its one-check gate are gone; the host-side
snapshotViewportSizeFrom guard is the only place that decides what a usable box is.

Design simplifications from the review

All applied: publishIosSnapshot/presentIosRunnerSnapshot now return the validated rect on the
presentation result, so snapshot-presentation.ts and ios-snapshot-runtime.ts only pass it through
the shared guard instead of re-deriving "regular validates, raw does not"; DisplaySizeReader is
deleted; snapshotAndroid derives the viewport once instead of threading an
AndroidUiHierarchyCapture.viewport sibling through the re-capture signatures; readSnapshotViewportSize
delegates to snapshotViewportSizeFrom; and SnapshotViewportSize is branded, so a literal with a
zero in it is no longer assignable. Proving the brand bites, the three test fixtures that carried a
{ width, height } literal now mint their viewport through the guard.

Validation on 8ff568ab4

  • pnpm check:affected --run: all runnable checks passed (1036 files, 8403 tests).
  • pnpm check:quick (lint + typecheck) and pnpm format:check clean.
  • Android helper rebuilt; its JVM suite (SnapshotHelperTestSuite, now DisplayExtentTest) compiles and runs in the build.
  • Focused lanes for every touched module green, including the apple-runner project's snapshot-presentation.test.ts.
  • The no-index Maestro tapOn smoke failure was rerun: this head's own Android lane is in progress at 8ff568ab4 and the previous head's job is being retried.

Cleanup

Sessions closed: vp3182 (Android), vp3182ios (iOS). Emulator left in portrait with 3-button nav
enabled, as the reviewer asked for the configuration; the overlay can be reverted with
cmd overlay disable com.android.internal.systemui.navbar.threebutton.

…erge (#3182)

The state seam added the backend's keyboard and viewport facts to the state
without carrying the private clickability evidence over, and that evidence is
retained by object identity. Android Maestro taps then read
`exact-evidence-not-retained`, skip clickable-first ordering, and tap the
document-order duplicate of a requested id — the Android smoke lane's
no-index `tapOn` failure, which is this PR's, not a flake.

The mutation that makes the new test fail: remove the copy and the evidence
read on the published result is undefined.
@thymikee

thymikee commented Oct 5, 2026 •

Copy link
Copy Markdown
Member Author

Summary — head 5a05a3ea4 (all merge-blocker items addressed)

What landed on top of a31e682

Android viewport fix (the blocker): the helper now reads the real display extent — WindowManager.getMaximumWindowMetrics().getBounds() on API 30+, Display.getRealMetrics below — before and after each tree dump on both the one-shot and the socket-session path, publishing displayWidth/displayHeight only when the two reads agree. The old Resources.getSystem() app-display read excluded the persistent nav bar; live numbers below. DisplaySizeReader is gone; DisplayExtent + DisplayExtentTest replace it.

Design simplifications, all applied: publishIosSnapshot / presentIosRunnerSnapshot return the validated rect, so snapshot-presentation.ts and ios-snapshot-runtime.ts pass it through the guard instead of re-deriving the regular/raw rule; snapshotAndroid derives the viewport once from helper metadata (no sibling threaded through the re-capture signatures); readSnapshotViewportSize delegates to snapshotViewportSizeFrom; SnapshotViewportSize is branded — a zero-bearing literal is not assignable, and three fixtures that carried raw literals now mint through the guard (the mutation that proves the brand). Docs: --raw viewport absence is limited to Apple targets, matching Android behavior.

The Android smoke failure was this PR's, not a flake. You asked to rerun to confirm; the reruns reproduced assertVisible 'Maestro selection: clickable' deterministically (4/4 on this branch, while main and three other branches passed the same step the same evening). Root cause: the state-seam commit made normalizeBackendSnapshot spread-copy the backend's own state, but Android clickability evidence is retained by object identity (WeakMap) — the copy silently dropped it, Maestro's clickable-first ordering fell back to exact-evidence-not-retained divergence, and tapOn id picked the document-order inert duplicate. Fixed at the owning seam by carrying the evidence across the copy (the pattern snapshot-runtime.ts already follows at every spread). Regression test: fails without the fix (evidence undefined), passes with it. Your Android lane now passes on 5a05a3ea4.

Live evidence (3-button nav, emulator-5554 API 35 1080x2400, daemon CLI)

Full transcript with commands and node dumps: #3210 (comment)

  • (a) Portrait viewport {1080,2400} == wm size == adb screencap PNG == CLI screenshot PNG; Back/Home/Recents at y 2279..2395 (inside; the old read ended at 2274); 0 of 45 node rects outside.
  • (b) Landscape second snapshot, same session: transport=persistent-session, sessionReused=true, viewport {2400,1080} swapped, nav bar moved to the right edge inside the box, PNG 2400x1080.
  • (c) Android snapshot --json --raw publishes viewport {2400,1080} with backend=android-helper — settled; docs updated.
  • (d) Empty screen: Android 0-node capture still reports viewport {2400,1080}; iOS 27.0 simulator 0-node capture still reports viewport {402,874}; iOS --raw omits viewport (same session as the regular capture that publishes it).
  • Resources.getSystem() staleness answer: yes, it can stay stale — it is a process-wide snapshot advanced only by delivered config changes, which a reused instrumentation session with no Activity/callbacks has nothing forcing; DisplayExtent.read() asks the live display object instead, making the question moot. Details in the evidence comment.

Validation on 5a05a3ea4

  • pnpm check:affected --run green (1036 files / 8404 tests, incl. helper JVM suite via build, format, lint, typecheck, layering, fallow --base, build).
  • CI on this head: CI workflow, Linux, macOS, Size, Test App Build Cache green; Android smoke green (was failing 4/4 at a31e682/8ff568ab4).
  • Every lane green on 5a05a3ea4: CI, Android, iOS, macOS, Linux, Size, Test App Build Cache, Deploy PR previews.
  • Failed smoke reruns: 37212547526 (old head, cancelled once a same-branch run made it redundant) and 37265360866 attempts — these reproduced the defect and then validated its fix's diagnosis; the current head's own Android run is the authoritative pass.

Review threads

r4178245047 / r4178245048 / r4178245055 / r4178245057 all answered with the fix or the rebuttal above; all four are resolved.

Cleanup: verification sessions closed (vp3182, vp3182ios, local repro session), pnpm clean:daemon run, emulator-5554 left portrait with 3-button nav enabled. Not merged.

@thymikee

thymikee commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

The PR is ready at 5a05a3e. The Android display-extent fix, the TS clickability copy and the docs change from the earlier findings (#3210 (comment)) are all in, and nothing from that review remains open. Not blocking, and you can take or leave it: the doc on SnapshotViewportSize at https://github.com/callstack/agent-device/blob/5a05a3e/packages/kernel/src/snapshot.ts#L358 says only modules that import the brand symbol can build the type, but rect.ts mints it with a cast and never imports the symbol, so please reword the doc to say snapshotViewportSizeFrom is the sanctioned mint and casts are the escape hatch, or stop exporting the brand symbol.

All 21 checks pass at 5a05a3e, and the earlier Android smoke failure came from this PR's snapshot spread, which the latest change fixes. I saw no conflicts. Nothing else must happen before merge.

On the open inline threads: the Android display-extent read (#3210 (comment)), the viewport brand (#3210 (comment)) and the docs note on --raw (#3210 (comment)) are fixed at this head, so please resolve them. The non-positive rect guard (#3210 (comment)) is benign, because the guard and the fold agree and absence is allowed by the contract, so please resolve it too.

For the record, the live run was at 8ff568a, not 5a05a3e. I accepted it because the only later change is the TS clickability copy, which does not touch the helper or the viewport path. The legacy API below 30 Display.getRealMetrics branch has no live run, since only API 35 was exercised. I did not run the JVM DisplayExtentTest or the vitest regression locally, and I took the live transcript, CI status and smoke reproduction as you quoted them.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 5, 2026
@thymikee
thymikee merged commit 3a4bcbe into main Oct 5, 2026
21 checks passed
@thymikee
thymikee deleted the feat/snapshot-response-viewport branch October 5, 2026 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Snapshot response should publish the viewport its rects are measured in

1 participant