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.
@@ -166,7 +166,7 @@ async function extendPage(page, options) {
166
166
  aiInvocations: [],
167
167
  };
168
168
  const showMouse = async (p) => {
169
- if (interactionVisualizer.defaultMessageDurationMillis > 0) {
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
- // When set, Playwright only records on retries; if a video exists
139
- // it implies we're on a retry always retain.
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.defaultMessageDurationMillis,
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 defaultMessageDurationMillis: number;
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
- * Moves the virtual cursor to the center of the specified element and optionally displays a message.
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 message - Optional message to display near the cursor during the interaction.
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 when the cursor movement and message display are complete
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
- * - Messages are positioned automatically to avoid viewport edges
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'>, message?: string, duration?: number): Promise<void>;
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 ensureContainer;
56
- private ensureCursor;
57
- private moveCursor;
58
- private showMessage;
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(defaultMessageDurationMillis) {
8
- this.defaultMessageDurationMillis = defaultMessageDurationMillis;
9
- this.cursorPos = { x: 150, y: 150 };
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 and optionally displays a message.
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 message - Optional message to display near the cursor during the interaction.
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 when the cursor movement and message display are complete
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
- * - Messages are positioned automatically to avoid viewport edges
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, message, duration = this.defaultMessageDurationMillis) {
32
+ async pointAt(page, locator, duration = this.defaultCursorDurationMillis) {
32
33
  if (!duration || duration <= 0) {
33
34
  return;
34
35
  }
35
36
  try {
36
- let target = { x: this.cursorPos.x, y: this.cursorPos.y };
37
+ const state = this.getState(page);
38
+ const from = { ...state.pos };
39
+ let target = from;
37
40
  if (locator) {
38
- const box = await locator.boundingBox();
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
- await this.ensureContainer(page);
44
- await Promise.all([
45
- this.moveCursor(page, target, duration / 2),
46
- message?.trim()
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
- await this.ensureContainer(page);
71
- await this.ensureCursor(page);
72
- await page.evaluate(([containerId]) => {
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 id = InteractionVisualizer.CONTAINER_ID;
101
- await page.evaluate(([containerId]) => {
102
- const container = document.getElementById(containerId);
103
- if (!container?.shadowRoot) {
104
- return;
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
- /* Private helpers */
120
- /* ------------------------------------------------------------------ */
121
- async ensureContainer(page) {
122
- const id = InteractionVisualizer.CONTAINER_ID;
123
- await page.evaluate(([containerId, css]) => {
124
- if (document.getElementById(containerId)) {
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
- async ensureCursor(page) {
147
- const id = InteractionVisualizer.CONTAINER_ID;
148
- await page.evaluate(([containerId, position, svg, svgAttrs, tipX, tipY]) => {
149
- const root = document.getElementById(containerId).shadowRoot;
150
- let cursor = root.querySelector('.donobu-virtual-mouse');
151
- if (cursor) {
152
- return;
153
- }
154
- cursor = document.createElement('div');
155
- cursor.className = 'donobu-virtual-mouse hidden';
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
- cursor.insertAdjacentHTML('beforeend', svg);
123
+ await previous.dispose();
159
124
  }
160
- catch (_error) {
161
- // Fallback: create SVG programmatically using attributes object
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
- const pos = position;
177
- cursor.style.transitionDuration = '0s';
178
- cursor.style.transform = `translate(${pos.x - tipX}px, ${pos.y - tipY}px)`;
179
- root.appendChild(cursor);
180
- }, [
181
- id,
182
- this.cursorPos,
183
- InteractionVisualizer.SVG_MOUSE,
184
- JSON.stringify(InteractionVisualizer.SVG_MOUSE_JSON),
185
- InteractionVisualizer.TIP_X,
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
- return done;
209
- }, [
210
- id,
211
- target,
212
- duration,
213
- InteractionVisualizer.TIP_X,
214
- InteractionVisualizer.TIP_Y,
215
- ]);
216
- this.cursorPos = target;
139
+ }
217
140
  }
218
- async showMessage(page, target, text, duration) {
219
- const id = InteractionVisualizer.CONTAINER_ID;
220
- await page.evaluate(([containerId, target, text, duration]) => {
221
- containerId = containerId;
222
- target = target;
223
- text = text;
224
- duration = duration;
225
- const root = document.getElementById(containerId)
226
- .shadowRoot;
227
- const msg = document.createElement('div');
228
- msg.className = 'donobu-message';
229
- msg.textContent = text;
230
- root.appendChild(msg);
231
- /* ── compute final coordinates ────────────────────────────────── */
232
- const { width, height } = msg.getBoundingClientRect();
233
- let x = target.x - width / 2;
234
- let y = target.y + 24; // default - below cursor
235
- /* clamp horizontally inside viewport */
236
- x = Math.max(8, Math.min(window.innerWidth - width - 8, x));
237
- /* if it overflows bottom, flip above the element */
238
- if (y + height + 8 > window.innerHeight) {
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
- InteractionVisualizer.SVG_MOUSE_JSON = {
273
- svg: {
274
- xmlns: 'http://www.w3.org/2000/svg',
275
- width: '32',
276
- height: '32',
277
- viewBox: '0 0 24 24',
278
- fill: 'oklch(13.09% 0.005 165.18)',
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(), undefined, ReplayableInteraction.PREVIEW_CURSOR_DURATION_MILLIS);
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
- // Extract sessionStorage from the page
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "donobu",
3
- "version": "5.60.4",
3
+ "version": "5.60.6",
4
4
  "description": "Create browser automations with an LLM agent and replay them as Playwright scripts.",
5
5
  "main": "dist/main.js",
6
6
  "module": "dist/esm/main.js",
@@ -46,7 +46,7 @@
46
46
  "license": "UNLICENSED",
47
47
  "devDependencies": {
48
48
  "@eslint/js": "^10.0.1",
49
- "@playwright/test": "^1.60.0",
49
+ "@playwright/test": "^1.61.0",
50
50
  "@types/better-sqlite3": "^7.6.13",
51
51
  "@types/express": "^5.0.6",
52
52
  "@types/node": "^22.10.5",
@@ -59,8 +59,8 @@
59
59
  "eslint-plugin-perfectionist": "^5.9.0",
60
60
  "eslint-plugin-simple-import-sort": "^12.1.1",
61
61
  "globals": "^16.0.0",
62
- "playwright": "^1.60.0",
63
- "playwright-core": "^1.60.0",
62
+ "playwright": "^1.61.0",
63
+ "playwright-core": "^1.61.0",
64
64
  "typescript-eslint": "^8.47.0",
65
65
  "vitest": "^4.0.17",
66
66
  "winston-transport": "^4.9.0"
@@ -86,9 +86,9 @@
86
86
  "zod": "^4.3.5"
87
87
  },
88
88
  "peerDependencies": {
89
- "@playwright/test": ">=1.40.0",
90
- "playwright": ">=1.40.0",
91
- "playwright-core": ">=1.40.0"
89
+ "@playwright/test": ">=1.61.0",
90
+ "playwright": ">=1.61.0",
91
+ "playwright-core": ">=1.61.0"
92
92
  },
93
93
  "peerDependenciesMeta": {
94
94
  "playwright": {