@dyyz1993/agent-browser 0.9.2 → 0.11.1

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 (116) hide show
  1. package/dist/__tests__/utils/parseCli.d.ts +1 -0
  2. package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
  3. package/dist/__tests__/utils/parseCli.js +18 -10
  4. package/dist/__tests__/utils/parseCli.js.map +1 -1
  5. package/dist/actions.d.ts.map +1 -1
  6. package/dist/actions.js +63 -3
  7. package/dist/actions.js.map +1 -1
  8. package/dist/browser.d.ts +46 -2
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +343 -13
  11. package/dist/browser.js.map +1 -1
  12. package/dist/cli/commands.d.ts.map +1 -1
  13. package/dist/cli/commands.js +8 -3
  14. package/dist/cli/commands.js.map +1 -1
  15. package/dist/cli/connection.d.ts.map +1 -1
  16. package/dist/cli/connection.js +39 -1
  17. package/dist/cli/connection.js.map +1 -1
  18. package/dist/cli/help.d.ts.map +1 -1
  19. package/dist/cli/help.js +27 -20
  20. package/dist/cli/help.js.map +1 -1
  21. package/dist/cli/output.d.ts.map +1 -1
  22. package/dist/cli/output.js +5 -0
  23. package/dist/cli/output.js.map +1 -1
  24. package/dist/cli.js +20 -0
  25. package/dist/cli.js.map +1 -1
  26. package/dist/daemon.d.ts.map +1 -1
  27. package/dist/daemon.js +147 -1
  28. package/dist/daemon.js.map +1 -1
  29. package/dist/message-bridge.d.ts.map +1 -1
  30. package/dist/message-bridge.js +22 -4
  31. package/dist/message-bridge.js.map +1 -1
  32. package/dist/openapi.d.ts +22 -0
  33. package/dist/openapi.d.ts.map +1 -0
  34. package/dist/openapi.js +382 -0
  35. package/dist/openapi.js.map +1 -0
  36. package/dist/protocol.d.ts.map +1 -1
  37. package/dist/protocol.js +18 -0
  38. package/dist/protocol.js.map +1 -1
  39. package/dist/recorder/inject.js +61 -134
  40. package/dist/stream-server-standalone.d.ts +10 -0
  41. package/dist/stream-server-standalone.d.ts.map +1 -1
  42. package/dist/stream-server-standalone.js +594 -74
  43. package/dist/stream-server-standalone.js.map +1 -1
  44. package/dist/stream-server.d.ts +67 -2
  45. package/dist/stream-server.d.ts.map +1 -1
  46. package/dist/stream-server.js +371 -51
  47. package/dist/stream-server.js.map +1 -1
  48. package/dist/swagger-ui.d.ts +6 -0
  49. package/dist/swagger-ui.d.ts.map +1 -0
  50. package/dist/swagger-ui.js +51 -0
  51. package/dist/swagger-ui.js.map +1 -0
  52. package/dist/test-live.d.ts +2 -0
  53. package/dist/test-live.d.ts.map +1 -0
  54. package/dist/test-live.js +333 -0
  55. package/dist/test-live.js.map +1 -0
  56. package/dist/types.d.ts +7 -1
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/types.js.map +1 -1
  59. package/dist/viewer-html.d.ts.map +1 -1
  60. package/dist/viewer-html.js +270 -58
  61. package/dist/viewer-html.js.map +1 -1
  62. package/dist/viewer-script.d.ts +20 -2
  63. package/dist/viewer-script.d.ts.map +1 -1
  64. package/dist/viewer-script.js +911 -154
  65. package/dist/viewer-script.js.map +1 -1
  66. package/package.json +1 -1
  67. package/scripts/postinstall.js +6 -32
  68. package/scripts/test-cli-help.sh +51 -0
  69. package/scripts/verify-form.sh +67 -0
  70. package/scripts/verify-login.sh +65 -0
  71. package/scripts/verify-recording.sh +80 -0
  72. package/scripts/verify-upload.sh +41 -0
  73. package/skills/agent-browser/SKILL.md +297 -160
  74. package/skills/agent-browser/references/commands.md +3 -0
  75. package/skills/agent-browser/references/mobile-viewer.md +188 -0
  76. package/skills/agent-browser/references/network-monitoring.md +232 -0
  77. package/skills/agent-browser/references/recorder.md +319 -0
  78. package/skills/agent-browser/references/viewer-mode.md +148 -0
  79. package/skills/agent-browser/templates/api-interception.sh +3 -1
  80. package/skills/agent-browser/templates/data-extraction.sh +8 -4
  81. package/skills/agent-browser/templates/form-automation.sh +18 -23
  82. package/skills/agent-browser/templates/network-intercept-crawl.sh +256 -0
  83. package/skills/agent-browser/templates/recorder-workflow.sh +51 -0
  84. package/skills/agent-browser/templates/viewer-remote.sh +41 -0
  85. package/dist/__tests__/test-iframe.d.ts +0 -2
  86. package/dist/__tests__/test-iframe.d.ts.map +0 -1
  87. package/dist/__tests__/test-iframe.js +0 -52
  88. package/dist/__tests__/test-iframe.js.map +0 -1
  89. package/dist/cli-new.d.ts +0 -3
  90. package/dist/cli-new.d.ts.map +0 -1
  91. package/dist/cli-new.js +0 -308
  92. package/dist/cli-new.js.map +0 -1
  93. package/dist/cli-old.d.ts +0 -3
  94. package/dist/cli-old.d.ts.map +0 -1
  95. package/dist/cli-old.js +0 -1101
  96. package/dist/cli-old.js.map +0 -1
  97. package/dist/recorder/binding.d.ts +0 -24
  98. package/dist/recorder/binding.d.ts.map +0 -1
  99. package/dist/recorder/binding.js +0 -215
  100. package/dist/recorder/binding.js.map +0 -1
  101. package/dist/recorder/index.d.ts +0 -4
  102. package/dist/recorder/index.d.ts.map +0 -1
  103. package/dist/recorder/index.js +0 -4
  104. package/dist/recorder/index.js.map +0 -1
  105. package/dist/recorder/recorder.d.ts +0 -19
  106. package/dist/recorder/recorder.d.ts.map +0 -1
  107. package/dist/recorder/recorder.js +0 -101
  108. package/dist/recorder/recorder.js.map +0 -1
  109. package/dist/recorder/store.d.ts +0 -22
  110. package/dist/recorder/store.d.ts.map +0 -1
  111. package/dist/recorder/store.js +0 -150
  112. package/dist/recorder/store.js.map +0 -1
  113. package/dist/recorder/types.d.ts +0 -73
  114. package/dist/recorder/types.d.ts.map +0 -1
  115. package/dist/recorder/types.js +0 -5
  116. package/dist/recorder/types.js.map +0 -1
