Skip to content

feat(screenshot): add --crop-on to crop captures to a selector frame - #2276

Merged
thymikee merged 9 commits into
mainfrom
feat/screenshot-selector-crop
Sep 4, 2026
Merged

feat(screenshot): add --crop-on to crop captures to a selector frame#2276
thymikee merged 9 commits into
mainfrom
feat/screenshot-selector-crop

Conversation

@thymikee

@thymikee thymikee commented Sep 4, 2026

Copy link
Copy Markdown
Member

Summary

Add screenshot --crop-on <selector>: capture the screen, then crop the saved PNG to the frame the selector resolves to on the same screen. Runs daemon-side after the platform write and before --scale, against a fresh full-tree snapshot (no setSessionSnapshot). Accepted on iOS simulators and Android emulators; every other target is refused before device work. Cannot combine with --overlay-refs or --fullscreen.

agent-device screenshot out.png --crop-on 'role=cell label=General'
agent-device screenshot out.png --crop-on 'label="Network & internet"' --pixel-density 3

Success carries post-crop width/height and a warnings entry when the frame is clipped to the image. Failures use stable details.reason codes: CROP_TARGET_NOT_FOUND (+find hint), CROP_TARGET_AMBIGUOUS (+candidates), CROP_SELECTOR_INVALID, CROP_FRAME_MISMATCH, CROP_TARGET_NOT_ACCEPTED.

Validation

Head 62f39143ce (rebased onto 33084c7748; the review commits 7edcd2adecc5be390a85643c9b8a8062f39143ce are behavior-preserving — pnpm check:affected --run green, same seam/geometry tests): pnpm check:affected --run — all runnable checks passed.

Live pixel-identity (crop region vs independent raw capture, mean-abs-diff, collected at the pre-refactor head 570da2c417; the refactor is behavior-preserving):

  • iOS sim (Settings "General" cell): 370×52 @1× / 1110×156 @3×; vs raw simctl 0.0000 at both densities; one-row-shifted control 8.44.
  • Android emulator (android-helper @ package version): 436×71 @1:1; vs raw screencap 0.0000; control 42.83.
  • Live error paths: NOT_FOUND, AMBIGUOUS (matches:11), SELECTOR_INVALID, FRAME_MISMATCH. EMPTY_INTERSECTION is unit-tested (off-screen rows are absent from the live tree). macOS cell stays rejected (terminal lacks Screen Recording).

Size review

41 files, +1899 −155: net +670 production / +971 tests (was ≈740 net / 2043 gross at the pre-review head; the review pass trimmed ~70 production lines by splitting the crop module, reusing kernel rect helpers, hoisting the duplicated meaningful-signal predicate into contracts, and dropping two redundant test cases).

