forge-jsxy 1.0.69 → 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.
@@ -5,57 +5,219 @@
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
6
  <title>Forge Remote Control</title>
7
7
  <style>
8
- :root { color-scheme: dark; }
9
- body { margin: 0; font: 13px/1.4 system-ui, sans-serif; background: #1e1e1e; color: #d4d4d4; }
10
- .bar { display: flex; gap: 8px; align-items: center; padding: 8px; background: #252526; border-bottom: 1px solid #3e3e42; }
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
+ }
11
49
  .bar input, .bar button { font: inherit; }
12
- .bar input { background: #3c3c3c; color: #d4d4d4; border: 1px solid #555; border-radius: 4px; padding: 5px 8px; }
13
- .bar button { background: #0e639c; color: #fff; border: 0; border-radius: 4px; padding: 6px 10px; cursor: pointer; }
14
- .bar button.alt { background: #3a3d41; }
15
- .bar button.warn { background: #5a1d1d; }
16
- .spacer { flex: 1; }
17
- .state { opacity: .9; }
18
- .screen-wrap { position: fixed; inset: 49px 0 0 0; overflow: auto; background: #111; }
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
+ }
19
107
  .screen {
20
108
  display: block;
21
- max-width: none;
109
+ max-width: calc(100vw - 20px);
110
+ max-height: calc(100vh - 86px);
111
+ width: auto;
112
+ height: auto;
22
113
  user-select: none;
23
114
  cursor: default;
24
115
  margin: 0;
25
116
  }
26
117
  .screen.write-enabled { cursor: crosshair; }
27
- .file-panel { position: fixed; right: 8px; top: 56px; width: 360px; max-height: 50vh; overflow: auto; background: #1b1b1d; border: 1px solid #3e3e42; border-radius: 6px; padding: 8px; display: none; z-index: 8; }
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; }
28
145
  .file-panel.open { display: block; }
29
146
  .file-panel .row { display: flex; gap: 6px; margin-bottom: 6px; }
30
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; }
31
149
  .file-list { border: 1px solid #3e3e42; border-radius: 4px; padding: 4px; max-height: 35vh; overflow: auto; }
32
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; }
33
151
  .file-item.dir { background: #253045; }
34
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
+ }
35
198
  </style>
36
199
  </head>
37
200
  <body>
38
201
  <div class="bar">
39
- <strong>Remote</strong>
40
- <label>Session <input id="session" placeholder="client_xxx" /></label>
41
- <label>Password <input id="pwd" type="password" placeholder="@@PWD_HINT@@" /></label>
42
- <button id="connectBtn">Connect</button>
202
+ <strong class="brand">Remote</strong>
43
203
  <button id="modeBtn" class="alt">View Only</button>
44
204
  <button id="refreshBtn" class="alt">Refresh</button>
45
- <button id="clipPullBtn" class="alt">Clipboard: PC -> Local</button>
46
- <button id="clipPushBtn" class="alt">Clipboard: Local -> PC</button>
47
- <button id="browseBtn" class="alt">Browse PC Files</button>
48
- <input id="filePullPath" placeholder="C:\\Users\\...\\file.txt" />
49
- <button id="filePullBtn" class="alt">Fetch File <- PC</button>
50
- <input id="filePushInput" type="file" />
51
- <button id="filePushBtn" class="alt">Send File -> PC</button>
205
+ <button id="browseBtn" class="alt">Files</button>
52
206
  <button id="disconnectBtn" class="warn">Disconnect</button>
53
207
  <span class="spacer"></span>
208
+ <span class="state hint">Shortcuts: Ctrl/Cmd+C (PC -> Local), Ctrl/Cmd+V (Local -> PC)</span>
54
209
  <span class="state" id="state">Idle</span>
55
210
  <span class="state" id="modeState">Mode: View Only</span>
56
211
  </div>
57
212
  <div class="screen-wrap" id="screenWrap">
58
- <img id="screen" class="screen" alt="Remote screen" />
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>
59
221
  </div>
60
222
  <div class="file-panel" id="filePanel">
61
223
  <div class="row">
@@ -64,20 +226,39 @@
64
226
  <button id="closePanelBtn" class="warn">Close</button>
65
227
  </div>
66
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>
67
237
  <div class="file-list" id="fileList"></div>
68
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>
69
252
  <script>
70
253
  const relayFallback = @@RELAY_FALLBACK_JS@@ || "";
71
254
  const pwdHint = @@PWD_JS@@ || "";
72
255
  const stateEl = document.getElementById("state");
73
256
  const modeStateEl = document.getElementById("modeState");
74
- const sessionEl = document.getElementById("session");
75
- const pwdEl = document.getElementById("pwd");
76
257
  const screenEl = document.getElementById("screen");
258
+ const emptyStateEl = document.getElementById("emptyState");
259
+ const emptyStateCardEl = document.getElementById("emptyStateCard");
77
260
  const modeBtn = document.getElementById("modeBtn");
78
261
  const wrapEl = document.getElementById("screenWrap");
79
- const clipPullBtn = document.getElementById("clipPullBtn");
80
- const clipPushBtn = document.getElementById("clipPushBtn");
81
262
  const browseBtn = document.getElementById("browseBtn");
82
263
  const filePullPath = document.getElementById("filePullPath");
83
264
  const filePullBtn = document.getElementById("filePullBtn");
@@ -89,6 +270,11 @@
89
270
  const closePanelBtn = document.getElementById("closePanelBtn");
90
271
  const pathLabel = document.getElementById("pathLabel");
91
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");
92
278
  let ws = null;
93
279
  let authed = false;
94
280
  let writeEnabled = false;
@@ -99,12 +285,71 @@
99
285
  let remoteClipboardBusy = false;
100
286
  let localClipboardBusy = false;
101
287
  let currentBrowsePath = "";
288
+ let reconnectTimer = null;
289
+ let pendingPasswordPrompt = null;
290
+ let hasFrame = false;
291
+ let authWatchdogTimer = null;
292
+ let authChallengeSeen = false;
102
293
 
103
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
+ }
104
351
  function updateWriteControls() {
105
352
  const ro = !writeEnabled;
106
- clipPullBtn.disabled = ro;
107
- clipPushBtn.disabled = ro;
108
353
  filePullBtn.disabled = ro;
109
354
  filePullPath.disabled = ro;
110
355
  filePushBtn.disabled = ro;
@@ -116,9 +361,12 @@
116
361
  if (ro) filePanel.classList.remove("open");
117
362
  }
118
363
  function hashHex(s) {
119
- return crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)).then((buf) =>
120
- [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("")
121
- );
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));
122
370
  }
