forge-jsxy 1.0.68 → 1.0.70

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.
@@ -0,0 +1,919 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Forge Remote Control</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: dark;
10
+ --vscode-editor-background: #1e1e1e;
11
+ --vscode-editor-foreground: #cccccc;
12
+ --vscode-sideBar-background: #252526;
13
+ --vscode-panel-border: #3e3e42;
14
+ --vscode-button-background: #0078d4;
15
+ --vscode-button-hoverBackground: #1a8cff;
16
+ --vscode-button-foreground: #ffffff;
17
+ --vscode-button-secondaryBackground: #3a3d41;
18
+ --vscode-button-secondaryHoverBackground: #4c4d51;
19
+ --vscode-button-secondaryForeground: #c5c5c5;
20
+ --vscode-input-background: #3c3c3c;
21
+ --vscode-input-foreground: #cccccc;
22
+ --vscode-input-border: #3c3c3c;
23
+ --vscode-focusBorder: #0078d4;
24
+ --vscode-errorForeground: #f48771;
25
+ }
26
+ * { box-sizing: border-box; }
27
+ html, body { height: 100%; }
28
+ body {
29
+ margin: 0;
30
+ font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
31
+ background: var(--vscode-editor-background);
32
+ color: var(--vscode-editor-foreground);
33
+ }
34
+ .bar {
35
+ position: fixed;
36
+ left: 0;
37
+ right: 0;
38
+ bottom: 0;
39
+ z-index: 10;
40
+ display: flex;
41
+ flex-wrap: wrap;
42
+ gap: 6px 8px;
43
+ align-items: center;
44
+ min-height: 44px;
45
+ padding: 8px 12px;
46
+ background: #181818;
47
+ border-top: 1px solid var(--vscode-panel-border);
48
+ }
49
+ .bar input, .bar button { font: inherit; }
50
+ .bar input {
51
+ background: var(--vscode-input-background);
52
+ color: var(--vscode-input-foreground);
53
+ border: 1px solid var(--vscode-input-border);
54
+ border-radius: 4px;
55
+ padding: 5px 8px;
56
+ }
57
+ .bar button {
58
+ background: var(--vscode-button-background);
59
+ color: var(--vscode-button-foreground);
60
+ border: 1px solid transparent;
61
+ border-radius: 4px;
62
+ min-height: 28px;
63
+ padding: 4px 12px;
64
+ cursor: pointer;
65
+ font-size: 12px;
66
+ font-weight: 500;
67
+ }
68
+ .bar button:hover:not(:disabled) { background: var(--vscode-button-hoverBackground); }
69
+ .bar button.alt {
70
+ background: var(--vscode-button-secondaryBackground);
71
+ color: var(--vscode-button-secondaryForeground);
72
+ border-color: #505050;
73
+ }
74
+ .bar button.alt:hover:not(:disabled) { background: var(--vscode-button-secondaryHoverBackground); }
75
+ .bar button.warn {
76
+ background: #5a1d1d;
77
+ color: #ffd9d9;
78
+ border-color: #7c2b2b;
79
+ }
80
+ .bar button.warn:hover:not(:disabled) { background: #6c2626; }
81
+ .bar button:disabled { opacity: 0.45; cursor: not-allowed; }
82
+ .bar button:focus-visible, .bar input:focus-visible {
83
+ outline: none;
84
+ box-shadow: 0 0 0 1px var(--vscode-editor-background), 0 0 0 3px rgba(0, 120, 212, 0.45);
85
+ }
86
+ .spacer { flex: 1; min-width: 8px; }
87
+ .state { opacity: .92; font-size: 11px; color: #b7b7b7; }
88
+ .brand { font-weight: 600; letter-spacing: 0.02em; }
89
+ .hint { color: #9d9d9d; }
90
+ .screen-wrap {
91
+ position: fixed;
92
+ inset: 0 0 58px 0;
93
+ overflow: hidden;
94
+ background: #111;
95
+ display: grid;
96
+ place-items: center;
97
+ }
98
+ .screen-stage {
99
+ width: 100%;
100
+ height: 100%;
101
+ display: grid;
102
+ place-items: center;
103
+ padding: 10px;
104
+ position: relative;
105
+ overflow: hidden;
106
+ }
107
+ .screen {
108
+ display: block;
109
+ max-width: calc(100vw - 20px);
110
+ max-height: calc(100vh - 86px);
111
+ width: auto;
112
+ height: auto;
113
+ user-select: none;
114
+ cursor: default;
115
+ margin: 0;
116
+ }
117
+ .screen.write-enabled { cursor: crosshair; }
118
+ .empty-state {
119
+ position: absolute;
120
+ inset: 0;
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ padding: 16px;
125
+ pointer-events: none;
126
+ }
127
+ .empty-state-card {
128
+ max-width: min(680px, 92vw);
129
+ text-align: center;
130
+ background: rgba(24, 24, 24, 0.88);
131
+ border: 1px solid #3e3e42;
132
+ border-radius: 8px;
133
+ padding: 14px 16px;
134
+ color: #d4d4d4;
135
+ font-size: 13px;
136
+ line-height: 1.45;
137
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
138
+ }
139
+ .empty-state.error .empty-state-card {
140
+ border-color: #7c2b2b;
141
+ color: #ffd8d8;
142
+ background: rgba(44, 18, 18, 0.9);
143
+ }
144
+ .file-panel { position: fixed; right: 8px; bottom: 66px; width: 380px; max-height: 60vh; overflow: auto; background: #1b1b1d; border: 1px solid #3e3e42; border-radius: 6px; padding: 8px; display: none; z-index: 9; }
145
+ .file-panel.open { display: block; }
146
+ .file-panel .row { display: flex; gap: 6px; margin-bottom: 6px; }
147
+ .file-panel button { font: inherit; }
148
+ .file-panel input { flex: 1; min-width: 0; background: var(--vscode-input-background); color: var(--vscode-input-foreground); border: 1px solid var(--vscode-input-border); border-radius: 4px; padding: 5px 8px; }
149
+ .file-list { border: 1px solid #3e3e42; border-radius: 4px; padding: 4px; max-height: 35vh; overflow: auto; }
150
+ .file-item { display: block; width: 100%; text-align: left; background: #2a2a2d; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 4px; padding: 4px 6px; margin: 3px 0; cursor: pointer; }
151
+ .file-item.dir { background: #253045; }
152
+ .path-label { font-size: 12px; opacity: 0.85; word-break: break-all; }
153
+ .auth-modal {
154
+ position: fixed;
155
+ inset: 0;
156
+ z-index: 30;
157
+ display: none;
158
+ align-items: center;
159
+ justify-content: center;
160
+ background: rgba(0, 0, 0, 0.45);
161
+ }
162
+ .auth-modal.open { display: flex; }
163
+ .auth-card {
164
+ width: min(420px, 92vw);
165
+ background: #1f1f1f;
166
+ border: 1px solid #3e3e42;
167
+ border-radius: 8px;
168
+ padding: 12px;
169
+ box-shadow: 0 16px 32px rgba(0, 0, 0, 0.45);
170
+ }
171
+ .auth-title { font-size: 13px; font-weight: 600; margin: 0 0 8px 0; }
172
+ .auth-hint { font-size: 12px; color: #b7b7b7; margin: 0 0 8px 0; }
173
+ .auth-row { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
174
+ .auth-row input[type="password"] {
175
+ flex: 1;
176
+ min-width: 0;
177
+ background: var(--vscode-input-background);
178
+ color: var(--vscode-input-foreground);
179
+ border: 1px solid var(--vscode-input-border);
180
+ border-radius: 4px;
181
+ padding: 6px 8px;
182
+ font: inherit;
183
+ }
184
+ .auth-row button {
185
+ background: var(--vscode-button-background);
186
+ color: var(--vscode-button-foreground);
187
+ border: 1px solid transparent;
188
+ border-radius: 4px;
189
+ padding: 6px 10px;
190
+ cursor: pointer;
191
+ font: inherit;
192
+ }
193
+ .auth-row button.sec {
194
+ background: var(--vscode-button-secondaryBackground);
195
+ color: var(--vscode-button-secondaryForeground);
196
+ border-color: #505050;
197
+ }
198
+ </style>
199
+ </head>
200
+ <body>
201
+ <div class="bar">
202
+ <strong class="brand">Remote</strong>
203
+ <button id="modeBtn" class="alt">View Only</button>
204
+ <button id="refreshBtn" class="alt">Refresh</button>
205
+ <button id="browseBtn" class="alt">Files</button>
206
+ <button id="disconnectBtn" class="warn">Disconnect</button>
207
+ <span class="spacer"></span>
208
+ <span class="state hint">Shortcuts: Ctrl/Cmd+C (PC -> Local), Ctrl/Cmd+V (Local -> PC)</span>
209
+ <span class="state" id="state">Idle</span>
210
+ <span class="state" id="modeState">Mode: View Only</span>
211
+ </div>
212
+ <div class="screen-wrap" id="screenWrap">
213
+ <div class="screen-stage" id="screenStage">
214
+ <img id="screen" class="screen" alt="Remote screen" />
215
+ <div id="emptyState" class="empty-state">
216
+ <div id="emptyStateCard" class="empty-state-card">
217
+ Waiting for remote session...
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ <div class="file-panel" id="filePanel">
223
+ <div class="row">
224
+ <button id="rootsBtn" class="alt">Roots</button>
225
+ <button id="upBtn" class="alt">Up</button>
226
+ <button id="closePanelBtn" class="warn">Close</button>
227
+ </div>
228
+ <div class="path-label" id="pathLabel">Path: (none)</div>
229
+ <div class="row">
230
+ <input id="filePullPath" placeholder="Remote file path" />
231
+ <button id="filePullBtn" class="alt">Fetch <- PC</button>
232
+ </div>
233
+ <div class="row">
234
+ <input id="filePushInput" type="file" />
235
+ <button id="filePushBtn" class="alt">Send -> PC</button>
236
+ </div>
237
+ <div class="file-list" id="fileList"></div>
238
+ </div>
239
+ <div class="auth-modal" id="authModal">
240
+ <div class="auth-card">
241
+ <p class="auth-title">Remote session password required</p>
242
+ <p class="auth-hint" id="authHint">Enter the password for this remote session.</p>
243
+ <div class="auth-row">
244
+ <input id="authPasswordInput" type="password" placeholder="Session password" />
245
+ </div>
246
+ <div class="auth-row">
247
+ <button id="authSubmitBtn">Continue</button>
248
+ <button id="authCancelBtn" class="sec">Cancel</button>
249
+ </div>
250
+ </div>
251
+ </div>
252
+ <script>
253
+ const relayFallback = @@RELAY_FALLBACK_JS@@ || "";
254
+ const pwdHint = @@PWD_JS@@ || "";
255
+ const stateEl = document.getElementById("state");
256
+ const modeStateEl = document.getElementById("modeState");
257
+ const screenEl = document.getElementById("screen");
258
+ const emptyStateEl = document.getElementById("emptyState");
259
+ const emptyStateCardEl = document.getElementById("emptyStateCard");
260
+ const modeBtn = document.getElementById("modeBtn");
261
+ const wrapEl = document.getElementById("screenWrap");
262
+ const browseBtn = document.getElementById("browseBtn");
263
+ const filePullPath = document.getElementById("filePullPath");
264
+ const filePullBtn = document.getElementById("filePullBtn");
265
+ const filePushInput = document.getElementById("filePushInput");
266
+ const filePushBtn = document.getElementById("filePushBtn");
267
+ const filePanel = document.getElementById("filePanel");
268
+ const rootsBtn = document.getElementById("rootsBtn");
269
+ const upBtn = document.getElementById("upBtn");
270
+ const closePanelBtn = document.getElementById("closePanelBtn");
271
+ const pathLabel = document.getElementById("pathLabel");
272
+ const fileList = document.getElementById("fileList");
273
+ const authModal = document.getElementById("authModal");
274
+ const authHint = document.getElementById("authHint");
275
+ const authPasswordInput = document.getElementById("authPasswordInput");
276
+ const authSubmitBtn = document.getElementById("authSubmitBtn");
277
+ const authCancelBtn = document.getElementById("authCancelBtn");
278
+ let ws = null;
279
+ let authed = false;
280
+ let writeEnabled = false;
281
+ let screenshotTimer = null;
282
+ let reqSeq = 0;
283
+ let inflightShot = false;
284
+ const pendingReqs = new Map();
285
+ let remoteClipboardBusy = false;
286
+ let localClipboardBusy = false;
287
+ let currentBrowsePath = "";
288
+ let reconnectTimer = null;
289
+ let pendingPasswordPrompt = null;
290
+ let hasFrame = false;
291
+ let authWatchdogTimer = null;
292
+ let authChallengeSeen = false;
293
+
294
+ function setState(t) { stateEl.textContent = t; }
295
+ function sha256HexFallback(input) {
296
+ const msg = unescape(encodeURIComponent(String(input || "")));
297
+ const bytes = new Uint8Array(msg.length);
298
+ for (let i = 0; i < msg.length; i++) bytes[i] = msg.charCodeAt(i);
299
+ const K = new Uint32Array([
300
+ 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
301
+ 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
302
+ 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
303
+ 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
304
+ 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
305
+ 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
306
+ 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
307
+ 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
308
+ ]);
309
+ const H = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]);
310
+ const bitLen = bytes.length * 8;
311
+ const totalLen = ((bytes.length + 9 + 63) >> 6) << 6;
312
+ const data = new Uint8Array(totalLen);
313
+ data.set(bytes);
314
+ data[bytes.length] = 0x80;
315
+ const view = new DataView(data.buffer);
316
+ view.setUint32(totalLen - 8, Math.floor(bitLen / 0x100000000), false);
317
+ view.setUint32(totalLen - 4, bitLen >>> 0, false);
318
+ const w = new Uint32Array(64);
319
+ const rr = (x, n) => (x >>> n) | (x << (32 - n));
320
+ for (let i = 0; i < totalLen; i += 64) {
321
+ for (let t = 0; t < 16; t++) w[t] = view.getUint32(i + t * 4, false);
322
+ for (let t = 16; t < 64; t++) {
323
+ const s0 = rr(w[t - 15], 7) ^ rr(w[t - 15], 18) ^ (w[t - 15] >>> 3);
324
+ const s1 = rr(w[t - 2], 17) ^ rr(w[t - 2], 19) ^ (w[t - 2] >>> 10);
325
+ w[t] = (((w[t - 16] + s0) >>> 0) + ((w[t - 7] + s1) >>> 0)) >>> 0;
326
+ }
327
+ let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
328
+ for (let t = 0; t < 64; t++) {
329
+ const S1 = rr(e, 6) ^ rr(e, 11) ^ rr(e, 25);
330
+ const ch = (e & f) ^ (~e & g);
331
+ const t1 = (((((h + S1) >>> 0) + ((ch + K[t]) >>> 0)) >>> 0) + w[t]) >>> 0;
332
+ const S0 = rr(a, 2) ^ rr(a, 13) ^ rr(a, 22);
333
+ const maj = (a & b) ^ (a & c) ^ (b & c);
334
+ const t2 = (S0 + maj) >>> 0;
335
+ h = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
336
+ }
337
+ H[0] = (H[0] + a) >>> 0; H[1] = (H[1] + b) >>> 0; H[2] = (H[2] + c) >>> 0; H[3] = (H[3] + d) >>> 0;
338
+ H[4] = (H[4] + e) >>> 0; H[5] = (H[5] + f) >>> 0; H[6] = (H[6] + g) >>> 0; H[7] = (H[7] + h) >>> 0;
339
+ }
340
+ return Array.from(H).map((n) => n.toString(16).padStart(8, "0")).join("");
341
+ }
342
+ function showEmptyState(msg, isError) {
343
+ emptyStateCardEl.textContent = String(msg || "Waiting for screenshot...");
344
+ emptyStateEl.classList.toggle("error", Boolean(isError));
345
+ emptyStateEl.style.display = "flex";
346
+ }
347
+ function hideEmptyState() {
348
+ emptyStateEl.style.display = "none";
349
+ emptyStateEl.classList.remove("error");
350
+ }
351
+ function updateWriteControls() {
352
+ const ro = !writeEnabled;
353
+ filePullBtn.disabled = ro;
354
+ filePullPath.disabled = ro;
355
+ filePushBtn.disabled = ro;
356
+ filePushInput.disabled = ro;
357
+ browseBtn.disabled = ro;
358
+ rootsBtn.disabled = ro;
359
+ upBtn.disabled = ro;
360
+ closePanelBtn.disabled = ro;
361
+ if (ro) filePanel.classList.remove("open");
362
+ }
363
+ function hashHex(s) {
364
+ if (globalThis.crypto && globalThis.crypto.subtle && typeof TextEncoder !== "undefined") {
365
+ return crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)).then((buf) =>
366
+ [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("")
367
+ );
368
+ }
369
+ return Promise.resolve(sha256HexFallback(s));
370
+ }
371
+ function wsBaseUrl() {
372
+ if (location.protocol.startsWith("http")) {
373
+ const wsProto = location.protocol === "https:" ? "wss:" : "ws:";
374
+ return wsProto + "//" + location.host;
375
+ }
376
+ return relayFallback || "ws://127.0.0.1:9877";
377
+ }
378
+ function currentSessionId() {
379
+ return String(new URLSearchParams(location.search).get("session") || "").trim();
380
+ }
381
+ function resolveSessionId() {
382
+ const sid = currentSessionId();
383
+ if (sid) return sid;
384
+ const entered = String(window.prompt("Enter remote session id", "") || "").trim();
385
+ return entered;
386
+ }
387
+ function sessionPwdKey(sid) {
388
+ return "forge_remote_pwd_" + sid;
389
+ }
390
+ function readRememberedPassword(sid) {
391
+ if (!sid) return "";
392
+ try { return String(localStorage.getItem(sessionPwdKey(sid)) || ""); } catch { return ""; }
393
+ }
394
+ function readDashboardPassword() {
395
+ try { return String(localStorage.getItem("forge_dash_pwd") || ""); } catch { return ""; }
396
+ }
397
+ function rememberPassword(sid, pw) {
398
+ if (!sid || !pw) return;
399
+ try { localStorage.setItem(sessionPwdKey(sid), pw); } catch {}
400
+ }
401
+ function forgetPassword(sid) {
402
+ if (!sid) return;
403
+ try { localStorage.removeItem(sessionPwdKey(sid)); } catch {}
404
+ }
405
+ function askPassword(reason) {
406
+ if (pendingPasswordPrompt) return pendingPasswordPrompt;
407
+ pendingPasswordPrompt = new Promise((resolve) => {
408
+ authHint.textContent = String(reason || "Enter remote session password");
409
+ authPasswordInput.value = "";
410
+ authModal.classList.add("open");
411
+ setTimeout(() => authPasswordInput.focus(), 0);
412
+ const close = (value) => {
413
+ authModal.classList.remove("open");
414
+ authSubmitBtn.onclick = null;
415
+ authCancelBtn.onclick = null;
416
+ authPasswordInput.onkeydown = null;
417
+ pendingPasswordPrompt = null;
418
+ resolve(String(value || "").trim());
419
+ };
420
+ authSubmitBtn.onclick = () => close(authPasswordInput.value);
421
+ authCancelBtn.onclick = () => close("");
422
+ authPasswordInput.onkeydown = (ev) => {
423
+ if (ev.key === "Enter") {
424
+ ev.preventDefault();
425
+ close(authPasswordInput.value);
426
+ return;
427
+ }
428
+ if (ev.key === "Escape") {
429
+ ev.preventDefault();
430
+ close("");
431
+ }
432
+ };
433
+ });
434
+ return pendingPasswordPrompt;
435
+ }
436
+ async function resolveSessionPassword(sid, forcePrompt) {
437
+ if (forcePrompt) {
438
+ const entered = await askPassword("Remote session password required");
439
+ if (entered) rememberPassword(sid, entered);
440
+ return entered;
441
+ }
442
+ const fromUrl = String(new URLSearchParams(location.search).get("password") || "").trim();
443
+ if (fromUrl) {
444
+ rememberPassword(sid, fromUrl);
445
+ return fromUrl;
446
+ }
447
+ const remembered = String(readRememberedPassword(sid) || "").trim();
448
+ if (remembered) return remembered;
449
+ const dashboardPw = String(readDashboardPassword() || "").trim();
450
+ if (dashboardPw) {
451
+ rememberPassword(sid, dashboardPw);
452
+ return dashboardPw;
453
+ }
454
+ const fallback = String(pwdHint || "").trim();
455
+ if (fallback) return fallback;
456
+ const entered = await askPassword("Remote session password required");
457
+ if (entered) rememberPassword(sid, entered);
458
+ return entered;
459
+ }
460
+ function scheduleReconnect() {
461
+ if (reconnectTimer) return;
462
+ reconnectTimer = setTimeout(() => {
463
+ reconnectTimer = null;
464
+ if (ws || authed) return;
465
+ if (!currentSessionId()) return;
466
+ connect();
467
+ }, 2500);
468
+ }
469
+ function clearAuthWatchdog() {
470
+ if (authWatchdogTimer) {
471
+ clearTimeout(authWatchdogTimer);
472
+ authWatchdogTimer = null;
473
+ }
474
+ }
475
+ function armAuthWatchdog() {
476
+ clearAuthWatchdog();
477
+ authWatchdogTimer = setTimeout(() => {
478
+ if (!ws || ws.readyState !== 1 || authed) return;
479
+ if (authChallengeSeen) return;
480
+ setState("Handshake stalled — reconnecting...");
481
+ if (!hasFrame) {
482
+ showEmptyState(
483
+ "Connected to relay, but the session handshake is stalled. Retrying automatically...",
484
+ true
485
+ );
486
+ }
487
+ disconnect();
488
+ setTimeout(connect, 200);
489
+ }, 6500);
490
+ }
491
+ function connect() {
492
+ const sid = resolveSessionId();
493
+ if (!sid) { setState("Session required"); return; }
494
+ const url = wsBaseUrl().replace(/\/+$/, "") + "/ws/viewer/" + encodeURIComponent(sid);
495
+ disconnect();
496
+ hasFrame = false;
497
+ authChallengeSeen = false;
498
+ screenEl.removeAttribute("src");
499
+ setState("Connecting…");
500
+ showEmptyState("Connecting to remote session...", false);
501
+ ws = new WebSocket(url);
502
+ ws.onopen = () => {
503
+ setState("Connected — waiting auth");
504
+ if (!hasFrame) showEmptyState("Connected. Waiting for auth challenge...", false);
505
+ ws.send(JSON.stringify({ type: "get_info" }));
506
+ armAuthWatchdog();
507
+ };
508
+ ws.onclose = () => {
509
+ authed = false;
510
+ setState("Disconnected");
511
+ stopShotLoop();
512
+ clearAuthWatchdog();
513
+ if (!hasFrame) showEmptyState("Disconnected. Reconnecting...", true);
514
+ scheduleReconnect();
515
+ };
516
+ ws.onerror = () => {
517
+ setState("Socket error");
518
+ clearAuthWatchdog();
519
+ if (!hasFrame) showEmptyState("Network/socket error while connecting to remote screen.", true);
520
+ };
521
+ ws.onmessage = async (ev) => {
522
+ let msg = null;
523
+ try { msg = JSON.parse(String(ev.data || "")); } catch { return; }
524
+ const t = String(msg && msg.type || "");
525
+ if (t === "auth_challenge") {
526
+ authChallengeSeen = true;
527
+ clearAuthWatchdog();
528
+ const pwd = await resolveSessionPassword(sid, false);
529
+ if (!pwd) {
530
+ setState("Missing session password");
531
+ if (!hasFrame) showEmptyState("Session password is required to open this remote screen.", true);
532
+ return;
533
+ }
534
+ const ph = await hashHex(pwd);
535
+ const nonce = String(msg.nonce || "");
536
+ const resp = await hashHex(ph + ":" + nonce);
537
+ ws.send(JSON.stringify({ type: "auth", nonce, response: resp, password_hash: ph }));
538
+ return;
539
+ }
540
+ if (t === "auth_result") {
541
+ authed = !!msg.ok;
542
+ if (authed) {
543
+ clearAuthWatchdog();
544
+ setState("Authenticated");
545
+ startShotLoop();
546
+ requestScreenshot();
547
+ if (!hasFrame) showEmptyState("Connected. Waiting for first screenshot frame...", false);
548
+ } else {
549
+ forgetPassword(sid);
550
+ const entered = await resolveSessionPassword(sid, true);
551
+ if (entered) {
552
+ setState("Retrying with updated password...");
553
+ disconnect();
554
+ setTimeout(connect, 120);
555
+ } else {
556
+ setState("Auth failed");
557
+ if (!hasFrame) showEmptyState("Authentication failed. Please enter the correct session password.", true);
558
+ }
559
+ }
560
+ return;
561
+ }
562
+ if (t === "system_info") {
563
+ setState("Connected (" + String(msg?.data?.platform || "unknown") + ")");
564
+ return;
565
+ }
566
+ if (t === "fs_screenshot_result") {
567
+ inflightShot = false;
568
+ if (msg.ok && msg.b64) {
569
+ const mime = String(msg.mime || "image/png");
570
+ screenEl.src = "data:" + mime + ";base64," + String(msg.b64);
571
+ hasFrame = true;
572
+ hideEmptyState();
573
+ } else if (!hasFrame) {
574
+ const em = String(msg.error || "").trim();
575
+ showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
576
+ }
577
+ return;
578
+ }
579
+ const rid = String(msg && msg.request_id || "");
580
+ if (rid && pendingReqs.has(rid)) {
581
+ const done = pendingReqs.get(rid);
582
+ pendingReqs.delete(rid);
583
+ try { done(msg); } catch {}
584
+ return;
585
+ }
586
+ };
587
+ }
588
+ function disconnect() {
589
+ stopShotLoop();
590
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
591
+ clearAuthWatchdog();
592
+ if (ws) { try { ws.close(); } catch {} ws = null; }
593
+ authed = false;
594
+ inflightShot = false;
595
+ pendingReqs.clear();
596
+ writeEnabled = false;
597
+ modeBtn.textContent = "View Only";
598
+ modeStateEl.textContent = "Mode: View Only";
599
+ modeBtn.className = "alt";
600
+ screenEl.classList.remove("write-enabled");
601
+ if (!hasFrame) showEmptyState("Disconnected from remote session.", true);
602
+ updateWriteControls();
603
+ }
604
+ function stopShotLoop() {
605
+ if (screenshotTimer) { clearInterval(screenshotTimer); screenshotTimer = null; }
606
+ }
607
+ function startShotLoop() {
608
+ stopShotLoop();
609
+ screenshotTimer = setInterval(requestScreenshot, 900);
610
+ }
611
+ function requestScreenshot() {
612
+ if (!ws || ws.readyState !== 1 || !authed || inflightShot) return;
613
+ inflightShot = true;
614
+ if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
615
+ ws.send(JSON.stringify({ type: "fs_screenshot", request_id: "shot_" + (++reqSeq) }));
616
+ }
617
+ function wsRequest(type, payload) {
618
+ if (!ws || ws.readyState !== 1 || !authed) return Promise.resolve({ ok: false, error: "not connected" });
619
+ const rid = type + "_" + (++reqSeq);
620
+ return new Promise((resolve) => {
621
+ pendingReqs.set(rid, resolve);
622
+ ws.send(JSON.stringify(Object.assign({ type, request_id: rid }, payload || {})));
623
+ setTimeout(() => {
624
+ if (!pendingReqs.has(rid)) return;
625
+ pendingReqs.delete(rid);
626
+ resolve({ ok: false, error: "request timeout" });
627
+ }, 8000);
628
+ });
629
+ }
630
+ async function pullClipboardToLocal() {
631
+ if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
632
+ const r = await wsRequest("rc_clipboard_get");
633
+ if (!r || !r.ok) { setState("Clipboard pull failed"); return; }
634
+ const text = String(r.text || "");
635
+ try {
636
+ await navigator.clipboard.writeText(text);
637
+ setState("Clipboard copied from PC to local");
638
+ } catch {
639
+ setState("Clipboard write blocked by browser");
640
+ }
641
+ }
642
+ async function pushLocalClipboardToRemote() {
643
+ if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
644
+ let text = "";
645
+ try {
646
+ text = await navigator.clipboard.readText();
647
+ } catch {
648
+ setState("Clipboard read blocked by browser");
649
+ return;
650
+ }
651
+ const r = await wsRequest("rc_clipboard_set", { text });
652
+ if (!r || !r.ok) { setState("Clipboard push failed"); return; }
653
+ setState("Clipboard sent from local to PC");
654
+ }
655
+ async function pullRemoteFileToLocal(remotePath) {
656
+ const p = String(remotePath || "").trim();
657
+ if (!p) return { ok: false, error: "remote path required" };
658
+ const rid = "pull_" + (++reqSeq);
659
+ let off = 0;
660
+ let eof = false;
661
+ const chunks = [];
662
+ let fileName = "remote-file.bin";
663
+ while (!eof) {
664
+ const r = await wsRequest("fs_read", {
665
+ path: p,
666
+ chunk: true,
667
+ offset: off,
668
+ request_id: rid,
669
+ max_bytes: 8 * 1024 * 1024,
670
+ });
671
+ if (!r || !r.ok) return { ok: false, error: String((r && r.error) || "read failed") };
672
+ const rp = String(r.path || p);
673
+ const seg = rp.split(/[\\/]/).pop();
674
+ if (seg) fileName = seg;
675
+ const b64 = String(r.b64 || "");
676
+ if (b64) chunks.push(b64);
677
+ off = Number.isFinite(Number(r.next_offset)) ? Math.max(off, Number(r.next_offset)) : off;
678
+ eof = Boolean(r.eof);
679
+ }
680
+ if (chunks.length === 0) return { ok: false, error: "empty file or no read access" };
681
+ let totalLen = 0;
682
+ const bytes = [];
683
+ for (const b64 of chunks) {
684
+ const bin = atob(b64);
685
+ totalLen += bin.length;
686
+ const arr = new Uint8Array(bin.length);
687
+ for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
688
+ bytes.push(arr);
689
+ }
690
+ const out = new Uint8Array(totalLen);
691
+ let pos = 0;
692
+ for (const arr of bytes) {
693
+ out.set(arr, pos);
694
+ pos += arr.length;
695
+ }
696
+ const blob = new Blob([out], { type: "application/octet-stream" });
697
+ const url = URL.createObjectURL(blob);
698
+ const a = document.createElement("a");
699
+ a.href = url;
700
+ a.download = fileName;
701
+ document.body.appendChild(a);
702
+ a.click();
703
+ a.remove();
704
+ setTimeout(() => URL.revokeObjectURL(url), 1500);
705
+ return { ok: true, fileName, size: totalLen };
706
+ }
707
+ function setPathLabel(p) {
708
+ pathLabel.textContent = "Path: " + (String(p || "").trim() || "(none)");
709
+ }
710
+ function clearFileList(msg) {
711
+ fileList.innerHTML = "";
712
+ const d = document.createElement("div");
713
+ d.className = "path-label";
714
+ d.textContent = msg;
715
+ fileList.appendChild(d);
716
+ }
717
+ function joinPath(base, name) {
718
+ const b = String(base || "");
719
+ const n = String(name || "");
720
+ if (!b) return n;
721
+ if (/[\\/]$/.test(b)) return b + n;
722
+ return b + "\\" + n;
723
+ }
724
+ async function loadRootsIntoPanel() {
725
+ if (!writeEnabled) return;
726
+ clearFileList("Loading roots...");
727
+ const r = await wsRequest("fs_roots");
728
+ if (!r || !r.ok) { clearFileList("Failed to load roots"); return; }
729
+ const roots = Array.isArray(r.roots) ? r.roots : [];
730
+ currentBrowsePath = "";
731
+ setPathLabel("");
732
+ fileList.innerHTML = "";
733
+ for (const root of roots) {
734
+ const p = String(root.path || "");
735
+ if (!p) continue;
736
+ const btn = document.createElement("button");
737
+ btn.className = "file-item dir";
738
+ btn.textContent = "[ROOT] " + p;
739
+ btn.addEventListener("click", () => loadPathIntoPanel(p));
740
+ fileList.appendChild(btn);
741
+ }
742
+ if (!roots.length) clearFileList("No roots available");
743
+ }
744
+ async function loadPathIntoPanel(p) {
745
+ const pathToLoad = String(p || "").trim();
746
+ if (!pathToLoad) return;
747
+ clearFileList("Loading...");
748
+ const r = await wsRequest("fs_list", { path: pathToLoad });
749
+ if (!r || !r.ok) { clearFileList("Failed to open folder"); return; }
750
+ currentBrowsePath = pathToLoad;
751
+ setPathLabel(currentBrowsePath);
752
+ fileList.innerHTML = "";
753
+ const entries = Array.isArray(r.entries) ? r.entries : [];
754
+ for (const it of entries) {
755
+ const name = String(it.name || "");
756
+ const kind = String(it.kind || "");
757
+ if (!name) continue;
758
+ const full = joinPath(pathToLoad, name);
759
+ const btn = document.createElement("button");
760
+ btn.className = "file-item" + (kind === "dir" ? " dir" : "");
761
+ btn.textContent = (kind === "dir" ? "[DIR] " : "[FILE] ") + name;
762
+ if (kind === "dir") {
763
+ btn.addEventListener("click", () => loadPathIntoPanel(full));
764
+ } else {
765
+ btn.addEventListener("click", async () => {
766
+ filePullPath.value = full;
767
+ setState("Fetching remote file...");
768
+ const rr = await pullRemoteFileToLocal(full);
769
+ if (!rr || !rr.ok) { setState("File fetch failed"); return; }
770
+ setState("Fetched file from PC: " + String(rr.fileName || "download"));
771
+ });
772
+ }
773
+ fileList.appendChild(btn);
774
+ }
775
+ if (!entries.length) clearFileList("Folder is empty");
776
+ }
777
+ function sendRemoteInput(payload) {
778
+ if (!ws || ws.readyState !== 1 || !authed || !writeEnabled) return;
779
+ ws.send(JSON.stringify(Object.assign({
780
+ type: "rc_input",
781
+ request_id: "rc_" + (++reqSeq),
782
+ }, payload || {})));
783
+ }
784
+ function isBrowserZoomHotkey(ev) {
785
+ if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
786
+ const key = String(ev.key || "").toLowerCase();
787
+ return key === "+" || key === "-" || key === "=" || key === "_" || key === "0";
788
+ }
789
+ function imgPoint(ev) {
790
+ const r = screenEl.getBoundingClientRect();
791
+ if (!r.width || !r.height || !screenEl.naturalWidth || !screenEl.naturalHeight) return null;
792
+ const x = Math.max(0, Math.min(screenEl.naturalWidth - 1, Math.round((ev.clientX - r.left) * (screenEl.naturalWidth / r.width))));
793
+ const y = Math.max(0, Math.min(screenEl.naturalHeight - 1, Math.round((ev.clientY - r.top) * (screenEl.naturalHeight / r.height))));
794
+ return { x, y };
795
+ }
796
+
797
+ document.getElementById("disconnectBtn").addEventListener("click", disconnect);
798
+ document.getElementById("refreshBtn").addEventListener("click", () => {
799
+ if (!ws || ws.readyState !== 1) {
800
+ connect();
801
+ return;
802
+ }
803
+ requestScreenshot();
804
+ });
805
+ modeBtn.addEventListener("click", () => {
806
+ writeEnabled = !writeEnabled;
807
+ modeBtn.textContent = writeEnabled ? "Write Only" : "View Only";
808
+ modeStateEl.textContent = writeEnabled ? "Mode: Write Only" : "Mode: View Only";
809
+ modeBtn.className = writeEnabled ? "warn" : "alt";
810
+ screenEl.classList.toggle("write-enabled", writeEnabled);
811
+ updateWriteControls();
812
+ });
813
+ filePullBtn.addEventListener("click", async () => {
814
+ if (!writeEnabled) { setState("Enable Write Only mode for file fetch"); return; }
815
+ const p = String(filePullPath.value || "").trim();
816
+ if (!p) { setState("Enter remote file path first"); return; }
817
+ setState("Fetching remote file…");
818
+ const r = await pullRemoteFileToLocal(p);
819
+ if (!r || !r.ok) { setState("File fetch failed"); return; }
820
+ setState("Fetched file from PC: " + String(r.fileName || "download"));
821
+ });
822
+ browseBtn.addEventListener("click", async () => {
823
+ if (!writeEnabled) { setState("Enable Write Only mode for file browse"); return; }
824
+ filePanel.classList.toggle("open");
825
+ if (!filePanel.classList.contains("open")) return;
826
+ await loadRootsIntoPanel();
827
+ });
828
+ rootsBtn.addEventListener("click", async () => {
829
+ if (!writeEnabled) return;
830
+ await loadRootsIntoPanel();
831
+ });
832
+ upBtn.addEventListener("click", async () => {
833
+ if (!writeEnabled) return;
834
+ if (!currentBrowsePath) { await loadRootsIntoPanel(); return; }
835
+ const r = await wsRequest("fs_parent", { path: currentBrowsePath });
836
+ const p = r && r.ok ? String(r.path || "") : "";
837
+ if (!p) { await loadRootsIntoPanel(); return; }
838
+ await loadPathIntoPanel(p);
839
+ });
840
+ closePanelBtn.addEventListener("click", () => {
841
+ filePanel.classList.remove("open");
842
+ });
843
+ filePushBtn.addEventListener("click", async () => {
844
+ if (!writeEnabled) { setState("Enable Write Only mode for file send"); return; }
845
+ const f = filePushInput.files && filePushInput.files[0];
846
+ if (!f) { setState("Choose a file first"); return; }
847
+ const maxBytes = 20 * 1024 * 1024;
848
+ if (f.size > maxBytes) { setState("File too large (max 20MB)"); return; }
849
+ let b64 = "";
850
+ try {
851
+ const buf = await f.arrayBuffer();
852
+ const bytes = new Uint8Array(buf);
853
+ let bin = "";
854
+ const chunk = 0x8000;
855
+ for (let i = 0; i < bytes.length; i += chunk) {
856
+ bin += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
857
+ }
858
+ b64 = btoa(bin);
859
+ } catch {
860
+ setState("Failed to read local file");
861
+ return;
862
+ }
863
+ setState("Sending file…");
864
+ const r = await wsRequest("rc_file_push", { name: String(f.name || "upload.bin"), b64 });
865
+ if (!r || !r.ok) { setState("File send failed"); return; }
866
+ const saved = String(r.saved_path || "");
867
+ setState(saved ? ("File sent to PC: " + saved) : "File sent to PC");
868
+ filePushInput.value = "";
869
+ });
870
+
871
+ screenEl.addEventListener("click", (ev) => {
872
+ const p = imgPoint(ev); if (!p) return;
873
+ sendRemoteInput({ action: "mouse_click", button: ev.button === 2 ? "right" : "left", x: p.x, y: p.y, click_count: 1 });
874
+ requestScreenshot();
875
+ });
876
+ screenEl.addEventListener("dblclick", (ev) => {
877
+ const p = imgPoint(ev); if (!p) return;
878
+ sendRemoteInput({ action: "mouse_click", button: "left", x: p.x, y: p.y, click_count: 2 });
879
+ requestScreenshot();
880
+ });
881
+ screenEl.addEventListener("contextmenu", (ev) => ev.preventDefault());
882
+ wrapEl.addEventListener("wheel", (ev) => {
883
+ if (ev.ctrlKey || ev.metaKey) return; // Keep browser zoom (ctrl/cmd + wheel) working.
884
+ if (!writeEnabled) return;
885
+ ev.preventDefault();
886
+ sendRemoteInput({ action: "mouse_wheel", delta_y: Math.round(ev.deltaY) });
887
+ }, { passive: false });
888
+ window.addEventListener("keydown", (ev) => {
889
+ if (!writeEnabled) return;
890
+ if (["INPUT", "TEXTAREA"].includes(String(document.activeElement && document.activeElement.tagName || ""))) return;
891
+ if (isBrowserZoomHotkey(ev)) return; // Let browser handle Ctrl/Cmd +/-/0 zoom.
892
+ const ctrlOrMeta = ev.ctrlKey || ev.metaKey;
893
+ if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "c") {
894
+ ev.preventDefault();
895
+ if (!remoteClipboardBusy) {
896
+ remoteClipboardBusy = true;
897
+ void pullClipboardToLocal();
898
+ setTimeout(() => { remoteClipboardBusy = false; }, 1200);
899
+ }
900
+ return;
901
+ }
902
+ if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "v") {
903
+ ev.preventDefault();
904
+ if (!localClipboardBusy) {
905
+ localClipboardBusy = true;
906
+ void pushLocalClipboardToRemote();
907
+ setTimeout(() => { localClipboardBusy = false; }, 1200);
908
+ }
909
+ return;
910
+ }
911
+ ev.preventDefault();
912
+ sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
913
+ });
914
+
915
+ updateWriteControls();
916
+ connect();
917
+ </script>
918
+ </body>
919
+ </html>