@@ -8,9 +8,9 @@ import { executeCommand } from './actions.js';
8
8
  import { errorResponse, serializeResponse } from './protocol.js';
9
9
  import { getSocketDir, getSession, getInstanceId } from './daemon.js';
10
10
  export const STATE_CONFIGS = {
11
- user_interacting: { format: 'jpeg', quality: 80, maxFps: 60, scale: 0.4 },
12
- screen_moving: { format: 'webp', quality: 50, maxFps: 1, scale: 0.6 },
13
- static: { format: 'webp', quality: 80, maxFps: 0.5, scale: 1 },
11
+ user_interacting: { format: 'jpeg', quality: 80, maxFps: 60, scale: 0.6 },
12
+ screen_moving: { format: 'jpeg', quality: 75, maxFps: 8, scale: 0.8 },
13
+ static: { format: 'jpeg', quality: 80, maxFps: 2, scale: 1 },
14
14
  };
15
15
  export class StreamStateManager {
16
16
  currentState = 'static';
@@ -119,10 +119,26 @@ export class FrameRateController {
119
119
  }
120
120
  }
121
121
  export class FrameProcessor {
122
- async process(data, config, viewportWidth, viewportHeight) {
122
+ screencastFormat = 'jpeg';
123
+ screencastQuality = 80;
124
+ async process(data, config, viewportWidth, viewportHeight, cropConfig) {
123
125
  const buffer = Buffer.from(data, 'base64');
126
+ const needsResize = config.scale < 1 && viewportWidth && viewportHeight;
127
+ const needsCrop = !!cropConfig;
128
+ const needsReencode = config.format !== this.screencastFormat || config.quality !== this.screencastQuality;
129
+ if (!needsResize && !needsCrop && !needsReencode) {
130
+ return buffer;
131
+ }
124
132
  let processed = sharp(buffer);
125
- if (config.scale < 1 && viewportWidth && viewportHeight) {
133
+ if (cropConfig) {
134
+ processed = processed.extract({
135
+ left: Math.round(cropConfig.x),
136
+ top: Math.round(cropConfig.y),
137
+ width: Math.round(cropConfig.width),
138
+ height: Math.round(cropConfig.height),
139
+ });
140
+ }
141
+ if (needsResize) {
126
142
  const newWidth = Math.round(viewportWidth * config.scale);
127
143
  const newHeight = Math.round(viewportHeight * config.scale);
128
144
  processed = processed.resize(newWidth, newHeight);
@@ -136,6 +152,25 @@ export class FrameProcessor {
136
152
  return processed.toBuffer();
137
153
  }
138
154
  }
155
+ function isPrivateIP(hostname) {
156
+ if (hostname === 'localhost' ||
157
+ hostname === '127.0.0.1' ||
158
+ hostname === '::1' ||
159
+ hostname === '[::1]') {
160
+ return true;
161
+ }
162
+ const parts = hostname.split('.').map(Number);
163
+ if (parts.length === 4 && parts.every((p) => !isNaN(p))) {
164
+ const [a, b] = parts;
165
+ if (a === 10)
166
+ return true;
167
+ if (a === 172 && b >= 16 && b <= 31)
168
+ return true;
169
+ if (a === 192 && b === 168)
170
+ return true;
171
+ }
172
+ return false;
173
+ }
139
174
  export function isAllowedOrigin(origin) {
140
175
  if (!origin) {
141
176
  return true;
@@ -145,8 +180,7 @@ export function isAllowedOrigin(origin) {
145
180
  }
146
181
  try {
147
182
  const url = new URL(origin);
148
- const host = url.hostname;
149
- if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]') {
183
+ if (isPrivateIP(url.hostname)) {
150
184
  return true;
151
185
  }
152
186
  }
@@ -160,7 +194,7 @@ function isCommandMessage(msg) {
160
194
  }
161
195
  export class StreamServer {
162
196
  wss = null;
163
- clients = new Set();
197
+ clients = new Map();
164
198
  browser;
165
199
  port;
166
200
  isScreencasting = false;
@@ -188,7 +222,7 @@ export class StreamServer {
188
222
  fps: this.frameRateController.getCurrentFps(),
189
223
  state: newState,
190
224
  };
191
- for (const client of this.clients) {
225
+ for (const [client, _state] of this.clients) {
192
226
  if (client.readyState === WebSocket.OPEN) {
193
227
  client.send(JSON.stringify(headerMessage));
194
228
  client.send(processedBuffer);
@@ -213,8 +247,8 @@ export class StreamServer {
213
247
  return false;
214
248
  },
215
249
  });
216
- this.wss.on('connection', (ws) => {
217
- this.handleConnection(ws);
250
+ this.wss.on('connection', (ws, req) => {
251
+ this.handleConnection(ws, req);
218
252
  });
219
253
  this.wss.on('error', (error) => {
220
254
  console.error('[StreamServer] WebSocket error:', error);
@@ -255,7 +289,7 @@ export class StreamServer {
255
289
  }
256
290
  setScreencastFrameCallback(null);
257
291
  setEventCallbacks({});
258
- for (const client of this.clients) {
292
+ for (const [client, _state] of this.clients) {
259
293
  client.close();
260
294
  }
261
295
  this.clients.clear();
@@ -268,10 +302,60 @@ export class StreamServer {
268
302
  });
269
303
  }
270
304
  }
271
- handleConnection(ws) {
272
- console.log('[StreamServer] Client connected');
273
- this.clients.add(ws);
274
- this.sendStatus(ws);
305
+ handleConnection(ws, req) {
306
+ const clientState = {};
307
+ try {
308
+ const url = new URL(req.url || '/', 'http://localhost');
309
+ const rawSelector = url.searchParams.get('selector');
310
+ if (rawSelector) {
311
+ clientState.selector = decodeURIComponent(rawSelector);
312
+ }
313
+ }
314
+ catch {
315
+ // Invalid URL, ignore
316
+ }
317
+ this.clients.set(ws, clientState);
318
+ if (clientState.selector) {
319
+ clientState.elementCheckTimer = setInterval(() => {
320
+ if (!clientState.selector)
321
+ return;
322
+ this.getElementBox(clientState.selector)
323
+ .then((newBox) => {
324
+ if (!newBox) {
325
+ if (clientState.elementBox || !clientState.degraded) {
326
+ clientState.elementBox = undefined;
327
+ clientState.degraded = true;
328
+ const statusMsg = {
329
+ type: 'status',
330
+ connected: true,
331
+ screencasting: this.isScreencasting,
332
+ degraded: true,
333
+ };
334
+ if (ws.readyState === WebSocket.OPEN) {
335
+ ws.send(JSON.stringify(statusMsg));
336
+ }
337
+ }
338
+ }
339
+ else {
340
+ clientState.elementBox = newBox;
341
+ clientState.degraded = false;
342
+ }
343
+ })
344
+ .catch(() => { });
345
+ }, 2500);
346
+ }
347
+ this.sendStatus(ws, clientState);
348
+ this.refreshElementBox(ws, clientState)
349
+ .then(async () => {
350
+ this.sendStatus(ws, clientState);
351
+ if (this.isScreencasting && this.lastFrameData && this.lastFrameMetadata) {
352
+ await new Promise((r) => setTimeout(r, 100));
353
+ this.replayLastFrame(ws, clientState);
354
+ }
355
+ })
356
+ .catch((err) => {
357
+ console.error('[StreamServer] Failed to refresh element box:', err);
358
+ });
275
359
  if (this.clients.size === 1 && !this.isScreencasting) {
276
360
  this.startScreencast().catch((error) => {
277
361
  console.error('[StreamServer] Failed to start screencast:', error);
@@ -288,7 +372,10 @@ export class StreamServer {
288
372
  }
289
373
  });
290
374
  ws.on('close', () => {
291
- console.log('[StreamServer] Client disconnected');
375
+ if (clientState.elementCheckTimer) {
376
+ clearInterval(clientState.elementCheckTimer);
377
+ clientState.elementCheckTimer = undefined;
378
+ }
292
379
  this.clients.delete(ws);
293
380
  if (this.clients.size === 0 && this.isScreencasting) {
294
381
  this.stopScreencast().catch((error) => {
@@ -361,11 +448,33 @@ export class StreamServer {
361
448
  this.stateManager.onUserInteraction();
362
449
  await this.browser.getPage().keyboard.insertText(message.text);
363
450
  break;
451
+ case 'input_focused':
452
+ case 'input_value':
453
+ case 'input_blur':
454
+ for (const [ws] of this.clients.entries()) {
455
+ if (ws.readyState === WebSocket.OPEN) {
456
+ try {
457
+ ws.send(JSON.stringify(message));
458
+ }
459
+ catch (_) { }
460
+ }
461
+ }
462
+ break;
364
463
  case 'user_activity':
365
464
  this.stateManager.onUserInteraction();
366
465
  break;
367
466
  case 'status':
368
- this.sendStatus(ws);
467
+ const cs = this.clients.get(ws);
468
+ if (cs?.selector) {
469
+ this.sendStatus(ws, cs);
470
+ this.refreshElementBox(ws, cs).catch(() => { });
471
+ }
472
+ else if (cs) {
473
+ this.sendStatus(ws, cs);
474
+ }
475
+ else {
476
+ this.sendStatus(ws);
477
+ }
369
478
  break;
370
479
  }
371
480
  }
@@ -382,36 +491,97 @@ export class StreamServer {
382
491
  if (!this.frameRateController.shouldSendFrame(config.maxFps)) {
383
492
  return;
384
493
  }
385
- let processedBuffer;
386
- try {
387
- processedBuffer = await this.frameProcessor.process(frame.data, config, frame.metadata.deviceWidth, frame.metadata.deviceHeight);
388
- }
389
- catch {
390
- processedBuffer = Buffer.from(frame.data, 'base64');
391
- }
392
- const headerMessage = {
393
- type: 'frame',
394
- metadata: frame.metadata,
395
- format: config.format,
396
- fps: this.frameRateController.getCurrentFps(),
397
- state: this.stateManager.getState(),
398
- };
399
- for (const client of this.clients) {
400
- if (client.readyState === WebSocket.OPEN) {
494
+ for (const [client, clientState] of this.clients) {
495
+ if (client.readyState !== WebSocket.OPEN) {
496
+ continue;
497
+ }
498
+ try {
499
+ let processedBuffer;
500
+ let metadata = { ...frame.metadata };
501
+ if (clientState.selector && clientState.elementBox) {
502
+ processedBuffer = await this.frameProcessor.process(frame.data, config, clientState.elementBox.width, clientState.elementBox.height, clientState.elementBox);
503
+ metadata.deviceWidth = clientState.elementBox.width;
504
+ metadata.deviceHeight = clientState.elementBox.height;
505
+ }
506
+ else {
507
+ processedBuffer = await this.frameProcessor.process(frame.data, config, frame.metadata.deviceWidth, frame.metadata.deviceHeight);
508
+ }
509
+ const headerMessage = {
510
+ type: 'frame',
511
+ metadata,
512
+ format: config.format,
513
+ fps: this.frameRateController.getCurrentFps(),
514
+ state: this.stateManager.getState(),
515
+ };
401
516
  client.send(JSON.stringify(headerMessage));
402
517
  client.send(processedBuffer);
403
518
  }
519
+ catch (err) {
520
+ console.error('[StreamServer] Failed to process frame for client:', err);
521
+ }
522
+ }
523
+ }
524
+ async replayLastFrame(ws, clientState) {
525
+ if (!this.lastFrameData || !this.lastFrameMetadata)
526
+ return;
527
+ if (ws.readyState !== WebSocket.OPEN)
528
+ return;
529
+ try {
530
+ const config = this.stateManager.getConfig();
531
+ let processedBuffer;
532
+ let metadata = { ...this.lastFrameMetadata };
533
+ if (clientState.selector && clientState.elementBox) {
534
+ processedBuffer = await this.frameProcessor.process(this.lastFrameData, config, clientState.elementBox.width, clientState.elementBox.height, clientState.elementBox);
535
+ metadata.deviceWidth = clientState.elementBox.width;
536
+ metadata.deviceHeight = clientState.elementBox.height;
537
+ }
538
+ else {
539
+ processedBuffer = await this.frameProcessor.process(this.lastFrameData, config, this.lastFrameMetadata.deviceWidth, this.lastFrameMetadata.deviceHeight);
540
+ }
541
+ const headerMessage = {
542
+ type: 'frame',
543
+ metadata,
544
+ format: config.format,
545
+ fps: this.frameRateController.getCurrentFps(),
546
+ state: this.stateManager.getState(),
547
+ };
548
+ ws.send(JSON.stringify(headerMessage));
549
+ ws.send(processedBuffer);
550
+ }
551
+ catch (err) {
552
+ console.error('[StreamServer] Failed to replay last frame:', err);
404
553
  }
405
554
  }
406
555
  broadcastEvent(message) {
407
556
  const payload = JSON.stringify(message);
408
- for (const client of this.clients) {
557
+ for (const [client, _state] of this.clients) {
409
558
  if (client.readyState === WebSocket.OPEN) {
410
559
  client.send(payload);
411
560
  }
412
561
  }
413
562
  }
414
- sendStatus(ws) {
563
+ async getElementBox(selector) {
564
+ try {
565
+ const page = this.browser.getPage();
566
+ const box = await page.evaluate((sel) => {
567
+ const el = document.querySelector(sel);
568
+ if (!el)
569
+ return null;
570
+ const rect = el.getBoundingClientRect();
571
+ return {
572
+ x: rect.x,
573
+ y: rect.y,
574
+ width: rect.width,
575
+ height: rect.height,
576
+ };
577
+ }, selector);
578
+ return box ?? undefined;
579
+ }
580
+ catch {
581
+ return undefined;
582
+ }
583
+ }
584
+ sendStatus(ws, clientState) {
415
585
  let viewportWidth;
416
586
  let viewportHeight;
417
587
  try {
@@ -420,9 +590,7 @@ export class StreamServer {
420
590
  viewportWidth = viewport?.width;
421
591
  viewportHeight = viewport?.height;
422
592
  }
423
- catch {
424
- // Browser not launched yet
425
- }
593
+ catch { }
426
594
  const message = {
427
595
  type: 'status',
428
596
  connected: true,
@@ -431,11 +599,48 @@ export class StreamServer {
431
599
  viewportHeight,
432
600
  fps: this.frameRateController.getCurrentFps(),
433
601
  state: this.stateManager.getState(),
602
+ version: process.env.npm_package_version || '0.9.5',
434
603
  };
604
+ if (clientState?.elementBox) {
605
+ message.element = {
606
+ selector: clientState.selector,
607
+ x: clientState.elementBox.x,
608
+ y: clientState.elementBox.y,
609
+ width: clientState.elementBox.width,
610
+ height: clientState.elementBox.height,
611
+ };
612
+ message.viewportWidth = clientState.elementBox.width;
613
+ message.viewportHeight = clientState.elementBox.height;
614
+ }
615
+ if (clientState?.degraded) {
616
+ message.degraded = true;
617
+ }
435
618
  if (ws.readyState === WebSocket.OPEN) {
436
619
  ws.send(JSON.stringify(message));
437
620
  }
438
621
  }
622
+ async refreshElementBox(ws, clientState) {
623
+ if (!clientState.selector)
624
+ return;
625
+ const box = await this.getElementBox(clientState.selector);
626
+ if (box) {
627
+ clientState.elementBox = box;
628
+ clientState.degraded = false;
629
+ }
630
+ else if (!clientState.degraded) {
631
+ clientState.elementBox = undefined;
632
+ clientState.degraded = true;
633
+ const message = {
634
+ type: 'status',
635
+ connected: true,
636
+ screencasting: this.isScreencasting,
637
+ degraded: true,
638
+ };
639
+ if (ws.readyState === WebSocket.OPEN) {
640
+ ws.send(JSON.stringify(message));
641
+ }
642
+ }
643
+ }
439
644
  sendError(ws, errorMessage) {
440
645
  const message = {
441
646
  type: 'error',
@@ -453,16 +658,58 @@ export class StreamServer {
453
658
  if (!this.browser.isLaunched()) {
454
659
  throw new Error('Browser not launched');
455
660
  }
456
- await this.browser.startScreencast((frame) => this.broadcastFrame(frame), {
457
- format: 'jpeg',
458
- quality: 80,
459
- maxWidth: 1280,
460
- maxHeight: 720,
461
- everyNthFrame: 1,
462
- });
463
- for (const client of this.clients) {
464
- this.sendStatus(client);
661
+ try {
662
+ await this.browser.startScreencast((frame) => this.broadcastFrame(frame), {
663
+ format: 'jpeg',
664
+ quality: 80,
665
+ maxWidth: 1280,
666
+ maxHeight: 720,
667
+ everyNthFrame: 1,
668
+ });
669
+ }
670
+ catch (startError) {
671
+ if (startError instanceof Error && startError.message === 'Screencast already active') {
672
+ await this.browser.stopScreencast();
673
+ await this.browser.startScreencast((frame) => this.broadcastFrame(frame), {
674
+ format: 'jpeg',
675
+ quality: 80,
676
+ maxWidth: 1280,
677
+ maxHeight: 720,
678
+ everyNthFrame: 1,
679
+ });
680
+ }
681
+ else {
682
+ throw startError;
683
+ }
684
+ }
685
+ for (const [client, state] of this.clients) {
686
+ this.sendStatus(client, state);
465
687
  }
688
+ try {
689
+ await this.browser.injectFocusListener((data) => {
690
+ for (const [ws] of this.clients.entries()) {
691
+ if (ws.readyState === WebSocket.OPEN) {
692
+ try {
693
+ ws.send(JSON.stringify(data));
694
+ }
695
+ catch (_) { }
696
+ }
697
+ }
698
+ });
699
+ }
700
+ catch (e) {
701
+ console.error('[StreamServer] Failed to inject focus listener:', e);
702
+ }
703
+ try {
704
+ const page = this.browser.getPage();
705
+ await page.evaluate(() => {
706
+ document.body.style.opacity = '0.999';
707
+ requestAnimationFrame(() => {
708
+ document.body.style.opacity = '';
709
+ });
710
+ });
711
+ }
712
+ catch { }
466
713
  }
467
714
  catch (error) {
468
715
  this.isScreencasting = false;
@@ -472,10 +719,10 @@ export class StreamServer {
472
719
  async stopScreencast() {
473
720
  if (!this.isScreencasting)
474
721
  return;
475
- await this.browser.stopScreencast();
476
722
  this.isScreencasting = false;
477
- for (const client of this.clients) {
478
- this.sendStatus(client);
723
+ await this.browser.stopScreencast();
724
+ for (const [client, state] of this.clients) {
725
+ this.sendStatus(client, state);
479
726
  }
480
727
  }
481
728
  getPort() {
@@ -507,6 +754,8 @@ export class StreamServerProxy {
507
754
  reconnectTimer = null;
508
755
  lastFrameData = null;
509
756
  lastFrameMetadata = null;
757
+ inputFillDebounceMap = new Map();
758
+ static INPUT_FILL_DEBOUNCE_MS = 60;
510
759
  constructor(browser) {
511
760
  this.browser = browser;
512
761
  this.ipcPath = getStreamServerIpcPath();
@@ -587,6 +836,8 @@ export class StreamServerProxy {
587
836
  case 'keyboard_down':
588
837
  case 'keyboard_up':
589
838
  case 'keyboard_insert_text':
839
+ case 'input_fill':
840
+ case 'input_blur_element':
590
841
  case 'user_activity':
591
842
  this.handleInputMessage(message);
592
843
  break;
@@ -596,6 +847,25 @@ export class StreamServerProxy {
596
847
  case 'client_disconnected':
597
848
  this.handleClientDisconnected(message.session);
598
849
  break;
850
+ case 'request_element_box':
851
+ (async () => {
852
+ const box = await this.getElementBox(message.selector);
853
+ const response = {
854
+ type: 'selector_element',
855
+ session: message.session ?? this.session,
856
+ selector: message.selector,
857
+ };
858
+ if (box) {
859
+ response.elementBox = box;
860
+ }
861
+ this.send(response);
862
+ })();
863
+ break;
864
+ case 'input_focused':
865
+ case 'input_value':
866
+ case 'input_blur':
867
+ this.send(message);
868
+ break;
599
869
  }
600
870
  }
601
871
  catch (error) {
@@ -673,6 +943,23 @@ export class StreamServerProxy {
673
943
  this.stateManager.onUserInteraction();
674
944
  await this.browser.getPage().keyboard.insertText(message.text);
675
945
  break;
946
+ case 'input_fill': {
947
+ const sel = message.selector || '';
948
+ const txt = message.text || '';
949
+ const key = sel || '__global__';
950
+ const prev = this.inputFillDebounceMap.get(key);
951
+ if (prev)
952
+ clearTimeout(prev.timer);
953
+ const timer = setTimeout(async () => {
954
+ this.inputFillDebounceMap.delete(key);
955
+ await this.browser.fillValue(sel, txt);
956
+ }, StreamServerProxy.INPUT_FILL_DEBOUNCE_MS);
957
+ this.inputFillDebounceMap.set(key, { timer, text: txt, selector: sel });
958
+ break;
959
+ }
960
+ case 'input_blur_element':
961
+ await this.browser.blurElement(message.selector || '');
962
+ break;
676
963
  case 'user_activity':
677
964
  this.stateManager.onUserInteraction();
678
965
  break;
@@ -733,6 +1020,39 @@ export class StreamServerProxy {
733
1020
  this.ipcSocket.write(JSON.stringify(message) + '\n');
734
1021
  }
735
1022
  }
1023
+ sendSelectorElement(selector, elementBox) {
1024
+ this.send({
1025
+ type: 'selector_element',
1026
+ session: this.session,
1027
+ selector,
1028
+ elementBox,
1029
+ });
1030
+ }
1031
+ async getElementBox(selector) {
1032
+ try {
1033
+ const page = this.browser.getPage();
1034
+ if (!page) {
1035
+ return undefined;
1036
+ }
1037
+ const box = await page.evaluate((sel) => {
1038
+ const el = document.querySelector(sel);
1039
+ if (!el)
1040
+ return null;
1041
+ const rect = el.getBoundingClientRect();
1042
+ return {
1043
+ x: rect.x,
1044
+ y: rect.y,
1045
+ width: rect.width,
1046
+ height: rect.height,
1047
+ };
1048
+ }, selector);
1049
+ return box ?? undefined;
1050
+ }
1051
+ catch (err) {
1052
+ console.error('[StreamServerProxy] getElementBox error:', err);
1053
+ return undefined;
1054
+ }
1055
+ }
736
1056
  getDaemonSocketPath() {
737
1057
  const isWindows = process.platform === 'win32';
738
1058
  if (isWindows) {