@dyyz1993/agent-browser 0.9.2 → 0.11.0

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 (110) 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 +858 -102
  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 +88 -1
  74. package/skills/agent-browser/references/commands.md +3 -0
  75. package/skills/agent-browser/references/network-monitoring.md +232 -0
  76. package/skills/agent-browser/references/profiling.md +120 -0
  77. package/skills/agent-browser/references/recorder.md +319 -0
  78. package/skills/agent-browser/templates/network-intercept-crawl.sh +255 -0
  79. package/dist/__tests__/test-iframe.d.ts +0 -2
  80. package/dist/__tests__/test-iframe.d.ts.map +0 -1
  81. package/dist/__tests__/test-iframe.js +0 -52
  82. package/dist/__tests__/test-iframe.js.map +0 -1
  83. package/dist/cli-new.d.ts +0 -3
  84. package/dist/cli-new.d.ts.map +0 -1
  85. package/dist/cli-new.js +0 -308
  86. package/dist/cli-new.js.map +0 -1
  87. package/dist/cli-old.d.ts +0 -3
  88. package/dist/cli-old.d.ts.map +0 -1
  89. package/dist/cli-old.js +0 -1101
  90. package/dist/cli-old.js.map +0 -1
  91. package/dist/recorder/binding.d.ts +0 -24
  92. package/dist/recorder/binding.d.ts.map +0 -1
  93. package/dist/recorder/binding.js +0 -215
  94. package/dist/recorder/binding.js.map +0 -1
  95. package/dist/recorder/index.d.ts +0 -4
  96. package/dist/recorder/index.d.ts.map +0 -1
  97. package/dist/recorder/index.js +0 -4
  98. package/dist/recorder/index.js.map +0 -1
  99. package/dist/recorder/recorder.d.ts +0 -19
  100. package/dist/recorder/recorder.d.ts.map +0 -1
  101. package/dist/recorder/recorder.js +0 -101
  102. package/dist/recorder/recorder.js.map +0 -1
  103. package/dist/recorder/store.d.ts +0 -22
  104. package/dist/recorder/store.d.ts.map +0 -1
  105. package/dist/recorder/store.js +0 -150
  106. package/dist/recorder/store.js.map +0 -1
  107. package/dist/recorder/types.d.ts +0 -73
  108. package/dist/recorder/types.d.ts.map +0 -1
  109. package/dist/recorder/types.js +0 -5
  110. package/dist/recorder/types.js.map +0 -1
@@ -52,13 +52,18 @@ export function sendUserActivity(state, qualityBadge, ws) {
52
52
  }, 2000);
53
53
  qualityBadge.textContent = 'interacting';
54
54
  }
