donobu 5.60.4 → 5.60.6
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/esm/lib/ai/locate/locateElement.js +12 -2
- package/dist/esm/lib/page/extendPage.js +1 -1
- package/dist/esm/lib/test/testExtension.js +20 -4
- package/dist/esm/managers/DonobuFlowsManager.js +1 -1
- package/dist/esm/managers/InteractionVisualizer.d.ts +34 -30
- package/dist/esm/managers/InteractionVisualizer.js +98 -204
- package/dist/esm/tools/ReplayableInteraction.js +1 -1
- package/dist/esm/utils/BrowserUtils.js +1 -16
- package/dist/lib/ai/locate/locateElement.js +12 -2
- package/dist/lib/page/extendPage.js +1 -1
- package/dist/lib/test/testExtension.js +20 -4
- package/dist/managers/DonobuFlowsManager.js +1 -1
- package/dist/managers/InteractionVisualizer.d.ts +34 -30
- package/dist/managers/InteractionVisualizer.js +98 -204
- package/dist/tools/ReplayableInteraction.js +1 -1
- package/dist/utils/BrowserUtils.js +1 -16
- package/package.json +7 -7
|
@@ -13,6 +13,14 @@ const locateSchema_1 = require("./locateSchema");
|
|
|
13
13
|
const DISAMBIGUATE_THRESHOLD = 5;
|
|
14
14
|
/** Maximum outer HTML length per snippet shown during disambiguation. */
|
|
15
15
|
const SNIPPET_MAX_CHARS = 200;
|
|
16
|
+
/**
|
|
17
|
+
* Per-candidate read budget during disambiguation. Locator `evaluate`/
|
|
18
|
+
* `boundingBox` default to *no* timeout, so a candidate that detaches mid-loop
|
|
19
|
+
* would wait forever (the surrounding try/catch only traps errors, not hangs,
|
|
20
|
+
* and the AbortSignal isn't plumbed into these ops). Cap it and let a miss fall
|
|
21
|
+
* through to the placeholder snippet below.
|
|
22
|
+
*/
|
|
23
|
+
const CANDIDATE_READ_TIMEOUT_MILLIS = 1000;
|
|
16
24
|
/**
|
|
17
25
|
* Resolve a natural-language element description to a Playwright {@link Locator}.
|
|
18
26
|
*
|
|
@@ -87,8 +95,10 @@ async function disambiguate(page, description, gptClient, locator, locateResult,
|
|
|
87
95
|
const html = await nth.evaluate((el, max) => {
|
|
88
96
|
const raw = el.outerHTML;
|
|
89
97
|
return raw.length > max ? raw.slice(0, max) + '…' : raw;
|
|
90
|
-
}, SNIPPET_MAX_CHARS);
|
|
91
|
-
const box = await nth.boundingBox(
|
|
98
|
+
}, SNIPPET_MAX_CHARS, { timeout: CANDIDATE_READ_TIMEOUT_MILLIS });
|
|
99
|
+
const box = await nth.boundingBox({
|
|
100
|
+
timeout: CANDIDATE_READ_TIMEOUT_MILLIS,
|
|
101
|
+
});
|
|
92
102
|
const pos = box
|
|
93
103
|
? `(${Math.round(box.x)}, ${Math.round(box.y)})`
|
|
94
104
|
: '(unknown)';
|
|
@@ -166,7 +166,7 @@ async function extendPage(page, options) {
|
|
|
166
166
|
aiInvocations: [],
|
|
167
167
|
};
|
|
168
168
|
const showMouse = async (p) => {
|
|
169
|
-
if (interactionVisualizer.
|
|
169
|
+
if (interactionVisualizer.defaultCursorDurationMillis > 0) {
|
|
170
170
|
await interactionVisualizer.showMouse(p);
|
|
171
171
|
}
|
|
172
172
|
};
|
|
@@ -122,28 +122,44 @@ async function waitForPendingVideoPersists(timeoutMs) {
|
|
|
122
122
|
* - `'on'` → always retain → always persist.
|
|
123
123
|
* - `'retain-on-failure'` → retain only on non-passing → persist
|
|
124
124
|
* only when status !== 'passed'.
|
|
125
|
+
* - `'retain-on-first-failure'` → recorded only on the first run; a video
|
|
126
|
+
* only exists when that run failed → same
|
|
127
|
+
* persist condition as 'retain-on-failure'.
|
|
128
|
+
* - `'retain-on-failure-and-retries'` → retain on non-passing OR on any
|
|
129
|
+
* retry → persist when status !== 'passed'
|
|
130
|
+
* or retry > 0.
|
|
125
131
|
* - `'on-first-retry'` → recorded only on retries; if a video
|
|
126
132
|
* exists, we're on a retry → persist.
|
|
133
|
+
* - `'on-all-retries'` → recorded on every retry; same as
|
|
134
|
+
* 'on-first-retry' once a video exists.
|
|
127
135
|
* - any unknown future mode → conservatively SKIP, with a warn log.
|
|
128
136
|
* Better to under-persist than violate
|
|
129
137
|
* user intent for a mode we don't yet
|
|
130
138
|
* understand.
|
|
131
139
|
*/
|
|
132
|
-
function shouldPersistVideo(videoOption, status) {
|
|
140
|
+
function shouldPersistVideo(videoOption, status, retry) {
|
|
133
141
|
const mode = typeof videoOption === 'string' ? videoOption : videoOption?.mode;
|
|
134
142
|
switch (mode) {
|
|
135
143
|
case 'on':
|
|
136
144
|
return true;
|
|
137
145
|
case 'on-first-retry':
|
|
138
|
-
|
|
139
|
-
//
|
|
146
|
+
case 'on-all-retries':
|
|
147
|
+
// Playwright only records on retries under these modes; if a video
|
|
148
|
+
// exists it implies we're on a retry — always retain.
|
|
140
149
|
return true;
|
|
141
150
|
case 'retry-with-video':
|
|
142
151
|
// Deprecated alias for 'on-first-retry' that Playwright still
|
|
143
152
|
// accepts on the type. Same retain semantics.
|
|
144
153
|
return true;
|
|
145
154
|
case 'retain-on-failure':
|
|
155
|
+
case 'retain-on-first-failure':
|
|
156
|
+
// Both record up front and discard on a passing run. ('first-failure'
|
|
157
|
+
// only records the first run, but a video existing already implies
|
|
158
|
+
// that's the run we're persisting.)
|
|
146
159
|
return status !== 'passed';
|
|
160
|
+
case 'retain-on-failure-and-retries':
|
|
161
|
+
// Records every run; kept on failure OR on any retry attempt.
|
|
162
|
+
return status !== 'passed' || retry > 0;
|
|
147
163
|
case 'off':
|
|
148
164
|
case undefined:
|
|
149
165
|
return false;
|
|
@@ -182,7 +198,7 @@ function persistVideoIfApplicable(page, testInfo, videoOption) {
|
|
|
182
198
|
// and this isn't a retry, etc. Nothing to do.
|
|
183
199
|
return;
|
|
184
200
|
}
|
|
185
|
-
if (!shouldPersistVideo(videoOption, testInfo.status)) {
|
|
201
|
+
if (!shouldPersistVideo(videoOption, testInfo.status, testInfo.retry)) {
|
|
186
202
|
Logger_1.appLogger.info(`Skipping video persist for flow ${flowId}: video mode ` +
|
|
187
203
|
`"${describeVideoMode(videoOption)}" + status "${testInfo.status}" ` +
|
|
188
204
|
`means Playwright will discard the file and we'd be violating user ` +
|
|
@@ -174,7 +174,7 @@ class DonobuFlowsManager {
|
|
|
174
174
|
gptConfigName: gptClientData.gptConfigName,
|
|
175
175
|
hasGptConfigNameOverride: !gptClientData.agentName,
|
|
176
176
|
customTools: flowParams.customTools ?? null,
|
|
177
|
-
defaultMessageDuration: interactionVisualizer.
|
|
177
|
+
defaultMessageDuration: interactionVisualizer.defaultCursorDurationMillis,
|
|
178
178
|
callbackUrl: flowParams.callbackUrl || null,
|
|
179
179
|
overallObjective: flowParams.overallObjective ?? null,
|
|
180
180
|
allowedTools: allowedTools.map((tool) => tool.name),
|
|
@@ -1,60 +1,64 @@
|
|
|
1
1
|
import type { Locator, Page } from 'playwright';
|
|
2
2
|
export declare class InteractionVisualizer {
|
|
3
|
-
readonly
|
|
3
|
+
readonly defaultCursorDurationMillis: number;
|
|
4
4
|
private static readonly RAW_MOUSE_D;
|
|
5
5
|
private static readonly TIP_X;
|
|
6
6
|
private static readonly TIP_Y;
|
|
7
7
|
private static readonly SVG_MOUSE;
|
|
8
|
-
private static readonly SVG_MOUSE_JSON;
|
|
9
|
-
private static readonly CSS;
|
|
10
|
-
private static readonly CONTAINER_ID;
|
|
11
|
-
private cursorPos;
|
|
12
|
-
constructor(defaultMessageDurationMillis: number);
|
|
13
8
|
/**
|
|
14
|
-
*
|
|
9
|
+
* Upper bound on resolving a target's bounding box. `Locator.boundingBox`
|
|
10
|
+
* defaults to *no* timeout, so a locator that resolves to zero elements
|
|
11
|
+
* (e.g. detached mid-flow) would otherwise wait forever and hang the flow.
|
|
12
|
+
* The cursor is cosmetic, so we cap the wait and treat a miss as a no-op.
|
|
13
|
+
*/
|
|
14
|
+
private static readonly BOUNDING_BOX_TIMEOUT_MILLIS;
|
|
15
|
+
/**
|
|
16
|
+
* Per-page cursor state. Keyed weakly so closed/GC'd pages drop out without
|
|
17
|
+
* us tracking page lifecycles.
|
|
18
|
+
*/
|
|
19
|
+
private readonly states;
|
|
20
|
+
constructor(defaultCursorDurationMillis: number);
|
|
21
|
+
/**
|
|
22
|
+
* Moves the virtual cursor to the center of the specified element.
|
|
15
23
|
*
|
|
16
24
|
* @param page - The Playwright page instance where the cursor will be displayed.
|
|
17
25
|
* @param locator - Optional target element to point at. If omitted, cursor remains at current position.
|
|
18
|
-
* @param
|
|
19
|
-
* @param duration - Duration in milliseconds for the cursor animation and message display.
|
|
26
|
+
* @param duration - Duration in milliseconds for the cursor animation.
|
|
20
27
|
* Defaults to the instance's configured duration. If ≤ 0, no action is taken
|
|
21
28
|
*
|
|
22
|
-
* @returns Promise that resolves
|
|
29
|
+
* @returns Promise that resolves once the cursor has reached the target
|
|
23
30
|
*
|
|
24
31
|
* @remarks
|
|
25
|
-
* - The target element will be scrolled into view if necessary
|
|
26
32
|
* - The cursor animates smoothly to the element's center point
|
|
27
|
-
* -
|
|
28
|
-
* - The virtual cursor does not interfere with actual page interactions
|
|
33
|
+
* - The overlay is `pointer-events: none`, so it never intercepts real interactions
|
|
29
34
|
*/
|
|
30
|
-
pointAt(page: Page, locator?: Pick<Locator, 'boundingBox'>,
|
|
35
|
+
pointAt(page: Page, locator?: Pick<Locator, 'boundingBox'>, duration?: number): Promise<void>;
|
|
31
36
|
/**
|
|
32
|
-
* Shows the virtual mouse cursor on the page.
|
|
37
|
+
* Shows the virtual mouse cursor on the page at its current position.
|
|
33
38
|
*
|
|
34
39
|
* @param page - The Playwright page instance where the cursor will be displayed.
|
|
35
|
-
*
|
|
36
40
|
* @returns Promise that resolves when the cursor is shown.
|
|
37
|
-
*
|
|
38
|
-
* @remarks
|
|
39
|
-
* - If the cursor doesn't exist yet, it will be created.
|
|
40
|
-
* - The cursor will be made visible with a smooth opacity transition.
|
|
41
41
|
*/
|
|
42
42
|
showMouse(page: Page): Promise<void>;
|
|
43
43
|
/**
|
|
44
44
|
* Hides the virtual mouse cursor on the page.
|
|
45
45
|
*
|
|
46
46
|
* @param page - The Playwright page instance where the cursor is displayed.
|
|
47
|
-
*
|
|
48
|
-
* @returns Promise that resolves when the cursor is hidden
|
|
49
|
-
*
|
|
50
|
-
* @remarks
|
|
51
|
-
* - The cursor will be hidden with a smooth opacity transition
|
|
52
|
-
* - The cursor element remains in the DOM but becomes invisible
|
|
47
|
+
* @returns Promise that resolves when the cursor is hidden.
|
|
53
48
|
*/
|
|
54
49
|
hideMouse(page: Page): Promise<void>;
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
private getState;
|
|
51
|
+
/**
|
|
52
|
+
* Replaces the sticky cursor overlay with one positioned at `to`, gliding
|
|
53
|
+
* from `from` when `animMs > 0`. We show the new overlay before disposing
|
|
54
|
+
* the old one to avoid a visible flicker, and tolerate a stale disposable
|
|
55
|
+
* (e.g. cleared by a navigation) by swallowing its dispose error.
|
|
56
|
+
*/
|
|
57
|
+
private renderCursor;
|
|
58
|
+
/**
|
|
59
|
+
* Builds the cursor overlay markup: the arrow SVG plus a `@keyframes` glide
|
|
60
|
+
* from the previous position and a one-shot ripple at the arrow tip.
|
|
61
|
+
*/
|
|
62
|
+
private buildCursorHtml;
|
|
59
63
|
}
|
|
60
64
|
//# sourceMappingURL=InteractionVisualizer.d.ts.map
|
|
@@ -4,49 +4,55 @@ exports.InteractionVisualizer = void 0;
|
|
|
4
4
|
const Logger_1 = require("../utils/Logger");
|
|
5
5
|
const PlaywrightUtils_1 = require("../utils/PlaywrightUtils");
|
|
6
6
|
class InteractionVisualizer {
|
|
7
|
-
constructor(
|
|
8
|
-
this.
|
|
9
|
-
|
|
7
|
+
constructor(defaultCursorDurationMillis) {
|
|
8
|
+
this.defaultCursorDurationMillis = defaultCursorDurationMillis;
|
|
9
|
+
/**
|
|
10
|
+
* Per-page cursor state. Keyed weakly so closed/GC'd pages drop out without
|
|
11
|
+
* us tracking page lifecycles.
|
|
12
|
+
*/
|
|
13
|
+
this.states = new WeakMap();
|
|
10
14
|
}
|
|
11
15
|
/* ------------------------------------------------------------------ */
|
|
12
16
|
/* Public API */
|
|
13
17
|
/* ------------------------------------------------------------------ */
|
|
14
18
|
/**
|
|
15
|
-
* Moves the virtual cursor to the center of the specified element
|
|
19
|
+
* Moves the virtual cursor to the center of the specified element.
|
|
16
20
|
*
|
|
17
21
|
* @param page - The Playwright page instance where the cursor will be displayed.
|
|
18
22
|
* @param locator - Optional target element to point at. If omitted, cursor remains at current position.
|
|
19
|
-
* @param
|
|
20
|
-
* @param duration - Duration in milliseconds for the cursor animation and message display.
|
|
23
|
+
* @param duration - Duration in milliseconds for the cursor animation.
|
|
21
24
|
* Defaults to the instance's configured duration. If ≤ 0, no action is taken
|
|
22
25
|
*
|
|
23
|
-
* @returns Promise that resolves
|
|
26
|
+
* @returns Promise that resolves once the cursor has reached the target
|
|
24
27
|
*
|
|
25
28
|
* @remarks
|
|
26
|
-
* - The target element will be scrolled into view if necessary
|
|
27
29
|
* - The cursor animates smoothly to the element's center point
|
|
28
|
-
* -
|
|
29
|
-
* - The virtual cursor does not interfere with actual page interactions
|
|
30
|
+
* - The overlay is `pointer-events: none`, so it never intercepts real interactions
|
|
30
31
|
*/
|
|
31
|
-
async pointAt(page, locator,
|
|
32
|
+
async pointAt(page, locator, duration = this.defaultCursorDurationMillis) {
|
|
32
33
|
if (!duration || duration <= 0) {
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
try {
|
|
36
|
-
|
|
37
|
+
const state = this.getState(page);
|
|
38
|
+
const from = { ...state.pos };
|
|
39
|
+
let target = from;
|
|
37
40
|
if (locator) {
|
|
38
|
-
|
|
41
|
+
// Cap the wait and swallow a timeout: an unresolvable locator must
|
|
42
|
+
// leave the (cosmetic) cursor in place, never block the flow.
|
|
43
|
+
const box = await locator
|
|
44
|
+
.boundingBox({
|
|
45
|
+
timeout: InteractionVisualizer.BOUNDING_BOX_TIMEOUT_MILLIS,
|
|
46
|
+
})
|
|
47
|
+
.catch(() => null);
|
|
39
48
|
if (box) {
|
|
40
49
|
target = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
41
50
|
}
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.
|
|
46
|
-
|
|
47
|
-
? this.showMessage(page, target, message.trim(), duration)
|
|
48
|
-
: Promise.resolve(),
|
|
49
|
-
]);
|
|
52
|
+
state.pos = target;
|
|
53
|
+
if (state.visible) {
|
|
54
|
+
await this.renderCursor(page, state, from, target, duration / 2);
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
catch (error) {
|
|
52
58
|
if (!PlaywrightUtils_1.PlaywrightUtils.isPageClosedError(error)) {
|
|
@@ -55,28 +61,16 @@ class InteractionVisualizer {
|
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
/**
|
|
58
|
-
* Shows the virtual mouse cursor on the page.
|
|
64
|
+
* Shows the virtual mouse cursor on the page at its current position.
|
|
59
65
|
*
|
|
60
66
|
* @param page - The Playwright page instance where the cursor will be displayed.
|
|
61
|
-
*
|
|
62
67
|
* @returns Promise that resolves when the cursor is shown.
|
|
63
|
-
*
|
|
64
|
-
* @remarks
|
|
65
|
-
* - If the cursor doesn't exist yet, it will be created.
|
|
66
|
-
* - The cursor will be made visible with a smooth opacity transition.
|
|
67
68
|
*/
|
|
68
69
|
async showMouse(page) {
|
|
69
70
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await
|
|
73
|
-
const root = document.getElementById(containerId)
|
|
74
|
-
.shadowRoot;
|
|
75
|
-
const cursor = root.querySelector('.donobu-virtual-mouse');
|
|
76
|
-
if (cursor) {
|
|
77
|
-
cursor.classList.remove('hidden');
|
|
78
|
-
}
|
|
79
|
-
}, [InteractionVisualizer.CONTAINER_ID]);
|
|
71
|
+
const state = this.getState(page);
|
|
72
|
+
state.visible = true;
|
|
73
|
+
await this.renderCursor(page, state, state.pos, state.pos, 0);
|
|
80
74
|
}
|
|
81
75
|
catch (error) {
|
|
82
76
|
if (!PlaywrightUtils_1.PlaywrightUtils.isPageClosedError(error)) {
|
|
@@ -84,30 +78,22 @@ class InteractionVisualizer {
|
|
|
84
78
|
}
|
|
85
79
|
}
|
|
86
80
|
}
|
|
81
|
+
/* ------------------------------------------------------------------ */
|
|
82
|
+
/* Private helpers */
|
|
83
|
+
/* ------------------------------------------------------------------ */
|
|
87
84
|
/**
|
|
88
85
|
* Hides the virtual mouse cursor on the page.
|
|
89
86
|
*
|
|
90
87
|
* @param page - The Playwright page instance where the cursor is displayed.
|
|
91
|
-
*
|
|
92
|
-
* @returns Promise that resolves when the cursor is hidden
|
|
93
|
-
*
|
|
94
|
-
* @remarks
|
|
95
|
-
* - The cursor will be hidden with a smooth opacity transition
|
|
96
|
-
* - The cursor element remains in the DOM but becomes invisible
|
|
88
|
+
* @returns Promise that resolves when the cursor is hidden.
|
|
97
89
|
*/
|
|
98
90
|
async hideMouse(page) {
|
|
99
91
|
try {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
const cursor = container.shadowRoot.querySelector('.donobu-virtual-mouse');
|
|
107
|
-
if (cursor) {
|
|
108
|
-
cursor.classList.add('hidden');
|
|
109
|
-
}
|
|
110
|
-
}, [id]);
|
|
92
|
+
const state = this.getState(page);
|
|
93
|
+
state.visible = false;
|
|
94
|
+
const cursor = state.cursor;
|
|
95
|
+
state.cursor = undefined;
|
|
96
|
+
await cursor?.dispose();
|
|
111
97
|
}
|
|
112
98
|
catch (error) {
|
|
113
99
|
if (!PlaywrightUtils_1.PlaywrightUtils.isPageClosedError(error)) {
|
|
@@ -115,138 +101,64 @@ class InteractionVisualizer {
|
|
|
115
101
|
}
|
|
116
102
|
}
|
|
117
103
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const el = document.createElement('div');
|
|
128
|
-
el.id = containerId;
|
|
129
|
-
Object.assign(el.style, {
|
|
130
|
-
position: 'fixed',
|
|
131
|
-
top: '0',
|
|
132
|
-
left: '0',
|
|
133
|
-
width: '100vw',
|
|
134
|
-
height: '100vh',
|
|
135
|
-
pointerEvents: 'none',
|
|
136
|
-
zIndex: '2147483646', // just below the message itself
|
|
137
|
-
});
|
|
138
|
-
const shadow = el.attachShadow({ mode: 'open' });
|
|
139
|
-
const style = document.createElement('style');
|
|
140
|
-
style.textContent = css;
|
|
141
|
-
shadow.appendChild(style);
|
|
142
|
-
// Append to <html> so it is not clipped by overflow/transform on <body>
|
|
143
|
-
(document.documentElement || document.body).appendChild(el);
|
|
144
|
-
}, [id, InteractionVisualizer.CSS]);
|
|
104
|
+
getState(page) {
|
|
105
|
+
let state = this.states.get(page);
|
|
106
|
+
if (!state) {
|
|
107
|
+
state = { pos: { x: 150, y: 150 }, visible: false };
|
|
108
|
+
this.states.set(page, state);
|
|
109
|
+
}
|
|
110
|
+
return state;
|
|
145
111
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Hybrid approach: try insertAdjacentHTML first, fallback to createElementNS
|
|
112
|
+
/**
|
|
113
|
+
* Replaces the sticky cursor overlay with one positioned at `to`, gliding
|
|
114
|
+
* from `from` when `animMs > 0`. We show the new overlay before disposing
|
|
115
|
+
* the old one to avoid a visible flicker, and tolerate a stale disposable
|
|
116
|
+
* (e.g. cleared by a navigation) by swallowing its dispose error.
|
|
117
|
+
*/
|
|
118
|
+
async renderCursor(page, state, from, to, animMs) {
|
|
119
|
+
const previous = state.cursor;
|
|
120
|
+
state.cursor = await page.screencast.showOverlay(this.buildCursorHtml(from, to, animMs));
|
|
121
|
+
if (previous) {
|
|
157
122
|
try {
|
|
158
|
-
|
|
123
|
+
await previous.dispose();
|
|
159
124
|
}
|
|
160
|
-
catch
|
|
161
|
-
//
|
|
162
|
-
const attrs = JSON.parse(svgAttrs);
|
|
163
|
-
const svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
164
|
-
// Set SVG attributes
|
|
165
|
-
Object.entries(attrs.svg).forEach(([key, value]) => {
|
|
166
|
-
svgElement.setAttribute(key, value);
|
|
167
|
-
});
|
|
168
|
-
// Create and add path element
|
|
169
|
-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
170
|
-
Object.entries(attrs.path).forEach(([key, value]) => {
|
|
171
|
-
path.setAttribute(key, value);
|
|
172
|
-
});
|
|
173
|
-
svgElement.appendChild(path);
|
|
174
|
-
cursor.appendChild(svgElement);
|
|
125
|
+
catch {
|
|
126
|
+
// Stale overlay (page navigated/closed) — the fresh one already replaced it.
|
|
175
127
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
InteractionVisualizer.TIP_Y,
|
|
187
|
-
]);
|
|
188
|
-
}
|
|
189
|
-
async moveCursor(page, target, duration) {
|
|
190
|
-
await this.ensureCursor(page);
|
|
191
|
-
const id = InteractionVisualizer.CONTAINER_ID;
|
|
192
|
-
await page.evaluate(([containerId, end, duration, tipX, tipY]) => {
|
|
193
|
-
const root = document.getElementById(containerId).shadowRoot;
|
|
194
|
-
const cursor = root.querySelector('.donobu-virtual-mouse');
|
|
195
|
-
cursor.style.transitionDuration = `${duration}ms`;
|
|
196
|
-
const endPos = end;
|
|
197
|
-
cursor.style.transform = `translate(${endPos.x - tipX}px, ${endPos.y - tipY}px)`;
|
|
198
|
-
cursor.classList.add('rippling');
|
|
199
|
-
const done = new Promise((resolve) => {
|
|
200
|
-
const finish = () => {
|
|
201
|
-
cursor.removeEventListener('transitionend', finish);
|
|
202
|
-
cursor.classList.remove('rippling');
|
|
203
|
-
resolve();
|
|
204
|
-
};
|
|
205
|
-
cursor.addEventListener('transitionend', finish);
|
|
206
|
-
setTimeout(finish, duration + 50);
|
|
128
|
+
}
|
|
129
|
+
// showOverlay resolves the moment the overlay is added, before the CSS
|
|
130
|
+
// glide plays. Callers (e.g. ClickTool) await pointAt expecting the cursor
|
|
131
|
+
// to reach the element *before* they act, so block for the animation here.
|
|
132
|
+
// Only the moving case animates (see buildCursorHtml), so a no-op move
|
|
133
|
+
// (e.g. an unresolved target) shouldn't incur the wait.
|
|
134
|
+
const animating = animMs > 0 && (from.x !== to.x || from.y !== to.y);
|
|
135
|
+
if (animating) {
|
|
136
|
+
await new Promise((resolve) => {
|
|
137
|
+
setTimeout(resolve, animMs);
|
|
207
138
|
});
|
|
208
|
-
|
|
209
|
-
}, [
|
|
210
|
-
id,
|
|
211
|
-
target,
|
|
212
|
-
duration,
|
|
213
|
-
InteractionVisualizer.TIP_X,
|
|
214
|
-
InteractionVisualizer.TIP_Y,
|
|
215
|
-
]);
|
|
216
|
-
this.cursorPos = target;
|
|
139
|
+
}
|
|
217
140
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
y = target.y - height - 24;
|
|
240
|
-
}
|
|
241
|
-
/* if still off-screen at the top, clamp to 8 px margin */
|
|
242
|
-
if (y < 8) {
|
|
243
|
-
y = 8;
|
|
244
|
-
}
|
|
245
|
-
msg.style.left = `${x}px`;
|
|
246
|
-
msg.style.top = `${y}px`;
|
|
247
|
-
/* auto-remove after the requested duration */
|
|
248
|
-
setTimeout(() => msg.remove(), duration);
|
|
249
|
-
}, [id, target, text, duration]);
|
|
141
|
+
/**
|
|
142
|
+
* Builds the cursor overlay markup: the arrow SVG plus a `@keyframes` glide
|
|
143
|
+
* from the previous position and a one-shot ripple at the arrow tip.
|
|
144
|
+
*/
|
|
145
|
+
buildCursorHtml(from, to, animMs) {
|
|
146
|
+
const tipX = InteractionVisualizer.TIP_X;
|
|
147
|
+
const tipY = InteractionVisualizer.TIP_Y;
|
|
148
|
+
const fx = from.x - tipX;
|
|
149
|
+
const fy = from.y - tipY;
|
|
150
|
+
const tx = to.x - tipX;
|
|
151
|
+
const ty = to.y - tipY;
|
|
152
|
+
const animate = animMs > 0 && (from.x !== to.x || from.y !== to.y);
|
|
153
|
+
return `
|
|
154
|
+
<style>
|
|
155
|
+
@keyframes donobu-glide { from { transform: translate(${fx}px, ${fy}px); } to { transform: translate(${tx}px, ${ty}px); } }
|
|
156
|
+
@keyframes donobu-ripple { 0% { transform: translate(-50%,-50%) scale(.5); opacity:0; } 40% { transform: translate(-50%,-50%) scale(1); opacity:.5; } 100% { transform: translate(-50%,-50%) scale(2); opacity:0; } }
|
|
157
|
+
.donobu-cursor { position:fixed; left:0; top:0; width:32px; height:32px; z-index:2147483646; pointer-events:none; filter:drop-shadow(1px 1px 1px rgba(0,0,0,.5)); transform: translate(${tx}px, ${ty}px); ${animate ? `animation: donobu-glide ${animMs}ms ease-in-out;` : ''} }
|
|
158
|
+
.donobu-cursor svg { width:100%; height:100%; display:block; }
|
|
159
|
+
${animate ? `.donobu-cursor::after { content:""; position:absolute; left:${tipX}px; top:${tipY}px; width:24px; height:24px; border-radius:50%; background:#FF781B; opacity:0; animation: donobu-ripple .6s ease-out ${animMs}ms both; }` : ''}
|
|
160
|
+
</style>
|
|
161
|
+
<div class="donobu-cursor">${InteractionVisualizer.SVG_MOUSE}</div>`;
|
|
250
162
|
}
|
|
251
163
|
}
|
|
252
164
|
exports.InteractionVisualizer = InteractionVisualizer;
|
|
@@ -269,29 +181,11 @@ InteractionVisualizer.SVG_MOUSE = `
|
|
|
269
181
|
stroke-linejoin="round">
|
|
270
182
|
<path d="${InteractionVisualizer.RAW_MOUSE_D}" />
|
|
271
183
|
</svg>`;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
stroke: '#FF781B',
|
|
280
|
-
'stroke-width': '2',
|
|
281
|
-
'stroke-linecap': 'round',
|
|
282
|
-
'stroke-linejoin': 'round',
|
|
283
|
-
},
|
|
284
|
-
path: {
|
|
285
|
-
d: InteractionVisualizer.RAW_MOUSE_D,
|
|
286
|
-
},
|
|
287
|
-
};
|
|
288
|
-
InteractionVisualizer.CSS = `
|
|
289
|
-
.donobu-message{position:absolute;z-index:2147483647;background:#000;color:#fff;padding:8px 10px;border-radius:5px;font:12px/1 'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;max-width:300px;white-space:pre-wrap;box-shadow:2px 2px 10px rgba(0,0,0,.2);pointer-events:none}
|
|
290
|
-
.donobu-virtual-mouse{width:24px;height:24px;position:absolute;z-index:2147483646;filter:drop-shadow(1px 1px 1px rgba(0,0,0,.5));transition:transform .15s ease-in-out;pointer-events:none;opacity:1;visibility:visible}
|
|
291
|
-
.donobu-virtual-mouse.hidden{opacity:0;visibility:hidden}
|
|
292
|
-
.donobu-virtual-mouse svg{width:100%;height:100%;display:block}
|
|
293
|
-
.donobu-virtual-mouse.rippling::after{content:"";position:absolute;left:${InteractionVisualizer.TIP_X}px;top:${InteractionVisualizer.TIP_Y}px;width:24px;height:24px;border-radius:50%;background:#FF781B;transform:translate(-50%,-50%) scale(.5);opacity:.4;animation:ripple-pop .6s ease-out forwards}
|
|
294
|
-
@keyframes ripple-pop{40%{transform:translate(-50%,-50%) scale(1);opacity:.5}100%{transform:translate(-50%,-50%) scale(2);opacity:0}}
|
|
295
|
-
`;
|
|
296
|
-
InteractionVisualizer.CONTAINER_ID = 'donobu-iv-overlay';
|
|
184
|
+
/**
|
|
185
|
+
* Upper bound on resolving a target's bounding box. `Locator.boundingBox`
|
|
186
|
+
* defaults to *no* timeout, so a locator that resolves to zero elements
|
|
187
|
+
* (e.g. detached mid-flow) would otherwise wait forever and hang the flow.
|
|
188
|
+
* The cursor is cosmetic, so we cap the wait and treat a miss as a no-op.
|
|
189
|
+
*/
|
|
190
|
+
InteractionVisualizer.BOUNDING_BOX_TIMEOUT_MILLIS = 1000;
|
|
297
191
|
//# sourceMappingURL=InteractionVisualizer.js.map
|
|
@@ -432,7 +432,7 @@ class ReplayableInteraction extends Tool_1.Tool {
|
|
|
432
432
|
// Only reveal the cursor now that we have a real target to point at, so a
|
|
433
433
|
// non-interactive proposal never pops a stationary cursor.
|
|
434
434
|
await (0, TargetUtils_1.webInspector)(context).showInteractionCursor();
|
|
435
|
-
await context.interactionVisualizer.pointAt(page, pointTarget.first(),
|
|
435
|
+
await context.interactionVisualizer.pointAt(page, pointTarget.first(), ReplayableInteraction.PREVIEW_CURSOR_DURATION_MILLIS);
|
|
436
436
|
}
|
|
437
437
|
async callFromGpt(context, parameters) {
|
|
438
438
|
const page = (0, TargetUtils_1.webPage)(context);
|
|
@@ -199,22 +199,7 @@ class BrowserUtils {
|
|
|
199
199
|
// If the entry exists but doesn't have sessionStorage yet, add the property
|
|
200
200
|
originEntry.sessionStorage = [];
|
|
201
201
|
}
|
|
202
|
-
|
|
203
|
-
const sessionStorageItems = await page.evaluate(() => {
|
|
204
|
-
const items = [];
|
|
205
|
-
for (let i = 0; i < sessionStorage.length; i++) {
|
|
206
|
-
const name = sessionStorage.key(i);
|
|
207
|
-
if (name) {
|
|
208
|
-
items.push({
|
|
209
|
-
name,
|
|
210
|
-
value: sessionStorage.getItem(name) || '',
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return items;
|
|
215
|
-
});
|
|
216
|
-
// Add sessionStorage items to the origin entry
|
|
217
|
-
originEntry.sessionStorage = sessionStorageItems;
|
|
202
|
+
originEntry.sessionStorage = await page.sessionStorage.items();
|
|
218
203
|
}
|
|
219
204
|
catch (error) {
|
|
220
205
|
Logger_1.appLogger.warn(`Failed to extract sessionStorage for page: ${pageUrl}`, error);
|
|
@@ -13,6 +13,14 @@ const locateSchema_1 = require("./locateSchema");
|
|
|
13
13
|
const DISAMBIGUATE_THRESHOLD = 5;
|
|
14
14
|
/** Maximum outer HTML length per snippet shown during disambiguation. */
|
|
15
15
|
const SNIPPET_MAX_CHARS = 200;
|
|
16
|
+
/**
|
|
17
|
+
* Per-candidate read budget during disambiguation. Locator `evaluate`/
|
|
18
|
+
* `boundingBox` default to *no* timeout, so a candidate that detaches mid-loop
|
|
19
|
+
* would wait forever (the surrounding try/catch only traps errors, not hangs,
|
|
20
|
+
* and the AbortSignal isn't plumbed into these ops). Cap it and let a miss fall
|
|
21
|
+
* through to the placeholder snippet below.
|
|
22
|
+
*/
|
|
23
|
+
const CANDIDATE_READ_TIMEOUT_MILLIS = 1000;
|
|
16
24
|
/**
|
|
17
25
|
* Resolve a natural-language element description to a Playwright {@link Locator}.
|
|
18
26
|
*
|
|
@@ -87,8 +95,10 @@ async function disambiguate(page, description, gptClient, locator, locateResult,
|
|
|
87
95
|
const html = await nth.evaluate((el, max) => {
|
|
88
96
|
const raw = el.outerHTML;
|
|
89
97
|
return raw.length > max ? raw.slice(0, max) + '…' : raw;
|
|
90
|
-
}, SNIPPET_MAX_CHARS);
|
|
91
|
-
const box = await nth.boundingBox(
|
|
98
|
+
}, SNIPPET_MAX_CHARS, { timeout: CANDIDATE_READ_TIMEOUT_MILLIS });
|
|
99
|
+
const box = await nth.boundingBox({
|
|
100
|
+
timeout: CANDIDATE_READ_TIMEOUT_MILLIS,
|
|
101
|
+
});
|
|
92
102
|
const pos = box
|
|
93
103
|
? `(${Math.round(box.x)}, ${Math.round(box.y)})`
|
|
94
104
|
: '(unknown)';
|