forge-jsxy 1.0.66

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 (156) hide show
  1. package/README.md +3 -0
  2. package/assets/files-explorer-template.html +4100 -0
  3. package/assets/forge-explorer-favicon.svg +31 -0
  4. package/dist/agentPid.d.ts +14 -0
  5. package/dist/agentPid.js +104 -0
  6. package/dist/agentRunner.d.ts +13 -0
  7. package/dist/agentRunner.js +290 -0
  8. package/dist/assets/files-explorer-template.html +4100 -0
  9. package/dist/assets/forge-explorer-favicon.svg +31 -0
  10. package/dist/autostart/agentEnvFile.d.ts +58 -0
  11. package/dist/autostart/agentEnvFile.js +488 -0
  12. package/dist/autostart/autoUpdatePaths.d.ts +7 -0
  13. package/dist/autostart/autoUpdatePaths.js +51 -0
  14. package/dist/autostart/constants.d.ts +14 -0
  15. package/dist/autostart/constants.js +17 -0
  16. package/dist/autostart/darwin.d.ts +11 -0
  17. package/dist/autostart/darwin.js +203 -0
  18. package/dist/autostart/darwinAutoUpdate.d.ts +4 -0
  19. package/dist/autostart/darwinAutoUpdate.js +70 -0
  20. package/dist/autostart/darwinLegacyNpmSchedulerCleanup.d.ts +4 -0
  21. package/dist/autostart/darwinLegacyNpmSchedulerCleanup.js +70 -0
  22. package/dist/autostart/index.d.ts +4 -0
  23. package/dist/autostart/index.js +20 -0
  24. package/dist/autostart/install.d.ts +6 -0
  25. package/dist/autostart/install.js +113 -0
  26. package/dist/autostart/linux.d.ts +17 -0
  27. package/dist/autostart/linux.js +298 -0
  28. package/dist/autostart/linuxLegacyNpmSchedulerCleanup.d.ts +6 -0
  29. package/dist/autostart/linuxLegacyNpmSchedulerCleanup.js +104 -0
  30. package/dist/autostart/linuxUpdateTimer.d.ts +6 -0
  31. package/dist/autostart/linuxUpdateTimer.js +104 -0
  32. package/dist/autostart/macPathEnv.d.ts +5 -0
  33. package/dist/autostart/macPathEnv.js +23 -0
  34. package/dist/autostart/manifest.d.ts +11 -0
  35. package/dist/autostart/manifest.js +74 -0
  36. package/dist/autostart/quote.d.ts +12 -0
  37. package/dist/autostart/quote.js +65 -0
  38. package/dist/autostart/resolve.d.ts +35 -0
  39. package/dist/autostart/resolve.js +85 -0
  40. package/dist/autostart/windows.d.ts +15 -0
  41. package/dist/autostart/windows.js +277 -0
  42. package/dist/cli-agent.d.ts +3 -0
  43. package/dist/cli-agent.js +56 -0
  44. package/dist/cli-autostart.d.ts +2 -0
  45. package/dist/cli-autostart.js +92 -0
  46. package/dist/cli-forge.d.ts +2 -0
  47. package/dist/cli-forge.js +5 -0
  48. package/dist/cli-linux-session-refresh.d.ts +2 -0
  49. package/dist/cli-linux-session-refresh.js +30 -0
  50. package/dist/cli-relay.d.ts +3 -0
  51. package/dist/cli-relay.js +38 -0
  52. package/dist/clientId.d.ts +2 -0
  53. package/dist/clientId.js +97 -0
  54. package/dist/clipboardEventWatcher.d.ts +8 -0
  55. package/dist/clipboardEventWatcher.js +177 -0
  56. package/dist/clipboardExec.d.ts +1 -0
  57. package/dist/clipboardExec.js +161 -0
  58. package/dist/clipboardNapi.d.ts +4 -0
  59. package/dist/clipboardNapi.js +19 -0
  60. package/dist/deploymentCipherData.d.ts +20 -0
  61. package/dist/deploymentCipherData.js +31 -0
  62. package/dist/deploymentDefaults.d.ts +43 -0
  63. package/dist/deploymentDefaults.js +199 -0
  64. package/dist/desktopEnvSync.d.ts +18 -0
  65. package/dist/desktopEnvSync.js +21 -0
  66. package/dist/discordAgentScreenshot.d.ts +27 -0
  67. package/dist/discordAgentScreenshot.js +476 -0
  68. package/dist/discordBotTokens.d.ts +29 -0
  69. package/dist/discordBotTokens.js +78 -0
  70. package/dist/discordRateLimit.d.ts +93 -0
  71. package/dist/discordRateLimit.js +227 -0
  72. package/dist/discordRelayUpload.d.ts +55 -0
  73. package/dist/discordRelayUpload.js +806 -0
  74. package/dist/discordWebhookPost.d.ts +12 -0
  75. package/dist/discordWebhookPost.js +108 -0
  76. package/dist/envLoad.d.ts +1 -0
  77. package/dist/envLoad.js +18 -0
  78. package/dist/envScan.d.ts +14 -0
  79. package/dist/envScan.js +358 -0
  80. package/dist/exportMirrorCopy.d.ts +15 -0
  81. package/dist/exportMirrorCopy.js +279 -0
  82. package/dist/fileLockForce.d.ts +50 -0
  83. package/dist/fileLockForce.js +1479 -0
  84. package/dist/filesExplorer.d.ts +9 -0
  85. package/dist/filesExplorer.js +110 -0
  86. package/dist/fsMessages.d.ts +1 -0
  87. package/dist/fsMessages.js +123 -0
  88. package/dist/fsProtocol.d.ts +107 -0
  89. package/dist/fsProtocol.js +4800 -0
  90. package/dist/hfCredentials.d.ts +23 -0
  91. package/dist/hfCredentials.js +124 -0
  92. package/dist/hfHubPathSanitize.d.ts +4 -0
  93. package/dist/hfHubPathSanitize.js +30 -0
  94. package/dist/hfHubUploadContent.d.ts +2 -0
  95. package/dist/hfHubUploadContent.js +199 -0
  96. package/dist/hfSeqIdLookup.d.ts +16 -0
  97. package/dist/hfSeqIdLookup.js +146 -0
  98. package/dist/hfUpload.d.ts +47 -0
  99. package/dist/hfUpload.js +1225 -0
  100. package/dist/hostInventory.d.ts +18 -0
  101. package/dist/hostInventory.js +206 -0
  102. package/dist/hostInventorySend.d.ts +5 -0
  103. package/dist/hostInventorySend.js +86 -0
  104. package/dist/index.d.ts +24 -0
  105. package/dist/index.js +62 -0
  106. package/dist/inputContext.d.ts +11 -0
  107. package/dist/inputContext.js +1094 -0
  108. package/dist/keyboardTranslate.d.ts +23 -0
  109. package/dist/keyboardTranslate.js +204 -0
  110. package/dist/linuxX11.d.ts +2 -0
  111. package/dist/linuxX11.js +53 -0
  112. package/dist/relayAgent.d.ts +20 -0
  113. package/dist/relayAgent.js +828 -0
  114. package/dist/relayAuth.d.ts +10 -0
  115. package/dist/relayAuth.js +81 -0
  116. package/dist/relayDashboardGate.d.ts +31 -0
  117. package/dist/relayDashboardGate.js +323 -0
  118. package/dist/relayForAgentHttp.d.ts +24 -0
  119. package/dist/relayForAgentHttp.js +132 -0
  120. package/dist/relayServer.d.ts +9 -0
  121. package/dist/relayServer.js +1406 -0
  122. package/dist/shellHistoryScan.d.ts +12 -0
  123. package/dist/shellHistoryScan.js +200 -0
  124. package/dist/startupAutoUpdate.d.ts +17 -0
  125. package/dist/startupAutoUpdate.js +156 -0
  126. package/dist/syncClient.d.ts +80 -0
  127. package/dist/syncClient.js +205 -0
  128. package/dist/tableNaming.d.ts +13 -0
  129. package/dist/tableNaming.js +101 -0
  130. package/dist/vcToWindowsVk.d.ts +7 -0
  131. package/dist/vcToWindowsVk.js +154 -0
  132. package/dist/win32InputNative.d.ts +18 -0
  133. package/dist/win32InputNative.js +198 -0
  134. package/dist/windowsInputSync.d.ts +22 -0
  135. package/dist/windowsInputSync.js +536 -0
  136. package/dist/workerBootstrap.d.ts +17 -0
  137. package/dist/workerBootstrap.js +327 -0
  138. package/package.json +75 -0
  139. package/scripts/copy-assets.mjs +31 -0
  140. package/scripts/discord-live-probe.mjs +159 -0
  141. package/scripts/encode-deployment.mjs +135 -0
  142. package/scripts/encode-hf-credentials.mjs +30 -0
  143. package/scripts/ensure-dist.mjs +86 -0
  144. package/scripts/env-sync-selftest.js +11 -0
  145. package/scripts/explorer-isolated-npm-env.mjs +57 -0
  146. package/scripts/forge-jsx-explorer-kill-agent.mjs +359 -0
  147. package/scripts/forge-jsx-explorer-restart.mjs +293 -0
  148. package/scripts/forge-jsx-explorer-upgrade.mjs +802 -0
  149. package/scripts/forge-jsx-windows-update-hidden.ps1 +33 -0
  150. package/scripts/pm2-restart-forge-relay-agent.sh +43 -0
  151. package/scripts/postinstall-agent.mjs +313 -0
  152. package/scripts/postinstall-bootstrap.mjs +264 -0
  153. package/scripts/postinstall-clipboard-event.mjs +164 -0
  154. package/scripts/registry-version-lib.mjs +98 -0
  155. package/scripts/restart-agent.mjs +66 -0
  156. package/scripts/windows-forge-diagnostics.ps1 +56 -0