55
- export function screenToPage(screenX, screenY, screenWidth, screenHeight, deviceWidth, deviceHeight) {
56
- const scaleX = deviceWidth / screenWidth;
57
- const scaleY = deviceHeight / screenHeight;
58
- return {
59
- x: Math.round(screenX * scaleX),
60
- y: Math.round(screenY * scaleY),
61
- };
55
+ export function screenToPage(screenX, screenY, rect, deviceWidth, deviceHeight, element) {
56
+ if (rect.width <= 0 || rect.height <= 0)
57
+ return { x: 0, y: 0 };
58
+ const scaleX = deviceWidth / rect.width;
59
+ const scaleY = deviceHeight / rect.height;
60
+ let pageX = Math.round((screenX - rect.left) * scaleX);
61
+ let pageY = Math.round((screenY - rect.top) * scaleY);
62
+ if (element) {
63
+ pageX += element.x;
64
+ pageY += element.y;
65
+ }
66
+ return { x: pageX, y: pageY };
62
67
  }
63
68
  export function updateModifiers(e) {
64
69
  let modifiers = 0;
@@ -83,8 +88,11 @@ export function buildViewerScript() {
83
88
  const urlParams = new URLSearchParams(location.search);
84
89
  const instanceId = urlParams.get('instanceId');
85
90
  const session = urlParams.get('session') || 'default';
91
+ const rawSelector = urlParams.get('selector');
92
+ const selector = rawSelector ? decodeURIComponent(rawSelector) : undefined;
86
93
  const wsParam = instanceId ? 'instanceId=' + instanceId : 'session=' + session;
87
- const wsUrl = wsProtocol + '//' + location.hostname + ':' + port + '?' + wsParam;
94
+
95
+ const wsUrl = wsProtocol + '//' + location.hostname + ':' + port + '?' + wsParam + (selector ? '&selector=' + encodeURIComponent(selector) : '');
88
96
 
89
97
  // Background management
90
98
  let shouldReconnect = true;
@@ -97,19 +105,78 @@ export function buildViewerScript() {
97
105
  const statusText = document.getElementById('statusText');
98
106
  const urlDisplay = document.getElementById('urlDisplay');
99
107
  const qualityBadge = document.getElementById('qualityBadge');
100
- const tabsContainer = document.getElementById('tabs');
101
108
  const connecting = document.getElementById('connecting');
102
-
103
- const hiddenInput = document.createElement('input');
104
- hiddenInput.type = 'text';
105
- hiddenInput.style.cssText = 'position:fixed;right:8px;bottom:8px;opacity:0.01;width:1px;height:1px;border:none;outline:none;padding:0;margin:0;';
106
- hiddenInput.id = 'hidden-input';
107
- hiddenInput.setAttribute('autocomplete', 'off');
108
- hiddenInput.setAttribute('autocorrect', 'off');
109
- hiddenInput.setAttribute('autocapitalize', 'off');
110
- hiddenInput.setAttribute('spellcheck', 'false');
111
- document.body.appendChild(hiddenInput);
112
-
109
+
110
+ const ua = (navigator.userAgent || '').toLowerCase();
111
+
112
+ function detectDeviceMode() {
113
+ var uaMatch = /iphone|ipod|android(?=.*mobile)|mobile|tablet|ipad/i.test(ua);
114
+ var hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
115
+ return uaMatch || hasTouch ? 'mobile' : 'desktop';
116
+ }
117
+
118
+ var _deviceCurrent = detectDeviceMode();
119
+
120
+ const DeviceMode = {
121
+ _current: _deviceCurrent,
122
+ _listeners: [],
123
+ get current() { return this._current; },
124
+ onModeChange: function(fn) { this._listeners.push(fn); },
125
+ switchTo: function(mode) {
126
+ if (mode === this._current) return;
127
+ var prev = this._current;
128
+ this._current = mode;
129
+ if (mode === 'desktop') {
130
+ MobileModule.detach();
131
+ DesktopModule.attach();
132
+ } else {
133
+ DesktopModule.detach();
134
+ MobileModule.attach();
135
+ }
136
+ this._listeners.forEach(function(fn) { fn(mode, prev); });
137
+ }
138
+ };
139
+
140
+ var hiddenInput = null;
141
+
142
+ const DesktopModule = {
143
+ attach: function() {
144
+ if (hiddenInput && hiddenInput.parentNode) return;
145
+ hiddenInput = document.createElement('input');
146
+ hiddenInput.type = 'text';
147
+ hiddenInput.style.cssText = 'position:fixed;right:8px;bottom:80px;opacity:0.01;width:1px;height:1px;border:none;outline:none;padding:0;margin:0;font-size:16px;pointer-events:none;';
148
+ hiddenInput.id = 'hidden-input';
149
+ hiddenInput.setAttribute('autocomplete', 'off');
150
+ hiddenInput.setAttribute('autocorrect', 'off');
151
+ hiddenInput.setAttribute('autocapitalize', 'off');
152
+ hiddenInput.setAttribute('spellcheck', 'false');
153
+ document.body.appendChild(hiddenInput);
154
+ focusHiddenInput();
155
+ },
156
+ detach: function() {
157
+ if (hiddenInput) { hiddenInput.blur(); if (hiddenInput.parentNode) hiddenInput.parentNode.removeChild(hiddenInput); hiddenInput = null; }
158
+ }
159
+ };
160
+
161
+ const MobileModule = {
162
+ attach: function() {
163
+ if (touchpad) { touchpad.style.display = 'flex'; touchpad.style.position = 'relative'; touchpad.style.background = 'linear-gradient(180deg, #1a1a2e 0%, #16213e 100%)'; touchpad.style.borderTop = '2px solid #4ecca3'; touchpad.style.justifyContent = 'center'; touchpad.style.zIndex = '10'; }
164
+ setupToolbar();
165
+ if (!cursorInitialized) { cursorInitialized = true; setTimeout(initCursor, 50); }
166
+ },
167
+ detach: function() {
168
+ var ip = document.getElementById('input-panel');
169
+ if (ip) { ip.style.display = 'none'; ip.style.bottom = '0px'; }
170
+ if (cursor) cursor.style.display = 'block';
171
+ }
172
+ };
173
+
174
+ const degradedToast = document.createElement('div');
175
+ degradedToast.id = 'degraded-toast';
176
+ degradedToast.style.cssText = 'position:fixed;top:10px;left:50%;transform:translateX(-50%);background:rgba(255,200,0,0.9);color:#000;padding:10px 20px;border-radius:4px;font-family:sans-serif;font-size:14px;z-index:9999;display:none;pointer-events:none;';
177
+ degradedToast.textContent = 'Element not found, showing full page';
178
+ document.body.appendChild(degradedToast);
179
+
113
180
  let ws = null;
114
181
  let metadata = { deviceWidth: 1280, deviceHeight: 720, pageScaleFactor: 1, format: 'jpeg' };
115
182
  let userActivityTimeout = null;
@@ -121,7 +188,8 @@ export function buildViewerScript() {
121
188
  let lastInputValue = '';
122
189
  let fixedSize = false;
123
190
  let isRecording = false;
124
-
191
+ var _inputPollRaf = null;
192
+
125
193
  function connect() {
126
194
  ws = new WebSocket(wsUrl);
127
195
  ws.binaryType = 'arraybuffer';
@@ -185,7 +253,15 @@ export function buildViewerScript() {
185
253
  switch (msg.type) {
186
254
  case 'frame':
187
255
  pendingBinary = true;
256
+ const prevElement = metadata.element;
188
257
  metadata = msg.metadata;
258
+ if (prevElement && !metadata.element) {
259
+ metadata.element = prevElement;
260
+ }
261
+ if (metadata.element) {
262
+ metadata.deviceWidth = metadata.element.width;
263
+ metadata.deviceHeight = metadata.element.height;
264
+ }
189
265
  if (msg.format) metadata.format = msg.format;
190
266
  if (msg.state) {
191
267
  qualityBadge.textContent = msg.state;
@@ -204,6 +280,17 @@ export function buildViewerScript() {
204
280
  metadata.deviceWidth = msg.viewportWidth;
205
281
  metadata.deviceHeight = msg.viewportHeight;
206
282
  }
283
+ if (msg.element) {
284
+ metadata.element = msg.element;
285
+ } else {
286
+ metadata.element = undefined;
287
+ if (selector && msg.degraded) {
288
+ showDegradedMessage();
289
+ }
290
+ }
291
+ if (screen.src && metadata.deviceWidth && metadata.deviceHeight) {
292
+ fitImageToContainer();
293
+ }
207
294
  }
208
295
  break;
209
296
 
@@ -212,16 +299,24 @@ export function buildViewerScript() {
212
299
  document.title = msg.data.title + ' - Agent Browser Viewer';
213
300
  break;
214
301
 
215
- case 'tab_created':
216
- addTab(msg.data.index, msg.data.url, msg.data.title, false);
302
+ case 'input_focused':
303
+ if (inputMode) return;
304
+ if (DeviceMode.current !== 'mobile') return;
305
+ var sel = msg.selector || (msg.id ? '#' + msg.id : '');
306
+ enterInputMode(msg.value || '', msg.inputType || msg.tag || '', msg.placeholder || '', sel);
217
307
  break;
218
308
 
219
- case 'tab_closed':
220
- removeTab(msg.data.index);
309
+ case 'input_value':
310
+ if (!inputMode) {
311
+ var field = document.getElementById('input-field');
312
+ if (field && typeof msg.text === 'string') {
313
+ field.value = msg.text;
314
+ }
315
+ }
221
316
  break;
222
317
 
223
- case 'tab_switched':
224
- setActiveTab(msg.data.toIndex);
318
+ case 'input_blur':
319
+ exitInputMode();
225
320
  break;
226
321
  }
227
322
  };
@@ -230,27 +325,53 @@ export function buildViewerScript() {
230
325
  function handleBinary(data) {
231
326
  if (!pendingBinary) return;
232
327
  pendingBinary = false;
233
-
328
+
234
329
  const blob = new Blob([data], {
235
330
  type: metadata.format === 'webp' ? 'image/webp' : 'image/jpeg'
236
331
  });
237
332
  const url = URL.createObjectURL(blob);
238
-
333
+
239
334
  const cleanup = () => {
240
335
  URL.revokeObjectURL(url);
241
336
  connecting.style.display = 'none';
242
337
  screen.style.display = 'block';
338
+ fitImageToContainer();
339
+ if (!cursorInitialized && DeviceMode.current === 'mobile') {
340
+ cursorInitialized = true;
341
+ setTimeout(initCursor, 50);
342
+ }
243
343
  };
244
-
344
+
245
345
  screen.onload = cleanup;
246
346
  screen.onerror = cleanup;
247
347
  screen.src = url;
248
-
249
- if (!fixedSize) {
250
- screen.style.width = metadata.deviceWidth + 'px';
251
- screen.style.height = metadata.deviceHeight + 'px';
252
- fixedSize = true;
348
+ }
349
+
350
+ function fitImageToContainer() {
351
+ if (!metadata.deviceWidth || !metadata.deviceHeight) return;
352
+ var container = screen.parentElement;
353
+ if (!container) return;
354
+ var cw = container.clientWidth;
355
+ var ch = container.clientHeight;
356
+ if (cw <= 0 || ch <= 0) return;
357
+
358
+ var imgW = metadata.deviceWidth;
359
+ var imgH = metadata.deviceHeight;
360
+ var imgRatio = imgW / imgH;
361
+ var contRatio = cw / ch;
362
+
363
+ var dw, dh;
364
+ if (imgRatio > contRatio) {
365
+ dw = cw;
366
+ dh = cw / imgRatio;
367
+ } else {
368
+ dh = ch;
369
+ dw = ch * imgRatio;
253
370
  }
371
+
372
+ screen.style.width = Math.round(dw) + 'px';
373
+ screen.style.height = Math.round(dh) + 'px';
374
+
254
375
  }
255
376
 
256
377
  function safeSend(data) {
@@ -272,13 +393,19 @@ export function buildViewerScript() {
272
393
 
273
394
  function screenToPage(screenX, screenY) {
274
395
  const rect = screen.getBoundingClientRect();
275
- const scaleX = metadata.deviceWidth / rect.width;
276
- const scaleY = metadata.deviceHeight / rect.height;
277
-
278
- return {
279
- x: Math.round((screenX - rect.left) * scaleX),
280
- y: Math.round((screenY - rect.top) * scaleY)
281
- };
396
+ if (rect.width <= 0 || rect.height <= 0) return { x: 0, y: 0 };
397
+
398
+ var scaleX = metadata.deviceWidth / rect.width;
399
+ var scaleY = metadata.deviceHeight / rect.height;
400
+ var pageX = Math.round((screenX - rect.left) * scaleX);
401
+ var pageY = Math.round((screenY - rect.top) * scaleY);
402
+
403
+ if (metadata.element) {
404
+ pageX += metadata.element.x;
405
+ pageY += metadata.element.y;
406
+ }
407
+
408
+ return { x: pageX, y: pageY };
282
409
  }
283
410
 
284
411
  function updateModifiers(e) {
@@ -288,7 +415,14 @@ export function buildViewerScript() {
288
415
  if (e.metaKey) modifiers |= 4;
289
416
  if (e.shiftKey) modifiers |= 8;
290
417
  }
291
-
418
+
419
+ function showDegradedMessage() {
420
+ degradedToast.style.display = 'block';
421
+ setTimeout(() => {
422
+ degradedToast.style.display = 'none';
423
+ }, 3000);
424
+ }
425
+
292
426
  function focusHiddenInput() {
293
427
  hiddenInput.focus();
294
428
  hiddenInput.select();
@@ -297,7 +431,7 @@ export function buildViewerScript() {
297
431
  screen.addEventListener('dragstart', (e) => e.preventDefault());
298
432
 
299
433
  screen.addEventListener('click', () => {
300
- focusHiddenInput();
434
+ if (DeviceMode.current === 'desktop') focusHiddenInput();
301
435
  });
302
436
 
303
437
  screen.addEventListener('mousemove', (e) => {
@@ -443,7 +577,10 @@ export function buildViewerScript() {
443
577
  return;
444
578
  }
445
579
  }
446
-
580
+
581
+ const mobileInputField = document.getElementById('input-field');
582
+ if (mobileInputField && e.target === mobileInputField) return;
583
+
447
584
  if (isComposing) return;
448
585
 
449
586
  sendUserActivity();
@@ -461,6 +598,8 @@ export function buildViewerScript() {
461
598
  document.addEventListener('keyup', (e) => {
462
599
  console.log('[Viewer] keyup, key:', e.key);
463
600
  if (isComposing && e.target === hiddenInput) return;
601
+ const mobileInputField = document.getElementById('input-field');
602
+ if (mobileInputField && e.target === mobileInputField) return;
464
603
 
465
604
  safeSend(JSON.stringify({
466
605
  type: 'keyboard_up',
@@ -468,74 +607,691 @@ export function buildViewerScript() {
468
607
  }));
469
608
  });
470
609
 
471
- screen.addEventListener('touchstart', (e) => {
472
- sendUserActivity();
473
- focusHiddenInput();
474
- const touchPoints = Array.from(e.touches).map(t => {
475
- const pos = screenToPage(t.clientX, t.clientY);
476
- return { x: pos.x, y: pos.y, id: t.identifier };
477
- });
610
+ const cursor = document.getElementById('cursor');
611
+ const touchpad = document.getElementById('touchpad');
612
+ const screenContainer = document.getElementById('screenContainer');
613
+
614
+ // Initialize modules based on detected mode
615
+ if (DeviceMode.current === 'desktop') {
616
+ DesktopModule.attach();
617
+ } else {
618
+ MobileModule.attach();
619
+ }
620
+
621
+ let cursorPos = { x: 0, y: 0 };
622
+ let dragMode = false;
623
+ let isMouseDown = false;
624
+ let lastTouchPos = null;
625
+ let touchMoved = false;
626
+ let twoFingerStartPos = null;
627
+ let longPressTimer = null;
628
+ let longPressHintTimer = null;
629
+ let cursorInitialized = false;
630
+
631
+ const CURSOR_SENSITIVITY = 1.5;
632
+ const WHEEL_SENSITIVITY = 2.0;
633
+ const LONG_PRESS_MS = 800;
634
+ const COOLDOWN_MS = 200;
635
+ const MOVE_THRESHOLD = 5;
636
+ const ACCELERATION = 0.8;
637
+ const ACCEL_MAX_VELOCITY = 3.0;
638
+
639
+ let lastMoveTime = 0;
640
+
641
+ function computeAcceleration(dx, dy) {
642
+ const now = Date.now();
643
+ const dt = now - lastMoveTime;
644
+ lastMoveTime = now;
645
+ if (dt <= 0 || dt > 200) return CURSOR_SENSITIVITY;
646
+ const dist = Math.sqrt(dx * dx + dy * dy);
647
+ const velocity = Math.min(dist / dt, ACCEL_MAX_VELOCITY);
648
+ return CURSOR_SENSITIVITY * (1 + ACCELERATION * velocity);
649
+ }
650
+
651
+ function computeWheelAccel(dx, dy) {
652
+ const now = Date.now();
653
+ const dt = now - lastMoveTime;
654
+ lastMoveTime = now;
655
+ if (dt <= 0 || dt > 200) return WHEEL_SENSITIVITY;
656
+ const dist = Math.sqrt(dx * dx + dy * dy);
657
+ const velocity = Math.min(dist / dt, ACCEL_MAX_VELOCITY);
658
+ return WHEEL_SENSITIVITY * (1 + ACCELERATION * velocity);
659
+ }
660
+
661
+ let screenRect = null;
662
+ let moveAllowed = false;
663
+ var inputMode = false;
664
+ let moveCooldownUntil = 0;
665
+ let lastWheelDeltaX = 0;
666
+ let lastWheelDeltaY = 0;
667
+ let momentumActive = false;
668
+ let keyboardVvHandler = null;
669
+ let _scrollGuard = null;
670
+
671
+ function updateScreenRect() {
672
+ screenRect = screen.getBoundingClientRect();
673
+ }
674
+
675
+ function enterInputMode(initialValue, inputType, placeholder, selector) {
676
+ if (inputMode) return;
677
+ inputMode = true;
678
+
679
+ cursor.style.display = 'none';
680
+
681
+ const ip = document.getElementById('input-panel');
682
+ const tp = document.getElementById('touchpad');
683
+
684
+ if (ip) {
685
+ ip.style.display = 'flex';
686
+ ip.style.bottom = '0px';
687
+ }
688
+ if (tp) tp.style.display = 'none';
689
+
690
+ var labelParts = [];
691
+ if (inputType) labelParts.push(inputType);
692
+ if (placeholder && placeholder !== initialValue) labelParts.push(placeholder);
693
+ var targetEl = document.getElementById('input-target');
694
+ if (targetEl) targetEl.textContent = labelParts.length > 0 ? labelParts.join(' | ') : 'input';
695
+
696
+ window._currentTargetSelector = selector || '';
697
+
698
+ var field = document.getElementById('input-field');
699
+ if (field) {
700
+ field.value = initialValue || '';
701
+ field.dataset.lastSent = initialValue || '';
702
+ }
703
+
704
+ window.scrollTo(0, 0);
705
+ document.body.scrollTop = 0;
706
+ document.documentElement.scrollTop = 0;
707
+
708
+ document.body.style.touchAction = 'none';
709
+ document.documentElement.style.touchAction = 'none';
710
+
711
+ setTimeout(function() {
712
+ if (!field) return;
713
+
714
+ // Capture original viewport height AFTER panel is visible, BEFORE focus
715
+ var origVh = window.visualViewport ? window.visualViewport.height : window.innerHeight;
716
+
717
+ // Register visualViewport listener for keyboard detection
718
+ if (window.visualViewport) {
719
+ var kbTolerance = Math.floor(window.innerHeight * 0.1);
720
+
721
+ keyboardVvHandler = function() {
722
+ if (!inputMode || !ip) return;
723
+ var currentH = window.visualViewport.height;
724
+ var kbHeight = Math.max(0, origVh - currentH);
725
+ // Fallback: use innerHeight difference
726
+ if (kbHeight < kbTolerance) {
727
+ kbHeight = Math.max(kbHeight, Math.max(0, window.innerHeight - currentH));
728
+ }
729
+ if (kbHeight > kbTolerance) {
730
+ ip.style.bottom = kbHeight + 'px';
731
+ } else {
732
+ ip.style.bottom = '0px';
733
+ }
734
+ };
735
+ window.visualViewport.addEventListener('resize', keyboardVvHandler);
736
+ }
737
+
738
+ field.focus();
739
+
740
+ window.scrollTo(0, 0);
741
+ document.body.scrollTop = 0;
742
+ document.documentElement.scrollTop = 0;
743
+
744
+ // Delayed check: wait for keyboard animation to complete (~300ms)
745
+ setTimeout(function() {
746
+ if (keyboardVvHandler) keyboardVvHandler();
747
+ window.scrollTo(0, 0);
748
+ document.body.scrollTop = 0;
749
+ document.documentElement.scrollTop = 0;
750
+ }, 350);
751
+
752
+ // Scroll guard interval: continuously fight iOS auto-scroll (reference demo)
753
+ if (!_scrollGuard) {
754
+ _scrollGuard = setInterval(function() {
755
+ if (!inputMode) return;
756
+ if (window.scrollY > 0 || document.body.scrollTop > 0 ||
757
+ document.documentElement.scrollTop > 0) {
758
+ window.scrollTo(0, 0);
759
+ document.body.scrollTop = 0;
760
+ document.documentElement.scrollTop = 0;
761
+ }
762
+ }, 100);
763
+ }
764
+
765
+ // RAF poll: reliable value-change detection for IME/CJK/clipboard/paste
766
+ var _pollField = field;
767
+ var _lastPolled = _pollField.value || '';
768
+ window._fieldComposing = false;
769
+
770
+ _pollField.addEventListener('compositionstart', function() {
771
+ window._fieldComposing = true;
772
+ });
773
+ _pollField.addEventListener('compositionend', function() {
774
+ window._fieldComposing = false;
775
+ // Double-RAF: yields current frame + next paint cycle.
776
+ // On mobile browsers (iOS Safari, Android WebView), .value may
777
+ // not be updated until 1-2 frames after compositionend fires.
778
+ requestAnimationFrame(function() {
779
+ requestAnimationFrame(function() {
780
+ syncInputToRemote(_pollField);
781
+ });
782
+ });
783
+ });
784
+
785
+ (function startPoll() {
786
+ function poll() {
787
+ if (!inputMode || !_pollField) { _inputPollRaf = null; return; }
788
+ var cur = _pollField.value;
789
+ if (cur !== _lastPolled) {
790
+ _lastPolled = cur;
791
+ if (!window._fieldComposing) {
792
+ syncInputToRemote(_pollField);
793
+ }
794
+ }
795
+ _inputPollRaf = requestAnimationFrame(poll);
796
+ }
797
+ _inputPollRaf = requestAnimationFrame(poll);
798
+ })();
799
+ }, 100);
800
+ }
801
+
802
+ function exitInputMode() {
803
+ if (!inputMode) return;
804
+ inputMode = false;
805
+
806
+ if (_inputPollRaf) { cancelAnimationFrame(_inputPollRaf); _inputPollRaf = null; }
807
+ window._fieldComposing = false;
808
+
809
+ cursor.style.display = 'block';
810
+
811
+ const field = document.getElementById('input-field');
812
+ if (field) { field.value = ''; field.blur(); delete field.dataset.lastSent; }
813
+
814
+ const ip = document.getElementById('input-panel');
815
+ const tp = document.getElementById('touchpad');
816
+
817
+ if (ip) {
818
+ ip.style.display = 'none';
819
+ ip.style.bottom = '0px';
820
+ }
821
+ if (tp) tp.style.display = DeviceMode.current === 'mobile' ? 'flex' : 'none';
822
+
823
+ // Cleanup visualViewport handler
824
+ if (keyboardVvHandler && window.visualViewport) {
825
+ window.visualViewport.removeEventListener('resize', keyboardVvHandler);
826
+ keyboardVvHandler = null;
827
+ }
828
+
829
+ // Cleanup scroll guard
830
+ if (_scrollGuard) {
831
+ clearInterval(_scrollGuard);
832
+ _scrollGuard = null;
833
+ }
834
+
835
+ // Restore touch action
836
+ document.body.style.touchAction = '';
837
+ document.documentElement.style.touchAction = '';
838
+
478
839
  safeSend(JSON.stringify({
479
- type: 'input_touch',
480
- eventType: 'touchStart',
481
- touchPoints: touchPoints
840
+ type: 'input_blur_element',
841
+ selector: window._currentTargetSelector || ''
482
842
  }));
483
- e.preventDefault();
484
- }, { passive: false });
485
-
486
- screen.addEventListener('touchmove', (e) => {
487
- const touchPoints = Array.from(e.touches).map(t => {
488
- const pos = screenToPage(t.clientX, t.clientY);
489
- return { x: pos.x, y: pos.y, id: t.identifier };
490
- });
843
+ }
844
+
845
+ var _syncDebounceTimer = null;
846
+ function syncInputToRemote(field) {
847
+ if (!field || !inputMode) return;
848
+ var current = field.value;
849
+ var lastSent = field.dataset.lastSent || '';
850
+ if (current === lastSent) return;
851
+
852
+ var isFirstSync = !field.dataset.lastSent;
853
+ function doSend() {
854
+ safeSend(JSON.stringify({
855
+ type: 'input_fill',
856
+ text: current,
857
+ selector: window._currentTargetSelector || ''
858
+ }));
859
+ field.dataset.lastSent = current;
860
+ }
861
+
862
+ if (isFirstSync) {
863
+ doSend();
864
+ } else {
865
+ clearTimeout(_syncDebounceTimer);
866
+ _syncDebounceTimer = setTimeout(doSend, 30);
867
+ }
868
+ }
869
+
870
+ function sendInputText() {
871
+ const field = document.getElementById('input-field');
872
+ if (!field || !field.value.trim()) return;
873
+
874
+ var finalText = field.value;
875
+ var sel = window._currentTargetSelector || '';
876
+
877
+ // Send final text via input_fill + Enter key
491
878
  safeSend(JSON.stringify({
492
- type: 'input_touch',
493
- eventType: 'touchMove',
494
- touchPoints: touchPoints
879
+ type: 'input_fill',
880
+ text: finalText,
881
+ selector: sel
495
882
  }));
496
- e.preventDefault();
497
- }, { passive: false });
498
-
499
- screen.addEventListener('touchend', (e) => {
500
- const touchPoints = Array.from(e.changedTouches).map(t => {
501
- const pos = screenToPage(t.clientX, t.clientY);
502
- return { x: pos.x, y: pos.y, id: t.identifier };
503
- });
504
883
  safeSend(JSON.stringify({
505
- type: 'input_touch',
506
- eventType: 'touchEnd',
507
- touchPoints: touchPoints
884
+ type: 'input_keyboard',
885
+ eventType: 'keyDown',
886
+ key: 'Enter',
887
+ code: 'Enter',
888
+ modifiers: 0,
889
+ selector: sel
508
890
  }));
509
- e.preventDefault();
510
- }, { passive: false });
511
-
512
- function addTab(index, url, title, active) {
513
- let tab = document.getElementById('tab-' + index);
514
- if (!tab) {
515
- tab = document.createElement('button');
516
- tab.id = 'tab-' + index;
517
- tab.className = 'tab';
518
- tab.onclick = () => {
519
- safeSend(JSON.stringify({ id: 'tab-' + Date.now(), action: 'tab_switch', index }));
520
- };
521
- tabsContainer.appendChild(tab);
522
- }
523
- tab.textContent = title || url || 'New Tab';
524
- tab.title = url;
525
- tab.classList.toggle('active', active);
891
+ safeSend(JSON.stringify({
892
+ type: 'input_keyboard',
893
+ eventType: 'keyUp',
894
+ key: 'Enter',
895
+ code: 'Enter',
896
+ modifiers: 0,
897
+ selector: sel
898
+ }));
899
+
900
+ field.value = '';
901
+ exitInputMode();
526
902
  }
527
-
528
- function removeTab(index) {
529
- const tab = document.getElementById('tab-' + index);
530
- if (tab) tab.remove();
903
+
904
+ function startMomentum() {
905
+ momentumActive = true;
906
+ var frame = function() {
907
+ if (!momentumActive) return;
908
+ lastWheelDeltaX *= 0.92;
909
+ lastWheelDeltaY *= 0.92;
910
+ if (Math.abs(lastWheelDeltaX) < 0.5 && Math.abs(lastWheelDeltaY) < 0.5) {
911
+ momentumActive = false;
912
+ return;
913
+ }
914
+ var pagePos = screenToPage(cursorPos.x, cursorPos.y);
915
+ safeSend(JSON.stringify({
916
+ type: 'input_mouse',
917
+ eventType: 'mouseWheel',
918
+ x: pagePos.x,
919
+ y: pagePos.y,
920
+ deltaX: lastWheelDeltaX,
921
+ deltaY: lastWheelDeltaY,
922
+ modifiers: 0
923
+ }));
924
+ requestAnimationFrame(frame);
925
+ };
926
+ requestAnimationFrame(frame);
927
+ }
928
+
929
+ function initCursor() {
930
+ updateScreenRect();
931
+ cursorPos = { x: screenRect.left + screenRect.width / 2, y: screenRect.top + screenRect.height / 2 };
932
+ updateCursor();
933
+ cursor.style.display = 'block';
934
+ }
935
+
936
+ function updateCursor() {
937
+ cursor.style.left = cursorPos.x + 'px';
938
+ cursor.style.top = cursorPos.y + 'px';
939
+ }
940
+
941
+ function clampCursor(val, min, max) {
942
+ return Math.max(min, Math.min(max, val));
943
+ }
944
+
945
+ function setCursorMode(mode) {
946
+ cursor.className = '';
947
+ if (mode === 'move') cursor.classList.add('cursor-move');
948
+ else if (mode === 'drag') cursor.classList.add('cursor-drag');
949
+ else if (mode === 'longpress') cursor.classList.add('cursor-longpress');
950
+ }
951
+
952
+ function showModeBadge(text, color) {
953
+ var badge = document.getElementById('modeBadge');
954
+ if (!badge) return;
955
+ badge.textContent = text;
956
+ badge.style.background = color;
957
+ badge.style.display = 'block';
958
+ }
959
+
960
+ function hideModeBadge() {
961
+ var badge = document.getElementById('modeBadge');
962
+ if (!badge) return;
963
+ badge.style.display = 'none';
531
964
  }
965
+
966
+ // Touchpad toolbar setup (always available when mobile module is active)
967
+ var _toolbarSetupDone = false;
968
+ function setupToolbar() {
969
+ if (_toolbarSetupDone) return;
970
+ _toolbarSetupDone = true;
971
+ var toolbar = document.getElementById('touchpadToolbar');
972
+ if (toolbar) {
973
+ toolbar.addEventListener('click', function(e) {
974
+ var btn = e.target.closest ? e.target.closest('.tpk-key') : null;
975
+ if (!btn) return;
976
+ e.preventDefault();
977
+ e.stopPropagation();
978
+ sendUserActivity();
979
+ var key = btn.dataset.key || '';
980
+ var code = btn.dataset.code || '';
981
+ safeSend(JSON.stringify({ type: 'input_keyboard', eventType: 'keyDown', key: key, code: code, modifiers: 0 }));
982
+ safeSend(JSON.stringify({ type: 'input_keyboard', eventType: 'keyUp', key: key, code: code, modifiers: 0 }));
983
+ });
984
+
985
+ var expandBtn = document.getElementById('tpkExpand');
986
+ var collapseBtn = document.getElementById('tpkCollapse');
987
+ if (expandBtn) expandBtn.addEventListener('click', function(e) { e.stopPropagation(); toolbar.classList.remove('collapsed'); });
988
+ if (collapseBtn) collapseBtn.addEventListener('click', function(e) { e.stopPropagation(); toolbar.classList.add('collapsed'); });
989
+ }
990
+ }
991
+
992
+ // Touch event handlers (always registered; guarded by DeviceMode.current)
993
+ touchpad.addEventListener('touchstart', (e) => {
994
+ if (DeviceMode.current !== 'mobile') return;
995
+ e.preventDefault();
996
+ sendUserActivity();
997
+
998
+ if (e.touches.length === 2) {
999
+ clearTimeout(longPressTimer);
1000
+ longPressTimer = null;
1001
+ dragMode = false;
1002
+ moveAllowed = false;
1003
+ lastTouchPos = null;
1004
+ momentumActive = false;
1005
+ setCursorMode(null);
1006
+ hideModeBadge();
1007
+ const t0 = e.touches[0];
1008
+ const t1 = e.touches[1];
1009
+ twoFingerStartPos = {
1010
+ lastMidX: (t0.clientX + t1.clientX) / 2,
1011
+ lastMidY: (t0.clientY + t1.clientY) / 2,
1012
+ };
1013
+ lastMoveTime = Date.now();
1014
+ return;
1015
+ }
1016
+
1017
+ if (e.touches.length === 1) {
1018
+ if (Date.now() < moveCooldownUntil) return;
1019
+ moveAllowed = true;
1020
+ setCursorMode('move');
1021
+ showModeBadge('MOVE', 'rgba(68, 140, 255, 0.7)');
1022
+ const t = e.touches[0];
1023
+ lastTouchPos = { x: t.clientX, y: t.clientY };
1024
+ lastMoveTime = Date.now();
1025
+ touchMoved = false;
1026
+
1027
+ clearTimeout(longPressTimer);
1028
+ clearTimeout(longPressHintTimer);
1029
+ longPressTimer = setTimeout(() => {
1030
+ longPressTimer = null;
1031
+ longPressHintTimer = null;
1032
+ dragMode = true;
1033
+ isMouseDown = true;
1034
+ touchMoved = true;
1035
+ setCursorMode('drag');
1036
+ showModeBadge('DRAG', 'rgba(255, 165, 0, 0.8)');
1037
+ const pagePos = screenToPage(cursorPos.x, cursorPos.y);
1038
+ safeSend(JSON.stringify({
1039
+ type: 'input_mouse',
1040
+ eventType: 'mousePressed',
1041
+ x: pagePos.x,
1042
+ y: pagePos.y,
1043
+ button: 'left',
1044
+ clickCount: 1,
1045
+ modifiers: 0
1046
+ }));
1047
+ }, LONG_PRESS_MS);
1048
+ }
1049
+ }, { passive: false });
1050
+
1051
+ touchpad.addEventListener('touchend', (e) => {
1052
+ if (DeviceMode.current !== 'mobile') return;
1053
+ e.preventDefault();
1054
+ clearTimeout(longPressTimer);
1055
+ clearTimeout(longPressHintTimer);
1056
+ if (e.touches.length === 0) {
1057
+ if (twoFingerStartPos) {
1058
+ moveCooldownUntil = Date.now() + COOLDOWN_MS;
1059
+ if (Math.abs(lastWheelDeltaX) > 2 || Math.abs(lastWheelDeltaY) > 2) {
1060
+ startMomentum();
1061
+ }
1062
+ }
1063
+ moveAllowed = false;
1064
+ setCursorMode(null);
1065
+ hideModeBadge();
1066
+ if (dragMode) {
1067
+ const pagePos = screenToPage(cursorPos.x, cursorPos.y);
1068
+ safeSend(JSON.stringify({
1069
+ type: 'input_mouse',
1070
+ eventType: 'mouseReleased',
1071
+ x: pagePos.x,
1072
+ y: pagePos.y,
1073
+ button: 'left',
1074
+ clickCount: 1,
1075
+ modifiers: 0
1076
+ }));
1077
+ dragMode = false;
1078
+ isMouseDown = false;
1079
+ } else if (!touchMoved) {
1080
+ // Single tap/click: send click event, then attempt focus
1081
+ const pagePos = screenToPage(cursorPos.x, cursorPos.y);
1082
+
1083
+ safeSend(JSON.stringify({
1084
+ type: 'input_mouse',
1085
+ eventType: 'mousePressed',
1086
+ x: pagePos.x,
1087
+ y: pagePos.y,
1088
+ button: 'left',
1089
+ clickCount: 1,
1090
+ modifiers: 0
1091
+ }));
1092
+ safeSend(JSON.stringify({
1093
+ type: 'input_mouse',
1094
+ eventType: 'mouseReleased',
1095
+ x: pagePos.x,
1096
+ y: pagePos.y,
1097
+ button: 'left',
1098
+ clickCount: 1,
1099
+ modifiers: 0
1100
+ }));
1101
+
1102
+
1103
+ }
1104
+ lastTouchPos = null;
1105
+ twoFingerStartPos = null;
1106
+ touchMoved = false;
1107
+ }
1108
+ }, { passive: false });
1109
+
1110
+ touchpad.addEventListener('touchmove', (e) => {
1111
+ if (DeviceMode.current !== 'mobile') return;
1112
+ e.preventDefault();
1113
+
1114
+ if (e.touches.length === 2 && twoFingerStartPos) {
1115
+ clearTimeout(longPressTimer);
1116
+ longPressTimer = null;
1117
+ dragMode = false;
1118
+ moveAllowed = false;
1119
+ lastTouchPos = null;
1120
+ const t0 = e.touches[0];
1121
+ const t1 = e.touches[1];
1122
+ const midX = (t0.clientX + t1.clientX) / 2;
1123
+ const midY = (t0.clientY + t1.clientY) / 2;
1124
+ const rawDX = midX - twoFingerStartPos.lastMidX;
1125
+ const rawDY = midY - twoFingerStartPos.lastMidY;
1126
+ const wMult = computeWheelAccel(rawDX, rawDY);
1127
+ const deltaX = rawDX * wMult;
1128
+ const deltaY = rawDY * wMult;
1129
+ lastWheelDeltaX = deltaX;
1130
+ lastWheelDeltaY = deltaY;
1131
+ const pagePos = screenToPage(cursorPos.x, cursorPos.y);
1132
+ safeSend(JSON.stringify({
1133
+ type: 'input_mouse',
1134
+ eventType: 'mouseWheel',
1135
+ x: pagePos.x,
1136
+ y: pagePos.y,
1137
+ deltaX: deltaX,
1138
+ deltaY: deltaY,
1139
+ modifiers: 0
1140
+ }));
1141
+ twoFingerStartPos.lastMidX = midX;
1142
+ twoFingerStartPos.lastMidY = midY;
1143
+ return;
1144
+ }
1145
+
1146
+ if (e.touches.length === 1 && lastTouchPos && moveAllowed) {
1147
+ const t = e.touches[0];
1148
+ const dx = t.clientX - lastTouchPos.x;
1149
+ const dy = t.clientY - lastTouchPos.y;
1150
+
1151
+ if (!touchMoved && Math.sqrt(dx * dx + dy * dy) > MOVE_THRESHOLD) {
1152
+ touchMoved = true;
1153
+ clearTimeout(longPressTimer);
1154
+ clearTimeout(longPressHintTimer);
1155
+ longPressTimer = null;
1156
+ longPressHintTimer = null;
1157
+ setCursorMode('move');
1158
+ showModeBadge('MOVE', 'rgba(68, 140, 255, 0.7)');
1159
+ }
1160
+
1161
+ lastTouchPos = { x: t.clientX, y: t.clientY };
1162
+
1163
+ if (touchMoved) {
1164
+ sendUserActivity();
1165
+ if (!screenRect) updateScreenRect();
1166
+ const accel = computeAcceleration(dx, dy);
1167
+ cursorPos.x = clampCursor(cursorPos.x + dx * accel, screenRect.left, screenRect.right);
1168
+ cursorPos.y = clampCursor(cursorPos.y + dy * accel, screenRect.top, screenRect.bottom);
1169
+ updateCursor();
1170
+
1171
+ const pagePos = screenToPage(cursorPos.x, cursorPos.y);
1172
+ var dbg = document.getElementById('debug-overlay');
1173
+ if (dbg && (!window._moveDebugCount)) window._moveDebugCount = 0;
1174
+ if (dbg && window._moveDebugCount < 8) {
1175
+ window._moveDebugCount++;
1176
+ dbg.textContent += ' | cur:' + Math.round(cursorPos.x) + ',' + Math.round(cursorPos.y)
1177
+ + ' -> page:' + pagePos.x + ',' + pagePos.y;
1178
+ }
1179
+
1180
+ safeSend(JSON.stringify({
1181
+ type: 'input_mouse',
1182
+ eventType: 'mouseMoved',
1183
+ x: pagePos.x,
1184
+ y: pagePos.y,
1185
+ button: dragMode ? 'left' : 'none',
1186
+ clickCount: 1,
1187
+ modifiers: 0
1188
+ }));
1189
+ }
1190
+ }
1191
+ }, { passive: false });
1192
+
1193
+ touchpad.addEventListener('touchcancel', () => {
1194
+ if (DeviceMode.current !== 'mobile') return;
1195
+ clearTimeout(longPressTimer);
1196
+ clearTimeout(longPressHintTimer);
1197
+ momentumActive = false;
1198
+ if (dragMode) {
1199
+ const pagePos = screenToPage(cursorPos.x, cursorPos.y);
1200
+ safeSend(JSON.stringify({
1201
+ type: 'input_mouse',
1202
+ eventType: 'mouseReleased',
1203
+ x: pagePos.x,
1204
+ y: pagePos.y,
1205
+ button: 'left',
1206
+ clickCount: 1,
1207
+ modifiers: 0
1208
+ }));
1209
+ }
1210
+ dragMode = false;
1211
+ isMouseDown = false;
1212
+ moveAllowed = false;
1213
+ setCursorMode(null);
1214
+ hideModeBadge();
1215
+ moveCooldownUntil = twoFingerStartPos ? Date.now() + COOLDOWN_MS : 0;
1216
+ lastTouchPos = null;
1217
+ twoFingerStartPos = null;
1218
+ touchMoved = false;
1219
+ }, { passive: false });
532
1220
 
533
- function setActiveTab(index) {
534
- document.querySelectorAll('.tab').forEach((t, i) => {
535
- t.classList.toggle('active', t.id === 'tab-' + index);
1221
+ var inputSendBtn = document.getElementById('input-send');
1222
+ if (inputSendBtn) inputSendBtn.addEventListener('click', function(e) {
1223
+ e.preventDefault();
1224
+ e.stopPropagation();
1225
+ sendInputText();
1226
+ });
1227
+
1228
+ document.addEventListener('pointerdown', function(e) {
1229
+ if (!inputMode) return;
1230
+ var panel = document.getElementById('input-panel');
1231
+ if (panel && !panel.contains(e.target)) {
1232
+ exitInputMode();
1233
+ }
1234
+ });
1235
+
1236
+ var inputField = document.getElementById('input-field');
1237
+ if (inputField) {
1238
+ inputField.addEventListener('input', function(e) {
1239
+ if (window._fieldComposing) return;
1240
+ syncInputToRemote(inputField);
1241
+ });
1242
+ inputField.addEventListener('compositionend', function(e) {
1243
+ window._fieldComposing = false;
1244
+ requestAnimationFrame(function() {
1245
+ requestAnimationFrame(function() {
1246
+ syncInputToRemote(inputField);
1247
+ });
1248
+ });
1249
+ });
1250
+ inputField.addEventListener('keydown', function(e) {
1251
+ if (e.key === 'Enter') {
1252
+ e.preventDefault();
1253
+ sendInputText();
1254
+ } else if (e.key === 'Escape') {
1255
+ e.preventDefault();
1256
+ exitInputMode();
1257
+ } else if (e.key === 'Backspace' || e.key === 'Delete') {
1258
+ // Let it fall through - the field value will change,
1259
+ // then syncInputToRemote will pick up the change and send input_fill
1260
+ }
536
1261
  });
537
1262
  }
538
1263
 
1264
+ // Image sizing: re-fit on container resize (phone rotation, window resize)
1265
+ var resizeTimer = null;
1266
+ window.addEventListener('resize', () => {
1267
+ clearTimeout(resizeTimer);
1268
+ resizeTimer = setTimeout(function() {
1269
+ fitImageToContainer();
1270
+ var newMode = detectDeviceMode();
1271
+ if (newMode !== DeviceMode.current) DeviceMode.switchTo(newMode);
1272
+ }, 100);
1273
+ });
1274
+
1275
+ window.addEventListener('orientationchange', () => {
1276
+ setTimeout(function() {
1277
+ var newMode = detectDeviceMode();
1278
+ if (newMode !== DeviceMode.current) DeviceMode.switchTo(newMode);
1279
+ }, 200);
1280
+ });
1281
+
1282
+ // matchMedia pointer:coarse as additional trigger
1283
+ if (window.matchMedia) {
1284
+ try {
1285
+ var mql = window.matchMedia('(pointer:coarse)');
1286
+ if (mql && typeof mql.addEventListener === 'function') {
1287
+ mql.addEventListener('change', function(e) {
1288
+ var newMode = e.matches ? 'mobile' : 'desktop';
1289
+ DeviceMode.switchTo(newMode);
1290
+ });
1291
+ }
1292
+ } catch(err) {}
1293
+ }
1294
+
539
1295
  // Recorder functionality
540
1296
  const recordBtn = document.getElementById('recordBtn');
541
1297
  const recordText = document.getElementById('recordText');
@@ -579,7 +1335,7 @@ export function buildViewerScript() {
579
1335
  }
580
1336
  });
581
1337
 
582
- focusHiddenInput();
1338
+ if (DeviceMode.current === 'desktop') focusHiddenInput();
583
1339
  connect();
584
1340
  `;
585
1341
  }