pi-agent-browser-native 0.2.48 → 0.2.49
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/CHANGELOG.md +17 -0
- package/README.md +16 -6
- package/dist/extensions/agent-browser/index.js +785 -0
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +71 -0
- package/dist/extensions/agent-browser/lib/argv-grammar.js +121 -0
- package/dist/extensions/agent-browser/lib/bash-guard.js +190 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +85 -0
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +302 -0
- package/dist/extensions/agent-browser/lib/config-policy.js +686 -0
- package/dist/extensions/agent-browser/lib/config.js +122 -0
- package/dist/extensions/agent-browser/lib/electron/cdp.js +51 -0
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +212 -0
- package/dist/extensions/agent-browser/lib/electron/discovery.js +633 -0
- package/dist/extensions/agent-browser/lib/electron/launch.js +351 -0
- package/{extensions/agent-browser/lib/electron/text.ts → dist/extensions/agent-browser/lib/electron/text.js} +5 -5
- package/dist/extensions/agent-browser/lib/executable-path.js +20 -0
- package/dist/extensions/agent-browser/lib/fs-utils.js +18 -0
- package/dist/extensions/agent-browser/lib/input-modes/electron.js +165 -0
- package/dist/extensions/agent-browser/lib/input-modes/job.js +519 -0
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +440 -0
- package/dist/extensions/agent-browser/lib/input-modes/params.js +164 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +119 -0
- package/dist/extensions/agent-browser/lib/input-modes/shared.js +42 -0
- package/dist/extensions/agent-browser/lib/input-modes/types.js +21 -0
- package/dist/extensions/agent-browser/lib/input-modes.js +10 -0
- package/dist/extensions/agent-browser/lib/json-schema.js +58 -0
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +59 -0
- package/dist/extensions/agent-browser/lib/navigation-policy.js +83 -0
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +62 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +39 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +276 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +909 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +443 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +47 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +141 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +108 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +112 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +158 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +54 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +762 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +491 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.js +40 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +5 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +731 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/types.js +1 -0
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +718 -0
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +247 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +68 -0
- package/{extensions/agent-browser/lib/parsing.ts → dist/extensions/agent-browser/lib/parsing.js} +12 -11
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +241 -0
- package/dist/extensions/agent-browser/lib/playbook.js +121 -0
- package/dist/extensions/agent-browser/lib/process.js +448 -0
- package/dist/extensions/agent-browser/lib/prompt-policy.js +91 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +220 -0
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +111 -0
- package/{extensions/agent-browser/lib/results/artifact-state.ts → dist/extensions/agent-browser/lib/results/artifact-state.js} +4 -8
- package/dist/extensions/agent-browser/lib/results/categories.js +76 -0
- package/dist/extensions/agent-browser/lib/results/confirmation.js +63 -0
- package/dist/extensions/agent-browser/lib/results/contracts.js +8 -0
- package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +74 -0
- package/dist/extensions/agent-browser/lib/results/envelope.js +166 -0
- package/dist/extensions/agent-browser/lib/results/network-routes.js +92 -0
- package/dist/extensions/agent-browser/lib/results/network.js +73 -0
- package/dist/extensions/agent-browser/lib/results/next-actions.js +72 -0
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +515 -0
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +397 -0
- package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +55 -0
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +46 -0
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +24 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +960 -0
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +205 -0
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +134 -0
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +159 -0
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +216 -0
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +104 -0
- package/dist/extensions/agent-browser/lib/results/presentation/skills.js +152 -0
- package/dist/extensions/agent-browser/lib/results/presentation.js +177 -0
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +107 -0
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +50 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +225 -0
- package/{extensions/agent-browser/lib/results/shared.ts → dist/extensions/agent-browser/lib/results/shared.js} +0 -1
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +208 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +78 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +331 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +40 -0
- package/dist/extensions/agent-browser/lib/results/snapshot.js +264 -0
- package/dist/extensions/agent-browser/lib/results/text.js +40 -0
- package/{extensions/agent-browser/lib/results.ts → dist/extensions/agent-browser/lib/results.js} +2 -32
- package/dist/extensions/agent-browser/lib/runtime.js +816 -0
- package/dist/extensions/agent-browser/lib/session-page-state.js +411 -0
- package/dist/extensions/agent-browser/lib/string-enum-schema.js +13 -0
- package/dist/extensions/agent-browser/lib/temp.js +498 -0
- package/dist/extensions/agent-browser/lib/web-search.js +562 -0
- package/docs/RELEASE.md +22 -11
- package/docs/SUPPORT_MATRIX.md +4 -3
- package/package.json +9 -5
- package/scripts/config.mjs +8 -2
- package/scripts/doctor.mjs +8 -7
- package/extensions/agent-browser/index.ts +0 -961
- package/extensions/agent-browser/lib/argv-descriptor.ts +0 -90
- package/extensions/agent-browser/lib/argv-grammar.ts +0 -128
- package/extensions/agent-browser/lib/bash-guard.ts +0 -205
- package/extensions/agent-browser/lib/command-policy.ts +0 -71
- package/extensions/agent-browser/lib/command-taxonomy.ts +0 -336
- package/extensions/agent-browser/lib/config-policy.js +0 -690
- package/extensions/agent-browser/lib/config.ts +0 -211
- package/extensions/agent-browser/lib/electron/cdp.ts +0 -69
- package/extensions/agent-browser/lib/electron/cleanup.ts +0 -235
- package/extensions/agent-browser/lib/electron/discovery.ts +0 -710
- package/extensions/agent-browser/lib/electron/launch.ts +0 -499
- package/extensions/agent-browser/lib/executable-path.ts +0 -19
- package/extensions/agent-browser/lib/fs-utils.ts +0 -18
- package/extensions/agent-browser/lib/input-modes/electron.ts +0 -170
- package/extensions/agent-browser/lib/input-modes/job.ts +0 -527
- package/extensions/agent-browser/lib/input-modes/lookups.ts +0 -447
- package/extensions/agent-browser/lib/input-modes/params.ts +0 -205
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +0 -127
- package/extensions/agent-browser/lib/input-modes/shared.ts +0 -46
- package/extensions/agent-browser/lib/input-modes/types.ts +0 -225
- package/extensions/agent-browser/lib/input-modes.ts +0 -45
- package/extensions/agent-browser/lib/json-schema.ts +0 -73
- package/extensions/agent-browser/lib/launch-scoped-flags.ts +0 -67
- package/extensions/agent-browser/lib/navigation-policy.ts +0 -95
- package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +0 -65
- package/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.ts +0 -44
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +0 -280
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +0 -914
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +0 -521
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +0 -53
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.ts +0 -158
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.ts +0 -116
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.ts +0 -147
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.ts +0 -183
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.ts +0 -58
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +0 -847
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +0 -559
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -47
- package/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.ts +0 -8
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +0 -868
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +0 -565
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +0 -855
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +0 -375
- package/extensions/agent-browser/lib/orchestration/output-file.ts +0 -86
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +0 -267
- package/extensions/agent-browser/lib/playbook.ts +0 -142
- package/extensions/agent-browser/lib/process.ts +0 -516
- package/extensions/agent-browser/lib/prompt-policy.ts +0 -105
- package/extensions/agent-browser/lib/results/action-recommendations.ts +0 -264
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +0 -111
- package/extensions/agent-browser/lib/results/categories.ts +0 -106
- package/extensions/agent-browser/lib/results/confirmation.ts +0 -76
- package/extensions/agent-browser/lib/results/contracts.ts +0 -241
- package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +0 -72
- package/extensions/agent-browser/lib/results/envelope.ts +0 -195
- package/extensions/agent-browser/lib/results/network-routes.ts +0 -83
- package/extensions/agent-browser/lib/results/network.ts +0 -78
- package/extensions/agent-browser/lib/results/next-actions.ts +0 -117
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +0 -588
- package/extensions/agent-browser/lib/results/presentation/batch.ts +0 -450
- package/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.ts +0 -67
- package/extensions/agent-browser/lib/results/presentation/common.ts +0 -53
- package/extensions/agent-browser/lib/results/presentation/content.ts +0 -36
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +0 -923
- package/extensions/agent-browser/lib/results/presentation/errors.ts +0 -227
- package/extensions/agent-browser/lib/results/presentation/large-output.ts +0 -182
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +0 -184
- package/extensions/agent-browser/lib/results/presentation/registry.ts +0 -242
- package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +0 -131
- package/extensions/agent-browser/lib/results/presentation/skills.ts +0 -143
- package/extensions/agent-browser/lib/results/presentation.ts +0 -257
- package/extensions/agent-browser/lib/results/recovery-actions.ts +0 -139
- package/extensions/agent-browser/lib/results/recovery-next-actions.ts +0 -71
- package/extensions/agent-browser/lib/results/selector-recovery.ts +0 -320
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +0 -273
- package/extensions/agent-browser/lib/results/snapshot-refs.ts +0 -100
- package/extensions/agent-browser/lib/results/snapshot-segments.ts +0 -366
- package/extensions/agent-browser/lib/results/snapshot-spill.ts +0 -63
- package/extensions/agent-browser/lib/results/snapshot.ts +0 -329
- package/extensions/agent-browser/lib/results/text.ts +0 -40
- package/extensions/agent-browser/lib/runtime.ts +0 -988
- package/extensions/agent-browser/lib/session-page-state.ts +0 -512
- package/extensions/agent-browser/lib/string-enum-schema.ts +0 -20
- package/extensions/agent-browser/lib/temp.ts +0 -577
- package/extensions/agent-browser/lib/web-search.ts +0 -728
- /package/{extensions/agent-browser/lib/orchestration/browser-run.ts → dist/extensions/agent-browser/lib/orchestration/browser-run.js} +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Centralize recovery-oriented nextAction ids and action construction.
|
|
3
|
+
* Responsibilities: Build tab/about:blank/no-active-page/connected-session follow-ups and rich-input recovery ids.
|
|
4
|
+
* Scope: Recovery action contracts only; result category classification and artifact follow-ups live elsewhere.
|
|
5
|
+
* Usage: Imported by shared result action builders and the extension entrypoint.
|
|
6
|
+
* Invariants/Assumptions: Ids are public machine-readable contracts mirrored by docs and tests.
|
|
7
|
+
*/
|
|
8
|
+
import { buildNextToolAction, withOptionalSessionArgs } from "./next-actions.js";
|
|
9
|
+
export const AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS = {
|
|
10
|
+
aboutBlankListTabs: "list-tabs-for-about-blank-recovery",
|
|
11
|
+
connectedSessionListTabs: "list-connected-session-tabs",
|
|
12
|
+
genericTabDriftListTabs: "list-tabs-for-recovery",
|
|
13
|
+
noActivePageListTabs: "list-tabs-after-no-active-page",
|
|
14
|
+
selectIntendedTabAfterDrift: "select-intended-tab-after-drift",
|
|
15
|
+
snapshotAfterTabRecovery: "snapshot-after-tab-recovery",
|
|
16
|
+
tabDriftListTabs: "list-tabs-for-tab-drift-recovery",
|
|
17
|
+
};
|
|
18
|
+
export const AGENT_BROWSER_RICH_INPUT_RECOVERY_NEXT_ACTION_IDS = {
|
|
19
|
+
click: "click-current-editable-ref",
|
|
20
|
+
focus: "focus-current-editable-ref",
|
|
21
|
+
};
|
|
22
|
+
function getNumberedAgentBrowserNextActionId(baseId, index, total) {
|
|
23
|
+
return total > 1 ? `${baseId}-${index + 1}` : baseId;
|
|
24
|
+
}
|
|
25
|
+
export function getAgentBrowserRichInputRecoveryNextActionId(kind, index, candidateCount) {
|
|
26
|
+
return getNumberedAgentBrowserNextActionId(AGENT_BROWSER_RICH_INPUT_RECOVERY_NEXT_ACTION_IDS[kind], index, candidateCount);
|
|
27
|
+
}
|
|
28
|
+
export function getAgentBrowserRichInputRecoveryNextActionIds(candidateCount) {
|
|
29
|
+
const ids = [];
|
|
30
|
+
for (let index = 0; index < candidateCount; index += 1) {
|
|
31
|
+
ids.push(getAgentBrowserRichInputRecoveryNextActionId("focus", index, candidateCount), getAgentBrowserRichInputRecoveryNextActionId("click", index, candidateCount));
|
|
32
|
+
}
|
|
33
|
+
return ids;
|
|
34
|
+
}
|
|
35
|
+
function getRecoveryTargetDescription(recovery) {
|
|
36
|
+
const target = [recovery.targetTitle, recovery.targetUrl].filter((item) => item !== undefined && item.length > 0).join(" at ");
|
|
37
|
+
return target.length > 0 ? target : "the intended tab";
|
|
38
|
+
}
|
|
39
|
+
function isStableTabId(tab) {
|
|
40
|
+
return /^t\d+$/.test(tab ?? "");
|
|
41
|
+
}
|
|
42
|
+
function buildTabSnapshotRecoveryAction(options) {
|
|
43
|
+
if (options.recovery.recoveryApplied === true) {
|
|
44
|
+
return buildNextToolAction({
|
|
45
|
+
args: options.sessionArgs(["snapshot", "-i"]),
|
|
46
|
+
id: options.id,
|
|
47
|
+
reason: options.reason,
|
|
48
|
+
safety: options.safety,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return buildNextToolAction({
|
|
52
|
+
args: options.sessionArgs(["batch"]),
|
|
53
|
+
id: options.id,
|
|
54
|
+
reason: `${options.reason} The batch selects the stable tab before snapshotting.`,
|
|
55
|
+
safety: `${options.safety} The snapshot retry is atomic with tab selection, so it does not assume the intended tab is already active.`,
|
|
56
|
+
stdin: JSON.stringify([["tab", options.tabId], ["snapshot", "-i"]]),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function buildRecoveryNextActions(recovery) {
|
|
60
|
+
const sessionArgs = (args) => withOptionalSessionArgs(recovery.sessionName, args);
|
|
61
|
+
if (recovery.kind === "connected-session") {
|
|
62
|
+
return [
|
|
63
|
+
buildNextToolAction({
|
|
64
|
+
args: sessionArgs(["tab", "list"]),
|
|
65
|
+
id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.connectedSessionListTabs,
|
|
66
|
+
reason: "Inspect tabs exposed by the connected CDP endpoint before assuming the app surface is active.",
|
|
67
|
+
safety: "Read-only. Raw connect can succeed before the desktop app has an active rendered page.",
|
|
68
|
+
}),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
if (recovery.kind === "no-active-page") {
|
|
72
|
+
return [
|
|
73
|
+
buildNextToolAction({
|
|
74
|
+
args: sessionArgs(["tab", "list"]),
|
|
75
|
+
id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.noActivePageListTabs,
|
|
76
|
+
reason: "The snapshot found no active page; inspect the session tabs before retrying refs.",
|
|
77
|
+
safety: "Read-only tab listing for the same connected session.",
|
|
78
|
+
}),
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
const targetDescription = getRecoveryTargetDescription(recovery);
|
|
82
|
+
const listAction = buildNextToolAction({
|
|
83
|
+
args: sessionArgs(["tab", "list"]),
|
|
84
|
+
id: recovery.kind === "about-blank" ? AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.aboutBlankListTabs : AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.tabDriftListTabs,
|
|
85
|
+
reason: `Inspect tabs for ${targetDescription} before continuing after tab drift.`,
|
|
86
|
+
safety: "Read-only tab listing; prefer stable tN tab ids over positional tab guesses.",
|
|
87
|
+
});
|
|
88
|
+
if (!isStableTabId(recovery.selectedTab))
|
|
89
|
+
return [listAction];
|
|
90
|
+
return [
|
|
91
|
+
listAction,
|
|
92
|
+
buildNextToolAction({
|
|
93
|
+
args: sessionArgs(["tab", recovery.selectedTab]),
|
|
94
|
+
id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.selectIntendedTabAfterDrift,
|
|
95
|
+
reason: `Re-select ${targetDescription} with the stable tab id already observed by the wrapper.`,
|
|
96
|
+
safety: "Switches only the active tab in this browser session; it does not mutate page content.",
|
|
97
|
+
}),
|
|
98
|
+
buildTabSnapshotRecoveryAction({
|
|
99
|
+
id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.snapshotAfterTabRecovery,
|
|
100
|
+
reason: "Refresh interactive refs on the recovered tab before using @e refs again.",
|
|
101
|
+
recovery,
|
|
102
|
+
safety: "Read-only snapshot. Treat previous refs as stale until this succeeds.",
|
|
103
|
+
sessionArgs,
|
|
104
|
+
tabId: recovery.selectedTab,
|
|
105
|
+
}),
|
|
106
|
+
];
|
|
107
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Build session-aware recovery nextActions that combine result category policy with known session/tab context.
|
|
3
|
+
* Responsibilities: Prefix recovery argv with the active session and adapt tab/about:blank/no-active/stale-ref contexts into stable nextAction lists.
|
|
4
|
+
* Scope: Recovery nextAction assembly only; diagnostic detection and action-list merge ordering stay in the extension entrypoint.
|
|
5
|
+
* Usage: Imported by the extension entrypoint when adding recovery nextActions to tool details.
|
|
6
|
+
* Invariants/Assumptions: Action ids and argv ordering are public contracts; session prefixing must not double-prefix explicit --session args.
|
|
7
|
+
*/
|
|
8
|
+
import { buildAgentBrowserNextActions } from "./action-recommendations.js";
|
|
9
|
+
import { withOptionalSessionArgs } from "./next-actions.js";
|
|
10
|
+
export function buildConnectedSessionNextActions(sessionName) {
|
|
11
|
+
if (!sessionName)
|
|
12
|
+
return [];
|
|
13
|
+
return buildAgentBrowserNextActions({
|
|
14
|
+
recovery: { kind: "connected-session", sessionName },
|
|
15
|
+
resultCategory: "success",
|
|
16
|
+
successCategory: "completed",
|
|
17
|
+
}) ?? [];
|
|
18
|
+
}
|
|
19
|
+
export function buildNoActivePageNextActions(sessionName) {
|
|
20
|
+
if (!sessionName)
|
|
21
|
+
return [];
|
|
22
|
+
return buildAgentBrowserNextActions({
|
|
23
|
+
recovery: { kind: "no-active-page", sessionName },
|
|
24
|
+
resultCategory: "failure",
|
|
25
|
+
}) ?? [];
|
|
26
|
+
}
|
|
27
|
+
export function buildSessionTabRecoveryNextActions(options) {
|
|
28
|
+
const resultCategory = options.resultCategory ?? "success";
|
|
29
|
+
return buildAgentBrowserNextActions({
|
|
30
|
+
recovery: {
|
|
31
|
+
kind: options.kind,
|
|
32
|
+
recoveryApplied: options.recoveryApplied,
|
|
33
|
+
selectedTab: options.tabCorrection?.selectedTab,
|
|
34
|
+
sessionName: options.sessionName,
|
|
35
|
+
targetTitle: options.tabCorrection?.targetTitle ?? options.target?.title,
|
|
36
|
+
targetUrl: options.tabCorrection?.targetUrl ?? options.target?.url,
|
|
37
|
+
},
|
|
38
|
+
resultCategory,
|
|
39
|
+
successCategory: resultCategory === "success" ? "completed" : undefined,
|
|
40
|
+
}) ?? [];
|
|
41
|
+
}
|
|
42
|
+
export function buildSessionAwareStaleRefNextActions(sessionName) {
|
|
43
|
+
return (buildAgentBrowserNextActions({ failureCategory: "stale-ref", resultCategory: "failure" }) ?? []).map((action) => {
|
|
44
|
+
const actionArgs = action.params?.args;
|
|
45
|
+
return {
|
|
46
|
+
...action,
|
|
47
|
+
params: action.params && actionArgs ? { ...action.params, args: withOptionalSessionArgs(sessionName, actionArgs) } : action.params,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Own pure selector-miss recovery diagnostics for visible refs and rich editable inputs.
|
|
3
|
+
* Responsibilities: Parse find/semantic action targets, match current snapshot refs, build public diagnostics, text, and safe nextActions.
|
|
4
|
+
* Scope: Selector recovery policy only; subprocess snapshot probing and result orchestration stay in the extension entrypoint.
|
|
5
|
+
* Usage: The extension entrypoint supplies command tokens plus snapshot data after a selector-not-found failure.
|
|
6
|
+
* Invariants/Assumptions: Public fill recovery must never echo or auto-submit the user-provided fill text; guarded semanticAction fill pre-resolution may execute only one exact current editable ref.
|
|
7
|
+
*/
|
|
8
|
+
import { isRecord } from "../parsing.js";
|
|
9
|
+
import { extractRefSnapshotFromData } from "../session-page-state.js";
|
|
10
|
+
import { getEditableRefEvidence } from "./editable-ref-evidence.js";
|
|
11
|
+
import { withOptionalSessionArgs } from "./next-actions.js";
|
|
12
|
+
import { getAgentBrowserRichInputRecoveryNextActionId, getAgentBrowserRichInputRecoveryNextActionIds, } from "./recovery-actions.js";
|
|
13
|
+
import { getSnapshotLineTextByRef, getSnapshotRefRecord, getSnapshotRefRole, } from "./snapshot-refs.js";
|
|
14
|
+
import { compareRefIds } from "./text.js";
|
|
15
|
+
const SELECTOR_RECOVERY_ACTION_NAMES = new Set(["check", "click", "fill", "select", "uncheck"]);
|
|
16
|
+
const VISIBLE_REF_FALLBACK_CANDIDATE_LIMIT = 3;
|
|
17
|
+
const EDITABLE_CONTROL_ROLES = new Set(["combobox", "searchbox", "textbox"]);
|
|
18
|
+
const RICH_INPUT_RECOVERY_EDITABLE_ROLES = new Set(["searchbox", "textbox"]);
|
|
19
|
+
const RICH_INPUT_RECOVERY_HINT = "After the editable ref is focused, use keyboard inserttext or keyboard type with the intended text in a separate call, and do not press Enter or otherwise submit unless the user flow explicitly calls for it.";
|
|
20
|
+
function isSelectorRecoveryActionName(action) {
|
|
21
|
+
return SELECTOR_RECOVERY_ACTION_NAMES.has(action);
|
|
22
|
+
}
|
|
23
|
+
function getFindNameFlagValue(args, startIndex) {
|
|
24
|
+
const nameFlagIndex = args.indexOf("--name", startIndex);
|
|
25
|
+
const name = nameFlagIndex >= 0 ? args[nameFlagIndex + 1] : undefined;
|
|
26
|
+
return name && !name.startsWith("-") ? name : undefined;
|
|
27
|
+
}
|
|
28
|
+
function getFindVisibleRefFallbackTarget(args, options = {}) {
|
|
29
|
+
const findIndex = args[0] === "--session" ? 2 : 0;
|
|
30
|
+
if (args[findIndex] !== "find")
|
|
31
|
+
return undefined;
|
|
32
|
+
const locator = args[findIndex + 1];
|
|
33
|
+
const value = args[findIndex + 2];
|
|
34
|
+
const action = args[findIndex + 3];
|
|
35
|
+
if (!locator || !value || !isSelectorRecoveryActionName(action) || action === "select")
|
|
36
|
+
return undefined;
|
|
37
|
+
const text = action === "fill" ? args[findIndex + 4] : undefined;
|
|
38
|
+
if (action === "fill" && (!text || (!options.allowLeadingDashFillText && text.startsWith("-"))))
|
|
39
|
+
return undefined;
|
|
40
|
+
if (locator === "role") {
|
|
41
|
+
const targetName = getFindNameFlagValue(args, findIndex + 4);
|
|
42
|
+
return targetName ? { action, roles: [value], targetName, text } : undefined;
|
|
43
|
+
}
|
|
44
|
+
if (locator === "text" && action === "click") {
|
|
45
|
+
return { action, roles: ["button", "link"], targetName: value };
|
|
46
|
+
}
|
|
47
|
+
if (locator === "text" && action === "fill") {
|
|
48
|
+
return { action, roles: ["searchbox", "textbox"], targetName: value, text };
|
|
49
|
+
}
|
|
50
|
+
if (locator === "label" && action === "fill") {
|
|
51
|
+
return { action, roles: ["textbox"], targetName: value, text };
|
|
52
|
+
}
|
|
53
|
+
if (locator === "placeholder" && action === "fill") {
|
|
54
|
+
return { action, roles: ["searchbox", "textbox"], targetName: value, text };
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
export function getVisibleRefFallbackTarget(options) {
|
|
59
|
+
return getFindVisibleRefFallbackTarget(options.commandTokens, { allowLeadingDashFillText: true }) ?? (options.compiledSemanticAction ? getFindVisibleRefFallbackTarget(options.compiledSemanticAction.args, { allowLeadingDashFillText: true }) : undefined);
|
|
60
|
+
}
|
|
61
|
+
function getVisibleRefFallbackCandidates(target, snapshotData) {
|
|
62
|
+
const refs = getSnapshotRefRecord(snapshotData);
|
|
63
|
+
if (!refs)
|
|
64
|
+
return [];
|
|
65
|
+
const snapshotLineByRef = getSnapshotLineTextByRef(snapshotData);
|
|
66
|
+
const roleOrder = target.roles.map((role) => role.toLowerCase());
|
|
67
|
+
const targetName = normalizeSemanticActionAccessibleName(target.targetName);
|
|
68
|
+
const candidates = Object.entries(refs).flatMap(([ref, entry]) => {
|
|
69
|
+
if (!/^e\d+$/.test(ref) || !isRecord(entry))
|
|
70
|
+
return [];
|
|
71
|
+
const snapshotLine = snapshotLineByRef.get(ref);
|
|
72
|
+
const editableEvidence = getEditableRefEvidence({ ref: entry, text: snapshotLine });
|
|
73
|
+
const role = getSnapshotRefRole(entry, editableEvidence);
|
|
74
|
+
const name = typeof entry.name === "string" ? entry.name : undefined;
|
|
75
|
+
if (!role || !name || !roleOrder.includes(role.toLowerCase()) || normalizeSemanticActionAccessibleName(name) !== targetName)
|
|
76
|
+
return [];
|
|
77
|
+
if (target.action === "fill" && editableEvidence === false && EDITABLE_CONTROL_ROLES.has(role.toLowerCase()))
|
|
78
|
+
return [];
|
|
79
|
+
const directRefArgs = target.action === "fill" ? undefined : [target.action, `@${ref}`];
|
|
80
|
+
return [{
|
|
81
|
+
action: target.action,
|
|
82
|
+
...(directRefArgs ? { args: directRefArgs } : {}),
|
|
83
|
+
name,
|
|
84
|
+
reason: `Current snapshot shows ${role} ${JSON.stringify(name)} at @${ref}, matching the failed ${target.action} locator exactly.`,
|
|
85
|
+
ref: `@${ref}`,
|
|
86
|
+
role,
|
|
87
|
+
...(editableEvidence !== undefined ? { editableEvidence } : {}),
|
|
88
|
+
}];
|
|
89
|
+
});
|
|
90
|
+
candidates.sort((left, right) => roleOrder.indexOf(left.role.toLowerCase()) - roleOrder.indexOf(right.role.toLowerCase()) || compareRefIds(left.ref.slice(1), right.ref.slice(1)));
|
|
91
|
+
return candidates.slice(0, VISIBLE_REF_FALLBACK_CANDIDATE_LIMIT);
|
|
92
|
+
}
|
|
93
|
+
export function buildVisibleRefFallbackDiagnosticFromSnapshot(options) {
|
|
94
|
+
const snapshot = extractRefSnapshotFromData(options.snapshotData);
|
|
95
|
+
if (!snapshot)
|
|
96
|
+
return undefined;
|
|
97
|
+
const candidates = getVisibleRefFallbackCandidates(options.target, options.snapshotData);
|
|
98
|
+
if (candidates.length === 0)
|
|
99
|
+
return undefined;
|
|
100
|
+
return {
|
|
101
|
+
candidates,
|
|
102
|
+
snapshot,
|
|
103
|
+
summary: candidates.length === 1
|
|
104
|
+
? `Current snapshot has one exact visible ref match for ${options.target.action} ${JSON.stringify(options.target.targetName)}.`
|
|
105
|
+
: `Current snapshot has ${candidates.length} exact visible ref matches for ${options.target.action} ${JSON.stringify(options.target.targetName)}; choose only if the intended control is unambiguous.`,
|
|
106
|
+
target: { action: options.target.action, roles: options.target.roles, targetName: options.target.targetName },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function resolveVisibleRefActionFromSnapshot(options) {
|
|
110
|
+
const target = getFindVisibleRefFallbackTarget(options.compiledAction.args, { allowLeadingDashFillText: true });
|
|
111
|
+
if (!target || target.action === "select")
|
|
112
|
+
return undefined;
|
|
113
|
+
const snapshot = extractRefSnapshotFromData(options.snapshotData);
|
|
114
|
+
if (!snapshot)
|
|
115
|
+
return undefined;
|
|
116
|
+
const candidates = getVisibleRefFallbackCandidates(target, options.snapshotData);
|
|
117
|
+
if (target.action === "fill") {
|
|
118
|
+
if (!options.allowFill || candidates.length !== 1 || target.text === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
const [candidate] = candidates;
|
|
121
|
+
if (!candidate || candidate.editableEvidence === false || !EDITABLE_CONTROL_ROLES.has(candidate.role.toLowerCase()))
|
|
122
|
+
return undefined;
|
|
123
|
+
return { args: ["fill", candidate.ref, target.text], snapshot };
|
|
124
|
+
}
|
|
125
|
+
const candidate = candidates.find((item) => item.args !== undefined);
|
|
126
|
+
if (!candidate?.args)
|
|
127
|
+
return undefined;
|
|
128
|
+
return { args: candidate.args, snapshot };
|
|
129
|
+
}
|
|
130
|
+
export function buildVisibleRefFallbackNextActions(options) {
|
|
131
|
+
const ambiguous = options.diagnostic.candidates.length > 1;
|
|
132
|
+
return options.diagnostic.candidates.flatMap((candidate, index) => candidate.args ? [{
|
|
133
|
+
id: ambiguous ? `try-current-visible-ref-${index + 1}` : "try-current-visible-ref",
|
|
134
|
+
params: { args: withOptionalSessionArgs(options.sessionName, candidate.args) },
|
|
135
|
+
reason: candidate.reason,
|
|
136
|
+
safety: ambiguous
|
|
137
|
+
? "Several current refs share the same exact role/name. Inspect the snapshot and use only the ref that clearly matches the intended target."
|
|
138
|
+
: "Use only while this current snapshot still represents the page; refresh refs first if the page changed.",
|
|
139
|
+
tool: "agent_browser",
|
|
140
|
+
}] : []);
|
|
141
|
+
}
|
|
142
|
+
export function formatVisibleRefFallbackText(diagnostic) {
|
|
143
|
+
if (!diagnostic)
|
|
144
|
+
return undefined;
|
|
145
|
+
return [
|
|
146
|
+
"Current snapshot ref fallback:",
|
|
147
|
+
...diagnostic.candidates.map((candidate) => `- ${candidate.ref}${candidate.role ? ` ${candidate.role}` : ""} ${JSON.stringify(candidate.name)}: ${candidate.reason}`),
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
export function sanitizeVisibleRefFallbackDiagnostic(diagnostic) {
|
|
151
|
+
return {
|
|
152
|
+
candidates: diagnostic.candidates.map(({ editableEvidence: _editableEvidence, ...candidate }) => candidate),
|
|
153
|
+
snapshot: diagnostic.snapshot,
|
|
154
|
+
summary: diagnostic.summary,
|
|
155
|
+
target: diagnostic.target,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function isRichInputRecoveryCandidate(candidate) {
|
|
159
|
+
return candidate.action === "fill" && candidate.editableEvidence !== false && RICH_INPUT_RECOVERY_EDITABLE_ROLES.has(candidate.role.toLowerCase());
|
|
160
|
+
}
|
|
161
|
+
export function buildRichInputRecoveryDiagnostic(diagnostic) {
|
|
162
|
+
if (!diagnostic || diagnostic.target.action !== "fill")
|
|
163
|
+
return undefined;
|
|
164
|
+
const candidates = diagnostic.candidates.filter(isRichInputRecoveryCandidate).map((candidate) => ({
|
|
165
|
+
clickArgs: ["click", candidate.ref],
|
|
166
|
+
focusArgs: ["focus", candidate.ref],
|
|
167
|
+
name: candidate.name,
|
|
168
|
+
reason: `Current snapshot shows editable ${candidate.role} ${JSON.stringify(candidate.name)} at ${candidate.ref}; focus or click it before keyboard insertion instead of retrying fill with copied text.`,
|
|
169
|
+
ref: candidate.ref,
|
|
170
|
+
role: candidate.role,
|
|
171
|
+
}));
|
|
172
|
+
if (candidates.length === 0)
|
|
173
|
+
return undefined;
|
|
174
|
+
return {
|
|
175
|
+
candidates,
|
|
176
|
+
inputMethodHint: RICH_INPUT_RECOVERY_HINT,
|
|
177
|
+
nextActionIds: getAgentBrowserRichInputRecoveryNextActionIds(candidates.length),
|
|
178
|
+
summary: candidates.length === 1
|
|
179
|
+
? "Fill locator missed, but the current snapshot has one exact editable ref candidate for safe keyboard-based recovery."
|
|
180
|
+
: `Fill locator missed, but the current snapshot has ${candidates.length} exact editable ref candidates; choose only if the intended input is unambiguous.`,
|
|
181
|
+
target: { roles: diagnostic.target.roles, targetName: diagnostic.target.targetName },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export function buildRichInputRecoveryNextActions(options) {
|
|
185
|
+
const candidateCount = options.diagnostic.candidates.length;
|
|
186
|
+
const ambiguous = candidateCount > 1;
|
|
187
|
+
return options.diagnostic.candidates.flatMap((candidate, index) => {
|
|
188
|
+
const focusId = getAgentBrowserRichInputRecoveryNextActionId("focus", index, candidateCount);
|
|
189
|
+
const clickId = getAgentBrowserRichInputRecoveryNextActionId("click", index, candidateCount);
|
|
190
|
+
const safety = ambiguous
|
|
191
|
+
? `Several editable refs share the same exact name. Inspect the current snapshot and use only the ${candidate.ref} ${candidate.role} if it is clearly the intended input. No fill text or submit key is included.`
|
|
192
|
+
: "Does not include fill text or submit the form. After focus/click succeeds, use keyboard inserttext or keyboard type with the intended text only if this is the right input.";
|
|
193
|
+
return [
|
|
194
|
+
{
|
|
195
|
+
id: focusId,
|
|
196
|
+
params: { args: withOptionalSessionArgs(options.sessionName, candidate.focusArgs) },
|
|
197
|
+
reason: candidate.reason,
|
|
198
|
+
safety,
|
|
199
|
+
tool: "agent_browser",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: clickId,
|
|
203
|
+
params: { args: withOptionalSessionArgs(options.sessionName, candidate.clickArgs) },
|
|
204
|
+
reason: `Click ${candidate.ref} to focus the editable ${candidate.role} before keyboard insertion when focus alone is insufficient.`,
|
|
205
|
+
safety: `${safety} A click may run normal focus/click handlers, but this action does not press Enter or auto-submit.`,
|
|
206
|
+
tool: "agent_browser",
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
export function formatRichInputRecoveryText(diagnostic) {
|
|
212
|
+
if (!diagnostic)
|
|
213
|
+
return undefined;
|
|
214
|
+
return [
|
|
215
|
+
"Rich input recovery:",
|
|
216
|
+
...diagnostic.candidates.map((candidate, index) => {
|
|
217
|
+
const [focusId, clickId] = diagnostic.nextActionIds.slice(index * 2, index * 2 + 2);
|
|
218
|
+
return `- ${candidate.ref} ${candidate.role} ${JSON.stringify(candidate.name)}: use ${focusId} or ${clickId}; then use keyboard inserttext/type with the intended text.`;
|
|
219
|
+
}),
|
|
220
|
+
`- ${diagnostic.inputMethodHint}`,
|
|
221
|
+
].join("\n");
|
|
222
|
+
}
|
|
223
|
+
export function normalizeSemanticActionAccessibleName(name) {
|
|
224
|
+
return name.replace(/\s+/g, " ").trim().toLowerCase();
|
|
225
|
+
}
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* Usage: Existing internal imports may keep using this path during migration, while new code should prefer focused modules.
|
|
6
6
|
* Invariants/Assumptions: This file intentionally contains no business logic so `shared` cannot grow back into a catch-all module.
|
|
7
7
|
*/
|
|
8
|
-
|
|
9
8
|
export * from "./contracts.js";
|
|
10
9
|
export * from "./categories.js";
|
|
11
10
|
export * from "./action-recommendations.js";
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Select the omitted snapshot refs most likely to be actionable controls.
|
|
3
|
+
* Responsibilities: Classify editable, surface, primary-action, and role-based control refs with deterministic diversity and top-up rules.
|
|
4
|
+
* Scope: High-value control ranking only; snapshot parsing, noise filtering, and presentation text live in neighboring modules.
|
|
5
|
+
* Usage: Snapshot presentation passes already-visible, non-noise omitted refs and receives the bounded high-value subset to surface.
|
|
6
|
+
* Invariants/Assumptions: Ranking must preserve scarce control categories before filling dominant buckets so dense desktop hosts stay navigable.
|
|
7
|
+
*/
|
|
8
|
+
import { compareRefIds } from "./text.js";
|
|
9
|
+
const SNAPSHOT_HIGH_VALUE_EDITABLE_REF_FILL_TARGET_LINES = 4;
|
|
10
|
+
const SNAPSHOT_HIGH_VALUE_SURFACE_REF_FILL_TARGET_LINES = 3;
|
|
11
|
+
const SNAPSHOT_HIGH_VALUE_PRIMARY_ACTION_REF_FILL_TARGET_LINES = 3;
|
|
12
|
+
const SNAPSHOT_HIGH_VALUE_CONTROL_ROLES = new Set([
|
|
13
|
+
"button",
|
|
14
|
+
"checkbox",
|
|
15
|
+
"combobox",
|
|
16
|
+
"link",
|
|
17
|
+
"menuitem",
|
|
18
|
+
"option",
|
|
19
|
+
"radio",
|
|
20
|
+
"searchbox",
|
|
21
|
+
"tab",
|
|
22
|
+
"textbox",
|
|
23
|
+
]);
|
|
24
|
+
const SNAPSHOT_HIGH_VALUE_CONTROL_ROLE_PRIORITY = {
|
|
25
|
+
searchbox: 0,
|
|
26
|
+
textbox: 1,
|
|
27
|
+
combobox: 2,
|
|
28
|
+
button: 3,
|
|
29
|
+
link: 4,
|
|
30
|
+
tab: 5,
|
|
31
|
+
checkbox: 6,
|
|
32
|
+
radio: 7,
|
|
33
|
+
option: 8,
|
|
34
|
+
menuitem: 9,
|
|
35
|
+
};
|
|
36
|
+
const SNAPSHOT_SURFACE_CONTROL_NAME_PATTERNS = [
|
|
37
|
+
/\b(?:agents?|browser|canvas|chat|editor|panel|pane|preview|surface|tab|terminal|thread|view|window|workspace)\b/i,
|
|
38
|
+
];
|
|
39
|
+
const SNAPSHOT_PRIMARY_ACTION_BUTTON_NAME_PATTERNS = [
|
|
40
|
+
/^(?:add|apply|ask|attach|choose|confirm|connect|continue|create|deploy|done|download|go|insert|launch|log in|new|next|ok|open|publish|refresh|retry|run|save|search|select|send|sign in|sign up|start|submit|upload)$/i,
|
|
41
|
+
/^(?:add|apply|ask|confirm|connect|continue|create|launch|new|open|refresh|retry|run|save|search|send|start|submit)\b/i,
|
|
42
|
+
];
|
|
43
|
+
const SNAPSHOT_HIGH_VALUE_LINK_NAME_PATTERNS = [
|
|
44
|
+
/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i,
|
|
45
|
+
];
|
|
46
|
+
function getHighValueControlRole(entry) {
|
|
47
|
+
return entry.isEditable === true && (entry.role === "unknown" || entry.role === "generic") ? "textbox" : entry.role;
|
|
48
|
+
}
|
|
49
|
+
function isEditableControlRef(entry) {
|
|
50
|
+
if (entry.isEditable === false)
|
|
51
|
+
return false;
|
|
52
|
+
const role = getHighValueControlRole(entry);
|
|
53
|
+
return entry.isEditable === true || role === "searchbox" || role === "textbox" || role === "combobox";
|
|
54
|
+
}
|
|
55
|
+
function isNamedSurfaceControlRef(entry) {
|
|
56
|
+
if (entry.name.length === 0)
|
|
57
|
+
return false;
|
|
58
|
+
const role = getHighValueControlRole(entry);
|
|
59
|
+
if (role === "tab")
|
|
60
|
+
return true;
|
|
61
|
+
if (role !== "button" && role !== "menuitem" && role !== "option")
|
|
62
|
+
return false;
|
|
63
|
+
return SNAPSHOT_SURFACE_CONTROL_NAME_PATTERNS.some((pattern) => pattern.test(entry.name));
|
|
64
|
+
}
|
|
65
|
+
function isPrimaryActionButtonRef(entry) {
|
|
66
|
+
return (getHighValueControlRole(entry) === "button" &&
|
|
67
|
+
entry.name.length > 0 &&
|
|
68
|
+
SNAPSHOT_PRIMARY_ACTION_BUTTON_NAME_PATTERNS.some((pattern) => pattern.test(entry.name)));
|
|
69
|
+
}
|
|
70
|
+
const SNAPSHOT_HIGH_VALUE_CONTROL_CATEGORY_RULES = [
|
|
71
|
+
{
|
|
72
|
+
bucketKey: () => "editable",
|
|
73
|
+
fillTarget: SNAPSHOT_HIGH_VALUE_EDITABLE_REF_FILL_TARGET_LINES,
|
|
74
|
+
id: "editable",
|
|
75
|
+
matches: (entry) => isEditableControlRef(entry),
|
|
76
|
+
priority: 0,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
bucketKey: () => "named-surface",
|
|
80
|
+
fillTarget: SNAPSHOT_HIGH_VALUE_SURFACE_REF_FILL_TARGET_LINES,
|
|
81
|
+
id: "named-surface",
|
|
82
|
+
matches: (entry) => isNamedSurfaceControlRef(entry),
|
|
83
|
+
priority: 1,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
bucketKey: () => "primary-action",
|
|
87
|
+
fillTarget: SNAPSHOT_HIGH_VALUE_PRIMARY_ACTION_REF_FILL_TARGET_LINES,
|
|
88
|
+
id: "primary-action",
|
|
89
|
+
matches: (entry) => isPrimaryActionButtonRef(entry),
|
|
90
|
+
priority: 2,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
bucketKey: (_entry, role) => role,
|
|
94
|
+
id: "role",
|
|
95
|
+
matches: () => true,
|
|
96
|
+
priority: 3,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
function isHighValueLinkRef(entry) {
|
|
100
|
+
return entry.name.length > 0 && SNAPSHOT_HIGH_VALUE_LINK_NAME_PATTERNS.some((pattern) => pattern.test(entry.name));
|
|
101
|
+
}
|
|
102
|
+
export function isHighValueControlEntry(entry) {
|
|
103
|
+
const role = getHighValueControlRole(entry);
|
|
104
|
+
if (!SNAPSHOT_HIGH_VALUE_CONTROL_ROLES.has(role))
|
|
105
|
+
return false;
|
|
106
|
+
if (role === "link")
|
|
107
|
+
return isHighValueLinkRef(entry);
|
|
108
|
+
if (entry.isEditable === false && (role === "searchbox" || role === "textbox" || role === "combobox"))
|
|
109
|
+
return false;
|
|
110
|
+
return entry.name.length > 0 || isEditableControlRef(entry);
|
|
111
|
+
}
|
|
112
|
+
function getHighValueControlCategoryRule(entry, role) {
|
|
113
|
+
return SNAPSHOT_HIGH_VALUE_CONTROL_CATEGORY_RULES.find((rule) => rule.matches(entry, role));
|
|
114
|
+
}
|
|
115
|
+
function classifyHighValueControlRef(entry) {
|
|
116
|
+
if (!isHighValueControlEntry(entry))
|
|
117
|
+
return undefined;
|
|
118
|
+
const role = getHighValueControlRole(entry);
|
|
119
|
+
const rule = getHighValueControlCategoryRule(entry, role);
|
|
120
|
+
if (!rule)
|
|
121
|
+
return undefined;
|
|
122
|
+
return {
|
|
123
|
+
entry,
|
|
124
|
+
score: {
|
|
125
|
+
category: rule.id,
|
|
126
|
+
categoryPriority: rule.priority,
|
|
127
|
+
diversityBucketKey: `${rule.priority}:${rule.bucketKey(entry, role)}`,
|
|
128
|
+
lineIndex: entry.lineIndex ?? Number.MAX_SAFE_INTEGER,
|
|
129
|
+
namePriority: entry.name.length > 0 ? 0 : 1,
|
|
130
|
+
refId: entry.id,
|
|
131
|
+
role,
|
|
132
|
+
rolePriority: SNAPSHOT_HIGH_VALUE_CONTROL_ROLE_PRIORITY[role] ?? 50,
|
|
133
|
+
roundRobinBucketKey: `${rule.priority}:${role}`,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function compareHighValueControlCandidates(left, right) {
|
|
138
|
+
return (left.score.categoryPriority - right.score.categoryPriority ||
|
|
139
|
+
left.score.rolePriority - right.score.rolePriority ||
|
|
140
|
+
left.score.namePriority - right.score.namePriority ||
|
|
141
|
+
left.score.lineIndex - right.score.lineIndex ||
|
|
142
|
+
compareRefIds(left.score.refId, right.score.refId));
|
|
143
|
+
}
|
|
144
|
+
function takeHighValueCandidate(candidate, selected, selectedIds) {
|
|
145
|
+
selected.push(candidate);
|
|
146
|
+
selectedIds.add(candidate.entry.id);
|
|
147
|
+
}
|
|
148
|
+
function takeFirstPerDiversityBucket(candidates, selected, selectedIds, limit) {
|
|
149
|
+
const seenBuckets = new Set();
|
|
150
|
+
for (const candidate of candidates) {
|
|
151
|
+
if (selected.length >= limit)
|
|
152
|
+
break;
|
|
153
|
+
if (seenBuckets.has(candidate.score.diversityBucketKey))
|
|
154
|
+
continue;
|
|
155
|
+
seenBuckets.add(candidate.score.diversityBucketKey);
|
|
156
|
+
takeHighValueCandidate(candidate, selected, selectedIds);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function topUpHighValueCategory(candidates, selected, selectedIds, category, target, limit) {
|
|
160
|
+
let count = selected.filter((candidate) => candidate.score.category === category).length;
|
|
161
|
+
for (const candidate of candidates) {
|
|
162
|
+
if (selected.length >= limit || count >= target)
|
|
163
|
+
break;
|
|
164
|
+
if (selectedIds.has(candidate.entry.id) || candidate.score.category !== category)
|
|
165
|
+
continue;
|
|
166
|
+
takeHighValueCandidate(candidate, selected, selectedIds);
|
|
167
|
+
count += 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function buildRemainingHighValueBuckets(candidates, selectedIds) {
|
|
171
|
+
const buckets = new Map();
|
|
172
|
+
for (const candidate of candidates) {
|
|
173
|
+
if (selectedIds.has(candidate.entry.id))
|
|
174
|
+
continue;
|
|
175
|
+
const bucket = buckets.get(candidate.score.roundRobinBucketKey);
|
|
176
|
+
if (bucket)
|
|
177
|
+
bucket.push(candidate);
|
|
178
|
+
else
|
|
179
|
+
buckets.set(candidate.score.roundRobinBucketKey, [candidate]);
|
|
180
|
+
}
|
|
181
|
+
return [...buckets.values()].sort((left, right) => compareHighValueControlCandidates(left[0], right[0]));
|
|
182
|
+
}
|
|
183
|
+
function roundRobinHighValueBuckets(buckets, selected, selectedIds, limit) {
|
|
184
|
+
let bucketIndex = 0;
|
|
185
|
+
while (selected.length < limit && buckets.some((bucket) => bucket.length > 0)) {
|
|
186
|
+
const bucket = buckets[bucketIndex % buckets.length];
|
|
187
|
+
const candidate = bucket.shift();
|
|
188
|
+
if (candidate)
|
|
189
|
+
takeHighValueCandidate(candidate, selected, selectedIds);
|
|
190
|
+
bucketIndex += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export function selectHighValueControlEntries(entries, limit) {
|
|
194
|
+
const candidates = entries
|
|
195
|
+
.map(classifyHighValueControlRef)
|
|
196
|
+
.filter((candidate) => Boolean(candidate))
|
|
197
|
+
.sort(compareHighValueControlCandidates);
|
|
198
|
+
const selected = [];
|
|
199
|
+
const selectedIds = new Set();
|
|
200
|
+
takeFirstPerDiversityBucket(candidates, selected, selectedIds, limit);
|
|
201
|
+
for (const rule of SNAPSHOT_HIGH_VALUE_CONTROL_CATEGORY_RULES) {
|
|
202
|
+
if (rule.fillTarget === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
topUpHighValueCategory(candidates, selected, selectedIds, rule.id, rule.fillTarget, limit);
|
|
205
|
+
}
|
|
206
|
+
roundRobinHighValueBuckets(buildRemainingHighValueBuckets(candidates, selectedIds), selected, selectedIds, limit);
|
|
207
|
+
return selected.map((candidate) => candidate.entry);
|
|
208
|
+
}
|