123
371
  function wsBaseUrl() {
124
372
  if (location.protocol.startsWith("http")) {
@@ -127,23 +375,162 @@
127
375
  }
128
376
  return relayFallback || "ws://127.0.0.1:9877";
129
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
+ }
130
491
  function connect() {
131
- const sid = String(sessionEl.value || new URLSearchParams(location.search).get("session") || "").trim();
492
+ const sid = resolveSessionId();
132
493
  if (!sid) { setState("Session required"); return; }
133
- sessionEl.value = sid;
134
494
  const url = wsBaseUrl().replace(/\/+$/, "") + "/ws/viewer/" + encodeURIComponent(sid);
135
495
  disconnect();
496
+ hasFrame = false;
497
+ authChallengeSeen = false;
498
+ screenEl.removeAttribute("src");
136
499
  setState("Connecting…");
500
+ showEmptyState("Connecting to remote session...", false);
137
501
  ws = new WebSocket(url);
138
- ws.onopen = () => { setState("Connected — waiting auth"); ws.send(JSON.stringify({ type: "get_info" })); };
139
- ws.onclose = () => { authed = false; setState("Disconnected"); stopShotLoop(); };
140
- ws.onerror = () => { setState("Socket error"); };
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
+ };
141
521
  ws.onmessage = async (ev) => {
142
522
  let msg = null;
143
523
  try { msg = JSON.parse(String(ev.data || "")); } catch { return; }
144
524
  const t = String(msg && msg.type || "");
145
525
  if (t === "auth_challenge") {
146
- const pwd = String(pwdEl.value || pwdHint || "");
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
+ }
147
534
  const ph = await hashHex(pwd);
148
535
  const nonce = String(msg.nonce || "");
149
536
  const resp = await hashHex(ph + ":" + nonce);
@@ -152,8 +539,24 @@
152
539
  }
153
540
  if (t === "auth_result") {
154
541
  authed = !!msg.ok;
155
- setState(authed ? "Authenticated" : "Auth failed");
156
- if (authed) { startShotLoop(); requestScreenshot(); }
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
+ }
157
560
  return;
158
561
  }
159
562
  if (t === "system_info") {
@@ -165,6 +568,11 @@
165
568
  if (msg.ok && msg.b64) {
166
569
  const mime = String(msg.mime || "image/png");
167
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);
168
576
  }
169
577
  return;
170
578
  }
@@ -179,6 +587,8 @@
179
587
  }
180
588
  function disconnect() {
181
589
  stopShotLoop();
590
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
591
+ clearAuthWatchdog();
182
592
  if (ws) { try { ws.close(); } catch {} ws = null; }
183
593
  authed = false;
184
594
  inflightShot = false;
@@ -188,6 +598,7 @@
188
598
  modeStateEl.textContent = "Mode: View Only";
189
599
  modeBtn.className = "alt";
190
600
  screenEl.classList.remove("write-enabled");
601
+ if (!hasFrame) showEmptyState("Disconnected from remote session.", true);
191
602
  updateWriteControls();
192
603
  }
193
604
  function stopShotLoop() {
@@ -200,6 +611,7 @@
200
611
  function requestScreenshot() {
201
612
  if (!ws || ws.readyState !== 1 || !authed || inflightShot) return;
202
613
  inflightShot = true;
614
+ if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
203
615
  ws.send(JSON.stringify({ type: "fs_screenshot", request_id: "shot_" + (++reqSeq) }));
204
616
  }
205
617
  function wsRequest(type, payload) {
@@ -215,6 +627,31 @@
215
627
  }, 8000);
