@reticlehq/server 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +13 -12
- package/dist/command/cli/adhoc-suite.js +2 -0
- package/dist/command/cli/adhoc-verdict.d.ts +2 -43
- package/dist/command/cli/adhoc-verdict.js +50 -187
- package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
- package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
- package/dist/command/cli/cli-doctor.js +1 -1
- package/dist/command/cli/cli-flow-commands.js +2 -9
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-compare.d.ts +22 -0
- package/dist/command/cli/cli-parse-compare.js +75 -0
- package/dist/command/cli/cli-parse-grade.d.ts +36 -0
- package/dist/command/cli/cli-parse-grade.js +75 -0
- package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
- package/dist/command/cli/cli-parse-grammar.js +18 -20
- package/dist/command/cli/cli-parse-verify.d.ts +10 -0
- package/dist/command/cli/cli-parse-verify.js +63 -14
- package/dist/command/cli/cli-parse.d.ts +17 -15
- package/dist/command/cli/cli-parse.js +44 -27
- package/dist/command/cli/cli-update-commands.d.ts +1 -1
- package/dist/command/cli/cli-update-commands.js +29 -20
- package/dist/command/cli/cli-usage.js +58 -29
- package/dist/command/cli/cli-verify.d.ts +11 -2
- package/dist/command/cli/cli-verify.js +27 -8
- package/dist/command/cli/cloud-cli.js +2 -119
- package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
- package/dist/command/cli/daemon-link/daemon-client.js +205 -0
- package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
- package/dist/command/cli/doctor-repairs.js +1 -1
- package/dist/command/cli/ensure-daemon.d.ts +26 -0
- package/dist/command/cli/ensure-daemon.js +57 -0
- package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
- package/dist/command/cli/finished-run/grade-command.js +180 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
- package/dist/command/cli/ports/port-holder.d.ts +9 -0
- package/dist/command/cli/ports/port-holder.js +27 -3
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +22 -0
- package/dist/command/cli/status-fields.d.ts +5 -1
- package/dist/command/cli/status-fields.js +7 -3
- package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
- package/dist/command/cli/two-builds/compare-command.js +314 -0
- package/dist/command/cli/verify-against.d.ts +61 -0
- package/dist/command/cli/verify-against.js +187 -0
- package/dist/command/cli/verify-no-session.d.ts +2 -0
- package/dist/command/cli/verify-no-session.js +13 -0
- package/dist/command/cli/verify-results.d.ts +35 -2
- package/dist/command/cli/verify-results.js +76 -10
- package/dist/command/cli.js +46 -41
- package/dist/command/daemon/daemon-resolve.d.ts +8 -1
- package/dist/command/daemon/daemon-resolve.js +27 -1
- package/dist/command/daemon/daemon.d.ts +12 -1
- package/dist/command/daemon/daemon.js +35 -4
- package/dist/command/daemon/lifetime/death-cause.js +21 -2
- package/dist/command/daemon/lifetime/heartbeat.js +7 -2
- package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
- package/dist/command/daemon/lifetime/startup-failure.js +62 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
- package/dist/command/setup/bringup/dev-server-files.js +17 -0
- package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
- package/dist/command/setup/bringup/static-page-server.js +43 -9
- package/dist/command/setup/init/init-runtime.js +6 -0
- package/dist/command/setup/node-effects.d.ts +3 -2
- package/dist/command/setup/node-effects.js +9 -3
- package/dist/command/setup/remaining-steps.d.ts +8 -0
- package/dist/command/setup/remaining-steps.js +10 -0
- package/dist/command/setup/run-setup.d.ts +2 -0
- package/dist/command/setup/run-setup.js +9 -1
- package/dist/command/setup/setup-command.js +6 -3
- package/dist/command/update/updater.js +6 -0
- package/dist/command/version/version-skew.js +1 -1
- package/dist/features/compare/action-plan.d.ts +260 -0
- package/dist/features/compare/action-plan.js +543 -0
- package/dist/features/compare/behaviour-compare.d.ts +24 -0
- package/dist/features/compare/behaviour-compare.js +85 -0
- package/dist/features/compare/behaviour-diff.d.ts +97 -0
- package/dist/features/compare/behaviour-diff.js +202 -0
- package/dist/features/compare/behaviour-report.d.ts +40 -0
- package/dist/features/compare/behaviour-report.js +128 -0
- package/dist/features/compare/build-readings.d.ts +106 -0
- package/dist/features/compare/build-readings.js +250 -0
- package/dist/features/compare/compare-diff.d.ts +62 -0
- package/dist/features/compare/compare-diff.js +177 -0
- package/dist/features/compare/compare-report-html.d.ts +6 -0
- package/dist/features/compare/compare-report-html.js +102 -0
- package/dist/features/compare/compare-report.d.ts +15 -0
- package/dist/features/compare/compare-report.js +73 -0
- package/dist/features/compare/compare-run.d.ts +60 -0
- package/dist/features/compare/compare-run.js +123 -0
- package/dist/features/compare/outside/effect-lines.d.ts +26 -0
- package/dist/features/compare/outside/effect-lines.js +103 -0
- package/dist/features/compare/outside/state-sources.d.ts +76 -0
- package/dist/features/compare/outside/state-sources.js +164 -0
- package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
- package/dist/features/compare/outside/value-dictionary.js +227 -0
- package/dist/features/compare/visible-content.d.ts +20 -0
- package/dist/features/compare/visible-content.js +125 -0
- package/dist/features/crawl/crawl-empty.js +2 -1
- package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
- package/dist/features/crawl/crawl-exhaustive.js +138 -0
- package/dist/features/crawl/crawl-tools.d.ts +9 -2
- package/dist/features/crawl/crawl-tools.js +28 -12
- package/dist/features/crawl/crawl.d.ts +8 -9
- package/dist/features/crawl/crawl.js +61 -65
- package/dist/features/exhaust/app-coverage.d.ts +5 -1
- package/dist/features/exhaust/app-coverage.js +3 -1
- package/dist/features/exhaust/code-coverage.js +8 -4
- package/dist/features/exhaust/edge-cases.d.ts +133 -0
- package/dist/features/exhaust/edge-cases.js +310 -0
- package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
- package/dist/features/exhaust/edges/edge-drive.js +364 -0
- package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
- package/dist/features/exhaust/edges/edge-run.js +169 -0
- package/dist/features/exhaust/explorer.d.ts +87 -14
- package/dist/features/exhaust/explorer.js +159 -25
- package/dist/features/exhaust/failure-branches.d.ts +5 -0
- package/dist/features/exhaust/failure-branches.js +29 -11
- package/dist/features/exhaust/invariants.d.ts +112 -0
- package/dist/features/exhaust/invariants.js +188 -0
- package/dist/features/exhaust/ledger.d.ts +68 -0
- package/dist/features/exhaust/ledger.js +121 -13
- package/dist/features/exhaust/session-fold.d.ts +10 -0
- package/dist/features/exhaust/session-fold.js +46 -3
- package/dist/features/exhaust/session-port.d.ts +3 -1
- package/dist/features/exhaust/session-port.js +97 -28
- package/dist/features/grade/grade-answer.d.ts +99 -0
- package/dist/features/grade/grade-answer.js +112 -0
- package/dist/features/grade/grade-noise.d.ts +27 -0
- package/dist/features/grade/grade-noise.js +55 -0
- package/dist/features/grade/grade-renumber.d.ts +12 -0
- package/dist/features/grade/grade-renumber.js +86 -0
- package/dist/features/grade/grade-state.d.ts +50 -0
- package/dist/features/grade/grade-state.js +93 -0
- package/dist/features/grade/grade-suggest.d.ts +33 -0
- package/dist/features/grade/grade-suggest.js +98 -0
- package/dist/features/visual/visual-tools.js +66 -19
- package/dist/index.js +7 -4
- package/dist/judgement/capsule/causal-summary.js +9 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +16 -2
- package/dist/judgement/runs/runner-port.d.ts +2 -0
- package/dist/judgement/runs/runner-port.js +23 -5
- package/dist/language/flows/affected-tools.d.ts +1 -1
- package/dist/language/flows/affected-tools.js +49 -30
- package/dist/language/flows/change/git-changed.d.ts +2 -1
- package/dist/language/flows/change/git-changed.js +19 -2
- package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
- package/dist/language/flows/fields/flow-secret-field.js +77 -21
- package/dist/language/flows/flow-anchor.d.ts +60 -2
- package/dist/language/flows/flow-anchor.js +125 -20
- package/dist/language/flows/flow-classify.d.ts +20 -0
- package/dist/language/flows/flow-classify.js +90 -12
- package/dist/language/flows/flow-cross-step.d.ts +7 -1
- package/dist/language/flows/flow-cross-step.js +25 -1
- package/dist/language/flows/flow-journey.d.ts +38 -4
- package/dist/language/flows/flow-journey.js +138 -8
- package/dist/language/flows/flow-replay-run.d.ts +0 -16
- package/dist/language/flows/flow-replay-run.js +76 -14
- package/dist/language/flows/flow-replay-types.d.ts +4 -2
- package/dist/language/flows/flow-replay.d.ts +19 -1
- package/dist/language/flows/flow-replay.js +169 -52
- package/dist/language/flows/flow-replayable.js +7 -1
- package/dist/language/flows/flow-start-path.d.ts +34 -5
- package/dist/language/flows/flow-start-path.js +202 -15
- package/dist/language/flows/flow-step-runners.d.ts +14 -13
- package/dist/language/flows/flow-step-runners.js +184 -109
- package/dist/language/flows/flow-store-for-session.d.ts +26 -25
- package/dist/language/flows/flow-store-for-session.js +44 -0
- package/dist/language/flows/flow-tools.js +46 -33
- package/dist/language/flows/flows.js +176 -76
- package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
- package/dist/language/flows/recording/tape/recordings.js +71 -5
- package/dist/language/flows/replay.d.ts +19 -5
- package/dist/language/flows/replay.js +99 -7
- package/dist/language/flows/steps/page-steps.d.ts +68 -0
- package/dist/language/flows/steps/page-steps.js +198 -0
- package/dist/language/flows/suite/sequential-run.d.ts +68 -0
- package/dist/language/flows/suite/sequential-run.js +96 -0
- package/dist/language/flows/verify-change-tools.js +26 -15
- package/dist/memory/cloud/background-sync.d.ts +13 -0
- package/dist/memory/cloud/background-sync.js +24 -3
- package/dist/memory/cloud/sync-daemon.js +26 -27
- package/dist/memory/journal/attach-journal.js +2 -1
- package/dist/memory/journal/drive-flow.d.ts +7 -0
- package/dist/memory/journal/drive-flow.js +44 -13
- package/dist/memory/journal/journal-recorder.d.ts +4 -2
- package/dist/memory/journal/session-journal.d.ts +7 -0
- package/dist/memory/journal/session-journal.js +58 -10
- package/dist/memory/project/root-claims.d.ts +9 -0
- package/dist/memory/project/root-claims.js +9 -4
- package/dist/portal/input/download-capture.d.ts +7 -0
- package/dist/portal/input/download-capture.js +92 -0
- package/dist/portal/input/network-detail.d.ts +6 -0
- package/dist/portal/input/network-detail.js +12 -1
- package/dist/portal/input/network-mock-tools.js +49 -13
- package/dist/portal/input/network-mock.d.ts +8 -0
- package/dist/portal/input/network-mock.js +28 -9
- package/dist/portal/input/page-capture.d.ts +24 -0
- package/dist/portal/input/page-capture.js +33 -0
- package/dist/portal/input/real-input.d.ts +22 -20
- package/dist/portal/input/real-input.js +41 -31
- package/dist/portal/input/viewport-tools.d.ts +8 -0
- package/dist/portal/input/viewport-tools.js +75 -18
- package/dist/portal/pool/browser-pool.d.ts +11 -29
- package/dist/portal/pool/browser-pool.js +35 -17
- package/dist/portal/pool/context-permissions.d.ts +17 -3
- package/dist/portal/pool/context-permissions.js +36 -5
- package/dist/portal/pool/determinism.d.ts +52 -0
- package/dist/portal/pool/determinism.js +259 -0
- package/dist/portal/pool/lease-emulation.d.ts +34 -1
- package/dist/portal/pool/lease-emulation.js +18 -0
- package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
- package/dist/portal/pool/lease-marker-stamp.js +15 -1
- package/dist/portal/pool/lease-page-error.d.ts +44 -17
- package/dist/portal/pool/lease-page-error.js +147 -5
- package/dist/portal/pool/page-error-journal.d.ts +7 -1
- package/dist/portal/pool/page-error-journal.js +27 -0
- package/dist/portal/pool/playwright-launcher.js +51 -8
- package/dist/portal/pool/pool-contract.d.ts +27 -1
- package/dist/portal/pool/pool-lease.d.ts +36 -1
- package/dist/portal/pool/zero-install.d.ts +8 -0
- package/dist/portal/pool/zero-install.js +23 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
- package/dist/portal/session/dev-server/dev-command.js +61 -15
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
- package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
- package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
- package/dist/portal/session/dev-server/project-ports.js +62 -0
- package/dist/portal/session/facts/departed-network.d.ts +66 -0
- package/dist/portal/session/facts/departed-network.js +142 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -25
- package/dist/portal/session/no-session-next-action.d.ts +2 -0
- package/dist/portal/session/no-session-next-action.js +12 -3
- package/dist/portal/session/no-session-watch.d.ts +6 -0
- package/dist/portal/session/no-session-watch.js +38 -3
- package/dist/portal/session/serving-directory.d.ts +10 -0
- package/dist/portal/session/serving-directory.js +39 -8
- package/dist/portal/session/session-manager.d.ts +10 -0
- package/dist/portal/session/session-manager.js +174 -63
- package/dist/start-wiring.d.ts +2 -1
- package/dist/start-wiring.js +2 -1
- package/dist/surface/mcp/mcp.js +3 -2
- package/dist/surface/mcp/shared-params.d.ts +2 -0
- package/dist/surface/mcp/shared-params.js +31 -0
- package/dist/surface/tools/act/act-observation.d.ts +40 -2
- package/dist/surface/tools/act/act-observation.js +118 -12
- package/dist/surface/tools/act/act-preflight.js +1 -1
- package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
- package/dist/surface/tools/act/act-sequence-retry.js +61 -5
- package/dist/surface/tools/act/after-match.d.ts +4 -1
- package/dist/surface/tools/act/after-match.js +8 -1
- package/dist/surface/tools/act/already-true.js +1 -1
- package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
- package/dist/surface/tools/act/navigation/reload-result.js +25 -2
- package/dist/surface/tools/act/resolve-target.d.ts +15 -1
- package/dist/surface/tools/act/resolve-target.js +24 -1
- package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
- package/dist/surface/tools/act/sequence-grade.js +37 -26
- package/dist/surface/tools/act/settle-in-flight.js +27 -15
- package/dist/surface/tools/act-command.d.ts +23 -0
- package/dist/surface/tools/act-command.js +50 -0
- package/dist/surface/tools/act-sequence-tool.js +78 -50
- package/dist/surface/tools/act-tools.d.ts +1 -13
- package/dist/surface/tools/act-tools.js +74 -86
- package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
- package/dist/surface/tools/agent/build-staleness.js +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +16 -37
- package/dist/surface/tools/browser-tools.js +12 -6
- package/dist/surface/tools/compare-tools.d.ts +11 -0
- package/dist/surface/tools/compare-tools.js +94 -0
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.js +9 -2
- package/dist/surface/tools/error-recovery.d.ts +12 -1
- package/dist/surface/tools/error-recovery.js +91 -21
- package/dist/surface/tools/explore-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +3 -2
- package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
- package/dist/surface/tools/lease-coverage-tools.js +128 -0
- package/dist/surface/tools/lease-hint.d.ts +35 -1
- package/dist/surface/tools/lease-hint.js +42 -0
- package/dist/surface/tools/lease-readiness.d.ts +26 -0
- package/dist/surface/tools/lease-readiness.js +79 -0
- package/dist/surface/tools/lease-seed.d.ts +14 -1
- package/dist/surface/tools/lease-seed.js +35 -3
- package/dist/surface/tools/lease-tools.d.ts +3 -41
- package/dist/surface/tools/lease-tools.js +62 -110
- package/dist/surface/tools/lease-url-identity.d.ts +46 -0
- package/dist/surface/tools/lease-url-identity.js +97 -0
- package/dist/surface/tools/merge-tools.js +99 -2
- package/dist/surface/tools/merged-name-redirect.js +5 -1
- package/dist/surface/tools/observe-tools.js +15 -9
- package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
- package/dist/surface/tools/read/console-pre-attach.js +23 -0
- package/dist/surface/tools/read-tools.js +33 -30
- package/dist/surface/tools/real-input-attempt.d.ts +0 -8
- package/dist/surface/tools/real-input-attempt.js +143 -28
- package/dist/surface/tools/session-ports.d.ts +14 -0
- package/dist/surface/tools/session-ports.js +53 -0
- package/dist/surface/tools/tool-kit.d.ts +6 -0
- package/dist/surface/tools/tool-kit.js +2 -3
- package/dist/surface/tools/tool-surface.js +0 -1
- package/dist/surface/tools/tools.js +28 -5
- package/dist/surface/tools/type-spelling.d.ts +13 -0
- package/dist/surface/tools/type-spelling.js +36 -0
- package/dist/telemetry/agent-client.d.ts +6 -0
- package/dist/telemetry/agent-client.js +10 -0
- package/dist/telemetry/argument-shape.js +1 -0
- package/dist/telemetry/tool-refused.d.ts +6 -3
- package/dist/telemetry/tool-refused.js +8 -3
- package/docs/actions.mdx +3 -3
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/capabilities.mdx +61 -1
- package/docs/cli/affected.mdx +9 -7
- package/docs/cli/cloud.mdx +3 -3
- package/docs/cli/compare.mdx +94 -0
- package/docs/cli/doctor.mdx +1 -1
- package/docs/cli/grade.mdx +97 -0
- package/docs/cli/license.mdx +9 -7
- package/docs/cli/restart.mdx +7 -5
- package/docs/cli/rollback.mdx +5 -5
- package/docs/cli/stop.mdx +3 -3
- package/docs/cli/update.mdx +2 -2
- package/docs/cli/verify.mdx +23 -1
- package/docs/cli.mdx +33 -23
- package/docs/deploy-checks.md +1 -1
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +2 -2
- package/docs/environment.mdx +1 -1
- package/docs/flows.md +11 -1
- package/docs/frameworks.mdx +9 -9
- package/docs/getting-started.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +3 -3
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +1 -1
- package/docs/quickstart.mdx +68 -49
- package/docs/skill-setup.md +1 -1
- package/docs/skill.md +14 -13
- package/docs/telemetry.md +2 -2
- package/docs/tools/navigate.mdx +1 -1
- package/docs/tools/verify.mdx +4 -1
- package/docs/troubleshooting.mdx +1 -1
- package/docs/usage.md +17 -14
- package/package.json +13 -13
- package/dist/features/exhaust/exhaust-run.d.ts +0 -33
- package/dist/features/exhaust/exhaust-run.js +0 -82
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { type ScriptCoverage } from './js-coverage.js';
|
|
13
13
|
import type { Browser, Page } from 'playwright';
|
|
14
14
|
import { DriveError } from './drive-error.js';
|
|
15
|
-
import { ActionType, InputMode } from '@reticlehq/core';
|
|
15
|
+
import { ActionType, InputMode, type BoxFraction } from '@reticlehq/core';
|
|
16
16
|
import { type MockRule } from './network-mock.js';
|
|
17
17
|
import { type NetworkDetail } from './network-detail.js';
|
|
18
18
|
/** Viewport CSS-px box as returned by the INSPECT command (getBoundingClientRect). */
|
|
@@ -49,8 +49,12 @@ export type RealInputArgs = {
|
|
|
49
49
|
* for exactly as long as one driven in the page.
|
|
50
50
|
*/
|
|
51
51
|
holdMs?: number;
|
|
52
|
-
/** For drag: the resolved box of the drop-target ref (toRef). */
|
|
52
|
+
/** For drag: the resolved box of the drop-target ref (toRef), or the source box for `to` alone. */
|
|
53
53
|
toBox?: ElementBox;
|
|
54
|
+
/** For a pointer action: the point inside the box, as fractions of it. Absent means the centre. */
|
|
55
|
+
at?: BoxFraction;
|
|
56
|
+
/** For drag: the end point inside `toBox`, as fractions of it. Absent means its centre. */
|
|
57
|
+
to?: BoxFraction;
|
|
54
58
|
steps?: number;
|
|
55
59
|
};
|
|
56
60
|
/** What a provider reports for one driven gesture. Exported so a fake can build it by name. */
|
|
@@ -80,19 +84,8 @@ export interface RealInputResult {
|
|
|
80
84
|
*/
|
|
81
85
|
heldMs?: number;
|
|
82
86
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
fullPage?: boolean;
|
|
86
|
-
/** Restrict the capture to one element/region (viewport CSS px). */
|
|
87
|
-
clip?: ElementBox;
|
|
88
|
-
/**
|
|
89
|
-
* A live picture, as a JPEG at this quality: what a person is watching, taken as it is. The
|
|
90
|
-
* baseline tweaks (hiding the HUD, stopping animations) would make their window flicker each second.
|
|
91
|
-
*/
|
|
92
|
-
jpegQuality?: number;
|
|
93
|
-
/** One image pixel per CSS pixel, so a high-density display does not double a step screenshot. */
|
|
94
|
-
cssScale?: boolean;
|
|
95
|
-
}
|
|
87
|
+
export { capturePage, type ScreenshotOpts } from './page-capture.js';
|
|
88
|
+
import { type ScreenshotOpts } from './page-capture.js';
|
|
96
89
|
/** The capability surface reticle_act depends on. A FAKE implementing this is injected in tests. */
|
|
97
90
|
export interface RealInputProvider {
|
|
98
91
|
/** Whether a Playwright Page currently matches this SDK session URL. */
|
|
@@ -120,6 +113,14 @@ export interface RealInputProvider {
|
|
|
120
113
|
*/
|
|
121
114
|
storageState?(sessionUrl: string): Promise<unknown>;
|
|
122
115
|
applyStorageState?(sessionUrl: string, state: unknown): Promise<boolean>;
|
|
116
|
+
/**
|
|
117
|
+
* Close every page of the driven context except the one this provider drives, and say how many.
|
|
118
|
+
*
|
|
119
|
+
* A flow that opens another tab (a modifier-click on a link) leaves it behind; its page connects
|
|
120
|
+
* as a second session, and everything after that asks for "the" session and is refused. Optional:
|
|
121
|
+
* only a provider that owns its context can know which pages are its own.
|
|
122
|
+
*/
|
|
123
|
+
closeOtherPages?(): Promise<number>;
|
|
123
124
|
/**
|
|
124
125
|
* Install (or replace, or with [] clear) network-mock rules on the correlated page — stub a 500,
|
|
125
126
|
* force offline, delay a response — for deterministic error/edge-state testing. Returns true when
|
|
@@ -162,6 +163,11 @@ export declare function boxCenter(box: ElementBox): {
|
|
|
162
163
|
cx: number;
|
|
163
164
|
cy: number;
|
|
164
165
|
};
|
|
166
|
+
/** The point `at` names inside a box, or its centre when `at` is absent. */
|
|
167
|
+
export declare function boxPoint(box: ElementBox, at: BoxFraction | undefined): {
|
|
168
|
+
cx: number;
|
|
169
|
+
cy: number;
|
|
170
|
+
};
|
|
165
171
|
/**
|
|
166
172
|
* Which actions are driven by a native pointer. `press` is excluded on purpose — a key has no
|
|
167
173
|
* pointer, and widening this would change what every existing caller of it means.
|
|
@@ -191,11 +197,6 @@ now?: () => number): Promise<RealInputResult>;
|
|
|
191
197
|
export declare function withChromePassThrough<T>(page: {
|
|
192
198
|
evaluate: (script: string) => Promise<unknown>;
|
|
193
199
|
}, gesture: () => Promise<T>): Promise<T>;
|
|
194
|
-
/**
|
|
195
|
-
* Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
|
|
196
|
-
* screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
|
|
197
|
-
*/
|
|
198
|
-
export declare function capturePage(page: Page, opts: ScreenshotOpts): Promise<Uint8Array>;
|
|
199
200
|
interface CdpProviderOptions {
|
|
200
201
|
cdpUrl: string;
|
|
201
202
|
/** Injected so the settle delay is deterministic in tests; defaults to a real Node timer. */
|
|
@@ -297,6 +298,7 @@ export declare class LaunchedRealInputProvider implements OwnedRealInputProvider
|
|
|
297
298
|
* driven session had no fixture port at all, and every suite seeded nothing while reading as if it
|
|
298
299
|
* seeded everything. Same two methods, same contract, one page instead of a page lookup.
|
|
299
300
|
*/
|
|
301
|
+
closeOtherPages(): Promise<number>;
|
|
300
302
|
storageState(_sessionUrl: string): Promise<unknown>;
|
|
301
303
|
/**
|
|
302
304
|
* Put a captured state back: cookies through the context, per-origin storage through the page.
|
|
@@ -15,20 +15,33 @@ import { pressViaKeyboard } from './real-input-press.js';
|
|
|
15
15
|
import { stampedDriveUrl } from './drive-url-stamp.js';
|
|
16
16
|
import { launchChromium } from '../../launch-chromium.js';
|
|
17
17
|
import { pageViewportOptions } from '../../chromium-launch-options.js';
|
|
18
|
-
import { chromiumLaunchHint, gotoOptions
|
|
18
|
+
import { chromiumLaunchHint, gotoOptions } from '../pool/playwright-launcher.js';
|
|
19
19
|
import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
|
|
20
20
|
import { getSessionMetrics } from '../../telemetry/session-metrics.js';
|
|
21
21
|
import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
|
|
22
|
-
import { ActionType, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG, ClickButton, clickButtonFromArgs, InputMode, pressModifiersFromArgs, RETICLE_CHROME_PASS_THROUGH_SCRIPT, RETICLE_CHROME_RESTORE_POINTER_SCRIPT, } from '@reticlehq/core';
|
|
22
|
+
import { ActionType, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG, ClickButton, clickButtonFromArgs, InputMode, pointInBox, pressModifiersFromArgs, RETICLE_CHROME_PASS_THROUGH_SCRIPT, RETICLE_CHROME_RESTORE_POINTER_SCRIPT, } from '@reticlehq/core';
|
|
23
23
|
import { installNetworkMocks } from './network-mock.js';
|
|
24
24
|
import { attachNetworkDetail } from './network-detail.js';
|
|
25
|
+
import { attachDownloadCapture } from './download-capture.js';
|
|
25
26
|
import { injectedConnectArgs } from '../pool/zero-install.js';
|
|
27
|
+
import { EARLY_CAPTURE_ARG, queueEarlyConsole } from '../pool/lease-page-error.js';
|
|
28
|
+
// The screenshot capture lives in page-capture.ts; re-exported for existing importers.
|
|
29
|
+
export { capturePage } from './page-capture.js';
|
|
30
|
+
import { capturePage } from './page-capture.js';
|
|
26
31
|
export { DriveError };
|
|
27
32
|
export { unspellablePressReason } from './real-input-press.js';
|
|
28
33
|
/** Center of a viewport box in CSS px. Pure — unit-tested directly. */
|
|
29
34
|
export function boxCenter(box) {
|
|
30
35
|
return { cx: box.x + box.width / 2, cy: box.y + box.height / 2 };
|
|
31
36
|
}
|
|
37
|
+
/** The point `at` names inside a box, or its centre when `at` is absent. */
|
|
38
|
+
export function boxPoint(box, at) {
|
|
39
|
+
if (at === undefined) {
|
|
40
|
+
return boxCenter(box);
|
|
41
|
+
}
|
|
42
|
+
const p = pointInBox(box, at);
|
|
43
|
+
return { cx: p.x, cy: p.y };
|
|
44
|
+
}
|
|
32
45
|
/**
|
|
33
46
|
* Which actions are driven by a native pointer. `press` is excluded on purpose — a key has no
|
|
34
47
|
* pointer, and widening this would change what every existing caller of it means.
|
|
@@ -107,7 +120,7 @@ export async function withChromePassThrough(page, gesture) {
|
|
|
107
120
|
}
|
|
108
121
|
/** The pointer half of `performGesture`: every action that lands at a point. */
|
|
109
122
|
async function pointerGesture(page, action, box, args, sleep) {
|
|
110
|
-
const center =
|
|
123
|
+
const center = boxPoint(box, args.at);
|
|
111
124
|
const { cx, cy } = center;
|
|
112
125
|
const real = (performed) => ({
|
|
113
126
|
performed,
|
|
@@ -147,7 +160,7 @@ async function pointerGesture(page, action, box, args, sleep) {
|
|
|
147
160
|
if (args.toBox === undefined) {
|
|
148
161
|
return real(false);
|
|
149
162
|
}
|
|
150
|
-
const dst =
|
|
163
|
+
const dst = boxPoint(args.toBox, args.to);
|
|
151
164
|
const steps = args.steps ?? DEFAULT_DRAG_STEPS;
|
|
152
165
|
await page.mouse.move(cx, cy);
|
|
153
166
|
await page.mouse.down();
|
|
@@ -166,31 +179,6 @@ async function pointerGesture(page, action, box, args, sleep) {
|
|
|
166
179
|
}
|
|
167
180
|
return real(false);
|
|
168
181
|
}
|
|
169
|
-
/**
|
|
170
|
-
* Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
|
|
171
|
-
* screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
|
|
172
|
-
*/
|
|
173
|
-
export async function capturePage(page, opts) {
|
|
174
|
-
// A step capture injects nothing into the page, so Reticle's own hosts are hidden another way.
|
|
175
|
-
if (true === opts.cssScale) {
|
|
176
|
-
return captureWithChromeHidden({ evaluate: (script) => page.evaluate(script) }, () => shoot(page, opts));
|
|
177
|
-
}
|
|
178
|
-
return shoot(page, opts);
|
|
179
|
-
}
|
|
180
|
-
async function shoot(page, opts) {
|
|
181
|
-
const buf = await page.screenshot(opts.jpegQuality !== undefined
|
|
182
|
-
? {
|
|
183
|
-
type: 'jpeg',
|
|
184
|
-
quality: opts.jpegQuality,
|
|
185
|
-
...(true === opts.cssScale ? { scale: 'css' } : {}),
|
|
186
|
-
}
|
|
187
|
-
: opts.clip !== undefined
|
|
188
|
-
? { ...SCREENSHOT_DETERMINISM, clip: opts.clip }
|
|
189
|
-
: true === opts.fullPage
|
|
190
|
-
? { ...SCREENSHOT_DETERMINISM, fullPage: true }
|
|
191
|
-
: { ...SCREENSHOT_DETERMINISM });
|
|
192
|
-
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
193
|
-
}
|
|
194
182
|
const nodeSleep = (ms) => new Promise((resolve) => {
|
|
195
183
|
setTimeout(resolve, ms);
|
|
196
184
|
});
|
|
@@ -433,9 +421,19 @@ export class LaunchedRealInputProvider {
|
|
|
433
421
|
...(this.#storageState !== undefined ? { storageState: this.#storageState } : {}),
|
|
434
422
|
});
|
|
435
423
|
this.#page = page;
|
|
424
|
+
// Before goto: a console error the page logs while it loads comes before its SDK connects.
|
|
425
|
+
await page.addInitScript(queueEarlyConsole, EARLY_CAPTURE_ARG);
|
|
436
426
|
// Capture CDP-authoritative response detail into the driven session's journal (best-effort).
|
|
437
|
-
|
|
438
|
-
|
|
427
|
+
const sink = this.#onNetworkDetail;
|
|
428
|
+
if (sink !== undefined) {
|
|
429
|
+
attachNetworkDetail(page, sink);
|
|
430
|
+
// A server-sent download never reaches the page's network events; CDP request interception
|
|
431
|
+
// does see it (download-capture.ts). Attached before the first navigation, best-effort.
|
|
432
|
+
await page
|
|
433
|
+
.context()
|
|
434
|
+
.newCDPSession(page)
|
|
435
|
+
.then((cdp) => attachDownloadCapture(cdp, sink, () => page.url()))
|
|
436
|
+
.catch(() => undefined);
|
|
439
437
|
}
|
|
440
438
|
try {
|
|
441
439
|
// Same navigation rule as the pool, and for the same measured reason: Playwright's default
|
|
@@ -538,6 +536,18 @@ export class LaunchedRealInputProvider {
|
|
|
538
536
|
* driven session had no fixture port at all, and every suite seeded nothing while reading as if it
|
|
539
537
|
* seeded everything. Same two methods, same contract, one page instead of a page lookup.
|
|
540
538
|
*/
|
|
539
|
+
async closeOtherPages() {
|
|
540
|
+
const page = this.#livePage();
|
|
541
|
+
if (page === undefined) {
|
|
542
|
+
return 0;
|
|
543
|
+
}
|
|
544
|
+
const others = page
|
|
545
|
+
.context()
|
|
546
|
+
.pages()
|
|
547
|
+
.filter((other) => other !== page && !other.isClosed());
|
|
548
|
+
await Promise.all(others.map((other) => other.close()));
|
|
549
|
+
return others.length;
|
|
550
|
+
}
|
|
541
551
|
async storageState(_sessionUrl) {
|
|
542
552
|
const page = this.#livePage();
|
|
543
553
|
if (page === undefined) {
|
|
@@ -10,6 +10,10 @@ export declare const REDUCED_MOTION_ARG: z.ZodOptional<z.ZodEnum<{
|
|
|
10
10
|
reduce: "reduce";
|
|
11
11
|
"no-preference": "no-preference";
|
|
12
12
|
}>>;
|
|
13
|
+
export declare const POINTER_ARG: z.ZodOptional<z.ZodEnum<{
|
|
14
|
+
fine: "fine";
|
|
15
|
+
coarse: "coarse";
|
|
16
|
+
}>>;
|
|
13
17
|
export declare const LEASE_EMULATE_ARG: z.ZodOptional<z.ZodObject<{
|
|
14
18
|
colorScheme: z.ZodOptional<z.ZodEnum<{
|
|
15
19
|
light: "light";
|
|
@@ -22,6 +26,10 @@ export declare const LEASE_EMULATE_ARG: z.ZodOptional<z.ZodObject<{
|
|
|
22
26
|
}>>;
|
|
23
27
|
width: z.ZodOptional<z.ZodNumber>;
|
|
24
28
|
height: z.ZodOptional<z.ZodNumber>;
|
|
29
|
+
pointer: z.ZodOptional<z.ZodEnum<{
|
|
30
|
+
fine: "fine";
|
|
31
|
+
coarse: "coarse";
|
|
32
|
+
}>>;
|
|
25
33
|
}, z.core.$strict>>;
|
|
26
34
|
/** The media part of what a caller asked for, or undefined when it asked for none. */
|
|
27
35
|
export declare function mediaOf(raw: {
|
|
@@ -4,11 +4,12 @@ import { ReticleTool } from '@reticlehq/core';
|
|
|
4
4
|
import { sessionIdShape } from '../../surface/tools/tool-kit.js';
|
|
5
5
|
import { MAX_VIEWPORT_PX, MIN_VIEWPORT_PX, viewportPxSchema, } from '../../surface/tools/args/numeric-bounds.js';
|
|
6
6
|
import { asString } from '@reticlehq/core';
|
|
7
|
-
import {
|
|
7
|
+
import { HEADLESS_VIEWPORT } from '../../chromium-launch-options.js';
|
|
8
|
+
import { ColorScheme, Pointer, ReducedMotion, } from '../pool/lease-emulation.js';
|
|
8
9
|
// ── Media and width, as a tool call names them (reticle_viewport here, reticle_lease acquire) ──
|
|
9
|
-
/**
|
|
10
|
-
const DEFAULT_WIDTH =
|
|
11
|
-
const DEFAULT_HEIGHT =
|
|
10
|
+
/** The side a caller did not name is the page size a headless lease already opens with. */
|
|
11
|
+
const DEFAULT_WIDTH = HEADLESS_VIEWPORT.width;
|
|
12
|
+
const DEFAULT_HEIGHT = HEADLESS_VIEWPORT.height;
|
|
12
13
|
export const COLOR_SCHEME_ARG = z
|
|
13
14
|
.enum([ColorScheme.LIGHT, ColorScheme.DARK, ColorScheme.NO_PREFERENCE])
|
|
14
15
|
.optional()
|
|
@@ -17,16 +18,24 @@ export const REDUCED_MOTION_ARG = z
|
|
|
17
18
|
.enum([ReducedMotion.REDUCE, ReducedMotion.NO_PREFERENCE])
|
|
18
19
|
.optional()
|
|
19
20
|
.describe('prefers-reduced-motion for this page: reduce or no-preference.');
|
|
21
|
+
export const POINTER_ARG = z
|
|
22
|
+
.enum([Pointer.FINE, Pointer.COARSE])
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('The primary pointer this page reports to @media (pointer: ...): fine (a mouse) or coarse (touch). ' +
|
|
25
|
+
'Settable only when the lease opens, because touch support is fixed for a browser context.');
|
|
20
26
|
const EmulateSchema = z
|
|
21
27
|
.object({
|
|
22
28
|
colorScheme: COLOR_SCHEME_ARG,
|
|
23
29
|
reducedMotion: REDUCED_MOTION_ARG,
|
|
24
30
|
width: viewportPxSchema.optional(),
|
|
25
31
|
height: viewportPxSchema.optional(),
|
|
32
|
+
pointer: POINTER_ARG,
|
|
26
33
|
})
|
|
27
34
|
.strict();
|
|
28
|
-
export const LEASE_EMULATE_ARG = EmulateSchema.optional().describe('Open the lease with these set before the first paint: { colorScheme, reducedMotion, width, height }. ' +
|
|
29
|
-
'A side you leave out keeps its default (
|
|
35
|
+
export const LEASE_EMULATE_ARG = EmulateSchema.optional().describe('Open the lease with these set before the first paint: { colorScheme, reducedMotion, width, height, pointer }. ' +
|
|
36
|
+
'A side you leave out keeps its default (1440 by 900). Change the media and the size later with ' +
|
|
37
|
+
'reticle_viewport; `pointer` cannot change on an open lease, so a touch-only rule needs a lease ' +
|
|
38
|
+
'acquired with pointer: "coarse".');
|
|
30
39
|
/** The media part of what a caller asked for, or undefined when it asked for none. */
|
|
31
40
|
export function mediaOf(raw) {
|
|
32
41
|
const parsed = EmulateSchema.pick({ colorScheme: true, reducedMotion: true }).parse({
|
|
@@ -41,21 +50,27 @@ export function mediaOf(raw) {
|
|
|
41
50
|
}
|
|
42
51
|
/** A lease's `emulate` argument, read; a value it does not know is refused by name. */
|
|
43
52
|
export function leaseEmulationOf(raw) {
|
|
44
|
-
if (raw === undefined)
|
|
53
|
+
if (raw === undefined) {
|
|
45
54
|
return undefined;
|
|
55
|
+
}
|
|
46
56
|
const parsed = EmulateSchema.safeParse(raw);
|
|
47
57
|
if (!parsed.success) {
|
|
48
58
|
const field = parsed.error.issues[0]?.path.join('.') ?? 'emulate';
|
|
49
59
|
throw new Error(`emulate.${field}: ${parsed.error.issues[0]?.message ?? 'not valid'}`);
|
|
50
60
|
}
|
|
51
|
-
const { width, height } = parsed.data;
|
|
61
|
+
const { width, height, pointer } = parsed.data;
|
|
52
62
|
const media = mediaOf(parsed.data);
|
|
53
63
|
const viewport = width === undefined && height === undefined
|
|
54
64
|
? undefined
|
|
55
65
|
: { width: width ?? DEFAULT_WIDTH, height: height ?? DEFAULT_HEIGHT };
|
|
56
|
-
if (media === undefined && viewport === undefined)
|
|
66
|
+
if (media === undefined && viewport === undefined && pointer === undefined) {
|
|
57
67
|
return undefined;
|
|
58
|
-
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
...media,
|
|
71
|
+
...(viewport === undefined ? {} : { viewport }),
|
|
72
|
+
...(pointer === undefined ? {} : { pointer }),
|
|
73
|
+
};
|
|
59
74
|
}
|
|
60
75
|
/** Bounds so a viewport request stays sane (and a typo can't ask for a 1px or 100k-px window). */
|
|
61
76
|
const MIN_DIM = MIN_VIEWPORT_PX;
|
|
@@ -66,6 +81,25 @@ function viewportProvider(deps) {
|
|
|
66
81
|
? p
|
|
67
82
|
: undefined;
|
|
68
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether this caller holds the session as a lease. Another agent's lease is never this caller's
|
|
86
|
+
* to change (#1226).
|
|
87
|
+
*/
|
|
88
|
+
function holdsLease(deps, sessionId) {
|
|
89
|
+
return true === deps.pool?.heldBy(sessionId, deps.attachId);
|
|
90
|
+
}
|
|
91
|
+
/** Whether this call can resize the session: a lease of this caller, or a driven page. */
|
|
92
|
+
async function canResize(deps, session) {
|
|
93
|
+
if (holdsLease(deps, session.id)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
// A driven browser resizes only a page it has. Having a driven browser is not enough.
|
|
97
|
+
const driven = viewportProvider(deps);
|
|
98
|
+
if (driven === undefined) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return driven.isAvailableFor(session.url);
|
|
102
|
+
}
|
|
69
103
|
export const VIEWPORT_TOOLS = [
|
|
70
104
|
{
|
|
71
105
|
name: ReticleTool.VIEWPORT,
|
|
@@ -73,12 +107,14 @@ export const VIEWPORT_TOOLS = [
|
|
|
73
107
|
'reproducible across machines — the missing piece of CI-stable visual regression, alongside ' +
|
|
74
108
|
'reticle_visual_diff `masks` and a frozen clock (reticle_clock). Set it once before reticle_screenshot / ' +
|
|
75
109
|
'reticle_visual_diff. On a lease it also sets prefers-color-scheme and prefers-reduced-motion; ' +
|
|
76
|
-
'give only those to change the media and keep the size.
|
|
110
|
+
'give only those to change the media and keep the size. A `pointer` is refused here: touch support is fixed when a lease opens, so ask for it with reticle_lease acquire `emulate.pointer`. ' +
|
|
111
|
+
'Returns { applied, width, height, observed } or the no-provider recommendation; `observed` is the size the leased page reports after the resize.',
|
|
77
112
|
inputSchema: {
|
|
78
113
|
width: viewportPxSchema.optional().describe('Viewport width in CSS px (e.g. 1280).'),
|
|
79
114
|
height: viewportPxSchema.optional().describe('Viewport height in CSS px (e.g. 800).'),
|
|
80
115
|
colorScheme: COLOR_SCHEME_ARG,
|
|
81
116
|
reducedMotion: REDUCED_MOTION_ARG,
|
|
117
|
+
pointer: POINTER_ARG,
|
|
82
118
|
...sessionIdShape,
|
|
83
119
|
},
|
|
84
120
|
outputSchema: {
|
|
@@ -88,14 +124,27 @@ export const VIEWPORT_TOOLS = [
|
|
|
88
124
|
observed: z.object({ width: z.number(), height: z.number() }).optional(),
|
|
89
125
|
colorScheme: z.string().optional(),
|
|
90
126
|
reducedMotion: z.string().optional(),
|
|
127
|
+
pointer: z.string().optional(),
|
|
91
128
|
ok: z.boolean().optional(),
|
|
92
129
|
reason: z.string().optional(),
|
|
93
130
|
recommendation: z.string().optional(),
|
|
94
131
|
},
|
|
95
132
|
handler: async (deps, args) => {
|
|
133
|
+
// Touch support is a Playwright context option and a lease is one context, so a pointer can
|
|
134
|
+
// only be chosen when the lease opens. Said out loud instead of dropped: a silently ignored
|
|
135
|
+
// `pointer` would hand back a verdict about a touch-only rule that was taken with a mouse.
|
|
136
|
+
if (args['pointer'] !== undefined) {
|
|
137
|
+
return {
|
|
138
|
+
applied: false,
|
|
139
|
+
ok: false,
|
|
140
|
+
reason: POINTER_IS_FIXED,
|
|
141
|
+
recommendation: POINTER_NEEDS_A_NEW_LEASE,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
96
144
|
const media = mediaOf(args);
|
|
97
145
|
if (media !== undefined && args['width'] === undefined && args['height'] === undefined) {
|
|
98
|
-
|
|
146
|
+
// Only a lease can set the media.
|
|
147
|
+
const session = await deps.sessions.resolveServing(asString(args['sessionId']), (s) => holdsLease(deps, s.id));
|
|
99
148
|
if (true === (await deps.pool?.emulateLease(session.id, media))) {
|
|
100
149
|
// No size was asked for, so answer the one the lease has; 0 by 0 reads as a broken
|
|
101
150
|
// viewport (#1902). When the page cannot be read, leave the size out.
|
|
@@ -106,7 +155,7 @@ export const VIEWPORT_TOOLS = [
|
|
|
106
155
|
}
|
|
107
156
|
const width = clampDim('width', args['width']);
|
|
108
157
|
const height = clampDim('height', args['height']);
|
|
109
|
-
const session = deps.sessions.
|
|
158
|
+
const session = await deps.sessions.resolveServing(asString(args['sessionId']), (s) => canResize(deps, s));
|
|
110
159
|
// A lease is resized as the lease. The driven provider resizes its own page, whatever
|
|
111
160
|
// session was named, so taking it first resized the wrong tab and said `applied: true`.
|
|
112
161
|
const driven = viewportProvider(deps);
|
|
@@ -114,10 +163,12 @@ export const VIEWPORT_TOOLS = [
|
|
|
114
163
|
if (provider !== undefined) {
|
|
115
164
|
const applied = await provider.setViewport(session.url, { width, height });
|
|
116
165
|
// The driven page takes the size; its media stay as the person's browser has them.
|
|
117
|
-
if (applied && media === undefined)
|
|
166
|
+
if (applied && media === undefined) {
|
|
118
167
|
return { applied: true, width, height };
|
|
119
|
-
|
|
168
|
+
}
|
|
169
|
+
if (applied) {
|
|
120
170
|
return { applied: true, width, height, recommendation: MEDIA_NEEDS_A_LEASE };
|
|
171
|
+
}
|
|
121
172
|
}
|
|
122
173
|
// A lease is a Playwright-owned page, so the resize was always possible — this tool just had
|
|
123
174
|
// no route to it. Tried after the driven provider, on the same rule reticle_network_mock
|
|
@@ -128,16 +179,18 @@ export const VIEWPORT_TOOLS = [
|
|
|
128
179
|
// only mounts under a breakpoint — could not be driven at a desktop viewport at all, and the
|
|
129
180
|
// recommendation printed alongside the refusal asked the reader to install a second browser.
|
|
130
181
|
const leased = await deps.pool?.setViewportLease(session.id, { width, height });
|
|
131
|
-
if (true !== leased)
|
|
182
|
+
if (true !== leased) {
|
|
132
183
|
return refused();
|
|
184
|
+
}
|
|
133
185
|
// What the page reports after the resize, so `applied` is a reading and not a hope.
|
|
134
186
|
const observed = await deps.pool?.viewportOfLease(session.id);
|
|
135
187
|
if (observed !== undefined && (observed.width !== width || observed.height !== height)) {
|
|
136
188
|
return { applied: false, width, height, observed, ok: false, reason: VIEWPORT_NOT_TAKEN };
|
|
137
189
|
}
|
|
138
190
|
const seen = observed === undefined ? {} : { observed };
|
|
139
|
-
if (media === undefined)
|
|
191
|
+
if (media === undefined) {
|
|
140
192
|
return { applied: true, width, height, ...seen };
|
|
193
|
+
}
|
|
141
194
|
if (true === (await deps.pool?.emulateLease(session.id, media))) {
|
|
142
195
|
return { applied: true, width, height, ...seen, ...media };
|
|
143
196
|
}
|
|
@@ -147,6 +200,9 @@ export const VIEWPORT_TOOLS = [
|
|
|
147
200
|
];
|
|
148
201
|
/** The resize went through, but the page still reports another size. */
|
|
149
202
|
const VIEWPORT_NOT_TAKEN = 'the page still reports the size in `observed` after the resize, so the size was not applied';
|
|
203
|
+
/** Touch support rides on the browser context, so an open lease's pointer cannot be changed. */
|
|
204
|
+
const POINTER_IS_FIXED = "a lease's pointer is fixed when its context opens, so it cannot be changed on a live lease";
|
|
205
|
+
const POINTER_NEEDS_A_NEW_LEASE = 'Acquire a new lease with emulate.pointer: reticle_lease { action: "acquire", emulate: { pointer: "coarse" } }, then call this again with its sessionId.';
|
|
150
206
|
/** prefers-color-scheme and prefers-reduced-motion are set on a leased page only. */
|
|
151
207
|
const MEDIA_NEEDS_A_LEASE = 'Media features are set on a leased tab: open one with reticle_lease { action: "acquire", emulate }, then call this again with its sessionId.';
|
|
152
208
|
function refused() {
|
|
@@ -167,8 +223,9 @@ function refused() {
|
|
|
167
223
|
* snapshot, click and screenshot on that page read as broken (#1118).
|
|
168
224
|
*/
|
|
169
225
|
function clampDim(key, value) {
|
|
170
|
-
if (value === undefined)
|
|
226
|
+
if (value === undefined) {
|
|
171
227
|
return MIN_DIM;
|
|
228
|
+
}
|
|
172
229
|
if ('number' !== typeof value || !Number.isFinite(value)) {
|
|
173
230
|
throw new Error(`${ReticleTool.VIEWPORT}: \`${key}\` must be a number of CSS px, got ${JSON.stringify(value)} — NOT applied`);
|
|
174
231
|
}
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* The browser is injected via a `Launcher` so all lifecycle logic is testable without real Chromium;
|
|
10
10
|
* the thin Playwright adapter that satisfies these interfaces lives separately.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import type { Launcher, PooledMockRule, ScreenshotOptions } from './pool-contract.js';
|
|
12
|
+
import type { Launcher, PooledCoverage, PooledMockRule, ScreenshotOptions } from './pool-contract.js';
|
|
14
13
|
import type { RealInputArgs, RealInputResult } from '../input/real-input.js';
|
|
15
|
-
import { type
|
|
14
|
+
import { type LeaseMedia } from './lease-emulation.js';
|
|
15
|
+
import { type DeterminismStep } from './determinism.js';
|
|
16
16
|
export type { InitScriptHandle, Launcher, PooledBrowser, PooledContext, PooledCookie, PooledDialog, PooledMockRule, PooledPage, PooledPageError, } from './pool-contract.js';
|
|
17
|
-
import { type BrowserPoolOptions, type Lease } from './pool-lease.js';
|
|
17
|
+
import { type AcquireOptions, type BrowserPoolOptions, type Lease } from './pool-lease.js';
|
|
18
18
|
export { DEFAULT_LEASE_TTL_MS, NO_HEADED_BROWSER, ORPHANED_LEASE_GRACE_MS, PoolAtCapacityError, } from './pool-lease.js';
|
|
19
19
|
export type { Lease } from './pool-lease.js';
|
|
20
20
|
/**
|
|
@@ -119,6 +119,8 @@ export declare class BrowserPool {
|
|
|
119
119
|
* Touches the lease like any other tool call, so hovering keeps it alive.
|
|
120
120
|
*/
|
|
121
121
|
hoverLease(sessionId: string, x: number, y: number): Promise<boolean>;
|
|
122
|
+
/** Reload a lease with the browser cache ignored, or false when this is not a lease or its page cannot. */
|
|
123
|
+
hardReloadLease(sessionId: string): Promise<boolean>;
|
|
122
124
|
/** Set a live lease's media features, or false when this is not a lease or its page cannot. */
|
|
123
125
|
emulateLease(sessionId: string, media: LeaseMedia): Promise<boolean>;
|
|
124
126
|
/** Whether this lease's page is still loading. False for a non-lease, or a page that cannot say. */
|
|
@@ -155,6 +157,10 @@ export declare class BrowserPool {
|
|
|
155
157
|
height: number;
|
|
156
158
|
} | undefined>;
|
|
157
159
|
setMocksLease(sessionId: string, rules: readonly PooledMockRule[]): Promise<boolean>;
|
|
160
|
+
/** Step a deterministic lease's clock by `frames` frames; undefined when it is not one. */
|
|
161
|
+
stepLease(sessionId: string, frames: number): Promise<DeterminismStep | undefined>;
|
|
162
|
+
/** The JS coverage of a lease's page, or undefined when this is not a lease or cannot measure. */
|
|
163
|
+
coverageOf(sessionId: string): PooledCoverage | undefined;
|
|
158
164
|
/** Guard a leased page's requests, or false when this is not a lease or its page cannot. */
|
|
159
165
|
guardLease(sessionId: string, decide: (method: string, url: string) => string | undefined): Promise<boolean>;
|
|
160
166
|
/** What a lease was granted when it opened, or undefined for a session that is not a lease. */
|
|
@@ -207,31 +213,7 @@ export declare class BrowserPool {
|
|
|
207
213
|
* waits FIFO until a slot frees (or until `signal` aborts, if provided), unless `whenFull` is
|
|
208
214
|
* `'refuse'`: then it throws {@link PoolAtCapacityError} at once and claims nothing.
|
|
209
215
|
*/
|
|
210
|
-
acquire(url: string, opts?:
|
|
211
|
-
signal?: AbortSignal;
|
|
212
|
-
/**
|
|
213
|
-
* `'wait'` (the default) queues behind the cap. `'refuse'` is for a caller that has a person
|
|
214
|
-
* or an agent waiting on the answer and no way to see a queue: it is told the pool is full.
|
|
215
|
-
*/
|
|
216
|
-
whenFull?: 'wait' | 'refuse';
|
|
217
|
-
sessionId?: string;
|
|
218
|
-
seedStorage?: SeedStorage;
|
|
219
|
-
owner?: string;
|
|
220
|
-
/** In a browser with a window the person can watch. */
|
|
221
|
-
headed?: boolean;
|
|
222
|
-
/**
|
|
223
|
-
* JavaScript installed before any page script runs, for this lease only. A view built for a
|
|
224
|
-
* desktop shell calls bindings the shell injects (`window.go`, `window.runtime`, a preload
|
|
225
|
-
* object) on mount; a stub here is what lets it reach its data path in a leased tab.
|
|
226
|
-
*/
|
|
227
|
-
initScript?: string;
|
|
228
|
-
/** Granted on the lease's origin before the first navigation, so the first render sees them. */
|
|
229
|
-
permissions?: readonly string[];
|
|
230
|
-
/** Accept a self-signed or local-CA dev certificate for this lease's context only (#1255). */
|
|
231
|
-
ignoreHTTPSErrors?: boolean;
|
|
232
|
-
/** Media and viewport set on the context, so the first paint already has them. */
|
|
233
|
-
emulate?: LeaseEmulation;
|
|
234
|
-
}): Promise<Lease>;
|
|
216
|
+
acquire(url: string, opts?: AcquireOptions): Promise<Lease>;
|
|
235
217
|
/**
|
|
236
218
|
* The websocket address this lease's page reported it could not reach, if it reported one.
|
|
237
219
|
*
|
|
@@ -16,7 +16,8 @@ import { seedStorageInto, targetOriginOf } from './storage-seed.js';
|
|
|
16
16
|
import { installLeaseMarker, leaseMarkerOf } from './lease-marker-stamp.js';
|
|
17
17
|
import { grantLeasePermissions, grantLocalNetworkAccess, readNotificationPermission, widenLeaseCsp, } from './context-permissions.js';
|
|
18
18
|
import { readViewport } from './lease-emulation.js';
|
|
19
|
-
import {
|
|
19
|
+
import { injectReaderInto } from './zero-install.js';
|
|
20
|
+
import { stepPage } from './determinism.js';
|
|
20
21
|
import { DEFAULT_LEASE_TTL_MS, DEFAULT_NAV_TIMEOUT_MS, INIT_SCRIPT_UNSUPPORTED, MAX_REMEMBERED_REAPED_LEASES, NO_HEADED_BROWSER, ORPHANED_LEASE_GRACE_MS, navStatusOf, pageLoading, PoolAtCapacityError, } from './pool-lease.js';
|
|
21
22
|
import { listenForPageErrors, reportLeaseClosed, } from './lease-page-error.js';
|
|
22
23
|
export { DEFAULT_LEASE_TTL_MS, NO_HEADED_BROWSER, ORPHANED_LEASE_GRACE_MS, PoolAtCapacityError, } from './pool-lease.js';
|
|
@@ -77,7 +78,7 @@ export class BrowserPool {
|
|
|
77
78
|
this.#navTimeout = opts.navTimeoutMs ?? DEFAULT_NAV_TIMEOUT_MS;
|
|
78
79
|
this.#zeroInstall = opts.zeroInstallScript;
|
|
79
80
|
this.#daemonOrigin = opts.daemonOrigin;
|
|
80
|
-
this.#pageErrorHooks =
|
|
81
|
+
this.#pageErrorHooks = opts;
|
|
81
82
|
}
|
|
82
83
|
/** The TTL configured for leases — how long an untouched lease lives before the reaper reclaims it. */
|
|
83
84
|
leaseTtlMs() {
|
|
@@ -303,6 +304,21 @@ export class BrowserPool {
|
|
|
303
304
|
return false;
|
|
304
305
|
}
|
|
305
306
|
}
|
|
307
|
+
/** Reload a lease with the browser cache ignored, or false when this is not a lease or its page cannot. */
|
|
308
|
+
async hardReloadLease(sessionId) {
|
|
309
|
+
const lease = this.#active.get(this.#leaseIdOf(sessionId));
|
|
310
|
+
if (lease?.page.reloadIgnoringCache === undefined) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
this.touch(sessionId);
|
|
314
|
+
try {
|
|
315
|
+
await lease.page.reloadIgnoringCache();
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
306
322
|
/** Set a live lease's media features, or false when this is not a lease or its page cannot. */
|
|
307
323
|
async emulateLease(sessionId, media) {
|
|
308
324
|
const lease = this.#active.get(this.#leaseIdOf(sessionId));
|
|
@@ -395,6 +411,17 @@ export class BrowserPool {
|
|
|
395
411
|
return false;
|
|
396
412
|
}
|
|
397
413
|
}
|
|
414
|
+
/** Step a deterministic lease's clock by `frames` frames; undefined when it is not one. */
|
|
415
|
+
stepLease(sessionId, frames) {
|
|
416
|
+
const page = this.#active.get(this.#leaseIdOf(sessionId))?.page;
|
|
417
|
+
this.touch(sessionId);
|
|
418
|
+
return page === undefined ? Promise.resolve(undefined) : stepPage(page, frames);
|
|
419
|
+
}
|
|
420
|
+
/** The JS coverage of a lease's page, or undefined when this is not a lease or cannot measure. */
|
|
421
|
+
coverageOf(sessionId) {
|
|
422
|
+
this.touch(sessionId);
|
|
423
|
+
return this.#active.get(this.#leaseIdOf(sessionId))?.page.jsCoverage;
|
|
424
|
+
}
|
|
398
425
|
/** Guard a leased page's requests, or false when this is not a lease or its page cannot. */
|
|
399
426
|
async guardLease(sessionId, decide) {
|
|
400
427
|
const lease = this.#active.get(this.#leaseIdOf(sessionId));
|
|
@@ -513,6 +540,7 @@ export class BrowserPool {
|
|
|
513
540
|
const browser = await this.#ensureBrowser(slot);
|
|
514
541
|
const contextOpts = {
|
|
515
542
|
...(true === opts.ignoreHTTPSErrors ? { ignoreHTTPSErrors: true } : {}),
|
|
543
|
+
...(true === opts.jsCoverage ? { jsCoverage: true } : {}),
|
|
516
544
|
...opts.emulate,
|
|
517
545
|
};
|
|
518
546
|
context = await browser.newContext(0 < Object.keys(contextOpts).length ? contextOpts : undefined);
|
|
@@ -552,9 +580,8 @@ export class BrowserPool {
|
|
|
552
580
|
pending = dialled;
|
|
553
581
|
} // logged during goto, before the lease is registered below
|
|
554
582
|
});
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
// just ones opened after the lease is registered.
|
|
583
|
+
// Listen before goto, as onConsole does: a dialog in an onload handler is dismissed too. The SDK
|
|
584
|
+
// answers a dialog from its own input first; the hook records this "no" for the verdict (#1350).
|
|
558
585
|
page.onDialog?.((dialog) => {
|
|
559
586
|
const active = this.#active.get(sessionId);
|
|
560
587
|
if (active !== undefined) {
|
|
@@ -564,9 +591,10 @@ export class BrowserPool {
|
|
|
564
591
|
pendingDialogMessage = dialog.message;
|
|
565
592
|
}
|
|
566
593
|
void dialog.dismiss();
|
|
594
|
+
this.#pageErrorHooks?.onDialog?.(sessionId, dialog);
|
|
567
595
|
});
|
|
568
596
|
// Listener on BEFORE goto — an import-time throw lands during it (#1966).
|
|
569
|
-
listenForPageErrors(page, sessionId, this.#pageErrorHooks);
|
|
597
|
+
await listenForPageErrors(page, sessionId, this.#pageErrorHooks);
|
|
570
598
|
const marker = leaseMarkerOf(url);
|
|
571
599
|
if (marker?.session === sessionId) {
|
|
572
600
|
await installLeaseMarker(page, sessionId, targetOriginOf(url), marker.project);
|
|
@@ -620,17 +648,7 @@ export class BrowserPool {
|
|
|
620
648
|
url,
|
|
621
649
|
...(navStatus !== undefined ? { navStatus } : {}),
|
|
622
650
|
release: () => this.#release(sessionId),
|
|
623
|
-
injectReader:
|
|
624
|
-
const script = this.#zeroInstall?.(url);
|
|
625
|
-
if (script === undefined || page.evaluate === undefined) {
|
|
626
|
-
return false;
|
|
627
|
-
}
|
|
628
|
-
await page.addInitScript?.(script);
|
|
629
|
-
if (READER_NOT_HTML === (await page.evaluate(script))) {
|
|
630
|
-
throw new NotHtmlDocumentError();
|
|
631
|
-
}
|
|
632
|
-
return true;
|
|
633
|
-
},
|
|
651
|
+
injectReader: () => injectReaderInto(page, this.#zeroInstall?.(url)),
|
|
634
652
|
isLoading: () => pageLoading(page),
|
|
635
653
|
};
|
|
636
654
|
}
|