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
|
@@ -36,6 +36,32 @@ async function screenOf(session) {
|
|
|
36
36
|
return null;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Emit an action result, attaching an inline screenshot when the session is in
|
|
41
|
+
* QA vision mode (2026-10).
|
|
42
|
+
*
|
|
43
|
+
* `browser_open({ qaVision: true })` makes EVERY interaction result carry the
|
|
44
|
+
* page image, so the model sees what its click/type/hover actually did instead
|
|
45
|
+
* of spending a second round-trip on `screenshot` after each step. Failure
|
|
46
|
+
* payloads get one too: the toast or validation error that explains a failure
|
|
47
|
+
* is exactly what the agent needs to see. The capture is BEST-EFFORT - a
|
|
48
|
+
* screenshot problem must never turn a successful action into a failed one.
|
|
49
|
+
*/
|
|
50
|
+
async function emitWithVision(emit, session, cmdId, payload) {
|
|
51
|
+
if (session?.connected && session.qaVision === true) {
|
|
52
|
+
try {
|
|
53
|
+
const shot = await session.screenshot('jpeg', 62);
|
|
54
|
+
if (shot) {
|
|
55
|
+
payload.screenshot = shot;
|
|
56
|
+
payload.format = 'jpeg';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* vision is best-effort */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
emit('orchestrator:browser-result', cmdId, payload);
|
|
64
|
+
}
|
|
39
65
|
/**
|
|
40
66
|
* The session an identity used most recently (2026-10).
|
|
41
67
|
*
|
|
@@ -122,21 +148,36 @@ async function handleBrowserCommand(data, emit) {
|
|
|
122
148
|
// OPT-IN credential entry (default false). A QA task signs in with
|
|
123
149
|
// supplied credentials; the auto-answer browser is a separate path.
|
|
124
150
|
const allowCredentials = data.allowCredentials === true;
|
|
151
|
+
// OPT-IN QA vision mode (default false, 2026-10): every mutating action
|
|
152
|
+
// returns a screenshot. Only the QA brief turns it on.
|
|
153
|
+
const qaVision = data.qaVision === true;
|
|
125
154
|
// REUSE only a LIVE session. A disconnected one may have lost its port
|
|
126
155
|
// and its profile to a concurrent browser while it was down, so it is
|
|
127
156
|
// re-allocated below instead of silently reconnecting onto them.
|
|
128
157
|
const existing = manager.get(sessionId);
|
|
129
158
|
if (existing?.connected) {
|
|
130
159
|
existing.setAllowCredentials(allowCredentials);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
160
|
+
existing.setQaVision(qaVision);
|
|
161
|
+
// reset:true drops a crashed run's leftovers (open modal, corrupted
|
|
162
|
+
// field values, stale validation errors) before the new run starts.
|
|
163
|
+
// A resumed run omits it and keeps its page.
|
|
164
|
+
let pageState;
|
|
165
|
+
if (data.url) {
|
|
166
|
+
pageState = await existing.navigate(String(data.url));
|
|
167
|
+
}
|
|
168
|
+
else if (data.reset === true) {
|
|
169
|
+
pageState = await existing.navigate('about:blank');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
pageState = await existing.pageInfo();
|
|
173
|
+
}
|
|
134
174
|
const screen = await screenOf(existing);
|
|
135
175
|
emit('orchestrator:browser-result', data.cmdId, {
|
|
136
176
|
success: true,
|
|
137
177
|
alreadyConnected: true,
|
|
138
178
|
sessionId,
|
|
139
179
|
port: existing.port,
|
|
180
|
+
qaVision: existing.qaVision,
|
|
140
181
|
...pageState,
|
|
141
182
|
...(screen ? { screen } : {}),
|
|
142
183
|
});
|
|
@@ -178,10 +219,12 @@ async function handleBrowserCommand(data, emit) {
|
|
|
178
219
|
headless,
|
|
179
220
|
profileDir: profile.dir,
|
|
180
221
|
allowCredentials,
|
|
222
|
+
qaVision,
|
|
181
223
|
});
|
|
182
224
|
// A reused session keeps its flag from a previous connect; honour the
|
|
183
225
|
// explicit value on every connect so the caller stays in control.
|
|
184
226
|
session.setAllowCredentials(allowCredentials);
|
|
227
|
+
session.setQaVision(qaVision);
|
|
185
228
|
const info = await session.connect(port);
|
|
186
229
|
const actualPort = session.port;
|
|
187
230
|
log.info(`Browser session "${sessionId}" on port ${actualPort} ` +
|
|
@@ -195,6 +238,7 @@ async function handleBrowserCommand(data, emit) {
|
|
|
195
238
|
sessionId,
|
|
196
239
|
port: actualPort,
|
|
197
240
|
profile: profile.shared ? 'shared' : 'isolated',
|
|
241
|
+
qaVision: session.qaVision,
|
|
198
242
|
...pageState,
|
|
199
243
|
...(screen ? { screen } : {}),
|
|
200
244
|
});
|
|
@@ -238,7 +282,7 @@ async function handleBrowserCommand(data, emit) {
|
|
|
238
282
|
}
|
|
239
283
|
const info = await session.navigate(url);
|
|
240
284
|
const screen = await screenOf(session);
|
|
241
|
-
emit
|
|
285
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
242
286
|
success: true,
|
|
243
287
|
...info,
|
|
244
288
|
...(screen ? { screen } : {}),
|
|
@@ -254,15 +298,92 @@ async function handleBrowserCommand(data, emit) {
|
|
|
254
298
|
});
|
|
255
299
|
return;
|
|
256
300
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
301
|
+
// Explicit pointer-sequence click (2026-10): the rescue for an
|
|
302
|
+
// activator that ignores a normal click. ASKED FOR, never automatic.
|
|
303
|
+
if (data.pointer === true) {
|
|
304
|
+
if (typeof data.ref !== 'string' || !data.ref) {
|
|
305
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
306
|
+
success: false,
|
|
307
|
+
error: 'POINTER_CLICK_REF_REQUIRED: pointer:true needs a ref (getRefs / snapshot refs:true).',
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const pt = await session.clickPointerRef(data.ref);
|
|
313
|
+
const screen = await screenOf(session);
|
|
314
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
315
|
+
success: true,
|
|
316
|
+
clicked: true,
|
|
317
|
+
pointer: true,
|
|
318
|
+
ref: data.ref,
|
|
319
|
+
x: pt.x,
|
|
320
|
+
y: pt.y,
|
|
321
|
+
...(screen ? { screen } : {}),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
const screen = await screenOf(session);
|
|
326
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
327
|
+
success: false,
|
|
328
|
+
error: err instanceof Error ? err.message : String(err),
|
|
329
|
+
...(screen ? { screen } : {}),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
// Coordinates are CSS pixels (getElements/snapshot) — never screenshot
|
|
335
|
+
// pixels. Prefer elementIndex/selector: resolved bridge-side against a
|
|
336
|
+
// fresh DOM read, so a stale or scaled coordinate cannot misfire.
|
|
337
|
+
// Out-of-viewport coordinates are refused with CLICK_OUT_OF_VIEWPORT.
|
|
338
|
+
let clicked;
|
|
339
|
+
if (typeof data.ref === 'string' ||
|
|
340
|
+
typeof data.elementIndex === 'number' ||
|
|
341
|
+
typeof data.selector === 'string') {
|
|
342
|
+
try {
|
|
343
|
+
clicked = await session.clickRef({
|
|
344
|
+
...(typeof data.ref === 'string' ? { ref: data.ref } : {}),
|
|
345
|
+
...(typeof data.elementIndex === 'number'
|
|
346
|
+
? { elementIndex: data.elementIndex }
|
|
347
|
+
: {}),
|
|
348
|
+
...(typeof data.selector === 'string'
|
|
349
|
+
? { selector: data.selector }
|
|
350
|
+
: {}),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
const screen = await screenOf(session);
|
|
355
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
356
|
+
success: false,
|
|
357
|
+
error: err instanceof Error ? err.message : String(err),
|
|
358
|
+
...(screen ? { screen } : {}),
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
const x = typeof data.x === 'number' ? data.x : 0;
|
|
365
|
+
const y = typeof data.y === 'number' ? data.y : 0;
|
|
366
|
+
try {
|
|
367
|
+
await session.click(x, y);
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
const screen = await screenOf(session);
|
|
371
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
372
|
+
success: false,
|
|
373
|
+
error: err instanceof Error ? err.message : String(err),
|
|
374
|
+
...(screen ? { screen } : {}),
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
clicked = { x, y };
|
|
379
|
+
}
|
|
260
380
|
const screen = await screenOf(session);
|
|
261
|
-
emit
|
|
381
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
262
382
|
success: true,
|
|
263
383
|
clicked: true,
|
|
264
|
-
|
|
265
|
-
|
|
384
|
+
...(typeof data.ref === 'string' ? { ref: data.ref } : {}),
|
|
385
|
+
x: clicked.x,
|
|
386
|
+
y: clicked.y,
|
|
266
387
|
...(screen ? { screen } : {}),
|
|
267
388
|
});
|
|
268
389
|
break;
|
|
@@ -281,14 +402,53 @@ async function handleBrowserCommand(data, emit) {
|
|
|
281
402
|
if (data.allowCredentials === true)
|
|
282
403
|
session.setAllowCredentials(true);
|
|
283
404
|
const text = String(data.text ?? '');
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
405
|
+
// With a reference the field is clicked first (same as the batch
|
|
406
|
+
// type step), so one call fills a form field without a prior click.
|
|
407
|
+
if (typeof data.ref === 'string' ||
|
|
408
|
+
typeof data.elementIndex === 'number' ||
|
|
409
|
+
typeof data.selector === 'string') {
|
|
410
|
+
try {
|
|
411
|
+
await session.clickRef({
|
|
412
|
+
...(typeof data.ref === 'string' ? { ref: data.ref } : {}),
|
|
413
|
+
...(typeof data.elementIndex === 'number'
|
|
414
|
+
? { elementIndex: data.elementIndex }
|
|
415
|
+
: {}),
|
|
416
|
+
...(typeof data.selector === 'string'
|
|
417
|
+
? { selector: data.selector }
|
|
418
|
+
: {}),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
423
|
+
success: false,
|
|
424
|
+
error: err instanceof Error ? err.message : String(err),
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Verified typing: TYPE_NOT_VERIFIED (with the observed field state)
|
|
430
|
+
// when the insert does not land — never a silent typed:true.
|
|
431
|
+
try {
|
|
432
|
+
const observed = await session.type(text, {
|
|
433
|
+
clearFirst: data.clearFirst === true,
|
|
434
|
+
});
|
|
435
|
+
const screen = await screenOf(session);
|
|
436
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
437
|
+
success: true,
|
|
438
|
+
typed: true,
|
|
439
|
+
length: text.length,
|
|
440
|
+
observed,
|
|
441
|
+
...(screen ? { screen } : {}),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
const screen = await screenOf(session);
|
|
446
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
447
|
+
success: false,
|
|
448
|
+
error: err instanceof Error ? err.message : String(err),
|
|
449
|
+
...(screen ? { screen } : {}),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
292
452
|
break;
|
|
293
453
|
}
|
|
294
454
|
case 'login': {
|
|
@@ -320,7 +480,7 @@ async function handleBrowserCommand(data, emit) {
|
|
|
320
480
|
});
|
|
321
481
|
// Never echo the password: the payload carries booleans + an error code.
|
|
322
482
|
const screen = await screenOf(session);
|
|
323
|
-
emit
|
|
483
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
324
484
|
success: true,
|
|
325
485
|
...result,
|
|
326
486
|
...(screen ? { screen } : {}),
|
|
@@ -339,13 +499,185 @@ async function handleBrowserCommand(data, emit) {
|
|
|
339
499
|
const key = String(data.key ?? '');
|
|
340
500
|
await session.press(key);
|
|
341
501
|
const screen = await screenOf(session);
|
|
342
|
-
emit
|
|
502
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
343
503
|
success: true,
|
|
344
504
|
pressed: key,
|
|
345
505
|
...(screen ? { screen } : {}),
|
|
346
506
|
});
|
|
347
507
|
break;
|
|
348
508
|
}
|
|
509
|
+
// 2026-10: pointer hover without a click. Radix menus/tooltips open on
|
|
510
|
+
// hover, and a click on a hover-only trigger dismisses them again - this
|
|
511
|
+
// is the action that makes those reachable at all.
|
|
512
|
+
case 'hover': {
|
|
513
|
+
const session = manager.get(sessionId);
|
|
514
|
+
if (!session?.connected) {
|
|
515
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
516
|
+
success: false,
|
|
517
|
+
error: 'No active browser session.',
|
|
518
|
+
});
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
if (typeof data.ref === 'string' ||
|
|
523
|
+
typeof data.selector === 'string') {
|
|
524
|
+
const pt = await session.hoverRef({
|
|
525
|
+
...(typeof data.ref === 'string' ? { ref: data.ref } : {}),
|
|
526
|
+
...(typeof data.selector === 'string'
|
|
527
|
+
? { selector: data.selector }
|
|
528
|
+
: {}),
|
|
529
|
+
});
|
|
530
|
+
const screen = await screenOf(session);
|
|
531
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
532
|
+
success: true,
|
|
533
|
+
hovered: true,
|
|
534
|
+
...(typeof data.ref === 'string' ? { ref: data.ref } : {}),
|
|
535
|
+
x: pt.x,
|
|
536
|
+
y: pt.y,
|
|
537
|
+
...(screen ? { screen } : {}),
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
const x = typeof data.x === 'number' ? data.x : 0;
|
|
542
|
+
const y = typeof data.y === 'number' ? data.y : 0;
|
|
543
|
+
await session.hover(x, y);
|
|
544
|
+
const screen = await screenOf(session);
|
|
545
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
546
|
+
success: true,
|
|
547
|
+
hovered: true,
|
|
548
|
+
x,
|
|
549
|
+
y,
|
|
550
|
+
...(screen ? { screen } : {}),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
const screen = await screenOf(session);
|
|
556
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
557
|
+
success: false,
|
|
558
|
+
error: err instanceof Error ? err.message : String(err),
|
|
559
|
+
...(screen ? { screen } : {}),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
// 2026-10: focus a control by ref. Reports whether focus actually moved;
|
|
565
|
+
// a non-focusable div says ok:false instead of pretending.
|
|
566
|
+
case 'focus': {
|
|
567
|
+
const session = manager.get(sessionId);
|
|
568
|
+
if (!session?.connected) {
|
|
569
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
570
|
+
success: false,
|
|
571
|
+
error: 'No active browser session.',
|
|
572
|
+
});
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
if (typeof data.ref === 'string' && data.ref) {
|
|
577
|
+
const result = await session.focusRef(data.ref);
|
|
578
|
+
const screen = await screenOf(session);
|
|
579
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
580
|
+
success: result.ok === true,
|
|
581
|
+
...result,
|
|
582
|
+
...(result.ok
|
|
583
|
+
? {}
|
|
584
|
+
: {
|
|
585
|
+
error: `FOCUS_NOT_TAKEN: ${data.ref} did not take focus (it may not be focusable).`,
|
|
586
|
+
}),
|
|
587
|
+
...(screen ? { screen } : {}),
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
else if (typeof data.selector === 'string' && data.selector) {
|
|
591
|
+
const ok = await session.focus(data.selector);
|
|
592
|
+
const screen = await screenOf(session);
|
|
593
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
594
|
+
success: ok,
|
|
595
|
+
focused: ok,
|
|
596
|
+
...(ok
|
|
597
|
+
? {}
|
|
598
|
+
: {
|
|
599
|
+
error: 'FOCUS_NOT_TAKEN: the selector matched no visible element, or it cannot take focus.',
|
|
600
|
+
}),
|
|
601
|
+
...(screen ? { screen } : {}),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
606
|
+
success: false,
|
|
607
|
+
error: 'focus requires ref or selector.',
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
const screen = await screenOf(session);
|
|
613
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
614
|
+
success: false,
|
|
615
|
+
error: err instanceof Error ? err.message : String(err),
|
|
616
|
+
...(screen ? { screen } : {}),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
// 2026-10: dismiss whatever menu/popover layer is open.
|
|
622
|
+
case 'closePopup': {
|
|
623
|
+
const session = manager.get(sessionId);
|
|
624
|
+
if (!session?.connected) {
|
|
625
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
626
|
+
success: false,
|
|
627
|
+
error: 'No active browser session.',
|
|
628
|
+
});
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const result = await session.closePopup();
|
|
632
|
+
const screen = await screenOf(session);
|
|
633
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
634
|
+
success: true,
|
|
635
|
+
...result,
|
|
636
|
+
...(screen ? { screen } : {}),
|
|
637
|
+
});
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
// 2026-10: the interactive-element ref list (`e1 … eN`), including
|
|
641
|
+
// portalled menus/popovers/dialogs. Actions accept `ref`.
|
|
642
|
+
case 'getRefs': {
|
|
643
|
+
const session = manager.get(sessionId);
|
|
644
|
+
if (!session?.connected) {
|
|
645
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
646
|
+
success: false,
|
|
647
|
+
error: 'No active browser session. Call browser_open first.',
|
|
648
|
+
});
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const refs = await session.getRefs();
|
|
652
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
653
|
+
success: true,
|
|
654
|
+
...refs,
|
|
655
|
+
});
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
// 2026-11: clear cookies/storage between test accounts. It existed on the
|
|
659
|
+
// session and in the batch path, but the SINGLE-action switch had no case,
|
|
660
|
+
// so browser_action({action:"clearSession"}) answered "Unknown browser
|
|
661
|
+
// action" - a silent dead end for invitation flows.
|
|
662
|
+
case 'clearSession': {
|
|
663
|
+
const session = manager.get(sessionId);
|
|
664
|
+
if (!session?.connected) {
|
|
665
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
666
|
+
success: false,
|
|
667
|
+
error: 'No active browser session. Call browser_open first.',
|
|
668
|
+
});
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const result = await session.clearSession();
|
|
672
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
673
|
+
success: result.ok === true,
|
|
674
|
+
cleared: result.ok === true,
|
|
675
|
+
...(result.ok
|
|
676
|
+
? {}
|
|
677
|
+
: { error: result.error ?? 'the session could not be cleared' }),
|
|
678
|
+
});
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
349
681
|
case 'scroll': {
|
|
350
682
|
const session = manager.get(sessionId);
|
|
351
683
|
if (!session?.connected) {
|
|
@@ -357,7 +689,7 @@ async function handleBrowserCommand(data, emit) {
|
|
|
357
689
|
}
|
|
358
690
|
const delta = typeof data.delta === 'number' ? data.delta : 3;
|
|
359
691
|
await session.scroll(delta);
|
|
360
|
-
emit
|
|
692
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
361
693
|
success: true,
|
|
362
694
|
scrolled: true,
|
|
363
695
|
delta,
|
|
@@ -431,15 +763,24 @@ async function handleBrowserCommand(data, emit) {
|
|
|
431
763
|
...(typeof data.maxChars === 'number'
|
|
432
764
|
? { maxTextChars: data.maxChars }
|
|
433
765
|
: {}),
|
|
766
|
+
...(data.refs === true ? { refs: true } : {}),
|
|
434
767
|
});
|
|
435
768
|
const payload = {
|
|
436
769
|
success: true,
|
|
437
770
|
...state,
|
|
438
771
|
};
|
|
439
|
-
|
|
772
|
+
const wantsShot = data.screenshot === true ||
|
|
773
|
+
(session.qaVision && data.screenshot !== false);
|
|
774
|
+
if (wantsShot && !session.nativeDialog) {
|
|
440
775
|
const format = data.format === 'png' ? 'png' : 'jpeg';
|
|
441
776
|
const quality = typeof data.quality === 'number' ? data.quality : 70;
|
|
442
|
-
|
|
777
|
+
if (data.annotate === true) {
|
|
778
|
+
payload.screenshot = await session.annotatedScreenshot(format, quality);
|
|
779
|
+
payload.annotated = true;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
payload.screenshot = await session.screenshot(format, quality);
|
|
783
|
+
}
|
|
443
784
|
payload.format = format;
|
|
444
785
|
}
|
|
445
786
|
emit('orchestrator:browser-result', data.cmdId, payload);
|
|
@@ -462,6 +803,7 @@ async function handleBrowserCommand(data, emit) {
|
|
|
462
803
|
'selector',
|
|
463
804
|
'text',
|
|
464
805
|
'dialog',
|
|
806
|
+
'popover',
|
|
465
807
|
'toast',
|
|
466
808
|
'url',
|
|
467
809
|
'networkIdle',
|
|
@@ -543,13 +885,113 @@ async function handleBrowserCommand(data, emit) {
|
|
|
543
885
|
: {}),
|
|
544
886
|
});
|
|
545
887
|
const screen = await screenOf(session);
|
|
546
|
-
emit
|
|
888
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
889
|
+
success: result.ok,
|
|
890
|
+
...result,
|
|
891
|
+
...(screen ? { screen } : {}),
|
|
892
|
+
});
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
// 2026-10: pagination. `nextPage`/`prevPage` click the real control and
|
|
896
|
+
// wait for the page signature to change; `collectTable` walks every page
|
|
897
|
+
// and returns deduplicated rows in one call.
|
|
898
|
+
case 'nextPage':
|
|
899
|
+
case 'prevPage': {
|
|
900
|
+
const session = manager.get(sessionId);
|
|
901
|
+
if (!session?.connected) {
|
|
902
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
903
|
+
success: false,
|
|
904
|
+
error: 'No active browser session. Call browser_open first.',
|
|
905
|
+
});
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const timeout = typeof data.timeoutMs === 'number'
|
|
909
|
+
? data.timeoutMs
|
|
910
|
+
: typeof data.stepTimeoutMs === 'number'
|
|
911
|
+
? data.stepTimeoutMs
|
|
912
|
+
: 8000;
|
|
913
|
+
const result = action === 'nextPage'
|
|
914
|
+
? await session.nextPage(timeout)
|
|
915
|
+
: await session.prevPage(timeout);
|
|
916
|
+
const screen = await screenOf(session);
|
|
917
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
918
|
+
success: true,
|
|
919
|
+
...result,
|
|
920
|
+
...(screen ? { screen } : {}),
|
|
921
|
+
});
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
case 'gotoPage': {
|
|
925
|
+
const session = manager.get(sessionId);
|
|
926
|
+
if (!session?.connected) {
|
|
927
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
928
|
+
success: false,
|
|
929
|
+
error: 'No active browser session. Call browser_open first.',
|
|
930
|
+
});
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (typeof data.page !== 'number') {
|
|
934
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
935
|
+
success: false,
|
|
936
|
+
error: 'page parameter required for gotoPage',
|
|
937
|
+
});
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const result = await session.gotoPage(data.page, typeof data.timeoutMs === 'number' ? data.timeoutMs : undefined);
|
|
941
|
+
const screen = await screenOf(session);
|
|
942
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
943
|
+
success: true,
|
|
944
|
+
...result,
|
|
945
|
+
...(screen ? { screen } : {}),
|
|
946
|
+
});
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
case 'setPageSize': {
|
|
950
|
+
const session = manager.get(sessionId);
|
|
951
|
+
if (!session?.connected) {
|
|
952
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
953
|
+
success: false,
|
|
954
|
+
error: 'No active browser session. Call browser_open first.',
|
|
955
|
+
});
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (typeof data.size !== 'number') {
|
|
959
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
960
|
+
success: false,
|
|
961
|
+
error: 'size parameter required for setPageSize',
|
|
962
|
+
});
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const result = await session.setPageSize(data.size);
|
|
966
|
+
const screen = await screenOf(session);
|
|
967
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
547
968
|
success: result.ok,
|
|
548
969
|
...result,
|
|
549
970
|
...(screen ? { screen } : {}),
|
|
550
971
|
});
|
|
551
972
|
break;
|
|
552
973
|
}
|
|
974
|
+
case 'collectTable': {
|
|
975
|
+
const session = manager.get(sessionId);
|
|
976
|
+
if (!session?.connected) {
|
|
977
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
978
|
+
success: false,
|
|
979
|
+
error: 'No active browser session. Call browser_open first.',
|
|
980
|
+
});
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const result = await session.collectTable({
|
|
984
|
+
selector: typeof data.selector === 'string' ? data.selector : undefined,
|
|
985
|
+
maxPages: typeof data.maxPages === 'number' ? data.maxPages : undefined,
|
|
986
|
+
maxRows: typeof data.maxRows === 'number' ? data.maxRows : undefined,
|
|
987
|
+
pageSize: typeof data.size === 'number' ? data.size : undefined,
|
|
988
|
+
});
|
|
989
|
+
emit('orchestrator:browser-result', data.cmdId, {
|
|
990
|
+
success: true,
|
|
991
|
+
...result,
|
|
992
|
+
});
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
553
995
|
case 'networkLog': {
|
|
554
996
|
const session = manager.get(sessionId);
|
|
555
997
|
if (!session?.connected) {
|
|
@@ -831,7 +1273,7 @@ async function handleBrowserCommand(data, emit) {
|
|
|
831
1273
|
return;
|
|
832
1274
|
}
|
|
833
1275
|
const result = await session.submit(typeof data.selector === 'string' ? data.selector : undefined);
|
|
834
|
-
emit
|
|
1276
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
835
1277
|
success: result?.ok === true,
|
|
836
1278
|
submitted: result?.ok === true,
|
|
837
1279
|
...(result?.ok
|
|
@@ -856,7 +1298,7 @@ async function handleBrowserCommand(data, emit) {
|
|
|
856
1298
|
const steps = data.actions;
|
|
857
1299
|
const summary = await (0, batch_1.executeBatch)(session, steps ?? []);
|
|
858
1300
|
const screen = await screenOf(session);
|
|
859
|
-
emit
|
|
1301
|
+
await emitWithVision(emit, session, data.cmdId, {
|
|
860
1302
|
success: true,
|
|
861
1303
|
batch: true,
|
|
862
1304
|
...summary,
|
|
@@ -901,7 +1343,10 @@ async function handleBrowserCommand(data, emit) {
|
|
|
901
1343
|
catch (err) {
|
|
902
1344
|
const message = err instanceof Error ? err.message : String(err);
|
|
903
1345
|
log.info(`Browser command FAILED (${action}): ${message}`);
|
|
904
|
-
|
|
1346
|
+
// In QA vision mode a thrown failure still shows the page: the error text
|
|
1347
|
+
// alone ("TYPE_NOT_VERIFIED", "SELECTOR_NOT_FOUND") leaves the model
|
|
1348
|
+
// guessing what the screen actually looked like.
|
|
1349
|
+
await emitWithVision(emit, manager.get(sessionId), data.cmdId, {
|
|
905
1350
|
success: false,
|
|
906
1351
|
error: message,
|
|
907
1352
|
});
|