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.
Files changed (48) hide show
  1. package/dist/cli/commands/research.js +10 -2
  2. package/dist/cli/commands/research.js.map +1 -1
  3. package/dist/cli/commands/run.js +0 -21
  4. package/dist/cli/commands/run.js.map +1 -1
  5. package/dist/lib/browser/batch.d.ts +77 -1
  6. package/dist/lib/browser/batch.js +151 -3
  7. package/dist/lib/browser/batch.js.map +1 -1
  8. package/dist/lib/browser/cdp.d.ts +61 -2
  9. package/dist/lib/browser/cdp.js +235 -14
  10. package/dist/lib/browser/cdp.js.map +1 -1
  11. package/dist/lib/browser/index.d.ts +2 -2
  12. package/dist/lib/browser/index.js +4 -1
  13. package/dist/lib/browser/index.js.map +1 -1
  14. package/dist/lib/browser/login.d.ts +20 -2
  15. package/dist/lib/browser/login.js +125 -88
  16. package/dist/lib/browser/login.js.map +1 -1
  17. package/dist/lib/browser/network-log.d.ts +31 -0
  18. package/dist/lib/browser/network-log.js +94 -12
  19. package/dist/lib/browser/network-log.js.map +1 -1
  20. package/dist/lib/browser/page-context.d.ts +62 -1
  21. package/dist/lib/browser/page-context.js +284 -1
  22. package/dist/lib/browser/page-context.js.map +1 -1
  23. package/dist/lib/browser/refs.d.ts +104 -0
  24. package/dist/lib/browser/refs.js +430 -0
  25. package/dist/lib/browser/refs.js.map +1 -0
  26. package/dist/lib/browser/session.d.ts +188 -7
  27. package/dist/lib/browser/session.js +537 -8
  28. package/dist/lib/browser/session.js.map +1 -1
  29. package/dist/lib/git/git-finalize.js +17 -2
  30. package/dist/lib/git/git-finalize.js.map +1 -1
  31. package/dist/lib/harness/harness-adapter.d.ts +2 -5
  32. package/dist/lib/harness/harness-adapter.js.map +1 -1
  33. package/dist/lib/harness/opencode-control.d.ts +73 -0
  34. package/dist/lib/harness/opencode-control.js +318 -0
  35. package/dist/lib/harness/opencode-control.js.map +1 -1
  36. package/dist/lib/harness/opencode-outcome.d.ts +16 -2
  37. package/dist/lib/harness/opencode-outcome.js +36 -7
  38. package/dist/lib/harness/opencode-outcome.js.map +1 -1
  39. package/dist/lib/harness/spawn.js +112 -48
  40. package/dist/lib/harness/spawn.js.map +1 -1
  41. package/dist/lib/orchestrator/handlers/browser.js +472 -27
  42. package/dist/lib/orchestrator/handlers/browser.js.map +1 -1
  43. package/dist/lib/tools/browser-tool.js +24 -3
  44. package/dist/lib/tools/browser-tool.js.map +1 -1
  45. package/dist/lib/workspace/repo-lock.d.ts +5 -1
  46. package/dist/lib/workspace/repo-lock.js +5 -3
  47. package/dist/lib/workspace/repo-lock.js.map +1 -1
  48. 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
- const pageState = data.url
132
- ? await existing.navigate(String(data.url))
133
- : await existing.pageInfo();
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('orchestrator:browser-result', data.cmdId, {
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
- const x = typeof data.x === 'number' ? data.x : 0;
258
- const y = typeof data.y === 'number' ? data.y : 0;
259
- await session.click(x, y);
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('orchestrator:browser-result', data.cmdId, {
381
+ await emitWithVision(emit, session, data.cmdId, {
262
382
  success: true,
263
383
  clicked: true,
264
- x,
265
- y,
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
- await session.type(text, { clearFirst: data.clearFirst === true });
285
- const screen = await screenOf(session);
286
- emit('orchestrator:browser-result', data.cmdId, {
287
- success: true,
288
- typed: true,
289
- length: text.length,
290
- ...(screen ? { screen } : {}),
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('orchestrator:browser-result', data.cmdId, {
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('orchestrator:browser-result', data.cmdId, {
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('orchestrator:browser-result', data.cmdId, {
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
- if (data.screenshot === true && !session.nativeDialog) {
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
- payload.screenshot = await session.screenshot(format, quality);
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('orchestrator:browser-result', data.cmdId, {
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('orchestrator:browser-result', data.cmdId, {
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('orchestrator:browser-result', data.cmdId, {
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
- emit('orchestrator:browser-result', data.cmdId, {
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
  });