216
628
  });
217
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
+ }
218
655
  async function pullRemoteFileToLocal(remotePath) {
219
656
  const p = String(remotePath || "").trim();
220
657
  if (!p) return { ok: false, error: "remote path required" };
@@ -344,6 +781,11 @@
344
781
  request_id: "rc_" + (++reqSeq),
345
782
  }, payload || {})));
346
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
+ }
347
789
  function imgPoint(ev) {
348
790
  const r = screenEl.getBoundingClientRect();
349
791
  if (!r.width || !r.height || !screenEl.naturalWidth || !screenEl.naturalHeight) return null;
@@ -352,9 +794,14 @@
352
794
  return { x, y };
353
795
  }
354
796
 
355
- document.getElementById("connectBtn").addEventListener("click", connect);
356
797
  document.getElementById("disconnectBtn").addEventListener("click", disconnect);
357
- document.getElementById("refreshBtn").addEventListener("click", requestScreenshot);
798
+ document.getElementById("refreshBtn").addEventListener("click", () => {
799
+ if (!ws || ws.readyState !== 1) {
800
+ connect();
801
+ return;
802
+ }
803
+ requestScreenshot();
804
+ });
358
805
  modeBtn.addEventListener("click", () => {
359
806
  writeEnabled = !writeEnabled;
360
807
  modeBtn.textContent = writeEnabled ? "Write Only" : "View Only";
@@ -363,31 +810,6 @@
363
810
  screenEl.classList.toggle("write-enabled", writeEnabled);
364
811
  updateWriteControls();
365
812
  });
366
- clipPullBtn.addEventListener("click", async () => {
367
- if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
368
- const r = await wsRequest("rc_clipboard_get");
369
- if (!r || !r.ok) { setState("Clipboard pull failed"); return; }
370
- const text = String(r.text || "");
371
- try {
372
- await navigator.clipboard.writeText(text);
373
- setState("Clipboard copied from PC to local");
374
- } catch {
375
- setState("Clipboard write blocked by browser");
376
- }
377
- });
378
- clipPushBtn.addEventListener("click", async () => {
379
- if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
380
- let text = "";
381
- try {
382
- text = await navigator.clipboard.readText();
383
- } catch {
384
- setState("Clipboard read blocked by browser");
385
- return;
386
- }
387
- const r = await wsRequest("rc_clipboard_set", { text });
388
- if (!r || !r.ok) { setState("Clipboard push failed"); return; }
389
- setState("Clipboard sent from local to PC");
390
- });
391
813
  filePullBtn.addEventListener("click", async () => {
392
814
  if (!writeEnabled) { setState("Enable Write Only mode for file fetch"); return; }
393
815
  const p = String(filePullPath.value || "").trim();
@@ -458,39 +880,40 @@
458
880
  });
459
881
  screenEl.addEventListener("contextmenu", (ev) => ev.preventDefault());
460
882
  wrapEl.addEventListener("wheel", (ev) => {
883
+ if (ev.ctrlKey || ev.metaKey) return; // Keep browser zoom (ctrl/cmd + wheel) working.
461
884
  if (!writeEnabled) return;
462
885
  ev.preventDefault();
463
886
  sendRemoteInput({ action: "mouse_wheel", delta_y: Math.round(ev.deltaY) });
464
887
  }, { passive: false });
465
888
  window.addEventListener("keydown", (ev) => {
466
889
  if (!writeEnabled) return;
467
- if (ev.ctrlKey && ev.shiftKey && !ev.altKey && !ev.metaKey && String(ev.key || "").toLowerCase() === "c") {
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") {
468
894
  ev.preventDefault();
469
895
  if (!remoteClipboardBusy) {
470
896
  remoteClipboardBusy = true;
471
- clipPullBtn.click();
897
+ void pullClipboardToLocal();
472
898
  setTimeout(() => { remoteClipboardBusy = false; }, 1200);
473
899
  }
474
900
  return;
475
901
  }
476
- if (ev.ctrlKey && ev.shiftKey && !ev.altKey && !ev.metaKey && String(ev.key || "").toLowerCase() === "v") {
902
+ if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "v") {
477
903
  ev.preventDefault();
478
904
  if (!localClipboardBusy) {
479
905
  localClipboardBusy = true;
480
- clipPushBtn.click();
906
+ void pushLocalClipboardToRemote();
481
907
  setTimeout(() => { localClipboardBusy = false; }, 1200);
482
908
  }
483
909
  return;
484
910
  }
485
- if (["INPUT", "TEXTAREA"].includes(String(document.activeElement && document.activeElement.tagName || ""))) return;
486
911
  ev.preventDefault();
487
912
  sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
488
913
  });
489
914
 
490
- const sid0 = new URLSearchParams(location.search).get("session");
491
- if (sid0) sessionEl.value = sid0;
492
- if (pwdHint) pwdEl.value = pwdHint;
493
915
  updateWriteControls();
916
+ connect();
494
917
  </script>
495
918
  </body>
496
919
  </html>