pi-agent-browser-native 0.2.32 → 0.2.34
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 +36 -0
- package/README.md +61 -20
- package/docs/ARCHITECTURE.md +9 -2
- package/docs/COMMAND_REFERENCE.md +45 -14
- package/docs/ELECTRON.md +23 -4
- package/docs/RELEASE.md +15 -5
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +36 -22
- package/docs/TOOL_CONTRACT.md +90 -31
- package/extensions/agent-browser/index.ts +407 -4373
- package/extensions/agent-browser/lib/input-modes/electron.ts +170 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +265 -0
- package/extensions/agent-browser/lib/input-modes/lookups.ts +447 -0
- package/extensions/agent-browser/lib/input-modes/params.ts +188 -0
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +107 -0
- package/extensions/agent-browser/lib/input-modes/shared.ts +46 -0
- package/extensions/agent-browser/lib/input-modes/types.ts +221 -0
- package/extensions/agent-browser/lib/input-modes.ts +44 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +762 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +450 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +46 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +736 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +413 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +868 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +482 -0
- package/extensions/agent-browser/lib/orchestration/browser-run.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +338 -0
- package/extensions/agent-browser/lib/playbook.ts +22 -20
- package/extensions/agent-browser/lib/process.ts +106 -4
- package/extensions/agent-browser/lib/results/action-recommendations.ts +269 -0
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +114 -0
- package/extensions/agent-browser/lib/results/artifact-state.ts +13 -0
- package/extensions/agent-browser/lib/results/categories.ts +106 -0
- package/extensions/agent-browser/lib/results/contracts.ts +220 -0
- package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +72 -0
- package/extensions/agent-browser/lib/results/envelope.ts +2 -1
- package/extensions/agent-browser/lib/results/network.ts +64 -0
- package/extensions/agent-browser/lib/results/next-actions.ts +117 -0
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +506 -0
- package/extensions/agent-browser/lib/results/presentation/batch.ts +355 -0
- package/extensions/agent-browser/lib/results/presentation/common.ts +53 -0
- package/extensions/agent-browser/lib/results/presentation/content.ts +36 -0
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +730 -0
- package/extensions/agent-browser/lib/results/presentation/errors.ts +125 -0
- package/extensions/agent-browser/lib/results/presentation/large-output.ts +182 -0
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +216 -0
- package/extensions/agent-browser/lib/results/presentation/registry.ts +182 -0
- package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +133 -0
- package/extensions/agent-browser/lib/results/presentation/skills.ts +143 -0
- package/extensions/agent-browser/lib/results/presentation.ts +96 -2403
- package/extensions/agent-browser/lib/results/recovery-actions.ts +139 -0
- package/extensions/agent-browser/lib/results/recovery-next-actions.ts +71 -0
- package/extensions/agent-browser/lib/results/selector-recovery.ts +312 -0
- package/extensions/agent-browser/lib/results/shared.ts +17 -789
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +262 -0
- package/extensions/agent-browser/lib/results/snapshot-refs.ts +100 -0
- package/extensions/agent-browser/lib/results/snapshot-segments.ts +366 -0
- package/extensions/agent-browser/lib/results/snapshot-spill.ts +63 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +37 -489
- package/extensions/agent-browser/lib/results/text.ts +40 -0
- package/extensions/agent-browser/lib/results.ts +16 -5
- package/extensions/agent-browser/lib/session-page-state.ts +486 -0
- package/package.json +2 -1
|
@@ -0,0 +1,262 @@
|
|
|
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
|
+
|
|
9
|
+
import type { SnapshotRefEntry } from "./snapshot-refs.js";
|
|
10
|
+
import { compareRefIds } from "./text.js";
|
|
11
|
+
|
|
12
|
+
const SNAPSHOT_HIGH_VALUE_EDITABLE_REF_FILL_TARGET_LINES = 4;
|
|
13
|
+
const SNAPSHOT_HIGH_VALUE_SURFACE_REF_FILL_TARGET_LINES = 3;
|
|
14
|
+
const SNAPSHOT_HIGH_VALUE_PRIMARY_ACTION_REF_FILL_TARGET_LINES = 3;
|
|
15
|
+
|
|
16
|
+
const SNAPSHOT_HIGH_VALUE_CONTROL_ROLES = new Set([
|
|
17
|
+
"button",
|
|
18
|
+
"checkbox",
|
|
19
|
+
"combobox",
|
|
20
|
+
"menuitem",
|
|
21
|
+
"option",
|
|
22
|
+
"radio",
|
|
23
|
+
"searchbox",
|
|
24
|
+
"tab",
|
|
25
|
+
"textbox",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const SNAPSHOT_HIGH_VALUE_CONTROL_ROLE_PRIORITY: Record<string, number> = {
|
|
29
|
+
searchbox: 0,
|
|
30
|
+
textbox: 1,
|
|
31
|
+
combobox: 2,
|
|
32
|
+
button: 3,
|
|
33
|
+
tab: 4,
|
|
34
|
+
checkbox: 5,
|
|
35
|
+
radio: 6,
|
|
36
|
+
option: 7,
|
|
37
|
+
menuitem: 8,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const SNAPSHOT_SURFACE_CONTROL_NAME_PATTERNS = [
|
|
41
|
+
/\b(?:agents?|browser|canvas|chat|editor|panel|pane|preview|surface|tab|terminal|thread|view|window|workspace)\b/i,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const SNAPSHOT_PRIMARY_ACTION_BUTTON_NAME_PATTERNS = [
|
|
45
|
+
/^(?: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,
|
|
46
|
+
/^(?:add|apply|ask|confirm|connect|continue|create|launch|new|open|refresh|retry|run|save|search|send|start|submit)\b/i,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function getHighValueControlRole(entry: SnapshotRefEntry): string {
|
|
50
|
+
return entry.isEditable === true && (entry.role === "unknown" || entry.role === "generic") ? "textbox" : entry.role;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isEditableControlRef(entry: SnapshotRefEntry): boolean {
|
|
54
|
+
if (entry.isEditable === false) return false;
|
|
55
|
+
const role = getHighValueControlRole(entry);
|
|
56
|
+
return entry.isEditable === true || role === "searchbox" || role === "textbox" || role === "combobox";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isNamedSurfaceControlRef(entry: SnapshotRefEntry): boolean {
|
|
60
|
+
if (entry.name.length === 0) return false;
|
|
61
|
+
const role = getHighValueControlRole(entry);
|
|
62
|
+
if (role === "tab") return true;
|
|
63
|
+
if (role !== "button" && role !== "menuitem" && role !== "option") return false;
|
|
64
|
+
return SNAPSHOT_SURFACE_CONTROL_NAME_PATTERNS.some((pattern) => pattern.test(entry.name));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isPrimaryActionButtonRef(entry: SnapshotRefEntry): boolean {
|
|
68
|
+
return (
|
|
69
|
+
getHighValueControlRole(entry) === "button" &&
|
|
70
|
+
entry.name.length > 0 &&
|
|
71
|
+
SNAPSHOT_PRIMARY_ACTION_BUTTON_NAME_PATTERNS.some((pattern) => pattern.test(entry.name))
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type HighValueControlCategory = "editable" | "named-surface" | "primary-action" | "role";
|
|
76
|
+
|
|
77
|
+
interface HighValueControlCategoryRule {
|
|
78
|
+
bucketKey(entry: SnapshotRefEntry, role: string): string;
|
|
79
|
+
fillTarget?: number;
|
|
80
|
+
id: HighValueControlCategory;
|
|
81
|
+
matches(entry: SnapshotRefEntry, role: string): boolean;
|
|
82
|
+
priority: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface HighValueControlScore {
|
|
86
|
+
category: HighValueControlCategory;
|
|
87
|
+
categoryPriority: number;
|
|
88
|
+
diversityBucketKey: string;
|
|
89
|
+
lineIndex: number;
|
|
90
|
+
namePriority: 0 | 1;
|
|
91
|
+
refId: string;
|
|
92
|
+
role: string;
|
|
93
|
+
rolePriority: number;
|
|
94
|
+
roundRobinBucketKey: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface HighValueControlCandidate {
|
|
98
|
+
entry: SnapshotRefEntry;
|
|
99
|
+
score: HighValueControlScore;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const SNAPSHOT_HIGH_VALUE_CONTROL_CATEGORY_RULES: readonly HighValueControlCategoryRule[] = [
|
|
103
|
+
{
|
|
104
|
+
bucketKey: () => "editable",
|
|
105
|
+
fillTarget: SNAPSHOT_HIGH_VALUE_EDITABLE_REF_FILL_TARGET_LINES,
|
|
106
|
+
id: "editable",
|
|
107
|
+
matches: (entry) => isEditableControlRef(entry),
|
|
108
|
+
priority: 0,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
bucketKey: () => "named-surface",
|
|
112
|
+
fillTarget: SNAPSHOT_HIGH_VALUE_SURFACE_REF_FILL_TARGET_LINES,
|
|
113
|
+
id: "named-surface",
|
|
114
|
+
matches: (entry) => isNamedSurfaceControlRef(entry),
|
|
115
|
+
priority: 1,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
bucketKey: () => "primary-action",
|
|
119
|
+
fillTarget: SNAPSHOT_HIGH_VALUE_PRIMARY_ACTION_REF_FILL_TARGET_LINES,
|
|
120
|
+
id: "primary-action",
|
|
121
|
+
matches: (entry) => isPrimaryActionButtonRef(entry),
|
|
122
|
+
priority: 2,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
bucketKey: (_entry, role) => role,
|
|
126
|
+
id: "role",
|
|
127
|
+
matches: () => true,
|
|
128
|
+
priority: 3,
|
|
129
|
+
},
|
|
130
|
+
] as const;
|
|
131
|
+
|
|
132
|
+
export function isHighValueControlEntry(entry: SnapshotRefEntry): boolean {
|
|
133
|
+
const role = getHighValueControlRole(entry);
|
|
134
|
+
if (!SNAPSHOT_HIGH_VALUE_CONTROL_ROLES.has(role)) return false;
|
|
135
|
+
if (entry.isEditable === false && (role === "searchbox" || role === "textbox" || role === "combobox")) return false;
|
|
136
|
+
return entry.name.length > 0 || isEditableControlRef(entry);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getHighValueControlCategoryRule(entry: SnapshotRefEntry, role: string): HighValueControlCategoryRule | undefined {
|
|
140
|
+
return SNAPSHOT_HIGH_VALUE_CONTROL_CATEGORY_RULES.find((rule) => rule.matches(entry, role));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function classifyHighValueControlRef(entry: SnapshotRefEntry): HighValueControlCandidate | undefined {
|
|
144
|
+
if (!isHighValueControlEntry(entry)) return undefined;
|
|
145
|
+
const role = getHighValueControlRole(entry);
|
|
146
|
+
const rule = getHighValueControlCategoryRule(entry, role);
|
|
147
|
+
if (!rule) return undefined;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
entry,
|
|
151
|
+
score: {
|
|
152
|
+
category: rule.id,
|
|
153
|
+
categoryPriority: rule.priority,
|
|
154
|
+
diversityBucketKey: `${rule.priority}:${rule.bucketKey(entry, role)}`,
|
|
155
|
+
lineIndex: entry.lineIndex ?? Number.MAX_SAFE_INTEGER,
|
|
156
|
+
namePriority: entry.name.length > 0 ? 0 : 1,
|
|
157
|
+
refId: entry.id,
|
|
158
|
+
role,
|
|
159
|
+
rolePriority: SNAPSHOT_HIGH_VALUE_CONTROL_ROLE_PRIORITY[role] ?? 50,
|
|
160
|
+
roundRobinBucketKey: `${rule.priority}:${role}`,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function compareHighValueControlCandidates(left: HighValueControlCandidate, right: HighValueControlCandidate): number {
|
|
166
|
+
return (
|
|
167
|
+
left.score.categoryPriority - right.score.categoryPriority ||
|
|
168
|
+
left.score.rolePriority - right.score.rolePriority ||
|
|
169
|
+
left.score.namePriority - right.score.namePriority ||
|
|
170
|
+
left.score.lineIndex - right.score.lineIndex ||
|
|
171
|
+
compareRefIds(left.score.refId, right.score.refId)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function takeHighValueCandidate(
|
|
176
|
+
candidate: HighValueControlCandidate,
|
|
177
|
+
selected: HighValueControlCandidate[],
|
|
178
|
+
selectedIds: Set<string>,
|
|
179
|
+
): void {
|
|
180
|
+
selected.push(candidate);
|
|
181
|
+
selectedIds.add(candidate.entry.id);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function takeFirstPerDiversityBucket(
|
|
185
|
+
candidates: HighValueControlCandidate[],
|
|
186
|
+
selected: HighValueControlCandidate[],
|
|
187
|
+
selectedIds: Set<string>,
|
|
188
|
+
limit: number,
|
|
189
|
+
): void {
|
|
190
|
+
const seenBuckets = new Set<string>();
|
|
191
|
+
for (const candidate of candidates) {
|
|
192
|
+
if (selected.length >= limit) break;
|
|
193
|
+
if (seenBuckets.has(candidate.score.diversityBucketKey)) continue;
|
|
194
|
+
seenBuckets.add(candidate.score.diversityBucketKey);
|
|
195
|
+
takeHighValueCandidate(candidate, selected, selectedIds);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function topUpHighValueCategory(
|
|
200
|
+
candidates: HighValueControlCandidate[],
|
|
201
|
+
selected: HighValueControlCandidate[],
|
|
202
|
+
selectedIds: Set<string>,
|
|
203
|
+
category: HighValueControlCategory,
|
|
204
|
+
target: number,
|
|
205
|
+
limit: number,
|
|
206
|
+
): void {
|
|
207
|
+
let count = selected.filter((candidate) => candidate.score.category === category).length;
|
|
208
|
+
for (const candidate of candidates) {
|
|
209
|
+
if (selected.length >= limit || count >= target) break;
|
|
210
|
+
if (selectedIds.has(candidate.entry.id) || candidate.score.category !== category) continue;
|
|
211
|
+
takeHighValueCandidate(candidate, selected, selectedIds);
|
|
212
|
+
count += 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildRemainingHighValueBuckets(
|
|
217
|
+
candidates: HighValueControlCandidate[],
|
|
218
|
+
selectedIds: Set<string>,
|
|
219
|
+
): HighValueControlCandidate[][] {
|
|
220
|
+
const buckets = new Map<string, HighValueControlCandidate[]>();
|
|
221
|
+
for (const candidate of candidates) {
|
|
222
|
+
if (selectedIds.has(candidate.entry.id)) continue;
|
|
223
|
+
const bucket = buckets.get(candidate.score.roundRobinBucketKey);
|
|
224
|
+
if (bucket) bucket.push(candidate);
|
|
225
|
+
else buckets.set(candidate.score.roundRobinBucketKey, [candidate]);
|
|
226
|
+
}
|
|
227
|
+
return [...buckets.values()].sort((left, right) => compareHighValueControlCandidates(left[0], right[0]));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function roundRobinHighValueBuckets(
|
|
231
|
+
buckets: HighValueControlCandidate[][],
|
|
232
|
+
selected: HighValueControlCandidate[],
|
|
233
|
+
selectedIds: Set<string>,
|
|
234
|
+
limit: number,
|
|
235
|
+
): void {
|
|
236
|
+
let bucketIndex = 0;
|
|
237
|
+
while (selected.length < limit && buckets.some((bucket) => bucket.length > 0)) {
|
|
238
|
+
const bucket = buckets[bucketIndex % buckets.length];
|
|
239
|
+
const candidate = bucket.shift();
|
|
240
|
+
if (candidate) takeHighValueCandidate(candidate, selected, selectedIds);
|
|
241
|
+
bucketIndex += 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function selectHighValueControlEntries(entries: SnapshotRefEntry[], limit: number): SnapshotRefEntry[] {
|
|
246
|
+
const candidates = entries
|
|
247
|
+
.map(classifyHighValueControlRef)
|
|
248
|
+
.filter((candidate): candidate is HighValueControlCandidate => Boolean(candidate))
|
|
249
|
+
.sort(compareHighValueControlCandidates);
|
|
250
|
+
const selected: HighValueControlCandidate[] = [];
|
|
251
|
+
const selectedIds = new Set<string>();
|
|
252
|
+
|
|
253
|
+
takeFirstPerDiversityBucket(candidates, selected, selectedIds, limit);
|
|
254
|
+
|
|
255
|
+
for (const rule of SNAPSHOT_HIGH_VALUE_CONTROL_CATEGORY_RULES) {
|
|
256
|
+
if (rule.fillTarget === undefined) continue;
|
|
257
|
+
topUpHighValueCategory(candidates, selected, selectedIds, rule.id, rule.fillTarget, limit);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
roundRobinHighValueBuckets(buildRemainingHighValueBuckets(candidates, selectedIds), selected, selectedIds, limit);
|
|
261
|
+
return selected.map((candidate) => candidate.entry);
|
|
262
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Own canonical parsing and enrichment of refs from agent-browser snapshot payloads.
|
|
3
|
+
* Responsibilities: Read structured refs, correlate them with raw snapshot lines, and infer editable/ref role evidence once for consumers.
|
|
4
|
+
* Scope: Snapshot ref metadata only; section preview, high-value ranking, and presentation assembly live in neighboring modules.
|
|
5
|
+
* Usage: Imported by snapshot presentation and recovery diagnostics that need consistent ref/name/role/editable evidence.
|
|
6
|
+
* Invariants/Assumptions: Snapshot text parsing is best-effort and must tolerate upstream formatting changes by preserving structured ref data when line parsing fails.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isRecord } from "../parsing.js";
|
|
10
|
+
import { getEditableRefEvidence } from "./editable-ref-evidence.js";
|
|
11
|
+
import { compareRefIds, normalizeWhitespace } from "./text.js";
|
|
12
|
+
|
|
13
|
+
export interface SnapshotRefEntry {
|
|
14
|
+
id: string;
|
|
15
|
+
isEditable?: boolean;
|
|
16
|
+
lineIndex?: number;
|
|
17
|
+
name: string;
|
|
18
|
+
refData?: Record<string, unknown>;
|
|
19
|
+
role: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SnapshotLineRefInfo {
|
|
23
|
+
index: number;
|
|
24
|
+
name: string;
|
|
25
|
+
raw: string;
|
|
26
|
+
ref?: string;
|
|
27
|
+
role: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getSnapshotRefRecord(data: unknown): Record<string, unknown> | undefined {
|
|
31
|
+
return isRecord(data) && isRecord(data.refs) ? data.refs : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getSnapshotLineTextByRef(data: unknown): Map<string, string> {
|
|
35
|
+
const snapshot = isRecord(data) && typeof data.snapshot === "string" ? data.snapshot : "";
|
|
36
|
+
const lineByRef = new Map<string, string>();
|
|
37
|
+
for (const line of snapshot.split("\n")) {
|
|
38
|
+
const ref = line.match(/\bref=([^,\]\s]+)/)?.[1];
|
|
39
|
+
if (!ref || lineByRef.has(ref)) continue;
|
|
40
|
+
lineByRef.set(ref, line);
|
|
41
|
+
}
|
|
42
|
+
return lineByRef;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getSnapshotRefEntries(data: Record<string, unknown>): SnapshotRefEntry[] {
|
|
46
|
+
const refs = getSnapshotRefRecord(data);
|
|
47
|
+
if (!refs) return [];
|
|
48
|
+
|
|
49
|
+
return Object.entries(refs)
|
|
50
|
+
.map(([id, value]) => {
|
|
51
|
+
if (!isRecord(value)) {
|
|
52
|
+
return { id, name: "", role: "unknown" } satisfies SnapshotRefEntry;
|
|
53
|
+
}
|
|
54
|
+
const name = typeof value.name === "string" ? normalizeWhitespace(value.name) : "";
|
|
55
|
+
const role = typeof value.role === "string" && value.role.length > 0 ? value.role : "unknown";
|
|
56
|
+
const isEditable = getEditableRefEvidence({ ref: value });
|
|
57
|
+
return { id, isEditable, name, refData: value, role } satisfies SnapshotRefEntry;
|
|
58
|
+
})
|
|
59
|
+
.sort((a, b) => compareRefIds(a.id, b.id));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isEditableSnapshotLine(line: SnapshotLineRefInfo): boolean | undefined {
|
|
63
|
+
const editableEvidence = getEditableRefEvidence({ text: line.raw });
|
|
64
|
+
if (editableEvidence !== undefined) return editableEvidence;
|
|
65
|
+
return line.role === "searchbox" || line.role === "textbox" || line.role === "combobox" ? true : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getSnapshotRefRole(entry: { role?: unknown }, editableEvidence: boolean | undefined): string {
|
|
69
|
+
const rawRole = typeof entry.role === "string" && entry.role.length > 0 ? entry.role : "unknown";
|
|
70
|
+
const normalizedRole = rawRole.toLowerCase();
|
|
71
|
+
if ((normalizedRole === "generic" || normalizedRole === "unknown") && editableEvidence === true) {
|
|
72
|
+
return "textbox";
|
|
73
|
+
}
|
|
74
|
+
return rawRole;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function enrichSnapshotRefEntries(refEntries: SnapshotRefEntry[], snapshotLines: SnapshotLineRefInfo[]): SnapshotRefEntry[] {
|
|
78
|
+
const lineByRef = new Map<string, SnapshotLineRefInfo>();
|
|
79
|
+
for (const line of snapshotLines) {
|
|
80
|
+
if (!line.ref || lineByRef.has(line.ref)) continue;
|
|
81
|
+
lineByRef.set(line.ref, line);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return refEntries.map((entry) => {
|
|
85
|
+
const line = lineByRef.get(entry.id);
|
|
86
|
+
const lineRole = line && line.role !== "unknown" ? line.role : undefined;
|
|
87
|
+
const editableEvidence = getEditableRefEvidence({ ref: entry.refData, text: line?.raw });
|
|
88
|
+
const hasEditableRole = line ? isEditableSnapshotLine(line) === true && !["unknown", "generic"].includes(line.role) : false;
|
|
89
|
+
const isEditable = editableEvidence === true || (editableEvidence !== false && hasEditableRole);
|
|
90
|
+
const roleFromRefOrLine = entry.role !== "unknown" && entry.role !== "generic" ? entry.role : lineRole ?? entry.role;
|
|
91
|
+
const role = getSnapshotRefRole({ role: roleFromRefOrLine }, isEditable);
|
|
92
|
+
return {
|
|
93
|
+
...entry,
|
|
94
|
+
isEditable,
|
|
95
|
+
lineIndex: line?.index,
|
|
96
|
+
name: entry.name.length > 0 ? entry.name : (line?.name ?? ""),
|
|
97
|
+
role,
|
|
98
|
+
} satisfies SnapshotRefEntry;
|
|
99
|
+
});
|
|
100
|
+
}
|