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