@enricai/barnacle 1.12.3 → 1.12.5
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/dist/lib/llm/anthropic-client.d.ts +10 -2
- package/dist/lib/llm/anthropic-client.d.ts.map +1 -1
- package/dist/lib/llm/anthropic-client.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/scraper/deep-locator-fake.js +1 -1
- package/dist/scraper/deep-locator-scan.d.ts +1 -1
- package/dist/scraper/deep-locator-scan.js +1 -1
- package/dist/scraper/flow-runner.d.ts +27 -4
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +248 -106
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/session-browserbase.d.ts.map +1 -1
- package/dist/scraper/session-browserbase.js +7 -0
- package/dist/scraper/session-browserbase.js.map +1 -1
- package/dist/scraper/session-steel.d.ts.map +1 -1
- package/dist/scraper/session-steel.js +7 -0
- package/dist/scraper/session-steel.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +33 -0
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +22 -0
- package/dist/scripts/recon-browser.js.map +1 -1
- package/package.json +5 -3
|
@@ -68,6 +68,7 @@ exports.parseSelectStep = parseSelectStep;
|
|
|
68
68
|
exports.parseFillStep = parseFillStep;
|
|
69
69
|
exports.parseFillValueIntent = parseFillValueIntent;
|
|
70
70
|
exports.parseRadioStep = parseRadioStep;
|
|
71
|
+
exports.parseWidgetOptionClickStep = parseWidgetOptionClickStep;
|
|
71
72
|
exports.parseCheckStep = parseCheckStep;
|
|
72
73
|
exports.pollEnumerate = pollEnumerate;
|
|
73
74
|
exports.waitForTransitionBody = waitForTransitionBody;
|
|
@@ -388,6 +389,13 @@ const SELECT_SETTLE_MS = 400;
|
|
|
388
389
|
* triggered it.
|
|
389
390
|
*/
|
|
390
391
|
const PROMPT_SELECTOR_SETTLE_MS = 400;
|
|
392
|
+
/**
|
|
393
|
+
* Cap on the category->leaf drill loop in {@link commitPromptOption}: a
|
|
394
|
+
* two-level cascading multiselect (category click re-renders the popup to
|
|
395
|
+
* leaves) only ever drills one level deep in the wild, so 3 gives headroom
|
|
396
|
+
* for a rarer 2-level cascade without letting a genuinely broken widget spin.
|
|
397
|
+
*/
|
|
398
|
+
const PROMPT_SELECTOR_MAX_DRILL_DEPTH = 3;
|
|
391
399
|
/**
|
|
392
400
|
* Temporary attribute {@link tryPromptSelectorPrimitive} stamps onto each
|
|
393
401
|
* candidate widget during its read-only enumerate pass, so the Node-side click
|
|
@@ -3814,6 +3822,48 @@ function parseRadioStep(instruction) {
|
|
|
3814
3822
|
const questionLabel = pickQuestionLabel(instruction, option);
|
|
3815
3823
|
return { option, questionLabel };
|
|
3816
3824
|
}
|
|
3825
|
+
/**
|
|
3826
|
+
* Parse a cascading-multiselect CATEGORY/LEAF widget-option click step into
|
|
3827
|
+
* the option to click.
|
|
3828
|
+
*
|
|
3829
|
+
* Why this exists (sibling of `parseRadioStep`): flow/replan generation
|
|
3830
|
+
* describes a two-level cascading multiselect's category and leaf clicks as
|
|
3831
|
+
* "click the top-level category 'Job Boards' to expand its sub-options" and
|
|
3832
|
+
* "click the leaf option 'Internet - Job Boards/Search Engines' to commit the
|
|
3833
|
+
* source value" — a `click` verb paired with a `category`/`option` noun that
|
|
3834
|
+
* `parseRadioStep` deliberately does not recognize (its `answer`/`radio` noun
|
|
3835
|
+
* set, and `tryRadioPrimitive`'s native `input[type=radio]` matching, are
|
|
3836
|
+
* scoped to actual radio groups). Left unrecognized, this phrasing matches
|
|
3837
|
+
* none of `tryPromptSelectorPrimitive`'s accepted shapes, so the primitive
|
|
3838
|
+
* returns before ever inspecting the DOM and the step falls to the observe
|
|
3839
|
+
* cascade's untrusted `el.click()`, which this widget family ignores.
|
|
3840
|
+
*
|
|
3841
|
+
* Deliberately excludes select/checkbox/radio-answer steps (checked first,
|
|
3842
|
+
* mirroring `parseRadioStep`'s own precedence) so a single step never
|
|
3843
|
+
* resolves through two primitives.
|
|
3844
|
+
*/
|
|
3845
|
+
function parseWidgetOptionClickStep(instruction) {
|
|
3846
|
+
const lower = instruction.toLowerCase();
|
|
3847
|
+
if (/\bselect(\s+or\s+check)?\b/.test(lower))
|
|
3848
|
+
return null;
|
|
3849
|
+
if (!/\bclick\b/.test(lower))
|
|
3850
|
+
return null;
|
|
3851
|
+
if (!/\b(category|option)\b/.test(lower))
|
|
3852
|
+
return null;
|
|
3853
|
+
// Catch-all steps ("for any remaining…") have no concrete single target.
|
|
3854
|
+
if (/\bany\s+remaining\b/.test(lower))
|
|
3855
|
+
return null;
|
|
3856
|
+
// The OPTION is the quoted string after "click the <category|option noun>",
|
|
3857
|
+
// tolerant of a short qualifier ("top-level", "leaf") between the noun and
|
|
3858
|
+
// the quote — e.g. "click the top-level category 'Job Boards'" or "click
|
|
3859
|
+
// the leaf option 'Internet - Job Boards/Search Engines'".
|
|
3860
|
+
const optMatch = instruction.match(/\bclick\s+the\s+(?:\S+\s+)?(?:category|option)\s+'([^']+)'/i);
|
|
3861
|
+
if (!optMatch)
|
|
3862
|
+
return null;
|
|
3863
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by the !optMatch early-return; group 1 is required by the pattern
|
|
3864
|
+
const option = optMatch[1].trim();
|
|
3865
|
+
return { option };
|
|
3866
|
+
}
|
|
3817
3867
|
/**
|
|
3818
3868
|
* Parse a CHECKBOX flow step into the label of the checkbox it targets.
|
|
3819
3869
|
*
|
|
@@ -5020,19 +5070,29 @@ async function tryPromptSelectorPrimitive(params) {
|
|
|
5020
5070
|
// than a SELECT step, and its Yes/No variant renders no native radio inputs
|
|
5021
5071
|
// at all, so flow/replan generation describes it with the same answer-verb
|
|
5022
5072
|
// phrasing used for native radios ("Click the 'Yes' answer for the question
|
|
5023
|
-
// '…'").
|
|
5024
|
-
// (
|
|
5025
|
-
//
|
|
5026
|
-
//
|
|
5027
|
-
//
|
|
5073
|
+
// '…'"). Also accepts a cascading-multiselect CATEGORY/LEAF click step
|
|
5074
|
+
// (`parseWidgetOptionClickStep`, "click the top-level category 'X'" /
|
|
5075
|
+
// "click the leaf option 'Y'") — that widget's category/leaf clicks carry
|
|
5076
|
+
// no question label, matched instead by the single-unfilled-widget rule
|
|
5077
|
+
// below. Accept all four shapes: parseSelectStep first, then
|
|
5078
|
+
// parseFillStep (fieldLabel -> questionLabel, value -> option), then
|
|
5079
|
+
// parseRadioStep (option/questionLabel already in this primitive's
|
|
5080
|
+
// shape), then parseWidgetOptionClickStep (option only, no question
|
|
5081
|
+
// label) so the widget-matching/open/readback phases below run unchanged
|
|
5082
|
+
// regardless of which verb the instruction used.
|
|
5028
5083
|
const parsedSelect = parseSelectStep(instruction);
|
|
5029
5084
|
const parsedFill = parsedSelect ? null : parseFillStep(instruction);
|
|
5030
5085
|
const parsedAnswer = parsedSelect || parsedFill ? null : parseRadioStep(instruction);
|
|
5086
|
+
const parsedOptionClick = parsedSelect || parsedFill || parsedAnswer ? null : parseWidgetOptionClickStep(instruction);
|
|
5031
5087
|
const parsed = parsedSelect
|
|
5032
5088
|
? parsedSelect
|
|
5033
5089
|
: parsedFill
|
|
5034
5090
|
? { option: parsedFill.value, questionLabel: stripQuotedLabel(parsedFill.fieldLabel) }
|
|
5035
|
-
: parsedAnswer
|
|
5091
|
+
: parsedAnswer
|
|
5092
|
+
? parsedAnswer
|
|
5093
|
+
: parsedOptionClick
|
|
5094
|
+
? { option: parsedOptionClick.option, questionLabel: null }
|
|
5095
|
+
: null;
|
|
5036
5096
|
if (!parsed)
|
|
5037
5097
|
return null;
|
|
5038
5098
|
const { option, questionLabel } = parsed;
|
|
@@ -5167,33 +5227,6 @@ async function tryPromptSelectorPrimitive(params) {
|
|
|
5167
5227
|
logger.info(`prompt-selector primitive: no unambiguous widget match for ${optLabel}; falling through to cascade`);
|
|
5168
5228
|
return null;
|
|
5169
5229
|
}
|
|
5170
|
-
// Phase 2 (real gesture): open the popup. These widgets' trigger requires a
|
|
5171
|
-
// genuine click — a synthetic dispatchEvent does not fire its handler.
|
|
5172
|
-
// Prefer the marked widget's own interactive descendant (a real form
|
|
5173
|
-
// control) over the marked container itself, since the container is
|
|
5174
|
-
// frequently a non-interactive layout wrapper for widget shapes whose
|
|
5175
|
-
// actual trigger sits a level deeper (see PROMPT_INTERACTIVE_CONTROL_SELECTORS).
|
|
5176
|
-
const containerTriggerSel = `[${PROMPT_WIDGET_MARK_ATTR}="${chosen.wIdx}"]`;
|
|
5177
|
-
const innerTriggerSel = PROMPT_INTERACTIVE_CONTROL_SELECTORS.split(",")
|
|
5178
|
-
.map((s) => `${containerTriggerSel} ${s}`)
|
|
5179
|
-
.join(",");
|
|
5180
|
-
let triggerSel = containerTriggerSel;
|
|
5181
|
-
try {
|
|
5182
|
-
const innerCount = await target.locator(innerTriggerSel).count();
|
|
5183
|
-
if (innerCount > 0)
|
|
5184
|
-
triggerSel = innerTriggerSel;
|
|
5185
|
-
}
|
|
5186
|
-
catch {
|
|
5187
|
-
// Fall back to the container selector.
|
|
5188
|
-
}
|
|
5189
|
-
try {
|
|
5190
|
-
await target.locator(triggerSel).first().click();
|
|
5191
|
-
}
|
|
5192
|
-
catch (err) {
|
|
5193
|
-
logger.info(`prompt-selector primitive: trigger click failed for ${optLabel}: ${(0, errors_1.toErrorMessage)(err)}; falling through`);
|
|
5194
|
-
return null;
|
|
5195
|
-
}
|
|
5196
|
-
await page.waitForTimeout(PROMPT_SELECTOR_SETTLE_MS);
|
|
5197
5230
|
const enumerateOptionsExpr = `((widgetMarkAttr, wIdx, markAttr, optionSel, searchSel) => {
|
|
5198
5231
|
const w = document.querySelector("[" + widgetMarkAttr + '="' + wIdx + '"]');
|
|
5199
5232
|
if (!w) return { optionsPresent: false };
|
|
@@ -5232,9 +5265,65 @@ async function tryPromptSelectorPrimitive(params) {
|
|
|
5232
5265
|
const label = opts[i].getAttribute("data-automation-label") || opts[i].getAttribute("aria-label") || opts[i].textContent || "";
|
|
5233
5266
|
options.push({ oIdx: i, text: label.replace(/\\s+/g, " ").trim() });
|
|
5234
5267
|
}
|
|
5235
|
-
|
|
5268
|
+
// scopeIsDocument distinguishes a genuinely resolved (portal or inline)
|
|
5269
|
+
// popup from the document-wide last-resort fallback in
|
|
5270
|
+
// PROMPT_SCOPE_ROOT_EXPR — the pre-open pre-check below must NOT treat a
|
|
5271
|
+
// still-unopened widget as "already open" on the strength of some OTHER
|
|
5272
|
+
// widget's stale/leftover popup elsewhere in the document; only a scope
|
|
5273
|
+
// that resolved to THIS widget's own inline subtree or its own
|
|
5274
|
+
// aria-controls/aria-owns target counts.
|
|
5275
|
+
return { optionsPresent: true, searchable: !!searchInput, options, scopeIsDocument: scope === document };
|
|
5236
5276
|
})(${JSON.stringify(PROMPT_WIDGET_MARK_ATTR)}, ${JSON.stringify(chosen.wIdx)}, ${JSON.stringify(PROMPT_OPTION_MARK_ATTR)}, ${JSON.stringify(PROMPT_OPTION_SELECTORS)}, ${JSON.stringify(PROMPT_SEARCH_SELECTORS)})`;
|
|
5237
|
-
|
|
5277
|
+
// Pre-check (single evaluate, no polling): a PRIOR primitive call on this
|
|
5278
|
+
// same unreloaded page may have already opened/drilled this exact widget's
|
|
5279
|
+
// popup (a two-level cascading multiselect authored as two flow steps —
|
|
5280
|
+
// category, then leaf — re-enters here for the leaf with the popup already
|
|
5281
|
+
// open). Re-clicking the trigger in that case perturbs/closes the
|
|
5282
|
+
// already-rendered popup instead of reading it. Skip the open-click
|
|
5283
|
+
// entirely when options are already present AND resolved to THIS widget's
|
|
5284
|
+
// OWN scope (not the document-wide fallback, which could otherwise credit
|
|
5285
|
+
// an unrelated widget's stale/leftover popup elsewhere on the page as
|
|
5286
|
+
// "already open"); a genuinely closed popup falls through to the normal
|
|
5287
|
+
// open-click + poll path below unchanged.
|
|
5288
|
+
const precheck = (await target.evaluate(enumerateOptionsExpr));
|
|
5289
|
+
const alreadyOpen = precheck?.optionsPresent === true &&
|
|
5290
|
+
(precheck.options?.length ?? 0) > 0 &&
|
|
5291
|
+
precheck.scopeIsDocument !== true;
|
|
5292
|
+
if (!alreadyOpen) {
|
|
5293
|
+
// Phase 2 (real gesture): open the popup. These widgets' trigger requires a
|
|
5294
|
+
// genuine click — a synthetic dispatchEvent does not fire its handler.
|
|
5295
|
+
// Prefer the marked widget's own interactive descendant (a real form
|
|
5296
|
+
// control) over the marked container itself, since the container is
|
|
5297
|
+
// frequently a non-interactive layout wrapper for widget shapes whose
|
|
5298
|
+
// actual trigger sits a level deeper (see PROMPT_INTERACTIVE_CONTROL_SELECTORS).
|
|
5299
|
+
const containerTriggerSel = `[${PROMPT_WIDGET_MARK_ATTR}="${chosen.wIdx}"]`;
|
|
5300
|
+
const innerTriggerSel = PROMPT_INTERACTIVE_CONTROL_SELECTORS.split(",")
|
|
5301
|
+
.map((s) => `${containerTriggerSel} ${s}`)
|
|
5302
|
+
.join(",");
|
|
5303
|
+
let triggerSel = containerTriggerSel;
|
|
5304
|
+
try {
|
|
5305
|
+
const innerCount = await target.locator(innerTriggerSel).count();
|
|
5306
|
+
if (innerCount > 0)
|
|
5307
|
+
triggerSel = innerTriggerSel;
|
|
5308
|
+
}
|
|
5309
|
+
catch {
|
|
5310
|
+
// Fall back to the container selector.
|
|
5311
|
+
}
|
|
5312
|
+
try {
|
|
5313
|
+
await target.locator(triggerSel).first().click();
|
|
5314
|
+
}
|
|
5315
|
+
catch (err) {
|
|
5316
|
+
logger.info(`prompt-selector primitive: trigger click failed for ${optLabel}: ${(0, errors_1.toErrorMessage)(err)}; falling through`);
|
|
5317
|
+
return null;
|
|
5318
|
+
}
|
|
5319
|
+
await page.waitForTimeout(PROMPT_SELECTOR_SETTLE_MS);
|
|
5320
|
+
}
|
|
5321
|
+
else {
|
|
5322
|
+
logger.info(`prompt-selector primitive: popup for ${optLabel} already open with rendered options; skipping trigger click`);
|
|
5323
|
+
}
|
|
5324
|
+
const optionsInitial = alreadyOpen
|
|
5325
|
+
? precheck
|
|
5326
|
+
: await pollEnumerate(page, target, enumerateOptionsExpr, (r) => r?.optionsPresent === true);
|
|
5238
5327
|
if (!optionsInitial?.optionsPresent) {
|
|
5239
5328
|
logger.info(`prompt-selector primitive: popup for ${optLabel} did not render options; falling through`);
|
|
5240
5329
|
return null;
|
|
@@ -5257,6 +5346,7 @@ async function tryPromptSelectorPrimitive(params) {
|
|
|
5257
5346
|
questionLabel,
|
|
5258
5347
|
chosen,
|
|
5259
5348
|
optionsResult,
|
|
5349
|
+
enumerateOptionsExpr,
|
|
5260
5350
|
});
|
|
5261
5351
|
}
|
|
5262
5352
|
try {
|
|
@@ -5290,6 +5380,7 @@ async function tryPromptSelectorPrimitive(params) {
|
|
|
5290
5380
|
questionLabel,
|
|
5291
5381
|
chosen,
|
|
5292
5382
|
optionsResult: optionsFiltered,
|
|
5383
|
+
enumerateOptionsExpr,
|
|
5293
5384
|
});
|
|
5294
5385
|
}
|
|
5295
5386
|
catch (err) {
|
|
@@ -5306,82 +5397,115 @@ async function tryPromptSelectorPrimitive(params) {
|
|
|
5306
5397
|
* by the value union, or its invalid marker cleared). Split out from the main
|
|
5307
5398
|
* function because both the static and searchable-then-filtered branches need
|
|
5308
5399
|
* the identical match/click/verify sequence.
|
|
5400
|
+
*
|
|
5401
|
+
* Bounded (see {@link PROMPT_SELECTOR_MAX_DRILL_DEPTH}) category->leaf drill:
|
|
5402
|
+
* when a click's readback fails, that is ambiguous between a genuine
|
|
5403
|
+
* non-commit and a category click that swapped the popup to a deeper level
|
|
5404
|
+
* (a two-level cascading multiselect authored as a SINGLE step naming only
|
|
5405
|
+
* the leaf). Re-enumerating after a failed readback and comparing the
|
|
5406
|
+
* option set disambiguates the two: an unchanged set is a real failure and
|
|
5407
|
+
* must still fall through to the cascade; a changed set means the drill
|
|
5408
|
+
* fired, so re-match/re-click continues at the new level instead of
|
|
5409
|
+
* abandoning the primitive.
|
|
5309
5410
|
*/
|
|
5310
5411
|
async function commitPromptOption(params) {
|
|
5311
|
-
const { page, target, logger, anthropic, captureFn, optLabel, option, questionLabel, chosen,
|
|
5412
|
+
const { page, target, logger, anthropic, captureFn, optLabel, option, questionLabel, chosen, enumerateOptionsExpr, } = params;
|
|
5312
5413
|
const norm = (s) => s.replace(/\s+/g, " ").trim().toLowerCase();
|
|
5313
5414
|
const wantOpt = norm(option);
|
|
5314
|
-
const
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5415
|
+
const optionSetKey = (opts) => opts
|
|
5416
|
+
.map((o) => norm(o.text))
|
|
5417
|
+
.sort()
|
|
5418
|
+
.join("");
|
|
5419
|
+
let optionsResult = params.optionsResult;
|
|
5420
|
+
for (let depth = 0; depth <= PROMPT_SELECTOR_MAX_DRILL_DEPTH; depth++) {
|
|
5421
|
+
const options = optionsResult.options ?? [];
|
|
5422
|
+
const detMatch = options.find((o) => norm(o.text) === wantOpt) ?? null;
|
|
5423
|
+
if (detMatch === null && (anthropic === null || options.length === 0)) {
|
|
5424
|
+
logger.info(`prompt-selector primitive: no option match for ${optLabel}${anthropic === null ? " (no LLM client)" : ""}; falling through to cascade`);
|
|
5425
|
+
return null;
|
|
5426
|
+
}
|
|
5427
|
+
const verdict = detMatch === null
|
|
5428
|
+
? await (0, select_option_1.judgeSelectOptionWithLLM)({
|
|
5429
|
+
client: anthropic,
|
|
5430
|
+
input: {
|
|
5431
|
+
questionLabel,
|
|
5432
|
+
desiredHint: option,
|
|
5433
|
+
candidates: [{ label: chosen.label || null, options: options.map((o) => o.text) }],
|
|
5434
|
+
},
|
|
5435
|
+
captureFn,
|
|
5436
|
+
})
|
|
5437
|
+
: null;
|
|
5438
|
+
if (detMatch === null &&
|
|
5439
|
+
(!verdict || verdict.selectIndex === null || verdict.optionIndex === null)) {
|
|
5440
|
+
logger.info(`prompt-selector primitive: LLM found no matching option for ${optLabel}${verdict ? ` (${verdict.reason})` : ""}; falling through to cascade`);
|
|
5441
|
+
return null;
|
|
5442
|
+
}
|
|
5443
|
+
const chosenOption = detMatch ??
|
|
5444
|
+
(verdict && verdict.optionIndex !== null ? (options[verdict.optionIndex] ?? null) : null);
|
|
5445
|
+
if (!chosenOption) {
|
|
5446
|
+
logger.info(`prompt-selector primitive: option match out of range for ${optLabel}; falling through`);
|
|
5447
|
+
return null;
|
|
5448
|
+
}
|
|
5449
|
+
const matchReason = detMatch ? "deterministic" : `LLM: ${(verdict?.reason ?? "").slice(0, 60)}`;
|
|
5450
|
+
const optionSel = `[${PROMPT_OPTION_MARK_ATTR}="${chosenOption.oIdx}"]`;
|
|
5451
|
+
try {
|
|
5452
|
+
await target.locator(optionSel).first().click();
|
|
5453
|
+
}
|
|
5454
|
+
catch (err) {
|
|
5455
|
+
logger.info(`prompt-selector primitive: option click failed for ${optLabel}: ${(0, errors_1.toErrorMessage)(err)}; falling through`);
|
|
5456
|
+
return null;
|
|
5457
|
+
}
|
|
5458
|
+
await page.waitForTimeout(PROMPT_SELECTOR_SETTLE_MS);
|
|
5459
|
+
const readbackExpr = `((wIdx, markAttr, valueSel, emptyRxSrc, emptyRxFlags, wantText) => {
|
|
5460
|
+
const isInvalid = ${INVALID_MARKER_EL_EXPR};
|
|
5461
|
+
const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
|
|
5462
|
+
const buttonValue = ${BUTTON_VALUE_EXPR};
|
|
5463
|
+
const emptyRx = new RegExp(emptyRxSrc, emptyRxFlags);
|
|
5464
|
+
const w = document.querySelector("[" + markAttr + '="' + wIdx + '"]');
|
|
5465
|
+
if (!w) return { ok: false, id: "" };
|
|
5466
|
+
// Value via the union: aria-activedescendant, own input value, a <button>'s
|
|
5467
|
+
// own value text (popup-pollution-safe, see BUTTON_VALUE_EXPR), or a
|
|
5468
|
+
// selection-label node (empty-state phrase treated as no value).
|
|
5469
|
+
const adid = w.getAttribute && w.getAttribute("aria-activedescendant");
|
|
5470
|
+
const adText = adid && document.getElementById(adid) ? norm(document.getElementById(adid).textContent) : "";
|
|
5471
|
+
const own = w.tagName === "INPUT" ? norm(w.value || "") : w.tagName === "BUTTON" ? norm(buttonValue(w)) : "";
|
|
5472
|
+
const lbl = w.matches(valueSel) ? w : w.querySelector(valueSel);
|
|
5473
|
+
const lblRaw = lbl ? (lbl.textContent || "") : "";
|
|
5474
|
+
const lblText = lblRaw.trim() && !emptyRx.test(lblRaw.trim()) ? norm(lblRaw) : "";
|
|
5475
|
+
const text = adText || own || lblText;
|
|
5476
|
+
const textMatches = wantText ? text.includes(wantText) : text !== "";
|
|
5477
|
+
let node = w;
|
|
5478
|
+
let stillInvalid = false;
|
|
5479
|
+
for (let d = 0; d < 6 && node; d++) {
|
|
5480
|
+
if (node.getAttribute && isInvalid(node)) { stillInvalid = true; break; }
|
|
5481
|
+
node = node.parentElement;
|
|
5482
|
+
}
|
|
5483
|
+
return { ok: textMatches || !stillInvalid, id: w.id || "" };
|
|
5484
|
+
})(${JSON.stringify(chosen.wIdx)}, ${JSON.stringify(PROMPT_WIDGET_MARK_ATTR)}, ${JSON.stringify(PROMPT_VALUE_SELECTORS)}, ${JSON.stringify(PROMPT_EMPTY_VALUE_RX_SRC)}, ${JSON.stringify(PROMPT_EMPTY_VALUE_RX_FLAGS)}, ${JSON.stringify(norm(chosenOption.text))})`;
|
|
5485
|
+
const readback = (await target.evaluate(readbackExpr).catch(() => ({ ok: false, id: "" })));
|
|
5486
|
+
if (readback?.ok) {
|
|
5487
|
+
logger.info(`prompt-selector primitive: selected "${chosenOption.text.slice(0, 40)}" for ${optLabel} (${matchReason})`);
|
|
5488
|
+
return readback.id;
|
|
5489
|
+
}
|
|
5490
|
+
if (depth === PROMPT_SELECTOR_MAX_DRILL_DEPTH) {
|
|
5491
|
+
logger.info(`prompt-selector primitive: selection for ${optLabel} did not commit; falling through to cascade`);
|
|
5492
|
+
return null;
|
|
5493
|
+
}
|
|
5494
|
+
// Readback failed — disambiguate a genuine non-commit from a
|
|
5495
|
+
// category->leaf drill by re-enumerating and comparing the option set.
|
|
5496
|
+
const reEnumerated = (await target
|
|
5497
|
+
.evaluate(enumerateOptionsExpr)
|
|
5498
|
+
.catch(() => ({ optionsPresent: false })));
|
|
5499
|
+
const newOptions = reEnumerated?.optionsPresent ? (reEnumerated.options ?? []) : [];
|
|
5500
|
+
const drilled = newOptions.length > 0 && optionSetKey(newOptions) !== optionSetKey(options);
|
|
5501
|
+
if (!drilled) {
|
|
5502
|
+
logger.info(`prompt-selector primitive: selection for ${optLabel} did not commit; falling through to cascade`);
|
|
5503
|
+
return null;
|
|
5504
|
+
}
|
|
5505
|
+
logger.info(`prompt-selector primitive: click for ${optLabel} drilled the popup to a new option set; re-matching at the new level`);
|
|
5506
|
+
optionsResult = { options: newOptions };
|
|
5382
5507
|
}
|
|
5383
|
-
|
|
5384
|
-
return readback.id;
|
|
5508
|
+
return null;
|
|
5385
5509
|
}
|
|
5386
5510
|
/**
|
|
5387
5511
|
* Guard for the optional-step fast-skip: is there a REQUIRED, still-empty (or
|
|
@@ -8601,6 +8725,24 @@ async function runHealingFlow(deps) {
|
|
|
8601
8725
|
if (maxFlowMs !== undefined && Date.now() - start > maxFlowMs) {
|
|
8602
8726
|
throw new errors_2.StepVerificationError(`${formatStepPrefix(i, () => steps.length)} flow exceeded its maxFlowMs budget (${maxFlowMs}ms)`, "flow-timeout");
|
|
8603
8727
|
}
|
|
8728
|
+
// Liveness gate: a closed/crashed Stagehand session (e.g. the
|
|
8729
|
+
// shutdown-supervisor force-releasing mid-flow) makes `page.url()`
|
|
8730
|
+
// throw synchronously — the one call every Page implementation
|
|
8731
|
+
// answers without touching the DOM. Everything downstream (observe/
|
|
8732
|
+
// act probes) treats a thrown error as "page has no candidates right
|
|
8733
|
+
// now" and, for an optional step, quietly skips it — so without this
|
|
8734
|
+
// check the loop runs to completion and `runHealingFlow` resolves as
|
|
8735
|
+
// if the flow finished, even though the session died partway through.
|
|
8736
|
+
// Checking here, before any step-specific handling, means a dead
|
|
8737
|
+
// session is reported as a `SessionTimeoutError` distinct from a
|
|
8738
|
+
// legitimately-absent optional step, and the flow's own step count
|
|
8739
|
+
// never quietly reaches `steps.length` past the point of death.
|
|
8740
|
+
try {
|
|
8741
|
+
page.url();
|
|
8742
|
+
}
|
|
8743
|
+
catch (err) {
|
|
8744
|
+
throw new errors_2.SessionTimeoutError(`${formatStepPrefix(i, () => steps.length)} session appears closed/dead (page.url() threw: ${(0, errors_1.toErrorMessage)(err)}) — aborting after ${i} of ${steps.length} steps completed`);
|
|
8745
|
+
}
|
|
8604
8746
|
lastStepIndex = i;
|
|
8605
8747
|
// Resolved fresh per step (not cached across the run) so a cross-origin
|
|
8606
8748
|
// iframe that attaches mid-flow (e.g. after an "Apply" click reveals a
|