teamshare-bridge 0.26.13 → 0.26.15
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/cli/commands/research.js +10 -2
- package/dist/cli/commands/research.js.map +1 -1
- package/dist/cli/commands/run.js +0 -21
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/lib/browser/batch.d.ts +77 -1
- package/dist/lib/browser/batch.js +151 -3
- package/dist/lib/browser/batch.js.map +1 -1
- package/dist/lib/browser/cdp.d.ts +61 -2
- package/dist/lib/browser/cdp.js +235 -14
- package/dist/lib/browser/cdp.js.map +1 -1
- package/dist/lib/browser/index.d.ts +2 -2
- package/dist/lib/browser/index.js +4 -1
- package/dist/lib/browser/index.js.map +1 -1
- package/dist/lib/browser/login.d.ts +20 -2
- package/dist/lib/browser/login.js +125 -88
- package/dist/lib/browser/login.js.map +1 -1
- package/dist/lib/browser/network-log.d.ts +31 -0
- package/dist/lib/browser/network-log.js +94 -12
- package/dist/lib/browser/network-log.js.map +1 -1
- package/dist/lib/browser/page-context.d.ts +62 -1
- package/dist/lib/browser/page-context.js +284 -1
- package/dist/lib/browser/page-context.js.map +1 -1
- package/dist/lib/browser/refs.d.ts +104 -0
- package/dist/lib/browser/refs.js +430 -0
- package/dist/lib/browser/refs.js.map +1 -0
- package/dist/lib/browser/session.d.ts +188 -7
- package/dist/lib/browser/session.js +537 -8
- package/dist/lib/browser/session.js.map +1 -1
- package/dist/lib/git/git-finalize.js +17 -2
- package/dist/lib/git/git-finalize.js.map +1 -1
- package/dist/lib/harness/harness-adapter.d.ts +2 -5
- package/dist/lib/harness/harness-adapter.js.map +1 -1
- package/dist/lib/harness/opencode-control.d.ts +73 -0
- package/dist/lib/harness/opencode-control.js +318 -0
- package/dist/lib/harness/opencode-control.js.map +1 -1
- package/dist/lib/harness/opencode-outcome.d.ts +16 -2
- package/dist/lib/harness/opencode-outcome.js +36 -7
- package/dist/lib/harness/opencode-outcome.js.map +1 -1
- package/dist/lib/harness/spawn.js +112 -48
- package/dist/lib/harness/spawn.js.map +1 -1
- package/dist/lib/orchestrator/handlers/browser.js +472 -27
- package/dist/lib/orchestrator/handlers/browser.js.map +1 -1
- package/dist/lib/tools/browser-tool.js +24 -3
- package/dist/lib/tools/browser-tool.js.map +1 -1
- package/dist/lib/workspace/repo-lock.d.ts +5 -1
- package/dist/lib/workspace/repo-lock.js +5 -3
- package/dist/lib/workspace/repo-lock.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element references for the debug/MCP browser (2026-10).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS: the QA agent used to identify controls with raw CSS
|
|
5
|
+
* selectors, `getElements` indexes or x/y coordinates. On TeamShare's own
|
|
6
|
+
* shadcn/Radix UI that failed constantly: a dropdown, popover or kebab menu
|
|
7
|
+
* renders into a PORTAL at the end of `<body>`, the selector the model guessed
|
|
8
|
+
* matched nothing, and a coordinate read from a screenshot was device-scale.
|
|
9
|
+
* Most of a QA run was spent re-guessing selectors.
|
|
10
|
+
*
|
|
11
|
+
* A ref is a short opaque handle (`e1`, `e2`, ...) minted by ONE in-page scan:
|
|
12
|
+
*
|
|
13
|
+
* - the scanner walks every interactive element in the document, INCLUDING
|
|
14
|
+
* portalled menus/popovers/dialogs (they are still in the DOM, just not
|
|
15
|
+
* where the markup suggests), filters to what is actually visible, and
|
|
16
|
+
* records role/name/coords plus a direct reference to the live node in
|
|
17
|
+
* `window.__tsRefs.nodes`;
|
|
18
|
+
* - actions resolve a ref to the live node first and its centre point second,
|
|
19
|
+
* so an action is never sent to a coordinate that a re-render invalidated;
|
|
20
|
+
* - the live node is checked with `isConnected` before every use. A dead ref
|
|
21
|
+
* is refused with `REF_STALE` AND, when a unique same-role/same-name match
|
|
22
|
+
* exists in the fresh scan, transparently re-pointed at it.
|
|
23
|
+
*
|
|
24
|
+
* A ref list is invalidated by navigating or by a re-render, exactly like a
|
|
25
|
+
* Playwright locator; the session keeps the last list so it can explain what
|
|
26
|
+
* happened instead of failing mute.
|
|
27
|
+
*
|
|
28
|
+
* The scan deliberately runs in the page (one `Runtime.evaluate`) instead of
|
|
29
|
+
* joining `Accessibility.getFullAXTree` with `DOMSnapshot.captureSnapshot`:
|
|
30
|
+
* the DOM scan gives portal content AND geometry in one round trip, and it is
|
|
31
|
+
* the same document the actions operate on. Roles/names are computed from the
|
|
32
|
+
* explicit ARIA attributes first, then the implicit HTML semantics.
|
|
33
|
+
*/
|
|
34
|
+
import { type CdpPage } from './cdp';
|
|
35
|
+
/** A JSON-safe description of one interactive element. */
|
|
36
|
+
export interface RefElement {
|
|
37
|
+
ref: string;
|
|
38
|
+
tag: string;
|
|
39
|
+
role: string;
|
|
40
|
+
name: string;
|
|
41
|
+
disabled: boolean;
|
|
42
|
+
inDialog: boolean;
|
|
43
|
+
inPortal: boolean;
|
|
44
|
+
/** Length only - a field VALUE must never travel through a tool result. */
|
|
45
|
+
valueLength?: number;
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
}
|
|
51
|
+
/** Hard cap on refs per scan: keeps the tool result readable and bounded. */
|
|
52
|
+
export declare const MAX_REFS = 300;
|
|
53
|
+
/**
|
|
54
|
+
* The one scan. Returns `{ at, list }` and stores the live nodes on
|
|
55
|
+
* `window.__tsRefs.nodes` for later `isConnected` checks / focus calls.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildRefScanScript(): string;
|
|
58
|
+
/**
|
|
59
|
+
* Human/LLM-readable ref lines. One line per element:
|
|
60
|
+
* `e7 button "Publish draft" (640,412) [in menu]`
|
|
61
|
+
*/
|
|
62
|
+
export declare function formatRefs(list: RefElement[], maxLines?: number): string[];
|
|
63
|
+
/** Run a fresh scan and return it (the caller keeps the list). */
|
|
64
|
+
export declare function scanRefs(page: CdpPage): Promise<{
|
|
65
|
+
at: number;
|
|
66
|
+
list: RefElement[];
|
|
67
|
+
}>;
|
|
68
|
+
/** Liveness + geometry of one ref, from the node the scan stored. */
|
|
69
|
+
export declare function buildRefInfoScript(ref: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Focus a ref'd element, scrolling it to the middle first. Returns whether the
|
|
72
|
+
* element actually took focus plus its tag/role, so `focus` is never a silent
|
|
73
|
+
* no-op: a div without `tabindex` cannot be focused and says so.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildRefFocusScript(ref: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Dispatch the FULL pointer sequence on a ref'd element, in-page.
|
|
78
|
+
*
|
|
79
|
+
* This is the fallback for controls that ignore a coordinate click: the
|
|
80
|
+
* frontend tours (`frontend/src/modules/tour/utils/acts.ts`) taught us that a
|
|
81
|
+
* bare `.click()` is a SILENT no-op on Radix tabs/menus, which activate on
|
|
82
|
+
* `pointerdown`/`mousedown`. Same recipe, one copy per side of the wire.
|
|
83
|
+
*/
|
|
84
|
+
export declare function buildRefPointerScript(ref: string): string;
|
|
85
|
+
/**
|
|
86
|
+
* Draw ref labels over the live page (2026-10).
|
|
87
|
+
*
|
|
88
|
+
* A plain screenshot shows WHAT is on screen; the model still has to guess
|
|
89
|
+
* which button is `e7`. The overlay draws a numbered box on every ref'd
|
|
90
|
+
* element so ONE image both shows the page and names the targets. Removed
|
|
91
|
+
* immediately after the capture - the page is never left modified.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildAnnotateScript(): string;
|
|
94
|
+
/** Remove the ref overlay (always called, even when the capture failed). */
|
|
95
|
+
export declare function buildClearAnnotationScript(): string;
|
|
96
|
+
/** How many popovers/menus are currently open (used by closePopup). */
|
|
97
|
+
export declare function buildPopoverCountScript(): string;
|
|
98
|
+
/**
|
|
99
|
+
* Click OUTSIDE every open layer (the Radix DismissableLayer listens for a
|
|
100
|
+
* document-level `pointerdown`). Coordinate clicks can land on something
|
|
101
|
+
* dangerous in a corner, so this dispatches the pointer sequence on `<body>`
|
|
102
|
+
* at the layer's own outside point instead.
|
|
103
|
+
*/
|
|
104
|
+
export declare function buildOutsidePointerScript(): string;
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_REFS = void 0;
|
|
4
|
+
exports.buildRefScanScript = buildRefScanScript;
|
|
5
|
+
exports.formatRefs = formatRefs;
|
|
6
|
+
exports.scanRefs = scanRefs;
|
|
7
|
+
exports.buildRefInfoScript = buildRefInfoScript;
|
|
8
|
+
exports.buildRefFocusScript = buildRefFocusScript;
|
|
9
|
+
exports.buildRefPointerScript = buildRefPointerScript;
|
|
10
|
+
exports.buildAnnotateScript = buildAnnotateScript;
|
|
11
|
+
exports.buildClearAnnotationScript = buildClearAnnotationScript;
|
|
12
|
+
exports.buildPopoverCountScript = buildPopoverCountScript;
|
|
13
|
+
exports.buildOutsidePointerScript = buildOutsidePointerScript;
|
|
14
|
+
/**
|
|
15
|
+
* Element references for the debug/MCP browser (2026-10).
|
|
16
|
+
*
|
|
17
|
+
* WHY THIS EXISTS: the QA agent used to identify controls with raw CSS
|
|
18
|
+
* selectors, `getElements` indexes or x/y coordinates. On TeamShare's own
|
|
19
|
+
* shadcn/Radix UI that failed constantly: a dropdown, popover or kebab menu
|
|
20
|
+
* renders into a PORTAL at the end of `<body>`, the selector the model guessed
|
|
21
|
+
* matched nothing, and a coordinate read from a screenshot was device-scale.
|
|
22
|
+
* Most of a QA run was spent re-guessing selectors.
|
|
23
|
+
*
|
|
24
|
+
* A ref is a short opaque handle (`e1`, `e2`, ...) minted by ONE in-page scan:
|
|
25
|
+
*
|
|
26
|
+
* - the scanner walks every interactive element in the document, INCLUDING
|
|
27
|
+
* portalled menus/popovers/dialogs (they are still in the DOM, just not
|
|
28
|
+
* where the markup suggests), filters to what is actually visible, and
|
|
29
|
+
* records role/name/coords plus a direct reference to the live node in
|
|
30
|
+
* `window.__tsRefs.nodes`;
|
|
31
|
+
* - actions resolve a ref to the live node first and its centre point second,
|
|
32
|
+
* so an action is never sent to a coordinate that a re-render invalidated;
|
|
33
|
+
* - the live node is checked with `isConnected` before every use. A dead ref
|
|
34
|
+
* is refused with `REF_STALE` AND, when a unique same-role/same-name match
|
|
35
|
+
* exists in the fresh scan, transparently re-pointed at it.
|
|
36
|
+
*
|
|
37
|
+
* A ref list is invalidated by navigating or by a re-render, exactly like a
|
|
38
|
+
* Playwright locator; the session keeps the last list so it can explain what
|
|
39
|
+
* happened instead of failing mute.
|
|
40
|
+
*
|
|
41
|
+
* The scan deliberately runs in the page (one `Runtime.evaluate`) instead of
|
|
42
|
+
* joining `Accessibility.getFullAXTree` with `DOMSnapshot.captureSnapshot`:
|
|
43
|
+
* the DOM scan gives portal content AND geometry in one round trip, and it is
|
|
44
|
+
* the same document the actions operate on. Roles/names are computed from the
|
|
45
|
+
* explicit ARIA attributes first, then the implicit HTML semantics.
|
|
46
|
+
*/
|
|
47
|
+
const cdp_1 = require("./cdp");
|
|
48
|
+
/** Hard cap on refs per scan: keeps the tool result readable and bounded. */
|
|
49
|
+
exports.MAX_REFS = 300;
|
|
50
|
+
/** Elements that can be interacted with (and are useful to name). */
|
|
51
|
+
const REF_SELECTOR = [
|
|
52
|
+
'a[href]',
|
|
53
|
+
'button',
|
|
54
|
+
'input:not([type="hidden"])',
|
|
55
|
+
'select',
|
|
56
|
+
'textarea',
|
|
57
|
+
'summary',
|
|
58
|
+
'[role="button"]',
|
|
59
|
+
'[role="link"]',
|
|
60
|
+
'[role="menuitem"]',
|
|
61
|
+
'[role="menuitemcheckbox"]',
|
|
62
|
+
'[role="menuitemradio"]',
|
|
63
|
+
'[role="option"]',
|
|
64
|
+
'[role="tab"]',
|
|
65
|
+
'[role="checkbox"]',
|
|
66
|
+
'[role="radio"]',
|
|
67
|
+
'[role="switch"]',
|
|
68
|
+
'[role="combobox"]',
|
|
69
|
+
'[role="textbox"]',
|
|
70
|
+
'[contenteditable="true"]',
|
|
71
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
72
|
+
].join(',');
|
|
73
|
+
/**
|
|
74
|
+
* The one scan. Returns `{ at, list }` and stores the live nodes on
|
|
75
|
+
* `window.__tsRefs.nodes` for later `isConnected` checks / focus calls.
|
|
76
|
+
*/
|
|
77
|
+
function buildRefScanScript() {
|
|
78
|
+
return `
|
|
79
|
+
(() => {
|
|
80
|
+
const MAX = ${exports.MAX_REFS};
|
|
81
|
+
const SEL = ${JSON.stringify(REF_SELECTOR)};
|
|
82
|
+
const PORTAL_SEL = '[data-radix-popper-content-wrapper],[data-radix-portal],[data-floating-ui-portal]';
|
|
83
|
+
const DIALOG_SEL = 'dialog[open],[role="dialog"],[aria-modal="true"]';
|
|
84
|
+
const vis = (el) => {
|
|
85
|
+
if (!el || el.nodeType !== 1) return false;
|
|
86
|
+
const r = el.getBoundingClientRect();
|
|
87
|
+
if (r.width === 0 || r.height === 0) return false;
|
|
88
|
+
const cs = getComputedStyle(el);
|
|
89
|
+
if (cs.visibility === 'hidden' || cs.display === 'none' || cs.opacity === '0') return false;
|
|
90
|
+
if (cs.pointerEvents === 'none') return false;
|
|
91
|
+
return true;
|
|
92
|
+
};
|
|
93
|
+
const clean = (v, cap) => String(v == null ? '' : v)
|
|
94
|
+
.replace(/\\s+/g, ' ')
|
|
95
|
+
.trim()
|
|
96
|
+
.slice(0, cap || 80);
|
|
97
|
+
const nameOf = (el) => {
|
|
98
|
+
const aria = el.getAttribute('aria-label');
|
|
99
|
+
if (aria) return clean(aria, 80);
|
|
100
|
+
const by = el.getAttribute('aria-labelledby');
|
|
101
|
+
if (by) {
|
|
102
|
+
const t = by.split(/\\s+/).map((id) => {
|
|
103
|
+
const n = document.getElementById(id);
|
|
104
|
+
return n ? (n.innerText || n.textContent || '') : '';
|
|
105
|
+
}).join(' ');
|
|
106
|
+
if (t.trim()) return clean(t, 80);
|
|
107
|
+
}
|
|
108
|
+
const tag = String(el.tagName || '').toLowerCase();
|
|
109
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
110
|
+
const id = el.id;
|
|
111
|
+
if (id) {
|
|
112
|
+
try {
|
|
113
|
+
const l = document.querySelector('label[for="' + CSS.escape(id) + '"]');
|
|
114
|
+
if (l) return clean(l.innerText || l.textContent, 80);
|
|
115
|
+
} catch (e) {}
|
|
116
|
+
}
|
|
117
|
+
const wrap = el.closest && el.closest('label');
|
|
118
|
+
if (wrap) return clean(wrap.innerText || wrap.textContent, 80);
|
|
119
|
+
const ph = el.getAttribute('placeholder');
|
|
120
|
+
if (ph) return clean(ph, 80);
|
|
121
|
+
const nm = el.getAttribute('name');
|
|
122
|
+
if (nm) return clean(nm, 80);
|
|
123
|
+
if (el.id) return clean(el.id, 80);
|
|
124
|
+
}
|
|
125
|
+
const txt = el.innerText || el.textContent || '';
|
|
126
|
+
if (txt.trim()) return clean(txt, 80);
|
|
127
|
+
return clean(
|
|
128
|
+
el.getAttribute('title') ||
|
|
129
|
+
el.getAttribute('alt') ||
|
|
130
|
+
el.getAttribute('value') ||
|
|
131
|
+
'',
|
|
132
|
+
80,
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
const roleOf = (el) => {
|
|
136
|
+
const explicit = el.getAttribute('role');
|
|
137
|
+
if (explicit) return clean(explicit, 24);
|
|
138
|
+
const tag = String(el.tagName || '').toLowerCase();
|
|
139
|
+
const type = String(el.type || '').toLowerCase();
|
|
140
|
+
if (tag === 'a') return 'link';
|
|
141
|
+
if (tag === 'button' || tag === 'summary') return 'button';
|
|
142
|
+
if (tag === 'select') return 'combobox';
|
|
143
|
+
if (tag === 'textarea') return 'textbox';
|
|
144
|
+
if (tag === 'input') {
|
|
145
|
+
if (type === 'submit' || type === 'button' || type === 'reset') return 'button';
|
|
146
|
+
if (type === 'checkbox') return 'checkbox';
|
|
147
|
+
if (type === 'radio') return 'radio';
|
|
148
|
+
if (type === 'search') return 'searchbox';
|
|
149
|
+
return 'textbox';
|
|
150
|
+
}
|
|
151
|
+
if (el.isContentEditable === true) return 'textbox';
|
|
152
|
+
return tag || 'element';
|
|
153
|
+
};
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
const nodes = {};
|
|
156
|
+
const list = [];
|
|
157
|
+
const all = [...document.querySelectorAll(SEL)];
|
|
158
|
+
for (const el of all) {
|
|
159
|
+
if (list.length >= MAX) break;
|
|
160
|
+
if (seen.has(el)) continue;
|
|
161
|
+
seen.add(el);
|
|
162
|
+
// Skip an element wholly covered by another candidate we already took
|
|
163
|
+
// only when it IS that element - nested buttons are real (icon spans
|
|
164
|
+
// are not in the selector, so this is only a dedupe guard).
|
|
165
|
+
if (!vis(el)) continue;
|
|
166
|
+
const r = el.getBoundingClientRect();
|
|
167
|
+
const ref = 'e' + (list.length + 1);
|
|
168
|
+
const row = {
|
|
169
|
+
ref,
|
|
170
|
+
tag: clean(String(el.tagName || '').toLowerCase(), 20) || 'element',
|
|
171
|
+
role: roleOf(el),
|
|
172
|
+
name: nameOf(el),
|
|
173
|
+
disabled:
|
|
174
|
+
el.disabled === true || el.getAttribute('aria-disabled') === 'true',
|
|
175
|
+
inDialog: !!(el.closest && el.closest(DIALOG_SEL)),
|
|
176
|
+
inPortal: !!(el.closest && el.closest(PORTAL_SEL)),
|
|
177
|
+
x: Math.round(r.x + r.width / 2),
|
|
178
|
+
y: Math.round(r.y + r.height / 2),
|
|
179
|
+
width: Math.round(r.width),
|
|
180
|
+
height: Math.round(r.height),
|
|
181
|
+
};
|
|
182
|
+
if (
|
|
183
|
+
(row.tag === 'input' || row.tag === 'textarea') &&
|
|
184
|
+
typeof el.value === 'string'
|
|
185
|
+
) {
|
|
186
|
+
row.valueLength = el.value.length;
|
|
187
|
+
}
|
|
188
|
+
nodes[ref] = el;
|
|
189
|
+
list.push(row);
|
|
190
|
+
}
|
|
191
|
+
window.__tsRefs = { at: Date.now(), list, nodes };
|
|
192
|
+
return { at: window.__tsRefs.at, list };
|
|
193
|
+
})()
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Human/LLM-readable ref lines. One line per element:
|
|
198
|
+
* `e7 button "Publish draft" (640,412) [in menu]`
|
|
199
|
+
*/
|
|
200
|
+
function formatRefs(list, maxLines = 300) {
|
|
201
|
+
return list.slice(0, maxLines).map((r) => {
|
|
202
|
+
const parts = [r.ref, r.role];
|
|
203
|
+
if (r.name)
|
|
204
|
+
parts.push(JSON.stringify(r.name.slice(0, 80)));
|
|
205
|
+
if (r.valueLength !== undefined)
|
|
206
|
+
parts.push(`${r.valueLength} chars`);
|
|
207
|
+
parts.push(`(${r.x},${r.y})`);
|
|
208
|
+
if (r.disabled)
|
|
209
|
+
parts.push('[disabled]');
|
|
210
|
+
if (r.inDialog)
|
|
211
|
+
parts.push('[in dialog]');
|
|
212
|
+
if (r.inPortal)
|
|
213
|
+
parts.push('[in menu/popover]');
|
|
214
|
+
return parts.join(' ');
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/** Run a fresh scan and return it (the caller keeps the list). */
|
|
218
|
+
async function scanRefs(page) {
|
|
219
|
+
const raw = await (0, cdp_1.evaluateJs)(page, buildRefScanScript());
|
|
220
|
+
if (!raw || typeof raw !== 'object')
|
|
221
|
+
return { at: 0, list: [] };
|
|
222
|
+
const rec = raw;
|
|
223
|
+
return {
|
|
224
|
+
at: typeof rec.at === 'number' ? rec.at : 0,
|
|
225
|
+
list: Array.isArray(rec.list) ? rec.list : [],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** Liveness + geometry of one ref, from the node the scan stored. */
|
|
229
|
+
function buildRefInfoScript(ref) {
|
|
230
|
+
const id = JSON.stringify(ref);
|
|
231
|
+
return `(() => {
|
|
232
|
+
const s = window.__tsRefs;
|
|
233
|
+
const el = s && s.nodes ? s.nodes[${id}] : null;
|
|
234
|
+
if (!el || el.isConnected !== true) return { ref: ${id}, alive: false };
|
|
235
|
+
const r = el.getBoundingClientRect();
|
|
236
|
+
return {
|
|
237
|
+
ref: ${id},
|
|
238
|
+
alive: true,
|
|
239
|
+
x: Math.round(r.x + r.width / 2),
|
|
240
|
+
y: Math.round(r.y + r.height / 2),
|
|
241
|
+
width: Math.round(r.width),
|
|
242
|
+
height: Math.round(r.height),
|
|
243
|
+
inViewport:
|
|
244
|
+
r.top >= 0 && r.left >= 0 &&
|
|
245
|
+
r.bottom <= (window.innerHeight || 0) && r.right <= (window.innerWidth || 0),
|
|
246
|
+
};
|
|
247
|
+
})()`;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Focus a ref'd element, scrolling it to the middle first. Returns whether the
|
|
251
|
+
* element actually took focus plus its tag/role, so `focus` is never a silent
|
|
252
|
+
* no-op: a div without `tabindex` cannot be focused and says so.
|
|
253
|
+
*/
|
|
254
|
+
function buildRefFocusScript(ref) {
|
|
255
|
+
const id = JSON.stringify(ref);
|
|
256
|
+
return `(() => {
|
|
257
|
+
const s = window.__tsRefs;
|
|
258
|
+
const el = s && s.nodes ? s.nodes[${id}] : null;
|
|
259
|
+
if (!el || el.isConnected !== true) return { ref: ${id}, ok: false, alive: false };
|
|
260
|
+
try { el.scrollIntoView({ block: 'center', inline: 'center' }); } catch (e) {}
|
|
261
|
+
try { el.focus({ preventScroll: true }); } catch (e) {}
|
|
262
|
+
const a = document.activeElement;
|
|
263
|
+
return {
|
|
264
|
+
ref: ${id},
|
|
265
|
+
ok: a === el,
|
|
266
|
+
alive: true,
|
|
267
|
+
focused: a === el,
|
|
268
|
+
tag: String((el.tagName || '')).toLowerCase(),
|
|
269
|
+
contentEditable: el.isContentEditable === true,
|
|
270
|
+
valueLength: typeof el.value === 'string' ? el.value.length : undefined,
|
|
271
|
+
};
|
|
272
|
+
})()`;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Dispatch the FULL pointer sequence on a ref'd element, in-page.
|
|
276
|
+
*
|
|
277
|
+
* This is the fallback for controls that ignore a coordinate click: the
|
|
278
|
+
* frontend tours (`frontend/src/modules/tour/utils/acts.ts`) taught us that a
|
|
279
|
+
* bare `.click()` is a SILENT no-op on Radix tabs/menus, which activate on
|
|
280
|
+
* `pointerdown`/`mousedown`. Same recipe, one copy per side of the wire.
|
|
281
|
+
*/
|
|
282
|
+
function buildRefPointerScript(ref) {
|
|
283
|
+
const id = JSON.stringify(ref);
|
|
284
|
+
return `(() => {
|
|
285
|
+
const s = window.__tsRefs;
|
|
286
|
+
const el = s && s.nodes ? s.nodes[${id}] : null;
|
|
287
|
+
if (!el || el.isConnected !== true) return { ref: ${id}, ok: false, alive: false };
|
|
288
|
+
try { el.scrollIntoView({ block: 'center', inline: 'center' }); } catch (e) {}
|
|
289
|
+
const rect = el.getBoundingClientRect();
|
|
290
|
+
const x = Math.round(rect.x + rect.width / 2);
|
|
291
|
+
const y = Math.round(rect.y + rect.height / 2);
|
|
292
|
+
const base = {
|
|
293
|
+
bubbles: true,
|
|
294
|
+
cancelable: true,
|
|
295
|
+
composed: true,
|
|
296
|
+
clientX: x,
|
|
297
|
+
clientY: y,
|
|
298
|
+
view: window,
|
|
299
|
+
button: 0,
|
|
300
|
+
buttons: 1,
|
|
301
|
+
};
|
|
302
|
+
const pointer = (type) => {
|
|
303
|
+
try {
|
|
304
|
+
return new PointerEvent(type, Object.assign({}, base, {
|
|
305
|
+
pointerId: 1,
|
|
306
|
+
pointerType: 'mouse',
|
|
307
|
+
isPrimary: true,
|
|
308
|
+
}));
|
|
309
|
+
} catch (e) {
|
|
310
|
+
return new MouseEvent(type, base);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
const mouse = (type) => new MouseEvent(type, base);
|
|
314
|
+
el.dispatchEvent(pointer('pointerover'));
|
|
315
|
+
el.dispatchEvent(pointer('pointerenter'));
|
|
316
|
+
el.dispatchEvent(pointer('pointermove'));
|
|
317
|
+
el.dispatchEvent(pointer('pointerdown'));
|
|
318
|
+
el.dispatchEvent(mouse('mousedown'));
|
|
319
|
+
try { el.focus({ preventScroll: true }); } catch (e) {}
|
|
320
|
+
el.dispatchEvent(pointer('pointerup'));
|
|
321
|
+
el.dispatchEvent(mouse('mouseup'));
|
|
322
|
+
el.dispatchEvent(mouse('click'));
|
|
323
|
+
return { ref: ${id}, ok: true, alive: true, clicked: true, x, y };
|
|
324
|
+
})()`;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Draw ref labels over the live page (2026-10).
|
|
328
|
+
*
|
|
329
|
+
* A plain screenshot shows WHAT is on screen; the model still has to guess
|
|
330
|
+
* which button is `e7`. The overlay draws a numbered box on every ref'd
|
|
331
|
+
* element so ONE image both shows the page and names the targets. Removed
|
|
332
|
+
* immediately after the capture - the page is never left modified.
|
|
333
|
+
*/
|
|
334
|
+
function buildAnnotateScript() {
|
|
335
|
+
return `(() => {
|
|
336
|
+
const s = window.__tsRefs;
|
|
337
|
+
if (!s || !s.list) return 0;
|
|
338
|
+
const ID = 'ts-ref-overlay';
|
|
339
|
+
const old = document.getElementById(ID);
|
|
340
|
+
if (old) old.remove();
|
|
341
|
+
const root = document.createElement('div');
|
|
342
|
+
root.id = ID;
|
|
343
|
+
root.style.cssText = 'position:fixed;left:0;top:0;width:0;height:0;z-index:2147483647;pointer-events:none;';
|
|
344
|
+
let n = 0;
|
|
345
|
+
for (const item of s.list) {
|
|
346
|
+
const el = s.nodes ? s.nodes[item.ref] : null;
|
|
347
|
+
if (!el || el.isConnected !== true) continue;
|
|
348
|
+
const r = el.getBoundingClientRect();
|
|
349
|
+
if (r.width === 0 || r.height === 0) continue;
|
|
350
|
+
const box = document.createElement('div');
|
|
351
|
+
box.style.cssText =
|
|
352
|
+
'position:fixed;border:2px solid #e11d48;border-radius:3px;box-sizing:border-box;pointer-events:none;left:' +
|
|
353
|
+
Math.round(r.left) + 'px;top:' + Math.round(r.top) + 'px;width:' + Math.round(r.width) +
|
|
354
|
+
'px;height:' + Math.round(r.height) + 'px;';
|
|
355
|
+
const tag = document.createElement('div');
|
|
356
|
+
tag.textContent = item.ref;
|
|
357
|
+
tag.style.cssText =
|
|
358
|
+
'position:fixed;background:#e11d48;color:#ffffff;font:600 11px/1.2 sans-serif;padding:1px 3px;border-radius:2px;pointer-events:none;left:' +
|
|
359
|
+
Math.round(r.left) + 'px;top:' + Math.max(0, Math.round(r.top) - 14) + 'px;';
|
|
360
|
+
root.appendChild(box);
|
|
361
|
+
root.appendChild(tag);
|
|
362
|
+
if (++n >= 120) break;
|
|
363
|
+
}
|
|
364
|
+
document.documentElement.appendChild(root);
|
|
365
|
+
return n;
|
|
366
|
+
})()`;
|
|
367
|
+
}
|
|
368
|
+
/** Remove the ref overlay (always called, even when the capture failed). */
|
|
369
|
+
function buildClearAnnotationScript() {
|
|
370
|
+
return `(() => {
|
|
371
|
+
const el = document.getElementById('ts-ref-overlay');
|
|
372
|
+
if (el) el.remove();
|
|
373
|
+
return true;
|
|
374
|
+
})()`;
|
|
375
|
+
}
|
|
376
|
+
/** How many popovers/menus are currently open (used by closePopup). */
|
|
377
|
+
function buildPopoverCountScript() {
|
|
378
|
+
return `(() => {
|
|
379
|
+
const vis = (el) => {
|
|
380
|
+
if (!el || el.nodeType !== 1) return false;
|
|
381
|
+
const r = el.getBoundingClientRect();
|
|
382
|
+
if (r.width === 0 || r.height === 0) return false;
|
|
383
|
+
const cs = getComputedStyle(el);
|
|
384
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
385
|
+
};
|
|
386
|
+
const n = [...document.querySelectorAll('[data-radix-popper-content-wrapper],[role="menu"],[role="listbox"]')]
|
|
387
|
+
.filter(vis).length;
|
|
388
|
+
return n;
|
|
389
|
+
})()`;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Click OUTSIDE every open layer (the Radix DismissableLayer listens for a
|
|
393
|
+
* document-level `pointerdown`). Coordinate clicks can land on something
|
|
394
|
+
* dangerous in a corner, so this dispatches the pointer sequence on `<body>`
|
|
395
|
+
* at the layer's own outside point instead.
|
|
396
|
+
*/
|
|
397
|
+
function buildOutsidePointerScript() {
|
|
398
|
+
return `(() => {
|
|
399
|
+
const target = document.body;
|
|
400
|
+
if (!target) return false;
|
|
401
|
+
const base = {
|
|
402
|
+
bubbles: true,
|
|
403
|
+
cancelable: true,
|
|
404
|
+
composed: true,
|
|
405
|
+
clientX: 4,
|
|
406
|
+
clientY: 4,
|
|
407
|
+
view: window,
|
|
408
|
+
button: 0,
|
|
409
|
+
buttons: 1,
|
|
410
|
+
};
|
|
411
|
+
const pointer = (type) => {
|
|
412
|
+
try {
|
|
413
|
+
return new PointerEvent(type, Object.assign({}, base, {
|
|
414
|
+
pointerId: 1,
|
|
415
|
+
pointerType: 'mouse',
|
|
416
|
+
isPrimary: true,
|
|
417
|
+
}));
|
|
418
|
+
} catch (e) {
|
|
419
|
+
return new MouseEvent(type, base);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
target.dispatchEvent(pointer('pointerdown'));
|
|
423
|
+
target.dispatchEvent(new MouseEvent('mousedown', base));
|
|
424
|
+
target.dispatchEvent(pointer('pointerup'));
|
|
425
|
+
target.dispatchEvent(new MouseEvent('mouseup', base));
|
|
426
|
+
target.dispatchEvent(new MouseEvent('click', base));
|
|
427
|
+
return true;
|
|
428
|
+
})()`;
|
|
429
|
+
}
|
|
430
|
+
//# sourceMappingURL=refs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refs.js","sourceRoot":"","sources":["../../../src/lib/browser/refs.ts"],"names":[],"mappings":"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"}
|