Retained production growth, itemized:

  • screenshot-crop.ts (+144): the crop leaf — capture, sparse/empty refusal, selector resolution with typed ambiguity/missing reasons, project→intersect→crop, and the single warning-composition owner.
  • screenshot-crop-target.ts (+148): the target acceptance matrix (one cell per capture frame), the device→cell classifier, and the pre-device argument policy. Split from the leaf so both modules meet the 300-line target (the review's size item).
  • capture-kit/snapshot-rect-projection.ts (+138): space decision (device-pixels vs viewport-points), no-clamp projection, crop intersection, and the shared viewport-bounds resolver consumed by both the overlay and the crop. Reuses kernel isPositiveFiniteRect/rectArea and the shared isMeaningfulSignal (no local redefinitions).
  • capture-kit/png-crop.ts (+54): in-place PNG region crop on the worker.
  • screenshot-runtime.ts (+111 −15): the integration delta on the existing module — read flag, answer policy pre-device, admit the plan, run crop after the write/before scale, append the warning.
  • contracts/screenshot.ts (+66 −2): the machine-readable flag + 10-reason typed taxonomy, flag keys, --crop-on definition, options↔flags projection, script string-flag reader.
  • client/screenshot-result.ts (+55 −21): default-level normalization (incl. response-level warnings) + field picker, shared by client and CLI.
  • Smaller: screenshot-runtime-binding.ts (+26 −10, plan admits the crop's snapshot op), platform-runtime-operations.ts (+18 −7), contracts/snapshot-text.ts (+8, the hoisted isMeaningfulSignal shared by overlay + crop), selector-pipeline-policy.ts (+8), capture-kit/package.json (+8, subpaths), screenshot-overlay.ts (+7 −74, refactored to consume the shared projection and the shared predicate — net −67), cli/commands/screenshot.ts (+6), resolution-policy.ts (+5), commands/capture/screenshot.ts (+4), script-utils.ts (+4), agent-device-client.ts (+2 −12, inline reconstruction moved into the shared normalizer), contract facades (+2 each), response-views.ts (+1), snapshot-types.ts (+1).

Why a materially smaller design was rejected:

  • The acceptance matrix is explicit (one cell per target) because the completeness gate requires every target that admits the plan to have exactly one cell; a default-reject form would let a target slip through unclassified.
  • The typed reason taxonomy is mandated by "key behavior on typed reasons and details, never error text"; collapsing reasons would push callers back onto message sniffing.
  • The crop runs daemon-side (not client-side) so it reads the exact platform-written file and projects against the same admitted snapshot binding as the capture; a client-side crop would re-read the published artifact and could diverge from the snapshot frame.
  • The request-scoped crop snapshot (never setSessionSnapshot) is what keeps the authorized-ref frame a following interaction resolves untouched; reusing the session snapshot would corrupt that invariant.

Tests (+915): geometry fixtures, the matrix one-to-one classifier/policy, the crop orchestration seam across 8 outcome paths, runtime ordering/warning wiring, the contract flag round-trip, CLI warning rendering, and the focused screenshot-result normalization. Two redundant cases were dropped (the crop-on CLI forwarding case — covered by the flag-mapping contract tests plus the generic dispatcher — and the transport-based warnings case, now a focused unit test).

@github-actions

github-actions Bot commented Sep 4, 2026

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

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.60 MB 2.61 MB +8.6 kB
JS gzip 875.8 kB 878.4 kB +2.6 kB
npm bundled raw 2.60 MB 2.61 MB +8.6 kB
npm bundled gzip 875.8 kB 878.4 kB +2.6 kB
npm tarball 1.03 MB 1.03 MB +2.5 kB
npm unpacked 3.48 MB 3.49 MB +8.7 kB
npm clean-installed 3.48 MB 3.49 MB +8.7 kB

npm unpacked components

Component Base Current Diff
JS / dist source 2.76 MB 2.77 MB +8.7 kB
Apple runner source/project 544.4 kB 544.4 kB 0 B
Apple snapshot presentation source 33.8 kB 33.8 kB 0 B
macOS helper source 54.8 kB 54.8 kB 0 B
Android helper artifacts 43.3 kB 43.3 kB 0 B
Other package files 46.6 kB 46.6 kB 0 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 29.3 ms 29.3 ms +0.0 ms
CLI --help 78.0 ms 77.1 ms -0.9 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/registry.js +264 B +84 B
dist/src/sdk-batch-runner.js +251 B +52 B
dist/src/app-inventory-contract.js +108 B +17 B
dist/src/internal/daemon.js +48 B +5 B
dist/src/runtime.js -1 B +5 B

Top changed packed files

Packed file Base Current Diff
dist/src/screenshot-runtime.js 14.4 kB 20.8 kB +6.4 kB
dist/src/capture.js 4.7 kB 5.9 kB +1.1 kB
dist/src/screenshot-result.js 1.4 kB 1.8 kB +425 B
dist/src/registry.js 171.5 kB 171.8 kB +264 B
dist/src/sdk-batch-runner.js 83.9 kB 84.1 kB +251 B
dist/src/agent-device-client.js 19.0 kB 18.8 kB -204 B
dist/src/app-inventory-contract.js 54.7 kB 54.8 kB +108 B
dist/src/client-types.d.ts 57.9 kB 58.0 kB +103 B
dist/src/script.js 20.8 kB 20.9 kB +89 B
dist/src/screenshot3.js 5.3 kB 5.4 kB +64 B

@thymikee

thymikee commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

BLOCKED at exact head 570da2c417071427cd2df0d2dfabc278bf712560.

  1. Coverage is an owner-action failure, not infrastructure: the test-file size ratchet reports that this head grows src/__tests__/cli-client-commands.test.ts from 1304 to 1338 lines and src/__tests__/client.test.ts from 1554 to 1577. Move the new screenshot CLI forwarding case into the screenshot-command test topology and the response-warning normalization case into a focused screenshot-result test, then rerun exact-head CI.
  2. The readiness size requirement is not yet met. CI reports npm unpacked +9.2 kB and the PR reports roughly 740 net production / 2,043 gross lines, crossing both scrutiny triggers. The body names broad components but does not itemize retained growth or record why a materially smaller design was rejected. Add that review evidence and split the target/policy matrix from the new 328-line screenshot-crop.ts leaf so the new implementation modules meet the repository's 300-line target.

No separate runtime correctness finding: the admitted screenshot/snapshot route, typed failure handling, CLI/Node/script surfaces, and live iOS/Android pixel-identity evidence are otherwise sound, and every other exact-head check is green.

…ant coverage

Address review comments at 570da2c:
- Split the 328-line screenshot-crop.ts leaf: the target acceptance matrix,
  classifier, and pre-device argument policy move to screenshot-crop-target.ts,
  so both implementation modules meet the 300-line target.
- Reuse kernel isPositiveFiniteRect/rectArea in the rect-projection module
  instead of redefining them locally.
- Drop the crop-on CLI forwarding case (redundant with screenshot-options
  flag-mapping coverage + the generic dispatcher) and the transport-based
  warnings case, replacing the latter with a focused screenshot-result unit
  test. This also returns the two legacy aggregate test files to their
  merge-base length for the test-file size ratchet.
@thymikee

thymikee commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Both items addressed at new head 7edcd2adec (behavior-preserving refactor; pnpm check:affected --run green).

1. Test-file size ratchet — the two legacy aggregate files are back to their merge-base length, with the new cases relocated to the owning topology:

  • src/__tests__/cli-client-commands.test.ts → 1304 (was 1338). Dropped the crop-on forwarding case as redundant: the screenshotCropOncropOn mapping is covered by src/commands/capture/screenshot-options.test.ts (both projections + the --crop-on script reader + the flag-key registries), and the CLI dispatcher is generic (the adjacent overlay-refs case proves forwarding).
  • src/__tests__/client.test.ts → 1554 (was 1577). The transport-based warnings case moved to a focused unit test src/client/__tests__/screenshot-result.test.ts exercising normalizeScreenshotCaptureResult directly.

2. Readiness size — net production trimmed from ≈740 to +671 (39 files, +1836 −147). Concretely:

  • Split the 328-line screenshot-crop.ts into screenshot-crop.ts (144, the crop leaf) + screenshot-crop-target.ts (144, the acceptance matrix / classifier / pre-device policy) — both under the 300-line target. The split also shrank the module (compressed the repeated rejection cells, deduped the refusal).
  • snapshot-rect-projection.ts now reuses kernel isPositiveFiniteRect/rectArea instead of redefining them.
  • The body now carries the itemized retained-growth table and the rejected-smaller-design rationale (explicit matrix for the completeness gate, typed reasons per the repo rule, daemon-side crop + request-scoped snapshot for the authorized-ref invariant).

Fallow complexity on the split classifyAppleCropTarget is resolved by extracting the macOS surface decision back into a helper. Exact-head CI is the remaining authority.

…sifier under the complexity budget

classifyAppleCropTarget inlined the macOS surface decision, pushing its
cyclomatic complexity to the fallow threshold. Move it back out to a
small helper so the target classifier stays within budget.
…sh png-crop

- Hoist isMeaningfulSignal into @agent-device/contracts/snapshot (next to
  normalizeType/isMeaningfulLabel) so the ref overlay and the crop
  rect-projection share one copy instead of each carrying an identical
  private predicate. Behavior is unchanged.
- png-crop: isCropBox was a no-op 'box is Rect' predicate (input already
  Rect) — make it a plain boolean, and tighten the doc to the contract.
@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 4, 2026
@thymikee

thymikee commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Re-review of 643c9b8a80aebc5ea232d2c1c9a3570259177ab4: CLEAN / ready for human review. No remaining code finding.

Both prior blockers are resolved. The legacy aggregate tests are back at merge-base size; warning normalization now has a focused screenshot-result test and the redundant forwarding case remains covered by the owning flag-projection/script tests. The crop leaf and target-policy matrix are split into 144/148-line modules with mirrored tests, preserving the one-cell-per-target completeness gate. Rect and meaningful-signal logic now reuse the kernel/contracts owners instead of parallel predicates.

The production route remains coherent and behavior-preserving: policy before device work → admitted screenshot+snapshot binding → platform PNG write → request-scoped full-tree snapshot → selector resolution/projection/intersection → worker crop → scale/result metadata; it does not mutate the session snapshot. The pre-refactor live iOS/Android pixel-identity evidence remains applicable because the review commits only move/simplify ownership and exact-head geometry/runtime tests pass.

Size scrutiny is satisfied: +673 net production/+915 tests is itemized, smaller alternatives are addressed, and the refactor removed roughly 70 production lines. CI size is +8.7 kB unpacked/+2.6 kB gzip for the new functionality. All exact-head checks are green except iOS Smoke, which is still in progress; per review policy this clean head can be marked ready-for-human while it finishes.

…rojection seams

- ScreenshotCropOutcome.cropped was a constant true that no caller read;
  the crop either returns (success) or throws, so the outcome reduces to
  the partialIntersection observation.
- resolveScreenshotRectSpace and resolveSnapshotBounds were the only
  projection exports without coverage: pin the accepted-backend map, the
  unaccepted-backend typed refusal, and the viewport-root / union / empty
  bounds branches.
@thymikee
thymikee merged commit 172ee14 into main Sep 4, 2026
20 checks passed
@thymikee
thymikee deleted the feat/screenshot-selector-crop branch September 4, 2026 09:19
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.

1 participant