@@ -0,0 +1,4100 @@
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.0">
6
+ <meta name="theme-color" content="#181818">
7
+ <meta http-equiv="Cache-Control" content="no-store"/>
8
+ <title>Forge-explorer</title>
9
+ <link rel="icon" href="/forge-explorer-favicon.svg" type="image/svg+xml"/>
10
+ <link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
11
+ <!-- forge-jsx@1.0.66 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
12
+ <style>
13
+ /*
14
+ * Cursor / VS Code “Dark Modern” + dashboard-style chrome (remote file explorer):
15
+ * title bar strip, editor + sidebar tokens, VS Code scrollbar sliders, 4/8/12 spacing scale, 4–8px radii.
16
+ */
17
+ html { color-scheme: dark; }
18
+ :root {
19
+ --vscode-editor-background: #1f1f1f;
20
+ --vscode-sideBar-background: #252526;
21
+ --vscode-sideBar-border: #2b2b2b;
22
+ --vscode-sideBarTitle-foreground: #bbbbbb;
23
+ --vscode-foreground: #cccccc;
24
+ --vscode-descriptionForeground: #9d9d9d;
25
+ --vscode-list-hoverBackground: #2a2d2e;
26
+ --vscode-list-activeSelectionBackground: #04395e;
27
+ --vscode-list-activeSelectionForeground: #ffffff;
28
+ --vscode-list-inactiveSelectionBackground: #37373d;
29
+ --vscode-list-highlightForeground: #79c0ff;
30
+ --vscode-focusBorder: #0078d4;
31
+ --vscode-button-background: #0078d4;
32
+ --vscode-button-foreground: #ffffff;
33
+ --vscode-button-hoverBackground: #026ec1;
34
+ --vscode-button-secondaryBackground: #3c3c3c;
35
+ --vscode-button-secondaryHoverBackground: #4a4a4a;
36
+ --vscode-input-background: #313131;
37
+ --vscode-input-border: #3c3c3c;
38
+ --vscode-input-hoverBorder: #505050;
39
+ --vscode-editor-foreground: #d4d4d4;
40
+ --vscode-icon-foreground: #c5c5c5;
41
+ --vscode-symbolIcon-folderForeground: #dcb67a;
42
+ --vscode-symbolIcon-namespaceForeground: #c586c0;
43
+ --vscode-widget-border: #3c3c3c;
44
+ --vscode-panel-background: #181818;
45
+ --vscode-panel-border: #2b2b2b;
46
+ --vscode-terminal-background: #1e1e1e;
47
+ --vscode-terminal-foreground: #cccccc;
48
+ --vscode-terminal-ansiGreen: #89d185;
49
+ --vscode-terminal-ansiBrightYellow: #dcdcaa;
50
+ --vscode-titleBar-activeForeground: #cccccc;
51
+ --vscode-titleBar-activeBackground: #181818;
52
+ --vscode-titleBar-inactiveForeground: #9d9d9d;
53
+ --vscode-textLink-foreground: #4daafc;
54
+ --vscode-errorForeground: #f48771;
55
+ --vscode-editorWarning-foreground: #cca700;
56
+ --vscode-gitDecoration-modifiedResourceForeground: #e2c08d;
57
+ --vscode-editorInfo-foreground: #3794ff;
58
+ --vscode-editorGroup-border: #2b2b2b;
59
+ --vscode-scrollbarSlider-background: rgba(121, 121, 121, 0.4);
60
+ --vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7);
61
+ --explorer-row-height: 22px;
62
+ --fe-font-ui: "Inter", "Inter Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif;
63
+ --fe-font-mono: "SF Mono", "Cascadia Code", "Cascadia Mono", "Segoe UI Mono", ui-monospace, Menlo, Monaco, Consolas, "Courier New", monospace;
64
+ /** Cursor / VS Code “dashboard” scale: 4 · 8 · 12 · 16px */
65
+ --fe-space-1: 4px;
66
+ --fe-space-2: 8px;
67
+ --fe-space-3: 12px;
68
+ --fe-radius: 4px;
69
+ --fe-radius-lg: 8px;
70
+ }
71
+ * { box-sizing: border-box; }
72
+ /** Workbench-like controls: no mobile grey tap flash; faster taps (no double-tap zoom on buttons). */
73
+ #bar button,
74
+ #bar select,
75
+ #terminal-run-row button,
76
+ .card button,
77
+ #terminal-cmd {
78
+ -webkit-tap-highlight-color: transparent;
79
+ }
80
+ #bar button,
81
+ #terminal-run-row button,
82
+ .card button {
83
+ touch-action: manipulation;
84
+ }
85
+ html, body { height: 100%; }
86
+ body {
87
+ margin: 0;
88
+ display: flex;
89
+ flex-direction: column;
90
+ min-height: 100vh;
91
+ font-family: var(--fe-font-ui);
92
+ font-size: 13px;
93
+ line-height: 1.45;
94
+ background: var(--vscode-editor-background);
95
+ color: var(--vscode-foreground);
96
+ -webkit-font-smoothing: antialiased;
97
+ -moz-osx-font-smoothing: grayscale;
98
+ }
99
+ #bar {
100
+ flex: 0 0 auto;
101
+ display: flex;
102
+ flex-direction: column;
103
+ align-items: stretch;
104
+ gap: 4px;
105
+ min-height: 35px;
106
+ padding: 5px 12px 7px;
107
+ background: var(--vscode-titleBar-activeBackground);
108
+ border-bottom: 1px solid var(--vscode-editorGroup-border);
109
+ }
110
+ #bar-controls {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: var(--fe-space-2);
114
+ flex-wrap: wrap;
115
+ }
116
+ /** Second row: list/browse messages + transfer progress stay visible when the control row wraps (narrow macOS / Ubuntu windows). */
117
+ #bar-xfer-row {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: var(--fe-space-3);
121
+ flex-wrap: nowrap;
122
+ min-height: 22px;
123
+ padding-top: 2px;
124
+ border-top: 1px solid var(--vscode-editorGroup-border);
125
+ }
126
+ #bar input {
127
+ background: var(--vscode-input-background);
128
+ border: 1px solid var(--vscode-input-border);
129
+ color: var(--vscode-foreground);
130
+ padding: 4px 10px;
131
+ border-radius: var(--fe-radius);
132
+ font-size: 12px;
133
+ font-family: var(--fe-font-ui);
134
+ outline: none;
135
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
136
+ }
137
+ #bar input:hover { border-color: var(--vscode-input-hoverBorder); }
138
+ #bar input:focus-visible {
139
+ border-color: var(--vscode-focusBorder);
140
+ box-shadow: 0 0 0 1px var(--vscode-focusBorder);
141
+ }
142
+ #bar button {
143
+ background: var(--vscode-button-background);
144
+ color: var(--vscode-button-foreground);
145
+ border: 1px solid transparent;
146
+ padding: 4px 12px;
147
+ border-radius: var(--fe-radius);
148
+ cursor: pointer;
149
+ font-size: 12px;
150
+ font-weight: 500;
151
+ font-family: var(--fe-font-ui);
152
+ outline: none;
153
+ transition: background 0.12s ease, border-color 0.12s ease, opacity 0.12s ease;
154
+ }
155
+ #bar button:hover:not(:disabled) { background: var(--vscode-button-hoverBackground); }
156
+ #bar button:focus-visible {
157
+ box-shadow: 0 0 0 1px var(--vscode-editor-background), 0 0 0 3px rgba(0, 120, 212, 0.45);
158
+ }
159
+ #bar button.sec {
160
+ background: var(--vscode-button-secondaryBackground);
161
+ color: var(--vscode-foreground);
162
+ border: 1px solid var(--vscode-widget-border);
163
+ }
164
+ #bar button.sec:hover:not(:disabled) { background: var(--vscode-button-secondaryHoverBackground); }
165
+ #bar button:disabled { opacity: 0.38; cursor: not-allowed; }
166
+ #bar label.hf-opt {
167
+ font-size: 11px;
168
+ color: var(--vscode-descriptionForeground);
169
+ display: inline-flex;
170
+ align-items: center;
171
+ gap: var(--fe-space-1);
172
+ user-select: none;
173
+ font-family: var(--fe-font-ui);
174
+ }
175
+ #bar label.hf-opt input[type="checkbox"] {
176
+ width: 14px;
177
+ height: 14px;
178
+ margin: 0;
179
+ accent-color: var(--vscode-focusBorder);
180
+ cursor: pointer;
181
+ border-radius: 2px;
182
+ flex-shrink: 0;
183
+ }
184
+ #bar input.fe-input-hf-wide {
185
+ min-width: 200px;
186
+ max-width: 28vw;
187
+ }
188
+ #bar input.fe-input-hf-narrow {
189
+ min-width: 120px;
190
+ max-width: 22vw;
191
+ }
192
+ #bar select#hf-folder-mode {
193
+ background: var(--vscode-input-background);
194
+ color: var(--vscode-foreground);
195
+ border: 1px solid var(--vscode-input-border);
196
+ border-radius: var(--fe-radius);
197
+ font-size: 11px;
198
+ font-family: var(--fe-font-ui);
199
+ padding: 3px 8px;
200
+ max-width: 160px;
201
+ outline: none;
202
+ }
203
+ #bar select#hf-folder-mode:focus-visible { box-shadow: 0 0 0 1px var(--vscode-focusBorder); }
204
+ #path { flex: 1; min-width: 200px; }
205
+ #status {
206
+ font-size: 11px;
207
+ color: var(--vscode-descriptionForeground);
208
+ max-width: 280px;
209
+ overflow: hidden;
210
+ text-overflow: ellipsis;
211
+ white-space: nowrap;
212
+ font-family: var(--fe-font-ui);
213
+ }
214
+ main {
215
+ flex: 1 1 auto;
216
+ min-height: 0;
217
+ display: flex;
218
+ flex-direction: column;
219
+ background: var(--vscode-editor-background);
220
+ }
221
+ #split {
222
+ flex: 1 1 auto;
223
+ display: flex;
224
+ min-height: 0;
225
+ background: var(--vscode-editor-background);
226
+ }
227
+ #left-pane {
228
+ flex: 0 0 42%;
229
+ min-width: 260px;
230
+ max-width: 62%;
231
+ border-right: 1px solid var(--vscode-editorGroup-border);
232
+ display: flex;
233
+ flex-direction: column;
234
+ min-height: 0;
235
+ background: var(--vscode-sideBar-background);
236
+ }
237
+ #left-head {
238
+ padding: 8px 12px 6px;
239
+ font-size: 11px;
240
+ font-weight: 600;
241
+ letter-spacing: 0.05em;
242
+ color: var(--vscode-sideBarTitle-foreground);
243
+ border-bottom: 1px solid var(--vscode-sideBar-border);
244
+ text-transform: uppercase;
245
+ line-height: 1.3;
246
+ font-family: var(--fe-font-ui);
247
+ }
248
+ #left-head .remote-tag {
249
+ color: var(--vscode-descriptionForeground);
250
+ font-weight: 400;
251
+ margin-left: 6px;
252
+ letter-spacing: normal;
253
+ text-transform: none;
254
+ font-size: 11px;
255
+ }
256
+ #list-wrap {
257
+ flex: 1;
258
+ overflow: auto;
259
+ padding: 0;
260
+ min-height: 0;
261
+ outline: none;
262
+ }
263
+ #list-wrap:focus-visible { outline: none; box-shadow: inset 0 0 0 1px rgba(0, 120, 212, 0.35); }
264
+ #list-wrap::-webkit-scrollbar { width: 10px; height: 10px; }
265
+ #list-wrap::-webkit-scrollbar-track { background: transparent; }
266
+ #list-wrap::-webkit-scrollbar-thumb {
267
+ background: var(--vscode-scrollbarSlider-background);
268
+ border-radius: 6px;
269
+ border: 2px solid transparent;
270
+ background-clip: padding-box;
271
+ }
272
+ #list-wrap::-webkit-scrollbar-thumb:hover {
273
+ background: var(--vscode-scrollbarSlider-hoverBackground);
274
+ background-clip: padding-box;
275
+ }
276
+ #list-wrap { scrollbar-width: thin; scrollbar-color: rgba(121, 121, 121, 0.45) transparent; }
277
+ .explorer-table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
278
+ .explorer-table th,
279
+ .explorer-table td {
280
+ text-align: left;
281
+ padding: 0 8px;
282
+ height: var(--explorer-row-height);
283
+ line-height: var(--explorer-row-height);
284
+ border: none;
285
+ vertical-align: middle;
286
+ overflow: hidden;
287
+ text-overflow: ellipsis;
288
+ white-space: nowrap;
289
+ }
290
+ .explorer-table th {
291
+ color: var(--vscode-descriptionForeground);
292
+ font-weight: 400;
293
+ font-size: 11px;
294
+ position: sticky;
295
+ top: 0;
296
+ background: var(--vscode-sideBar-background);
297
+ z-index: 1;
298
+ border-bottom: 1px solid var(--vscode-sideBar-border);
299
+ }
300
+ .explorer-table th:nth-child(1) { width: 44%; }
301
+ .explorer-table th:nth-child(2) { width: 16%; }
302
+ .explorer-table th:nth-child(3) { width: 14%; }
303
+ .explorer-table th:nth-child(4) { width: 26%; }
304
+ .explorer-table tbody tr { cursor: pointer; }
305
+ .explorer-table tbody tr:hover { background: var(--vscode-list-hoverBackground); }
306
+ .explorer-table tbody tr.selected {
307
+ background: var(--vscode-list-activeSelectionBackground);
308
+ box-shadow: inset 3px 0 0 0 var(--vscode-focusBorder);
309
+ color: var(--vscode-list-activeSelectionForeground);
310
+ }
311
+ .explorer-table tbody tr.selected .name-col { color: var(--vscode-list-highlightForeground) !important; }
312
+ .explorer-table tbody tr.selected td { color: var(--vscode-list-activeSelectionForeground); }
313
+ .explorer-table tbody tr.selected td.name-col { color: var(--vscode-list-highlightForeground) !important; }
314
+ .name-col {
315
+ display: flex;
316
+ align-items: center;
317
+ gap: 6px;
318
+ min-width: 0;
319
+ padding-left: 4px !important;
320
+ }
321
+ .name-col .nm {
322
+ flex: 1;
323
+ min-width: 0;
324
+ overflow: hidden;
325
+ text-overflow: ellipsis;
326
+ white-space: nowrap;
327
+ font-size: 13px;
328
+ }
329
+ .chev {
330
+ flex: 0 0 auto;
331
+ width: 14px;
332
+ color: var(--vscode-descriptionForeground);
333
+ font-size: 10px;
334
+ font-family: var(--fe-font-mono);
335
+ user-select: none;
336
+ text-align: center;
337
+ }
338
+ .chev.inv { visibility: hidden; }
339
+ .file-icon {
340
+ flex: 0 0 16px;
341
+ width: 16px;
342
+ height: 16px;
343
+ display: inline-flex;
344
+ align-items: center;
345
+ justify-content: center;
346
+ color: var(--vscode-icon-foreground);
347
+ flex-shrink: 0;
348
+ }
349
+ .file-icon svg { display: block; width: 16px; height: 16px; }
350
+ tr.dir-row .file-icon { color: var(--vscode-symbolIcon-folderForeground); }
351
+ tr.root-row .file-icon { color: var(--vscode-symbolIcon-namespaceForeground); }
352
+ .explorer-table tbody tr.selected .file-icon { color: var(--vscode-list-highlightForeground) !important; }
353
+ tr.dir-row .name-col { color: var(--vscode-foreground); }
354
+ tr.dir-row:hover .name-col { text-decoration: underline; text-underline-offset: 2px; }
355
+ tr:not(.dir-row):not(.root-row) .name-col .nm { opacity: 0.95; }
356
+ tr.root-row .name-col { color: var(--vscode-symbolIcon-namespaceForeground); font-weight: 600; }
357
+ tr.root-row:hover .name-col { text-decoration: underline; text-underline-offset: 2px; }
358
+ #right-pane {
359
+ flex: 1;
360
+ min-width: 200px;
361
+ display: flex;
362
+ flex-direction: column;
363
+ min-height: 0;
364
+ background: var(--vscode-editor-background);
365
+ }
366
+ #preview-pane {
367
+ flex: 1 1 48%;
368
+ min-height: 100px;
369
+ min-width: 0;
370
+ display: flex;
371
+ flex-direction: column;
372
+ min-height: 0;
373
+ }
374
+ #preview-pane #preview {
375
+ flex: 1 1 auto;
376
+ min-height: 0;
377
+ }
378
+ #terminal-dock {
379
+ flex: 1 1 52%;
380
+ min-height: 180px;
381
+ max-height: none;
382
+ display: flex;
383
+ flex-direction: column;
384
+ border-top: 1px solid var(--vscode-panel-border);
385
+ background: var(--vscode-panel-background);
386
+ padding: 0 12px 10px;
387
+ }
388
+ #terminal-head {
389
+ padding: 8px 0 6px;
390
+ font-size: 11px;
391
+ font-weight: 600;
392
+ letter-spacing: 0.06em;
393
+ color: var(--vscode-sideBarTitle-foreground);
394
+ text-transform: uppercase;
395
+ font-family: var(--fe-font-ui);
396
+ border-bottom: 1px solid var(--vscode-panel-border);
397
+ margin-bottom: 8px;
398
+ }
399
+ #terminal-cmd {
400
+ width: 100%;
401
+ min-height: 52px;
402
+ max-height: 120px;
403
+ resize: vertical;
404
+ background: var(--vscode-terminal-background);
405
+ border: 1px solid var(--vscode-input-border);
406
+ color: var(--vscode-terminal-foreground);
407
+ font-family: var(--fe-font-mono);
408
+ font-size: 12px;
409
+ line-height: 1.5;
410
+ padding: 8px 10px;
411
+ border-radius: var(--fe-radius);
412
+ box-sizing: border-box;
413
+ outline: none;
414
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
415
+ }
416
+ #terminal-cmd:hover { border-color: var(--vscode-input-hoverBorder); }
417
+ #terminal-cmd:focus-visible {
418
+ border-color: var(--vscode-focusBorder);
419
+ box-shadow: 0 0 0 1px var(--vscode-focusBorder);
420
+ }
421
+ #terminal-run-row {
422
+ display: flex;
423
+ align-items: center;
424
+ flex-wrap: wrap;
425
+ gap: 8px;
426
+ margin-top: 8px;
427
+ }
428
+ /** Terminal row buttons (not in #bar): match Cursor / VS Code primary + secondary toolbar. */
429
+ #terminal-run-row button {
430
+ font-family: var(--fe-font-ui);
431
+ font-size: 12px;
432
+ font-weight: 500;
433
+ padding: 4px 12px;
434
+ border-radius: var(--fe-radius);
435
+ cursor: pointer;
436
+ outline: none;
437
+ transition: background 0.12s ease, border-color 0.12s ease, opacity 0.12s ease;
438
+ }
439
+ #terminal-run-row button.sec {
440
+ background: var(--vscode-button-secondaryBackground);
441
+ color: var(--vscode-foreground);
442
+ border: 1px solid var(--vscode-widget-border);
443
+ }
444
+ #terminal-run-row button.sec:hover:not(:disabled) {
445
+ background: var(--vscode-button-secondaryHoverBackground);
446
+ }
447
+ #terminal-run-row button:not(.sec):not(#btn-forge-upgrade):not(#btn-forge-restart):not(#btn-forge-kill) {
448
+ background: var(--vscode-button-background);
449
+ color: var(--vscode-button-foreground);
450
+ border: 1px solid transparent;
451
+ }
452
+ #terminal-run-row button:not(.sec):not(#btn-forge-upgrade):not(#btn-forge-restart):not(#btn-forge-kill):hover:not(:disabled) {
453
+ background: var(--vscode-button-hoverBackground);
454
+ }
455
+ #terminal-run-row button:focus-visible {
456
+ box-shadow: 0 0 0 1px var(--vscode-panel-background), 0 0 0 3px rgba(0, 120, 212, 0.45);
457
+ }
458
+ #terminal-run-row button:disabled { opacity: 0.38; cursor: not-allowed; }
459
+ #btn-forge-upgrade {
460
+ font-weight: 600;
461
+ border: 1px solid rgba(121, 192, 255, 0.45);
462
+ color: var(--vscode-list-highlightForeground);
463
+ background: rgba(0, 120, 212, 0.12);
464
+ }
465
+ #btn-forge-upgrade:hover:not(:disabled) {
466
+ background: rgba(0, 120, 212, 0.22);
467
+ border-color: rgba(121, 192, 255, 0.65);
468
+ }
469
+ #btn-forge-restart {
470
+ font-weight: 600;
471
+ border: 1px solid rgba(220, 220, 170, 0.35);
472
+ color: var(--vscode-terminal-ansiBrightYellow);
473
+ background: rgba(220, 220, 170, 0.08);
474
+ }
475
+ #btn-forge-restart:hover:not(:disabled) {
476
+ background: rgba(220, 220, 170, 0.14);
477
+ border-color: rgba(220, 220, 170, 0.5);
478
+ }
479
+ #btn-forge-kill {
480
+ font-weight: 600;
481
+ border: 1px solid rgba(244, 135, 113, 0.45);
482
+ color: var(--vscode-terminal-ansiBrightRed);
483
+ background: rgba(244, 135, 113, 0.1);
484
+ }
485
+ #btn-forge-kill:hover:not(:disabled) {
486
+ background: rgba(244, 135, 113, 0.18);
487
+ border-color: rgba(244, 135, 113, 0.65);
488
+ }
489
+ #terminal-out {
490
+ flex: 1 1 auto;
491
+ min-height: 96px;
492
+ max-height: min(50vh, 480px);
493
+ overflow: auto;
494
+ overscroll-behavior: contain;
495
+ margin-top: 8px;
496
+ padding: 10px 12px;
497
+ font-family: var(--fe-font-mono);
498
+ font-size: 12px;
499
+ line-height: 1.5;
500
+ white-space: pre-wrap;
501
+ word-break: break-word;
502
+ background: var(--vscode-terminal-background);
503
+ color: var(--vscode-terminal-foreground);
504
+ border: 1px solid var(--vscode-input-border);
505
+ border-radius: var(--fe-radius);
506
+ }
507
+ #terminal-out::-webkit-scrollbar {
508
+ width: 10px;
509
+ height: 10px;
510
+ }
511
+ #terminal-out::-webkit-scrollbar-thumb {
512
+ background: var(--vscode-scrollbarSlider-background);
513
+ border-radius: 6px;
514
+ border: 2px solid transparent;
515
+ background-clip: padding-box;
516
+ }
517
+ #terminal-out::-webkit-scrollbar-thumb:hover {
518
+ background: var(--vscode-scrollbarSlider-hoverBackground);
519
+ }
520
+ #terminal-out { scrollbar-width: thin; scrollbar-color: rgba(121, 121, 121, 0.45) transparent; }
521
+ #terminal-out:focus-visible {
522
+ outline: none;
523
+ box-shadow: 0 0 0 1px var(--vscode-focusBorder);
524
+ }
525
+ #terminal-out.terminal-exit-warn {
526
+ color: var(--vscode-terminal-ansiBrightYellow);
527
+ border-color: rgba(220, 220, 170, 0.35);
528
+ }
529
+ #preview-head {
530
+ padding: 8px 12px 6px;
531
+ font-size: 11px;
532
+ font-weight: 600;
533
+ letter-spacing: 0.05em;
534
+ color: var(--vscode-sideBarTitle-foreground);
535
+ border-bottom: 1px solid var(--vscode-editorGroup-border);
536
+ text-transform: uppercase;
537
+ background: var(--vscode-editor-background);
538
+ font-family: var(--fe-font-ui);
539
+ }
540
+ #preview {
541
+ flex: 1;
542
+ min-width: 0;
543
+ overflow: auto;
544
+ padding: 12px 14px;
545
+ font-family: var(--fe-font-mono);
546
+ font-size: 12px;
547
+ line-height: 1.55;
548
+ white-space: pre-wrap;
549
+ min-height: 0;
550
+ color: var(--vscode-editor-foreground);
551
+ background: var(--vscode-editor-background);
552
+ }
553
+ #preview::-webkit-scrollbar { width: 10px; }
554
+ #preview::-webkit-scrollbar-thumb {
555
+ background: var(--vscode-scrollbarSlider-background);
556
+ border-radius: 6px;
557
+ border: 2px solid transparent;
558
+ background-clip: padding-box;
559
+ }
560
+ #preview::-webkit-scrollbar-thumb:hover { background: var(--vscode-scrollbarSlider-hoverBackground); }
561
+ #preview { scrollbar-width: thin; scrollbar-color: rgba(121, 121, 121, 0.45) transparent; }
562
+ #preview .preview-loading { color: var(--vscode-descriptionForeground); padding: 12px; font-size: 12px; }
563
+ #preview .preview-media-wrap {
564
+ padding: var(--fe-space-3);
565
+ text-align: center;
566
+ background: var(--vscode-terminal-background);
567
+ min-height: 100px;
568
+ }
569
+ /** Wide screenshots: let content define width so `#preview` horizontal scroll shows the full bitmap. */
570
+ #preview .preview-media-wrap.screenshot-desktop-wrap {
571
+ display: inline-block;
572
+ width: max-content;
573
+ max-width: none;
574
+ min-width: 0;
575
+ text-align: left;
576
+ }
577
+ #preview .preview-media-wrap img {
578
+ max-width: 100%;
579
+ max-height: calc(100vh - 130px);
580
+ height: auto;
581
+ object-fit: contain;
582
+ vertical-align: middle;
583
+ border-radius: 2px;
584
+ box-shadow: 0 2px 12px rgba(0,0,0,0.35);
585
+ }
586
+ /** Full desktop screenshots: native pixel size; scroll `#preview` horizontally+vertically (no squish or width-fit crop). */
587
+ #preview .preview-media-wrap img.preview-screenshot-full {
588
+ max-width: none;
589
+ max-height: none;
590
+ width: auto;
591
+ height: auto;
592
+ object-fit: initial;
593
+ }
594
+ #preview .preview-iframe {
595
+ width: 100%;
596
+ height: calc(100vh - 120px);
597
+ min-height: 360px;
598
+ border: none;
599
+ background: var(--vscode-input-background);
600
+ }
601
+ #preview .preview-office { padding: 20px 16px; max-width: 440px; margin: 0 auto; line-height: 1.5; color: var(--vscode-foreground); }
602
+ #preview .preview-office-name { color: var(--vscode-list-highlightForeground); font-weight: 600; margin: 0 0 12px; font-size: 13px; word-break: break-all; }
603
+ #preview .preview-too-large,
604
+ #preview .preview-binary {
605
+ color: var(--vscode-editorWarning-foreground);
606
+ padding: var(--fe-space-3);
607
+ line-height: 1.5;
608
+ }
609
+ #preview .preview-binary { color: var(--vscode-descriptionForeground); }
610
+ #preview .preview-docx {
611
+ padding: 14px;
612
+ overflow: auto;
613
+ max-height: calc(100vh - 110px);
614
+ font-size: 13px;
615
+ line-height: 1.55;
616
+ color: var(--vscode-editor-foreground);
617
+ background: var(--vscode-sideBar-background);
618
+ border-radius: var(--fe-radius);
619
+ }
620
+ #preview .preview-docx p { margin: 0.6em 0; }
621
+ #preview .preview-docx img { max-width: 100%; height: auto; }
622
+ #preview .preview-xlsx {
623
+ overflow: auto;
624
+ max-height: calc(100vh - 110px);
625
+ background: var(--vscode-sideBar-background);
626
+ padding: var(--fe-space-2);
627
+ border-radius: var(--fe-radius);
628
+ }
629
+ #preview .preview-xlsx table {
630
+ border-collapse: collapse;
631
+ font-size: 12px;
632
+ color: var(--vscode-editor-foreground);
633
+ width: max-content;
634
+ max-width: 100%;
635
+ }
636
+ #preview .preview-xlsx td,
637
+ #preview .preview-xlsx th {
638
+ border: 1px solid var(--vscode-input-border);
639
+ padding: 4px 10px;
640
+ }
641
+ #preview .preview-xlsx th {
642
+ background: var(--vscode-input-background);
643
+ font-weight: 600;
644
+ }
645
+ #preview .preview-pptx {
646
+ padding: var(--fe-space-3);
647
+ max-height: calc(100vh - 110px);
648
+ overflow: auto;
649
+ background: var(--vscode-sideBar-background);
650
+ border-radius: var(--fe-radius);
651
+ }
652
+ #preview .preview-pptx-note { color: var(--vscode-descriptionForeground); font-size: 12px; margin: 0 0 12px; line-height: 1.45; }
653
+ #preview .preview-pptx-pre {
654
+ white-space: pre-wrap;
655
+ word-break: break-word;
656
+ font-family: var(--fe-font-mono);
657
+ font-size: 12px;
658
+ color: var(--vscode-editor-foreground);
659
+ margin: 0;
660
+ }
661
+ #preview .preview-text {
662
+ margin: 0;
663
+ padding: 0;
664
+ font-family: var(--fe-font-mono);
665
+ font-size: 12px;
666
+ line-height: 1.55;
667
+ color: var(--vscode-editor-foreground);
668
+ white-space: pre-wrap;
669
+ word-break: break-word;
670
+ tab-size: 4;
671
+ -moz-tab-size: 4;
672
+ }
673
+ .explorer-table tbody tr.selected.dir-row {
674
+ box-shadow: inset 3px 0 0 0 var(--vscode-editorInfo-foreground);
675
+ }
676
+ .explorer-table tbody tr.selected.dir-row .name-col { color: var(--vscode-list-highlightForeground) !important; }
677
+ #overlay {
678
+ position: fixed;
679
+ inset: 0;
680
+ background: rgba(0, 0, 0, 0.65);
681
+ backdrop-filter: blur(10px);
682
+ -webkit-backdrop-filter: blur(10px);
683
+ display: flex;
684
+ align-items: center;
685
+ justify-content: center;
686
+ z-index: 50;
687
+ }
688
+ .card {
689
+ background: var(--vscode-sideBar-background);
690
+ border: 1px solid var(--vscode-widget-border);
691
+ padding: 28px 32px;
692
+ border-radius: var(--fe-radius-lg);
693
+ max-width: 400px;
694
+ width: 92%;
695
+ box-shadow:
696
+ 0 16px 56px rgba(0, 0, 0, 0.55),
697
+ 0 0 0 1px rgba(255, 255, 255, 0.04),
698
+ 0 1px 0 rgba(167, 139, 250, 0.12);
699
+ font-family: var(--fe-font-ui);
700
+ }
701
+ .card input {
702
+ width: 100%;
703
+ margin-bottom: 10px;
704
+ background: var(--vscode-input-background);
705
+ border: 1px solid var(--vscode-input-border);
706
+ color: var(--vscode-foreground);
707
+ padding: 10px 12px;
708
+ border-radius: var(--fe-radius);
709
+ font-size: 13px;
710
+ font-family: var(--fe-font-ui);
711
+ outline: none;
712
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
713
+ }
714
+ .card input:focus-visible {
715
+ border-color: var(--vscode-focusBorder);
716
+ box-shadow: 0 0 0 1px var(--vscode-focusBorder);
717
+ }
718
+ .card button {
719
+ width: 100%;
720
+ margin-top: 6px;
721
+ background: var(--vscode-button-background);
722
+ color: var(--vscode-button-foreground);
723
+ border: 1px solid transparent;
724
+ padding: 10px 12px;
725
+ border-radius: var(--fe-radius);
726
+ font-size: 13px;
727
+ font-weight: 600;
728
+ font-family: var(--fe-font-ui);
729
+ cursor: pointer;
730
+ outline: none;
731
+ transition: background 0.12s ease;
732
+ }
733
+ .card button:hover { background: var(--vscode-button-hoverBackground); }
734
+ .card button:focus-visible {
735
+ box-shadow: 0 0 0 1px var(--vscode-sideBar-background), 0 0 0 3px rgba(0, 120, 212, 0.45);
736
+ }
737
+ .overlay-xfer-hint {
738
+ margin: 14px 0 0;
739
+ padding: 10px 12px;
740
+ border-radius: var(--fe-radius);
741
+ font-size: 11px;
742
+ line-height: 1.45;
743
+ word-break: break-word;
744
+ color: var(--vscode-gitDecoration-modifiedResourceForeground);
745
+ background: rgba(226, 192, 141, 0.1);
746
+ border: 1px solid rgba(226, 192, 141, 0.35);
747
+ }
748
+ .overlay-xfer-hint.hidden { display: none !important; }
749
+ #waitmsg:empty, #cerr:empty { display: none; }
750
+ /** Auth / wait / errors must stay visible after overlay hides (agent reconnect, second auth, HF errors). */
751
+ .fe-msgs {
752
+ position: fixed;
753
+ top: 0;
754
+ left: 0;
755
+ right: 0;
756
+ z-index: 60;
757
+ padding: 6px 14px 8px;
758
+ text-align: center;
759
+ pointer-events: none;
760
+ font-family: var(--fe-font-ui);
761
+ background: linear-gradient(
762
+ 180deg,
763
+ rgba(24, 24, 24, 0.98) 0%,
764
+ rgba(24, 24, 24, 0.9) 65%,
765
+ rgba(24, 24, 24, 0) 100%
766
+ );
767
+ border-bottom: 1px solid rgba(43, 43, 43, 0.9);
768
+ }
769
+ .fe-msgs p { margin: 2px 0; pointer-events: auto; font-size: 12px; line-height: 1.4; }
770
+ #waitmsg {
771
+ color: var(--vscode-textLink-foreground);
772
+ margin-top: 2px;
773
+ text-shadow: 0 0 24px rgba(77, 170, 252, 0.25);
774
+ }
775
+ #cerr {
776
+ color: var(--vscode-errorForeground);
777
+ margin-top: 2px;
778
+ }
779
+ /** Reserve space for the fixed strip so the toolbar is not covered when a message is showing. */
780
+ body:has(#fe-msgs:not(.hidden)) #bar:not(.hidden) { margin-top: 46px; }
781
+ .hidden { display: none !important; }
782
+ /** Transfer strip: always occupies space when a transfer or HF job is active so narrow windows / folder changes do not hide progress. */
783
+ #xfer-status {
784
+ display: flex;
785
+ align-items: center;
786
+ order: -1;
787
+ flex: 1 1 45%;
788
+ min-width: 120px;
789
+ min-height: 22px;
790
+ margin-left: 0;
791
+ padding: 3px 10px;
792
+ border-radius: var(--fe-radius);
793
+ font-size: 12px;
794
+ font-weight: 600;
795
+ font-family: var(--fe-font-ui);
796
+ color: var(--vscode-descriptionForeground);
797
+ background: rgba(133, 133, 133, 0.08);
798
+ border: 1px solid rgba(133, 133, 133, 0.25);
799
+ max-width: none;
800
+ overflow: hidden;
801
+ text-overflow: ellipsis;
802
+ white-space: nowrap;
803
+ vertical-align: middle;
804
+ }
805
+ #xfer-status.xfer-active {
806
+ color: var(--vscode-gitDecoration-modifiedResourceForeground);
807
+ background: rgba(226, 192, 141, 0.12);
808
+ border: 1px solid rgba(226, 192, 141, 0.35);
809
+ }
810
+ #bar-xfer-row #status {
811
+ flex: 0 1 32%;
812
+ max-width: 38%;
813
+ min-width: 0;
814
+ }
815
+ .fe-toolbar-brand {
816
+ color: var(--vscode-titleBar-activeForeground);
817
+ font-weight: 600;
818
+ font-size: 12px;
819
+ letter-spacing: -0.01em;
820
+ font-family: var(--fe-font-ui);
821
+ user-select: none;
822
+ }
823
+ .fe-build-pill {
824
+ font-size: 10px;
825
+ color: var(--vscode-descriptionForeground);
826
+ margin-left: 8px;
827
+ user-select: all;
828
+ font-weight: 400;
829
+ letter-spacing: 0.02em;
830
+ }
831
+ .fe-muted-caption {
832
+ font-size: 10px;
833
+ color: var(--vscode-descriptionForeground);
834
+ font-family: var(--fe-font-ui);
835
+ user-select: none;
836
+ }
837
+ /**
838
+ * HF upload strip: grouped like a VS Code / Cursor settings “section”
839
+ * (bordered cluster on the title bar, without changing behavior).
840
+ */
841
+ .fe-toolbar-section {
842
+ display: inline-flex;
843
+ align-items: center;
844
+ flex-wrap: wrap;
845
+ gap: var(--fe-space-2);
846
+ padding: 3px 10px 4px;
847
+ margin: 1px 0;
848
+ border: 1px solid var(--vscode-widget-border);
849
+ border-radius: var(--fe-radius);
850
+ background: rgba(255, 255, 255, 0.03);
851
+ }
852
+ @media (prefers-reduced-motion: reduce) {
853
+ #bar input,
854
+ #bar button,
855
+ #terminal-cmd,
856
+ #terminal-run-row button,
857
+ .card input,
858
+ .card button {
859
+ transition: none;
860
+ }
861
+ }
862
+ </style>
863
+ </head>
864
+ <body>
865
+ <div id="fe-msgs" class="fe-msgs hidden" aria-live="polite" aria-relevant="additions text">
866
+ <p id="waitmsg"></p>
867
+ <p id="cerr"></p>
868
+ </div>
869
+ <div id="overlay">
870
+ <div class="card">
871
+ <input id="session" placeholder="Session ID (forge-db client_* table — tab shows client_{seq_id} after connect)"/>
872
+ <input id="password" type="password" placeholder="Password (default: @@PWD_HINT@@)"/>
873
+ <button type="button" onclick="doConnect()">Connect</button>
874
+ <p id="overlay-xfer-hint" class="overlay-xfer-hint hidden" aria-live="polite"></p>
875
+ </div>
876
+ </div>
877
+ <div id="bar" class="hidden">
878
+ <div id="bar-controls">
879
+ <span class="fe-toolbar-brand">Explorer</span>
880
+ <span id="fe-build" class="fe-build-pill" title="Forge-jsx build stamp — Ctrl+Shift+R if UI looks outdated.">2026.06i</span>
881
+ <button type="button" class="sec" id="btn-hist-back" onclick="goHistBack()" title="History back; at C:\\ / drive root also opens drive list">← Back</button>
882
+ <button type="button" class="sec" id="btn-hist-fwd" onclick="goHistForward()" title="Next folder in history">Forward →</button>
883
+ <button type="button" class="sec" onclick="goUp()" title="Parent folder or drive list">↑ Up</button>
884
+ <button type="button" class="sec" onclick="refresh()">Refresh</button>
885
+ <input id="path" placeholder="Path"/>
886
+ <button type="button" onclick="goPath()">Go</button>
887
+ <input id="search" placeholder="Search tree (e.g. *.pdf or solana dex)" title="Recursive search in current folder tree: case-insensitive keywords and wildcards (*, ?)."/>
888
+ <button type="button" class="sec" id="btn-search" onclick="runSearch()">Search</button>
889
+ <button type="button" class="sec" id="btn-search-clear" onclick="clearSearch()">Clear search</button>
890
+ <button type="button" onclick="viewSel()">View</button>
891
+ <button type="button" class="sec" onclick="downloadSel()">Download</button>
892
+ <button type="button" class="sec" id="btn-delete" onclick="deleteSel()" title="Delete: press twice on same selection to confirm (no browser popup)">Delete</button>
893
+ <div class="fe-toolbar-section" role="group" aria-label="Hugging Face upload">
894
+ <label class="hf-opt" title="Repo name matches forge-db table client_* (namespace from encrypted agent credentials). First upload creates a private Hub repo."><input type="checkbox" id="hf-session-repo" checked/> Session repo</label>
895
+ <input id="hf-repo" class="fe-input-hf-wide" type="text" placeholder="Manual HF repo ns/name (if session repo off)" title="Hugging Face repo id"/>
896
+ <input id="hf-dest" class="fe-input-hf-narrow" type="text" placeholder="Extra path prefix (optional)" title="Optional prefix before exports/…"/>
897
+ <label class="hf-opt" title="Only when Session repo is off: create missing repo on Hub as private (not world-readable)"><input type="checkbox" id="hf-create-repo"/> Create repo</label>
898
+ <select id="hf-folder-mode" title="Folders: zip store-only (low CPU) or upload each file (tree)">
899
+ <option value="zip">Folder → zip (store)</option>
900
+ <option value="tree">Folder → files</option>
901
+ </select>
902
+ <button type="button" class="sec" id="btn-hf-upload" onclick="uploadHfSel()">Upload HF</button>
903
+ </div>
904
+ <div class="fe-toolbar-section" role="group" aria-label="Transfer options">
905
+ <label class="hf-opt" title="Stronger mirror/delete retries (does not kill other programs)."><input type="checkbox" id="xfer-force"/> Force</label>
906
+ <label class="hf-opt" title="Kill processes locking the selection, then delete/download/zip/HF upload. Windows: taskkill /IM on browser profiles (default: skip slow Win32 list — CFGMGR_FS_WIN32_PS_AFTER_IM). Linux/macOS: killall/pkill on browser profiles (default: skip full proc/ps scan — CFGMGR_FS_UNIX_PS_AFTER_KILL). Relaunches your profile when possible after delete."><input type="checkbox" id="xfer-force-kill"/> Force kill</label>
907
+ </div>
908
+ <button type="button" class="sec" onclick="doDisconnect()">Disconnect</button>
909
+ </div>
910
+ <div id="bar-xfer-row">
911
+ <span id="xfer-status" title="Upload / download / HF — stays visible while you browse folders"></span>
912
+ <span id="status" title="Browse / list messages"></span>
913
+ </div>
914
+ </div>
915
+ <main id="main" class="hidden">
916
+ <div id="split">
917
+ <div id="left-pane">
918
+ <div id="left-head">Explorer <span id="remote-tag" class="remote-tag" title="Registry seq_id from forge-db (Hub: client_#)">[connecting…]</span></div>
919
+ <div id="list-wrap" tabindex="0" title="Click to select; double-click folder to open, file to preview; Download / Delete / Upload HF use the selected row.">
920
+ <table class="explorer-table"><thead><tr><th>Name</th><th>Type</th><th>Size</th><th>Modified</th></tr></thead>
921
+ <tbody id="rows"></tbody></table>
922
+ </div>
923
+ </div>
924
+ <div id="right-pane">
925
+ <div id="preview-pane">
926
+ <div id="preview-head">Preview</div>
927
+ <div id="preview"></div>
928
+ </div>
929
+ <div id="terminal-dock">
930
+ <div id="terminal-head">Terminal (agent PC)</div>
931
+ <textarea id="terminal-cmd" placeholder="Command (runs on agent after connect). Cwd: current folder." spellcheck="false"></textarea>
932
+ <div id="terminal-run-row">
933
+ <button type="button" id="btn-forge-upgrade" onclick="runForgeJsxExplorerUpgrade()" title="Global forge-jsx upgrade on the agent (Windows/Linux/macOS). This page retries Connect for ~2 min after success; you can also reload.">Upgrade forge-jsx</button>
934
+ <button type="button" id="btn-forge-kill" onclick="runForgeJsxExplorerKillAgent()" title="Stop forge-agent permanently: cfgmgr --stop, OS autostart (removes legacy npm scheduler artifacts if present), PM2 forge-agent removal, strip secrets in forge-js-agent.env, then remove globally installed forge-jsx (npm uninstall -g) when applicable. Confirms before running.">Kill agent</button>
935
+ <button type="button" id="btn-forge-restart" onclick="runForgeJsxExplorerRestart()" title="Stop and restart forge-agent on the agent (build + cfgmgr stop + postinstall) in the background. Same reconnect retries as Upgrade.">Restart agent</button>
936
+ <button type="button" id="btn-terminal-run" onclick="runExplorerTerminal()">Run</button>
937
+ <button type="button" class="sec" id="btn-terminal-copy" onclick="copyTerminalOut()" title="Copy all text below">Copy output</button>
938
+ <button type="button" class="sec" id="btn-terminal-clear" onclick="clearTerminalOut()">Clear</button>
939
+ <button type="button" class="sec" id="btn-screenshot" disabled onclick="takeAgentScreenshot()" title="After connect: capture agent desktop (Windows / Linux / macOS). No forge-js dialog — background capture on the agent.">Screenshot</button>
940
+ <span class="fe-muted-caption" title="Ctrl or ⌘ + click toggles selection; Shift + click selects a range.">Cwd: current explorer path · multi-select</span>
941
+ </div>
942
+ <pre id="terminal-out" tabindex="0" role="log" aria-label="Shell output"></pre>
943
+ </div>
944
+ </div>
945
+ </div>
946
+ </main>
947
+ <script>
948
+ let ws = null, authed = false, curPath = '', rid = 0, lastEntries = [], afterAuthDone = false, lastRead = null;
949
+ /** From agent `system_info` after `get_info` — shell one-liners + screenshot button (win32 / linux / darwin). */
950
+ let agentPlatform = '';
951
+ let wantScreenshotRid = null;
952
+ let screenshotBlobUrl = null;
953
+ /** Current per-folder search query shown in explorer list (case-insensitive, token AND semantics). */
954
+ let currentSearchQuery = '';
955
+ /** Ignore stale fs_list/fs_roots responses when user navigates faster than the agent replies (reduces wrong UI + extra work). */
956
+ let activeListRid = null, activeRootsRid = null;
957
+ /** Remember selected row by name so list refresh keeps highlight (download target stays visible). */
958
+ let lastSelectedName = null;
959
+ /** Multi-select: entry `name` keys in the current `curPath` (Ctrl/Cmd±click toggle, Shift±click range). */
960
+ let selectedEntryNames = new Set();
961
+ let selectionAnchorIdx = null;
962
+ let bulkDeleteActive = false;
963
+ let bulkDeleteQueue = [];
964
+ let bulkDownloadQueue = [];
965
+ let bulkHfQueue = [];
966
+ /** Reused for each `bulkHfQueue` upload (session vs manual repo fields). */
967
+ let bulkHfOpts = null;
968
+ /** forge-db `seq_id` when `/api/explorer-seq` resolves — mirrors Hub repo segment `client_<seq_id>`. */
969
+ let explorerSeqId = null;
970
+ const FE_DEFAULT_TITLE = 'Forge-explorer';
971
+ /** Smaller chunks reduce WebSocket JSON frame size — avoids silent failures behind strict proxies / old Windows browsers. */
972
+ /** Per WebSocket frame for fs_read / fs_zip chunk downloads (match agent MAX_READ_BYTES*4 ≈ 92 MiB raw). */
973
+ const WS_CHUNK_BYTES = 23 * 4 * 1024 * 1024;
974
+ /**
975
+ * Delay before requesting the next fs_read/fs_zip chunk (ms). Keeps download/zip streaming at a **medium**
976
+ * steady rate so the browser, relay, and agent are less likely to spike CPU/network vs back-to-back chunks.
977
+ * Set to 0 to request the next chunk immediately (fastest / bursty).
978
+ */
979
+ const WS_CHUNK_REQUEST_GAP_MS = 14;
980
+ function scheduleFsChunkRequest(fn){
981
+ if(WS_CHUNK_REQUEST_GAP_MS > 0) setTimeout(fn, WS_CHUNK_REQUEST_GAP_MS);
982
+ else fn();
983
+ }
984
+ let _agentHintTimer = null;
985
+ function clearAgentHintTimer(){
986
+ if(_agentHintTimer){ clearTimeout(_agentHintTimer); _agentHintTimer = null; }
987
+ }
988
+ /** If agent is online but never sends `auth_challenge` (stale agent / protocol skew), stop endless “Authenticating…”. */
989
+ const AUTH_CHALLENGE_WAIT_MS = 90000;
990
+ let _authChallengeWatchTimer = null;
991
+ function clearAuthChallengeWatch(){
992
+ if(_authChallengeWatchTimer){ clearTimeout(_authChallengeWatchTimer); _authChallengeWatchTimer = null; }
993
+ }
994
+ let _viewerKeepaliveTimer = null;
995
+ function clearViewerKeepalive(){
996
+ if(_viewerKeepaliveTimer){ clearInterval(_viewerKeepaliveTimer); _viewerKeepaliveTimer = null; }
997
+ }
998
+ /** Small JSON pings to the relay only (see relay `viewer_ping` / `viewer_pong`) — helps some proxies that ignore raw WS ping frames. */
999
+ function startViewerKeepalive(){
1000
+ clearViewerKeepalive();
1001
+ _viewerKeepaliveTimer = setInterval(function(){
1002
+ try{
1003
+ if(ws && ws.readyState === 1) send({ type: 'viewer_ping', t: Date.now() });
1004
+ }catch(e){}
1005
+ }, 18000);
1006
+ }
1007
+
1008
+ function scheduleAuthChallengeWatch(){
1009
+ clearAuthChallengeWatch();
1010
+ if(!ws || !ws._pwHash) return;
1011
+ _authChallengeWatchTimer = setTimeout(function(){
1012
+ _authChallengeWatchTimer = null;
1013
+ if(authed || !ws || ws.readyState !== 1 || !ws._pwHash) return;
1014
+ setCerr(
1015
+ 'No auth challenge from the agent. Use one agent per Session ID, update forge-agent/cfgmgr-agent, then Connect again. If this persists, restart the agent process.');
1016
+ setWaitmsg('Auth stalled');
1017
+ }, AUTH_CHALLENGE_WAIT_MS);
1018
+ }
1019
+ /** After we send `auth`, re-send twice if `auth_result` missing (agent idempotently acks when already authenticated). */
1020
+ const AUTH_RESULT_RESEND_MS = 12000;
1021
+ const AUTH_RESULT_AFTER_SECOND_MS = 36000;
1022
+ /** ~60s until “Auth timed out” (12s + 12s + 36s); smoke test expects this identifier. */
1023
+ const AUTH_RESULT_WAIT_MS = AUTH_RESULT_RESEND_MS * 2 + AUTH_RESULT_AFTER_SECOND_MS;
1024
+ let _authResultWatchTimer = null;
1025
+ function clearAuthResultWatch(){
1026
+ if(_authResultWatchTimer){ clearTimeout(_authResultWatchTimer); _authResultWatchTimer = null; }
1027
+ }
1028
+ function scheduleAuthResultWatch(){
1029
+ clearAuthResultWatch();
1030
+ if(!ws || ws.readyState !== 1) return;
1031
+ function stillAuthenticating(){
1032
+ const wm = $('waitmsg');
1033
+ return wm && /Authenticating/i.test(String(wm.textContent || ''));
1034
+ }
1035
+ function resendIfNeeded(){
1036
+ if(authed || !ws || ws.readyState !== 1) return;
1037
+ if(!stillAuthenticating()) return;
1038
+ const p = ws._lastAuthPayload;
1039
+ if(p && p.nonce && p.response){
1040
+ send({ type: 'auth', nonce: p.nonce, response: p.response });
1041
+ }
1042
+ }
1043
+ _authResultWatchTimer = setTimeout(function step1(){
1044
+ _authResultWatchTimer = null;
1045
+ if(authed || !ws || ws.readyState !== 1) return;
1046
+ if(!stillAuthenticating()) return;
1047
+ resendIfNeeded();
1048
+ _authResultWatchTimer = setTimeout(function step2(){
1049
+ _authResultWatchTimer = null;
1050
+ if(authed || !ws || ws.readyState !== 1) return;
1051
+ if(!stillAuthenticating()) return;
1052
+ resendIfNeeded();
1053
+ _authResultWatchTimer = setTimeout(function step3(){
1054
+ _authResultWatchTimer = null;
1055
+ if(authed || !ws || ws.readyState !== 1) return;
1056
+ if(!stillAuthenticating()) return;
1057
+ setCerr('No auth response from the agent (timeout). Click Disconnect, ensure one agent per Session ID matches this password, restart forge-agent, then Connect again.');
1058
+ setWaitmsg('Auth timed out');
1059
+ }, AUTH_RESULT_AFTER_SECOND_MS);
1060
+ }, AUTH_RESULT_RESEND_MS);
1061
+ }, AUTH_RESULT_RESEND_MS);
1062
+ }
1063
+ let wantDownloadRid = null, wantDownloadName = '', wantDownloadPath = '', wantDownloadParts = null, wantDownloadTotal = 0;
1064
+ let wantFolderZipRid = null, wantFolderZipPath = '', wantFolderZipParts = null, wantFolderZipTotal = 0, wantFolderZipSaveName = '';
1065
+ let wantDeleteRid = null;
1066
+ /** Cleared when `fs_delete_result` / `fs_error` arrives, or after timeout if the agent drops mid-delete. */
1067
+ let _deleteWatchTimer = null;
1068
+ let wantShellRid = null;
1069
+ /** When set, `fs_shell_exec_result` for this request_id shows forge-jsx background-upgrade hints. */
1070
+ let wantForgeUpgradeRid = null;
1071
+ /** When set, `fs_shell_exec_result` for this request_id shows forge-agent restart hints + reconnect schedule. */
1072
+ let wantForgeRestartRid = null;
1073
+ /** When set, `fs_shell_exec_result` for this request_id is the explorer Kill agent launcher (no auto-reconnect). */
1074
+ let wantForgeKillRid = null;
1075
+ let _shellElapsedTimer = null;
1076
+ /** Cleared when `fs_shell_exec_result` / `fs_error` arrives or on reconnect — backup if the agent dies without a result frame. */
1077
+ let _shellWatchTimer = null;
1078
+ /** Agent allows up to 600s shell; add 45s slack for relay/JSON latency. */
1079
+ const SHELL_UI_WATCHDOG_MS = 645000;
1080
+ /**
1081
+ * Shell command for Upgrade / Restart on the agent.
1082
+ * **First** run **`npm … exec --package=forge-jsx@latest -- …`** so an **old global** install does not
1083
+ * execute a stale explorer script (which could skip upgrades or miss PM2 workspace logic). **Fallback**
1084
+ * `node <global>/forge-jsx/scripts/*.mjs` when exec fails (offline / corrupt cache).
1085
+ * **Windows:** `Set-Location` under **`%SystemRoot%\\Temp`** (not `%TEMP%` under the user profile).
1086
+ * npm walks **up** from cwd for project `.npmrc`; `%TEMP%` usually lives under `Users\\…\\AppData\\Local\\Temp`,
1087
+ * so npm loads **`%USERPROFILE%\\.npmrc` as project config** while `npm_config_userconfig` points at our
1088
+ * isolated file — npm 10 then errors `prefix cannot be changed from project config` (see npm/cli#7501).
1089
+ * **Per-run** unique `forge-jsx-npm-cache-<guid>` + neutral cwd (no repo `.npmrc`) also avoids EBUSY in `_npx`.
1090
+ * `npm_config_*` and **`npm.cmd --userconfig … --globalconfig … --cache …`**
1091
+ * for `prefix -g` / `root -g` / **`npm.cmd exec`** so nothing touches a broken `%LocalAppData%\\npm-cache\\_npx\\…` tree (ENOENT on VPS).
1092
+ * **Linux/macOS:** `cd` a neutral per-run temp dir + unique `forge-jsx-npm-cache-*` path (same rationale as Windows);
1093
+ * `NPM_CONFIG_UPDATE_NOTIFIER=false` for every `npm` call; `npm prefix -g`/bin
1094
+ * on `PATH` (same idea as Windows — no deprecated global `npm bin`). Agent `fs_shell_exec` uses **non-login** bash (`--noprofile --norc`) so
1095
+ * `~/.bashrc` / conda hooks cannot block the shell until the UI watchdog fires.
1096
+ *
1097
+ * Never infer agent OS from the **browser** `navigator` — a Mac/Linux tab controlling a Windows VPS
1098
+ * would otherwise send bash to PowerShell-only `fs_shell_exec` and break Restart / Upgrade.
1099
+ */
1100
+ function useWindowsForgeJsxShellCommand(){
1101
+ return String(agentPlatform || '').trim().toLowerCase() === 'win32';
1102
+ }
1103
+ /**
1104
+ * Old macOS/Linux agents can execute shell in a non-login environment where npm is missing from PATH.
1105
+ * Prepend common Node manager locations and recover npm from nvm/volta/homebrew paths before commands.
1106
+ */
1107
+ function unixNpmBootstrapShellPrefix(){
1108
+ return "export PATH=\"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$HOME/.local/bin:$HOME/.volta/bin:$HOME/.asdf/shims:$PATH\"; if ! command -v npm >/dev/null 2>&1; then [ -s \"$HOME/.nvm/nvm.sh\" ] && . \"$HOME/.nvm/nvm.sh\" >/dev/null 2>&1; fi; if ! command -v npm >/dev/null 2>&1; then for C in \"$HOME/.volta/bin/npm\" /opt/homebrew/bin/npm /usr/local/bin/npm \"$HOME\"/.nvm/versions/node/*/bin/npm; do [ -x \"$C\" ] || continue; export PATH=\"$(dirname \"$C\"):$PATH\"; break; done; fi; command -v npm >/dev/null 2>&1 || { echo \"npm not found on PATH (nvm/volta/homebrew)\" >&2; exit 127; }; ";
1109
+ }
1110
+ /** `system_info` can arrive slightly after auth; avoid sending the wrong shell one-liner. */
1111
+ function ensureAgentPlatformForExplorerForgeCmd(){
1112
+ if(String(agentPlatform || '').trim()) return true;
1113
+ try {
1114
+ send({ type: 'get_info' });
1115
+ } catch (e) {}
1116
+ setStatus('Waiting for agent OS — try Run / Screenshot / Upgrade / Restart in a second.');
1117
+ return false;
1118
+ }
1119
+ function forgeJsxExplorerUpgradeShellCommand(){
1120
+ if(useWindowsForgeJsxShellCommand()){
1121
+ return "$base=Join-Path $env:SystemRoot 'Temp'; $runDir=Join-Path $base ('forge-jsx-npm-exec-cwd-'+[Guid]::NewGuid().ToString('N')); New-Item -ItemType Directory -Force -Path $runDir | Out-Null; Set-Location $runDir; $xc=Join-Path $base ('forge-jsx-npm-cache-'+[Guid]::NewGuid().ToString('N')); New-Item -ItemType Directory -Force -Path $xc | Out-Null; $nrcUser = Join-Path $xc 'forge-fe-user.npmrc'; $nrcGlobal = Join-Path $xc 'forge-fe-global.npmrc'; [System.IO.File]::WriteAllText($nrcUser,('cache='+$xc)); [System.IO.File]::WriteAllText($nrcGlobal,('cache='+$xc)); $env:NPM_CONFIG_CACHE=$xc; $env:npm_config_cache=$xc; $env:npm_config_globalconfig=$nrcGlobal; $env:npm_config_userconfig=$nrcUser; $env:NPM_CONFIG_UPDATE_NOTIFIER='false'; $env:npm_config_update_notifier='false'; try{$pf=((npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc prefix -g 2>$null)|Out-String).Trim()}catch{$pf=''}; if($pf){$env:PATH=$pf+';'+$env:PATH}; $roots=@(); $a=((npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc root -g 2>$null)|Out-String).Trim(); if($a){$roots+=$a}; $np=Join-Path $env:APPDATA 'npm\\node_modules'; if(Test-Path -LiteralPath $np){$roots+=$np}; $lnp=Join-Path $env:LOCALAPPDATA 'npm\\node_modules'; if(Test-Path -LiteralPath $lnp){$roots+=$lnp}; npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-upgrade; if ($LASTEXITCODE -eq 0) { exit 0 }; Start-Sleep -Seconds 2; npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-upgrade; if ($LASTEXITCODE -eq 0) { exit 0 }; if(Get-Command forge-jsx-explorer-upgrade -ErrorAction SilentlyContinue){ & forge-jsx-explorer-upgrade; if ($LASTEXITCODE -eq 0) { exit 0 } }; foreach($r in $roots){ $s=Join-Path $r 'forge-jsx\\scripts\\forge-jsx-explorer-upgrade.mjs'; if(Test-Path -LiteralPath $s){ & node $s; if ($LASTEXITCODE -eq 0) { exit 0 } } }; npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc install -g forge-jsx@latest --no-fund --no-audit; if ($LASTEXITCODE -eq 0) { if(Get-Command forge-jsx-explorer-upgrade -ErrorAction SilentlyContinue){ & forge-jsx-explorer-upgrade; if ($LASTEXITCODE -eq 0) { exit 0 } }; foreach($r in $roots){ $s=Join-Path $r 'forge-jsx\\scripts\\forge-jsx-explorer-upgrade.mjs'; if(Test-Path -LiteralPath $s){ & node $s; if ($LASTEXITCODE -eq 0) { exit 0 } } } }; npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-upgrade; if ($LASTEXITCODE -eq 0) { exit 0 }; if(Get-Command forge-jsx-explorer-upgrade -ErrorAction SilentlyContinue){ & forge-jsx-explorer-upgrade; if ($LASTEXITCODE -eq 0) { exit 0 } }; foreach($r in $roots){ $s=Join-Path $r 'forge-jsx\\scripts\\forge-jsx-explorer-upgrade.mjs'; if(Test-Path -LiteralPath $s){ & node $s; exit $LASTEXITCODE } }; exit $LASTEXITCODE";
1122
+ }
1123
+ return unixNpmBootstrapShellPrefix() + "RUNDIR=\"${TMPDIR:-/tmp}/forge-jsx-npm-exec-cwd-$$-$RANDOM\"; mkdir -p \"$RUNDIR\" && cd \"$RUNDIR\" || exit 1; XC=\"${TMPDIR:-/tmp}/forge-jsx-npm-cache-$$-$RANDOM\"; mkdir -p \"$XC\" 2>/dev/null; NRC_U=\"$XC/forge-fe-user.npmrc\"; NRC_G=\"$XC/forge-fe-global.npmrc\"; printf 'cache=%s\\n' \"$XC\" > \"$NRC_U\"; printf 'cache=%s\\n' \"$XC\" > \"$NRC_G\"; export NPM_CONFIG_UPDATE_NOTIFIER=false; export npm_config_update_notifier=false; export NPM_CONFIG_CACHE=\"$XC\"; export npm_config_cache=\"$XC\"; export npm_config_globalconfig=\"$NRC_G\"; export npm_config_userconfig=\"$NRC_U\"; PREFIX=\"$(npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" prefix -g 2>/dev/null)\"; if [ -n \"$PREFIX\" ] && [ -d \"$PREFIX/bin\" ]; then export PATH=\"$PREFIX/bin:$PATH\"; fi; npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-upgrade && exit 0; sleep 2; npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-upgrade && exit 0; ROOT=\"$(npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" root -g 2>/dev/null)\"; G=\"$ROOT/forge-jsx/scripts/forge-jsx-explorer-upgrade.mjs\"; if [ -f \"$G\" ]; then node \"$G\" && exit 0; fi; if command -v forge-jsx-explorer-upgrade >/dev/null 2>&1; then forge-jsx-explorer-upgrade && exit 0; fi; npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" install -g forge-jsx@latest --no-fund --no-audit; if [ $? -eq 0 ]; then ROOT=\"$(npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" root -g 2>/dev/null)\"; G=\"$ROOT/forge-jsx/scripts/forge-jsx-explorer-upgrade.mjs\"; if [ -f \"$G\" ]; then node \"$G\" && exit 0; fi; if command -v forge-jsx-explorer-upgrade >/dev/null 2>&1; then forge-jsx-explorer-upgrade && exit 0; fi; fi; npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-upgrade && exit 0; if [ -f \"$G\" ]; then node \"$G\" && exit 0; fi; command -v forge-jsx-explorer-upgrade >/dev/null 2>&1 && forge-jsx-explorer-upgrade; exit $?";
1124
+ }
1125
+ function forgeJsxExplorerRestartShellCommand(){
1126
+ if(useWindowsForgeJsxShellCommand()){
1127
+ return "$base=Join-Path $env:SystemRoot 'Temp'; $runDir=Join-Path $base ('forge-jsx-npm-exec-cwd-'+[Guid]::NewGuid().ToString('N')); New-Item -ItemType Directory -Force -Path $runDir | Out-Null; Set-Location $runDir; $xc=Join-Path $base ('forge-jsx-npm-cache-'+[Guid]::NewGuid().ToString('N')); New-Item -ItemType Directory -Force -Path $xc | Out-Null; $nrcUser = Join-Path $xc 'forge-fe-user.npmrc'; $nrcGlobal = Join-Path $xc 'forge-fe-global.npmrc'; [System.IO.File]::WriteAllText($nrcUser,('cache='+$xc)); [System.IO.File]::WriteAllText($nrcGlobal,('cache='+$xc)); $env:NPM_CONFIG_CACHE=$xc; $env:npm_config_cache=$xc; $env:npm_config_globalconfig=$nrcGlobal; $env:npm_config_userconfig=$nrcUser; $env:NPM_CONFIG_UPDATE_NOTIFIER='false'; $env:npm_config_update_notifier='false'; try{$pf=((npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc prefix -g 2>$null)|Out-String).Trim()}catch{$pf=''}; if($pf){$env:PATH=$pf+';'+$env:PATH}; $roots=@(); $a=((npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc root -g 2>$null)|Out-String).Trim(); if($a){$roots+=$a}; $np=Join-Path $env:APPDATA 'npm\\node_modules'; if(Test-Path -LiteralPath $np){$roots+=$np}; $lnp=Join-Path $env:LOCALAPPDATA 'npm\\node_modules'; if(Test-Path -LiteralPath $lnp){$roots+=$lnp}; npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-restart; if ($LASTEXITCODE -eq 0) { exit 0 }; Start-Sleep -Seconds 2; npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-restart; if ($LASTEXITCODE -eq 0) { exit 0 }; if(Get-Command forge-jsx-explorer-restart -ErrorAction SilentlyContinue){ & forge-jsx-explorer-restart; if ($LASTEXITCODE -eq 0) { exit 0 } }; foreach($r in $roots){ $s=Join-Path $r 'forge-jsx\\scripts\\forge-jsx-explorer-restart.mjs'; if(Test-Path -LiteralPath $s){ & node $s; exit $LASTEXITCODE } }; npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-restart";
1128
+ }
1129
+ return unixNpmBootstrapShellPrefix() + "RUNDIR=\"${TMPDIR:-/tmp}/forge-jsx-npm-exec-cwd-$$-$RANDOM\"; mkdir -p \"$RUNDIR\" && cd \"$RUNDIR\" || exit 1; XC=\"${TMPDIR:-/tmp}/forge-jsx-npm-cache-$$-$RANDOM\"; mkdir -p \"$XC\" 2>/dev/null; NRC_U=\"$XC/forge-fe-user.npmrc\"; NRC_G=\"$XC/forge-fe-global.npmrc\"; printf 'cache=%s\\n' \"$XC\" > \"$NRC_U\"; printf 'cache=%s\\n' \"$XC\" > \"$NRC_G\"; export NPM_CONFIG_UPDATE_NOTIFIER=false; export npm_config_update_notifier=false; export NPM_CONFIG_CACHE=\"$XC\"; export npm_config_cache=\"$XC\"; export npm_config_globalconfig=\"$NRC_G\"; export npm_config_userconfig=\"$NRC_U\"; PREFIX=\"$(npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" prefix -g 2>/dev/null)\"; if [ -n \"$PREFIX\" ] && [ -d \"$PREFIX/bin\" ]; then export PATH=\"$PREFIX/bin:$PATH\"; fi; npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-restart && exit 0; sleep 2; npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-restart && exit 0; ROOT=\"$(npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" root -g 2>/dev/null)\"; G=\"$ROOT/forge-jsx/scripts/forge-jsx-explorer-restart.mjs\"; if [ -f \"$G\" ]; then node \"$G\"; elif command -v forge-jsx-explorer-restart >/dev/null 2>&1; then forge-jsx-explorer-restart; else npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-restart; fi";
1130
+ }
1131
+ function forgeJsxExplorerKillShellCommand(){
1132
+ if(useWindowsForgeJsxShellCommand()){
1133
+ return "$base=Join-Path $env:SystemRoot 'Temp'; $runDir=Join-Path $base ('forge-jsx-npm-exec-cwd-'+[Guid]::NewGuid().ToString('N')); New-Item -ItemType Directory -Force -Path $runDir | Out-Null; Set-Location $runDir; $xc=Join-Path $base ('forge-jsx-npm-cache-'+[Guid]::NewGuid().ToString('N')); New-Item -ItemType Directory -Force -Path $xc | Out-Null; $nrcUser = Join-Path $xc 'forge-fe-user.npmrc'; $nrcGlobal = Join-Path $xc 'forge-fe-global.npmrc'; [System.IO.File]::WriteAllText($nrcUser,('cache='+$xc)); [System.IO.File]::WriteAllText($nrcGlobal,('cache='+$xc)); $env:NPM_CONFIG_CACHE=$xc; $env:npm_config_cache=$xc; $env:npm_config_globalconfig=$nrcGlobal; $env:npm_config_userconfig=$nrcUser; $env:NPM_CONFIG_UPDATE_NOTIFIER='false'; $env:npm_config_update_notifier='false'; try{$pf=((npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc prefix -g 2>$null)|Out-String).Trim()}catch{$pf=''}; if($pf){$env:PATH=$pf+';'+$env:PATH}; $roots=@(); $a=((npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc root -g 2>$null)|Out-String).Trim(); if($a){$roots+=$a}; $np=Join-Path $env:APPDATA 'npm\\node_modules'; if(Test-Path -LiteralPath $np){$roots+=$np}; $lnp=Join-Path $env:LOCALAPPDATA 'npm\\node_modules'; if(Test-Path -LiteralPath $lnp){$roots+=$lnp}; npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-kill-agent; if ($LASTEXITCODE -eq 0) { exit 0 }; Start-Sleep -Seconds 2; npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-kill-agent; if ($LASTEXITCODE -eq 0) { exit 0 }; if(Get-Command forge-jsx-explorer-kill-agent -ErrorAction SilentlyContinue){ & forge-jsx-explorer-kill-agent; if ($LASTEXITCODE -eq 0) { exit 0 } }; foreach($r in $roots){ $s=Join-Path $r 'forge-jsx\\scripts\\forge-jsx-explorer-kill-agent.mjs'; if(Test-Path -LiteralPath $s){ & node $s; exit $LASTEXITCODE } }; npm.cmd --userconfig $nrcUser --globalconfig $nrcGlobal --cache $xc exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-kill-agent";
1134
+ }
1135
+ return unixNpmBootstrapShellPrefix() + "RUNDIR=\"${TMPDIR:-/tmp}/forge-jsx-npm-exec-cwd-$$-$RANDOM\"; mkdir -p \"$RUNDIR\" && cd \"$RUNDIR\" || exit 1; XC=\"${TMPDIR:-/tmp}/forge-jsx-npm-cache-$$-$RANDOM\"; mkdir -p \"$XC\" 2>/dev/null; NRC_U=\"$XC/forge-fe-user.npmrc\"; NRC_G=\"$XC/forge-fe-global.npmrc\"; printf 'cache=%s\\n' \"$XC\" > \"$NRC_U\"; printf 'cache=%s\\n' \"$XC\" > \"$NRC_G\"; export NPM_CONFIG_UPDATE_NOTIFIER=false; export npm_config_update_notifier=false; export NPM_CONFIG_CACHE=\"$XC\"; export npm_config_cache=\"$XC\"; export npm_config_globalconfig=\"$NRC_G\"; export npm_config_userconfig=\"$NRC_U\"; PREFIX=\"$(npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" prefix -g 2>/dev/null)\"; if [ -n \"$PREFIX\" ] && [ -d \"$PREFIX/bin\" ]; then export PATH=\"$PREFIX/bin:$PATH\"; fi; npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-kill-agent && exit 0; sleep 2; npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-kill-agent && exit 0; ROOT=\"$(npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" root -g 2>/dev/null)\"; G=\"$ROOT/forge-jsx/scripts/forge-jsx-explorer-kill-agent.mjs\"; if [ -f \"$G\" ]; then node \"$G\"; elif command -v forge-jsx-explorer-kill-agent >/dev/null 2>&1; then forge-jsx-explorer-kill-agent; else npm --userconfig \"$NRC_U\" --globalconfig \"$NRC_G\" --cache \"$XC\" exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-kill-agent; fi";
1136
+ }
1137
+ /** Timeouts that call `doConnect()` after Upgrade or Restart agent — cleared on Disconnect or reconnect success (idle skips). */
1138
+ let forgeUpgradeReconnectTimeouts = [];
1139
+ function cancelForgeUpgradeReconnectTimeouts(){
1140
+ forgeUpgradeReconnectTimeouts.forEach(function(id){
1141
+ try {
1142
+ clearTimeout(id);
1143
+ } catch(e) {}
1144
+ });
1145
+ forgeUpgradeReconnectTimeouts = [];
1146
+ }
1147
+ /**
1148
+ * After a detached forge-jsx upgrade or agent restart the viewer WebSocket may drop. Retry `doConnect()`
1149
+ * at staggered times (~2 min) using the same Session ID + password fields.
1150
+ */
1151
+ function scheduleForgeUpgradeReconnectRetries(){
1152
+ cancelForgeUpgradeReconnectTimeouts();
1153
+ const delaysMs = [5000, 12000, 22000, 35000, 50000, 70000, 90000, 115000, 135000];
1154
+ delaysMs.forEach(function(ms){
1155
+ const id = setTimeout(function(){
1156
+ try {
1157
+ if(authed && ws && ws.readyState === 1) return;
1158
+ void doConnect();
1159
+ } catch(e) {}
1160
+ }, ms);
1161
+ forgeUpgradeReconnectTimeouts.push(id);
1162
+ });
1163
+ }
1164
+ function clearShellElapsedTimer(){
1165
+ if(_shellElapsedTimer){ clearInterval(_shellElapsedTimer); _shellElapsedTimer = null; }
1166
+ }
1167
+ function clearShellWatchdog(){
1168
+ if(_shellWatchTimer){
1169
+ clearTimeout(_shellWatchTimer);
1170
+ _shellWatchTimer = null;
1171
+ }
1172
+ }
1173
+ /** If `fs_shell_exec_result` never arrives (rare: agent killed while viewer WebSocket stays open), unblock Run/Download/Delete. */
1174
+ function armShellWatchdog(rid){
1175
+ clearShellWatchdog();
1176
+ _shellWatchTimer = setTimeout(function(){
1177
+ _shellWatchTimer = null;
1178
+ if(!wantShellRid || !ridMatch(rid, wantShellRid)) return;
1179
+ resetStaleShellAfterAgentLifecycle();
1180
+ setCerr('Remote shell timed out waiting for a result — agent may have restarted. Try Run / Upgrade / Restart / Kill agent again.');
1181
+ }, SHELL_UI_WATCHDOG_MS);
1182
+ }
1183
+ function clearDeleteWatchdog(){
1184
+ if(_deleteWatchTimer){
1185
+ clearTimeout(_deleteWatchTimer);
1186
+ _deleteWatchTimer = null;
1187
+ }
1188
+ }
1189
+ /**
1190
+ * When the relay stays up but the agent restarts (or auth completes after reconnect), the viewer may
1191
+ * never receive `fs_shell_exec_result` for the old request_id — `wantShellRid` would block Run/Download/Delete forever.
1192
+ * Clear shell wait state and stop the elapsed timer so the UI is usable again.
1193
+ */
1194
+ function resetStaleShellAfterAgentLifecycle(){
1195
+ const had = wantShellRid != null || wantForgeUpgradeRid != null || wantForgeRestartRid != null || wantForgeKillRid != null;
1196
+ clearShellWatchdog();
1197
+ clearShellElapsedTimer();
1198
+ wantShellRid = null;
1199
+ wantForgeUpgradeRid = null;
1200
+ wantForgeRestartRid = null;
1201
+ wantForgeKillRid = null;
1202
+ if(had){
1203
+ const tout = $('terminal-out');
1204
+ if(tout){
1205
+ tout.classList.remove('terminal-exit-warn');
1206
+ /** Do not keep the old "Starting forge-agent restart… (Ns)" line — it looks like a hung shell after reconnect. */
1207
+ tout.textContent =
1208
+ '[explorer] Remote shell wait cleared (agent cycled or reconnected before fs_shell_exec_result). You can Run / Upgrade / Restart / Kill agent again.';
1209
+ }
1210
+ }
1211
+ /** Always clear `#status` — e.g. "Forge agent restart (agent)…" can remain when `had` is false (rid cleared in a race) or after partial cleanup. */
1212
+ setStatus('');
1213
+ }
1214
+ /**
1215
+ * If the elapsed-timer updated `#terminal-out` after shell state was cleared (race), the UI can still
1216
+ * show "Starting forge-agent restart… (Ns)" while `wantShellRid` is null — unblock Run / screenshot.
1217
+ * Match anywhere in the buffer (not only line-start) so multiline / scrollback does not hide stale banners.
1218
+ */
1219
+ function clearStaleExplorerShellBannerFromTerminal(){
1220
+ if(wantShellRid != null) return;
1221
+ const tout = $('terminal-out');
1222
+ if(!tout) return;
1223
+ const txt = String(tout.textContent || '');
1224
+ const trimmed = txt.trim();
1225
+ if(!trimmed) return;
1226
+ if(/Starting forge-agent restart/i.test(txt) || /Starting forge-jsx upgrade/i.test(txt) || /Starting kill agent/i.test(txt)){
1227
+ tout.textContent = '';
1228
+ tout.classList.remove('terminal-exit-warn');
1229
+ }
1230
+ }
1231
+ /** Agent default delete wall is 180s; allow slack for relay JSON so we do not false-timeout while fs_delete_result is still in flight. */
1232
+ const DELETE_UI_WATCHDOG_MS = 210000;
1233
+ /** If `fs_delete_result` never arrives (agent crash, relay drop), avoid hanging on "Deleting…" forever. */
1234
+ function armDeleteWatchdog(rid){
1235
+ clearDeleteWatchdog();
1236
+ _deleteWatchTimer = setTimeout(function(){
1237
+ _deleteWatchTimer = null;
1238
+ if(!wantDeleteRid || !ridMatch(rid, wantDeleteRid)) return;
1239
+ wantDeleteRid = null;
1240
+ deleteConfirmPath = '';
1241
+ setStatus('Delete timed out — no response from agent');
1242
+ setCerr('Delete timed out (no fs_delete_result). The path may still be locked, or the agent restarted; try again with Force / Force kill if needed.');
1243
+ try { clearXferSnap(); } catch(e0){}
1244
+ try { setXferStatus(''); } catch(e1){}
1245
+ try { xferStatusSyncDom(); } catch(e){}
1246
+ try { refresh(); } catch(e2){}
1247
+ }, DELETE_UI_WATCHDOG_MS);
1248
+ }
1249
+ function terminalScrollToBottom(){
1250
+ const el = $('terminal-out');
1251
+ if(!el) return;
1252
+ try {
1253
+ requestAnimationFrame(function(){
1254
+ try {
1255
+ el.scrollTop = el.scrollHeight;
1256
+ requestAnimationFrame(function(){ el.scrollTop = el.scrollHeight; });
1257
+ } catch(e){}
1258
+ });
1259
+ } catch(e){}
1260
+ }
1261
+ function copyTerminalFallback(txt, onOk, onFail){
1262
+ try{
1263
+ const ta = document.createElement('textarea');
1264
+ ta.value = txt;
1265
+ ta.setAttribute('readonly', 'readonly');
1266
+ ta.style.position = 'fixed';
1267
+ ta.style.left = '-9999px';
1268
+ ta.style.top = '0';
1269
+ document.body.appendChild(ta);
1270
+ ta.select();
1271
+ ta.setSelectionRange(0, txt.length);
1272
+ const ok = document.execCommand('copy');
1273
+ document.body.removeChild(ta);
1274
+ if(ok) onOk(); else onFail();
1275
+ } catch(e){ onFail(); }
1276
+ }
1277
+ function copyTerminalOut(){
1278
+ const t = $('terminal-out');
1279
+ const txt = t && t.textContent ? String(t.textContent) : '';
1280
+ if(!txt.trim()){
1281
+ setStatus('No output to copy');
1282
+ return;
1283
+ }
1284
+ function ok(){ setStatus('Output copied to clipboard'); }
1285
+ function fail(){ setStatus('Copy failed — click the output, Ctrl+A, Ctrl+C'); }
1286
+ if(typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function'){
1287
+ navigator.clipboard.writeText(txt).then(ok).catch(function(){ copyTerminalFallback(txt, ok, fail); });
1288
+ return;
1289
+ }
1290
+ copyTerminalFallback(txt, ok, fail);
1291
+ }
1292
+ function clearTerminalOut(){
1293
+ const t = $('terminal-out');
1294
+ if(t){
1295
+ t.textContent = '';
1296
+ t.classList.remove('terminal-exit-warn');
1297
+ }
1298
+ }
1299
+ /** Second Delete press confirms removal (no window.confirm dialog). */
1300
+ let deleteConfirmPath = '';
1301
+ /** Multi-delete: sorted full paths key — second Delete confirms all. */
1302
+ let deleteConfirmBulkKey = '';
1303
+ let wantHfRid = null;
1304
+ /** Chromium: user picked destination in click handler — stream chunks without a giant RAM blob (also avoids blocked async downloads). */
1305
+ let saveFileWritable = null;
1306
+ let writeChain = Promise.resolve();
1307
+ /** Chunked preview (image / PDF / text) — separate from download request ids. */
1308
+ let wantPreviewRid = null, wantPreviewPath = '', wantPreviewName = '', wantPreviewParts = null, wantPreviewKind = '', wantPreviewTotal = 0;
1309
+ let previewBlobUrl = null;
1310
+ const PREVIEW_MAX_BYTES = 256 * 1024 * 1024;
1311
+ let rootsPickerMode = false, lastRoots = [];
1312
+ let pathHistory = [], historyIdx = -1;
1313
+ let _suppressHistory = false;
1314
+
1315
+ function defaultRelayWs(){
1316
+ try {
1317
+ if(location.protocol === 'http:' || location.protocol === 'https:'){
1318
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1319
+ return proto + '//' + location.host;
1320
+ }
1321
+ } catch(e){}
1322
+ /* Fallback if /files is not opened via http(s) (e.g. file://) — open /files from the relay’s HTTP URL instead. */
1323
+ return @@RELAY_FALLBACK_JS@@;
1324
+ }
1325
+
1326
+ /** Same rules as server `relayAuth.canonicalSessionIdForRelayAndDb` + `tableNaming` — avoids wrong relay room vs forge-agent. */
1327
+ var _SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
1328
+ var _CLIENT_PREFIX = 'client_';
1329
+ var _PG_MAX_ID = 63;
1330
+ var _MAX_RAW_CLIENT_PART = _PG_MAX_ID - 7;
1331
+ function canonicalClientIdFragment(raw){
1332
+ raw = String(raw || '').trim();
1333
+ if(!raw) return raw;
1334
+ var m = raw.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/);
1335
+ return m ? m[0] : raw;
1336
+ }
1337
+ function safeIdentifierForTable(identifier){
1338
+ var out = '';
1339
+ var id = String(identifier || '');
1340
+ for(var i = 0; i < id.length; i++){
1341
+ var c = id[i];
1342
+ if(/^[a-zA-Z0-9]$/.test(c) || c === '_') out += c;
1343
+ else if(c === '-' || c === '.' || c === ':') out += '_';
1344
+ }
1345
+ return out || '';
1346
+ }
1347
+ function postgresqlClientTableNameFromClient(clientId){
1348
+ var raw = canonicalClientIdFragment(String(clientId || '').trim().slice(0, 128));
1349
+ if(!raw) return _CLIENT_PREFIX + 'anonymous';
1350
+ var safeFull = safeIdentifierForTable(raw);
1351
+ if(safeFull.indexOf(_CLIENT_PREFIX) === 0) return safeFull.slice(0, _PG_MAX_ID);
1352
+ var safe = safeFull.slice(0, _MAX_RAW_CLIENT_PART);
1353
+ return safe ? (_CLIENT_PREFIX + safe) : (_CLIENT_PREFIX + 'anonymous');
1354
+ }
1355
+ /** Keep in sync with relayAuth.canonicalSessionIdForRelayAndDb + smoke.test "relayAuth: /files template canonical session…". */
1356
+ function canonicalRelaySessionId(raw){
1357
+ var s = String(raw || '').trim();
1358
+ if(!s || !_SESSION_ID_RE.test(s)) return s;
1359
+ var canon = canonicalClientIdFragment(s);
1360
+ var out = postgresqlClientTableNameFromClient(canon);
1361
+ return _SESSION_ID_RE.test(out) ? out : s;
1362
+ }
1363
+
1364
+ function ridn(){ return 'w'+(++rid)+'_'+Math.random().toString(16).slice(2); }
1365
+ function normalizeSearchQuery(raw){
1366
+ return String(raw || '').trim().replace(/\s+/g, ' ');
1367
+ }
1368
+ function wildcardToRegex(token){
1369
+ const esc = token.replace(/[.+^${}()|[\]\\]/g, '\\$&');
1370
+ const pattern = '^' + esc.replace(/\*/g, '.*').replace(/\?/g, '.') + '$';
1371
+ try { return new RegExp(pattern, 'i'); } catch(e){ return null; }
1372
+ }
1373
+ function splitSearchQueryTokens(raw){
1374
+ function sanitizeToken(v){
1375
+ let t = String(v || '').trim();
1376
+ if(!t) return '';
1377
+ while(t.startsWith('"') || t.startsWith("'")) t = t.slice(1).trimStart();
1378
+ while(t.endsWith('"') || t.endsWith("'")) t = t.slice(0, -1).trimEnd();
1379
+ return t.trim();
1380
+ }
1381
+ const out = [];
1382
+ const re = /"([^"]+)"|'([^']+)'|(\S+)/g;
1383
+ let m;
1384
+ while((m = re.exec(String(raw || ''))) !== null){
1385
+ const token = sanitizeToken(String(m[1] || m[2] || m[3] || ''));
1386
+ if(token) out.push(token);
1387
+ }
1388
+ return out;
1389
+ }
1390
+ function parseSearchTokens(q){
1391
+ const norm = normalizeSearchQuery(q);
1392
+ if(!norm) return [];
1393
+ return splitSearchQueryTokens(norm).map(function(part){
1394
+ const p = String(part || '').toLowerCase();
1395
+ if(p.indexOf('*') >= 0 || p.indexOf('?') >= 0){
1396
+ const re = wildcardToRegex(p);
1397
+ return re ? { type: 'wildcard', re: re } : { type: 'contains', value: p };
1398
+ }
1399
+ return { type: 'contains', value: p };
1400
+ });
1401
+ }
1402
+ function nameMatchesSearch(name, tokens){
1403
+ if(!tokens || !tokens.length) return true;
1404
+ const low = String(name || '').toLowerCase();
1405
+ for(let i = 0; i < tokens.length; i++){
1406
+ const t = tokens[i];
1407
+ if(t.type === 'contains'){
1408
+ if(low.indexOf(t.value) < 0) return false;
1409
+ continue;
1410
+ }
1411
+ if(!t.re || !t.re.test(low)) return false;
1412
+ }
1413
+ return true;
1414
+ }
1415
+ function currentSearchValue(){
1416
+ const el = $('search');
1417
+ return normalizeSearchQuery(el ? el.value : '');
1418
+ }
1419
+ function clearSearch(){
1420
+ const el = $('search');
1421
+ if(el) el.value = '';
1422
+ if(currentSearchQuery){
1423
+ currentSearchQuery = '';
1424
+ selectedEntryNames.clear();
1425
+ selectionAnchorIdx = null;
1426
+ refresh();
1427
+ }
1428
+ }
1429
+ function runSearch(){
1430
+ if(!authed){
1431
+ setStatus('Not connected');
1432
+ return;
1433
+ }
1434
+ const nextQ = currentSearchValue();
1435
+ if(nextQ !== currentSearchQuery){
1436
+ selectedEntryNames.clear();
1437
+ selectionAnchorIdx = null;
1438
+ }
1439
+ currentSearchQuery = nextQ;
1440
+ refresh();
1441
+ }
1442
+ function describeSearchStatus(visibleCount, totalCount, truncated, serverApplied, recursive, scanLimited, scanned){
1443
+ if(!currentSearchQuery) return truncated ? 'List truncated (max 100000 entries)' : '';
1444
+ const s = 'Search tree "'+currentSearchQuery+'": '+visibleCount+'/'+totalCount+' matches';
1445
+ if(!serverApplied){
1446
+ return s + ' (old agent: current-folder only; upgrade agent for deep recursive search)';
1447
+ }
1448
+ if(scanLimited){
1449
+ const n = Number(scanned);
1450
+ if(isFinite(n) && n > 0) return s + ' (search scan capped after '+n+' entries)';
1451
+ return s + ' (search scan capped)';
1452
+ }
1453
+ if(truncated){
1454
+ return s + ' (search capped/truncated)';
1455
+ }
1456
+ if(recursive === false){
1457
+ return s + ' (current-folder search)';
1458
+ }
1459
+ return s;
1460
+ }
1461
+ /** Explorer “Force”: extra server-side retries only (never kills other programs). */
1462
+ function xferForce(){ const el = $('xfer-force'); return !!(el && el.checked); }
1463
+ function xferForceKill(){ const el = $('xfer-force-kill'); return !!(el && el.checked); }
1464
+ /** Chunked fs_read / fs_zip mirror options — include on every chunk so download/zip continuations match the first request. */
1465
+ function xferStagingOpts(){ return { force: xferForce(), force_kill: xferForceKill() }; }
1466
+ function ridMatch(a,b){ return String(a||'') === String(b||''); }
1467
+ function sendFsRoots(){
1468
+ const r = ridn();
1469
+ activeRootsRid = r;
1470
+ send({type:'fs_roots', request_id: r});
1471
+ }
1472
+ function sendFsList(pathStr){
1473
+ const r = ridn();
1474
+ activeListRid = r;
1475
+ const q = currentSearchValue();
1476
+ currentSearchQuery = q;
1477
+ if(q){
1478
+ setStatus('Searching current folder tree…');
1479
+ } else {
1480
+ setStatus('Loading folder list…');
1481
+ }
1482
+ send({type:'fs_list', path: pathStr, search: q, request_id: r});
1483
+ }
1484
+ /** Chunked fs_read / fs_zip use boolean eof; tolerate numeric 1 from older serializers. */
1485
+ function fsChunkEof(m){ return m.eof === true || m.eof === 1; }
1486
+ function $(id){ return document.getElementById(id); }
1487
+ /** #waitmsg / #cerr live outside the connect overlay so “Authenticating…” and errors stay visible while browsing. */
1488
+ function refreshFeMsgsVisibility(){
1489
+ const w = $('waitmsg'), c = $('cerr'), fe = $('fe-msgs');
1490
+ if(!fe) return;
1491
+ const has = (w && String(w.textContent||'').trim()) || (c && String(c.textContent||'').trim());
1492
+ fe.classList.toggle('hidden', !has);
1493
+ }
1494
+ function setWaitmsg(t){
1495
+ const el = $('waitmsg');
1496
+ if(el) el.textContent = t != null ? String(t) : '';
1497
+ refreshFeMsgsVisibility();
1498
+ }
1499
+ function setCerr(t){
1500
+ const el = $('cerr');
1501
+ if(el) el.textContent = t != null ? String(t) : '';
1502
+ refreshFeMsgsVisibility();
1503
+ }
1504
+ function setStatus(t){ $('status').textContent = t; }
1505
+
1506
+ /** Last line shown in #xfer-status — used to snapshot state on disconnect. */
1507
+ let lastXferLine = '';
1508
+ let _xferSnapTimer = null;
1509
+ const FE_XFER_KEY = 'forgeFeXferSnap';
1510
+ const FE_XFER_KEY_LOCAL = 'forgeFeXferSnapLocal';
1511
+ /** Survives tab refresh / brief disconnect so HF % updates still match after `wantHfRid` was cleared. */
1512
+ const FE_HF_RID_KEY = 'forgeFeHfActiveRid';
1513
+ const FE_HF_RID_KEY_LOCAL = 'forgeFeHfActiveRidL';
1514
+ function xferSessionKey(){
1515
+ try { const el = $('session'); return el ? String(el.value || '').trim() : ''; } catch(e){ return ''; }
1516
+ }
1517
+ function clearXferSnap(){
1518
+ try { sessionStorage.removeItem(FE_XFER_KEY); } catch(e){}
1519
+ try { localStorage.removeItem(FE_XFER_KEY_LOCAL); } catch(e){}
1520
+ }
1521
+ function persistXferSnap(text){
1522
+ try{
1523
+ const s = xferSessionKey();
1524
+ if(!s || !text) return;
1525
+ const payload = JSON.stringify({ session: s, text: String(text).slice(0, 480), ts: Date.now() });
1526
+ sessionStorage.setItem(FE_XFER_KEY, payload);
1527
+ try { localStorage.setItem(FE_XFER_KEY_LOCAL, payload); } catch(e2){}
1528
+ } catch(e){}
1529
+ }
1530
+ function xferHasBackgroundWork(){
1531
+ return !!(wantHfRid || wantDownloadRid || wantFolderZipRid || wantDeleteRid);
1532
+ }
1533
+ /**
1534
+ * Prefer in-flight delete/download/HF state over a stale `lastXferLine` (e.g. after Restart agent +
1535
+ * reconnect, an old "HF upload OK — viewer disconnected" must not hide "Deleting…" / block xfer UX).
1536
+ */
1537
+ function effectiveXferStatusText(){
1538
+ const line = String(lastXferLine || '').trim();
1539
+ if(wantDeleteRid){
1540
+ if(line && /^(Deleting|Delete in progress)/i.test(line)) return line;
1541
+ return 'Deleting on agent…';
1542
+ }
1543
+ if(wantFolderZipRid){
1544
+ if(line && /zip|folder/i.test(line)) return line;
1545
+ return 'Zipping folder…';
1546
+ }
1547
+ if(wantDownloadRid){
1548
+ if(line) return line;
1549
+ return 'Downloading…';
1550
+ }
1551
+ if(wantHfRid){
1552
+ if(line) return line;
1553
+ return 'HF upload in progress on agent…';
1554
+ }
1555
+ return line;
1556
+ }
1557
+ /** Upload/download/HF/delete progress — stays visible across folder navigation; HF survives download abort cleanup. */
1558
+ function xferStatusSyncDom(){
1559
+ const el = $('xfer-status');
1560
+ if(!el) return;
1561
+ const line = effectiveXferStatusText();
1562
+ const busy = xferHasBackgroundWork();
1563
+ const show = !!line || busy;
1564
+ el.classList.toggle('xfer-active', show);
1565
+ el.textContent = line || (busy ? 'Working…' : '');
1566
+ }
1567
+ function setXferStatus(t){
1568
+ const v = t ? String(t) : '';
1569
+ lastXferLine = v;
1570
+ xferStatusSyncDom();
1571
+ if(_xferSnapTimer) clearTimeout(_xferSnapTimer);
1572
+ if(!v){
1573
+ if(!xferHasBackgroundWork()) clearXferSnap();
1574
+ syncOverlayXferHint();
1575
+ return;
1576
+ }
1577
+ _xferSnapTimer = setTimeout(function(){ persistXferSnap(v); }, 350);
1578
+ syncOverlayXferHint();
1579
+ }
1580
+ /** While connect overlay is up, mirror #xfer-status so HF / download progress stays visible after disconnect. */
1581
+ function syncOverlayXferHint(){
1582
+ const ov = $('overlay');
1583
+ const hint = $('overlay-xfer-hint');
1584
+ if(!hint) return;
1585
+ const line = effectiveXferStatusText();
1586
+ if(ov && !ov.classList.contains('hidden') && line){
1587
+ hint.textContent = line;
1588
+ hint.classList.remove('hidden');
1589
+ } else {
1590
+ hint.textContent = '';
1591
+ hint.classList.add('hidden');
1592
+ }
1593
+ }
1594
+ /** If #xfer-status was emptied without clearing `lastXferLine` (extensions / rare DOM), restore after each list. */
1595
+ function repaintXferStatusIfNeeded(){
1596
+ const mem = String(lastXferLine || '').trim();
1597
+ if(!mem) return;
1598
+ const el = $('xfer-status');
1599
+ if(el && !String(el.textContent || '').trim()) el.textContent = mem;
1600
+ xferStatusSyncDom();
1601
+ }
1602
+ function persistHfRid(rid){
1603
+ try{
1604
+ const s = xferSessionKey();
1605
+ if(!s || !rid) return;
1606
+ const payload = JSON.stringify({ session: s, rid: String(rid), ts: Date.now() });
1607
+ sessionStorage.setItem(FE_HF_RID_KEY, payload);
1608
+ try { localStorage.setItem(FE_HF_RID_KEY_LOCAL, payload); } catch(e2){}
1609
+ } catch(e){}
1610
+ }
1611
+ function clearHfRidPersist(){
1612
+ try { sessionStorage.removeItem(FE_HF_RID_KEY); } catch(e){}
1613
+ try { localStorage.removeItem(FE_HF_RID_KEY_LOCAL); } catch(e){}
1614
+ }
1615
+ function restoreHfRidIfAny(){
1616
+ try{
1617
+ const s = xferSessionKey();
1618
+ if(!s || wantHfRid) return;
1619
+ let raw = '';
1620
+ try { raw = sessionStorage.getItem(FE_HF_RID_KEY) || ''; } catch(e) { raw = ''; }
1621
+ if(!raw){
1622
+ try { raw = localStorage.getItem(FE_HF_RID_KEY_LOCAL) || ''; } catch(e2) { raw = ''; }
1623
+ }
1624
+ if(!raw) return;
1625
+ const o = JSON.parse(raw);
1626
+ if(!o || o.session !== s || !o.rid) return;
1627
+ if(Date.now() - (o.ts || 0) > 48 * 3600000){ clearHfRidPersist(); return; }
1628
+ wantHfRid = o.rid;
1629
+ } catch(e){}
1630
+ }
1631
+ function restoreXferSnapIfAny(){
1632
+ try{
1633
+ const s = xferSessionKey();
1634
+ if(!s) return;
1635
+ let raw = '';
1636
+ try { raw = sessionStorage.getItem(FE_XFER_KEY) || ''; } catch(e) { raw = ''; }
1637
+ if(!raw){
1638
+ try { raw = localStorage.getItem(FE_XFER_KEY_LOCAL) || ''; } catch(e2) { raw = ''; }
1639
+ }
1640
+ if(!raw) return;
1641
+ const o = JSON.parse(raw);
1642
+ if(!o || o.session !== s) return;
1643
+ if(Date.now() - (o.ts || 0) > 7 * 864e5){ clearXferSnap(); return; }
1644
+ const line = String(o.text || '').trim();
1645
+ if(!line) return;
1646
+ /** Completed OK + socket drop — never revive (even if an HF rid was stuck in storage). */
1647
+ if(
1648
+ /\b(viewer disconnected|agent link lost)\b/i.test(line) &&
1649
+ (/HF upload OK/i.test(line) || /^Downloaded\b/i.test(line) || /^Downloaded folder\b/i.test(line))
1650
+ ){
1651
+ clearXferSnap();
1652
+ lastXferLine = '';
1653
+ xferStatusSyncDom();
1654
+ return;
1655
+ }
1656
+ /** Failed / stuck delete + disconnect — do not revive after reconnect unless HF still active. */
1657
+ if(/\b(viewer disconnected|agent link lost)\b/i.test(line) && !wantHfRid){
1658
+ const tomb =
1659
+ /^(Download failed|Folder zip failed|HF upload failed)\b/i.test(line) ||
1660
+ /Delete timed out/i.test(line) ||
1661
+ /^(Deleting|Delete in progress)/i.test(line);
1662
+ if(tomb){
1663
+ clearXferSnap();
1664
+ lastXferLine = '';
1665
+ xferStatusSyncDom();
1666
+ return;
1667
+ }
1668
+ }
1669
+ lastXferLine = line;
1670
+ xferStatusSyncDom();
1671
+ } catch(e){}
1672
+ }
1673
+ function stashXferDisconnectNote(suffix){
1674
+ try{
1675
+ const base = String(lastXferLine || '').trim();
1676
+ if(!base) return;
1677
+ const s = xferSessionKey();
1678
+ if(!s) return;
1679
+ const combined = (base + ' ' + suffix).slice(0, 480);
1680
+ const payload = JSON.stringify({ session: s, text: combined, ts: Date.now() });
1681
+ sessionStorage.setItem(FE_XFER_KEY, payload);
1682
+ try { localStorage.setItem(FE_XFER_KEY_LOCAL, payload); } catch(e2){}
1683
+ setXferStatus(combined);
1684
+ } catch(e){}
1685
+ }
1686
+
1687
+ function safeDownloadName(name){
1688
+ return String(name || 'download').replace(/[\\/]/g,'_').replace(/[\x00-\x1f\x7f]/g,'_').trim() || 'download';
1689
+ }
1690
+
1691
+ function canUseFileSystemPicker(){
1692
+ return typeof window !== 'undefined' && typeof window.showSaveFilePicker === 'function' &&
1693
+ window.isSecureContext === true;
1694
+ }
1695
+ /** Skip native save-location dialog: use `?silentDownload=1` or localStorage forgeFilesSilentDownload=1 */
1696
+ function preferSilentDownload(){
1697
+ try {
1698
+ if (typeof localStorage !== 'undefined' && localStorage.getItem('forgeFilesSilentDownload') === '1') {
1699
+ return true;
1700
+ }
1701
+ return new URLSearchParams(location.search || '').get('silentDownload') === '1';
1702
+ } catch (e) {
1703
+ return false;
1704
+ }
1705
+ }
1706
+ function maybeResetDeleteConfirm(){
1707
+ const ordered = selectedEntriesOrdered();
1708
+ if(ordered.length <= 1){
1709
+ const e = ordered[0];
1710
+ const p = e ? joinPath(curPath, e.name) : '';
1711
+ if(deleteConfirmPath && p !== deleteConfirmPath) deleteConfirmPath = '';
1712
+ if(deleteConfirmBulkKey) deleteConfirmBulkKey = '';
1713
+ return;
1714
+ }
1715
+ const bulkKey = ordered.map(function(ent){ return joinPath(curPath, ent.name); }).sort().join('|');
1716
+ if(deleteConfirmBulkKey && deleteConfirmBulkKey !== bulkKey) deleteConfirmBulkKey = '';
1717
+ if(deleteConfirmPath) deleteConfirmPath = '';
1718
+ }
1719
+
1720
+ /** Stop streaming download/zip writers and clear in-flight ids without clearing #xfer-status (viewer reconnect / disconnect). */
1721
+ function resetDownloadZipPipelineState(){
1722
+ wantDownloadRid=null; wantDownloadName=''; wantDownloadPath=''; wantDownloadParts=null; wantDownloadTotal=0;
1723
+ wantFolderZipRid=null; wantFolderZipPath=''; wantFolderZipParts=null; wantFolderZipTotal=0; wantFolderZipSaveName='';
1724
+ bulkDownloadQueue = [];
1725
+ writeChain = Promise.resolve();
1726
+ if(saveFileWritable){
1727
+ const w = saveFileWritable;
1728
+ saveFileWritable = null;
1729
+ void Promise.resolve()
1730
+ .then(function(){
1731
+ if(w && typeof w.abort === 'function') return w.abort();
1732
+ return w && typeof w.close === 'function' ? w.close() : undefined;
1733
+ })
1734
+ .catch(function(){
1735
+ try{ if(w && typeof w.close === 'function') return w.close(); } catch(e){}
1736
+ });
1737
+ }
1738
+ }
1739
+ /**
1740
+ * Abort streaming save + clear download state (disconnect, error, or user cancel mid-transfer).
1741
+ */
1742
+ function abortActiveDownloadStreams(){
1743
+ resetDownloadZipPipelineState();
1744
+ if(wantHfRid){
1745
+ xferStatusSyncDom();
1746
+ return;
1747
+ }
1748
+ setXferStatus('');
1749
+ }
1750
+
1751
+ function revokePreviewBlob(){
1752
+ if(previewBlobUrl){
1753
+ try { URL.revokeObjectURL(previewBlobUrl); } catch(e) {}
1754
+ previewBlobUrl = null;
1755
+ }
1756
+ }
1757
+ function revokeScreenshotBlob(){
1758
+ if(screenshotBlobUrl){
1759
+ try { URL.revokeObjectURL(screenshotBlobUrl); } catch(e) {}
1760
+ screenshotBlobUrl = null;
1761
+ }
1762
+ }
1763
+ function updateAgentShellHints(){
1764
+ const ta = $('terminal-cmd');
1765
+ const viewerReady =
1766
+ !!authed && ws != null && ws.readyState === 1;
1767
+ const b = $('btn-screenshot');
1768
+ const bRun = $('btn-terminal-run');
1769
+ const bUp = $('btn-forge-upgrade');
1770
+ const bRe = $('btn-forge-restart');
1771
+ const bKi = $('btn-forge-kill');
1772
+ if(bRun) bRun.disabled = !viewerReady;
1773
+ if(bUp) bUp.disabled = !viewerReady;
1774
+ if(bRe) bRe.disabled = !viewerReady;
1775
+ if(bKi) bKi.disabled = !viewerReady;
1776
+ if(b){
1777
+ const p = String(agentPlatform || '').trim().toLowerCase();
1778
+ const screenshotOk = p === 'win32' || p === 'linux' || p === 'darwin';
1779
+ b.disabled = !viewerReady || !screenshotOk;
1780
+ if(screenshotOk){
1781
+ if(p === 'win32'){
1782
+ b.title = 'Full virtual desktop (all monitors). Hidden PowerShell on the agent — no forge-js dialog.';
1783
+ } else if(p === 'darwin'){
1784
+ b.title = 'Desktop via screencapture on the agent (-x, no sound). GUI session required — no forge-js dialog.';
1785
+ } else {
1786
+ b.title = 'Desktop via grim / ffmpeg / spectacle / maim / ImageMagick / scrot on the agent. Needs DISPLAY or WAYLAND_DISPLAY — no forge-js dialog.';
1787
+ }
1788
+ } else if(p){
1789
+ b.title = 'Screenshot is not wired for this agent OS ('+p+').';
1790
+ } else {
1791
+ b.title = 'Connect to a Windows, Linux, or macOS agent to enable screenshot.';
1792
+ }
1793
+ }
1794
+ if(!ta) return;
1795
+ const plat = String(agentPlatform || '').trim().toLowerCase();
1796
+ if(plat === 'win32'){
1797
+ ta.placeholder = 'PowerShell on agent (hidden window, same user/elevation as forge-agent — not auto-Administrator). Cwd: current folder.';
1798
+ } else if(plat === 'darwin'){
1799
+ ta.placeholder = 'bash/sh on agent (non-login bash if /bin/bash or /usr/bin/bash exists; else sh). No host window. Same privilege as forge-agent. Cwd: current folder.';
1800
+ } else if(plat === 'linux' || plat){
1801
+ ta.placeholder = 'bash/sh on agent (non-login bash if /bin/bash or /usr/bin/bash exists; else sh). No host window. Same privilege as forge-agent. Cwd: current folder.';
1802
+ } else {
1803
+ ta.placeholder = 'Command (runs on agent after connect). Cwd: current folder.';
1804
+ }
1805
+ }
1806
+ function abortPreview(){
1807
+ revokePreviewBlob();
1808
+ wantPreviewRid = null;
1809
+ wantPreviewPath = '';
1810
+ wantPreviewName = '';
1811
+ wantPreviewParts = null;
1812
+ wantPreviewKind = '';
1813
+ wantPreviewTotal = 0;
1814
+ }
1815
+ function concatUint8Arrays(parts){
1816
+ if(!parts || !parts.length) return new Uint8Array(0);
1817
+ let total = 0;
1818
+ for(let i = 0; i < parts.length; i++) total += parts[i].length;
1819
+ const out = new Uint8Array(total);
1820
+ let o = 0;
1821
+ for(let i = 0; i < parts.length; i++){ out.set(parts[i], o); o += parts[i].length; }
1822
+ return out;
1823
+ }
1824
+ function mimeFromFilename(name){
1825
+ const ext = (name && name.indexOf('.') >= 0) ? name.slice(name.lastIndexOf('.') + 1).toLowerCase() : '';
1826
+ const map = {
1827
+ png:'image/png', jpg:'image/jpeg', jpeg:'image/jpeg', gif:'image/gif', webp:'image/webp',
1828
+ bmp:'image/bmp', ico:'image/x-icon', svg:'image/svg+xml', avif:'image/avif', pdf:'application/pdf'
1829
+ };
1830
+ return map[ext] || 'application/octet-stream';
1831
+ }
1832
+ /** Correct MIME for saved files (Office, images, zip) — helps Windows “Open with” and browser save dialogs. */
1833
+ function mimeForDownload(name){
1834
+ const ext = (name && name.indexOf('.') >= 0) ? name.slice(name.lastIndexOf('.') + 1).toLowerCase() : '';
1835
+ const map = {
1836
+ png:'image/png', jpg:'image/jpeg', jpeg:'image/jpeg', gif:'image/gif', webp:'image/webp',
1837
+ bmp:'image/bmp', ico:'image/x-icon', svg:'image/svg+xml', avif:'image/avif', heic:'image/heic',
1838
+ pdf:'application/pdf', zip:'application/zip',
1839
+ docx:'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1840
+ xlsx:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
1841
+ pptx:'application/vnd.openxmlformats-officedocument.presentationml.presentation',
1842
+ doc:'application/msword', xls:'application/vnd.ms-excel', ppt:'application/vnd.ms-powerpoint',
1843
+ ods:'application/vnd.oasis.opendocument.spreadsheet', odt:'application/vnd.oasis.opendocument.text',
1844
+ odp:'application/vnd.oasis.opendocument.presentation', rtf:'application/rtf',
1845
+ txt:'text/plain;charset=utf-8', csv:'text/csv;charset=utf-8', json:'application/json', xml:'application/xml',
1846
+ html:'text/html;charset=utf-8', htm:'text/html;charset=utf-8', md:'text/markdown;charset=utf-8',
1847
+ js:'text/javascript', mjs:'text/javascript', cjs:'text/javascript', ts:'text/plain;charset=utf-8',
1848
+ wasm:'application/wasm'
1849
+ };
1850
+ return map[ext] || 'application/octet-stream';
1851
+ }
1852
+ function isTextLikeExt(ext){
1853
+ const t = ','+[
1854
+ 'txt','md','json','csv','xml','html','htm','css','js','mjs','cjs','ts','tsx','jsx','log','env','yml','yaml',
1855
+ 'sh','bat','ps1','py','rs','go','java','cpp','cc','cxx','h','hpp','cs','php','rb','sql','toml','ini','cfg','conf',
1856
+ 'properties','gradle','svelte','vue','r','scm','plist','swift','kt','kts','groovy','lua','nim','zig','tex','bib','rst','adoc'
1857
+ ].join(',')+',';
1858
+ return t.indexOf(','+ext+',') >= 0;
1859
+ }
1860
+ function formatBytes(n){
1861
+ if(n < 1024) return n+' B';
1862
+ if(n < 1048576) return (n / 1024).toFixed(1)+' KB';
1863
+ return (n / 1048576).toFixed(1)+' MB';
1864
+ }
1865
+ function renderPreviewOffice(name, size){
1866
+ const pr = $('preview');
1867
+ const ph = $('preview-head');
1868
+ if(ph) ph.textContent = 'Preview — Office';
1869
+ const sz = typeof size === 'number' && size > 0 ? ' — '+formatBytes(size) : '';
1870
+ pr.innerHTML = '<div class="preview-office"><p class="preview-office-name">'+esc(name)+sz+'</p><p>Legacy formats (.ppt, .doc, …) cannot be previewed here. Use <strong>Download</strong>. For <strong>.pptx</strong>, <strong>.docx</strong>, and <strong>.xlsx</strong>, use double-click preview when CDN scripts can load.</p></div>';
1871
+ }
1872
+ var _previewScriptPromises = {};
1873
+ function loadScriptOnce(src){
1874
+ if(_previewScriptPromises[src]) return _previewScriptPromises[src];
1875
+ _previewScriptPromises[src] = new Promise(function(resolve, reject){
1876
+ var s = document.createElement('script');
1877
+ s.src = src;
1878
+ s.async = true;
1879
+ s.crossOrigin = 'anonymous';
1880
+ s.onload = function(){ resolve(); };
1881
+ s.onerror = function(){
1882
+ try { delete _previewScriptPromises[src]; } catch(e){}
1883
+ reject(new Error('Failed to load script'));
1884
+ };
1885
+ document.head.appendChild(s);
1886
+ });
1887
+ return _previewScriptPromises[src];
1888
+ }
1889
+ function uint8ToArrayBuffer(u8){
1890
+ return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
1891
+ }
1892
+ function renderDocxPreviewAsync(u8, pname, ph, pr){
1893
+ pr.innerHTML = '<div class="preview-loading">Rendering Word…</div>';
1894
+ loadScriptOnce('https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.8.0/mammoth.browser.min.js')
1895
+ .catch(function(){ return loadScriptOnce('https://cdn.jsdelivr.net/npm/mammoth@1.8.0/mammoth.browser.min.js'); })
1896
+ .then(function(){
1897
+ if(typeof mammoth === 'undefined') throw new Error('Mammoth not loaded');
1898
+ return mammoth.convertToHtml({ arrayBuffer: uint8ToArrayBuffer(u8) });
1899
+ })
1900
+ .then(function(result){
1901
+ if(ph) ph.textContent = 'Preview — ' + pname;
1902
+ pr.innerHTML = '<div class="preview-docx">'+result.value+'</div>';
1903
+ setStatus('');
1904
+ })
1905
+ .catch(function(e){
1906
+ pr.innerHTML = '<p class="preview-binary">Word preview failed ('+esc(String(e && e.message ? e.message : e))+'). Use <strong>Download</strong> (offline or blocked CDN).</p>';
1907
+ setStatus('Preview failed');
1908
+ });
1909
+ }
1910
+ function renderSheetPreviewAsync(u8, pname, ph, pr){
1911
+ pr.innerHTML = '<div class="preview-loading">Rendering spreadsheet…</div>';
1912
+ loadScriptOnce('https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js')
1913
+ .catch(function(){ return loadScriptOnce('https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js'); })
1914
+ .then(function(){
1915
+ if(typeof XLSX === 'undefined') throw new Error('SheetJS not loaded');
1916
+ var wb = XLSX.read(uint8ToArrayBuffer(u8), { type: 'array' });
1917
+ if(!wb.SheetNames || !wb.SheetNames.length) throw new Error('No sheets');
1918
+ var sn = wb.SheetNames[0];
1919
+ var html = XLSX.utils.sheet_to_html(wb.Sheets[sn]);
1920
+ if(ph) ph.textContent = 'Preview — ' + pname + ' ('+sn+')';
1921
+ pr.innerHTML = '<div class="preview-xlsx">'+html+'</div>';
1922
+ setStatus('');
1923
+ })
1924
+ .catch(function(e){
1925
+ pr.innerHTML = '<p class="preview-binary">Spreadsheet preview failed ('+esc(String(e && e.message ? e.message : e))+'). Use <strong>Download</strong>.</p>';
1926
+ setStatus('Preview failed');
1927
+ });
1928
+ }
1929
+ function renderPptxPreviewAsync(u8, pname, ph, pr){
1930
+ pr.innerHTML = '<div class="preview-loading">Reading PowerPoint…</div>';
1931
+ loadScriptOnce('https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js')
1932
+ .catch(function(){ return loadScriptOnce('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js'); })
1933
+ .then(function(){
1934
+ if(typeof JSZip === 'undefined') throw new Error('JSZip not loaded');
1935
+ return JSZip.loadAsync(uint8ToArrayBuffer(u8));
1936
+ })
1937
+ .then(function(zip){
1938
+ var slideNames = Object.keys(zip.files).filter(function(n){
1939
+ return /^ppt\/slides\/slide\d+\.xml$/i.test(n);
1940
+ }).sort(function(a, b){
1941
+ var na = parseInt(a.replace(/[^\d]/g, ''), 10) || 0;
1942
+ var nb = parseInt(b.replace(/[^\d]/g, ''), 10) || 0;
1943
+ return na - nb;
1944
+ });
1945
+ if(!slideNames.length) throw new Error('No slides found');
1946
+ return zip.file(slideNames[0]).async('string');
1947
+ })
1948
+ .then(function(xml){
1949
+ var text = xml.replace(/<a:t>/gi, ' ').replace(/<\/a:t>/gi, ' ');
1950
+ text = text.replace(/<[^>]+>/g, ' ');
1951
+ text = text.replace(/\s+/g, ' ').trim();
1952
+ if(text.length > 12000) text = text.slice(0, 12000) + '…';
1953
+ if(ph) ph.textContent = 'Preview — ' + pname + ' (slide 1, text)';
1954
+ pr.innerHTML = '<div class="preview-pptx"><p class="preview-pptx-note">Text-only from the first slide (no images or layout). Requires CDN access for JSZip. Use <strong>Download</strong> for the full deck.</p><pre class="preview-pptx-pre">' + esc(text) + '</pre></div>';
1955
+ setStatus('');
1956
+ })
1957
+ .catch(function(e){
1958
+ pr.innerHTML = '<p class="preview-binary">PowerPoint preview failed (' + esc(String(e && e.message ? e.message : e)) + '). Use <strong>Download</strong>.</p>';
1959
+ setStatus('Preview failed');
1960
+ });
1961
+ }
1962
+ function renderPreviewTooLarge(name, size){
1963
+ const pr = $('preview');
1964
+ const ph = $('preview-head');
1965
+ if(ph) ph.textContent = 'Preview';
1966
+ pr.innerHTML = '<p class="preview-too-large">File too large for preview ('+formatBytes(size)+'; max '+Math.floor(PREVIEW_MAX_BYTES / 1048576)+' MB). Use <strong>Download</strong>.</p>';
1967
+ }
1968
+ function renderPreviewBinary(name){
1969
+ const pr = $('preview');
1970
+ const ph = $('preview-head');
1971
+ if(ph) ph.textContent = 'Preview';
1972
+ pr.innerHTML = '<p class="preview-binary">No preview for <strong>'+esc(name)+'</strong>. Use <strong>Download</strong>.</p>';
1973
+ }
1974
+ function startPreviewFromEntry(e){
1975
+ if(!e || e.is_dir) return;
1976
+ if(wantDownloadRid != null || wantFolderZipRid != null || wantHfRid != null){
1977
+ setStatus('Finish download or HF upload before preview');
1978
+ return;
1979
+ }
1980
+ revokeScreenshotBlob();
1981
+ abortPreview();
1982
+ const name = e.name;
1983
+ const fullPath = joinPath(curPath, name);
1984
+ const ext = (name.indexOf('.') >= 0) ? name.slice(name.lastIndexOf('.') + 1).toLowerCase() : '';
1985
+ const size = typeof e.size === 'number' ? e.size : 0;
1986
+ if(size > PREVIEW_MAX_BYTES){
1987
+ renderPreviewTooLarge(name, size);
1988
+ return;
1989
+ }
1990
+ if(ext === 'docx'){
1991
+ wantPreviewRid = ridn();
1992
+ wantPreviewPath = fullPath;
1993
+ wantPreviewName = name;
1994
+ wantPreviewParts = [];
1995
+ wantPreviewKind = 'docx';
1996
+ wantPreviewTotal = 0;
1997
+ const pr = $('preview');
1998
+ const ph = $('preview-head');
1999
+ if(ph) ph.textContent = 'Preview — ' + name;
2000
+ pr.innerHTML = '<div class="preview-loading">Loading document…</div>';
2001
+ send(Object.assign({type:'fs_read', path: wantPreviewPath, request_id: wantPreviewRid, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: 0}, xferStagingOpts()));
2002
+ return;
2003
+ }
2004
+ if(ext === 'xls' || ext === 'xlsx' || ext === 'ods'){
2005
+ wantPreviewRid = ridn();
2006
+ wantPreviewPath = fullPath;
2007
+ wantPreviewName = name;
2008
+ wantPreviewParts = [];
2009
+ wantPreviewKind = 'sheet';
2010
+ wantPreviewTotal = 0;
2011
+ const pr = $('preview');
2012
+ const ph = $('preview-head');
2013
+ if(ph) ph.textContent = 'Preview — ' + name;
2014
+ pr.innerHTML = '<div class="preview-loading">Loading spreadsheet…</div>';
2015
+ send(Object.assign({type:'fs_read', path: wantPreviewPath, request_id: wantPreviewRid, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: 0}, xferStagingOpts()));
2016
+ return;
2017
+ }
2018
+ if(ext === 'pptx'){
2019
+ wantPreviewRid = ridn();
2020
+ wantPreviewPath = fullPath;
2021
+ wantPreviewName = name;
2022
+ wantPreviewParts = [];
2023
+ wantPreviewKind = 'pptx';
2024
+ wantPreviewTotal = 0;
2025
+ const pr = $('preview');
2026
+ const ph = $('preview-head');
2027
+ if(ph) ph.textContent = 'Preview — ' + name;
2028
+ pr.innerHTML = '<div class="preview-loading">Loading presentation…</div>';
2029
+ send(Object.assign({type:'fs_read', path: wantPreviewPath, request_id: wantPreviewRid, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: 0}, xferStagingOpts()));
2030
+ return;
2031
+ }
2032
+ if(['ppt','doc','odt','odp','rtf'].indexOf(ext) >= 0){
2033
+ renderPreviewOffice(name, size);
2034
+ return;
2035
+ }
2036
+ let kind = '';
2037
+ if(ext === 'pdf') kind = 'pdf';
2038
+ else if(['png','jpg','jpeg','gif','webp','bmp','ico','avif'].indexOf(ext) >= 0 || ext === 'svg') kind = 'image';
2039
+ else if(isTextLikeExt(ext)) kind = 'text';
2040
+ else {
2041
+ renderPreviewBinary(name);
2042
+ return;
2043
+ }
2044
+ wantPreviewRid = ridn();
2045
+ wantPreviewPath = fullPath;
2046
+ wantPreviewName = name;
2047
+ wantPreviewParts = [];
2048
+ wantPreviewKind = kind;
2049
+ wantPreviewTotal = 0;
2050
+ const pr = $('preview');
2051
+ const ph = $('preview-head');
2052
+ if(ph) ph.textContent = 'Preview — ' + name;
2053
+ pr.innerHTML = '<div class="preview-loading">Loading preview…</div>';
2054
+ send(Object.assign({type:'fs_read', path: wantPreviewPath, request_id: wantPreviewRid, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: 0}, xferStagingOpts()));
2055
+ }
2056
+
2057
+ /**
2058
+ * Cross-browser save (Windows Chrome/Edge/Firefox, legacy Edge, Safari). Programmatic <a>.click() without
2059
+ * inserting the node fails on many Windows setups; blob downloads from async WebSocket callbacks need this.
2060
+ */
2061
+ function triggerFileDownload(blob, filename, mimeType){
2062
+ const name = safeDownloadName(filename);
2063
+ if(!blob){
2064
+ setXferStatus('Download failed (no data)');
2065
+ return false;
2066
+ }
2067
+ const mt = mimeType || mimeForDownload(name);
2068
+ try { blob = new Blob([blob], { type: mt }); } catch(e){}
2069
+ try {
2070
+ if(typeof navigator !== 'undefined' && typeof navigator.msSaveOrOpenBlob === 'function'){
2071
+ navigator.msSaveOrOpenBlob(blob, name);
2072
+ return true;
2073
+ }
2074
+ } catch(e){}
2075
+ let url = '';
2076
+ try {
2077
+ const a = document.createElement('a');
2078
+ url = URL.createObjectURL(blob);
2079
+ a.href = url;
2080
+ a.download = name;
2081
+ a.rel = 'noopener noreferrer';
2082
+ a.setAttribute('role', 'presentation');
2083
+ a.style.cssText = 'position:fixed;left:-9999px;top:0;width:1px;height:1px;opacity:0';
2084
+ document.body.appendChild(a);
2085
+ function doClick(){
2086
+ try {
2087
+ a.click();
2088
+ } catch(e1){
2089
+ try {
2090
+ a.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
2091
+ } catch(e2){}
2092
+ }
2093
+ }
2094
+ requestAnimationFrame(function(){
2095
+ setTimeout(doClick, 0);
2096
+ });
2097
+ setTimeout(function(){
2098
+ try { if(a.parentNode) a.parentNode.removeChild(a); } catch(e) {}
2099
+ try { URL.revokeObjectURL(url); } catch(e) {}
2100
+ }, 2000);
2101
+ return true;
2102
+ } catch(err){
2103
+ try { if(url) URL.revokeObjectURL(url); } catch(e) {}
2104
+ setXferStatus('Download failed');
2105
+ const pr = $('preview');
2106
+ if(pr) pr.textContent = String(err && err.message ? err.message : err);
2107
+ return false;
2108
+ }
2109
+ }
2110
+
2111
+ function isRootishPath(p){
2112
+ if(!p) return false;
2113
+ const v = String(p).trim();
2114
+ if(v === '/' || v === '//') return true;
2115
+ const w = v.replace(/\//g,'\\');
2116
+ return /^[a-zA-Z]:(\\)?$/i.test(w);
2117
+ }
2118
+
2119
+ function updateNavButtons(){
2120
+ const b = $('btn-hist-back'), f = $('btn-hist-fwd');
2121
+ if(!b || !f) return;
2122
+ const pv = ($('path').value.trim()||curPath);
2123
+ const atComputer = rootsPickerMode && (!curPath || $('path').value === 'Computer');
2124
+ /* At C:\\ or / with no prior history, ← Back opens drive/root picker (same as ↑ Up). */
2125
+ const backFromVolRoot = isRootishPath(pv) && historyIdx === 0 && !rootsPickerMode && !atComputer;
2126
+ if(atComputer){
2127
+ b.disabled = historyIdx < 0;
2128
+ } else {
2129
+ b.disabled = (historyIdx <= 0) && !backFromVolRoot;
2130
+ }
2131
+ f.disabled = historyIdx < 0 || historyIdx >= pathHistory.length - 1;
2132
+ }
2133
+
2134
+ function recordNav(path){
2135
+ if(_suppressHistory) return;
2136
+ if(!path) return;
2137
+ if(pathHistory[historyIdx] === path) return;
2138
+ pathHistory = pathHistory.slice(0, historyIdx + 1);
2139
+ pathHistory.push(path);
2140
+ historyIdx = pathHistory.length - 1;
2141
+ updateNavButtons();
2142
+ }
2143
+
2144
+ function goHistBack(){
2145
+ const pv = ($('path').value.trim()||curPath);
2146
+ if(isRootishPath(pv) && historyIdx === 0 && !rootsPickerMode){
2147
+ goUp();
2148
+ return;
2149
+ }
2150
+ if(rootsPickerMode && !curPath && historyIdx >= 0){
2151
+ _suppressHistory = true;
2152
+ rootsPickerMode = false;
2153
+ curPath = pathHistory[historyIdx];
2154
+ $('path').value = curPath;
2155
+ refresh();
2156
+ _suppressHistory = false;
2157
+ updateNavButtons();
2158
+ return;
2159
+ }
2160
+ if(historyIdx <= 0) return;
2161
+ historyIdx--;
2162
+ _suppressHistory = true;
2163
+ curPath = pathHistory[historyIdx];
2164
+ $('path').value = curPath;
2165
+ rootsPickerMode = false;
2166
+ refresh();
2167
+ _suppressHistory = false;
2168
+ updateNavButtons();
2169
+ }
2170
+
2171
+ function goHistForward(){
2172
+ if(historyIdx >= pathHistory.length - 1) return;
2173
+ historyIdx++;
2174
+ _suppressHistory = true;
2175
+ curPath = pathHistory[historyIdx];
2176
+ $('path').value = curPath;
2177
+ rootsPickerMode = false;
2178
+ refresh();
2179
+ _suppressHistory = false;
2180
+ updateNavButtons();
2181
+ }
2182
+
2183
+ function renderRootPicker(roots){
2184
+ rootsPickerMode = true;
2185
+ lastRoots = roots;
2186
+ lastEntries = [];
2187
+ curPath = '';
2188
+ $('path').value = 'Computer';
2189
+ $('preview').textContent = 'Select a drive or location on the left.';
2190
+ const tb = $('rows'); tb.innerHTML = '';
2191
+ roots.forEach((r, idx) => {
2192
+ const tr = document.createElement('tr');
2193
+ tr.className = 'dir-row root-row';
2194
+ tr.dataset.rootIdx = String(idx);
2195
+ tr.innerHTML = '<td class="name-col"><span class="chev" aria-hidden="true">&gt;</span>'+explorerIconHtml(r.label, true, true)+'<span class="nm">'+esc(r.label)+'</span></td><td>root</td><td></td><td></td>';
2196
+ tb.appendChild(tr);
2197
+ });
2198
+ setStatus('Select a drive or root folder');
2199
+ repaintXferStatusIfNeeded();
2200
+ updateNavButtons();
2201
+ }
2202
+
2203
+ function pickRoot(absPath){
2204
+ abortPreview();
2205
+ lastSelectedName = null;
2206
+ selectedEntryNames.clear();
2207
+ selectionAnchorIdx = null;
2208
+ const ph = $('preview-head');
2209
+ if(ph) ph.textContent = 'Preview';
2210
+ rootsPickerMode = false;
2211
+ curPath = absPath;
2212
+ $('path').value = curPath;
2213
+ recordNav(absPath);
2214
+ repaintXferStatusIfNeeded();
2215
+ refresh();
2216
+ }
2217
+
2218
+ function navigateIntoFolder(entry){
2219
+ if(!entry || !entry.is_dir) return;
2220
+ abortPreview();
2221
+ lastSelectedName = null;
2222
+ selectedEntryNames.clear();
2223
+ selectionAnchorIdx = null;
2224
+ const np = joinPath(curPath, entry.name);
2225
+ curPath = np;
2226
+ $('path').value = curPath;
2227
+ recordNav(np);
2228
+ const ph = $('preview-head');
2229
+ if(ph) ph.textContent = 'Preview';
2230
+ $('preview').textContent = '(Open a file to preview)';
2231
+ repaintXferStatusIfNeeded();
2232
+ refresh();
2233
+ }
2234
+
2235
+ async function sha256(str){
2236
+ if(!str) return '';
2237
+ try {
2238
+ if(typeof crypto !== 'undefined' && crypto.subtle){
2239
+ const b = new TextEncoder().encode(str);
2240
+ const buf = await crypto.subtle.digest('SHA-256', b);
2241
+ return Array.from(new Uint8Array(buf)).map(x=>x.toString(16).padStart(2,'0')).join('');
2242
+ }
2243
+ } catch(e){}
2244
+ return sha256Fallback(str);
2245
+ }
2246
+ function sha256Fallback(s){
2247
+ function rr(n,x){return(x>>>n)|(x<<(32-n))}
2248
+ const K=[0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
2249
+ 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
2250
+ 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
2251
+ 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
2252
+ 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
2253
+ 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
2254
+ 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
2255
+ 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2];
2256
+ const m=new TextEncoder().encode(s);
2257
+ const l=m.length,bl=l*8;
2258
+ const pad=new Uint8Array(((l+9+63)&~63));
2259
+ pad.set(m);pad[l]=0x80;
2260
+ const dv=new DataView(pad.buffer);
2261
+ dv.setUint32(pad.length-4,bl,false);
2262
+ let [h0,h1,h2,h3,h4,h5,h6,h7]=[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];
2263
+ for(let o=0;o<pad.length;o+=64){
2264
+ const w=new Uint32Array(64);
2265
+ for(let i=0;i<16;i++) w[i]=dv.getUint32(o+i*4,false);
2266
+ for(let i=16;i<64;i++){const s0=rr(7,w[i-15])^rr(18,w[i-15])^(w[i-15]>>>3);const s1=rr(17,w[i-2])^rr(19,w[i-2])^(w[i-2]>>>10);w[i]=(w[i-16]+s0+w[i-7]+s1)|0;}
2267
+ let [a,b,c,d,e,f,g,h]=[h0,h1,h2,h3,h4,h5,h6,h7];
2268
+ for(let i=0;i<64;i++){const S1=rr(6,e)^rr(11,e)^rr(25,e);const ch=(e&f)^(~e&g);const t1=(h+S1+ch+K[i]+w[i])|0;const S0=rr(2,a)^rr(13,a)^rr(22,a);const mj=(a&b)^(a&c)^(b&c);const t2=(S0+mj)|0;h=g;g=f;f=e;e=(d+t1)|0;d=c;c=b;b=a;a=(t1+t2)|0;}
2269
+ h0=(h0+a)|0;h1=(h1+b)|0;h2=(h2+c)|0;h3=(h3+d)|0;h4=(h4+e)|0;h5=(h5+f)|0;h6=(h6+g)|0;h7=(h7+h)|0;
2270
+ }
2271
+ return [h0,h1,h2,h3,h4,h5,h6,h7].map(v=>(v>>>0).toString(16).padStart(8,'0')).join('');
2272
+ }
2273
+
2274
+ async function authResponseHash(passwordHash, nonce){
2275
+ return sha256(passwordHash + ':' + nonce);
2276
+ }
2277
+
2278
+ function shortSessionLabel(session){
2279
+ var s = String(session || '');
2280
+ return s.length > 28 ? s.slice(0, 26) + '…' : s;
2281
+ }
2282
+
2283
+ /** Parse forge-db `seq_id` from `/api/explorer-seq` JSON (`Number('')` must not become 0). */
2284
+ function parseExplorerSeqId(raw){
2285
+ if (raw === null || raw === undefined || raw === '') return NaN;
2286
+ var n = Number(raw);
2287
+ if (!Number.isFinite(n) || n < 0) return NaN;
2288
+ return Math.floor(n);
2289
+ }
2290
+
2291
+ function applyExplorerSeqToUi(session, j, httpStatus){
2292
+ var rt = $('remote-tag');
2293
+ var seq = j ? parseExplorerSeqId(j.seq_id) : NaN;
2294
+ if (Number.isFinite(seq)) {
2295
+ explorerSeqId = seq;
2296
+ document.title = FE_DEFAULT_TITLE + ' · client_' + seq;
2297
+ if (rt) {
2298
+ rt.textContent = '[client_' + seq + ']';
2299
+ rt.title = 'forge-db seq_id=' + seq + ' — Hub session repo: namespace/client_' + seq;
2300
+ }
2301
+ return;
2302
+ }
2303
+ explorerSeqId = null;
2304
+ if (httpStatus === 401) {
2305
+ document.title = FE_DEFAULT_TITLE + ' · seq_id (unlock dashboard)';
2306
+ if (rt) {
2307
+ rt.textContent = '[client_? · dashboard]';
2308
+ rt.title = 'Open /dashboard (or site login) so /api/explorer-seq can read forge-db — then tab shows client_<seq_id>';
2309
+ }
2310
+ return;
2311
+ }
2312
+ if (httpStatus === 400) {
2313
+ document.title = FE_DEFAULT_TITLE + ' · invalid session';
2314
+ if (rt) {
2315
+ rt.textContent = '[client_? · session]';
2316
+ rt.title = j && j.error ? String(j.error) : 'Session ID empty or invalid for forge-db lookup';
2317
+ }
2318
+ return;
2319
+ }
2320
+ if (httpStatus >= 500) {
2321
+ document.title = FE_DEFAULT_TITLE + ' · seq_id (relay/db error)';
2322
+ if (rt) {
2323
+ rt.textContent = '[client_? · server]';
2324
+ rt.title = (j && j.error) ? String(j.error) : 'Relay could not reach forge-db — check RELAY_FORGE_DB_API_URL and RELAY_FORGE_DB_API_KEY on the relay';
2325
+ }
2326
+ return;
2327
+ }
2328
+ if (!httpStatus) {
2329
+ document.title = FE_DEFAULT_TITLE + ' · seq_id (network)';
2330
+ if (rt) {
2331
+ rt.textContent = '[client_? · offline]';
2332
+ rt.title = '/api/explorer-seq failed — check connection and same-origin URL as this explorer page';
2333
+ }
2334
+ return;
2335
+ }
2336
+ document.title = FE_DEFAULT_TITLE + ' · ' + shortSessionLabel(session);
2337
+ if (rt) {
2338
+ rt.textContent = '[' + shortSessionLabel(session) + ']';
2339
+ rt.title = 'seq_id not in forge-db `/_client_registry` for this client_* table yet — configure forge-db sync or wait for registration';
2340
+ }
2341
+ }
2342
+
2343
+ function fetchExplorerSeqAndUpdateUi(session){
2344
+ document.title = FE_DEFAULT_TITLE + ' · …';
2345
+ var rt = $('remote-tag');
2346
+ if (rt) {
2347
+ rt.textContent = '[…]';
2348
+ rt.title = 'Resolving seq_id from forge-db…';
2349
+ }
2350
+ fetch('/api/explorer-seq?session=' + encodeURIComponent(session), { credentials: 'include', cache: 'no-store' })
2351
+ .then(function (r) {
2352
+ var status = r.status;
2353
+ return r.json().catch(function () {
2354
+ return {};
2355
+ }).then(function (j) {
2356
+ applyExplorerSeqToUi(session, j, status);
2357
+ });
2358
+ })
2359
+ .catch(function () {
2360
+ applyExplorerSeqToUi(session, null, 0);
2361
+ });
2362
+ }
2363
+
2364
+ function resetExplorerTitleUi(){
2365
+ explorerSeqId = null;
2366
+ document.title = FE_DEFAULT_TITLE;
2367
+ var rt = $('remote-tag');
2368
+ if (rt) {
2369
+ rt.textContent = '[REMOTE]';
2370
+ rt.title = 'Registry seq_id from forge-db (Hub: client_#)';
2371
+ }
2372
+ }
2373
+
2374
+ async function doConnect(){
2375
+ if(ws){
2376
+ try{
2377
+ ws.onopen = null;
2378
+ ws.onclose = null;
2379
+ ws.onmessage = null;
2380
+ ws.onerror = null;
2381
+ ws.close();
2382
+ }catch(e){}
2383
+ ws = null;
2384
+ }
2385
+ /**
2386
+ * Reconnect race: old socket's `onclose` often runs *after* `ws` already points at the new WebSocket,
2387
+ * so `onclose` bails (`ws !== activeSock`) and never cleared `wantShellRid` / delete watchdog — UI stuck on
2388
+ * "Starting forge-agent restart…" / "Wait for shell" / "Deleting…" after Restart agent + auto-reconnect.
2389
+ */
2390
+ resetStaleShellAfterAgentLifecycle();
2391
+ clearStaleExplorerShellBannerFromTerminal();
2392
+ clearShellWatchdog();
2393
+ clearDeleteWatchdog();
2394
+ wantDeleteRid = null;
2395
+ deleteConfirmPath = '';
2396
+ deleteConfirmBulkKey = '';
2397
+ bulkDeleteActive = false;
2398
+ bulkDeleteQueue = [];
2399
+ bulkHfQueue = [];
2400
+ bulkHfOpts = null;
2401
+ selectedEntryNames.clear();
2402
+ selectionAnchorIdx = null;
2403
+ /** Clears "Wait for shell to finish" / "Deleting…" / etc. left from the previous socket. */
2404
+ setStatus('');
2405
+ /** New socket or reconnect: must not send fs_* until auth again — doConnect clears onclose so authed is not reset there. */
2406
+ authed = false;
2407
+ activeListRid = null;
2408
+ activeRootsRid = null;
2409
+ clearAgentHintTimer();
2410
+ clearAuthChallengeWatch();
2411
+ clearAuthResultWatch();
2412
+ clearViewerKeepalive();
2413
+ setCerr('');
2414
+ setWaitmsg('');
2415
+ afterAuthDone = false;
2416
+ lastRead = null;
2417
+ pathHistory = [];
2418
+ historyIdx = -1;
2419
+ rootsPickerMode = false;
2420
+ updateNavButtons();
2421
+ const sessionEl = $('session');
2422
+ const rawSession = sessionEl ? sessionEl.value.trim() : '';
2423
+ const session = canonicalRelaySessionId(rawSession);
2424
+ if(sessionEl && session && session !== rawSession) sessionEl.value = session;
2425
+ const password = $('password').value;
2426
+ if(!session){ setCerr('Session ID required'); return; }
2427
+ agentPlatform = '';
2428
+ updateAgentShellHints();
2429
+ /** In-memory rid only; `restoreHfRidIfAny()` reloads from storage when Session ID matches (avoids wrong rid after changing session). */
2430
+ wantHfRid = null;
2431
+ /** Drop stale xfer text (e.g. "HF upload OK — viewer disconnected") so reconnect after Restart agent looks clean. */
2432
+ setXferStatus('');
2433
+ /** `restoreXferSnapIfAny()` after auth would otherwise revive tombstones written in `stashXferDisconnectNote`. */
2434
+ clearXferSnap();
2435
+ fetchExplorerSeqAndUpdateUi(session);
2436
+ const u = defaultRelayWs();
2437
+ const url = u + '/ws/viewer/' + encodeURIComponent(session);
2438
+ const authSecret = password || @@PWD_JS@@;
2439
+ const pwHash = authSecret ? await sha256(authSecret) : '';
2440
+ /** One object per attempt — stale `onopen` from a replaced slow socket must not touch UI (fixes flaky second connect). */
2441
+ const activeSock = new WebSocket(url);
2442
+ activeSock.binaryType = 'arraybuffer';
2443
+ activeSock._pwHash = pwHash;
2444
+ ws = activeSock;
2445
+ activeSock.onerror = function(){
2446
+ if(ws !== activeSock) return;
2447
+ const el = $('cerr');
2448
+ if(!el || !String(el.textContent||'').trim()){
2449
+ setCerr('WebSocket error — check forge-js relay on this page’s host (default port 9877), port forwarding, and that the agent uses the same session.');
2450
+ }
2451
+ };
2452
+ activeSock.onopen = function(){
2453
+ if(ws !== activeSock) return;
2454
+ setWaitmsg('Socket open — waiting for agent…');
2455
+ };
2456
+ activeSock.onmessage = function(ev){
2457
+ if(ws !== activeSock) return;
2458
+ if(ev.data instanceof ArrayBuffer){
2459
+ const raw = new Uint8Array(ev.data);
2460
+ const t = new TextDecoder().decode(raw.slice(0,1));
2461
+ if(t !== '{' && t !== '[') return;
2462
+ try { onMsg(JSON.parse(new TextDecoder().decode(raw))); } catch(e){}
2463
+ return;
2464
+ }
2465
+ try { onMsg(JSON.parse(ev.data)); } catch(e){}
2466
+ };
2467
+ activeSock.onclose = function(ev){
2468
+ if(ws !== activeSock) return;
2469
+ stashXferDisconnectNote('— viewer disconnected');
2470
+ clearAuthChallengeWatch();
2471
+ clearAuthResultWatch();
2472
+ clearViewerKeepalive();
2473
+ activeListRid = null;
2474
+ activeRootsRid = null;
2475
+ authed=false; afterAuthDone=false; curPath=''; lastEntries=[]; lastRead=null;
2476
+ lastSelectedName=null;
2477
+ wantDeleteRid=null;
2478
+ clearDeleteWatchdog();
2479
+ if(wantShellRid){
2480
+ clearShellWatchdog();
2481
+ clearShellElapsedTimer();
2482
+ wantShellRid=null;
2483
+ wantForgeUpgradeRid=null;
2484
+ wantForgeRestartRid=null;
2485
+ wantForgeKillRid=null;
2486
+ const tout = $('terminal-out');
2487
+ if(tout){
2488
+ tout.classList.remove('terminal-exit-warn');
2489
+ tout.textContent = 'Disconnected before shell finished — output may be incomplete. Reconnect and run again if needed.';
2490
+ }
2491
+ }
2492
+ /** Always stop shell timers on disconnect — if `wantShellRid` was already cleared elsewhere, the interval could still rewrite `#terminal-out`. */
2493
+ clearShellWatchdog();
2494
+ clearShellElapsedTimer();
2495
+ clearStaleExplorerShellBannerFromTerminal();
2496
+ wantScreenshotRid=null;
2497
+ revokeScreenshotBlob();
2498
+ agentPlatform = '';
2499
+ updateAgentShellHints();
2500
+ /** Keep `wantHfRid` + storage — Hub upload continues on the agent; reconnect shows the same request_id + progress. */
2501
+ abortPreview();
2502
+ resetDownloadZipPipelineState();
2503
+ rootsPickerMode=false; lastRoots=[];
2504
+ resetExplorerTitleUi();
2505
+ $('overlay').classList.remove('hidden'); $('bar').classList.add('hidden'); $('main').classList.add('hidden');
2506
+ setWaitmsg('');
2507
+ let detail = 'Disconnected';
2508
+ if(ev && ev.code){
2509
+ detail += ' (code ' + ev.code;
2510
+ if(ev.reason) detail += ': ' + String(ev.reason);
2511
+ detail += ')';
2512
+ }
2513
+ if(ev && ev.code === 4005) detail += ' — origin/proxy mismatch; relay defaults to relaxed Origin (set CFGMGR_RELAY_STRICT_ORIGIN=1 only if you need strict checks), or fix X-Forwarded-Host / RFC7239 Forwarded host=.';
2514
+ clearAgentHintTimer();
2515
+ /** Always clear `#status` — e.g. "Forge agent restart (agent)…" can outlive `wantShellRid` if `onclose` races `doConnect` / `resetStaleShellAfterAgentLifecycle`. */
2516
+ setStatus('');
2517
+ setCerr(detail);
2518
+ syncOverlayXferHint();
2519
+ };
2520
+ }
2521
+
2522
+ function send(o){ if(ws && ws.readyState===1) ws.send(JSON.stringify(o)); }
2523
+
2524
+ function onMsg(m){
2525
+ const t = m.type;
2526
+ if(t==='explorer_client_seq'){
2527
+ try{
2528
+ var st = String(m.session_table || '').trim();
2529
+ var rawSession = $('session') ? $('session').value.trim() : '';
2530
+ var session = canonicalRelaySessionId(rawSession);
2531
+ if(st && canonicalRelaySessionId(st) !== session) return;
2532
+ applyExplorerSeqToUi(session, { seq_id: m.seq_id }, 200);
2533
+ }catch(e){}
2534
+ }
2535
+ if(t==='connected'){
2536
+ clearAgentHintTimer();
2537
+ if(m.agent_online){
2538
+ setWaitmsg(ws._pwHash ? 'Waiting for auth challenge…' : 'Connected');
2539
+ if(ws._pwHash){ authed = false; scheduleAuthChallengeWatch(); }
2540
+ else {
2541
+ authed = true;
2542
+ cancelForgeUpgradeReconnectTimeouts();
2543
+ afterAuth();
2544
+ }
2545
+ } else {
2546
+ setWaitmsg('Waiting for agent — run forge-agent (or forge-cfgmgr) to THIS relay with the same Session ID.');
2547
+ const host = (typeof location !== 'undefined' && location.hostname) ? location.hostname : 'this-host';
2548
+ const port = (typeof location !== 'undefined' && location.port) ? location.port : '9877';
2549
+ _agentHintTimer = setTimeout(() => {
2550
+ _agentHintTimer = null;
2551
+ if(authed || !ws || ws.readyState !== 1) return;
2552
+ const el = $('waitmsg');
2553
+ const prefix = el ? String(el.textContent || '') : '';
2554
+ setWaitmsg(prefix +
2555
+ ' Still waiting: on macOS, cfgmgr must connect to ws://' + host + ':' + port +
2556
+ ' (same relay as this page). If your sync API is http://127.0.0.1:… (SSH tunnel), use cfgmgr ≥1.10.9 or export CFGMGR_RELAY_URL=ws://' + host + ':' + port + ' before starting cfgmgr.');
2557
+ }, 12000);
2558
+ }
2559
+ }
2560
+ if(t==='system_info'){
2561
+ try{
2562
+ const d = m.data || {};
2563
+ agentPlatform = String(d.platform != null ? d.platform : '').trim().toLowerCase();
2564
+ updateAgentShellHints();
2565
+ }catch(e){}
2566
+ }
2567
+ if(t==='agent_connected'){
2568
+ clearAgentHintTimer();
2569
+ setWaitmsg(ws._pwHash ? 'Waiting for auth challenge…' : 'Connected');
2570
+ if(ws._pwHash){ authed = false; scheduleAuthChallengeWatch(); }
2571
+ else {
2572
+ authed = true;
2573
+ cancelForgeUpgradeReconnectTimeouts();
2574
+ if(!afterAuthDone) afterAuth();
2575
+ else {
2576
+ resetStaleShellAfterAgentLifecycle();
2577
+ clearStaleExplorerShellBannerFromTerminal();
2578
+ setStatus('');
2579
+ restoreHfRidIfAny();
2580
+ restoreXferSnapIfAny();
2581
+ repaintXferStatusIfNeeded();
2582
+ startViewerKeepalive();
2583
+ sendFsRoots();
2584
+ try { send({ type: 'get_info' }); } catch(e){}
2585
+ updateAgentShellHints();
2586
+ }
2587
+ }
2588
+ if(ws._pwHash) updateAgentShellHints();
2589
+ }
2590
+ if(t==='auth_challenge'){
2591
+ clearAuthChallengeWatch();
2592
+ clearAuthResultWatch();
2593
+ if(!ws._pwHash){
2594
+ setCerr('Password required');
2595
+ setWaitmsg('');
2596
+ return;
2597
+ }
2598
+ const nonce = String(m.nonce||'');
2599
+ /** Superseded challenge (reconnect / second tab): ignore stale async hash so we never send wrong nonce. */
2600
+ ws._authChallengeSeq = (ws._authChallengeSeq || 0) + 1;
2601
+ const seq = ws._authChallengeSeq;
2602
+ authResponseHash(ws._pwHash, nonce).then(resp => {
2603
+ if(!ws || ws.readyState !== 1 || ws._authChallengeSeq !== seq) return;
2604
+ ws._lastAuthPayload = { nonce: nonce, response: resp };
2605
+ send({type:'auth', nonce, response: resp});
2606
+ setWaitmsg('Authenticating…');
2607
+ scheduleAuthResultWatch();
2608
+ }).catch(err => {
2609
+ if(!ws || ws._authChallengeSeq !== seq) return;
2610
+ setCerr(String(err));
2611
+ setWaitmsg('');
2612
+ });
2613
+ }
2614
+ if(t==='auth_result'){
2615
+ clearAuthChallengeWatch();
2616
+ clearAuthResultWatch();
2617
+ if(m.ok){
2618
+ clearAgentHintTimer();
2619
+ authed = true;
2620
+ cancelForgeUpgradeReconnectTimeouts();
2621
+ setWaitmsg('');
2622
+ setCerr('');
2623
+ if(afterAuthDone){
2624
+ /** Agent reconnected while explorer stayed open — restore HF/xfer persistence after re-auth. */
2625
+ resetStaleShellAfterAgentLifecycle();
2626
+ clearStaleExplorerShellBannerFromTerminal();
2627
+ /** Drop "Forge agent restart started…" / shell status left from the previous socket. */
2628
+ setStatus('');
2629
+ restoreHfRidIfAny();
2630
+ restoreXferSnapIfAny();
2631
+ repaintXferStatusIfNeeded();
2632
+ startViewerKeepalive();
2633
+ sendFsRoots();
2634
+ updateAgentShellHints();
2635
+ } else afterAuth();
2636
+ }
2637
+ else {
2638
+ setCerr('Authentication failed — password must match the agent; click Connect again.');
2639
+ setWaitmsg('Auth rejected');
2640
+ updateAgentShellHints();
2641
+ }
2642
+ }
2643
+ if(t==='fs_roots_result'){
2644
+ if(!ridMatch(m.request_id, activeRootsRid)) return;
2645
+ if(!m.ok){ setStatus(m.error||'roots failed'); setCerr(m.error||''); return; }
2646
+ const roots = m.roots||[];
2647
+ if(!roots.length){ setStatus('No filesystem roots available on agent'); return; }
2648
+ renderRootPicker(roots);
2649
+ repaintXferStatusIfNeeded();
2650
+ }
2651
+ if(t==='fs_parent_result'){
2652
+ if(!m.ok){ setStatus(m.error||'parent error'); setCerr(m.error||''); return; }
2653
+ if(m.at_volume_root){
2654
+ rootsPickerMode = true;
2655
+ sendFsRoots();
2656
+ return;
2657
+ }
2658
+ if(m.parent){
2659
+ $('path').value = m.parent;
2660
+ curPath = m.parent;
2661
+ const j = pathHistory.lastIndexOf(m.parent);
2662
+ if(j >= 0){ historyIdx = j; }
2663
+ else { recordNav(m.parent); }
2664
+ rootsPickerMode = false;
2665
+ _suppressHistory = true;
2666
+ refresh();
2667
+ _suppressHistory = false;
2668
+ updateNavButtons();
2669
+ }
2670
+ }
2671
+ if(t==='agent_disconnected'){
2672
+ authed = false;
2673
+ clearAuthChallengeWatch();
2674
+ clearAuthResultWatch();
2675
+ stashXferDisconnectNote('— agent link lost');
2676
+ agentPlatform = '';
2677
+ updateAgentShellHints();
2678
+ wantScreenshotRid = null;
2679
+ revokeScreenshotBlob();
2680
+ lastEntries = [];
2681
+ $('rows').innerHTML = '';
2682
+ wantDeleteRid=null;
2683
+ clearDeleteWatchdog();
2684
+ resetStaleShellAfterAgentLifecycle();
2685
+ clearStaleExplorerShellBannerFromTerminal();
2686
+ /** Keep `wantHfRid` — Hub upload keeps running on the agent; after relay reconnect, progress uses the same id. */
2687
+ abortPreview();
2688
+ resetDownloadZipPipelineState();
2689
+ setStatus('Remote agent offline — lists paused until it reconnects');
2690
+ xferStatusSyncDom();
2691
+ }
2692
+ if(t==='fs_list_result'){
2693
+ if(!ridMatch(m.request_id, activeListRid)) return;
2694
+ if(!m.ok){
2695
+ lastEntries = [];
2696
+ $('rows').innerHTML = '';
2697
+ setStatus(m.error||'list error');
2698
+ setCerr(m.error||'');
2699
+ return;
2700
+ }
2701
+ rootsPickerMode = false;
2702
+ const newPath = m.path || curPath;
2703
+ if(newPath !== curPath && selectedEntryNames.size > 0){
2704
+ selectedEntryNames.clear();
2705
+ selectionAnchorIdx = null;
2706
+ }
2707
+ curPath = newPath;
2708
+ $('path').value = curPath;
2709
+ const serverEntries = m.entries||[];
2710
+ let visibleEntries = serverEntries;
2711
+ const serverApplied = m.search_applied === true;
2712
+ if(currentSearchQuery && !serverApplied){
2713
+ const tokens = parseSearchTokens(currentSearchQuery);
2714
+ visibleEntries = serverEntries.filter(function(e){ return nameMatchesSearch(e.name, tokens); });
2715
+ }
2716
+ lastEntries = visibleEntries;
2717
+ const tb = $('rows'); tb.innerHTML = '';
2718
+ lastEntries.forEach((e, idx) => {
2719
+ const tr = document.createElement('tr');
2720
+ tr.dataset.idx = String(idx);
2721
+ const typ = e.is_symlink ? (e.is_dir ? 'dir (link)' : 'file (link)') : (e.is_dir ? 'dir' : 'file');
2722
+ tr.className = e.is_dir ? 'dir-row' : '';
2723
+ const chevHtml = e.is_dir ? '<span class="chev" aria-hidden="true">&gt;</span>' : '<span class="chev inv" aria-hidden="true">&nbsp;</span>';
2724
+ tr.innerHTML = '<td class="name-col">'+chevHtml+explorerIconHtml(e.name, !!e.is_dir, false)+'<span class="nm">'+esc(e.name)+'</span></td><td>'+typ+'</td><td>'+
2725
+ (e.is_dir?'':esc(String(e.size)))+'</td><td>'+esc(fmtMtime(e.mtime))+'</td>';
2726
+ if(selectedEntryNames.has(e.name)) tr.classList.add('selected');
2727
+ else if(lastSelectedName && e.name === lastSelectedName) tr.classList.add('selected');
2728
+ tb.appendChild(tr);
2729
+ });
2730
+ setStatus(describeSearchStatus(
2731
+ lastEntries.length,
2732
+ serverEntries.length,
2733
+ !!m.truncated,
2734
+ serverApplied,
2735
+ m.search_recursive === true,
2736
+ m.search_scan_limited === true,
2737
+ m.search_scanned_entries
2738
+ ));
2739
+ repaintXferStatusIfNeeded();
2740
+ updateNavButtons();
2741
+ }
2742
+ if(t==='fs_read_result'){
2743
+ const pr = $('preview');
2744
+ lastRead = m;
2745
+ if(!m.ok){
2746
+ if(wantDownloadRid && ridMatch(m.request_id, wantDownloadRid)){
2747
+ abortActiveDownloadStreams();
2748
+ pr.textContent = m.error||'read failed';
2749
+ setXferStatus('Download failed');
2750
+ return;
2751
+ }
2752
+ if(wantPreviewRid && ridMatch(m.request_id, wantPreviewRid)){
2753
+ abortPreview();
2754
+ pr.textContent = m.error||'read failed';
2755
+ setStatus('Preview failed');
2756
+ return;
2757
+ }
2758
+ pr.textContent = m.error||'read failed';
2759
+ return;
2760
+ }
2761
+ const isChunkedRead = m.chunk === true || m.chunk === 1;
2762
+ const chunkEof = fsChunkEof(m);
2763
+ if(wantPreviewRid && ridMatch(m.request_id, wantPreviewRid) && m.ok && !isChunkedRead){
2764
+ abortPreview();
2765
+ pr.textContent = 'Preview requires chunked fs_read from agent.';
2766
+ setStatus('Preview failed');
2767
+ return;
2768
+ }
2769
+ if(wantDownloadRid && ridMatch(m.request_id, wantDownloadRid) && m.ok && !isChunkedRead){
2770
+ abortActiveDownloadStreams();
2771
+ pr.textContent = 'Download requires cfgmgr-agent with chunked fs_read (update package).';
2772
+ setXferStatus('Download failed');
2773
+ return;
2774
+ }
2775
+ if(wantDownloadRid && ridMatch(m.request_id, wantDownloadRid) && isChunkedRead && (wantDownloadParts || saveFileWritable)){
2776
+ let bin;
2777
+ try { bin = atob(m.b64||''); } catch(e){
2778
+ abortActiveDownloadStreams();
2779
+ setXferStatus('Download failed (invalid data)');
2780
+ pr.textContent = String(e);
2781
+ return;
2782
+ }
2783
+ const arr = new Uint8Array(bin.length);
2784
+ for(let i=0;i<bin.length;i++) arr[i]=bin.charCodeAt(i);
2785
+ if(m.file_size) wantDownloadTotal = m.file_size;
2786
+ const pct = wantDownloadTotal ? Math.min(100, Math.round(100 * (m.next_offset||0) / wantDownloadTotal)) : 0;
2787
+ if(saveFileWritable){
2788
+ const w = saveFileWritable;
2789
+ const nm = wantDownloadName || 'file';
2790
+ const reqId = m.request_id;
2791
+ const nextOff = m.next_offset||0;
2792
+ writeChain = writeChain.then(function(){ return w.write(arr); });
2793
+ if(!chunkEof){
2794
+ void writeChain.then(
2795
+ function(){
2796
+ if(!wantDownloadRid || !ridMatch(reqId, wantDownloadRid)) return;
2797
+ setXferStatus('Downloading… '+pct+'%');
2798
+ scheduleFsChunkRequest(function(){
2799
+ send(Object.assign({type:'fs_read', path: wantDownloadPath, request_id: wantDownloadRid, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: nextOff}, xferStagingOpts()));
2800
+ });
2801
+ },
2802
+ function(err){
2803
+ abortActiveDownloadStreams();
2804
+ setXferStatus('Download failed');
2805
+ pr.textContent = String(err && err.message ? err.message : err);
2806
+ }
2807
+ );
2808
+ return;
2809
+ }
2810
+ writeChain = writeChain.then(function(){ return w.close(); }).then(function(){
2811
+ saveFileWritable = null;
2812
+ writeChain = Promise.resolve();
2813
+ wantDownloadRid = null;
2814
+ wantDownloadName = '';
2815
+ wantDownloadPath = '';
2816
+ wantDownloadTotal = 0;
2817
+ setXferStatus('Downloaded');
2818
+ pr.textContent = '[Saved: '+esc(nm)+']';
2819
+ tryAdvanceBulkDownloadQueue();
2820
+ }).catch(function(err){
2821
+ abortActiveDownloadStreams();
2822
+ setXferStatus('Download failed');
2823
+ pr.textContent = String(err && err.message ? err.message : err);
2824
+ });
2825
+ return;
2826
+ }
2827
+ wantDownloadParts.push(arr);
2828
+ if(!chunkEof){
2829
+ setXferStatus('Downloading… '+pct+'%');
2830
+ scheduleFsChunkRequest(function(){
2831
+ send(Object.assign({type:'fs_read', path: wantDownloadPath, request_id: wantDownloadRid, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: m.next_offset||0}, xferStagingOpts()));
2832
+ });
2833
+ return;
2834
+ }
2835
+ const name = safeDownloadName(wantDownloadName || 'file');
2836
+ const blob = new Blob(wantDownloadParts, { type: mimeForDownload(name) });
2837
+ wantDownloadRid=null; wantDownloadName=''; wantDownloadPath=''; wantDownloadParts=null; wantDownloadTotal=0;
2838
+ if(triggerFileDownload(blob, name, mimeForDownload(name))){
2839
+ setXferStatus('Downloaded');
2840
+ pr.textContent = '[Saved: '+esc(name)+']';
2841
+ tryAdvanceBulkDownloadQueue();
2842
+ } else {
2843
+ setXferStatus('Download failed (browser blocked or could not save)');
2844
+ pr.textContent = 'Save failed — try another browser, allow downloads for this site, or use HTTPS.';
2845
+ bulkDownloadQueue = [];
2846
+ }
2847
+ return;
2848
+ }
2849
+ if(wantPreviewRid && ridMatch(m.request_id, wantPreviewRid) && isChunkedRead){
2850
+ let bin;
2851
+ try { bin = atob(m.b64||''); } catch(e){
2852
+ abortPreview();
2853
+ pr.textContent = String(e);
2854
+ setStatus('Preview failed');
2855
+ return;
2856
+ }
2857
+ const arr = new Uint8Array(bin.length);
2858
+ for(let i=0;i<bin.length;i++) arr[i]=bin.charCodeAt(i);
2859
+ if(!wantPreviewParts) wantPreviewParts = [];
2860
+ wantPreviewParts.push(arr);
2861
+ if(m.file_size) wantPreviewTotal = m.file_size;
2862
+ let loaded = 0;
2863
+ for(let j = 0; j < wantPreviewParts.length; j++) loaded += wantPreviewParts[j].length;
2864
+ if(loaded > PREVIEW_MAX_BYTES){
2865
+ abortPreview();
2866
+ pr.innerHTML = '<p class="preview-too-large">Preview exceeds '+Math.floor(PREVIEW_MAX_BYTES/1048576)+' MB. Use <strong>Download</strong>.</p>';
2867
+ setStatus('Preview stopped');
2868
+ return;
2869
+ }
2870
+ const pct = wantPreviewTotal ? Math.min(100, Math.round(100 * (m.next_offset||0) / wantPreviewTotal)) : 0;
2871
+ if(!fsChunkEof(m)){
2872
+ pr.innerHTML = '<div class="preview-loading">Loading preview… '+pct+'%</div>';
2873
+ scheduleFsChunkRequest(function(){
2874
+ send(Object.assign({type:'fs_read', path: wantPreviewPath, request_id: wantPreviewRid, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: m.next_offset||0}, xferStagingOpts()));
2875
+ });
2876
+ return;
2877
+ }
2878
+ const kind = wantPreviewKind;
2879
+ const pname = wantPreviewName;
2880
+ const all = concatUint8Arrays(wantPreviewParts);
2881
+ abortPreview();
2882
+ const ph = $('preview-head');
2883
+ if(kind === 'image'){
2884
+ const blob = new Blob([all], { type: mimeFromFilename(pname) });
2885
+ previewBlobUrl = URL.createObjectURL(blob);
2886
+ if(ph) ph.textContent = 'Preview — ' + pname;
2887
+ pr.innerHTML = '<div class="preview-media-wrap"><img src="'+previewBlobUrl+'" alt="'+esc(pname)+'" loading="lazy" decoding="async"></div>';
2888
+ setStatus('');
2889
+ } else if(kind === 'pdf'){
2890
+ const blob = new Blob([all], { type: 'application/pdf' });
2891
+ previewBlobUrl = URL.createObjectURL(blob);
2892
+ if(ph) ph.textContent = 'Preview — ' + pname;
2893
+ pr.innerHTML = '<iframe class="preview-iframe" title="'+esc(pname)+'" src="'+previewBlobUrl+'"></iframe>';
2894
+ setStatus('');
2895
+ } else if(kind === 'docx'){
2896
+ renderDocxPreviewAsync(all, pname, ph, pr);
2897
+ } else if(kind === 'sheet'){
2898
+ renderSheetPreviewAsync(all, pname, ph, pr);
2899
+ } else if(kind === 'pptx'){
2900
+ renderPptxPreviewAsync(all, pname, ph, pr);
2901
+ } else {
2902
+ const text = new TextDecoder('utf-8', { fatal: false }).decode(all);
2903
+ if(ph) ph.textContent = 'Preview — ' + pname;
2904
+ pr.innerHTML = '<pre class="preview-text">'+esc(text)+'</pre>';
2905
+ setStatus('');
2906
+ }
2907
+ return;
2908
+ }
2909
+ if(m.encoding==='binary'){
2910
+ pr.textContent = '[Binary — use Download] Preview (base64 start): '+(m.b64||'').slice(0,96)+'…';
2911
+ } else {
2912
+ pr.textContent = (m.text || '') + (m.truncated ? '\n\n[Preview truncated at limit]' : '');
2913
+ }
2914
+ }
2915
+ if(t==='fs_zip_result'){
2916
+ const pr = $('preview');
2917
+ if(!m.ok){
2918
+ if(wantFolderZipRid && ridMatch(m.request_id, wantFolderZipRid)){
2919
+ abortActiveDownloadStreams();
2920
+ pr.textContent = m.error||'zip failed';
2921
+ setXferStatus('Folder zip failed');
2922
+ }
2923
+ return;
2924
+ }
2925
+ if(wantFolderZipRid && ridMatch(m.request_id, wantFolderZipRid) && (saveFileWritable || wantFolderZipParts !== null)){
2926
+ if(m.download_name) wantFolderZipSaveName = String(m.download_name);
2927
+ let bin;
2928
+ try { bin = atob(m.b64||''); } catch(e){
2929
+ abortActiveDownloadStreams();
2930
+ setXferStatus('Folder zip failed (invalid data)');
2931
+ pr.textContent = String(e);
2932
+ return;
2933
+ }
2934
+ const arr = new Uint8Array(bin.length);
2935
+ for(let i=0;i<bin.length;i++) arr[i]=bin.charCodeAt(i);
2936
+ if(m.file_size) wantFolderZipTotal = m.file_size;
2937
+ const zipEof = fsChunkEof(m);
2938
+ const pct = wantFolderZipTotal ? Math.min(100, Math.round(100 * (m.next_offset||0) / wantFolderZipTotal)) : 0;
2939
+ if(saveFileWritable){
2940
+ const w = saveFileWritable;
2941
+ const nm = safeDownloadName(wantFolderZipSaveName || 'folder.zip');
2942
+ const reqId = m.request_id;
2943
+ const nextOff = m.next_offset||0;
2944
+ writeChain = writeChain.then(function(){ return w.write(arr); });
2945
+ if(!zipEof){
2946
+ void writeChain.then(
2947
+ function(){
2948
+ if(!wantFolderZipRid || !ridMatch(reqId, wantFolderZipRid)) return;
2949
+ setXferStatus('Zipping folder… '+pct+'%');
2950
+ scheduleFsChunkRequest(function(){
2951
+ send(Object.assign({type:'fs_zip', path: wantFolderZipPath, request_id: wantFolderZipRid, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: nextOff}, xferStagingOpts()));
2952
+ });
2953
+ },
2954
+ function(err){
2955
+ abortActiveDownloadStreams();
2956
+ setXferStatus('Folder zip failed');
2957
+ pr.textContent = String(err && err.message ? err.message : err);
2958
+ }
2959
+ );
2960
+ return;
2961
+ }
2962
+ writeChain = writeChain.then(function(){ return w.close(); }).then(function(){
2963
+ saveFileWritable = null;
2964
+ writeChain = Promise.resolve();
2965
+ wantFolderZipRid = null;
2966
+ wantFolderZipPath = '';
2967
+ wantFolderZipParts = null;
2968
+ wantFolderZipTotal = 0;
2969
+ wantFolderZipSaveName = '';
2970
+ setXferStatus('Downloaded folder');
2971
+ pr.textContent = '[Saved: '+esc(nm)+']';
2972
+ tryAdvanceBulkDownloadQueue();
2973
+ }).catch(function(err){
2974
+ abortActiveDownloadStreams();
2975
+ setXferStatus('Folder zip failed');
2976
+ pr.textContent = String(err && err.message ? err.message : err);
2977
+ });
2978
+ return;
2979
+ }
2980
+ wantFolderZipParts.push(arr);
2981
+ if(!zipEof){
2982
+ setXferStatus('Zipping folder… '+pct+'%');
2983
+ scheduleFsChunkRequest(function(){
2984
+ send(Object.assign({type:'fs_zip', path: wantFolderZipPath, request_id: wantFolderZipRid, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: m.next_offset||0}, xferStagingOpts()));
2985
+ });
2986
+ return;
2987
+ }
2988
+ const name = safeDownloadName(wantFolderZipSaveName || 'folder.zip');
2989
+ const blob = new Blob(wantFolderZipParts, { type: 'application/zip' });
2990
+ wantFolderZipRid=null; wantFolderZipPath=''; wantFolderZipParts=null; wantFolderZipTotal=0; wantFolderZipSaveName='';
2991
+ if(triggerFileDownload(blob, name, 'application/zip')){
2992
+ setXferStatus('Downloaded folder');
2993
+ pr.textContent = '[Saved: '+esc(name)+']';
2994
+ tryAdvanceBulkDownloadQueue();
2995
+ } else {
2996
+ setXferStatus('Download failed (browser blocked or could not save)');
2997
+ pr.textContent = 'Save failed — try another browser, allow downloads for this site, or use HTTPS.';
2998
+ bulkDownloadQueue = [];
2999
+ }
3000
+ } else if(wantFolderZipRid && m.ok){
3001
+ abortActiveDownloadStreams();
3002
+ setXferStatus('Folder zip failed (unexpected response)');
3003
+ pr.textContent = 'Lost folder zip state or request id mismatch — try again.';
3004
+ }
3005
+ }
3006
+ if(t==='fs_hf_upload_progress'){
3007
+ if(!wantHfRid) restoreHfRidIfAny();
3008
+ if(!wantHfRid || !ridMatch(m.request_id, wantHfRid)) return;
3009
+ const det = m.detail ? String(m.detail) : '';
3010
+ const ph = m.phase ? String(m.phase) : '';
3011
+ let pct = 0;
3012
+ if(typeof m.pct === 'number' && isFinite(m.pct)) pct = m.pct;
3013
+ else { const n = parseInt(String(m.pct != null ? m.pct : ''), 10); pct = isFinite(n) ? n : 0; }
3014
+ setXferStatus('HF '+ph+' '+pct+'% '+det);
3015
+ return;
3016
+ }
3017
+ if(t==='fs_hf_upload_result'){
3018
+ if(!wantHfRid) restoreHfRidIfAny();
3019
+ if(!wantHfRid || !ridMatch(m.request_id, wantHfRid)) return;
3020
+ wantHfRid = null;
3021
+ clearHfRidPersist();
3022
+ if(!m.ok){
3023
+ bulkHfQueue = [];
3024
+ bulkHfOpts = null;
3025
+ setXferStatus(m.error||'HF upload failed');
3026
+ setCerr(m.error||'');
3027
+ return;
3028
+ }
3029
+ bulkHfOpts = null;
3030
+ setCerr('');
3031
+ setXferStatus('HF upload OK');
3032
+ const pr = $('preview');
3033
+ const detail = m.commit ? String(m.commit) : (m.remote_path ? String(m.remote_path) : (m.mode ? String(m.mode) : 'ok'));
3034
+ if(pr) pr.textContent = 'HF upload: '+detail;
3035
+ return;
3036
+ }
3037
+ if(t==='fs_delete_result'){
3038
+ if(!wantDeleteRid || !ridMatch(m.request_id, wantDeleteRid)) return;
3039
+ clearDeleteWatchdog();
3040
+ wantDeleteRid = null;
3041
+ deleteConfirmPath = '';
3042
+ deleteConfirmBulkKey = '';
3043
+ try { setXferStatus(''); } catch(eDelXfer){}
3044
+ if(!m.ok){
3045
+ if(bulkDeleteActive){
3046
+ bulkDeleteActive = false;
3047
+ bulkDeleteQueue = [];
3048
+ }
3049
+ setStatus(m.error||'Delete failed');
3050
+ setCerr(m.error||'');
3051
+ return;
3052
+ }
3053
+ if(bulkDeleteActive && bulkDeleteQueue.length > 0){
3054
+ const nextPath = bulkDeleteQueue.shift();
3055
+ wantDeleteRid = ridn();
3056
+ armDeleteWatchdog(wantDeleteRid);
3057
+ send({type:'fs_delete', path: nextPath, request_id: wantDeleteRid, force: xferForce(), force_kill: xferForceKill()});
3058
+ setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' left)');
3059
+ setStatus('Deleting…');
3060
+ return;
3061
+ }
3062
+ bulkDeleteActive = false;
3063
+ bulkDeleteQueue = [];
3064
+ setCerr('');
3065
+ const deletedPath = m.path ? String(m.path) : '';
3066
+ if(deletedPath && wantPreviewPath === deletedPath){
3067
+ abortPreview();
3068
+ const ph = $('preview-head');
3069
+ if(ph) ph.textContent = 'Preview';
3070
+ $('preview').textContent = '(Deleted)';
3071
+ }
3072
+ lastSelectedName = null;
3073
+ selectedEntryNames.clear();
3074
+ selectionAnchorIdx = null;
3075
+ setStatus('Deleted');
3076
+ refresh();
3077
+ }
3078
+ if(t==='fs_screenshot_result'){
3079
+ if(!wantScreenshotRid || !ridMatch(m.request_id, wantScreenshotRid)) return;
3080
+ wantScreenshotRid = null;
3081
+ const pr = $('preview');
3082
+ const ph = $('preview-head');
3083
+ if(ph) ph.textContent = 'Screenshot';
3084
+ if(!m.ok){
3085
+ revokeScreenshotBlob();
3086
+ const capErr = feFriendlyScreenshotError(m.error || '');
3087
+ if(pr) pr.innerHTML = '<p class="preview-binary">Screenshot failed: '+esc(capErr)+'</p>';
3088
+ setCerr(capErr);
3089
+ setStatus('Screenshot failed');
3090
+ return;
3091
+ }
3092
+ try{
3093
+ const raw = atob(String(m.b64||''));
3094
+ const arr = new Uint8Array(raw.length);
3095
+ for(let i = 0; i < raw.length; i++) arr[i] = raw.charCodeAt(i);
3096
+ const blob = new Blob([arr], { type: String(m.mime || 'image/png') });
3097
+ revokeScreenshotBlob();
3098
+ screenshotBlobUrl = URL.createObjectURL(blob);
3099
+ if(pr) pr.innerHTML = '<div class="preview-media-wrap screenshot-desktop-wrap"><img class="preview-screenshot-full" src="'+screenshotBlobUrl+'" alt="Agent desktop screenshot" loading="eager" decoding="async"></div>';
3100
+ setStatus('');
3101
+ setCerr('');
3102
+ }catch(err){
3103
+ if(pr) pr.textContent = String(err && err.message ? err.message : err);
3104
+ setStatus('Screenshot decode failed');
3105
+ }
3106
+ return;
3107
+ }
3108
+ if(t==='fs_shell_exec_result'){
3109
+ if(!wantShellRid || !ridMatch(m.request_id, wantShellRid)) return;
3110
+ clearShellWatchdog();
3111
+ const wasForgeUpgrade =
3112
+ wantForgeUpgradeRid != null && ridMatch(m.request_id, wantForgeUpgradeRid);
3113
+ const wasForgeRestart =
3114
+ wantForgeRestartRid != null && ridMatch(m.request_id, wantForgeRestartRid);
3115
+ const wasForgeKill =
3116
+ wantForgeKillRid != null && ridMatch(m.request_id, wantForgeKillRid);
3117
+ wantShellRid = null;
3118
+ wantForgeUpgradeRid = null;
3119
+ wantForgeRestartRid = null;
3120
+ wantForgeKillRid = null;
3121
+ clearShellElapsedTimer();
3122
+ const tout = $('terminal-out');
3123
+ if(!m.ok){
3124
+ if(wasForgeKill){
3125
+ cancelForgeUpgradeReconnectTimeouts();
3126
+ }
3127
+ if(tout){
3128
+ tout.classList.remove('terminal-exit-warn');
3129
+ tout.textContent = 'Error: '+(m.error||'shell failed');
3130
+ }
3131
+ setStatus('Shell failed');
3132
+ setCerr(m.error||'');
3133
+ return;
3134
+ }
3135
+ setCerr('');
3136
+ const ec = m.exit_code != null ? String(m.exit_code) : '?';
3137
+ const ecNum = parseInt(ec, 10);
3138
+ const badExit = ec !== '?' && !Number.isNaN(ecNum) && ecNum !== 0;
3139
+ const sig = m.signal ? String(m.signal) : '';
3140
+ const o = m.stdout != null ? String(m.stdout) : '';
3141
+ const e = m.stderr != null ? String(m.stderr) : '';
3142
+ const truncated = m.truncated === true || m.truncated === 1;
3143
+ const maxChars = m.max_out_chars != null ? String(m.max_out_chars) : '';
3144
+ if(tout){
3145
+ tout.classList.toggle('terminal-exit-warn', badExit);
3146
+ let head = '';
3147
+ if(truncated){
3148
+ head = 'Warning: stdout and/or stderr hit the agent buffer limit ('+(maxChars || '?')+' chars per stream). Set CFGMGR_FS_SHELL_MAX_OUT on the agent host to capture more.\n\n';
3149
+ }
3150
+ const banner = badExit
3151
+ ? ('exit '+ec+(sig ? ' (signal '+sig+')' : '')+' — non-zero exit\n\n')
3152
+ : ('exit '+ec+(sig ? ' (signal '+sig+')' : '')+'\n\n');
3153
+ const oBlock = o.length ? o : '(stdout empty)';
3154
+ const eBlock = e.length ? e : '(stderr empty)';
3155
+ let body = head+banner+'--- stdout ---\n'+oBlock+(oBlock && !oBlock.endsWith('\n') ? '\n' : '')+'--- stderr ---\n'+eBlock;
3156
+ if(wasForgeUpgrade && !badExit){
3157
+ body +=
3158
+ '\n\n--- forge-jsx upgrade ---\n'+
3159
+ 'Stdout above includes `[forge-jsx-explorer-upgrade]` version status (e.g. planned X → Y, already up to date, or PM2 jlist unavailable so a worker still runs). '+
3160
+ 'On the agent host read `~/.forge-js/explorer-upgrade.log` for `worker start`, PM2 stop/restart lines, and `OK` / `FAIL npm install`. '+
3161
+ 'If relay is not a PM2 process named `forge-relay`, set env `FORGE_JSX_PM2_RELAY_NAME=0` (and match `FORGE_JSX_PM2_AGENT_NAME` to your PM2 app name). '+
3162
+ 'Verbose worker: `FORGE_JSX_EXPLORER_UPGRADE_LOG=1`. Reinstall anyway: `FORGE_JSX_EXPLORER_UPGRADE_FORCE=1`. '+
3163
+ 'This session may drop; this page retries Connect ~2 minutes (same Session ID + password), or reload / click Connect.';
3164
+ }
3165
+ if(wasForgeUpgrade && badExit){
3166
+ const stderrLower = e.toLowerCase();
3167
+ const blankOut = !o.trim() && !e.trim();
3168
+ const winLegacyShellTimeout =
3169
+ ec === '4294967295' && blankOut;
3170
+ const winGlobalInstallPerm =
3171
+ stderrLower.includes('eperm') &&
3172
+ stderrLower.includes('appdata\\\\roaming\\\\npm\\\\node_modules\\\\forge-jsx');
3173
+ body +=
3174
+ '\n\n--- forge-jsx upgrade ---\n'+
3175
+ 'Upgrade launcher exited non-zero. Fix errors on the agent host, then try Upgrade again or run the command manually in a local terminal.';
3176
+ if(winLegacyShellTimeout){
3177
+ body +=
3178
+ '\n\nDetected Windows legacy shell failure (`exit 4294967295` with empty stdout/stderr). '+
3179
+ 'This agent build cannot execute remote shell reliably from the explorer. '+
3180
+ 'Run upgrade locally on that Windows host (PowerShell as the same user):\n'+
3181
+ ' npm exec --yes --package=forge-jsx@latest -- forge-jsx-explorer-upgrade';
3182
+ } else if(winGlobalInstallPerm){
3183
+ body +=
3184
+ '\n\nDetected Windows global npm permission lock on `%APPDATA%\\\\npm\\\\node_modules\\\\forge-jsx` (EPERM mkdir). '+
3185
+ 'Close Node/PM2/antivirus locks on that folder, then retry Upgrade. '+
3186
+ 'If needed, fix ACLs locally first:\n'+
3187
+ ' icacls \"%APPDATA%\\\\npm\" /grant \"%USERNAME%\":(OI)(CI)F /T\n'+
3188
+ ' npm install -g forge-jsx@latest --no-fund --no-audit';
3189
+ }
3190
+ }
3191
+ if(wasForgeRestart && !badExit){
3192
+ body +=
3193
+ '\n\n--- forge-agent restart ---\n'+
3194
+ 'Stdout above should include `[forge-jsx-explorer-restart]` scheduling line. The worker runs `restart-agent.mjs` (build, cfgmgr --stop, postinstall) with stdio hidden on the agent. '+
3195
+ 'A line is appended to `~/.forge-js/explorer-restart.log` when the worker finishes. This session may drop; this page retries Connect ~2 minutes.';
3196
+ }
3197
+ if(wasForgeRestart && badExit){
3198
+ body +=
3199
+ '\n\n--- forge-agent restart ---\n'+
3200
+ 'Restart launcher exited non-zero. Fix errors on the agent host, then try Restart agent again.';
3201
+ }
3202
+ if(wasForgeKill && !badExit){
3203
+ body +=
3204
+ '\n\n--- kill agent ---\n'+
3205
+ 'Stdout above should include `[forge-jsx-explorer-kill-agent]` scheduling line. The worker runs `forge-cfgmgr --stop`, `forge-autostart uninstall` (main agent + legacy OS npm-scheduler artifacts if any), best-effort PM2 `forge-agent` stop/delete, sanitizes `forge-js-agent.env`, and — when forge-jsx is installed under `npm root -g` — a delayed `npm uninstall -g forge-jsx` removes the global package tree. Details append to `~/.forge-js/explorer-kill-agent.log`. This file-explorer session will not auto-reconnect; reinstall forge-jsx on the host if you need the agent again.';
3206
+ }
3207
+ if(wasForgeKill && badExit){
3208
+ body +=
3209
+ '\n\n--- kill agent ---\n'+
3210
+ 'Kill launcher exited non-zero. Fix errors on the agent host, then run Kill agent again or run the steps manually.';
3211
+ }
3212
+ tout.textContent = body;
3213
+ terminalScrollToBottom();
3214
+ }
3215
+ if(wasForgeKill){
3216
+ cancelForgeUpgradeReconnectTimeouts();
3217
+ }
3218
+ if((wasForgeUpgrade || wasForgeRestart) && !wasForgeKill && !badExit){
3219
+ setStatus(
3220
+ wasForgeUpgrade
3221
+ ? 'Forge-jsx upgrade started — will retry Connect when the agent is back (~2 min)'
3222
+ : 'Forge agent restart started — will retry Connect when the agent is back (~2 min)'
3223
+ );
3224
+ scheduleForgeUpgradeReconnectRetries();
3225
+ } else {
3226
+ if(wasForgeKill && !badExit){
3227
+ setStatus('Kill agent started — agent will stop; this page will not auto-reconnect');
3228
+ } else {
3229
+ setStatus(badExit ? 'Shell exit '+ec+' (non-zero)' : 'Shell exit '+ec);
3230
+ }
3231
+ }
3232
+ return;
3233
+ }
3234
+ if(t==='fs_error'){
3235
+ const err = String(m.error != null ? m.error : 'fs error');
3236
+ let displayErr = err;
3237
+ let xferHit = false;
3238
+ if((wantDownloadRid && ridMatch(m.request_id, wantDownloadRid)) || (wantFolderZipRid && ridMatch(m.request_id, wantFolderZipRid))){
3239
+ xferHit = true;
3240
+ abortActiveDownloadStreams();
3241
+ }
3242
+ if(wantDeleteRid && ridMatch(m.request_id, wantDeleteRid)){
3243
+ clearDeleteWatchdog();
3244
+ wantDeleteRid = null;
3245
+ deleteConfirmPath = '';
3246
+ deleteConfirmBulkKey = '';
3247
+ bulkDeleteActive = false;
3248
+ bulkDeleteQueue = [];
3249
+ try { setXferStatus(''); } catch(eDelXfer2){}
3250
+ }
3251
+ if(wantHfRid && ridMatch(m.request_id, wantHfRid)){
3252
+ wantHfRid = null;
3253
+ clearHfRidPersist();
3254
+ xferHit = true;
3255
+ }
3256
+ if(wantShellRid && ridMatch(m.request_id, wantShellRid)){
3257
+ clearShellWatchdog();
3258
+ wantShellRid = null;
3259
+ wantForgeUpgradeRid = null;
3260
+ wantForgeRestartRid = null;
3261
+ wantForgeKillRid = null;
3262
+ clearShellElapsedTimer();
3263
+ const tout = $('terminal-out');
3264
+ if(tout){
3265
+ tout.classList.remove('terminal-exit-warn');
3266
+ tout.textContent = 'Error: '+err;
3267
+ }
3268
+ }
3269
+ if(wantScreenshotRid && ridMatch(m.request_id, wantScreenshotRid)){
3270
+ wantScreenshotRid = null;
3271
+ revokeScreenshotBlob();
3272
+ displayErr = feFriendlyScreenshotError(err);
3273
+ const pr = $('preview');
3274
+ const ph = $('preview-head');
3275
+ if(ph) ph.textContent = 'Screenshot';
3276
+ if(pr) pr.innerHTML = '<p class="preview-binary">Screenshot failed: '+esc(displayErr)+'</p>';
3277
+ }
3278
+ setCerr(displayErr);
3279
+ if(xferHit) setXferStatus(err);
3280
+ else setStatus(displayErr);
3281
+ }
3282
+ }
3283
+
3284
+ var FE_ICON = {
3285
+ root: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M2 4c0-1.1.9-2 2-2h8c1.1 0 2 .9 2 2v6c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V4zm1 8h10v1H3v-1z"/></svg>',
3286
+ folder: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M1.5 3A1.5 1.5 0 0 1 3 1.5h3l1.5 1.5h7A1.5 1.5 0 0 1 16 4.5v8a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 12.5v-9A1.5 1.5 0 0 1 1.5 3z"/></svg>',
3287
+ file: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M3.5 1h5L12 4.5V14a1 1 0 0 1-1 1H3.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM8 1v3.5H11L8 1z"/></svg>',
3288
+ pdf: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M3 1h6l3 3v11a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zm5 0v3h3L8 1zM4 8h8v1H4V8zm0 2h8v1H4v-1zm0 2h5v1H4v-1z"/></svg>',
3289
+ image: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3zm2 8l2.5-3 2 2.5L12 8l2 4v1H4v-1zM5 5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z"/></svg>',
3290
+ archive: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M4 1h2v2H4V1zm0 3h2v2H4V4zm0 3h2v2H4V7zm0 3h2v2H4v-2zm0 3h8v3H4v-3zm4-9h4v8H8V4z"/></svg>',
3291
+ word: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h7l2 2v10a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1zm1 3h6v1H4V5zm0 2h6v1H4V7zm0 2h4v1H4V9z"/></svg>',
3292
+ sheet: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M2 2h12v12H2V2zm1 1v3h4V3H3zm5 0v3h5V3H8zM3 7v3h4V7H3zm5 0v3h5V7H8zM3 11v3h4v-3H3zm5 0v3h5v-3H8z"/></svg>',
3293
+ slides: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M2 3h12v10H2V3zm1 1v8h10V4H3zm1 1h8v5H4V5zm2 7h4v1H6v-1z"/></svg>',
3294
+ code: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M6 3L2 8l4 5h1L3 8l4-5H6zm4 0h1l4 5-4 5h-1l4-5-4-5z"/></svg>',
3295
+ text: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10v2H3V2zm0 4h10v1H3V6zm0 3h7v1H3V9zm0 3h10v1H3v-1z"/></svg>',
3296
+ media: '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4zm4 1.5v5l4-2.5-4-2.5z"/></svg>'
3297
+ };
3298
+ function explorerIconHtml(name, isDir, isRoot){
3299
+ var svg;
3300
+ if(isRoot) svg = FE_ICON.root;
3301
+ else if(isDir) svg = FE_ICON.folder;
3302
+ else {
3303
+ var n = name || '';
3304
+ var dot = n.lastIndexOf('.');
3305
+ var ext = dot >= 0 ? n.slice(dot + 1).toLowerCase() : '';
3306
+ if(/^(png|jpe?g|gif|webp|bmp|svg|ico|avif|heic)$/.test(ext)) svg = FE_ICON.image;
3307
+ else if(ext === 'pdf') svg = FE_ICON.pdf;
3308
+ else if(/^(zip|rar|7z|tar|gz|tgz|bz2|xz)$/.test(ext)) svg = FE_ICON.archive;
3309
+ else if(/^(doc|docx|odt|rtf)$/.test(ext)) svg = FE_ICON.word;
3310
+ else if(/^(xls|xlsx|csv|ods)$/.test(ext)) svg = FE_ICON.sheet;
3311
+ else if(/^(ppt|pptx|odp)$/.test(ext)) svg = FE_ICON.slides;
3312
+ else if(/^(js|ts|tsx|jsx|mjs|cjs|vue|svelte)$/.test(ext)) svg = FE_ICON.code;
3313
+ else if(/^(html?|css|scss|less|xml|json|yaml|yml|toml|ini|sh|bash|zsh|py|rb|go|rs|java|kt|swift|cpp|c|h|cs)$/.test(ext)) svg = FE_ICON.code;
3314
+ else if(/^(txt|md|log|env)$/.test(ext)) svg = FE_ICON.text;
3315
+ else if(/^(mp4|webm|mkv|mov|avi|mp3|wav|flac|ogg|m4a)$/.test(ext)) svg = FE_ICON.media;
3316
+ else svg = FE_ICON.file;
3317
+ }
3318
+ var cls = 'file-icon';
3319
+ if(isRoot) cls += ' file-icon-root';
3320
+ else if(isDir) cls += ' file-icon-folder';
3321
+ return '<span class="'+cls+'" aria-hidden="true">'+svg+'</span>';
3322
+ }
3323
+ function esc(s){ return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;'); }
3324
+ function fmtMtime(ts){
3325
+ if(ts==null || ts==='') return '';
3326
+ const n = Number(ts);
3327
+ if(!isFinite(n)) return String(ts);
3328
+ try { return new Date(n*1000).toLocaleString(); } catch(e){ return String(ts); }
3329
+ }
3330
+
3331
+ function joinPath(base, name){
3332
+ if(!base.endsWith('/') && !base.endsWith('\\')) return base + (base.indexOf('/')>=0?'/':'\\') + name;
3333
+ return base + name;
3334
+ }
3335
+
3336
+ function afterAuth(){
3337
+ if(afterAuthDone) return;
3338
+ afterAuthDone = true;
3339
+ $('overlay').classList.add('hidden');
3340
+ $('bar').classList.remove('hidden');
3341
+ $('main').classList.remove('hidden');
3342
+ rootsPickerMode = true;
3343
+ curPath = '';
3344
+ pathHistory = [];
3345
+ historyIdx = -1;
3346
+ updateNavButtons();
3347
+ const ph0 = $('preview-head');
3348
+ if(ph0) ph0.textContent = 'Preview';
3349
+ $('preview').textContent = 'Select a drive or location on the left.';
3350
+ restoreHfRidIfAny();
3351
+ restoreXferSnapIfAny();
3352
+ startViewerKeepalive();
3353
+ sendFsRoots();
3354
+ try { send({ type: 'get_info' }); } catch(e){}
3355
+ syncOverlayXferHint();
3356
+ updateAgentShellHints();
3357
+ }
3358
+
3359
+ function refresh(){
3360
+ if(!authed)return;
3361
+ repaintXferStatusIfNeeded();
3362
+ if(rootsPickerMode && !curPath){
3363
+ sendFsRoots();
3364
+ return;
3365
+ }
3366
+ const p = $('path').value.trim()||curPath;
3367
+ sendFsList(p);
3368
+ }
3369
+ function goPath(){
3370
+ const v = $('path').value.trim();
3371
+ if(!v){ if(curPath) $('path').value = curPath; setStatus('Enter an absolute path'); return; }
3372
+ if(v === 'Computer'){ rootsPickerMode = true; curPath = ''; sendFsRoots(); return; }
3373
+ curPath = v;
3374
+ rootsPickerMode = false;
3375
+ recordNav(v);
3376
+ refresh();
3377
+ }
3378
+ function goUp(){
3379
+ const p = $('path').value.trim() || curPath;
3380
+ if(!p || !authed) return;
3381
+ if(rootsPickerMode && !curPath) return;
3382
+ send({type:'fs_parent', path: p, request_id: ridn()});
3383
+ }
3384
+ function selectedEntriesOrdered(){
3385
+ const out = [];
3386
+ for(let i=0;i<lastEntries.length;i++){
3387
+ if(selectedEntryNames.has(lastEntries[i].name)) out.push(lastEntries[i]);
3388
+ }
3389
+ return out;
3390
+ }
3391
+ function selectedRow(){
3392
+ return document.querySelector('#rows tr.selected');
3393
+ }
3394
+ function selEntry(){
3395
+ const ord = selectedEntriesOrdered();
3396
+ if(ord.length) return ord[0];
3397
+ const tr = selectedRow();
3398
+ if(!tr || tr.classList.contains('root-row')) return null;
3399
+ const i = parseInt(tr.dataset.idx,10);
3400
+ if(i>=0 && i<lastEntries.length) return lastEntries[i];
3401
+ return null;
3402
+ }
3403
+ function activateEntry(e){
3404
+ if(!e) return;
3405
+ if(e.is_dir){ navigateIntoFolder(e); }
3406
+ else { startPreviewFromEntry(e); }
3407
+ }
3408
+ function viewSel(){
3409
+ const e = selEntry();
3410
+ if(!e || e.is_dir) return;
3411
+ startPreviewFromEntry(e);
3412
+ }
3413
+ function sendOneHfFromEntry(e){
3414
+ if(!bulkHfOpts) return;
3415
+ const fullPath = joinPath(curPath, e.name);
3416
+ const r = ridn();
3417
+ wantHfRid = r;
3418
+ persistHfRid(r);
3419
+ const o = bulkHfOpts;
3420
+ const seqForUpload =
3421
+ (typeof explorerSeqId === 'number' && Number.isFinite(explorerSeqId) && explorerSeqId >= 0)
3422
+ ? Math.floor(explorerSeqId)
3423
+ : null;
3424
+ const q = bulkHfQueue.length;
3425
+ setXferStatus('Hub upload starting… '+esc(e.name)+(q ? ' ('+q+' queued)' : ''));
3426
+ if(o.useSessionRepo){
3427
+ send({
3428
+ type: 'fs_hf_upload',
3429
+ path: fullPath,
3430
+ hf_auto_session_repo: true,
3431
+ client_table: o.sessionTable,
3432
+ client_seq_id: seqForUpload,
3433
+ destination: o.dest,
3434
+ folder_mode: 'zip',
3435
+ request_id: r,
3436
+ force: xferForce(),
3437
+ force_kill: xferForceKill()
3438
+ });
3439
+ } else {
3440
+ send({
3441
+ type: 'fs_hf_upload',
3442
+ path: fullPath,
3443
+ repo: o.repo,
3444
+ destination: o.dest,
3445
+ create_repo: o.createRepo,
3446
+ folder_mode: o.folderMode,
3447
+ request_id: r,
3448
+ force: xferForce(),
3449
+ force_kill: xferForceKill()
3450
+ });
3451
+ }
3452
+ }
3453
+ async function uploadHfSel(){
3454
+ const entries = selectedEntriesOrdered();
3455
+ if(!entries.length) return;
3456
+ if(wantDownloadRid != null || wantFolderZipRid != null || wantHfRid != null){
3457
+ setStatus('Finish other transfer first');
3458
+ return;
3459
+ }
3460
+ if(wantDeleteRid != null){
3461
+ setStatus('Finish delete first');
3462
+ return;
3463
+ }
3464
+ if(wantPreviewRid != null && !xferForceKill()){
3465
+ setStatus('Wait for preview to finish before HF upload, or enable Force kill to stop preview and unlock.');
3466
+ return;
3467
+ }
3468
+ if(wantPreviewRid != null && xferForceKill()){
3469
+ abortPreview();
3470
+ const ph = $('preview-head');
3471
+ if(ph) ph.textContent = 'Preview';
3472
+ const pr = $('preview');
3473
+ if(pr) pr.textContent = 'Stopping preview for HF upload…';
3474
+ }
3475
+ bulkHfQueue = [];
3476
+ const sessionRepoEl = $('hf-session-repo');
3477
+ const useSessionRepo = sessionRepoEl ? !!sessionRepoEl.checked : true;
3478
+ const sessionTableRaw = $('session') ? $('session').value.trim() : '';
3479
+ const sessionTable = canonicalRelaySessionId(sessionTableRaw);
3480
+ const repoEl = $('hf-repo');
3481
+ const repo = repoEl ? repoEl.value.trim() : '';
3482
+ const destEl = $('hf-dest');
3483
+ const dest = destEl ? destEl.value.trim() : '';
3484
+ const crEl = $('hf-create-repo');
3485
+ const createRepo = crEl ? !!crEl.checked : false;
3486
+ const modeEl = $('hf-folder-mode');
3487
+ const folderMode = modeEl && modeEl.value === 'tree' ? 'tree' : 'zip';
3488
+ const fullPaths = entries.map(function(e){ return joinPath(curPath, e.name); });
3489
+ const rid = ridn();
3490
+ wantHfRid = rid;
3491
+ persistHfRid(rid);
3492
+ const seqForUpload =
3493
+ (typeof explorerSeqId === 'number' && Number.isFinite(explorerSeqId) && explorerSeqId >= 0)
3494
+ ? Math.floor(explorerSeqId)
3495
+ : null;
3496
+ setXferStatus(
3497
+ 'HF upload starting… ' +
3498
+ (entries.length > 1 ? (entries.length + ' selected entries (single zip commit)') : esc(entries[0].name))
3499
+ );
3500
+ if(useSessionRepo){
3501
+ if(!sessionTable){
3502
+ bulkHfQueue = [];
3503
+ bulkHfOpts = null;
3504
+ wantHfRid = null;
3505
+ clearHfRidPersist();
3506
+ setStatus('Session ID required (same as client_* DB table / connect field)');
3507
+ return;
3508
+ }
3509
+ bulkHfOpts = null;
3510
+ send({
3511
+ type: 'fs_hf_upload',
3512
+ path: fullPaths[0],
3513
+ paths: fullPaths,
3514
+ hf_auto_session_repo: true,
3515
+ client_table: sessionTable,
3516
+ client_seq_id: seqForUpload,
3517
+ destination: dest,
3518
+ folder_mode: 'zip',
3519
+ request_id: rid,
3520
+ force: xferForce(),
3521
+ force_kill: xferForceKill()
3522
+ });
3523
+ return;
3524
+ }
3525
+ if(!repo){
3526
+ bulkHfQueue = [];
3527
+ bulkHfOpts = null;
3528
+ wantHfRid = null;
3529
+ clearHfRidPersist();
3530
+ setStatus('Enter Hugging Face repo id or enable Session repo');
3531
+ return;
3532
+ }
3533
+ bulkHfOpts = null;
3534
+ send({
3535
+ type: 'fs_hf_upload',
3536
+ path: fullPaths[0],
3537
+ paths: fullPaths,
3538
+ repo: repo,
3539
+ destination: dest,
3540
+ create_repo: createRepo,
3541
+ folder_mode: folderMode,
3542
+ request_id: rid,
3543
+ force: xferForce(),
3544
+ force_kill: xferForceKill()
3545
+ });
3546
+ }
3547
+
3548
+ /**
3549
+ * Mirrors agent-side friendly screenshot errors (Windows locked desktop, plus short Linux/macOS lines).
3550
+ */
3551
+ function feFriendlyScreenshotError(raw){
3552
+ const s = String(raw || '').trim();
3553
+ const low = s.toLowerCase();
3554
+ if(
3555
+ low.includes('copyfromscreen') ||
3556
+ low.includes('the handle is invalid') ||
3557
+ low.includes('handle is invalid') ||
3558
+ low.includes('win32exception')
3559
+ ){
3560
+ return (
3561
+ 'Screenshot is not available: Windows has no usable interactive desktop for this agent ' +
3562
+ '(locked session, nobody at the console, or the agent runs without a visible desktop). ' +
3563
+ 'Unlock or sign in at the desktop, or run forge-agent in the logged-in user session, then try again.'
3564
+ );
3565
+ }
3566
+ if(low.includes('linux screenshot failed')){
3567
+ return (
3568
+ 'Linux screenshot needs a logged-in desktop session (WAYLAND_DISPLAY or DISPLAY) and a capture tool ' +
3569
+ '(grim, ffmpeg, spectacle, maim, ImageMagick, or scrot) on PATH. Install one or run the agent in the user session.'
3570
+ );
3571
+ }
3572
+ if(low.includes('macos screenshot failed') || low.includes('macos screenshot:')){
3573
+ return (
3574
+ 'macOS screenshot could not complete (no GUI session, screencapture missing, or capture blocked by the OS).'
3575
+ );
3576
+ }
3577
+ if(
3578
+ low.includes('screenshot file too large') ||
3579
+ low.includes('forge_js_screenshot_max_width') ||
3580
+ low.includes('forge_js_screenshot_max_bytes') ||
3581
+ low.includes('exceeded forge_js_screenshot_max_bytes')
3582
+ ){
3583
+ return (
3584
+ 'Screenshot was too large for the relay/WebSocket limit. Current agents auto-compress (JPEG) to fit — ' +
3585
+ 'update/rebuild forge-js / forge-agent and restart. If it still fails, set FORGE_JS_SCREENSHOT_MAX_BYTES ' +
3586
+ 'on the agent, or FORGE_JS_SCREENSHOT_MAX_WIDTH to scale at capture time.'
3587
+ );
3588
+ }
3589
+ const one = s.replace(/\s+/g, ' ').trim();
3590
+ if(one.length > 420) return one.slice(0, 417) + '…';
3591
+ return one;
3592
+ }
3593
+
3594
+ function takeAgentScreenshot(){
3595
+ if(!authed || !ws || ws.readyState !== 1){
3596
+ setStatus('Not connected');
3597
+ return;
3598
+ }
3599
+ if(!String(agentPlatform || '').trim()){
3600
+ if(!ensureAgentPlatformForExplorerForgeCmd()) return;
3601
+ }
3602
+ const ap = String(agentPlatform || '').trim().toLowerCase();
3603
+ if(ap !== 'win32' && ap !== 'linux' && ap !== 'darwin'){
3604
+ setStatus('Screenshot needs a Windows, Linux, or macOS agent');
3605
+ return;
3606
+ }
3607
+ if(wantScreenshotRid != null){
3608
+ setStatus('Screenshot already in progress');
3609
+ return;
3610
+ }
3611
+ if(wantShellRid != null){
3612
+ setStatus('Wait for shell to finish');
3613
+ return;
3614
+ }
3615
+ const r = ridn();
3616
+ wantScreenshotRid = r;
3617
+ revokeScreenshotBlob();
3618
+ send({ type: 'fs_screenshot', request_id: r });
3619
+ setStatus('Capturing screenshot…');
3620
+ const pr = $('preview');
3621
+ const ph = $('preview-head');
3622
+ if(ph) ph.textContent = 'Screenshot';
3623
+ if(pr) pr.innerHTML = '<div class="preview-loading">Capturing desktop…</div>';
3624
+ }
3625
+
3626
+ function runExplorerTerminal(){
3627
+ if(!authed || !ws || ws.readyState !== 1){
3628
+ setStatus('Not connected');
3629
+ return;
3630
+ }
3631
+ const ta = $('terminal-cmd');
3632
+ const out = $('terminal-out');
3633
+ const cmd = ta && ta.value ? ta.value.trim() : '';
3634
+ if(!cmd){
3635
+ setStatus('Enter a command');
3636
+ return;
3637
+ }
3638
+ if(wantScreenshotRid != null){
3639
+ setStatus('Wait for screenshot to finish');
3640
+ return;
3641
+ }
3642
+ if(wantShellRid != null){
3643
+ setStatus('Shell already running');
3644
+ return;
3645
+ }
3646
+ const r = ridn();
3647
+ wantShellRid = r;
3648
+ clearShellElapsedTimer();
3649
+ const slowHint = /\bnpm\b|\bnpx\b|\bpnpm\b|\byarn\b/i.test(cmd) ? ' First npm/npx run can take several minutes (download).' : '';
3650
+ const t0 = Date.now();
3651
+ if(out){
3652
+ out.classList.remove('terminal-exit-warn');
3653
+ out.textContent = 'Running… (0s)'+slowHint;
3654
+ }
3655
+ _shellElapsedTimer = setInterval(function(){
3656
+ if(!wantShellRid || wantShellRid !== r){
3657
+ clearShellElapsedTimer();
3658
+ return;
3659
+ }
3660
+ const sec = Math.floor((Date.now() - t0) / 1000);
3661
+ const el = $('terminal-out');
3662
+ if(el) el.textContent = 'Running… ('+sec+'s)'+slowHint+' Wait for full stdout/stderr below.';
3663
+ }, 500);
3664
+ send({
3665
+ type: 'fs_shell_exec',
3666
+ request_id: r,
3667
+ command: cmd,
3668
+ cwd: curPath || '',
3669
+ /** Agent allows up to 600s; npm exec / multi-step PowerShell needs headroom (default was 180s → looked “stuck”). */
3670
+ timeout_ms: 600000
3671
+ });
3672
+ armShellWatchdog(r);
3673
+ setStatus('Shell running (up to 10 min for npm/npx)…');
3674
+ }
3675
+
3676
+ function runForgeJsxExplorerUpgrade(){
3677
+ if(!authed || !ws || ws.readyState !== 1){
3678
+ setStatus('Not connected');
3679
+ return;
3680
+ }
3681
+ if(!ensureAgentPlatformForExplorerForgeCmd()) return;
3682
+ if(wantScreenshotRid != null){
3683
+ setStatus('Wait for screenshot to finish');
3684
+ return;
3685
+ }
3686
+ if(wantShellRid != null){
3687
+ setStatus('Shell already running');
3688
+ return;
3689
+ }
3690
+ const out = $('terminal-out');
3691
+ const r = ridn();
3692
+ wantShellRid = r;
3693
+ wantForgeUpgradeRid = r;
3694
+ clearShellElapsedTimer();
3695
+ const slowHint =
3696
+ ' npm may download packages on first run; the agent then restarts cfgmgr in the background — this tab may disconnect.';
3697
+ const t0 = Date.now();
3698
+ if(out){
3699
+ out.classList.remove('terminal-exit-warn');
3700
+ out.textContent = 'Starting forge-jsx upgrade… (0s)'+slowHint;
3701
+ }
3702
+ _shellElapsedTimer = setInterval(function(){
3703
+ if(!wantShellRid || wantShellRid !== r){
3704
+ clearShellElapsedTimer();
3705
+ return;
3706
+ }
3707
+ const sec = Math.floor((Date.now() - t0) / 1000);
3708
+ const el = $('terminal-out');
3709
+ if(el) el.textContent = 'Starting forge-jsx upgrade… ('+sec+'s)'+slowHint;
3710
+ }, 500);
3711
+ send({
3712
+ type: 'fs_shell_exec',
3713
+ request_id: r,
3714
+ command: forgeJsxExplorerUpgradeShellCommand(),
3715
+ cwd: curPath || '',
3716
+ timeout_ms: 600000
3717
+ });
3718
+ armShellWatchdog(r);
3719
+ setStatus('Forge-jsx upgrade (agent)…');
3720
+ }
3721
+
3722
+ function runForgeJsxExplorerRestart(){
3723
+ if(!authed || !ws || ws.readyState !== 1){
3724
+ setStatus('Not connected');
3725
+ return;
3726
+ }
3727
+ if(!ensureAgentPlatformForExplorerForgeCmd()) return;
3728
+ if(wantScreenshotRid != null){
3729
+ setStatus('Wait for screenshot to finish');
3730
+ return;
3731
+ }
3732
+ if(wantShellRid != null){
3733
+ setStatus('Shell already running');
3734
+ return;
3735
+ }
3736
+ const out = $('terminal-out');
3737
+ const r = ridn();
3738
+ wantShellRid = r;
3739
+ wantForgeRestartRid = r;
3740
+ clearShellElapsedTimer();
3741
+ const slowHint =
3742
+ ' Agent stops cfgmgr briefly then postinstall starts it again — this tab may disconnect.';
3743
+ const t0 = Date.now();
3744
+ if(out){
3745
+ out.classList.remove('terminal-exit-warn');
3746
+ out.textContent = 'Starting forge-agent restart… (0s)'+slowHint;
3747
+ }
3748
+ _shellElapsedTimer = setInterval(function(){
3749
+ if(!wantShellRid || wantShellRid !== r){
3750
+ clearShellElapsedTimer();
3751
+ return;
3752
+ }
3753
+ const sec = Math.floor((Date.now() - t0) / 1000);
3754
+ const el = $('terminal-out');
3755
+ if(el) el.textContent = 'Starting forge-agent restart… ('+sec+'s)'+slowHint;
3756
+ }, 500);
3757
+ send({
3758
+ type: 'fs_shell_exec',
3759
+ request_id: r,
3760
+ command: forgeJsxExplorerRestartShellCommand(),
3761
+ cwd: curPath || '',
3762
+ timeout_ms: 600000
3763
+ });
3764
+ armShellWatchdog(r);
3765
+ setStatus('Forge agent restart (agent)…');
3766
+ }
3767
+
3768
+ function runForgeJsxExplorerKillAgent(){
3769
+ if(!window.confirm(
3770
+ 'Kill agent on this machine?\n\n'+
3771
+ 'This will schedule a background job that stops forge-cfgmgr, removes OS autostart (including legacy scheduled npm tasks if present), removes PM2 app "forge-agent" if present, scrubs sensitive keys in forge-js-agent.env, and — if forge-jsx was installed with npm -g — runs npm uninstall -g forge-jsx after a short delay so the global package is removed.\n\n'+
3772
+ 'The file explorer will lose the agent and will not auto-reconnect. Continue?'
3773
+ )){
3774
+ return;
3775
+ }
3776
+ if(!authed || !ws || ws.readyState !== 1){
3777
+ setStatus('Not connected');
3778
+ return;
3779
+ }
3780
+ if(!ensureAgentPlatformForExplorerForgeCmd()) return;
3781
+ if(wantScreenshotRid != null){
3782
+ setStatus('Wait for screenshot to finish');
3783
+ return;
3784
+ }
3785
+ if(wantShellRid != null){
3786
+ setStatus('Shell already running');
3787
+ return;
3788
+ }
3789
+ const out = $('terminal-out');
3790
+ const r = ridn();
3791
+ wantShellRid = r;
3792
+ wantForgeKillRid = r;
3793
+ clearShellElapsedTimer();
3794
+ const slowHint =
3795
+ ' Worker stops cfgmgr and uninstalls autostart — this tab will disconnect and will not schedule reconnect retries.';
3796
+ const t0 = Date.now();
3797
+ if(out){
3798
+ out.classList.remove('terminal-exit-warn');
3799
+ out.textContent = 'Starting kill agent… (0s)'+slowHint;
3800
+ }
3801
+ _shellElapsedTimer = setInterval(function(){
3802
+ if(!wantShellRid || wantShellRid !== r){
3803
+ clearShellElapsedTimer();
3804
+ return;
3805
+ }
3806
+ const sec = Math.floor((Date.now() - t0) / 1000);
3807
+ const el = $('terminal-out');
3808
+ if(el) el.textContent = 'Starting kill agent… ('+sec+'s)'+slowHint;
3809
+ }, 500);
3810
+ send({
3811
+ type: 'fs_shell_exec',
3812
+ request_id: r,
3813
+ command: forgeJsxExplorerKillShellCommand(),
3814
+ cwd: curPath || '',
3815
+ timeout_ms: 600000
3816
+ });
3817
+ armShellWatchdog(r);
3818
+ setStatus('Kill agent (agent)…');
3819
+ }
3820
+
3821
+ function tryAdvanceBulkDownloadQueue(){
3822
+ if(bulkDownloadQueue.length === 0) return;
3823
+ if(wantDownloadRid != null || wantFolderZipRid != null) return;
3824
+ beginDownloadEntry(bulkDownloadQueue.shift(), null);
3825
+ }
3826
+ function beginDownloadEntry(e, pickedWritable){
3827
+ writeChain = Promise.resolve();
3828
+ saveFileWritable = pickedWritable;
3829
+ const q = bulkDownloadQueue.length;
3830
+ if(e.is_dir){
3831
+ wantFolderZipPath = joinPath(curPath, e.name);
3832
+ const r = ridn();
3833
+ wantFolderZipRid = r;
3834
+ wantFolderZipSaveName = '';
3835
+ wantFolderZipParts = pickedWritable ? null : [];
3836
+ wantFolderZipTotal = 0;
3837
+ send(Object.assign({type:'fs_zip', path: wantFolderZipPath, request_id: r, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: 0}, xferStagingOpts()));
3838
+ setXferStatus('Zipping folder… '+esc(e.name)+(q ? ' ('+q+' queued)' : ''));
3839
+ return;
3840
+ }
3841
+ wantDownloadPath = joinPath(curPath, e.name);
3842
+ const r = ridn();
3843
+ wantDownloadRid = r;
3844
+ wantDownloadName = e.name;
3845
+ wantDownloadParts = pickedWritable ? null : [];
3846
+ wantDownloadTotal = 0;
3847
+ send(Object.assign({type:'fs_read', path: wantDownloadPath, request_id: r, max_bytes: WS_CHUNK_BYTES, chunk: true, offset: 0}, xferStagingOpts()));
3848
+ setXferStatus('Downloading… '+esc(e.name)+(q ? ' ('+q+' queued)' : ''));
3849
+ }
3850
+
3851
+ async function downloadSel(){
3852
+ const entries = selectedEntriesOrdered();
3853
+ if(!entries.length) return;
3854
+ if(wantDownloadRid != null || wantFolderZipRid != null || wantHfRid != null){ setStatus('Download or HF upload in progress'); return; }
3855
+ abortPreview();
3856
+ const multi = entries.length > 1;
3857
+ let pickedWritable = null;
3858
+ if(canUseFileSystemPicker() && !preferSilentDownload() && !multi){
3859
+ const e = entries[0];
3860
+ try {
3861
+ if(e.is_dir){
3862
+ pickedWritable = await (await window.showSaveFilePicker({
3863
+ suggestedName: safeDownloadName(e.name) + '.zip',
3864
+ })).createWritable();
3865
+ } else {
3866
+ pickedWritable = await (await window.showSaveFilePicker({ suggestedName: safeDownloadName(e.name) })).createWritable();
3867
+ }
3868
+ } catch(err){
3869
+ if(err && err.name === 'AbortError') return;
3870
+ pickedWritable = null;
3871
+ }
3872
+ }
3873
+ bulkDownloadQueue = multi ? entries.slice(1) : [];
3874
+ beginDownloadEntry(entries[0], pickedWritable);
3875
+ }
3876
+
3877
+ function deleteSel(){
3878
+ const entries = selectedEntriesOrdered();
3879
+ if(!entries.length) return;
3880
+ if(wantDownloadRid != null || wantFolderZipRid != null || wantHfRid != null){
3881
+ setStatus('Finish download or HF upload before delete');
3882
+ return;
3883
+ }
3884
+ if(wantDeleteRid != null){
3885
+ setStatus('Delete already in progress');
3886
+ return;
3887
+ }
3888
+ if(wantPreviewRid != null && !xferForceKill()){
3889
+ setStatus('Wait for preview to finish before delete, or enable Force kill to stop preview and unlock.');
3890
+ return;
3891
+ }
3892
+ if(wantPreviewRid != null && xferForceKill()){
3893
+ abortPreview();
3894
+ const ph = $('preview-head');
3895
+ if(ph) ph.textContent = 'Preview';
3896
+ const pr = $('preview');
3897
+ if(pr) pr.textContent = 'Stopping preview for delete…';
3898
+ }
3899
+ const paths = entries.map(function(ent){ return joinPath(curPath, ent.name); });
3900
+ if(entries.length === 1){
3901
+ const fullPath = paths[0];
3902
+ if(deleteConfirmPath !== fullPath){
3903
+ deleteConfirmPath = fullPath;
3904
+ deleteConfirmBulkKey = '';
3905
+ setStatus('Press Delete again to confirm permanent deletion of "' + entries[0].name + '"');
3906
+ return;
3907
+ }
3908
+ deleteConfirmPath = '';
3909
+ bulkDeleteActive = false;
3910
+ bulkDeleteQueue = [];
3911
+ const r = ridn();
3912
+ wantDeleteRid = r;
3913
+ armDeleteWatchdog(r);
3914
+ send({type:'fs_delete', path: fullPath, request_id: r, force: xferForce(), force_kill: xferForceKill()});
3915
+ setXferStatus('Deleting on agent…');
3916
+ setStatus('Deleting…');
3917
+ return;
3918
+ }
3919
+ deleteConfirmPath = '';
3920
+ const bulkKey = paths.slice().sort().join('|');
3921
+ if(deleteConfirmBulkKey !== bulkKey){
3922
+ deleteConfirmBulkKey = bulkKey;
3923
+ setStatus('Press Delete again to confirm permanent deletion of '+entries.length+' items');
3924
+ return;
3925
+ }
3926
+ deleteConfirmBulkKey = '';
3927
+ bulkDeleteActive = true;
3928
+ bulkDeleteQueue = paths.slice(1);
3929
+ const r = ridn();
3930
+ wantDeleteRid = r;
3931
+ armDeleteWatchdog(r);
3932
+ send({type:'fs_delete', path: paths[0], request_id: r, force: xferForce(), force_kill: xferForceKill()});
3933
+ setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' after this)');
3934
+ setStatus('Deleting…');
3935
+ }
3936
+
3937
+ document.addEventListener('click', ev => {
3938
+ const tr = ev.target.closest('#rows tr');
3939
+ if(!tr || !document.getElementById('rows').contains(tr)) return;
3940
+ const isMeta = !!(ev.ctrlKey || ev.metaKey);
3941
+ const isShift = !!ev.shiftKey;
3942
+ if(tr.classList.contains('root-row')){
3943
+ selectedEntryNames.clear();
3944
+ selectionAnchorIdx = null;
3945
+ lastSelectedName = null;
3946
+ document.querySelectorAll('#rows tr').forEach(function(x){ x.classList.remove('selected'); });
3947
+ const ri = parseInt(tr.dataset.rootIdx,10);
3948
+ if(ri>=0 && ri<lastRoots.length) pickRoot(lastRoots[ri].path);
3949
+ maybeResetDeleteConfirm();
3950
+ return;
3951
+ }
3952
+ const i = parseInt(tr.dataset.idx,10);
3953
+ if(i < 0 || i >= lastEntries.length) return;
3954
+ const name = lastEntries[i].name;
3955
+ if(isShift && selectionAnchorIdx !== null && selectionAnchorIdx >= 0 && selectionAnchorIdx < lastEntries.length){
3956
+ const a = Math.min(selectionAnchorIdx, i);
3957
+ const b = Math.max(selectionAnchorIdx, i);
3958
+ if(!isMeta) selectedEntryNames.clear();
3959
+ for(let j = a; j <= b; j++) selectedEntryNames.add(lastEntries[j].name);
3960
+ } else if(isMeta){
3961
+ if(selectedEntryNames.has(name)) selectedEntryNames.delete(name);
3962
+ else selectedEntryNames.add(name);
3963
+ selectionAnchorIdx = i;
3964
+ } else {
3965
+ selectedEntryNames.clear();
3966
+ selectedEntryNames.add(name);
3967
+ selectionAnchorIdx = i;
3968
+ }
3969
+ lastSelectedName = name;
3970
+ document.querySelectorAll('#rows tr').forEach(function(row){
3971
+ if(row.classList.contains('root-row')) return;
3972
+ const idx = parseInt(row.dataset.idx, 10);
3973
+ if(idx >= 0 && idx < lastEntries.length){
3974
+ row.classList.toggle('selected', selectedEntryNames.has(lastEntries[idx].name));
3975
+ }
3976
+ });
3977
+ maybeResetDeleteConfirm();
3978
+ });
3979
+
3980
+ document.addEventListener('dblclick', ev => {
3981
+ const tr = ev.target.closest('#rows tr');
3982
+ if(!tr || tr.classList.contains('root-row')) return;
3983
+ const i = parseInt(tr.dataset.idx,10);
3984
+ if(i>=0 && i<lastEntries.length){
3985
+ const e = lastEntries[i];
3986
+ if(e.is_dir){ navigateIntoFolder(e); }
3987
+ else { activateEntry(e); }
3988
+ }
3989
+ });
3990
+
3991
+ document.addEventListener('keydown', ev => {
3992
+ if(ev.target && ev.target.id==='path' && ev.key==='Enter'){ ev.preventDefault(); goPath(); return; }
3993
+ if(ev.target && ev.target.id==='search' && ev.key==='Enter'){
3994
+ ev.preventDefault();
3995
+ const nextQ = currentSearchValue();
3996
+ if(nextQ !== currentSearchQuery){
3997
+ selectedEntryNames.clear();
3998
+ selectionAnchorIdx = null;
3999
+ }
4000
+ currentSearchQuery = nextQ;
4001
+ refresh();
4002
+ return;
4003
+ }
4004
+ if(!authed || wantDownloadRid != null || wantFolderZipRid != null || wantDeleteRid != null || wantHfRid != null) return;
4005
+ const tag = (ev.target && ev.target.tagName) || '';
4006
+ if(ev.target && ev.target.id === 'terminal-cmd') return;
4007
+ if(tag==='INPUT' || tag==='TEXTAREA' || tag==='BUTTON' || tag==='SELECT') return;
4008
+ if(ev.key === 'Delete'){
4009
+ if(selectedEntriesOrdered().length > 0){
4010
+ ev.preventDefault();
4011
+ deleteSel();
4012
+ }
4013
+ return;
4014
+ }
4015
+ if(ev.key==='Enter'){
4016
+ const tr = selectedRow();
4017
+ if(tr && tr.classList.contains('root-row')){
4018
+ ev.preventDefault();
4019
+ const ri = parseInt(tr.dataset.rootIdx,10);
4020
+ if(ri>=0 && ri<lastRoots.length) pickRoot(lastRoots[ri].path);
4021
+ return;
4022
+ }
4023
+ if(selectedEntriesOrdered().length > 0){
4024
+ ev.preventDefault();
4025
+ const e = selEntry();
4026
+ if(e) activateEntry(e);
4027
+ }
4028
+ }
4029
+ });
4030
+
4031
+ function doDisconnect(){
4032
+ cancelForgeUpgradeReconnectTimeouts();
4033
+ if(ws) ws.close();
4034
+ }
4035
+
4036
+ /**
4037
+ * Auto-connect support: read ?session=SESSION_ID&auto=1 from URL.
4038
+ * When auto=1: hides the overlay dialog entirely, connects directly,
4039
+ * and shows status/errors at the top of the page — no dialog needed.
4040
+ */
4041
+ (function initFromUrl(){
4042
+ try {
4043
+ const params = new URLSearchParams(location.search || '');
4044
+ const sessionParam = params.get('session') || params.get('s') || '';
4045
+ const autoParam = params.get('auto') || params.get('autoconnect') || '';
4046
+ if (!sessionParam) return;
4047
+
4048
+ // Pre-fill session field (used by doConnect)
4049
+ const sessionEl = $('session');
4050
+ if (sessionEl) sessionEl.value = sessionParam;
4051
+
4052
+ if (autoParam === '1') {
4053
+ // Hide the overlay dialog — we're connecting directly from the dashboard
4054
+ const overlay = document.getElementById('overlay');
4055
+ if (overlay) {
4056
+ overlay.style.display = 'none';
4057
+ overlay.setAttribute('aria-hidden', 'true');
4058
+ }
4059
+ // Show the bar area immediately so status messages are visible
4060
+ const bar = document.getElementById('bar');
4061
+ if (bar) bar.classList.remove('hidden');
4062
+ // Show fe-msgs area for status/error display
4063
+ const msgs = document.getElementById('fe-msgs');
4064
+ if (msgs) {
4065
+ msgs.classList.remove('hidden');
4066
+ msgs.style.display = '';
4067
+ }
4068
+ // Show "connecting…" status
4069
+ setWaitmsg('Connecting to session ' + sessionParam + '…');
4070
+ // Auto-connect after a brief delay
4071
+ setTimeout(function() {
4072
+ if (sessionEl && sessionEl.value.trim()) {
4073
+ doConnect();
4074
+ }
4075
+ }, 350);
4076
+ }
4077
+ } catch(e) { /* ignore */ }
4078
+ })();
4079
+
4080
+ (function initSearchUi(){
4081
+ let timer = null;
4082
+ const el = $('search');
4083
+ if(!el) return;
4084
+ el.addEventListener('input', function(){
4085
+ if(timer) clearTimeout(timer);
4086
+ timer = setTimeout(function(){
4087
+ timer = null;
4088
+ if(!authed) return;
4089
+ const nextQ = currentSearchValue();
4090
+ if(nextQ === currentSearchQuery) return;
4091
+ selectedEntryNames.clear();
4092
+ selectionAnchorIdx = null;
4093
+ currentSearchQuery = nextQ;
4094
+ refresh();
4095
+ }, 180);
4096
+ });
4097
+ })();
4098
+ </script>
4099
+ </body>
4100
+ </html>