@reticlehq/server 3.5.0 → 3.6.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/README.md +1 -1
- package/SKILL.md +16 -8
- package/dist/chromium-launch-options.d.ts +3 -1
- package/dist/chromium-launch-options.js +2 -1
- package/dist/command/cli/adhoc-verdict.d.ts +1 -0
- package/dist/command/cli/adhoc-verdict.js +6 -3
- package/dist/command/cli/answers/open-note.d.ts +2 -0
- package/dist/command/cli/answers/open-note.js +22 -0
- package/dist/command/cli/cli-flow-commands.js +4 -8
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +10 -10
- package/dist/command/cli/cli-verify.d.ts +1 -1
- package/dist/command/cli/cli-verify.js +35 -25
- package/dist/command/cli/cloud-cli.js +51 -12
- package/dist/command/cli/cloud-login.d.ts +8 -0
- package/dist/command/cli/cloud-login.js +36 -2
- package/dist/command/cli/demo/demo-page.js +11 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
- package/dist/command/cli/drive/drive-attach.d.ts +27 -2
- package/dist/command/cli/drive/drive-attach.js +71 -4
- package/dist/command/cli/drive/drive-command.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +13 -7
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
- package/dist/command/cli/ports/port-holder.d.ts +10 -0
- package/dist/command/cli/ports/port-holder.js +24 -0
- package/dist/command/cli/setup-mcp-cli.js +6 -4
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +6 -9
- package/dist/command/cli.d.ts +6 -0
- package/dist/command/cli.js +28 -22
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
- package/dist/command/daemon/daemon-resolve.d.ts +14 -0
- package/dist/command/daemon/daemon-resolve.js +33 -2
- package/dist/command/setup/agent-writer.d.ts +1 -1
- package/dist/command/setup/agent-writer.js +3 -2
- package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
- package/dist/command/setup/bringup/connect-lease.js +1 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
- package/dist/command/setup/bringup/dev-server-wait.js +19 -0
- package/dist/command/setup/init/init-port.d.ts +4 -0
- package/dist/command/setup/init/init-port.js +10 -0
- package/dist/command/setup/init/init-runtime.d.ts +10 -0
- package/dist/command/setup/init/init-runtime.js +18 -1
- package/dist/command/setup/node-effects.js +2 -1
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +5 -0
- package/dist/command/setup/run-setup.js +35 -27
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- package/dist/command/version/version-skew.js +11 -2
- package/dist/features/crawl/crawl-tools.js +4 -0
- package/dist/features/crawl/crawl.d.ts +6 -0
- package/dist/features/crawl/crawl.js +9 -1
- package/dist/features/exhaust/ledger.d.ts +7 -0
- package/dist/features/exhaust/ledger.js +35 -9
- package/dist/features/harness/app-findings.d.ts +2 -0
- package/dist/features/harness/app-findings.js +52 -0
- package/dist/features/harness/drive-report.d.ts +27 -1
- package/dist/features/harness/drive-report.js +162 -11
- package/dist/features/harness/drivers.d.ts +10 -14
- package/dist/features/harness/drivers.js +10 -14
- package/dist/features/harness/fill-values.d.ts +3 -73
- package/dist/features/harness/fill-values.js +3 -169
- package/dist/features/harness/goals.d.ts +21 -0
- package/dist/features/harness/goals.js +77 -0
- package/dist/features/harness/harness.d.ts +21 -0
- package/dist/features/harness/harness.js +69 -2
- package/dist/features/harness/platform/drive-target.d.ts +30 -0
- package/dist/features/harness/platform/drive-target.js +81 -0
- package/dist/features/harness/platform/local-apps.d.ts +94 -0
- package/dist/features/harness/platform/local-apps.js +204 -0
- package/dist/features/harness/platform/remote-drive.d.ts +180 -0
- package/dist/features/harness/platform/remote-drive.js +365 -0
- package/dist/features/harness/platform/script.d.ts +48 -0
- package/dist/features/harness/platform/script.js +128 -0
- package/dist/features/harness/platform/server-driver.d.ts +41 -0
- package/dist/features/harness/platform/server-driver.js +179 -0
- package/dist/features/harness/platform-config.d.ts +7 -0
- package/dist/features/harness/platform-config.js +14 -2
- package/dist/features/harness/script/app-controls.d.ts +5 -0
- package/dist/features/harness/script/app-controls.js +40 -0
- package/dist/features/harness/script/lane-ids.d.ts +14 -0
- package/dist/features/harness/script/lane-ids.js +16 -0
- package/dist/features/harness/script/script-run.d.ts +41 -0
- package/dist/features/harness/script/script-run.js +299 -0
- package/dist/features/license/license.d.ts +2 -2
- package/dist/features/visual/visual-tools.d.ts +10 -1
- package/dist/features/visual/visual-tools.js +40 -3
- package/dist/hooks/driven-by.d.ts +4 -0
- package/dist/hooks/driven-by.js +16 -0
- package/dist/hooks/hook-commands.js +15 -4
- package/dist/hooks/tool-log.d.ts +9 -0
- package/dist/hooks/tool-log.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +76 -82
- package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
- package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
- package/dist/judgement/runs/artifact/run-store.js +26 -1
- package/dist/judgement/runs/drive-run.d.ts +22 -0
- package/dist/judgement/runs/drive-run.js +99 -1
- package/dist/judgement/runs/repair-prompt.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +8 -1
- package/dist/judgement/runs/verification-sync.d.ts +3 -2
- package/dist/judgement/runs/verification-sync.js +4 -3
- package/dist/language/flows/change/gate.d.ts +10 -0
- package/dist/language/flows/change/gate.js +22 -0
- package/dist/language/flows/decision.js +34 -0
- package/dist/language/flows/flow-anchor.d.ts +54 -8
- package/dist/language/flows/flow-anchor.js +88 -3
- package/dist/language/flows/flow-cross-step.d.ts +15 -0
- package/dist/language/flows/flow-cross-step.js +21 -0
- package/dist/language/flows/flow-learning.js +8 -11
- package/dist/language/flows/flow-mutate-tools.js +5 -3
- package/dist/language/flows/flow-replay-run.d.ts +23 -9
- package/dist/language/flows/flow-replay-run.js +101 -94
- package/dist/language/flows/flow-replay.d.ts +23 -1
- package/dist/language/flows/flow-replay.js +92 -19
- package/dist/language/flows/flow-scope.js +5 -1
- package/dist/language/flows/flow-step-runners.d.ts +20 -2
- package/dist/language/flows/flow-step-runners.js +236 -42
- package/dist/language/flows/flow-store-for-session.d.ts +2 -2
- package/dist/language/flows/flow-store-for-session.js +2 -2
- package/dist/language/flows/flow-tools.js +58 -34
- package/dist/language/flows/flows.js +5 -2
- package/dist/language/flows/heal-run.js +6 -4
- package/dist/language/flows/recording/flake.d.ts +2 -2
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
- package/dist/language/flows/recording/tape/recordings.js +40 -8
- package/dist/language/flows/replay-from-hud.d.ts +20 -0
- package/dist/language/flows/replay-from-hud.js +51 -0
- package/dist/language/flows/replay.d.ts +9 -5
- package/dist/language/flows/replay.js +27 -9
- package/dist/language/flows/suite/suite-flakes.js +2 -0
- package/dist/language/flows/suite-selection.d.ts +2 -15
- package/dist/language/flows/suite-selection.js +4 -3
- package/dist/language/flows/verify-change-tools.js +8 -4
- package/dist/launch-chromium.d.ts +27 -5
- package/dist/launch-chromium.js +38 -8
- package/dist/memory/cloud/cloud-config.d.ts +12 -0
- package/dist/memory/cloud/cloud-config.js +15 -0
- package/dist/memory/cloud/cloud-sync.d.ts +17 -14
- package/dist/memory/cloud/cloud-sync.js +12 -1
- package/dist/memory/cloud/harness-config.d.ts +26 -1
- package/dist/memory/cloud/harness-config.js +99 -4
- package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
- package/dist/memory/cloud/hud-notices-source.js +128 -0
- package/dist/memory/cloud/memory-scope.d.ts +144 -0
- package/dist/memory/cloud/memory-scope.js +205 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -3
- package/dist/memory/cloud/sync-cycle.js +16 -3
- package/dist/memory/cloud/sync-daemon.js +7 -5
- package/dist/memory/cloud/sync-disk.d.ts +1 -1
- package/dist/memory/cloud/sync-disk.js +5 -8
- package/dist/memory/impact/account-watch.d.ts +19 -0
- package/dist/memory/impact/account-watch.js +34 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +12 -2
- package/dist/memory/impact/impact-store.d.ts +16 -0
- package/dist/memory/impact/impact-store.js +142 -26
- package/dist/memory/intent/intent-shard.d.ts +2 -2
- package/dist/memory/intent/intent-tools.js +80 -2
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-recorder.js +7 -1
- package/dist/memory/journal/on-disk/retention.d.ts +3 -0
- package/dist/memory/journal/on-disk/retention.js +5 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
- package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
- package/dist/memory/journal/session-end.js +6 -4
- package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
- package/dist/memory/project/artifact-root-resolver.js +94 -13
- package/dist/memory/project/artifact-root.d.ts +42 -5
- package/dist/memory/project/artifact-root.js +87 -8
- package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
- package/dist/memory/project/dir/reticle-dir.js +3 -1
- package/dist/memory/project/project-tools.js +22 -3
- package/dist/memory/project/session-root.d.ts +24 -1
- package/dist/memory/project/session-root.js +41 -5
- package/dist/memory/project/sync-status.d.ts +39 -0
- package/dist/memory/project/sync-status.js +90 -0
- package/dist/memory/recall/memory-tools.js +2 -0
- package/dist/memory/recall/project-memory.d.ts +19 -0
- package/dist/memory/recall/project-memory.js +45 -6
- package/dist/portal/bridge/bridge.d.ts +3 -1
- package/dist/portal/bridge/bridge.js +19 -2
- package/dist/portal/bridge/pairing-token.d.ts +9 -0
- package/dist/portal/bridge/pairing-token.js +17 -0
- package/dist/portal/input/real-input.d.ts +81 -10
- package/dist/portal/input/real-input.js +207 -22
- package/dist/portal/input/viewport-tools.js +3 -3
- package/dist/portal/pool/browser-pool.d.ts +22 -9
- package/dist/portal/pool/browser-pool.js +70 -35
- package/dist/portal/pool/playwright-launcher.d.ts +13 -0
- package/dist/portal/pool/playwright-launcher.js +17 -1
- package/dist/portal/pool/pool-contract.d.ts +7 -4
- package/dist/portal/session/command-payload.d.ts +8 -0
- package/dist/portal/session/command-payload.js +19 -0
- package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
- package/dist/portal/session/dev-server/dev-command.js +61 -12
- package/dist/portal/session/facts/departure-note.d.ts +22 -0
- package/dist/portal/session/facts/departure-note.js +47 -0
- package/dist/portal/session/facts/session-replaced.d.ts +5 -0
- package/dist/portal/session/facts/session-replaced.js +5 -0
- package/dist/portal/session/first-run-wiring.d.ts +42 -0
- package/dist/portal/session/first-run-wiring.js +132 -0
- package/dist/portal/session/human/notes-ledger.d.ts +20 -0
- package/dist/portal/session/human/notes-ledger.js +55 -0
- package/dist/portal/session/human/review-store.d.ts +12 -0
- package/dist/portal/session/human/review-store.js +18 -10
- package/dist/portal/session/live-control-tools.d.ts +0 -13
- package/dist/portal/session/live-control-tools.js +28 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
- package/dist/portal/session/no-session-diagnosis.js +15 -0
- package/dist/portal/session/no-session-next-action.d.ts +11 -0
- package/dist/portal/session/no-session-next-action.js +31 -5
- package/dist/portal/session/no-session-watch.d.ts +14 -1
- package/dist/portal/session/no-session-watch.js +63 -10
- package/dist/portal/session/presence/session-mirror.js +3 -0
- package/dist/portal/session/serving-directory.d.ts +27 -0
- package/dist/portal/session/serving-directory.js +45 -0
- package/dist/portal/session/session-health.d.ts +7 -0
- package/dist/portal/session/session-health.js +6 -0
- package/dist/portal/session/session-info.d.ts +8 -1
- package/dist/portal/session/session-info.js +6 -0
- package/dist/portal/session/session-manager.d.ts +21 -0
- package/dist/portal/session/session-manager.js +44 -8
- package/dist/portal/session/session-reaper.js +4 -1
- package/dist/portal/session/session-successor.d.ts +5 -0
- package/dist/portal/session/session-tools.js +26 -3
- package/dist/portal/session/session.d.ts +7 -2
- package/dist/portal/session/session.js +21 -18
- package/dist/start-options.d.ts +6 -0
- package/dist/status-payload.d.ts +10 -0
- package/dist/status-payload.js +23 -0
- package/dist/surface/http-server.js +11 -1
- package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
- package/dist/surface/mcp/mcp-proxy.js +36 -5
- package/dist/surface/mcp/mcp.d.ts +11 -6
- package/dist/surface/mcp/mcp.js +27 -13
- package/dist/surface/mcp/server-instructions.d.ts +4 -2
- package/dist/surface/mcp/server-instructions.js +14 -4
- package/dist/surface/mcp/shared-params.js +2 -2
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
- package/dist/surface/tools/act/settle-in-flight.js +25 -3
- package/dist/surface/tools/act-sequence-tool.js +2 -2
- package/dist/surface/tools/act-tools.js +23 -7
- package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +17 -2
- package/dist/surface/tools/browser-tools.js +8 -1
- package/dist/surface/tools/chat-drives.d.ts +26 -0
- package/dist/surface/tools/chat-drives.js +165 -0
- package/dist/surface/tools/empty-session-list.d.ts +41 -0
- package/dist/surface/tools/empty-session-list.js +80 -0
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +37 -4
- package/dist/surface/tools/explore-tools.d.ts +10 -1
- package/dist/surface/tools/explore-tools.js +82 -13
- package/dist/surface/tools/feedback-tools.js +14 -0
- package/dist/surface/tools/harness-explore.d.ts +96 -14
- package/dist/surface/tools/harness-explore.js +227 -206
- package/dist/surface/tools/harness-plan.d.ts +12 -0
- package/dist/surface/tools/harness-plan.js +31 -0
- package/dist/surface/tools/harness-script.d.ts +8 -0
- package/dist/surface/tools/harness-script.js +342 -0
- package/dist/surface/tools/harness-toolset.d.ts +14 -1
- package/dist/surface/tools/harness-toolset.js +77 -6
- package/dist/surface/tools/invoke-tool.js +52 -1
- package/dist/surface/tools/lease-seed.d.ts +29 -0
- package/dist/surface/tools/lease-seed.js +123 -0
- package/dist/surface/tools/lease-tools.d.ts +3 -23
- package/dist/surface/tools/lease-tools.js +30 -131
- package/dist/surface/tools/live-call-text.js +2 -2
- package/dist/surface/tools/next-step.d.ts +19 -0
- package/dist/surface/tools/next-step.js +159 -0
- package/dist/surface/tools/observe-tools.js +2 -4
- package/dist/surface/tools/platform-moment.d.ts +12 -0
- package/dist/surface/tools/platform-moment.js +168 -0
- package/dist/surface/tools/query-hint-schema.d.ts +7 -0
- package/dist/surface/tools/query-hint-schema.js +10 -0
- package/dist/surface/tools/read-tools.js +8 -2
- package/dist/surface/tools/real-input-attempt.js +96 -13
- package/dist/surface/tools/tool-kit.d.ts +15 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- package/dist/telemetry/telemetry.d.ts +12 -0
- package/dist/telemetry/telemetry.js +43 -14
- package/dist/telemetry/verification-of.js +1 -0
- package/dist/wire-journal.js +4 -1
- package/docs/README.md +1 -1
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- package/docs/cli/doctor.mdx +6 -0
- package/docs/cli/drive.mdx +8 -2
- package/docs/cli/init.mdx +11 -13
- package/docs/cli/status.mdx +5 -1
- package/docs/cli/version.mdx +3 -3
- package/docs/cli.mdx +1 -1
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +13 -11
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +22 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +17 -7
- package/docs/telemetry.md +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -2
- package/docs/what-is-recorded.md +13 -3
- package/docs/what-is-reticle.mdx +3 -1
- package/package.json +8 -12
- package/dist/features/harness/driver.d.ts +0 -50
- package/dist/features/harness/driver.js +0 -236
- package/dist/features/harness/jev-driver.d.ts +0 -188
- package/dist/features/harness/jev-driver.js +0 -678
- package/dist/features/harness/openai-driver.d.ts +0 -58
- package/dist/features/harness/openai-driver.js +0 -206
- package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
- package/dist/memory/project/dir/fill-value-store.js +0 -88
- package/docs/local-registry.md +0 -143
|
@@ -11,7 +11,7 @@ import { tokensMatch } from './token-auth.js';
|
|
|
11
11
|
import { pairingTokenSource } from './pairing-token.js';
|
|
12
12
|
import { log } from '../../log.js';
|
|
13
13
|
import { getSessionMetrics } from '../../telemetry/session-metrics.js';
|
|
14
|
-
import { sessionReplacedReason } from '../session/facts/session-replaced.js';
|
|
14
|
+
import { SESSION_PARKED_REASON, sessionReplacedReason, } from '../session/facts/session-replaced.js';
|
|
15
15
|
import { describeSkew, sdkFix, SkewPair } from '../../command/version/version-skew.js';
|
|
16
16
|
import { noteVersionSkew } from '../../command/version/version-nudge.js';
|
|
17
17
|
import { protocolSkewReason } from './protocol-skew.js';
|
|
@@ -147,6 +147,10 @@ function harnessRequest(event) {
|
|
|
147
147
|
return false;
|
|
148
148
|
return undefined;
|
|
149
149
|
}
|
|
150
|
+
/** True when this event is the panel's Sign in. Pure boundary narrowing, like the above. */
|
|
151
|
+
function isSigninRequest(event) {
|
|
152
|
+
return event.type === EventType.HUMAN_CONTROL && event.data['kind'] === HumanControlKind.SIGNIN;
|
|
153
|
+
}
|
|
150
154
|
/** True when this event is the panel's "sync now" button. Pure boundary narrowing, like the above. */
|
|
151
155
|
function isSyncRequest(event) {
|
|
152
156
|
return event.type === EventType.HUMAN_CONTROL && event.data['kind'] === HumanControlKind.SYNC;
|
|
@@ -273,6 +277,7 @@ export class Bridge {
|
|
|
273
277
|
/** Wired by the daemon: push to the dashboard now, because somebody asked in the panel. */
|
|
274
278
|
#onSyncRequest;
|
|
275
279
|
#onHarnessRequest;
|
|
280
|
+
#onSigninRequest;
|
|
276
281
|
constructor(options) {
|
|
277
282
|
const host = options.host ?? LOOPBACK_HOST;
|
|
278
283
|
if ((options.token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
|
|
@@ -543,6 +548,11 @@ export class Bridge {
|
|
|
543
548
|
socket.close(WS_CLOSE.AUTH_FAILED[0], reason);
|
|
544
549
|
return;
|
|
545
550
|
}
|
|
551
|
+
// A window the agent detached stays detached across its own reloads. See SessionManager.park.
|
|
552
|
+
if (this.sessions.isParked(parsed.sessionId)) {
|
|
553
|
+
socket.close(1008, SESSION_PARKED_REASON);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
546
556
|
const existing = this.sessions.get(parsed.sessionId);
|
|
547
557
|
if (existing === undefined && this.sessions.count() >= this.#maxSessions) {
|
|
548
558
|
socket.close(...WS_CLOSE.SESSION_LIMIT);
|
|
@@ -695,7 +705,10 @@ export class Bridge {
|
|
|
695
705
|
// The switch is written through to the platform by the daemon, for the same reason: the
|
|
696
706
|
// Session has no cloud credential and should not grow one.
|
|
697
707
|
else if (harness !== undefined)
|
|
698
|
-
this.#onHarnessRequest?.(harness);
|
|
708
|
+
this.#onHarnessRequest?.(harness, session);
|
|
709
|
+
// Signing in writes `~/.reticle`, which only the daemon may do; the panel only asks.
|
|
710
|
+
else if (isSigninRequest(parsed.event))
|
|
711
|
+
this.#onSigninRequest?.();
|
|
699
712
|
// Pass the raw frame's byte length so the buffer doesn't re-serialize every event for accounting.
|
|
700
713
|
else
|
|
701
714
|
session.pushEvent(parsed.event, Buffer.byteLength(text, 'utf8'));
|
|
@@ -838,6 +851,10 @@ export class Bridge {
|
|
|
838
851
|
attachHarnessRequest(handler) {
|
|
839
852
|
this.#onHarnessRequest = handler;
|
|
840
853
|
}
|
|
854
|
+
/** Register a handler for the panel's Sign in. Optional, like the above: without one it is ignored. */
|
|
855
|
+
attachSigninRequest(handler) {
|
|
856
|
+
this.#onSigninRequest = handler;
|
|
857
|
+
}
|
|
841
858
|
/** Register a handler to run when a session connects. Additive — every handler runs. */
|
|
842
859
|
attachSessionCreate(handler) {
|
|
843
860
|
this.#onSessionCreate.push(handler);
|
|
@@ -33,6 +33,15 @@ export declare function pairingTokenSource(input: {
|
|
|
33
33
|
explicit: boolean;
|
|
34
34
|
env: NodeJS.ProcessEnv;
|
|
35
35
|
}): 'explicit' | 'env:RETICLE_TOKEN' | 'env:RETICLE_PAIRING_TOKEN_DIR' | 'default';
|
|
36
|
+
/**
|
|
37
|
+
* The pairing token this machine was told to use, or undefined when it was told nothing.
|
|
38
|
+
*
|
|
39
|
+
* `RETICLE_TOKEN` first, then the token file in `RETICLE_PAIRING_TOKEN_DIR`, the precedence
|
|
40
|
+
* `pairingTokenSource` names. A command that mints its own one-shot token for a hosted preview asks
|
|
41
|
+
* this first: in a container the app's plugin bakes the directory token in, and a fresh token made
|
|
42
|
+
* the daemon refuse the only app it was started to verify (#1251).
|
|
43
|
+
*/
|
|
44
|
+
export declare function configuredPairingToken(env: NodeJS.ProcessEnv, deps: PairingTokenDeps): Promise<string | undefined>;
|
|
36
45
|
export declare function pairingTokenPath(dir: string): string;
|
|
37
46
|
/** Production IO adapter: 0600 file + 0700 dir so only the owner can read the secret. */
|
|
38
47
|
export declare function nodePairingTokenDeps(): PairingTokenDeps;
|
|
@@ -41,6 +41,23 @@ export function pairingTokenSource(input) {
|
|
|
41
41
|
return `env:${PAIRING_TOKEN_DIR_ENV}`;
|
|
42
42
|
return 'default';
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The pairing token this machine was told to use, or undefined when it was told nothing.
|
|
46
|
+
*
|
|
47
|
+
* `RETICLE_TOKEN` first, then the token file in `RETICLE_PAIRING_TOKEN_DIR`, the precedence
|
|
48
|
+
* `pairingTokenSource` names. A command that mints its own one-shot token for a hosted preview asks
|
|
49
|
+
* this first: in a container the app's plugin bakes the directory token in, and a fresh token made
|
|
50
|
+
* the daemon refuse the only app it was started to verify (#1251).
|
|
51
|
+
*/
|
|
52
|
+
export async function configuredPairingToken(env, deps) {
|
|
53
|
+
const token = env[ReticleEnv.TOKEN]?.trim();
|
|
54
|
+
if (token !== undefined && 0 < token.length)
|
|
55
|
+
return token;
|
|
56
|
+
const dir = env[PAIRING_TOKEN_DIR_ENV];
|
|
57
|
+
if (dir === undefined || 0 === dir.length)
|
|
58
|
+
return undefined;
|
|
59
|
+
return readOrCreatePairingToken(dir, deps);
|
|
60
|
+
}
|
|
44
61
|
export function pairingTokenPath(dir) {
|
|
45
62
|
return join(dir, ReticleDir.PAIRING_TOKEN_FILE);
|
|
46
63
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type ScriptCoverage } from './js-coverage.js';
|
|
13
13
|
import type { Browser, Page } from 'playwright';
|
|
14
|
-
import { ActionType, DriveErrorCode } from '@reticlehq/core';
|
|
14
|
+
import { ActionType, DriveErrorCode, InputMode, InputModeReason } from '@reticlehq/core';
|
|
15
15
|
import { type MockRule } from './network-mock.js';
|
|
16
16
|
import { type NetworkDetail } from './network-detail.js';
|
|
17
17
|
/** Viewport CSS-px box as returned by the INSPECT command (getBoundingClientRect). */
|
|
@@ -21,28 +21,72 @@ export interface ElementBox {
|
|
|
21
21
|
width: number;
|
|
22
22
|
height: number;
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Args forwarded from reticle_act (fill value, type text, press key, drag drop-target box).
|
|
26
|
+
*
|
|
27
|
+
* A `type`, not an `interface`: only the alias form gets an implicit index signature, so the core
|
|
28
|
+
* arg readers take it as a plain record without a cast at every call site.
|
|
29
|
+
*/
|
|
30
|
+
export type RealInputArgs = {
|
|
26
31
|
value?: string;
|
|
27
32
|
text?: string;
|
|
33
|
+
/** For press: the key name, e.g. Escape or Tab. `text` also carries it; `key` is the fallback. */
|
|
34
|
+
key?: string;
|
|
35
|
+
/** For press: modifier names (Meta/Control/Shift/Alt; aliases like `cmd` accepted). */
|
|
36
|
+
modifiers?: string[];
|
|
37
|
+
/**
|
|
38
|
+
* For press: several keys held down TOGETHER, pressed in order and released in reverse. Read but
|
|
39
|
+
* never forwarded — a chord with a non-modifier key has no `page.keyboard.press` spelling, so the
|
|
40
|
+
* caller routes such a press to the synthetic dispatcher before it reaches here.
|
|
41
|
+
*/
|
|
42
|
+
keys?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* For press: how long to keep the key down between keydown and keyup. The same argument the
|
|
45
|
+
* synthetic path has always honoured, so a hold-to-confirm key driven through the driver holds
|
|
46
|
+
* for exactly as long as one driven in the page.
|
|
47
|
+
*/
|
|
48
|
+
holdMs?: number;
|
|
28
49
|
/** For drag: the resolved box of the drop-target ref (toRef). */
|
|
29
50
|
toBox?: ElementBox;
|
|
30
51
|
steps?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
52
|
+
};
|
|
53
|
+
/** What a provider reports for one driven gesture. Exported so a fake can build it by name. */
|
|
54
|
+
export interface RealInputResult {
|
|
33
55
|
/** True if a native gesture was actually driven. */
|
|
34
56
|
performed: boolean;
|
|
35
|
-
/** Center used, for diagnostics/tests. */
|
|
36
|
-
center
|
|
57
|
+
/** Center used, for diagnostics/tests. ABSENT for a key press: see `undriven`. */
|
|
58
|
+
center?: {
|
|
37
59
|
cx: number;
|
|
38
60
|
cy: number;
|
|
39
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* Which path this result came from. `real` whenever a gesture was actually driven, so a caller
|
|
64
|
+
* reading it on a `performed: true` result learns nothing it did not already know — it is here
|
|
65
|
+
* for the `performed: false` case, where the caller is about to take the synthetic path and
|
|
66
|
+
* saying `real` would describe a gesture that never ran.
|
|
67
|
+
*/
|
|
68
|
+
inputMode: InputMode;
|
|
69
|
+
/**
|
|
70
|
+
* How long the key was ACTUALLY held, for a real press that was asked to hold. Absent for every
|
|
71
|
+
* other gesture, and for a press with no `holdMs` — the same rule the synthetic path uses, where
|
|
72
|
+
* an absent `heldMs` means "this action does not hold" rather than "it held for no time".
|
|
73
|
+
*
|
|
74
|
+
* Measured rather than echoed back: `holdMs: 1200` against a 1200ms animation is a race by
|
|
75
|
+
* construction, and a throttled tab stretches the wait. A caller needs to tell "held 1200" from
|
|
76
|
+
* "held 1204", and can only do that with the achieved number.
|
|
77
|
+
*/
|
|
78
|
+
heldMs?: number;
|
|
40
79
|
}
|
|
41
80
|
/** Options for a page screenshot — full-page scroll capture and/or a clip box. */
|
|
42
81
|
export interface ScreenshotOpts {
|
|
43
82
|
fullPage?: boolean;
|
|
44
83
|
/** Restrict the capture to one element/region (viewport CSS px). */
|
|
45
84
|
clip?: ElementBox;
|
|
85
|
+
/**
|
|
86
|
+
* A live picture, as a JPEG at this quality: what a person is watching, taken as it is. The
|
|
87
|
+
* baseline tweaks (hiding the HUD, stopping animations) would make their window flicker each second.
|
|
88
|
+
*/
|
|
89
|
+
jpegQuality?: number;
|
|
46
90
|
}
|
|
47
91
|
/** The capability surface reticle_act depends on. A FAKE implementing this is injected in tests. */
|
|
48
92
|
export interface RealInputProvider {
|
|
@@ -117,17 +161,40 @@ export declare function boxCenter(box: ElementBox): {
|
|
|
117
161
|
cy: number;
|
|
118
162
|
};
|
|
119
163
|
/**
|
|
120
|
-
* Which actions are driven by native pointer
|
|
121
|
-
*
|
|
164
|
+
* Which actions are driven by a native pointer. `press` is excluded on purpose — a key has no
|
|
165
|
+
* pointer, and widening this would change what every existing caller of it means.
|
|
122
166
|
*/
|
|
123
167
|
export declare function isPointerAction(action: ActionType): boolean;
|
|
168
|
+
/**
|
|
169
|
+
* The routing question `tryRealInput` asks: does this action have a real-input path at all? Wider
|
|
170
|
+
* than `isPointerAction` by `press`, which needs no coordinates.
|
|
171
|
+
*/
|
|
172
|
+
export declare function isRealInputAction(action: ActionType): boolean;
|
|
124
173
|
type SleepFn = (ms: number) => Promise<void>;
|
|
125
174
|
type ConnectFn = (url: string) => Promise<Browser>;
|
|
175
|
+
/**
|
|
176
|
+
* The `press`es this module will NOT drive: a chord that only the in-page dispatcher can express.
|
|
177
|
+
*
|
|
178
|
+
* `args.keys` asks for several keys held down TOGETHER, pressed in order and released in reverse.
|
|
179
|
+
* That is not a `page.keyboard.press` chord — a non-modifier key cannot appear before the last `+`
|
|
180
|
+
* — and a real keyboard cannot aim a key at a named element. `args.code` is the third: the driver
|
|
181
|
+
* presses by KEY name and offers no way to send a physical `code` that disagrees with it, so
|
|
182
|
+
* `{ text: 'z', code: 'KeyY' }` would strike a different key than the one asked for. All three are
|
|
183
|
+
* routed to the synthetic path by the caller, which reports the reason rather than pretending a
|
|
184
|
+
* different gesture happened.
|
|
185
|
+
*
|
|
186
|
+
* Ordered by how specifically the caller asked: `keys` then `code` name the KEY, `ref` names only
|
|
187
|
+
* the target. Two can apply at once, and the earlier one is the more unusual request — the reason
|
|
188
|
+
* should name that, not the generic "you passed a ref".
|
|
189
|
+
*/
|
|
190
|
+
export declare function unspellablePressReason(ref: string, args: Record<string, unknown>): InputModeReason | undefined;
|
|
126
191
|
/**
|
|
127
192
|
* Shared gesture executor: drive a native gesture on an already-resolved Page. Used by both the
|
|
128
193
|
* CDP-attached and the launched (drive) providers so the pointer logic lives in one place.
|
|
129
194
|
*/
|
|
130
|
-
export declare function performGesture(page: Page, action: ActionType, box: ElementBox, args: RealInputArgs, sleep: SleepFn
|
|
195
|
+
export declare function performGesture(page: Page, action: ActionType, box: ElementBox, args: RealInputArgs, sleep: SleepFn,
|
|
196
|
+
/** Injected so a test can assert the achieved hold deterministically; defaults to the real clock. */
|
|
197
|
+
now?: () => number): Promise<RealInputResult>;
|
|
131
198
|
/**
|
|
132
199
|
* Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
|
|
133
200
|
* screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
|
|
@@ -137,6 +204,8 @@ interface CdpProviderOptions {
|
|
|
137
204
|
cdpUrl: string;
|
|
138
205
|
/** Injected so the settle delay is deterministic in tests; defaults to a real Node timer. */
|
|
139
206
|
sleep?: SleepFn;
|
|
207
|
+
/** Injected so a measured hold is deterministic in tests; defaults to the real clock. */
|
|
208
|
+
now?: () => number;
|
|
140
209
|
/** Injected connector so unit tests can stub Playwright without import. */
|
|
141
210
|
connect?: ConnectFn;
|
|
142
211
|
/**
|
|
@@ -193,6 +262,8 @@ export interface LaunchedProviderOptions {
|
|
|
193
262
|
headless: boolean;
|
|
194
263
|
/** Injected so the settle delay is deterministic in tests; defaults to a real Node timer. */
|
|
195
264
|
sleep?: SleepFn;
|
|
265
|
+
/** Injected so a measured hold is deterministic in tests; defaults to the real clock. */
|
|
266
|
+
now?: () => number;
|
|
196
267
|
/** Injected launcher so unit tests can stub Playwright; defaults to dynamic import('playwright'). */
|
|
197
268
|
launch?: LaunchFn;
|
|
198
269
|
/** When set, re-invoke the page's reticle.connect with these after load (drive-a-hosted-preview). */
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
import { takeJsCoverage } from './js-coverage.js';
|
|
13
13
|
import { stampedDriveUrl } from './drive-url-stamp.js';
|
|
14
14
|
import { launchChromium } from '../../launch-chromium.js';
|
|
15
|
-
import { chromiumLaunchHint, gotoOptions } from '../pool/playwright-launcher.js';
|
|
15
|
+
import { HIDE_RETICLE_CHROME_CSS, chromiumLaunchHint, gotoOptions, } from '../pool/playwright-launcher.js';
|
|
16
16
|
import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
|
|
17
17
|
import { getSessionMetrics } from '../../telemetry/session-metrics.js';
|
|
18
18
|
import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
|
|
19
|
-
import { ActionType, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG } from '@reticlehq/core';
|
|
19
|
+
import { ActionType, clampHoldMs, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG, explicitCodeFromArgs, InputMode, InputModeReason, pressKeyFromArgs, pressKeysFromArgs, pressModifiersFromArgs, } from '@reticlehq/core';
|
|
20
20
|
import { installNetworkMocks } from './network-mock.js';
|
|
21
21
|
import { attachNetworkDetail } from './network-detail.js';
|
|
22
22
|
import { injectedConnectArgs } from '../pool/zero-install.js';
|
|
@@ -34,8 +34,8 @@ export function boxCenter(box) {
|
|
|
34
34
|
return { cx: box.x + box.width / 2, cy: box.y + box.height / 2 };
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Which actions are driven by native pointer
|
|
38
|
-
*
|
|
37
|
+
* Which actions are driven by a native pointer. `press` is excluded on purpose — a key has no
|
|
38
|
+
* pointer, and widening this would change what every existing caller of it means.
|
|
39
39
|
*/
|
|
40
40
|
export function isPointerAction(action) {
|
|
41
41
|
return (action === ActionType.HOVER ||
|
|
@@ -43,37 +43,217 @@ export function isPointerAction(action) {
|
|
|
43
43
|
action === ActionType.DBLCLICK ||
|
|
44
44
|
action === ActionType.DRAG);
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The routing question `tryRealInput` asks: does this action have a real-input path at all? Wider
|
|
48
|
+
* than `isPointerAction` by `press`, which needs no coordinates.
|
|
49
|
+
*/
|
|
50
|
+
export function isRealInputAction(action) {
|
|
51
|
+
return isPointerAction(action) || action === ActionType.PRESS;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A gesture that was NOT driven. No center for a key press — a zero would read as a real point.
|
|
55
|
+
*
|
|
56
|
+
* `inputMode` is SYNTHETIC: no real gesture ran, so this is the path the caller is about to take.
|
|
57
|
+
*/
|
|
58
|
+
function undriven(action, box) {
|
|
59
|
+
const base = { performed: false, inputMode: InputMode.SYNTHETIC };
|
|
60
|
+
return action === ActionType.PRESS ? base : { ...base, center: boxCenter(box) };
|
|
61
|
+
}
|
|
46
62
|
/** Settle delay after a native gesture so the reaction can begin to flush (named, not free). */
|
|
47
63
|
const REAL_INPUT_SETTLE_MS = 16;
|
|
48
64
|
/** Default number of interpolation steps for a native drag. */
|
|
49
65
|
const DEFAULT_DRAG_STEPS = 8;
|
|
66
|
+
/**
|
|
67
|
+
* Drive a `press` with a real keyboard — coordinate-free; the key goes to whatever holds focus.
|
|
68
|
+
* Key and modifiers come from the same core helpers the synthetic dispatcher uses, so the two paths
|
|
69
|
+
* cannot read the same args differently.
|
|
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;
|
|
125
|
+
}
|
|
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
|
+
return undefined;
|
|
210
|
+
}
|
|
50
211
|
/**
|
|
51
212
|
* Shared gesture executor: drive a native gesture on an already-resolved Page. Used by both the
|
|
52
213
|
* CDP-attached and the launched (drive) providers so the pointer logic lives in one place.
|
|
53
214
|
*/
|
|
54
|
-
export async function performGesture(page, action, box, args, sleep
|
|
215
|
+
export async function performGesture(page, action, box, args, sleep,
|
|
216
|
+
/** Injected so a test can assert the achieved hold deterministically; defaults to the real clock. */
|
|
217
|
+
now = Date.now) {
|
|
218
|
+
// Ahead of the box, not after it: a press addresses focus rather than a point, and the
|
|
219
|
+
// placeholder box its caller passes would otherwise be turned into a (0,0) that reads like a real
|
|
220
|
+
// location. The result reports NO `center` for the same reason.
|
|
221
|
+
if (action === ActionType.PRESS) {
|
|
222
|
+
const heldMs = await pressViaKeyboard(page, args, sleep, now);
|
|
223
|
+
return {
|
|
224
|
+
performed: true,
|
|
225
|
+
inputMode: InputMode.REAL,
|
|
226
|
+
// Omitted rather than 0 when there was no hold — see `RealInputResult.heldMs`.
|
|
227
|
+
...(heldMs > 0 ? { heldMs } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
55
230
|
const center = boxCenter(box);
|
|
56
231
|
const { cx, cy } = center;
|
|
232
|
+
const real = (performed) => ({
|
|
233
|
+
performed,
|
|
234
|
+
center,
|
|
235
|
+
inputMode: InputMode.REAL,
|
|
236
|
+
});
|
|
57
237
|
if (action === ActionType.HOVER) {
|
|
58
238
|
await page.mouse.move(cx, cy);
|
|
59
239
|
await page.mouse.move(cx + 1, cy);
|
|
60
240
|
await page.mouse.move(cx, cy);
|
|
61
241
|
await sleep(REAL_INPUT_SETTLE_MS);
|
|
62
|
-
return
|
|
242
|
+
return real(true);
|
|
63
243
|
}
|
|
64
244
|
if (action === ActionType.CLICK) {
|
|
65
245
|
await page.mouse.move(cx, cy);
|
|
66
246
|
await page.mouse.click(cx, cy);
|
|
67
|
-
return
|
|
247
|
+
return real(true);
|
|
68
248
|
}
|
|
69
249
|
if (action === ActionType.DBLCLICK) {
|
|
70
250
|
await page.mouse.move(cx, cy);
|
|
71
251
|
await page.mouse.dblclick(cx, cy);
|
|
72
|
-
return
|
|
252
|
+
return real(true);
|
|
73
253
|
}
|
|
74
254
|
if (action === ActionType.DRAG) {
|
|
75
255
|
if (args.toBox === undefined)
|
|
76
|
-
return
|
|
256
|
+
return real(false);
|
|
77
257
|
const dst = boxCenter(args.toBox);
|
|
78
258
|
const steps = args.steps ?? DEFAULT_DRAG_STEPS;
|
|
79
259
|
await page.mouse.move(cx, cy);
|
|
@@ -84,14 +264,14 @@ export async function performGesture(page, action, box, args, sleep) {
|
|
|
84
264
|
await page.mouse.move(px, py, { steps: 1 });
|
|
85
265
|
}
|
|
86
266
|
await page.mouse.up();
|
|
87
|
-
return
|
|
267
|
+
return real(true);
|
|
88
268
|
}
|
|
89
269
|
if (action === ActionType.FILL || action === ActionType.TYPE) {
|
|
90
270
|
await page.mouse.click(cx, cy);
|
|
91
271
|
await page.keyboard.type(args.value ?? args.text ?? '');
|
|
92
|
-
return
|
|
272
|
+
return real(true);
|
|
93
273
|
}
|
|
94
|
-
return
|
|
274
|
+
return real(false);
|
|
95
275
|
}
|
|
96
276
|
/**
|
|
97
277
|
* Reticle paints its own dev overlay (presenter HUD + border glow) into the page. That chrome is
|
|
@@ -100,18 +280,19 @@ export async function performGesture(page, action, box, args, sleep) {
|
|
|
100
280
|
* (Playwright applies this stylesheet only for the shot, then reverts) so visual baselines reflect
|
|
101
281
|
* the app, not Reticle. Disabling animations settles any remaining transitions for determinism.
|
|
102
282
|
*/
|
|
103
|
-
const HIDE_RETICLE_CHROME_CSS = '[data-reticle-overlay]{display:none !important}';
|
|
104
283
|
const SCREENSHOT_DETERMINISM = { style: HIDE_RETICLE_CHROME_CSS, animations: 'disabled' };
|
|
105
284
|
/**
|
|
106
285
|
* Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
|
|
107
286
|
* screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
|
|
108
287
|
*/
|
|
109
288
|
export async function capturePage(page, opts) {
|
|
110
|
-
const buf = await page.screenshot(opts.
|
|
111
|
-
? {
|
|
112
|
-
:
|
|
113
|
-
? { ...SCREENSHOT_DETERMINISM,
|
|
114
|
-
:
|
|
289
|
+
const buf = await page.screenshot(opts.jpegQuality !== undefined
|
|
290
|
+
? { type: 'jpeg', quality: opts.jpegQuality }
|
|
291
|
+
: opts.clip !== undefined
|
|
292
|
+
? { ...SCREENSHOT_DETERMINISM, clip: opts.clip }
|
|
293
|
+
: true === opts.fullPage
|
|
294
|
+
? { ...SCREENSHOT_DETERMINISM, fullPage: true }
|
|
295
|
+
: { ...SCREENSHOT_DETERMINISM });
|
|
115
296
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
116
297
|
}
|
|
117
298
|
const nodeSleep = (ms) => new Promise((resolve) => {
|
|
@@ -138,6 +319,7 @@ const cdpConnect = async (url) => {
|
|
|
138
319
|
export class CdpRealInputProvider {
|
|
139
320
|
#cdpUrl;
|
|
140
321
|
#sleep;
|
|
322
|
+
#now;
|
|
141
323
|
#connect;
|
|
142
324
|
#onNetworkDetail;
|
|
143
325
|
/** Pages already listening. #pageFor resolves on EVERY call, so without this each action would add
|
|
@@ -149,6 +331,7 @@ export class CdpRealInputProvider {
|
|
|
149
331
|
constructor(options) {
|
|
150
332
|
this.#cdpUrl = options.cdpUrl;
|
|
151
333
|
this.#sleep = options.sleep ?? nodeSleep;
|
|
334
|
+
this.#now = options.now ?? Date.now;
|
|
152
335
|
this.#connect = options.connect ?? cdpConnect;
|
|
153
336
|
this.#onNetworkDetail = options.onNetworkDetail;
|
|
154
337
|
}
|
|
@@ -205,8 +388,8 @@ export class CdpRealInputProvider {
|
|
|
205
388
|
async perform(sessionUrl, action, box, args) {
|
|
206
389
|
const page = await this.#pageFor(sessionUrl);
|
|
207
390
|
if (page === undefined)
|
|
208
|
-
return
|
|
209
|
-
return performGesture(page, action, box, args, this.#sleep);
|
|
391
|
+
return undriven(action, box);
|
|
392
|
+
return performGesture(page, action, box, args, this.#sleep, this.#now);
|
|
210
393
|
}
|
|
211
394
|
/** PNG of the correlated page, or undefined if none matches. */
|
|
212
395
|
async screenshot(sessionUrl, opts) {
|
|
@@ -309,6 +492,7 @@ export class LaunchedRealInputProvider {
|
|
|
309
492
|
#driveUrl;
|
|
310
493
|
#headless;
|
|
311
494
|
#sleep;
|
|
495
|
+
#now;
|
|
312
496
|
#launch;
|
|
313
497
|
#injectConnect;
|
|
314
498
|
#storageState;
|
|
@@ -321,6 +505,7 @@ export class LaunchedRealInputProvider {
|
|
|
321
505
|
this.#driveUrl = options.driveUrl;
|
|
322
506
|
this.#headless = options.headless;
|
|
323
507
|
this.#sleep = options.sleep ?? nodeSleep;
|
|
508
|
+
this.#now = options.now ?? Date.now;
|
|
324
509
|
this.#launch = options.launch ?? launchedChromium;
|
|
325
510
|
this.#injectConnect = options.injectConnect;
|
|
326
511
|
this.#storageState = options.storageState;
|
|
@@ -438,8 +623,8 @@ export class LaunchedRealInputProvider {
|
|
|
438
623
|
perform(_sessionUrl, action, box, args) {
|
|
439
624
|
const page = this.#livePage();
|
|
440
625
|
if (page === undefined)
|
|
441
|
-
return Promise.resolve(
|
|
442
|
-
return performGesture(page, action, box, args, this.#sleep);
|
|
626
|
+
return Promise.resolve(undriven(action, box));
|
|
627
|
+
return performGesture(page, action, box, args, this.#sleep, this.#now);
|
|
443
628
|
}
|
|
444
629
|
/** PNG of the owned page, or undefined before navigate / after dispose. */
|
|
445
630
|
screenshot(_sessionUrl, opts) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { CDP_NO_PROVIDER_REASON,
|
|
2
|
+
import { CDP_NO_PROVIDER_REASON, VIEWPORT_NO_PROVIDER_RECOMMENDATION } from '@reticlehq/core';
|
|
3
3
|
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';
|
|
@@ -16,7 +16,7 @@ function viewportProvider(deps) {
|
|
|
16
16
|
export const VIEWPORT_TOOLS = [
|
|
17
17
|
{
|
|
18
18
|
name: ReticleTool.VIEWPORT,
|
|
19
|
-
description: 'Pin the DRIVEN page (needs `reticle drive`) to a fixed viewport size so a screenshot baseline is ' +
|
|
19
|
+
description: 'Pin the DRIVEN or LEASED page (needs `reticle drive`, or `reticle_lease` acquire when there is no CDP provider) to a fixed viewport size so a screenshot baseline is ' +
|
|
20
20
|
'reproducible across machines — the missing piece of CI-stable visual regression, alongside ' +
|
|
21
21
|
'reticle_visual_diff `masks` and a frozen clock (reticle_clock). Set it once before reticle_screenshot / ' +
|
|
22
22
|
'reticle_visual_diff. Returns { applied, width, height } or the no-provider recommendation.',
|
|
@@ -60,7 +60,7 @@ export const VIEWPORT_TOOLS = [
|
|
|
60
60
|
height: 0,
|
|
61
61
|
ok: false,
|
|
62
62
|
reason: CDP_NO_PROVIDER_REASON,
|
|
63
|
-
recommendation:
|
|
63
|
+
recommendation: VIEWPORT_NO_PROVIDER_RECOMMENDATION,
|
|
64
64
|
};
|
|
65
65
|
},
|
|
66
66
|
},
|