@reticlehq/server 3.8.2 → 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/network-mock.js +3 -2
- package/dist/portal/input/real-input.d.ts +15 -0
- package/dist/portal/input/real-input.js +59 -3
- 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 +10 -10
- 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/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/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- 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 +13 -9
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SECRET_FIELD_ARG, SECRET_NAME_ARG, secretKeyForFill, secretValue, } from './fields/flow-secret-field.js';
|
|
2
|
-
import { EventType, FlowPredicateSchema, MUTATING_METHODS,
|
|
2
|
+
import { EventType, FlowPredicateSchema, MUTATING_METHODS, isDevToolingUrl, pathOf, } from '@reticlehq/core';
|
|
3
3
|
import { DANGEROUS_ACTION_CONFIRM_ARG, DOCUMENT_ANCHOR_ROLE, ReticleCommand, QueryBy, isGlobalPressCall, } from '@reticlehq/core';
|
|
4
4
|
import { ReticleTool, invokedFlowMissing } from '@reticlehq/core';
|
|
5
5
|
import { formatStepAddress } from 'open-verification';
|
|
@@ -22,8 +22,9 @@ function ambiguousRoleNote(role, name) {
|
|
|
22
22
|
* resolution — can't drift between them.
|
|
23
23
|
*/
|
|
24
24
|
export function queryRefs(result) {
|
|
25
|
-
if (!result.ok)
|
|
25
|
+
if (!result.ok) {
|
|
26
26
|
return [];
|
|
27
|
+
}
|
|
27
28
|
const payload = asRecord(result.result);
|
|
28
29
|
const elements = Array.isArray(payload['elements']) ? payload['elements'] : [];
|
|
29
30
|
return elements.map((e) => asString(asRecord(e)['ref']) ?? '').filter((r) => r.length > 0);
|
|
@@ -47,12 +48,14 @@ export function replayActionArgs(value, confirmDangerous = false, field) {
|
|
|
47
48
|
const key = field === undefined ? undefined : secretKeyForFill(args, field);
|
|
48
49
|
delete args[SECRET_NAME_ARG];
|
|
49
50
|
delete args[SECRET_FIELD_ARG];
|
|
50
|
-
if (confirmDangerous)
|
|
51
|
+
if (confirmDangerous) {
|
|
51
52
|
args[DANGEROUS_ACTION_CONFIRM_ARG] = true;
|
|
53
|
+
}
|
|
52
54
|
if (key !== undefined) {
|
|
53
55
|
const supplied = secretValue(key, process.env);
|
|
54
|
-
if (supplied !== undefined && supplied.length > 0)
|
|
56
|
+
if (supplied !== undefined && supplied.length > 0) {
|
|
55
57
|
args['value'] = supplied;
|
|
58
|
+
}
|
|
56
59
|
}
|
|
57
60
|
return args;
|
|
58
61
|
}
|
|
@@ -62,20 +65,24 @@ export function replayActionArgs(value, confirmDangerous = false, field) {
|
|
|
62
65
|
*/
|
|
63
66
|
function recordedFieldName(step) {
|
|
64
67
|
const by = asString(step['by']);
|
|
65
|
-
if (QueryBy.TESTID === by)
|
|
68
|
+
if (QueryBy.TESTID === by) {
|
|
66
69
|
return asString(step['value']);
|
|
67
|
-
|
|
70
|
+
}
|
|
71
|
+
if (QueryBy.ROLE === by) {
|
|
68
72
|
return asString(step['name']);
|
|
73
|
+
}
|
|
69
74
|
return undefined;
|
|
70
75
|
}
|
|
71
76
|
/** The element's source location from an action result, when the framework stamped one. */
|
|
72
77
|
function sourceFromResult(res) {
|
|
73
78
|
const source = asRecord(res['source']);
|
|
74
|
-
if (typeof source['file'] !== 'string' || typeof source['line'] !== 'number')
|
|
79
|
+
if (typeof source['file'] !== 'string' || typeof source['line'] !== 'number') {
|
|
75
80
|
return undefined;
|
|
81
|
+
}
|
|
76
82
|
const out = { file: source['file'], line: source['line'] };
|
|
77
|
-
if ('number' === typeof source['column'])
|
|
83
|
+
if ('number' === typeof source['column']) {
|
|
78
84
|
out['column'] = source['column'];
|
|
85
|
+
}
|
|
79
86
|
return out;
|
|
80
87
|
}
|
|
81
88
|
/**
|
|
@@ -91,8 +98,9 @@ export { pathOf };
|
|
|
91
98
|
*/
|
|
92
99
|
export function wroteIn(events) {
|
|
93
100
|
return events.some((event) => {
|
|
94
|
-
if (EventType.NET_REQUEST !== event.type)
|
|
101
|
+
if (EventType.NET_REQUEST !== event.type) {
|
|
95
102
|
return false;
|
|
103
|
+
}
|
|
96
104
|
const method = asString(event.data['method'])?.toUpperCase() ?? '';
|
|
97
105
|
return MUTATING_METHODS.includes(method) && !isDevToolingUrl(asString(event.data['url']));
|
|
98
106
|
});
|
|
@@ -122,7 +130,9 @@ route,
|
|
|
122
130
|
/** The session that acted: only recordings it started take the step. */
|
|
123
131
|
session,
|
|
124
132
|
/** The act's window sent a write request; see `wroteIn`. */
|
|
125
|
-
wrote = false
|
|
133
|
+
wrote = false,
|
|
134
|
+
/** The act as the ambient tape keeps it, when that differs. See RecordingStore.capture. */
|
|
135
|
+
ambientArgs) {
|
|
126
136
|
// NO `active()` gate. `RecordingStore.capture` opens the AMBIENT tape on its first step, and that
|
|
127
137
|
// branch was unreachable from here: this returned early whenever nothing was open, and the only
|
|
128
138
|
// thing that opened anything was `reticle_record { action: "start" }`. So the comment on `capture`
|
|
@@ -134,8 +144,15 @@ wrote = false) {
|
|
|
134
144
|
// recorded cannot contribute to either.
|
|
135
145
|
// A click on Reticle's own panel (the HUD exposed) is not a step of the app's journey: saved, the
|
|
136
146
|
// app's flow pressed Run Harness on replay, a paid drive, under an intent that never named it.
|
|
137
|
-
if (true === asRecord(res)['reticleUi'])
|
|
147
|
+
if (true === asRecord(res)['reticleUi']) {
|
|
138
148
|
return;
|
|
149
|
+
}
|
|
150
|
+
const step = capturedStep(args, res, route, wrote);
|
|
151
|
+
const ambient = ambientArgs === undefined ? undefined : capturedStep(ambientArgs, res, route, wrote);
|
|
152
|
+
recordings.capture(step, route, session, ambient);
|
|
153
|
+
}
|
|
154
|
+
/** One act as a recorded step, with the claim the agent declared on it. */
|
|
155
|
+
function capturedStep(args, res, route, wrote) {
|
|
139
156
|
const step = compileActStep(args, res);
|
|
140
157
|
// Keep the assertion the agent actually made. `act_and_wait { until }` IS the agent saying what
|
|
141
158
|
// success means — the large majority of calls carry one — and dropping it produced a flow graded
|
|
@@ -143,36 +160,50 @@ wrote = false) {
|
|
|
143
160
|
// story failing at its last step. See enforceableExpect below.
|
|
144
161
|
const declared = args['until'] ?? args['predicate'];
|
|
145
162
|
const expect = enforceableExpect(declared);
|
|
146
|
-
if (expect !== undefined)
|
|
163
|
+
if (expect !== undefined) {
|
|
147
164
|
step.expect = expect;
|
|
148
|
-
|
|
165
|
+
}
|
|
166
|
+
else if (declared !== undefined) {
|
|
149
167
|
step.unkeptExpect = UNKEPT_EXPECT;
|
|
150
|
-
|
|
168
|
+
}
|
|
169
|
+
if (route !== undefined) {
|
|
151
170
|
step.page = route;
|
|
171
|
+
}
|
|
152
172
|
const intent = asString(args['intent'])?.trim();
|
|
153
|
-
if (intent !== undefined && intent.length > 0)
|
|
173
|
+
if (intent !== undefined && intent.length > 0) {
|
|
154
174
|
step.intent = intent;
|
|
155
|
-
|
|
175
|
+
}
|
|
176
|
+
if (wrote) {
|
|
156
177
|
step.writes = true;
|
|
157
|
-
|
|
178
|
+
}
|
|
179
|
+
return step;
|
|
158
180
|
}
|
|
159
181
|
/**
|
|
160
|
-
* Capture an act_and_wait once its verdict is known
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
182
|
+
* Capture an act_and_wait once its verdict is known.
|
|
183
|
+
*
|
|
184
|
+
* The rule act_sequence keeps (#988). A recording somebody opened keeps every claim the agent
|
|
185
|
+
* declared, held or not: dropping a refuted one saved a flow that replayed green on the defect it
|
|
186
|
+
* had caught. The ambient tape keeps a claim only if the action made it hold (`held`), so a flow cut
|
|
187
|
+
* from it never replays a claim that was never once true because of the action. The action is
|
|
188
|
+
* always recorded: replay needs it to reach the next step.
|
|
189
|
+
*
|
|
190
|
+
* Keyed on `held`, not on `verified: yes`. A claim the action made hold under an `unknown` or a
|
|
191
|
+
* contradicted verdict is still what the agent proved about the page, and dropping it left the
|
|
192
|
+
* replay with no check at all. One function decides both tapes: a second rule that took the claim off the tape
|
|
193
|
+
* after the fact ran before this capture, so it took the PREVIOUS step's claim, which had passed.
|
|
164
194
|
*/
|
|
165
|
-
export function captureVerdictedAct(recordings, args, res, route,
|
|
195
|
+
export function captureVerdictedAct(recordings, args, res, route, held, session,
|
|
166
196
|
/** The act's window sent a write request; see `wroteIn`. */
|
|
167
197
|
wrote = false) {
|
|
168
198
|
const { until: _until, predicate: _predicate, ...unproved } = args;
|
|
169
|
-
captureAct(recordings,
|
|
199
|
+
captureAct(recordings, args, res, route, session, wrote, held ? undefined : unproved);
|
|
170
200
|
}
|
|
171
201
|
/** Fold a PASSING standalone assertion into the step it proved. See RecordingStore.attachExpect. */
|
|
172
202
|
export function captureAssertion(recordings, raw, session) {
|
|
173
203
|
const expect = enforceableExpect(raw);
|
|
174
|
-
if (expect !== undefined)
|
|
204
|
+
if (expect !== undefined) {
|
|
175
205
|
recordings.attachExpect(expect, session);
|
|
206
|
+
}
|
|
176
207
|
}
|
|
177
208
|
/**
|
|
178
209
|
* A declared predicate, as a recorded expectation — or nothing.
|
|
@@ -186,8 +217,9 @@ export function captureAssertion(recordings, raw, session) {
|
|
|
186
217
|
* Every one of those was a claim the agent made and the file did not keep.
|
|
187
218
|
*/
|
|
188
219
|
function enforceableExpect(raw) {
|
|
189
|
-
if (raw === undefined)
|
|
220
|
+
if (raw === undefined) {
|
|
190
221
|
return undefined;
|
|
222
|
+
}
|
|
191
223
|
// The flow file's own schema, so capture keeps exactly what a load would accept — a predicate
|
|
192
224
|
// holding a session ref is refused by both, rather than recorded here and rejected on read.
|
|
193
225
|
const parsed = FlowPredicateSchema.safeParse(raw);
|
|
@@ -232,8 +264,9 @@ function compileAnchorArgs(r, ref, action, actArgs) {
|
|
|
232
264
|
const compiled = anchorArgs(r, ref, action, actArgs);
|
|
233
265
|
// The SDK says the target holds a secret (a password input, whatever its label). Carried beside
|
|
234
266
|
// the anchor so the saved flow redacts the fill by what the field IS, not by what it is called.
|
|
235
|
-
if (true === r['secret'])
|
|
267
|
+
if (true === r['secret']) {
|
|
236
268
|
compiled.args[SECRET_FIELD_ARG] = true;
|
|
269
|
+
}
|
|
237
270
|
return compiled;
|
|
238
271
|
}
|
|
239
272
|
function anchorArgs(r, ref, action, actArgs) {
|
|
@@ -262,10 +295,12 @@ function anchorArgs(r, ref, action, actArgs) {
|
|
|
262
295
|
action,
|
|
263
296
|
args: actArgs,
|
|
264
297
|
};
|
|
265
|
-
if (source !== undefined)
|
|
298
|
+
if (source !== undefined) {
|
|
266
299
|
testidArgs['source'] = source;
|
|
267
|
-
|
|
300
|
+
}
|
|
301
|
+
if (testidOf !== undefined) {
|
|
268
302
|
testidArgs['of'] = testidOf;
|
|
303
|
+
}
|
|
269
304
|
return { args: testidArgs, stable: true };
|
|
270
305
|
}
|
|
271
306
|
// Role + NAME before component: a component/source anchor names the JSX site, so every row's copy
|
|
@@ -282,8 +317,9 @@ function anchorArgs(r, ref, action, actArgs) {
|
|
|
282
317
|
action,
|
|
283
318
|
args: actArgs,
|
|
284
319
|
};
|
|
285
|
-
if (source !== undefined)
|
|
320
|
+
if (source !== undefined) {
|
|
286
321
|
roleArgs['source'] = source;
|
|
322
|
+
}
|
|
287
323
|
if ('number' === typeof r['nth'] && 'number' === typeof r['of']) {
|
|
288
324
|
roleArgs['nth'] = r['nth'];
|
|
289
325
|
roleArgs['of'] = r['of'];
|
|
@@ -293,10 +329,12 @@ function anchorArgs(r, ref, action, actArgs) {
|
|
|
293
329
|
const component = asString(r['component']);
|
|
294
330
|
if (component !== undefined || source !== undefined) {
|
|
295
331
|
const componentArgs = { by: QueryBy.COMPONENT, action, args: actArgs };
|
|
296
|
-
if (component !== undefined)
|
|
332
|
+
if (component !== undefined) {
|
|
297
333
|
componentArgs['component'] = component;
|
|
298
|
-
|
|
334
|
+
}
|
|
335
|
+
if (source !== undefined) {
|
|
299
336
|
componentArgs['source'] = source;
|
|
337
|
+
}
|
|
300
338
|
return { args: componentArgs, stable: true };
|
|
301
339
|
}
|
|
302
340
|
return { args: { ref, action, args: actArgs }, stable: false };
|
|
@@ -314,12 +352,14 @@ export function compileSequenceStep(args, res) {
|
|
|
314
352
|
let stable = inputSteps.length > 0;
|
|
315
353
|
const subSteps = inputSteps.flatMap((raw, i) => {
|
|
316
354
|
// A step on Reticle's own panel is not a step of the app's journey, as for a single act.
|
|
317
|
-
if (true === asRecord(resolved[i])['reticleUi'])
|
|
355
|
+
if (true === asRecord(resolved[i])['reticleUi']) {
|
|
318
356
|
return [];
|
|
357
|
+
}
|
|
319
358
|
const step = asRecord(raw);
|
|
320
359
|
const compiled = compileAnchorArgs(asRecord(resolved[i]), asString(step['ref']) ?? '', asString(step['action']) ?? '', replayActionArgs(step['args']));
|
|
321
|
-
if (!compiled.stable)
|
|
360
|
+
if (!compiled.stable) {
|
|
322
361
|
stable = false;
|
|
362
|
+
}
|
|
323
363
|
// Each sub-step keeps its OWN declared consequence. A sequence records one step and replays as
|
|
324
364
|
// one batch, but what each step CLAIMED is per step -- folding them together would let one
|
|
325
365
|
// signal answer for every click in the journey.
|
|
@@ -346,18 +386,21 @@ export function compileSequenceStep(args, res) {
|
|
|
346
386
|
*/
|
|
347
387
|
async function resolveRef(session, step) {
|
|
348
388
|
// Dispatched the way it ran live: at the document, with no ref. See compileAnchorArgs.
|
|
349
|
-
if (isDocumentPress(step))
|
|
389
|
+
if (isDocumentPress(step)) {
|
|
350
390
|
return { ref: '' };
|
|
391
|
+
}
|
|
351
392
|
const by = asString(step.by);
|
|
352
393
|
const value = asString(step.value);
|
|
353
394
|
if (by === QueryBy.TESTID && value !== undefined) {
|
|
354
395
|
const result = await session.command(ReticleCommand.QUERY, { by, value });
|
|
355
|
-
if (!result.ok)
|
|
396
|
+
if (!result.ok) {
|
|
356
397
|
throw new Error(result.error ?? 'query failed');
|
|
398
|
+
}
|
|
357
399
|
const refs = queryRefs(result);
|
|
358
400
|
const ref = refs[0];
|
|
359
|
-
if (ref === undefined)
|
|
401
|
+
if (ref === undefined) {
|
|
360
402
|
throw new Error(`testid '${value}' did not resolve in current page`);
|
|
403
|
+
}
|
|
361
404
|
return refs.length > 1 ? { ref, note: ambiguousTestidNote(value) } : { ref };
|
|
362
405
|
}
|
|
363
406
|
// Role + accessible NAME: the anchor that distinguishes one row's control from another's, which
|
|
@@ -366,8 +409,9 @@ async function resolveRef(session, step) {
|
|
|
366
409
|
const name = asString(step.name);
|
|
367
410
|
const query = { by, value, ...(name === undefined ? {} : { name }) };
|
|
368
411
|
const result = await session.command(ReticleCommand.QUERY, query);
|
|
369
|
-
if (!result.ok)
|
|
412
|
+
if (!result.ok) {
|
|
370
413
|
throw new Error(result.error ?? 'query failed');
|
|
414
|
+
}
|
|
371
415
|
const refs = queryRefs(result);
|
|
372
416
|
const meant = refFor(step, refs);
|
|
373
417
|
const ref = meant ?? refs[0];
|
|
@@ -385,8 +429,9 @@ async function resolveRef(session, step) {
|
|
|
385
429
|
...(source === undefined ? {} : { source }),
|
|
386
430
|
};
|
|
387
431
|
const result = await session.command(ReticleCommand.QUERY, query);
|
|
388
|
-
if (!result.ok)
|
|
432
|
+
if (!result.ok) {
|
|
389
433
|
throw new Error(result.error ?? 'query failed');
|
|
434
|
+
}
|
|
390
435
|
const refs = queryRefs(result);
|
|
391
436
|
const ref = refs[0];
|
|
392
437
|
if (ref === undefined) {
|
|
@@ -468,8 +513,9 @@ via = []) {
|
|
|
468
513
|
...via,
|
|
469
514
|
]);
|
|
470
515
|
results.push(...nested);
|
|
471
|
-
if (nested.some((r) => !r.ok))
|
|
516
|
+
if (nested.some((r) => !r.ok)) {
|
|
472
517
|
break;
|
|
518
|
+
}
|
|
473
519
|
continue;
|
|
474
520
|
}
|
|
475
521
|
try {
|
|
@@ -480,8 +526,9 @@ via = []) {
|
|
|
480
526
|
for (const raw of subs) {
|
|
481
527
|
const sub = asRecord(raw);
|
|
482
528
|
const { ref, note } = await resolveRef(session, sub);
|
|
483
|
-
if (note !== undefined)
|
|
529
|
+
if (note !== undefined) {
|
|
484
530
|
notes.push(note);
|
|
531
|
+
}
|
|
485
532
|
liveSteps.push({
|
|
486
533
|
ref,
|
|
487
534
|
action: asString(sub['action']) ?? '',
|
|
@@ -502,8 +549,9 @@ via = []) {
|
|
|
502
549
|
session.finishAction();
|
|
503
550
|
}
|
|
504
551
|
results.push(buildResult(step.tool, r.ok, r.error, notes));
|
|
505
|
-
if (!r.ok)
|
|
552
|
+
if (!r.ok) {
|
|
506
553
|
break;
|
|
554
|
+
}
|
|
507
555
|
}
|
|
508
556
|
else {
|
|
509
557
|
const { ref, note } = await resolveRef(session, step.args);
|
|
@@ -520,8 +568,9 @@ via = []) {
|
|
|
520
568
|
session.finishAction();
|
|
521
569
|
}
|
|
522
570
|
results.push(buildResult(step.tool, r.ok, r.error, note !== undefined ? [note] : []));
|
|
523
|
-
if (!r.ok)
|
|
571
|
+
if (!r.ok) {
|
|
524
572
|
break;
|
|
573
|
+
}
|
|
525
574
|
}
|
|
526
575
|
}
|
|
527
576
|
catch (e) {
|
|
@@ -537,9 +586,11 @@ via = []) {
|
|
|
537
586
|
}
|
|
538
587
|
function buildResult(tool, ok, error, notes) {
|
|
539
588
|
const base = { tool, ok };
|
|
540
|
-
if (!ok)
|
|
589
|
+
if (!ok) {
|
|
541
590
|
base.error = error ?? 'command failed';
|
|
542
|
-
|
|
591
|
+
}
|
|
592
|
+
if (notes.length > 0) {
|
|
543
593
|
base.note = notes.join('; ');
|
|
594
|
+
}
|
|
544
595
|
return base;
|
|
545
596
|
}
|
|
@@ -87,6 +87,7 @@ interface ProbeMachine {
|
|
|
87
87
|
env: NodeJS.ProcessEnv;
|
|
88
88
|
exists: (path: string) => boolean;
|
|
89
89
|
pathProblem: (path: string) => ChromiumPathProblem | undefined;
|
|
90
|
+
platform?: NodeJS.Platform;
|
|
90
91
|
}
|
|
91
92
|
/**
|
|
92
93
|
* The lease preflight's probe: "can a launch succeed?", which now includes the Chrome/Edge fallback
|
package/dist/launch-chromium.js
CHANGED
|
@@ -48,11 +48,15 @@ const CHANNEL_PATHS = {
|
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
50
|
const WINDOWS_INSTALL_ROOTS = ['LOCALAPPDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)'];
|
|
51
|
+
function darwinChannelCandidates(appPath, env) {
|
|
52
|
+
const home = env['HOME']?.replace(/\/+$/, '');
|
|
53
|
+
return undefined === home || 0 === home.length ? [appPath] : [appPath, `${home}${appPath}`];
|
|
54
|
+
}
|
|
51
55
|
/** Candidate executable paths for a channel on this platform; empty where it is not installable. */
|
|
52
56
|
export function channelExecutableCandidates(channel, platform, env) {
|
|
53
57
|
const paths = CHANNEL_PATHS[channel];
|
|
54
58
|
if ('darwin' === platform)
|
|
55
|
-
return
|
|
59
|
+
return darwinChannelCandidates(paths.darwin, env);
|
|
56
60
|
if ('linux' === platform)
|
|
57
61
|
return [paths.linux];
|
|
58
62
|
if ('win32' === platform) {
|
|
@@ -63,9 +67,21 @@ export function channelExecutableCandidates(channel, platform, env) {
|
|
|
63
67
|
}
|
|
64
68
|
return [];
|
|
65
69
|
}
|
|
70
|
+
function findInstalledBrowser(deps) {
|
|
71
|
+
for (const channel of CHANNEL_ORDER) {
|
|
72
|
+
const found = channelExecutableCandidates(channel, deps.platform, deps.env).find((p) => deps.exists(p));
|
|
73
|
+
if (undefined !== found) {
|
|
74
|
+
// Playwright resolves a channel from its own registry (/Applications on macOS). A browser in
|
|
75
|
+
// ~/Applications is not in that registry, so launch it by executablePath instead.
|
|
76
|
+
const outsideRegistry = 'darwin' === deps.platform && found !== CHANNEL_PATHS[channel].darwin;
|
|
77
|
+
return outsideRegistry ? { channel, executablePath: found } : { channel };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
66
82
|
/** The first installed Chrome/Edge on this machine, or undefined. Looks only; installs nothing. */
|
|
67
83
|
export function findInstalledChannel(deps) {
|
|
68
|
-
return
|
|
84
|
+
return findInstalledBrowser(deps)?.channel;
|
|
69
85
|
}
|
|
70
86
|
const usingChannelLine = (channel) => `[reticle] Playwright's Chromium is not installed; using the installed ${CHANNEL_LABEL[channel]} instead.\n`;
|
|
71
87
|
const installingLine = (command) => `[reticle] No Chromium, Chrome or Edge found; installing Playwright's Chromium (${command}) — a one-time download…\n`;
|
|
@@ -98,10 +114,12 @@ export async function resolveChromiumTarget(deps) {
|
|
|
98
114
|
}
|
|
99
115
|
if (deps.exists(bundled))
|
|
100
116
|
return { found: true };
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
deps.announce(usingChannelLine(channel));
|
|
104
|
-
return
|
|
117
|
+
const installed = findInstalledBrowser(deps);
|
|
118
|
+
if (undefined !== installed) {
|
|
119
|
+
deps.announce(usingChannelLine(installed.channel));
|
|
120
|
+
return undefined === installed.executablePath
|
|
121
|
+
? { found: true, channel: installed.channel }
|
|
122
|
+
: { found: true, executablePath: installed.executablePath };
|
|
105
123
|
}
|
|
106
124
|
deps.announce(installingLine(chromiumInstallCommand(bundledPlaywrightVersion())));
|
|
107
125
|
const ok = await deps.install();
|
|
@@ -207,6 +225,7 @@ const THIS_MACHINE = {
|
|
|
207
225
|
env: process.env,
|
|
208
226
|
exists: existsSync,
|
|
209
227
|
pathProblem: (path) => chromiumPathProblem(path, process.platform),
|
|
228
|
+
platform: process.platform,
|
|
210
229
|
};
|
|
211
230
|
/**
|
|
212
231
|
* The lease preflight's probe: "can a launch succeed?", which now includes the Chrome/Edge fallback
|
|
@@ -217,16 +236,29 @@ export async function probeLaunchableChromium(machine = THIS_MACHINE) {
|
|
|
217
236
|
const configured = configuredChromiumProbe(machine.env, machine.pathProblem);
|
|
218
237
|
if (configured !== undefined)
|
|
219
238
|
return configured;
|
|
239
|
+
const platform = machine.platform ?? process.platform;
|
|
220
240
|
try {
|
|
221
241
|
const { chromium } = await import('playwright');
|
|
222
|
-
const target = await resolveChromiumTarget(
|
|
242
|
+
const target = await resolveChromiumTarget({
|
|
243
|
+
...defaultDeps(chromium),
|
|
244
|
+
exists: machine.exists,
|
|
245
|
+
pathProblem: machine.pathProblem,
|
|
246
|
+
platform,
|
|
247
|
+
env: machine.env,
|
|
248
|
+
...(machine === THIS_MACHINE
|
|
249
|
+
? {}
|
|
250
|
+
: {
|
|
251
|
+
install: () => Promise.resolve(false),
|
|
252
|
+
announce: () => { },
|
|
253
|
+
}),
|
|
254
|
+
});
|
|
223
255
|
if (target.found)
|
|
224
256
|
return { exists: true };
|
|
225
257
|
}
|
|
226
258
|
catch {
|
|
227
|
-
// No playwright at all —
|
|
259
|
+
// No playwright at all — probeChromiumWithFallback below says so, with where it looked.
|
|
228
260
|
}
|
|
229
|
-
return
|
|
261
|
+
return probeChromiumWithFallback(machine);
|
|
230
262
|
}
|
|
231
263
|
/**
|
|
232
264
|
* `doctor`'s probe: the plain Chromium probe, plus the installed browser that stands in when the
|
|
@@ -236,13 +268,18 @@ export async function probeChromiumWithFallback(machine = THIS_MACHINE) {
|
|
|
236
268
|
const configured = configuredChromiumProbe(machine.env, machine.pathProblem);
|
|
237
269
|
if (configured !== undefined)
|
|
238
270
|
return configured;
|
|
239
|
-
const probe = await probeChromium();
|
|
271
|
+
const probe = await probeChromium(machine.exists);
|
|
240
272
|
if (probe.exists)
|
|
241
273
|
return probe;
|
|
274
|
+
const platform = machine.platform ?? process.platform;
|
|
242
275
|
const channel = findInstalledChannel({
|
|
243
276
|
exists: machine.exists,
|
|
244
|
-
platform
|
|
277
|
+
platform,
|
|
245
278
|
env: machine.env,
|
|
246
279
|
});
|
|
247
|
-
|
|
280
|
+
if (undefined !== channel) {
|
|
281
|
+
return { ...probe, fallback: CHANNEL_LABEL[channel] };
|
|
282
|
+
}
|
|
283
|
+
const fallbackPaths = CHANNEL_ORDER.flatMap((ch) => channelExecutableCandidates(ch, platform, machine.env));
|
|
284
|
+
return 0 === fallbackPaths.length ? probe : { ...probe, fallbackPaths };
|
|
248
285
|
}
|
|
@@ -20,6 +20,11 @@ const HTTP_OK = 200;
|
|
|
20
20
|
const NOT_THERE = new Set([404, 405]);
|
|
21
21
|
/** Refused for good: too large (413), too many for one run (409). Retrying cannot change it. */
|
|
22
22
|
const REFUSED_FOR_GOOD = new Set([409, 413]);
|
|
23
|
+
/**
|
|
24
|
+
* The platform takes only JPEG and answers 400 to anything else, which stops the cycle. A desktop
|
|
25
|
+
* capture from a page SDK before 3.7 is a PNG, so it stays on this machine.
|
|
26
|
+
*/
|
|
27
|
+
const isJpeg = (image) => 0xff === image[0] && 0xd8 === image[1];
|
|
23
28
|
/** The screenshots a run artifact lists, read defensively: a malformed entry is skipped. */
|
|
24
29
|
function listed(payload) {
|
|
25
30
|
const raw = 'object' === typeof payload && null !== payload
|
|
@@ -78,7 +83,7 @@ export async function syncScreenshots(deps) {
|
|
|
78
83
|
done.add(keyOf(shot));
|
|
79
84
|
sent[run.runId] = [...done];
|
|
80
85
|
};
|
|
81
|
-
if (image.byteLength > STEP_SCREENSHOT.MAX_BYTES) {
|
|
86
|
+
if (image.byteLength > STEP_SCREENSHOT.MAX_BYTES || !isJpeg(image)) {
|
|
82
87
|
markDone();
|
|
83
88
|
continue;
|
|
84
89
|
}
|
|
@@ -57,6 +57,18 @@ export declare const WS_CLOSE_REASON: {
|
|
|
57
57
|
*/
|
|
58
58
|
readonly INVALID_HELLO_CLOSE: "invalid hello: version skew. Put SDK, build plugin and server on one release.";
|
|
59
59
|
};
|
|
60
|
+
/** WS close codes + reasons the bridge sends to the SDK (1008 = policy violation, 1013 = try again later). */
|
|
61
|
+
export declare const WS_CLOSE: {
|
|
62
|
+
readonly TOO_MANY_HANDSHAKES: readonly [1013, "too many pending handshakes"];
|
|
63
|
+
readonly HELLO_TIMEOUT: readonly [1008, "hello timeout"];
|
|
64
|
+
readonly INVALID_MESSAGE: readonly [1008, "invalid message"];
|
|
65
|
+
readonly INVALID_HELLO: readonly [1008, "invalid hello: version skew. Put SDK, build plugin and server on one release."];
|
|
66
|
+
readonly HELLO_DUPLICATE: readonly [1008, "hello already received"];
|
|
67
|
+
readonly AUTH_FAILED: readonly [1008, "authentication failed — restart the dev server; a reload keeps the old pairing token"];
|
|
68
|
+
readonly SESSION_LIMIT: readonly [1013, "session limit reached"];
|
|
69
|
+
readonly PROTOCOL_MISMATCH: readonly [1008, "protocol version mismatch — @reticlehq/browser and server disagree"];
|
|
70
|
+
readonly HANDLER_FAILED: readonly [1011, "the Reticle daemon failed handling a message — reload the page to reconnect"];
|
|
71
|
+
};
|
|
60
72
|
interface BridgeOptions {
|
|
61
73
|
port: number;
|
|
62
74
|
host?: string;
|
|
@@ -107,10 +119,7 @@ export declare class Bridge {
|
|
|
107
119
|
*/
|
|
108
120
|
attachSessionReady(handler: SessionReadyHandler): void;
|
|
109
121
|
/** Register a callback fired the instant a session is created — used to attach the durable journal. */
|
|
110
|
-
/**
|
|
111
|
-
* Teardown handler fired when a session is removed (tab closed/disconnected). Without it the journal's
|
|
112
|
-
* batched tail (< one flush batch) never reaches disk and the learned ambient map is discarded.
|
|
113
|
-
*/
|
|
122
|
+
/** Teardown when a session is removed (tab closed/disconnected). Additive — every handler runs. */
|
|
114
123
|
attachSessionEnd(handler: (session: Session) => Promise<void>): void;
|
|
115
124
|
/**
|
|
116
125
|
* Register what to do when the panel's sync button is pressed.
|
|
@@ -101,7 +101,7 @@ export const WS_CLOSE_REASON = {
|
|
|
101
101
|
INVALID_HELLO_CLOSE: 'invalid hello: version skew. Put SDK, build plugin and server on one release.',
|
|
102
102
|
};
|
|
103
103
|
/** WS close codes + reasons the bridge sends to the SDK (1008 = policy violation, 1013 = try again later). */
|
|
104
|
-
const WS_CLOSE = {
|
|
104
|
+
export const WS_CLOSE = {
|
|
105
105
|
TOO_MANY_HANDSHAKES: [1013, 'too many pending handshakes'],
|
|
106
106
|
HELLO_TIMEOUT: [1008, 'hello timeout'],
|
|
107
107
|
INVALID_MESSAGE: [1008, 'invalid message'],
|
|
@@ -190,8 +190,10 @@ export class Bridge {
|
|
|
190
190
|
*/
|
|
191
191
|
#onSessionCreate = [];
|
|
192
192
|
#onProjectAdopted = [];
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
// Fired when a session is removed. ADDITIVE: a single slot meant one teardown silently displaced
|
|
194
|
+
// the other — the same trap the create-side note describes. The durable journal flushes its tail;
|
|
195
|
+
// the page-error journal drops the session's dedupe memory.
|
|
196
|
+
#onSessionEnd = [];
|
|
195
197
|
/** Wired by the daemon: push to the dashboard now, because somebody asked in the panel. */
|
|
196
198
|
#onSyncRequest;
|
|
197
199
|
#onHarnessRequest;
|
|
@@ -737,7 +739,9 @@ export class Bridge {
|
|
|
737
739
|
log('session_disconnected', { sessionId: ended.id });
|
|
738
740
|
// Best-effort teardown: flush the journal tail + persist ambient learning. Never awaited here
|
|
739
741
|
// (the socket is already closing) and never allowed to reject into the close handler.
|
|
740
|
-
|
|
742
|
+
for (const handler of this.#onSessionEnd) {
|
|
743
|
+
void handler(ended).catch(() => undefined);
|
|
744
|
+
}
|
|
741
745
|
// AFTER teardown is started, and deliberately not awaited on it. The payload says a
|
|
742
746
|
// session ended, which is already true here; making it wait for the journal flush would
|
|
743
747
|
// put a hook in the path of a socket close for no gain to the consumer.
|
|
@@ -835,12 +839,9 @@ export class Bridge {
|
|
|
835
839
|
this.#onSessionReady.push(handler);
|
|
836
840
|
}
|
|
837
841
|
/** Register a callback fired the instant a session is created — used to attach the durable journal. */
|
|
838
|
-
/**
|
|
839
|
-
* Teardown handler fired when a session is removed (tab closed/disconnected). Without it the journal's
|
|
840
|
-
* batched tail (< one flush batch) never reaches disk and the learned ambient map is discarded.
|
|
841
|
-
*/
|
|
842
|
+
/** Teardown when a session is removed (tab closed/disconnected). Additive — every handler runs. */
|
|
842
843
|
attachSessionEnd(handler) {
|
|
843
|
-
this.#onSessionEnd
|
|
844
|
+
this.#onSessionEnd.push(handler);
|
|
844
845
|
}
|
|
845
846
|
/**
|
|
846
847
|
* Register what to do when the panel's sync button is pressed.
|
|
@@ -47,10 +47,11 @@ export function matchMock(rules, req) {
|
|
|
47
47
|
export async function applyOutcome(route, outcome, sleep) {
|
|
48
48
|
if ('continue' === outcome.kind) {
|
|
49
49
|
// Sleep FIRST, then let it through: the delay is the whole perturbation, and the response that
|
|
50
|
-
// comes back is the server's own.
|
|
50
|
+
// comes back is the server's own. Fallback, not continue: a lease's context route (the CSP
|
|
51
|
+
// widening, #2016) must still see the request.
|
|
51
52
|
if (outcome.delayMs !== undefined && outcome.delayMs > 0)
|
|
52
53
|
await sleep(outcome.delayMs);
|
|
53
|
-
await route.
|
|
54
|
+
await route.fallback();
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
57
|
if ('abort' === outcome.kind) {
|
|
@@ -181,6 +181,16 @@ type ConnectFn = (url: string) => Promise<Browser>;
|
|
|
181
181
|
export declare function performGesture(page: Page, action: ActionType, box: ElementBox, args: RealInputArgs, sleep: SleepFn,
|
|
182
182
|
/** Injected so a test can assert the achieved hold deterministically; defaults to the real clock. */
|
|
183
183
|
now?: () => number): Promise<RealInputResult>;
|
|
184
|
+
/**
|
|
185
|
+
* Run a real pointer gesture with Reticle's own UI out of the hit test, and put it back after.
|
|
186
|
+
*
|
|
187
|
+
* A real pointer goes to the topmost element under it. When the HUD sat over the app's control, a
|
|
188
|
+
* driven click went to the HUD and the verdict said the app did nothing; a drag ended short (#992).
|
|
189
|
+
* The restore runs whatever the gesture did, so the HUD does not stay unclickable for a person.
|
|
190
|
+
*/
|
|
191
|
+
export declare function withChromePassThrough<T>(page: {
|
|
192
|
+
evaluate: (script: string) => Promise<unknown>;
|
|
193
|
+
}, gesture: () => Promise<T>): Promise<T>;
|
|
184
194
|
/**
|
|
185
195
|
* Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
|
|
186
196
|
* screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
|
|
@@ -262,6 +272,11 @@ export interface LaunchedProviderOptions {
|
|
|
262
272
|
* inside-app view never loses fidelity to the outside-in view. Omitted → no network detail captured.
|
|
263
273
|
*/
|
|
264
274
|
onNetworkDetail?: (detail: NetworkDetail) => void;
|
|
275
|
+
/**
|
|
276
|
+
* Reticle's own reader for a page with no SDK, as the lease pool gives one (see zero-install.ts).
|
|
277
|
+
* Undefined, or a loader that answers undefined, leaves such a page with no session.
|
|
278
|
+
*/
|
|
279
|
+
zeroInstallScript?: () => string | undefined;
|
|
265
280
|
}
|
|
266
281
|
/** The only place the dynamic value import of Playwright lives for the launched (drive) path. */
|
|
267
282
|
export declare const launchedChromium: LaunchFn;
|