@reticlehq/server 3.8.1 → 3.9.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/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-verdict.js +34 -7
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +45 -4
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -17
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +46 -3
- package/dist/command/cli/cli-parse.d.ts +5 -2
- package/dist/command/cli/cli-parse.js +10 -5
- package/dist/command/cli/cli-usage.js +3 -2
- package/dist/command/cli/cli-verify.d.ts +6 -1
- package/dist/command/cli/cli-verify.js +32 -13
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +2 -0
- package/dist/command/cli/ports/port-holder.js +11 -3
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +50 -17
- package/dist/command/cli/status-fields.js +17 -3
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli.js +70 -33
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon.d.ts +10 -3
- package/dist/command/daemon/daemon.js +17 -6
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +130 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +4 -13
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +57 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/setup-command.js +30 -12
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +13 -2
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +2 -81
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +14 -5
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/flow-store-for-session.js +7 -5
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
- package/dist/language/flows/recording/tape/recordings.js +41 -44
- package/dist/language/flows/replay.d.ts +17 -7
- package/dist/language/flows/replay.js +95 -44
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/drive-error.d.ts +6 -0
- package/dist/portal/input/drive-error.js +9 -0
- package/dist/portal/input/network-mock.js +3 -2
- package/dist/portal/input/real-input-press.d.ts +34 -0
- package/dist/portal/input/real-input-press.js +150 -0
- package/dist/portal/input/real-input.d.ts +21 -23
- package/dist/portal/input/real-input.js +153 -194
- package/dist/portal/input/viewport-tools.js +6 -3
- package/dist/portal/pool/browser-pool.d.ts +5 -1
- package/dist/portal/pool/browser-pool.js +32 -38
- package/dist/portal/pool/context-permissions.d.ts +20 -17
- package/dist/portal/pool/context-permissions.js +63 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/lease-page-error.d.ts +29 -0
- package/dist/portal/pool/lease-page-error.js +18 -0
- package/dist/portal/pool/page-error-journal.d.ts +57 -0
- package/dist/portal/pool/page-error-journal.js +181 -0
- package/dist/portal/pool/playwright-launcher.js +21 -8
- package/dist/portal/pool/pool-contract.d.ts +22 -0
- package/dist/portal/pool/pool-lease.d.ts +35 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.js +69 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
- package/dist/portal/session/dev-server/dev-command.js +56 -8
- package/dist/portal/session/no-session-diagnosis.js +10 -3
- package/dist/portal/session/no-session-next-action.d.ts +12 -0
- package/dist/portal/session/no-session-next-action.js +44 -4
- package/dist/portal/session/no-session-watch.js +3 -1
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/session-manager.js +40 -21
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +29 -2
- package/dist/start-wiring.js +73 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/tools/act/act-preflight.js +37 -17
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +7 -0
- package/dist/surface/tools/act/after-match.js +33 -0
- package/dist/surface/tools/act/resolve-target.d.ts +2 -0
- package/dist/surface/tools/act/resolve-target.js +35 -8
- package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
- package/dist/surface/tools/act/sequence-grade.js +8 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -0
- package/dist/surface/tools/act-tools.js +12 -12
- package/dist/surface/tools/assert/assert-verdict.js +28 -1
- package/dist/surface/tools/browser-tools.js +75 -7
- package/dist/surface/tools/dynamic-tools.js +2 -1
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.js +43 -22
- package/dist/surface/tools/invoke-tool.js +27 -20
- package/dist/surface/tools/lease-hint.d.ts +7 -0
- package/dist/surface/tools/lease-hint.js +10 -3
- package/dist/surface/tools/lease-readiness.d.ts +21 -0
- package/dist/surface/tools/lease-readiness.js +47 -0
- package/dist/surface/tools/lease-seed.d.ts +2 -0
- package/dist/surface/tools/lease-seed.js +2 -1
- package/dist/surface/tools/lease-tools.js +43 -15
- package/dist/surface/tools/real-input-attempt.js +70 -31
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/tool-kit.d.ts +12 -0
- package/dist/surface/tools/tool-kit.js +7 -0
- package/dist/surface/tools/tools.js +26 -13
- package/dist/telemetry/argument-shape.js +16 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +2 -2
- package/dist/telemetry/tool-refused.js +9 -2
- package/docs/architecture.md +3 -6
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +4 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli.mdx +1 -1
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +15 -20
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/react.mdx +14 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +1 -0
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/telemetry.md +4 -2
- package/package.json +14 -10
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
|
@@ -10,25 +10,21 @@
|
|
|
10
10
|
* pay for it; the type-only import is elided by `tsc`, so the build stays green without it.
|
|
11
11
|
*/
|
|
12
12
|
import { takeJsCoverage } from './js-coverage.js';
|
|
13
|
+
import { DriveError } from './drive-error.js';
|
|
14
|
+
import { pressViaKeyboard } from './real-input-press.js';
|
|
13
15
|
import { stampedDriveUrl } from './drive-url-stamp.js';
|
|
14
16
|
import { launchChromium } from '../../launch-chromium.js';
|
|
17
|
+
import { pageViewportOptions } from '../../chromium-launch-options.js';
|
|
15
18
|
import { chromiumLaunchHint, gotoOptions, SCREENSHOT_DETERMINISM, captureWithChromeHidden, } from '../pool/playwright-launcher.js';
|
|
16
19
|
import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
|
|
17
20
|
import { getSessionMetrics } from '../../telemetry/session-metrics.js';
|
|
18
21
|
import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
|
|
19
|
-
import { ActionType,
|
|
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';
|
|
20
23
|
import { installNetworkMocks } from './network-mock.js';
|
|
21
24
|
import { attachNetworkDetail } from './network-detail.js';
|
|
22
25
|
import { injectedConnectArgs } from '../pool/zero-install.js';
|
|
23
|
-
|
|
24
|
-
export
|
|
25
|
-
code;
|
|
26
|
-
constructor(code, message) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.code = code;
|
|
29
|
-
this.name = 'DriveError';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
26
|
+
export { DriveError };
|
|
27
|
+
export { unspellablePressReason } from './real-input-press.js';
|
|
32
28
|
/** Center of a viewport box in CSS px. Pure — unit-tested directly. */
|
|
33
29
|
export function boxCenter(box) {
|
|
34
30
|
return { cx: box.x + box.width / 2, cy: box.y + box.height / 2 };
|
|
@@ -63,149 +59,12 @@ function undriven(action, box) {
|
|
|
63
59
|
const REAL_INPUT_SETTLE_MS = 16;
|
|
64
60
|
/** Default number of interpolation steps for a native drag. */
|
|
65
61
|
const DEFAULT_DRAG_STEPS = 8;
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* A `holdMs` splits the chord into down / wait / up, the same shape the page uses for a held key.
|
|
72
|
-
* `keyboard.press` would send it instantly instead, reporting a hold-to-confirm that never held.
|
|
73
|
-
* The one thing this cannot reproduce is the auto-repeat a held key emits: the driver sends a single
|
|
74
|
-
* keydown, where the in-page path sends the repeats a browser would. Named rather than papered over
|
|
75
|
-
* — an app that counts repeats needs the synthetic path, and a caller that sees a held key do
|
|
76
|
-
* nothing now has a reason to try it.
|
|
77
|
-
*/
|
|
78
|
-
async function pressViaKeyboard(page, args, sleep, now) {
|
|
79
|
-
const key = pressKeyFromArgs(args);
|
|
80
|
-
const modifiers = pressModifiersFromArgs(args);
|
|
81
|
-
const chord = 0 < modifiers.length ? `${modifiers.join('+')}+${key}` : key;
|
|
82
|
-
const holdMs = clampHoldMs(args.holdMs);
|
|
83
|
-
if (0 === holdMs) {
|
|
84
|
-
// `press` DOES take a chord string ("Control+k"), so the tap path needs no splitting.
|
|
85
|
-
await page.keyboard.press(chord);
|
|
86
|
-
return 0;
|
|
87
|
-
}
|
|
88
|
-
/*
|
|
89
|
-
* `down`/`up` take ONE key each, unlike `press`: `down('Control+k')` throws
|
|
90
|
-
* `Unknown key: "Control+k"` — measured on a real Chromium, not assumed. The chord string the
|
|
91
|
-
* tap path uses therefore cannot express a hold, so the chord is split here: modifiers down in
|
|
92
|
-
* chord order, the key last, and released in reverse (key first, modifiers reversed), which is
|
|
93
|
-
* what a hand does and what an app's keyup bookkeeping expects.
|
|
94
|
-
*
|
|
95
|
-
* Without the split a held shortcut threw before a single key moved, the caller fell back to
|
|
96
|
-
* the synthetic path, and a real hold was impossible for any press with modifiers — the exact
|
|
97
|
-
* gesture `holdMs` exists for.
|
|
98
|
-
*
|
|
99
|
-
* ## Why every key that went down is tracked, and every exit path cleans up
|
|
100
|
-
*
|
|
101
|
-
* The down calls are the ones that MOVE the keyboard, and they can fail partway: `Control` goes
|
|
102
|
-
* down, the key after it rejects, and the driver's copy of `Control` is still held. That path
|
|
103
|
-
* used to fall straight to the caller's synthetic replay — which presses the same chord again on
|
|
104
|
-
* a keyboard that is already holding a modifier. Tracking what actually went down is what makes
|
|
105
|
-
* the cleanup below able to release the right keys, and only those: releasing a key that never
|
|
106
|
-
* went down is its own lie, and it would turn a clean failure into an unnecessary refusal.
|
|
107
|
-
*/
|
|
108
|
-
const pressed = [];
|
|
109
|
-
const pressDown = async (k) => {
|
|
110
|
-
await page.keyboard.down(k);
|
|
111
|
-
pressed.push(k);
|
|
112
|
-
};
|
|
113
|
-
try {
|
|
114
|
-
for (const modifier of modifiers)
|
|
115
|
-
await pressDown(modifier);
|
|
116
|
-
await pressDown(key);
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
// The chord is PARTIALLY down. Release what is held before reporting the failure; if that
|
|
120
|
-
// cannot be proved, `releaseKeys` throws RELEASE_FAILED and the caller refuses the replay —
|
|
121
|
-
// the same rule as a failed release on the ordinary path, for the same reason. Only when the
|
|
122
|
-
// cleanup DOES succeed is the original error safe to hand back.
|
|
123
|
-
await releaseKeys(page, pressed);
|
|
124
|
-
throw error;
|
|
62
|
+
/** The mouse options a click needs: the button, only when it is not the default left one. */
|
|
63
|
+
function clickOptionsOf(args) {
|
|
64
|
+
const button = clickButtonFromArgs({ button: args.button });
|
|
65
|
+
if (button === ClickButton.RIGHT || button === ClickButton.MIDDLE) {
|
|
66
|
+
return { button };
|
|
125
67
|
}
|
|
126
|
-
const startedAt = now();
|
|
127
|
-
try {
|
|
128
|
-
await sleep(holdMs);
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
// The wait failed, and the caller is about to run the synthetic path — which presses this SAME
|
|
132
|
-
// key a second time. If the driver's copy stays down, a held modifier then colours every later
|
|
133
|
-
// action for the rest of the run. Best-effort on purpose: whatever went wrong with the wait is
|
|
134
|
-
// the error worth reporting. A cleanup that cannot be proved is the exception — that one is a
|
|
135
|
-
// stuck key, and it must reach the caller as a refusal rather than be swallowed.
|
|
136
|
-
try {
|
|
137
|
-
await releaseKeys(page, pressed);
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
throw new DriveError(DriveErrorCode.RELEASE_FAILED, 'the wait failed and the keys it pressed could not be released, so they may still be held');
|
|
141
|
-
}
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
// NOT best-effort: on the ordinary path a failed release is a real failure the caller must hear
|
|
145
|
-
// about, and swallowing it would report a press whose key is still down.
|
|
146
|
-
await releaseKeys(page, pressed);
|
|
147
|
-
return now() - startedAt;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Release the given keys, in reverse press order, and report a failure as one that leaves the
|
|
151
|
-
* keyboard UNKNOWN rather than as an ordinary provider error.
|
|
152
|
-
*
|
|
153
|
-
* Every release is attempted even after one fails: a stuck `Control` is worse than a stuck letter,
|
|
154
|
-
* so a failure on one key must not skip the rest. The first failure is what is thrown, since it is
|
|
155
|
-
* the one that happened first, and it is wrapped in `DriveError` so the caller can tell "this
|
|
156
|
-
* gesture failed" from "this gesture may have left a key down" — the difference between a safe
|
|
157
|
-
* synthetic replay and an unsafe one.
|
|
158
|
-
*/
|
|
159
|
-
async function releaseKeys(page, pressed) {
|
|
160
|
-
let failure;
|
|
161
|
-
for (const k of [...pressed].reverse()) {
|
|
162
|
-
try {
|
|
163
|
-
await page.keyboard.up(k);
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
failure ??= error;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (failure !== undefined) {
|
|
170
|
-
throw new DriveError(DriveErrorCode.RELEASE_FAILED, `the key was pressed but could not be released, so it may still be held: ${describeFailure(failure)}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* A thrown value as a line a reader can act on.
|
|
175
|
-
*
|
|
176
|
-
* Not `String(error)`: a non-Error thrown value (a rejected plain object, an SDK that throws a
|
|
177
|
-
* record) stringifies to `[object Object]`, which names nothing and hides the one fact the message
|
|
178
|
-
* exists to carry. Anything that is neither an Error nor a string says so rather than pretending.
|
|
179
|
-
*/
|
|
180
|
-
function describeFailure(error) {
|
|
181
|
-
if (error instanceof Error)
|
|
182
|
-
return error.message;
|
|
183
|
-
return 'string' === typeof error ? error : 'an unknown error';
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* The `press`es this module will NOT drive: a chord that only the in-page dispatcher can express.
|
|
187
|
-
*
|
|
188
|
-
* `args.keys` asks for several keys held down TOGETHER, pressed in order and released in reverse.
|
|
189
|
-
* That is not a `page.keyboard.press` chord — a non-modifier key cannot appear before the last `+`
|
|
190
|
-
* — and a real keyboard cannot aim a key at a named element. `args.code` is the third: the driver
|
|
191
|
-
* presses by KEY name and offers no way to send a physical `code` that disagrees with it, so
|
|
192
|
-
* `{ text: 'z', code: 'KeyY' }` would strike a different key than the one asked for. All three are
|
|
193
|
-
* routed to the synthetic path by the caller, which reports the reason rather than pretending a
|
|
194
|
-
* different gesture happened.
|
|
195
|
-
*
|
|
196
|
-
* Ordered by how specifically the caller asked: `keys` then `code` name the KEY, `ref` names only
|
|
197
|
-
* the target. Two can apply at once, and the earlier one is the more unusual request — the reason
|
|
198
|
-
* should name that, not the generic "you passed a ref".
|
|
199
|
-
*/
|
|
200
|
-
export function unspellablePressReason(ref, args) {
|
|
201
|
-
if (0 < pressKeysFromArgs(args).length) {
|
|
202
|
-
return InputModeReason.SYNTHETIC_MULTI_KEY_PRESS_PREFERRED;
|
|
203
|
-
}
|
|
204
|
-
if (explicitCodeFromArgs(args) !== undefined) {
|
|
205
|
-
return InputModeReason.SYNTHETIC_KEY_CODE_PRESS_PREFERRED;
|
|
206
|
-
}
|
|
207
|
-
if (0 < ref.length)
|
|
208
|
-
return InputModeReason.SYNTHETIC_ELEMENT_PRESS_PREFERRED;
|
|
209
68
|
return undefined;
|
|
210
69
|
}
|
|
211
70
|
/**
|
|
@@ -227,6 +86,27 @@ now = Date.now) {
|
|
|
227
86
|
...(heldMs > 0 ? { heldMs } : {}),
|
|
228
87
|
};
|
|
229
88
|
}
|
|
89
|
+
return withChromePassThrough(page, () => pointerGesture(page, action, box, args, sleep));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Run a real pointer gesture with Reticle's own UI out of the hit test, and put it back after.
|
|
93
|
+
*
|
|
94
|
+
* A real pointer goes to the topmost element under it. When the HUD sat over the app's control, a
|
|
95
|
+
* driven click went to the HUD and the verdict said the app did nothing; a drag ended short (#992).
|
|
96
|
+
* The restore runs whatever the gesture did, so the HUD does not stay unclickable for a person.
|
|
97
|
+
*/
|
|
98
|
+
export async function withChromePassThrough(page, gesture) {
|
|
99
|
+
await page.evaluate(RETICLE_CHROME_PASS_THROUGH_SCRIPT);
|
|
100
|
+
try {
|
|
101
|
+
return await gesture();
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
// A click that navigated takes the old page and its held values with it.
|
|
105
|
+
await page.evaluate(RETICLE_CHROME_RESTORE_POINTER_SCRIPT).catch(() => undefined);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** The pointer half of `performGesture`: every action that lands at a point. */
|
|
109
|
+
async function pointerGesture(page, action, box, args, sleep) {
|
|
230
110
|
const center = boxCenter(box);
|
|
231
111
|
const { cx, cy } = center;
|
|
232
112
|
const real = (performed) => ({
|
|
@@ -241,19 +121,32 @@ now = Date.now) {
|
|
|
241
121
|
await sleep(REAL_INPUT_SETTLE_MS);
|
|
242
122
|
return real(true);
|
|
243
123
|
}
|
|
244
|
-
if (action === ActionType.CLICK) {
|
|
124
|
+
if (action === ActionType.CLICK || action === ActionType.DBLCLICK) {
|
|
245
125
|
await page.mouse.move(cx, cy);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
126
|
+
const options = clickOptionsOf(args);
|
|
127
|
+
const modifiers = pressModifiersFromArgs(args);
|
|
128
|
+
for (const modifier of modifiers) {
|
|
129
|
+
await page.keyboard.down(modifier);
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
if (action === ActionType.CLICK) {
|
|
133
|
+
await page.mouse.click(cx, cy, options);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
await page.mouse.dblclick(cx, cy, options);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
for (const modifier of [...modifiers].reverse()) {
|
|
141
|
+
await page.keyboard.up(modifier);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
252
144
|
return real(true);
|
|
253
145
|
}
|
|
254
146
|
if (action === ActionType.DRAG) {
|
|
255
|
-
if (args.toBox === undefined)
|
|
147
|
+
if (args.toBox === undefined) {
|
|
256
148
|
return real(false);
|
|
149
|
+
}
|
|
257
150
|
const dst = boxCenter(args.toBox);
|
|
258
151
|
const steps = args.steps ?? DEFAULT_DRAG_STEPS;
|
|
259
152
|
await page.mouse.move(cx, cy);
|
|
@@ -341,8 +234,9 @@ export class CdpRealInputProvider {
|
|
|
341
234
|
/** Attach the response listener the first time we see a page; a no-op afterwards. */
|
|
342
235
|
#listen(page) {
|
|
343
236
|
const sink = this.#onNetworkDetail;
|
|
344
|
-
if (sink === undefined || this.#listening.has(page))
|
|
237
|
+
if (sink === undefined || this.#listening.has(page)) {
|
|
345
238
|
return;
|
|
239
|
+
}
|
|
346
240
|
this.#listening.add(page);
|
|
347
241
|
attachNetworkDetail(page, sink);
|
|
348
242
|
}
|
|
@@ -354,8 +248,9 @@ export class CdpRealInputProvider {
|
|
|
354
248
|
// Playwright's Browser exposes isConnected(); a test fake may not, so only an EXPLICIT false
|
|
355
249
|
// drops the cache — a fake without the method is assumed live.
|
|
356
250
|
const alive = this.#browser.isConnected?.() ?? true;
|
|
357
|
-
if (alive)
|
|
251
|
+
if (alive) {
|
|
358
252
|
return this.#browser;
|
|
253
|
+
}
|
|
359
254
|
this.#browser = undefined; // fall through and reconnect
|
|
360
255
|
}
|
|
361
256
|
try {
|
|
@@ -369,11 +264,13 @@ export class CdpRealInputProvider {
|
|
|
369
264
|
}
|
|
370
265
|
async #pageFor(sessionUrl) {
|
|
371
266
|
const browser = await this.#ensureBrowser();
|
|
372
|
-
if (browser === undefined)
|
|
267
|
+
if (browser === undefined) {
|
|
373
268
|
return undefined;
|
|
269
|
+
}
|
|
374
270
|
const page = selectPage(browser.contexts().flatMap((c) => c.pages()), sessionUrl);
|
|
375
|
-
if (page !== undefined)
|
|
271
|
+
if (page !== undefined) {
|
|
376
272
|
this.#listen(page);
|
|
273
|
+
}
|
|
377
274
|
return page;
|
|
378
275
|
}
|
|
379
276
|
async takeCodeCoverage(sessionUrl) {
|
|
@@ -390,22 +287,25 @@ export class CdpRealInputProvider {
|
|
|
390
287
|
}
|
|
391
288
|
async perform(sessionUrl, action, box, args) {
|
|
392
289
|
const page = await this.#pageFor(sessionUrl);
|
|
393
|
-
if (page === undefined)
|
|
290
|
+
if (page === undefined) {
|
|
394
291
|
return undriven(action, box);
|
|
292
|
+
}
|
|
395
293
|
return performGesture(page, action, box, args, this.#sleep, this.#now);
|
|
396
294
|
}
|
|
397
295
|
/** PNG of the correlated page, or undefined if none matches. */
|
|
398
296
|
async screenshot(sessionUrl, opts) {
|
|
399
297
|
const page = await this.#pageFor(sessionUrl);
|
|
400
|
-
if (page === undefined)
|
|
298
|
+
if (page === undefined) {
|
|
401
299
|
return undefined;
|
|
300
|
+
}
|
|
402
301
|
return capturePage(page, opts);
|
|
403
302
|
}
|
|
404
303
|
/** The context's cookies and per-origin storage; undefined when no driven page matches. */
|
|
405
304
|
async storageState(sessionUrl) {
|
|
406
305
|
const page = await this.#pageFor(sessionUrl);
|
|
407
|
-
if (page === undefined)
|
|
306
|
+
if (page === undefined) {
|
|
408
307
|
return undefined;
|
|
308
|
+
}
|
|
409
309
|
return page.context().storageState();
|
|
410
310
|
}
|
|
411
311
|
/**
|
|
@@ -416,27 +316,32 @@ export class CdpRealInputProvider {
|
|
|
416
316
|
*/
|
|
417
317
|
async applyStorageState(sessionUrl, state) {
|
|
418
318
|
const page = await this.#pageFor(sessionUrl);
|
|
419
|
-
if (page === undefined)
|
|
319
|
+
if (page === undefined) {
|
|
420
320
|
return false;
|
|
321
|
+
}
|
|
421
322
|
const saved = state;
|
|
422
|
-
if (saved.cookies !== undefined)
|
|
323
|
+
if (saved.cookies !== undefined) {
|
|
423
324
|
await page.context().addCookies(saved.cookies);
|
|
325
|
+
}
|
|
424
326
|
for (const origin of saved.origins ?? []) {
|
|
425
327
|
// Only the document for an origin can write that origin's localStorage, so this is applied in
|
|
426
328
|
// the page rather than through the context. A page sitting on a different origin cannot be
|
|
427
329
|
// given another origin's storage, and silently succeeding there would be the quiet half-restore
|
|
428
330
|
// this whole path exists to avoid.
|
|
429
|
-
if (!page.url().startsWith(origin.origin))
|
|
331
|
+
if (!page.url().startsWith(origin.origin)) {
|
|
430
332
|
continue;
|
|
333
|
+
}
|
|
431
334
|
await page.evaluate((entries) => {
|
|
432
335
|
// Reached through `globalThis` rather than `window`: this body is serialised and runs in the
|
|
433
336
|
// PAGE, but it is typechecked here, and this package never pulls in the DOM lib — a server
|
|
434
337
|
// that can name `window` is one step from a server that uses it.
|
|
435
338
|
const store = globalThis.localStorage;
|
|
436
|
-
if (store === undefined)
|
|
339
|
+
if (store === undefined) {
|
|
437
340
|
return;
|
|
438
|
-
|
|
341
|
+
}
|
|
342
|
+
for (const entry of entries) {
|
|
439
343
|
store.setItem(entry.name, entry.value);
|
|
344
|
+
}
|
|
440
345
|
}, origin.localStorage ?? []);
|
|
441
346
|
}
|
|
442
347
|
return true;
|
|
@@ -444,16 +349,18 @@ export class CdpRealInputProvider {
|
|
|
444
349
|
/** Apply network-mock rules to the correlated page; false when no driven page matches. */
|
|
445
350
|
async setMocks(sessionUrl, rules) {
|
|
446
351
|
const page = await this.#pageFor(sessionUrl);
|
|
447
|
-
if (page === undefined)
|
|
352
|
+
if (page === undefined) {
|
|
448
353
|
return false;
|
|
354
|
+
}
|
|
449
355
|
await installNetworkMocks(page, rules);
|
|
450
356
|
return true;
|
|
451
357
|
}
|
|
452
358
|
/** Pin the correlated page's viewport to fixed dimensions; false when no driven page matches. */
|
|
453
359
|
async setViewport(sessionUrl, size) {
|
|
454
360
|
const page = await this.#pageFor(sessionUrl);
|
|
455
|
-
if (page === undefined)
|
|
361
|
+
if (page === undefined) {
|
|
456
362
|
return false;
|
|
363
|
+
}
|
|
457
364
|
await page.setViewportSize({ width: size.width, height: size.height });
|
|
458
365
|
return true;
|
|
459
366
|
}
|
|
@@ -461,11 +368,14 @@ export class CdpRealInputProvider {
|
|
|
461
368
|
async dispose() {
|
|
462
369
|
const browser = this.#browser;
|
|
463
370
|
this.#browser = undefined;
|
|
464
|
-
if (browser !== undefined)
|
|
371
|
+
if (browser !== undefined) {
|
|
465
372
|
await browser.close();
|
|
373
|
+
}
|
|
466
374
|
}
|
|
467
375
|
}
|
|
468
376
|
const INJECT_CONNECT_WAIT_MS = 8_000;
|
|
377
|
+
/** True on a page whose Reticle SDK has loaded. */
|
|
378
|
+
const SDK_PRESENT = '!!globalThis.__reticleInstance';
|
|
469
379
|
/** The only place the dynamic value import of Playwright lives for the launched (drive) path. */
|
|
470
380
|
export const launchedChromium = async (headless) => {
|
|
471
381
|
let mod;
|
|
@@ -500,6 +410,7 @@ export class LaunchedRealInputProvider {
|
|
|
500
410
|
#injectConnect;
|
|
501
411
|
#storageState;
|
|
502
412
|
#onNetworkDetail;
|
|
413
|
+
#zeroInstallScript;
|
|
503
414
|
#browser;
|
|
504
415
|
#page;
|
|
505
416
|
/** Pages collecting V8 coverage — see js-coverage.ts. */
|
|
@@ -513,14 +424,19 @@ export class LaunchedRealInputProvider {
|
|
|
513
424
|
this.#injectConnect = options.injectConnect;
|
|
514
425
|
this.#storageState = options.storageState;
|
|
515
426
|
this.#onNetworkDetail = options.onNetworkDetail;
|
|
427
|
+
this.#zeroInstallScript = options.zeroInstallScript;
|
|
516
428
|
}
|
|
517
429
|
async navigate() {
|
|
518
430
|
this.#browser = await this.#launch(this.#headless);
|
|
519
|
-
const page = await this.#browser.newPage(
|
|
431
|
+
const page = await this.#browser.newPage({
|
|
432
|
+
...pageViewportOptions(this.#headless),
|
|
433
|
+
...(this.#storageState !== undefined ? { storageState: this.#storageState } : {}),
|
|
434
|
+
});
|
|
520
435
|
this.#page = page;
|
|
521
436
|
// Capture CDP-authoritative response detail into the driven session's journal (best-effort).
|
|
522
|
-
if (this.#onNetworkDetail !== undefined)
|
|
437
|
+
if (this.#onNetworkDetail !== undefined) {
|
|
523
438
|
attachNetworkDetail(page, this.#onNetworkDetail);
|
|
439
|
+
}
|
|
524
440
|
try {
|
|
525
441
|
// Same navigation rule as the pool, and for the same measured reason: Playwright's default
|
|
526
442
|
// waits for `load`, which an app with one never-finishing subresource never fires — 30s of
|
|
@@ -533,8 +449,35 @@ export class LaunchedRealInputProvider {
|
|
|
533
449
|
catch (e) {
|
|
534
450
|
throw new DriveError(DriveErrorCode.NAVIGATE_FAILED, e instanceof Error ? e.message : String(e));
|
|
535
451
|
}
|
|
452
|
+
await this.#supplyReader(page);
|
|
536
453
|
await this.#tryInjectConnect(page);
|
|
537
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Give a page with no Reticle SDK the zero-install reader, so `verify` on an app with no `init`
|
|
457
|
+
* still gets a session. The wait for the page's own SDK comes first, as on a leased tab, and the
|
|
458
|
+
* script checks the singleton, so a page with an SDK never gets a second one.
|
|
459
|
+
*/
|
|
460
|
+
async #supplyReader(page) {
|
|
461
|
+
const script = this.#zeroInstallScript?.();
|
|
462
|
+
if (script === undefined) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
await page.waitForFunction(SDK_PRESENT, { timeout: INJECT_CONNECT_WAIT_MS });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// No SDK on the page: supply the reader below.
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
// Also on every later document, so a full navigation keeps the session.
|
|
474
|
+
await page.addInitScript(script);
|
|
475
|
+
await page.evaluate(script);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// A page that refuses the reader has no session. verify's no-session guard reports it.
|
|
479
|
+
}
|
|
480
|
+
}
|
|
538
481
|
/**
|
|
539
482
|
* Wait for the page's Reticle singleton to exist, then re-invoke connect with our token + loopback
|
|
540
483
|
* URL so a hosted (non-localhost) preview pairs to our bridge without the app being reconfigured.
|
|
@@ -542,10 +485,11 @@ export class LaunchedRealInputProvider {
|
|
|
542
485
|
*/
|
|
543
486
|
async #tryInjectConnect(page) {
|
|
544
487
|
const opts = this.#injectConnect;
|
|
545
|
-
if (opts === undefined)
|
|
488
|
+
if (opts === undefined) {
|
|
546
489
|
return;
|
|
490
|
+
}
|
|
547
491
|
try {
|
|
548
|
-
await page.waitForFunction(
|
|
492
|
+
await page.waitForFunction(SDK_PRESENT, {
|
|
549
493
|
timeout: INJECT_CONNECT_WAIT_MS,
|
|
550
494
|
});
|
|
551
495
|
const arg = JSON.stringify(injectedConnectArgs(opts));
|
|
@@ -567,8 +511,9 @@ export class LaunchedRealInputProvider {
|
|
|
567
511
|
*/
|
|
568
512
|
#livePage() {
|
|
569
513
|
const page = this.#page;
|
|
570
|
-
if (page === undefined)
|
|
514
|
+
if (page === undefined) {
|
|
571
515
|
return undefined;
|
|
516
|
+
}
|
|
572
517
|
if (true === page.isClosed?.()) {
|
|
573
518
|
this.#page = undefined; // never ask a corpse twice
|
|
574
519
|
return undefined;
|
|
@@ -577,10 +522,12 @@ export class LaunchedRealInputProvider {
|
|
|
577
522
|
}
|
|
578
523
|
isAvailableFor(sessionUrl) {
|
|
579
524
|
const page = this.#livePage();
|
|
580
|
-
if (page === undefined)
|
|
525
|
+
if (page === undefined) {
|
|
581
526
|
return Promise.resolve(false);
|
|
582
|
-
|
|
527
|
+
}
|
|
528
|
+
if (page.url() === sessionUrl) {
|
|
583
529
|
return Promise.resolve(true);
|
|
530
|
+
}
|
|
584
531
|
return Promise.resolve(stripVolatile(page.url()) === stripVolatile(sessionUrl));
|
|
585
532
|
}
|
|
586
533
|
/**
|
|
@@ -593,8 +540,9 @@ export class LaunchedRealInputProvider {
|
|
|
593
540
|
*/
|
|
594
541
|
async storageState(_sessionUrl) {
|
|
595
542
|
const page = this.#livePage();
|
|
596
|
-
if (page === undefined)
|
|
543
|
+
if (page === undefined) {
|
|
597
544
|
return undefined;
|
|
545
|
+
}
|
|
598
546
|
return page.context().storageState();
|
|
599
547
|
}
|
|
600
548
|
/**
|
|
@@ -603,37 +551,44 @@ export class LaunchedRealInputProvider {
|
|
|
603
551
|
*/
|
|
604
552
|
async applyStorageState(_sessionUrl, state) {
|
|
605
553
|
const page = this.#livePage();
|
|
606
|
-
if (page === undefined)
|
|
554
|
+
if (page === undefined) {
|
|
607
555
|
return false;
|
|
556
|
+
}
|
|
608
557
|
const saved = state;
|
|
609
|
-
if (saved.cookies !== undefined)
|
|
558
|
+
if (saved.cookies !== undefined) {
|
|
610
559
|
await page.context().addCookies(saved.cookies);
|
|
560
|
+
}
|
|
611
561
|
for (const origin of saved.origins ?? []) {
|
|
612
562
|
// A page sitting on a different origin cannot be given another origin's storage, and silently
|
|
613
563
|
// succeeding there is the quiet half-restore this whole path exists to avoid.
|
|
614
|
-
if (!page.url().startsWith(origin.origin))
|
|
564
|
+
if (!page.url().startsWith(origin.origin)) {
|
|
615
565
|
continue;
|
|
566
|
+
}
|
|
616
567
|
await page.evaluate((entries) => {
|
|
617
568
|
const store = globalThis.localStorage;
|
|
618
|
-
if (store === undefined)
|
|
569
|
+
if (store === undefined) {
|
|
619
570
|
return;
|
|
620
|
-
|
|
571
|
+
}
|
|
572
|
+
for (const entry of entries) {
|
|
621
573
|
store.setItem(entry.name, entry.value);
|
|
574
|
+
}
|
|
622
575
|
}, origin.localStorage ?? []);
|
|
623
576
|
}
|
|
624
577
|
return true;
|
|
625
578
|
}
|
|
626
579
|
perform(_sessionUrl, action, box, args) {
|
|
627
580
|
const page = this.#livePage();
|
|
628
|
-
if (page === undefined)
|
|
581
|
+
if (page === undefined) {
|
|
629
582
|
return Promise.resolve(undriven(action, box));
|
|
583
|
+
}
|
|
630
584
|
return performGesture(page, action, box, args, this.#sleep, this.#now);
|
|
631
585
|
}
|
|
632
586
|
/** PNG of the owned page, or undefined before navigate / after dispose. */
|
|
633
587
|
screenshot(_sessionUrl, opts) {
|
|
634
588
|
const page = this.#livePage();
|
|
635
|
-
if (page === undefined)
|
|
589
|
+
if (page === undefined) {
|
|
636
590
|
return Promise.resolve(undefined);
|
|
591
|
+
}
|
|
637
592
|
return capturePage(page, opts);
|
|
638
593
|
}
|
|
639
594
|
/**
|
|
@@ -646,8 +601,9 @@ export class LaunchedRealInputProvider {
|
|
|
646
601
|
*/
|
|
647
602
|
async setViewport(_sessionUrl, size) {
|
|
648
603
|
const page = this.#livePage();
|
|
649
|
-
if (page === undefined)
|
|
604
|
+
if (page === undefined) {
|
|
650
605
|
return false;
|
|
606
|
+
}
|
|
651
607
|
await page.setViewportSize({ width: size.width, height: size.height });
|
|
652
608
|
return true;
|
|
653
609
|
}
|
|
@@ -658,8 +614,9 @@ export class LaunchedRealInputProvider {
|
|
|
658
614
|
/** Apply network-mock rules to the owned page; false before navigate / after dispose. */
|
|
659
615
|
async setMocks(_sessionUrl, rules) {
|
|
660
616
|
const page = this.#livePage();
|
|
661
|
-
if (page === undefined)
|
|
617
|
+
if (page === undefined) {
|
|
662
618
|
return false;
|
|
619
|
+
}
|
|
663
620
|
await installNetworkMocks(page, rules);
|
|
664
621
|
return true;
|
|
665
622
|
}
|
|
@@ -668,8 +625,9 @@ export class LaunchedRealInputProvider {
|
|
|
668
625
|
const browser = this.#browser;
|
|
669
626
|
this.#browser = undefined;
|
|
670
627
|
this.#page = undefined;
|
|
671
|
-
if (browser !== undefined)
|
|
628
|
+
if (browser !== undefined) {
|
|
672
629
|
await browser.close();
|
|
630
|
+
}
|
|
673
631
|
}
|
|
674
632
|
}
|
|
675
633
|
/** Drop hash/query so a page whose URL drifted by fragment still correlates to the session. */
|
|
@@ -688,8 +646,9 @@ export class LaunchedRealInputProvider {
|
|
|
688
646
|
*/
|
|
689
647
|
export function selectPage(pages, sessionUrl) {
|
|
690
648
|
const exact = pages.find((p) => p.url() === sessionUrl);
|
|
691
|
-
if (exact !== undefined)
|
|
649
|
+
if (exact !== undefined) {
|
|
692
650
|
return exact;
|
|
651
|
+
}
|
|
693
652
|
const target = stripVolatile(sessionUrl);
|
|
694
653
|
const loose = pages.filter((p) => stripVolatile(p.url()) === target);
|
|
695
654
|
return 1 === loose.length ? loose[0] : undefined;
|
|
@@ -83,8 +83,8 @@ export const VIEWPORT_TOOLS = [
|
|
|
83
83
|
},
|
|
84
84
|
outputSchema: {
|
|
85
85
|
applied: z.boolean(),
|
|
86
|
-
width: z.number(),
|
|
87
|
-
height: z.number(),
|
|
86
|
+
width: z.number().optional(),
|
|
87
|
+
height: z.number().optional(),
|
|
88
88
|
observed: z.object({ width: z.number(), height: z.number() }).optional(),
|
|
89
89
|
colorScheme: z.string().optional(),
|
|
90
90
|
reducedMotion: z.string().optional(),
|
|
@@ -97,7 +97,10 @@ export const VIEWPORT_TOOLS = [
|
|
|
97
97
|
if (media !== undefined && args['width'] === undefined && args['height'] === undefined) {
|
|
98
98
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
99
99
|
if (true === (await deps.pool?.emulateLease(session.id, media))) {
|
|
100
|
-
|
|
100
|
+
// No size was asked for, so answer the one the lease has; 0 by 0 reads as a broken
|
|
101
|
+
// viewport (#1902). When the page cannot be read, leave the size out.
|
|
102
|
+
const size = await deps.pool?.viewportOfLease(session.id);
|
|
103
|
+
return { applied: true, ...size, ...media };
|
|
101
104
|
}
|
|
102
105
|
return { ...refused(), recommendation: MEDIA_NEEDS_A_LEASE };
|
|
103
106
|
}
|
|
@@ -13,7 +13,7 @@ import { type SeedStorage } from '@reticlehq/core';
|
|
|
13
13
|
import type { Launcher, PooledMockRule, ScreenshotOptions } from './pool-contract.js';
|
|
14
14
|
import type { RealInputArgs, RealInputResult } from '../input/real-input.js';
|
|
15
15
|
import { type LeaseEmulation, type LeaseMedia } from './lease-emulation.js';
|
|
16
|
-
export type { InitScriptHandle, Launcher, PooledBrowser, PooledContext, PooledCookie, PooledDialog, PooledMockRule, PooledPage, } from './pool-contract.js';
|
|
16
|
+
export type { InitScriptHandle, Launcher, PooledBrowser, PooledContext, PooledCookie, PooledDialog, PooledMockRule, PooledPage, PooledPageError, } from './pool-contract.js';
|
|
17
17
|
import { 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';
|
|
@@ -101,6 +101,8 @@ export declare class BrowserPool {
|
|
|
101
101
|
* reaper take it out from under a diff.
|
|
102
102
|
*/
|
|
103
103
|
screenshotLease(sessionId: string, opts?: ScreenshotOptions): Promise<Uint8Array | undefined>;
|
|
104
|
+
/** The reason {@link screenshotLease} last failed for this session, if it did. */
|
|
105
|
+
leaseCaptureError(sessionId: string): string | undefined;
|
|
104
106
|
/** The URL a lease was opened on — the one that carries its Reticle identity. */
|
|
105
107
|
leaseUrl(sessionId: string): string | undefined;
|
|
106
108
|
/**
|
|
@@ -243,6 +245,8 @@ export declare class BrowserPool {
|
|
|
243
245
|
lastDialogMessage(sessionId: string): string | undefined;
|
|
244
246
|
/** Close every context and the browser. Pending waiters are rejected (the pool is terminal now). */
|
|
245
247
|
shutdown(): Promise<void>;
|
|
248
|
+
/** The origin added to this lease's page CSP `connect-src`, or undefined if none was (#2016). */
|
|
249
|
+
cspWidenedFor(sessionId: string): string | undefined;
|
|
246
250
|
/** Whether this lease's context skips certificate checks. A context cannot change it later. */
|
|
247
251
|
ignoresHTTPSErrors(sessionId: string): boolean;
|
|
248
252
|
/** Whether this lease lives in the headed browser. */
|