dsh-ssh-tunnel 1.0.0 → 1.0.2

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.
package/lib/client.js CHANGED
@@ -1,1472 +1,1643 @@
1
- window.__ModuleLoader__.load({
2
- id: "dsh-ssh-tunnel",
3
- factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
6
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- const React = require("react");
8
-
9
- // Lazy react-dom resolver (same module loader registry as react).
10
- // Only needed for the body portal in CenterOverlay; optional on purpose.
11
- let ReactDOMRef;
12
- function getReactDOM() {
13
- if (ReactDOMRef !== undefined) return ReactDOMRef;
14
- try { ReactDOMRef = require("react-dom"); } catch (e) { ReactDOMRef = null; }
15
- return ReactDOMRef;
16
- }
17
-
18
- // --- i18n (same pattern as dsh-better-sidebar) ---
19
- const LOCALE_NS = "sshTunnel";
20
- const zh = {
21
- tabTitle: "SSH 隧道",
22
- projectLabel: "项目: {path}",
23
- projectUnbound: "(未绑定)",
24
- refresh: "刷新",
25
- tabGrants: "项目授权",
26
- tabHosts: "主机库",
27
- tabTunnel: "隧道会话",
28
- needConfirm: "需要确认",
29
- trustConnect: "信任并连接",
30
- hostKeyFingerprint: "指纹",
31
- grantsHint: "勾选后,当前项目下的对话可通过 SSHManager 与隧道使用这些主机。连接前须先完成项目授权(不会在连接时自动授权)。",
32
- saveGrants: "保存项目授权",
33
- saving: "保存中…",
34
- noHostsYet: "还没有主机,请先到「主机库」添加。",
35
- newHost: "新建主机",
36
- scanOpenSsh: "扫描 OpenSSH",
37
- scanning: "扫描中…",
38
- edit: "编辑",
39
- delete: "删除",
40
- deleteHostConfirm: "确定删除主机「{name}」?此操作不可撤销。",
41
- import: "导入",
42
- scanResults: "扫描结果",
43
- name: "名称 / 别名",
44
- host: "Host",
45
- port: "Port",
46
- username: "Username",
47
- authType: "认证方式",
48
- authPrivateKey: "私钥",
49
- authPassword: "密码",
50
- privateKeyPath: "私钥路径",
51
- privateKeyPem: "或粘贴 PEM",
52
- passphrase: "私钥口令",
53
- password: "密码",
54
- save: "保存",
55
- back: "返回",
56
- editHost: "编辑主机",
57
- createHost: "新建主机",
58
- sftpOnConnect: "连接时启用 SFTP",
59
- connectSection: "连接",
60
- connect: "连接",
61
- connecting: "连接中…",
62
- needHostKey: "需要在提示中信任 host key",
63
- sessionsSection: "会话(终端 / SFTP 在中央打开)",
64
- noSessions: "暂无会话",
65
- terminal: "终端",
66
- sftp: "SFTP",
67
- disconnect: "断开",
68
- reconnect: "重连",
69
- reconnecting: "重连中 ({attempt}/{max})",
70
- disconnected: "已断开",
71
- connected: "已连接",
72
- sessionDropped: "会话已断开",
73
- kbiUnsupported: "不支持键盘交互登录,请编辑主机改为密码或私钥",
74
- closeSession: "关闭",
75
- footerHint: "连接与授权在此侧栏;终端与 SFTP 在中央面板打开。",
76
- bashTitle: "SSH 终端 · {name}",
77
- close: "关闭",
78
- bash: "Bash",
79
- bashStatusReady: "connected · 直接在终端内输入,Ctrl+C 等快捷键可用",
80
- bashStatusLoading: "loading",
81
- bashStatusError: "error",
82
- xtermLoadFailed: "无法加载 xterm",
83
- transferring: "传输中…",
84
- skipDir: "跳过目录 {name}",
85
- skipDirHint: "跳过目录 {name}(请先进入后多选文件)",
86
- transferDone: "",
87
- mkdirTitle: "新建文件夹",
88
- mkdirLocation: "位置:{path}",
89
- mkdirPlaceholder: "文件夹名称",
90
- create: "创建",
91
- renameTitle: "重命名",
92
- confirmDeleteTitle: "确认删除",
93
- confirmDeleteDesc: "将删除 {count} 项,此操作可能不可恢复。",
94
- copiedPath: "已复制路径",
95
- pathTitle: "路径",
96
- localProject: "本地项目",
97
- remoteDevice: "远端设备",
98
- parentDir: "上级",
99
- upload: "上传 →",
100
- download: "← 下载",
101
- sftpHint: "多选: Ctrl/⌘ 点击 · Shift 范围 · 拖拽跨栏传输 · 右键菜单",
102
- open: "打开",
103
- uploadToRemote: "上传到远端",
104
- downloadToLocal: "下载到本地",
105
- newFolder: "新建文件夹",
106
- rename: "重命名",
107
- copyPath: "复制路径",
108
- ok: "确定",
109
- cancel: "取消",
110
- gotIt: "好的",
111
- done: "完成",
112
- working: "工作中…",
113
- authLine: "{user}@{host}:{port} · {auth} · {cred}",
114
- };
115
- const en = {
116
- tabTitle: "SSH Tunnel",
117
- projectLabel: "Project: {path}",
118
- projectUnbound: "(not bound)",
119
- refresh: "Refresh",
120
- tabGrants: "Project access",
121
- tabHosts: "Hosts",
122
- tabTunnel: "Sessions",
123
- needConfirm: "Confirmation needed",
124
- trustConnect: "Trust and connect",
125
- hostKeyFingerprint: "Fingerprint",
126
- grantsHint: "Checked hosts can be used by SSHManager and the tunnel for this project. Grant access before connecting (connect does not auto-authorize).",
127
- saveGrants: "Save project access",
128
- saving: "Saving…",
129
- noHostsYet: "No hosts yet — add one under Hosts.",
130
- newHost: "New host",
131
- scanOpenSsh: "Scan OpenSSH",
132
- scanning: "Scanning…",
133
- edit: "Edit",
134
- delete: "Delete",
135
- deleteHostConfirm: "Delete host \"{name}\"? This cannot be undone.",
136
- import: "Import",
137
- scanResults: "Scan results",
138
- name: "Name / alias",
139
- host: "Host",
140
- port: "Port",
141
- username: "Username",
142
- authType: "Authentication",
143
- authPrivateKey: "Private key",
144
- authPassword: "Password",
145
- privateKeyPath: "Private key path",
146
- privateKeyPem: "Or paste PEM",
147
- passphrase: "Key passphrase",
148
- password: "Password",
149
- save: "Save",
150
- back: "Back",
151
- editHost: "Edit host",
152
- createHost: "New host",
153
- sftpOnConnect: "Enable SFTP on connect",
154
- connectSection: "Connect",
155
- connect: "Connect",
156
- connecting: "Connecting…",
157
- needHostKey: "Trust the host key in the prompt first",
158
- sessionsSection: "Sessions (terminal / SFTP open in the center)",
159
- noSessions: "No sessions",
160
- terminal: "Terminal",
161
- sftp: "SFTP",
162
- disconnect: "Disconnect",
163
- reconnect: "Reconnect",
164
- reconnecting: "Reconnecting ({attempt}/{max})",
165
- disconnected: "Disconnected",
166
- connected: "Connected",
167
- sessionDropped: "Session disconnected",
168
- kbiUnsupported: "Keyboard-interactive auth is not supported; edit the host to password or private key",
169
- closeSession: "Close",
170
- footerHint: "Use this sidebar for connect and access; terminal and SFTP open in the center panel.",
171
- bashTitle: "SSH terminal · {name}",
172
- close: "Close",
173
- bash: "Bash",
174
- bashStatusReady: "connected · type directly in the terminal (Ctrl+C etc.)",
175
- bashStatusLoading: "loading",
176
- bashStatusError: "error",
177
- xtermLoadFailed: "Failed to load xterm",
178
- transferring: "Transferring…",
179
- skipDir: "Skipped directory {name}",
180
- skipDirHint: "Skipped directory {name} (enter it and multi-select files)",
181
- transferDone: "",
182
- mkdirTitle: "New folder",
183
- mkdirLocation: "Location: {path}",
184
- mkdirPlaceholder: "Folder name",
185
- create: "Create",
186
- renameTitle: "Rename",
187
- confirmDeleteTitle: "Confirm delete",
188
- confirmDeleteDesc: "Delete {count} item(s)? This may not be recoverable.",
189
- copiedPath: "Path copied",
190
- pathTitle: "Path",
191
- localProject: "Local project",
192
- remoteDevice: "Remote",
193
- parentDir: "Up",
194
- upload: "Upload →",
195
- download: "← Download",
196
- sftpHint: "Multi-select: Ctrl/⌘ click · Shift range · drag across panes · right-click menu",
197
- open: "Open",
198
- uploadToRemote: "Upload to remote",
199
- downloadToLocal: "Download to local",
200
- newFolder: "New folder",
201
- rename: "Rename",
202
- copyPath: "Copy path",
203
- ok: "OK",
204
- cancel: "Cancel",
205
- gotIt: "OK",
206
- done: "Done",
207
- working: "Working…",
208
- authLine: "{user}@{host}:{port} · {auth} · {cred}",
209
- };
210
- let localeService;
211
- function attachLocale(service) {
212
- localeService = service;
213
- }
214
- function activeLocale() {
215
- const fromSvc = localeService && localeService.getSnapshot ? localeService.getSnapshot().active : undefined;
216
- const raw = fromSvc || (typeof navigator !== "undefined" ? navigator.language : "") || "en";
217
- return String(raw);
218
- }
219
- function isZh() {
220
- return activeLocale().toLowerCase().startsWith("zh");
221
- }
222
- /** Translate a copy key; `{name}` placeholders from params (better-sidebar style). */
223
- function t(key, params) {
224
- const dict = isZh() ? zh : en;
225
- let text = dict[key];
226
- if (text === undefined) text = en[key] || zh[key] || key;
227
- if (params !== undefined && params !== null) {
228
- for (const [name, value] of Object.entries(params)) {
229
- text = String(text).split("{" + name + "}").join(String(value));
230
- }
231
- }
232
- return text;
233
- }
234
-
235
- const TAB_ID = "dsh-ssh-tunnel";
236
- const API = "/dsh-ssh-tunnel/api";
237
-
238
- async function api(method, body) {
239
- const response = await fetch(API + "/" + method, {
240
- method: "POST",
241
- headers: { "content-type": "application/json" },
242
- body: JSON.stringify(body || {}),
243
- });
244
- const text = await response.text();
245
- let data;
246
- try { data = text ? JSON.parse(text) : {}; }
247
- catch (e) { throw new Error("bad JSON (" + response.status + "): " + text.slice(0, 200)); }
248
- if (!response.ok && data && data.error) throw new Error(String(data.error));
249
- if (!response.ok) throw new Error("HTTP " + response.status);
250
- return data;
251
- }
252
-
253
- let xtermPromise = null;
254
- function loadScript(src) {
255
- return new Promise(function (resolve, reject) {
256
- const s = document.createElement("script");
257
- s.src = src;
258
- s.async = true;
259
- s.onload = function () { resolve(); };
260
- s.onerror = function () { reject(new Error("load fail " + src)); };
261
- document.head.appendChild(s);
262
- });
263
- }
264
- function loadCssOnce(href) {
265
- if (document.querySelector('link[data-ssh-t="' + href + '"]')) return;
266
- const l = document.createElement("link");
267
- l.rel = "stylesheet";
268
- l.href = href;
269
- l.setAttribute("data-ssh-t", href);
270
- document.head.appendChild(l);
271
- }
272
- function ensureXterm() {
273
- if (xtermPromise) return xtermPromise;
274
- xtermPromise = (async function () {
275
- let Terminal = null;
276
- let FitAddon = null;
277
- // 1) Same-origin vendor assets served by this plugin's host route
278
- // (/dsh-ssh-tunnel/vendor/*). No CDN, no module-table dependency.
279
- try {
280
- if (typeof document !== "undefined") {
281
- loadCssOnce("/dsh-ssh-tunnel/vendor/xterm.css");
282
- await loadScript("/dsh-ssh-tunnel/vendor/xterm.js");
283
- await loadScript("/dsh-ssh-tunnel/vendor/addon-fit.js");
284
- Terminal = globalThis.Terminal || null;
285
- FitAddon = (globalThis.FitAddon && globalThis.FitAddon.FitAddon) || globalThis.FitAddon || null;
286
- }
287
- } catch (e) {}
288
- // 2) Fallback: a consumer that bundles @xterm into the module table.
289
- if (!Terminal) {
290
- try {
291
- const xt = require("@xterm/xterm");
292
- Terminal = xt.Terminal || xt.default || xt;
293
- } catch (e) {}
294
- try {
295
- const fit = require("@xterm/addon-fit");
296
- FitAddon = fit.FitAddon || fit.default || fit;
297
- } catch (e) {}
298
- }
299
- // Never fetch third-party CDNs (XSS / CSP surface).
300
- return { Terminal: Terminal, FitAddon: FitAddon };
301
- })();
302
- return xtermPromise;
303
- }
304
-
305
- function icon(size) {
306
- const s = size || 16;
307
- return React.createElement("svg", {
308
- width: s, height: s, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor",
309
- strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true,
310
- },
311
- React.createElement("path", { d: "M12 20h9" }),
312
- React.createElement("path", { d: "M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" }),
313
- React.createElement("path", { d: "M4 12v-2a8 8 0 0 1 14-5" }),
314
- );
315
- }
316
-
317
- function ensureStyles() {
318
- if (typeof document === "undefined") return;
319
- // bump id when theme CSS changes so hard-refresh replaces rules
320
- const STYLE_ID = "dsh-ssh-tunnel-style-v4";
321
- const prev = document.getElementById("dsh-ssh-tunnel-style") || document.getElementById("dsh-ssh-tunnel-style-v2") || document.getElementById("dsh-ssh-tunnel-style-v3") || document.getElementById("dsh-ssh-tunnel-style-v4");
322
- if (prev && prev.id === STYLE_ID && prev.getAttribute("data-rev") === "4") return;
323
- if (prev) prev.remove();
324
- const el = document.createElement("style");
325
- el.id = STYLE_ID;
326
- el.setAttribute("data-rev", "4");
327
- el.textContent = [
328
- /* Theme-only surfaces: no hard-coded black/gray panels */
329
- ".ssh-t-root{display:flex;flex-direction:column;gap:14px;padding:14px 14px 18px;height:100%;overflow:auto;font-size:13px;line-height:1.45;color:var(--dsw-alias-label-primary);box-sizing:border-box;background:transparent;}",
330
- ".ssh-t-head{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:10px;}",
331
- ".ssh-t-title{font-weight:600;font-size:15px;letter-spacing:-0.01em;color:var(--dsw-alias-label-primary);}",
332
- ".ssh-t-sub{color:var(--dsw-alias-label-secondary);font-size:12px;word-break:break-all;margin-top:3px;opacity:1;}",
333
- ".ssh-t-tabs{display:flex;gap:4px;flex-wrap:wrap;padding:3px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;width:fit-content;}",
334
- ".ssh-t-tab{padding:6px 12px;border-radius:999px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;font-weight:500;transition:background var(--ds-transition-duration-slow, .15s) ease,color .15s ease;}",
335
- ".ssh-t-tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);}",
336
- ".ssh-t-tab.on{background:var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));color:var(--dsw-alias-label-primary-inverted, #fff);}",
337
- ".ssh-t-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:14px;background:var(--dsw-alias-bg-layer-1);}",
338
- /* Rows: keep action buttons visible when endpoint (IPv6) is long */
339
- ".ssh-t-row{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:10px 12px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);}",
340
- ".ssh-t-row:last-child{border-bottom:none;padding-bottom:0;}",
341
- ".ssh-t-row:first-child{padding-top:0;}",
342
- ".ssh-t-row-main{flex:1 1 12rem;min-width:0;max-width:100%;}",
343
- ".ssh-t-row-main strong,.ssh-t-row-main .ssh-t-name{display:block;font-weight:600;overflow-wrap:anywhere;word-break:break-word;}",
344
- ".ssh-t-endpoint{display:block;color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:2px;overflow-wrap:anywhere;word-break:break-word;line-height:1.4;}",
345
- ".ssh-t-row > .ssh-t-actions{flex:0 0 auto;margin-top:0;margin-left:auto;}",
346
- ".ssh-t-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;align-items:center;}",
347
- ".ssh-t-check span{min-width:0;flex:1;overflow-wrap:anywhere;word-break:break-word;}",
348
- ".ssh-t-btn{padding:7px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2, transparent);color:var(--dsw-alias-label-primary);cursor:pointer;font-size:12px;font-weight:500;transition:background .15s,border-color .15s;}",
349
- ".ssh-t-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2);}",
350
- ".ssh-t-btn:disabled{opacity:.45;cursor:not-allowed;}",
351
- ".ssh-t-btn.primary{background:var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));border-color:transparent;color:var(--dsw-alias-label-primary-inverted, #fff);}",
352
- ".ssh-t-btn.primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover, var(--dsw-alias-button-primary-fill));}",
353
- ".ssh-t-btn.danger{border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);}",
354
- ".ssh-t-btn.danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 16%, transparent);}",
355
- ".ssh-t-field{display:block;margin-bottom:12px;}",
356
- ".ssh-t-label{display:block;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);margin-bottom:6px;letter-spacing:0;text-transform:none;}",
357
- ".ssh-t-input,.ssh-t-select,.ssh-t-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:13px;outline:none;transition:border-color .15s, box-shadow .15s;}",
358
- ".ssh-t-input:focus,.ssh-t-select:focus,.ssh-t-textarea:focus{border-color:var(--dsw-alias-border-l4, var(--dsw-alias-brand-primary));box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent);}",
359
- ".ssh-t-ok{color:var(--dsw-alias-state-success-primary, #3d9a5f);font-size:12px;}",
360
- ".ssh-t-err{color:var(--dsw-alias-state-error-primary);font-size:12px;white-space:pre-wrap;padding:8px 10px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);border:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent);}",
361
- ".ssh-t-muted{color:var(--dsw-alias-label-secondary);font-size:12px;}",
362
- ".ssh-t-check{display:flex;align-items:center;gap:10px;margin:8px 0;cursor:pointer;padding:8px 10px;border-radius:8px;border:1px solid transparent;color:var(--dsw-alias-label-primary);}",
363
- ".ssh-t-check:hover{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l1);}",
364
- ".ssh-t-check input{accent-color:var(--dsw-alias-brand-primary);}",
365
- ".ssh-t-prompt{border:1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #d97706) 40%, transparent);background:var(--dsw-alias-state-warn-tertiary, color-mix(in srgb, #d97706 12%, transparent));border-radius:12px;padding:12px 14px;color:var(--dsw-alias-label-primary);}",
366
- /* Overlay follows theme; only terminal canvas stays dark for readability */
367
- ".ssh-ov-root{position:fixed;inset:0;z-index:12000;display:flex;align-items:center;justify-content:center;padding:24px;background:color-mix(in srgb, var(--dsw-alias-bg-base) 35%, rgba(0,0,0,.45));backdrop-filter:blur(6px);}",
368
- ".ssh-ov-panel{display:flex;flex-direction:column;position:relative;width:min(1180px,100%);height:min(840px,100%);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;box-shadow:0 18px 50px color-mix(in srgb, #000 35%, transparent);overflow:hidden;}",
369
- ".ssh-ov-bar{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);flex:none;}",
370
- ".ssh-ov-bar .ssh-t-title{flex:1;min-width:0;font-size:14px;color:var(--dsw-alias-label-primary);}",
371
- ".ssh-ov-tabs{display:flex;gap:4px;flex:none;padding:3px;background:var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base));border-radius:999px;border:1px solid var(--dsw-alias-border-l1);}",
372
- ".ssh-ov-tabs button{padding:5px 12px;border-radius:999px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;font-weight:500;}",
373
- ".ssh-ov-tabs button.on{background:var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));color:var(--dsw-alias-label-primary-inverted, #fff);}",
374
- ".ssh-ov-body{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);}",
375
- ".ssh-ov-xterm{flex:1;min-height:0;padding:10px;background:#0b0f14;}",
376
- ".ssh-ov-xterm .xterm,.ssh-ov-xterm .xterm-viewport{height:100%;}",
377
- ".ssh-sftp{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);}",
378
- ".ssh-sftp-panes{flex:1;min-height:0;display:grid;grid-template-columns:1fr 1fr;gap:0;}",
379
- ".ssh-sftp-pane{display:flex;flex-direction:column;min-width:0;min-height:0;border-right:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);}",
380
- ".ssh-sftp-pane:last-child{border-right:none;}",
381
- ".ssh-sftp-pane-hd{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:10px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;background:var(--dsw-alias-bg-layer-1);}",
382
- ".ssh-sftp-pane-hd strong{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);min-width:64px;}",
383
- ".ssh-sftp-pane-hd input{flex:1;min-width:80px;}",
384
- ".ssh-sftp-list{flex:1;min-height:0;overflow:auto;user-select:none;padding:6px;background:var(--dsw-alias-bg-base);}",
385
- ".ssh-sftp-item{display:flex;align-items:center;gap:10px;padding:8px 10px;font-size:12px;cursor:default;border-radius:8px;margin-bottom:2px;color:var(--dsw-alias-label-primary);}",
386
- ".ssh-sftp-item:hover{background:var(--dsw-alias-interactive-bg-hover);}",
387
- ".ssh-sftp-item.sel{background:var(--dsw-alias-interactive-bg-active, color-mix(in srgb, var(--dsw-alias-brand-primary) 16%, transparent));outline:1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 40%, transparent);}",
388
- ".ssh-sftp-item .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
389
- ".ssh-sftp-item .sz{color:var(--dsw-alias-label-tertiary);flex:none;font-variant-numeric:tabular-nums;font-size:11px;}",
390
- ".ssh-ctx{position:fixed;z-index:13000;min-width:190px;background:var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1));border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:6px;box-shadow:0 12px 32px color-mix(in srgb, #000 25%, transparent);}",
391
- ".ssh-ctx button{display:block;width:100%;text-align:left;padding:8px 10px;border:none;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;font-size:12px;border-radius:6px;}",
392
- ".ssh-ctx button:hover{background:var(--dsw-alias-interactive-bg-hover);}",
393
- ".ssh-ctx button.danger{color:var(--dsw-alias-state-error-primary);}",
394
- ".ssh-ctx hr{border:none;border-top:1px solid var(--dsw-alias-border-l1);margin:4px 0;}",
395
- ".ssh-modal-root{position:absolute;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;padding:20px;background:color-mix(in srgb, var(--dsw-alias-bg-base) 30%, rgba(0,0,0,.35));}",".ssh-modal{width:min(400px,100%);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:14px;box-shadow:0 16px 40px color-mix(in srgb,#000 28%,transparent);padding:16px 16px 14px;}",".ssh-modal-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0 0 6px;}",".ssh-modal-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin:0 0 12px;line-height:1.45;word-break:break-all;}",".ssh-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;}",".ssh-status{padding:7px 14px;font-size:11px;color:var(--dsw-alias-label-secondary);border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);flex:none;}",
396
- ].join("\n");
397
- document.head.appendChild(el);
398
- }
399
-
400
- function Btn(props) {
401
- const cls = "ssh-t-btn" + (props.primary ? " primary" : "") + (props.danger ? " danger" : "");
402
- return React.createElement("button", { type: "button", className: cls, disabled: props.disabled, onClick: props.onClick }, props.children);
403
- }
404
- function Field(props) {
405
- return React.createElement("label", { className: "ssh-t-field" },
406
- React.createElement("span", { className: "ssh-t-label" }, props.label), props.children);
407
- }
408
- function emptyForm() {
409
- return { id: "", name: "", host: "", port: 22, username: "", authType: "privateKey", privateKeyPath: "", password: "", privateKeyPem: "", passphrase: "" };
410
- }
411
- function parentOf(p) {
412
- if (!p || p === "/" ) return "/";
413
- const n = String(p).replace(/\/+$/, "");
414
- const i = n.lastIndexOf("/");
415
- if (i <= 0) return "/";
416
- return n.slice(0, i) || "/";
417
- }
418
- function joinPath(base, name) {
419
- if (!base || base === "/") return "/" + name;
420
- return String(base).replace(/\/+$/, "") + "/" + name;
421
- }
422
- function formatSize(n) {
423
- n = Number(n) || 0;
424
- if (n < 1024) return n + " B";
425
- if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " K";
426
- return (n / (1024 * 1024)).toFixed(1) + " M";
427
- }
428
-
429
-
430
- /** In-panel dialog (replaces window.prompt / confirm). */
431
- function TextDialog(props) {
432
- const open = props.open;
433
- const title = props.title || "";
434
- const description = props.description || "";
435
- const mode = props.mode || "prompt"; // prompt | confirm | alert
436
- const confirmLabel = props.confirmLabel || (mode === "confirm" ? t("ok") : mode === "alert" ? t("gotIt") : t("ok"));
437
- const cancelLabel = props.cancelLabel || t("cancel");
438
- const danger = !!props.danger;
439
- const [value, setValue] = React.useState(props.defaultValue || "");
440
- const inputRef = React.useRef(null);
441
- React.useEffect(function () {
442
- if (!open) return;
443
- setValue(props.defaultValue || "");
444
- const t = setTimeout(function () {
445
- if (inputRef.current) {
446
- inputRef.current.focus();
447
- inputRef.current.select && inputRef.current.select();
448
- }
449
- }, 30);
450
- return function () { clearTimeout(t); };
451
- }, [open, props.defaultValue, props.token]);
452
- if (!open) return null;
453
- function submit() {
454
- if (mode === "prompt") props.onConfirm && props.onConfirm(value);
455
- else props.onConfirm && props.onConfirm(true);
456
- }
457
- function cancel() {
458
- props.onCancel && props.onCancel();
459
- }
460
- return React.createElement("div", {
461
- className: "ssh-modal-root",
462
- onMouseDown: function (e) {
463
- if (e.target === e.currentTarget) cancel();
464
- },
465
- },
466
- React.createElement("div", {
467
- className: "ssh-modal",
468
- role: "dialog",
469
- "aria-modal": "true",
470
- onMouseDown: function (e) { e.stopPropagation(); },
471
- },
472
- React.createElement("div", { className: "ssh-modal-title" }, title),
473
- description ? React.createElement("div", { className: "ssh-modal-desc" }, description) : null,
474
- mode === "prompt"
475
- ? React.createElement("input", {
476
- ref: inputRef,
477
- className: "ssh-t-input",
478
- value: value,
479
- placeholder: props.placeholder || "",
480
- onChange: function (e) { setValue(e.target.value); },
481
- onKeyDown: function (e) {
482
- if (e.key === "Enter") submit();
483
- if (e.key === "Escape") cancel();
484
- },
485
- })
486
- : null,
487
- React.createElement("div", { className: "ssh-modal-actions" },
488
- mode !== "alert"
489
- ? React.createElement(Btn, { onClick: cancel }, cancelLabel)
490
- : null,
491
- React.createElement(Btn, {
492
- primary: !danger,
493
- danger: danger,
494
- onClick: submit,
495
- }, confirmLabel),
496
- ),
497
- ),
498
- );
499
- }
500
-
501
-
502
- /** Full-screen center overlay host */
503
- function CenterOverlay(props) {
504
- const rootEl = React.createElement("div", {
505
- className: "ssh-ov-root",
506
- onMouseDown: function (e) {
507
- if (e.target === e.currentTarget && props.onClose) props.onClose();
508
- },
509
- }, React.createElement("div", { className: "ssh-ov-panel", role: "dialog", "aria-modal": "true" }, props.children));
510
- // Portal to <body>: the overlay relies on position:fixed being
511
- // viewport-relative. better-sidebar >=0.16 gives its panel
512
- // containers `contain: layout` (and panels animate with transform),
513
- // which makes them the containing block for fixed descendants — an
514
- // inline-rendered overlay would then be trapped inside the narrow
515
- // sidebar instead of covering the screen. Portaling removes that
516
- // dependency; fall back to inline rendering if react-dom is absent.
517
- const rd = getReactDOM();
518
- if (rd && typeof rd.createPortal === "function" && typeof document !== "undefined" && document.body) {
519
- return rd.createPortal(rootEl, document.body);
520
- }
521
- return rootEl;
522
- }
523
-
524
- /** LA-like SSH terminal: xterm fills center, type directly */
525
- function CenterBash(props) {
526
- const { sessionId, projectPathKey, title, onClose, onOpenSftp } = props;
527
- const hostRef = React.useRef(null);
528
- const termRef = React.useRef(null);
529
- const fitRef = React.useRef(null);
530
- const sinceRef = React.useRef(0);
531
- const [err, setErr] = React.useState("");
532
- const [status, setStatus] = React.useState("loading");
533
-
534
- React.useEffect(function () {
535
- let cancelled = false;
536
- let timer = null;
537
- let ro = null;
538
- (async function () {
539
- try {
540
- const mods = await ensureXterm();
541
- if (cancelled || !hostRef.current) return;
542
- if (!mods.Terminal) throw new Error(t("xtermLoadFailed"));
543
- await api("shellOpen", { sessionId, projectPathKey });
544
- const term = new mods.Terminal({
545
- cursorBlink: true,
546
- fontSize: 14,
547
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
548
- theme: { background: "#0b0f14", foreground: "#d7e0ea", cursor: "#d7e0ea" },
549
- convertEol: true,
550
- scrollback: 5000,
551
- });
552
- let fit = null;
553
- if (mods.FitAddon) {
554
- fit = new mods.FitAddon();
555
- term.loadAddon(fit);
556
- }
557
- term.open(hostRef.current);
558
- if (fit) { try { fit.fit(); } catch (e) {} }
559
- term.focus();
560
- termRef.current = term;
561
- fitRef.current = fit;
562
- sinceRef.current = 0;
563
- // initial dump
564
- const first = await api("shellRead", { sessionId, projectPathKey, since: 0, full: true });
565
- const boot = first.output || first.chunk || "";
566
- if (boot) term.write(boot);
567
- sinceRef.current = first.length != null ? first.length : boot.length;
568
- term.onData(function (data) {
569
- api("shellWrite", { sessionId, projectPathKey, data: data }).catch(function (e) {
570
- setErr(String(e && e.message ? e.message : e));
571
- });
572
- });
573
- const sendSize = function () {
574
- try {
575
- if (fitRef.current) fitRef.current.fit();
576
- } catch (e) {}
577
- if (!termRef.current) return;
578
- api("shellResize", {
579
- sessionId, projectPathKey,
580
- cols: termRef.current.cols,
581
- rows: termRef.current.rows,
582
- }).catch(function () {});
583
- };
584
- sendSize();
585
- if (typeof ResizeObserver !== "undefined" && hostRef.current) {
586
- ro = new ResizeObserver(sendSize);
587
- ro.observe(hostRef.current);
588
- }
589
- window.addEventListener("resize", sendSize);
590
- termRef.current._onWin = sendSize;
591
- setStatus("connected");
592
- timer = setInterval(async function () {
593
- if (cancelled || !termRef.current) return;
594
- try {
595
- const r = await api("shellRead", {
596
- sessionId, projectPathKey, since: sinceRef.current,
597
- });
598
- const chunk = r.chunk != null ? r.chunk : "";
599
- if (chunk) {
600
- termRef.current.write(chunk);
601
- sinceRef.current = r.length != null ? r.length : sinceRef.current + chunk.length;
602
- } else if (r.length != null) {
603
- sinceRef.current = r.length;
604
- }
605
- } catch (e) {
606
- if (cancelled) return;
607
- const msg = String(e && e.message ? e.message : e);
608
- // Terminal condition: the session was closed or its
609
- // host grant was revoked (see v0.4.2/0.4.3). Stop
610
- // polling and surface the drop instead of silently
611
- // hammering a dead session every 120ms.
612
- if (/session not found|session not running|not authorized|not in this project/i.test(msg)) {
613
- clearInterval(timer);
614
- setErr(msg);
615
- setStatus(t("sessionDropped"));
616
- }
617
- // transient errors (network etc.) keep polling
618
- }
619
- }, 120);
620
- } catch (e) {
621
- if (!cancelled) {
622
- const msg = String(e && e.message ? e.message : e);
623
- setErr(msg);
624
- setStatus(/not running|disconnected/i.test(msg) ? t("sessionDropped") : "error");
625
- }
626
- }
627
- })();
628
- return function () {
629
- cancelled = true;
630
- if (timer) clearInterval(timer);
631
- if (ro) try { ro.disconnect(); } catch (e) {}
632
- if (termRef.current) {
633
- if (termRef.current._onWin) window.removeEventListener("resize", termRef.current._onWin);
634
- try { termRef.current.dispose(); } catch (e) {}
635
- termRef.current = null;
636
- }
637
- };
638
- }, [sessionId, projectPathKey]);
639
-
640
- return React.createElement(CenterOverlay, { onClose: onClose },
641
- React.createElement("div", { className: "ssh-ov-bar" },
642
- React.createElement("div", { className: "ssh-t-title" }, t("bashTitle", { name: title || sessionId.slice(0, 8) })),
643
- React.createElement("div", { className: "ssh-ov-tabs" },
644
- React.createElement("button", { type: "button", className: "on" }, t("bash")),
645
- React.createElement("button", { type: "button", onClick: onOpenSftp }, t("sftp")),
646
- ),
647
- React.createElement(Btn, { onClick: onClose }, t("close")),
648
- ),
649
- React.createElement("div", { className: "ssh-ov-body" },
650
- React.createElement("div", { className: "ssh-ov-xterm", ref: hostRef }),
651
- ),
652
- React.createElement("div", { className: "ssh-status" }, (status === "connected" ? t("bashStatusReady") : status) + (err ? " · " + err : "")),
653
- );
654
- }
655
-
656
- function FileList(props) {
657
- const { entries, selected, onSelect, onActivate, onContext } = props;
658
- return React.createElement("div", { className: "ssh-sftp-list", onContextMenu: function (e) {
659
- // empty area
660
- if (e.target === e.currentTarget) {
661
- e.preventDefault();
662
- onContext && onContext(e, null);
663
- }
664
- } },
665
- entries.map(function (ent) {
666
- const sel = selected.indexOf(ent.path) >= 0;
667
- return React.createElement("div", {
668
- key: ent.path,
669
- className: "ssh-sftp-item" + (sel ? " sel" : ""),
670
- onClick: function (e) { onSelect(ent, e); },
671
- onDoubleClick: function () { onActivate(ent); },
672
- onContextMenu: function (e) {
673
- e.preventDefault();
674
- e.stopPropagation();
675
- onContext && onContext(e, ent);
676
- },
677
- draggable: true,
678
- onDragStart: function (e) {
679
- const paths = selected.indexOf(ent.path) >= 0 ? selected : [ent.path];
680
- e.dataTransfer.setData("application/x-dsh-sftp", JSON.stringify({
681
- side: props.side,
682
- paths: paths,
683
- }));
684
- e.dataTransfer.effectAllowed = "copyMove";
685
- },
686
- },
687
- React.createElement("span", null, ent.isDirectory ? "📁" : "📄"),
688
- React.createElement("span", { className: "nm" }, ent.name),
689
- React.createElement("span", { className: "sz" }, ent.isDirectory ? "" : formatSize(ent.size)),
690
- );
691
- }),
692
- );
693
- }
694
-
695
- /** LA-like dual-pane SFTP */
696
- function CenterSftp(props) {
697
- const { sessionId, projectPathKey, title, onClose, onOpenBash } = props;
698
- const [localPath, setLocalPath] = React.useState(projectPathKey || "/workspace");
699
- const [remotePath, setRemotePath] = React.useState("/");
700
- const [localEntries, setLocalEntries] = React.useState([]);
701
- const [remoteEntries, setRemoteEntries] = React.useState([]);
702
- const [localSel, setLocalSel] = React.useState([]);
703
- const [remoteSel, setRemoteSel] = React.useState([]);
704
- const [status, setStatus] = React.useState("");
705
- const [err, setErr] = React.useState("");
706
- const [ctx, setCtx] = React.useState(null); // {x,y,side,entry|null}
707
- const [dialog, setDialog] = React.useState(null); // in-panel modal config
708
- const dialogToken = React.useRef(0);
709
- const localAnchor = React.useRef(null);
710
- const remoteAnchor = React.useRef(null);
711
-
712
- function openDialog(cfg) {
713
- dialogToken.current += 1;
714
- return new Promise(function (resolve) {
715
- setDialog(Object.assign({}, cfg, {
716
- token: dialogToken.current,
717
- _resolve: resolve,
718
- }));
719
- });
720
- }
721
- function finishDialog(result) {
722
- setDialog(function (cur) {
723
- if (cur && cur._resolve) cur._resolve(result);
724
- return null;
725
- });
726
- }
727
- function askText(opts) {
728
- return openDialog({
729
- mode: "prompt",
730
- title: opts.title,
731
- description: opts.description || "",
732
- defaultValue: opts.defaultValue || "",
733
- placeholder: opts.placeholder || "",
734
- confirmLabel: opts.confirmLabel || t("ok"),
735
- }).then(function (v) {
736
- if (v == null) return null;
737
- const s = String(v).trim();
738
- return s ? s : null;
739
- });
740
- }
741
- function askConfirm(opts) {
742
- return openDialog({
743
- mode: "confirm",
744
- title: opts.title,
745
- description: opts.description || "",
746
- confirmLabel: opts.confirmLabel || t("ok"),
747
- danger: !!opts.danger,
748
- }).then(function (v) { return !!v; });
749
- }
750
- function askAlert(opts) {
751
- return openDialog({
752
- mode: "alert",
753
- title: opts.title,
754
- description: opts.description || "",
755
- confirmLabel: opts.confirmLabel || t("gotIt"),
756
- });
757
- }
758
-
759
- const refreshLocal = React.useCallback(async function (p) {
760
- const path = p != null ? p : localPath;
761
- const r = await api("localList", { projectPathKey, path: path });
762
- setLocalPath(r.path);
763
- setLocalEntries(r.entries || []);
764
- setLocalSel([]);
765
- }, [localPath, projectPathKey]);
766
-
767
- const refreshRemote = React.useCallback(async function (p) {
768
- const path = p != null ? p : remotePath;
769
- const r = await api("sftpList", { sessionId, projectPathKey, path: path });
770
- setRemotePath(r.path || path);
771
- setRemoteEntries(r.entries || []);
772
- setRemoteSel([]);
773
- }, [remotePath, sessionId, projectPathKey]);
774
-
775
- React.useEffect(function () {
776
- refreshLocal(projectPathKey || "/workspace").catch(function (e) {
777
- setErr(String(e && e.message ? e.message : e));
778
- });
779
- refreshRemote("/").catch(function (e) {
780
- const msg = String(e && e.message ? e.message : e);
781
- setErr(/not running|disconnected/i.test(msg) ? t("sessionDropped") + " · " + msg : msg);
782
- });
783
- }, [sessionId, projectPathKey]); // eslint-disable-line
784
-
785
- function selectHandler(side) {
786
- return function (ent, e) {
787
- const list = side === "local" ? localEntries : remoteEntries;
788
- const setSel = side === "local" ? setLocalSel : setRemoteSel;
789
- const sel = side === "local" ? localSel : remoteSel;
790
- const anchorRef = side === "local" ? localAnchor : remoteAnchor;
791
- if (e.shiftKey && anchorRef.current) {
792
- const paths = list.map(function (x) { return x.path; });
793
- const a = paths.indexOf(anchorRef.current);
794
- const b = paths.indexOf(ent.path);
795
- if (a >= 0 && b >= 0) {
796
- const lo = Math.min(a, b), hi = Math.max(a, b);
797
- setSel(paths.slice(lo, hi + 1));
798
- return;
799
- }
800
- }
801
- if (e.metaKey || e.ctrlKey) {
802
- if (sel.indexOf(ent.path) >= 0) setSel(sel.filter(function (p) { return p !== ent.path; }));
803
- else setSel(sel.concat([ent.path]));
804
- anchorRef.current = ent.path;
805
- return;
806
- }
807
- setSel([ent.path]);
808
- anchorRef.current = ent.path;
809
- };
810
- }
811
-
812
- function activate(side) {
813
- return function (ent) {
814
- if (ent.isDirectory) {
815
- if (side === "local") refreshLocal(ent.path).catch(function (e) { setErr(String(e.message || e)); });
816
- else refreshRemote(ent.path).catch(function (e) { setErr(String(e.message || e)); });
817
- }
818
- };
819
- }
820
-
821
- async function transfer(direction, paths) {
822
- // direction: 'upload' local->remote, 'download' remote->local
823
- setErr("");
824
- setStatus(t("transferring"));
825
- try {
826
- for (let i = 0; i < paths.length; i++) {
827
- const src = paths[i];
828
- const name = src.split("/").filter(Boolean).pop();
829
- if (direction === "upload") {
830
- // skip dirs for v1 simple file upload
831
- const ent = localEntries.find(function (e) { return e.path === src; });
832
- if (ent && ent.isDirectory) {
833
- setStatus(t("skipDirHint", { name: name }));
834
- continue;
835
- }
836
- const remote = joinPath(remotePath, name);
837
- await api("sftpUpload", {
838
- sessionId, projectPathKey,
839
- local_path: src,
840
- remote_path: remote,
841
- });
842
- } else {
843
- const ent = remoteEntries.find(function (e) { return e.path === src; });
844
- if (ent && ent.isDirectory) {
845
- setStatus(t("skipDir", { name: name }));
846
- continue;
847
- }
848
- const local = joinPath(localPath, name);
849
- await api("sftpDownload", {
850
- sessionId, projectPathKey,
851
- remote_path: src,
852
- local_path: local,
853
- });
854
- }
855
- }
856
- await refreshLocal(localPath);
857
- await refreshRemote(remotePath);
858
- setStatus("");
859
- } catch (e) {
860
- setErr(String(e && e.message ? e.message : e));
861
- setStatus("");
862
- }
863
- }
864
-
865
- function onDropPane(side) {
866
- return function (e) {
867
- e.preventDefault();
868
- let raw = e.dataTransfer.getData("application/x-dsh-sftp");
869
- if (!raw) return;
870
- let payload;
871
- try { payload = JSON.parse(raw); } catch (err) { return; }
872
- if (!payload || !payload.paths) return;
873
- if (payload.side === side) return;
874
- if (payload.side === "local" && side === "remote") transfer("upload", payload.paths);
875
- if (payload.side === "remote" && side === "local") transfer("download", payload.paths);
876
- };
877
- }
878
-
879
- async function ctxAction(action) {
880
- if (!ctx) return;
881
- const side = ctx.side;
882
- const entry = ctx.entry;
883
- const sel = side === "local" ? (localSel.length ? localSel : (entry ? [entry.path] : [])) : (remoteSel.length ? remoteSel : (entry ? [entry.path] : []));
884
- setCtx(null);
885
- try {
886
- if (action === "open" && entry) {
887
- activate(side)(entry);
888
- return;
889
- }
890
- if (action === "upload" && side === "local") {
891
- await transfer("upload", sel);
892
- return;
893
- }
894
- if (action === "download" && side === "remote") {
895
- await transfer("download", sel);
896
- return;
897
- }
898
- if (action === "mkdir") {
899
- const name = await askText({
900
- title: t("mkdirTitle"),
901
- description: t("mkdirLocation", { path: side === "local" ? localPath : remotePath }),
902
- placeholder: t("mkdirPlaceholder"),
903
- confirmLabel: t("create"),
904
- });
905
- if (!name) return;
906
- if (side === "local") {
907
- await api("localMkdir", { projectPathKey, path: joinPath(localPath, name) });
908
- await refreshLocal(localPath);
909
- } else {
910
- await api("sftpMkdir", { sessionId, projectPathKey, path: joinPath(remotePath, name) });
911
- await refreshRemote(remotePath);
912
- }
913
- return;
914
- }
915
- if (action === "rename" && entry) {
916
- const name = await askText({
917
- title: t("renameTitle"),
918
- description: entry.path,
919
- defaultValue: entry.name,
920
- confirmLabel: t("save"),
921
- });
922
- if (!name || name === entry.name) return;
923
- const dest = joinPath(side === "local" ? localPath : remotePath, name);
924
- if (side === "local") {
925
- await api("localRename", { projectPathKey, from_path: entry.path, to_path: dest });
926
- await refreshLocal(localPath);
927
- } else {
928
- await api("sftpRename", { sessionId, projectPathKey, from_path: entry.path, to_path: dest });
929
- await refreshRemote(remotePath);
930
- }
931
- return;
932
- }
933
- if (action === "delete") {
934
- if (!sel.length) return;
935
- const ok = await askConfirm({
936
- title: t("confirmDeleteTitle"),
937
- description: t("confirmDeleteDesc", { count: sel.length }),
938
- confirmLabel: t("delete"),
939
- danger: true,
940
- });
941
- if (!ok) return;
942
- for (let i = 0; i < sel.length; i++) {
943
- if (side === "local") await api("localDelete", { projectPathKey, path: sel[i] });
944
- else await api("sftpDelete", { sessionId, projectPathKey, path: sel[i] });
945
- }
946
- if (side === "local") await refreshLocal(localPath);
947
- else await refreshRemote(remotePath);
948
- return;
949
- }
950
- if (action === "copyPath" && entry) {
951
- try {
952
- await navigator.clipboard.writeText(entry.path);
953
- setStatus(t("copiedPath"));
954
- } catch (e) {
955
- await askAlert({ title: t("pathTitle"), description: entry.path });
956
- }
957
- }
958
- } catch (e) {
959
- setErr(String(e && e.message ? e.message : e));
960
- }
961
- }
962
-
963
- function pane(side) {
964
- const isLocal = side === "local";
965
- const path = isLocal ? localPath : remotePath;
966
- const setPath = isLocal ? setLocalPath : setRemotePath;
967
- const entries = isLocal ? localEntries : remoteEntries;
968
- const sel = isLocal ? localSel : remoteSel;
969
- const refresh = isLocal ? refreshLocal : refreshRemote;
970
- return React.createElement("div", {
971
- className: "ssh-sftp-pane",
972
- onDragOver: function (e) { e.preventDefault(); },
973
- onDrop: onDropPane(side),
974
- },
975
- React.createElement("div", { className: "ssh-sftp-pane-hd" },
976
- React.createElement("strong", null, isLocal ? t("localProject") : t("remoteDevice")),
977
- React.createElement(Btn, { onClick: function () { refresh(parentOf(path)).catch(function (e) { setErr(String(e.message || e)); }); } }, t("parentDir")),
978
- React.createElement(Btn, { onClick: function () { refresh(path).catch(function (e) { setErr(String(e.message || e)); }); } }, t("refresh")),
979
- React.createElement("input", {
980
- className: "ssh-t-input",
981
- value: path,
982
- onChange: function (e) { setPath(e.target.value); },
983
- onKeyDown: function (e) {
984
- if (e.key === "Enter") refresh(path).catch(function (err) { setErr(String(err.message || err)); });
985
- },
986
- }),
987
- ),
988
- React.createElement(FileList, {
989
- side: side,
990
- entries: entries,
991
- selected: sel,
992
- onSelect: selectHandler(side),
993
- onActivate: activate(side),
994
- onContext: function (e, ent) {
995
- // ensure selection includes entry
996
- if (ent) {
997
- const cur = isLocal ? localSel : remoteSel;
998
- if (cur.indexOf(ent.path) < 0) {
999
- if (isLocal) setLocalSel([ent.path]);
1000
- else setRemoteSel([ent.path]);
1001
- }
1002
- }
1003
- setCtx({ x: e.clientX, y: e.clientY, side: side, entry: ent });
1004
- },
1005
- }),
1006
- );
1007
- }
1008
-
1009
- return React.createElement(CenterOverlay, { onClose: onClose },
1010
- React.createElement("div", { className: "ssh-ov-bar" },
1011
- React.createElement("div", { className: "ssh-t-title" }, t("sftp") + " · " + (title || sessionId.slice(0, 8))),
1012
- React.createElement("div", { className: "ssh-ov-tabs" },
1013
- React.createElement("button", { type: "button", onClick: onOpenBash }, t("bash")),
1014
- React.createElement("button", { type: "button", className: "on" }, t("sftp")),
1015
- ),
1016
- React.createElement(Btn, {
1017
- primary: true,
1018
- disabled: !localSel.length,
1019
- onClick: function () { transfer("upload", localSel); },
1020
- }, t("upload")),
1021
- React.createElement(Btn, {
1022
- primary: true,
1023
- disabled: !remoteSel.length,
1024
- onClick: function () { transfer("download", remoteSel); },
1025
- }, t("download")),
1026
- React.createElement(Btn, { onClick: onClose }, t("close")),
1027
- ),
1028
- React.createElement("div", { className: "ssh-ov-body ssh-sftp" },
1029
- React.createElement("div", { className: "ssh-sftp-panes" },
1030
- pane("local"),
1031
- pane("remote"),
1032
- ),
1033
- React.createElement("div", { className: "ssh-status" },
1034
- (status || t("sftpHint")) + (err ? " · " + err : ""),
1035
- ),
1036
- ),
1037
- ctx ? React.createElement("div", {
1038
- className: "ssh-ctx",
1039
- style: { left: ctx.x, top: ctx.y },
1040
- onMouseDown: function (e) { e.stopPropagation(); },
1041
- },
1042
- ctx.entry ? React.createElement("button", { type: "button", onClick: function () { ctxAction("open"); } }, t("open")) : null,
1043
- ctx.side === "local" ? React.createElement("button", { type: "button", onClick: function () { ctxAction("upload"); } }, t("uploadToRemote")) : null,
1044
- ctx.side === "remote" ? React.createElement("button", { type: "button", onClick: function () { ctxAction("download"); } }, t("downloadToLocal")) : null,
1045
- React.createElement("button", { type: "button", onClick: function () { ctxAction("mkdir"); } }, t("newFolder")),
1046
- ctx.entry ? React.createElement("button", { type: "button", onClick: function () { ctxAction("rename"); } }, t("rename")) : null,
1047
- ctx.entry ? React.createElement("button", { type: "button", onClick: function () { ctxAction("copyPath"); } }, t("copyPath")) : null,
1048
- React.createElement("hr"),
1049
- React.createElement("button", { type: "button", className: "danger", onClick: function () { ctxAction("delete"); } }, t("delete")),
1050
- ) : null,
1051
- ctx ? React.createElement("div", {
1052
- style: { position: "fixed", inset: 0, zIndex: 12999 },
1053
- onMouseDown: function () { setCtx(null); },
1054
- onContextMenu: function (e) { e.preventDefault(); setCtx(null); },
1055
- }) : null,
1056
- React.createElement(TextDialog, {
1057
- open: !!dialog,
1058
- token: dialog && dialog.token,
1059
- mode: dialog && dialog.mode,
1060
- title: dialog && dialog.title,
1061
- description: dialog && dialog.description,
1062
- defaultValue: dialog && dialog.defaultValue,
1063
- placeholder: dialog && dialog.placeholder,
1064
- confirmLabel: dialog && dialog.confirmLabel,
1065
- danger: dialog && dialog.danger,
1066
- onCancel: function () { finishDialog(null); },
1067
- onConfirm: function (v) { finishDialog(v); },
1068
- }),
1069
- );
1070
- }
1071
-
1072
- function TunnelPanel(props) {
1073
- const visible = props.visible;
1074
- const scope = props.scope || {};
1075
- const [view, setView] = React.useState("tunnel");
1076
- const [projectPathKey, setProjectPathKey] = React.useState("");
1077
- const [sessionId, setSessionId] = React.useState(scope.sessionId || "");
1078
- const [hosts, setHosts] = React.useState([]);
1079
- const [granted, setGranted] = React.useState([]);
1080
- const [sessions, setSessions] = React.useState([]);
1081
- const [prompts, setPrompts] = React.useState([]);
1082
- const [scan, setScan] = React.useState(null);
1083
- const [form, setForm] = React.useState(emptyForm());
1084
- const [sftpOn, setSftpOn] = React.useState(true);
1085
- const [busy, setBusy] = React.useState("");
1086
- const [message, setMessage] = React.useState("");
1087
- const [error, setError] = React.useState("");
1088
- // center overlay state
1089
- const [overlay, setOverlay] = React.useState(null); // {sessionId, title, mode:'bash'|'sftp'}
1090
-
1091
- const cwdGuess = scope.cwd || scope.workspacePath || "";
1092
-
1093
- const refresh = React.useCallback(async () => {
1094
- setError("");
1095
- const ctx = await api("getProjectContext", {
1096
- sessionId: scope.sessionId || sessionId || "",
1097
- cwd: cwdGuess || undefined,
1098
- });
1099
- const key = ctx.projectPathKey || cwdGuess || "/workspace";
1100
- setProjectPathKey(key);
1101
- if (ctx.sessionId) setSessionId(ctx.sessionId);
1102
- const [h, g, s, p] = await Promise.all([
1103
- api("listHosts"),
1104
- api("getGrants", { projectPathKey: key }),
1105
- api("listSessions", { projectPathKey: key }),
1106
- api("listPrompts"),
1107
- ]);
1108
- setHosts(h.hosts || []);
1109
- setGranted(g.hostIds || []);
1110
- setSessions(s.sessions || []);
1111
- setPrompts(p.prompts || []);
1112
- }, [scope.sessionId, cwdGuess, sessionId]);
1113
-
1114
- React.useEffect(() => {
1115
- if (!visible) return;
1116
- refresh().catch(function (e) { setError(String(e && e.message ? e.message : e)); });
1117
- const t = setInterval(function () {
1118
- if (!visible) return;
1119
- api("listSessions", { projectPathKey: projectPathKey || cwdGuess || "/workspace" })
1120
- .then(function (s) { setSessions(s.sessions || []); }).catch(function () {});
1121
- api("listPrompts").then(function (p) { setPrompts(p.prompts || []); }).catch(function () {});
1122
- }, 4000);
1123
- return function () { clearInterval(t); };
1124
- }, [visible, refresh, projectPathKey, cwdGuess]);
1125
-
1126
- function patchForm(p) {
1127
- setForm(function (prev) { return Object.assign({}, prev, p); });
1128
- }
1129
- async function run(name, fn) {
1130
- setBusy(name); setMessage(""); setError("");
1131
- try { await fn(); setMessage(""); await refresh(); }
1132
- catch (e) { setError(String(e && e.message ? e.message : e)); }
1133
- finally { setBusy(""); }
1134
- }
1135
- function toggleGrant(id) {
1136
- setGranted(granted.includes(id) ? granted.filter(function (x) { return x !== id; }) : granted.concat([id]));
1137
- }
1138
- function openCenter(sess, mode) {
1139
- setOverlay({
1140
- sessionId: sess.session_id,
1141
- title: sess.title || sess.endpoint || sess.session_id,
1142
- mode: mode,
1143
- });
1144
- }
1145
-
1146
- const tabs = [
1147
- { id: "grants", label: t("tabGrants") },
1148
- { id: "hosts", label: t("tabHosts") },
1149
- { id: "tunnel", label: t("tabTunnel") },
1150
- ];
1151
-
1152
- const overlayEl = overlay
1153
- ? (overlay.mode === "bash"
1154
- ? React.createElement(CenterBash, {
1155
- sessionId: overlay.sessionId,
1156
- projectPathKey: projectPathKey,
1157
- title: overlay.title,
1158
- onClose: function () { setOverlay(null); },
1159
- onOpenSftp: function () { setOverlay(Object.assign({}, overlay, { mode: "sftp" })); },
1160
- })
1161
- : React.createElement(CenterSftp, {
1162
- sessionId: overlay.sessionId,
1163
- projectPathKey: projectPathKey,
1164
- title: overlay.title,
1165
- onClose: function () { setOverlay(null); },
1166
- onOpenBash: function () { setOverlay(Object.assign({}, overlay, { mode: "bash" })); },
1167
- }))
1168
- : null;
1169
-
1170
- return React.createElement(React.Fragment, null,
1171
- overlayEl,
1172
- React.createElement("div", { className: "ssh-t-root" },
1173
- React.createElement("div", { className: "ssh-t-head" },
1174
- React.createElement("div", null,
1175
- React.createElement("div", { className: "ssh-t-title" }, t("tabTitle")),
1176
- React.createElement("div", { className: "ssh-t-sub" }, t("projectLabel", { path: projectPathKey || t("projectUnbound") })),
1177
- ),
1178
- React.createElement(Btn, { disabled: !!busy, onClick: function () {
1179
- refresh().catch(function (e) { setError(String(e.message || e)); });
1180
- } }, t("refresh")),
1181
- ),
1182
- React.createElement("div", { className: "ssh-t-tabs" },
1183
- tabs.map(function (t) {
1184
- return React.createElement("button", {
1185
- key: t.id, type: "button",
1186
- className: "ssh-t-tab" + (view === t.id || (view === "edit" && t.id === "hosts") ? " on" : ""),
1187
- onClick: function () { setView(t.id); },
1188
- }, t.label);
1189
- }),
1190
- ),
1191
- prompts && prompts.length ? React.createElement("div", { className: "ssh-t-prompt" },
1192
- React.createElement("div", { style: { fontWeight: 600, marginBottom: 6 } }, t("needConfirm")),
1193
- prompts.map(function (p) {
1194
- const fpLabel = p.fingerprint
1195
- ? ((p.fingerprintAlgo || "SHA256") + ":" + String(p.fingerprint))
1196
- : "";
1197
- return React.createElement("div", { key: p.promptId, style: { marginBottom: 8 } },
1198
- React.createElement("div", null, p.message || p.kind),
1199
- fpLabel
1200
- ? React.createElement("div", {
1201
- className: "ssh-t-muted",
1202
- style: { marginTop: 6, wordBreak: "break-all", fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11 },
1203
- }, t("hostKeyFingerprint") + ": " + fpLabel)
1204
- : null,
1205
- React.createElement("div", { className: "ssh-t-actions" },
1206
- React.createElement(Btn, { primary: true, disabled: !!busy, onClick: function () {
1207
- run("trust", async function () {
1208
- const r = await api("answerPrompt", {
1209
- promptId: p.promptId, trustHostKey: true, connectAfter: true,
1210
- projectPathKey: projectPathKey, sftpEnabled: sftpOn,
1211
- });
1212
- if (r && r.connected === false && r.error) throw new Error(String(r.error));
1213
- });
1214
- } }, t("trustConnect")),
1215
- ),
1216
- );
1217
- }),
1218
- ) : null,
1219
- view === "grants" ? React.createElement("div", { className: "ssh-t-card" },
1220
- React.createElement("div", { className: "ssh-t-muted", style: { marginBottom: 8 } }, t("grantsHint")),
1221
- hosts.map(function (h) {
1222
- return React.createElement("label", { key: h.id, className: "ssh-t-check" },
1223
- React.createElement("input", {
1224
- type: "checkbox", checked: granted.includes(h.id),
1225
- onChange: function () { toggleGrant(h.id); },
1226
- }),
1227
- React.createElement("span", null,
1228
- React.createElement("span", { className: "ssh-t-name" }, h.name || h.id),
1229
- React.createElement("span", { className: "ssh-t-endpoint" },
1230
- h.username + "@" + h.host + ":" + (h.port || 22) + " · " + h.credentialStatus,
1231
- ),
1232
- ),
1233
- );
1234
- }),
1235
- React.createElement("div", { className: "ssh-t-actions" },
1236
- React.createElement(Btn, { primary: true, disabled: !!busy, onClick: function () {
1237
- run("grants", async function () {
1238
- await api("setGrants", { projectPathKey: projectPathKey, hostIds: granted });
1239
- });
1240
- } }, t("saveGrants")),
1241
- ),
1242
- ) : null,
1243
- view === "hosts" ? React.createElement("div", { className: "ssh-t-card" },
1244
- React.createElement("div", { className: "ssh-t-actions", style: { marginTop: 0 } },
1245
- React.createElement(Btn, { primary: true, onClick: function () { setForm(emptyForm()); setView("edit"); } }, t("newHost")),
1246
- React.createElement(Btn, { disabled: !!busy, onClick: function () {
1247
- run("scan", async function () { setScan(await api("scanOpenSsh")); });
1248
- } }, t("scanOpenSsh")),
1249
- ),
1250
- hosts.map(function (h) {
1251
- return React.createElement("div", { key: h.id, className: "ssh-t-row" },
1252
- React.createElement("div", { className: "ssh-t-row-main" },
1253
- React.createElement("div", { className: "ssh-t-name" }, h.name || h.id),
1254
- React.createElement("div", { className: "ssh-t-endpoint", title: h.username + "@" + h.host + ":" + h.port },
1255
- h.username + "@" + h.host + ":" + h.port,
1256
- ),
1257
- ),
1258
- React.createElement("div", { className: "ssh-t-actions" },
1259
- React.createElement(Btn, { onClick: function () {
1260
- setForm({
1261
- id: h.id, name: h.name || "", host: h.host || "", port: h.port || 22,
1262
- username: h.username || "",
1263
- authType: h.authType === "keyboardInteractive" ? "password" : (h.authType || "privateKey"),
1264
- privateKeyPath: h.privateKeyPath || "", password: "", privateKeyPem: "", passphrase: "",
1265
- });
1266
- setView("edit");
1267
- } }, t("edit")),
1268
- React.createElement(Btn, { danger: true, onClick: function () {
1269
- if (!window.confirm(t("deleteHostConfirm", { name: h.name || h.id }))) return;
1270
- run("del", async function () { await api("deleteHost", { id: h.id }); });
1271
- } }, t("delete")),
1272
- ),
1273
- );
1274
- }),
1275
- scan ? React.createElement("div", { style: { marginTop: 12 } },
1276
- (scan.configs || []).map(function (c, i) {
1277
- return React.createElement("div", { key: i, className: "ssh-t-row" },
1278
- React.createElement("div", { className: "ssh-t-row-main" },
1279
- React.createElement("div", { className: "ssh-t-name" }, c.name),
1280
- React.createElement("div", { className: "ssh-t-endpoint", title: c.host }, "→ " + c.host),
1281
- ),
1282
- React.createElement("div", { className: "ssh-t-actions" },
1283
- React.createElement(Btn, { onClick: function () {
1284
- run("imp", async function () { await api("importScan", { entry: c }); });
1285
- } }, t("import")),
1286
- ),
1287
- );
1288
- }),
1289
- ) : null,
1290
- ) : null,
1291
- view === "edit" ? React.createElement("div", { className: "ssh-t-card" },
1292
- React.createElement(Field, { label: t("name") }, React.createElement("input", { className: "ssh-t-input", value: form.name, onChange: function (e) { patchForm({ name: e.target.value }); } })),
1293
- React.createElement(Field, { label: t("host") }, React.createElement("input", { className: "ssh-t-input", value: form.host, onChange: function (e) { patchForm({ host: e.target.value }); } })),
1294
- React.createElement(Field, { label: t("port") }, React.createElement("input", { className: "ssh-t-input", type: "number", value: form.port, onChange: function (e) { patchForm({ port: e.target.value }); } })),
1295
- React.createElement(Field, { label: t("username") }, React.createElement("input", { className: "ssh-t-input", value: form.username, onChange: function (e) { patchForm({ username: e.target.value }); } })),
1296
- React.createElement(Field, { label: t("authType") },
1297
- React.createElement("select", { className: "ssh-t-select", value: form.authType, onChange: function (e) { patchForm({ authType: e.target.value }); } },
1298
- React.createElement("option", { value: "privateKey" }, t("authPrivateKey")),
1299
- React.createElement("option", { value: "password" }, t("authPassword")),
1300
- ),
1301
- ),
1302
- form.authType === "privateKey" ? React.createElement(React.Fragment, null,
1303
- React.createElement(Field, { label: t("privateKeyPath") }, React.createElement("input", { className: "ssh-t-input", value: form.privateKeyPath, onChange: function (e) { patchForm({ privateKeyPath: e.target.value }); } })),
1304
- React.createElement(Field, { label: t("privateKeyPem") }, React.createElement("textarea", { className: "ssh-t-textarea", rows: 3, value: form.privateKeyPem, onChange: function (e) { patchForm({ privateKeyPem: e.target.value }); } })),
1305
- React.createElement(Field, { label: t("passphrase") }, React.createElement("input", { className: "ssh-t-input", type: "password", value: form.passphrase, onChange: function (e) { patchForm({ passphrase: e.target.value }); } })),
1306
- ) : null,
1307
- form.authType === "password" ? React.createElement(Field, { label: t("password") }, React.createElement("input", { className: "ssh-t-input", type: "password", value: form.password, onChange: function (e) { patchForm({ password: e.target.value }); } })) : null,
1308
- form.authType === "keyboardInteractive" ? React.createElement("div", { className: "ssh-t-err" }, t("kbiUnsupported")) : null,
1309
- React.createElement("div", { className: "ssh-t-actions" },
1310
- React.createElement(Btn, { primary: true, disabled: !!busy, onClick: function () {
1311
- run("save", async function () {
1312
- if (form.authType === "keyboardInteractive") {
1313
- throw new Error(t("kbiUnsupported"));
1314
- }
1315
- const payload = {
1316
- id: form.id || undefined, name: form.name, host: form.host,
1317
- port: Number(form.port) || 22, username: form.username, authType: form.authType,
1318
- privateKeyPath: form.privateKeyPath,
1319
- };
1320
- if (form.password) payload.password = form.password;
1321
- if (form.privateKeyPem) payload.privateKeyPem = form.privateKeyPem;
1322
- if (form.passphrase) payload.passphrase = form.passphrase;
1323
- await api("saveHost", { host: payload });
1324
- setView("hosts");
1325
- });
1326
- } }, t("save")),
1327
- React.createElement(Btn, { onClick: function () { setView("hosts"); } }, t("back")),
1328
- ),
1329
- ) : null,
1330
- view === "tunnel" ? React.createElement("div", { className: "ssh-t-card" },
1331
- React.createElement("label", { className: "ssh-t-check" },
1332
- React.createElement("input", { type: "checkbox", checked: sftpOn, onChange: function (e) { setSftpOn(!!e.target.checked); } }),
1333
- React.createElement("span", null, t("sftpOnConnect")),
1334
- ),
1335
- React.createElement("div", { style: { fontWeight: 600, margin: "8px 0" } }, t("connectSection")),
1336
- hosts.filter(function (h) { return granted.includes(h.id); }).map(function (h) {
1337
- return React.createElement("div", { key: h.id, className: "ssh-t-row" },
1338
- React.createElement("div", { className: "ssh-t-row-main" },
1339
- React.createElement("div", { className: "ssh-t-name" }, h.name || h.id),
1340
- React.createElement("div", { className: "ssh-t-endpoint", title: h.username + "@" + h.host + ":" + (h.port || 22) },
1341
- h.username + "@" + h.host + ":" + (h.port || 22),
1342
- ),
1343
- ),
1344
- React.createElement("div", { className: "ssh-t-actions" },
1345
- React.createElement(Btn, { primary: true, disabled: !!busy, onClick: function () {
1346
- run("connect", async function () {
1347
- if (h.authType === "keyboardInteractive") {
1348
- throw new Error(t("kbiUnsupported"));
1349
- }
1350
- const r = await api("connect", {
1351
- hostId: h.id, projectPathKey: projectPathKey,
1352
- sftpEnabled: sftpOn, trustHostKey: false,
1353
- });
1354
- if (r && r.ok === false && r.promptId) {
1355
- setMessage(r.message || t("needHostKey"));
1356
- setPrompts((await api("listPrompts")).prompts || []);
1357
- }
1358
- });
1359
- } }, t("connect")),
1360
- ),
1361
- );
1362
- }),
1363
- React.createElement("div", { style: { fontWeight: 600, margin: "12px 0 6px" } }, t("sessionsSection")),
1364
- sessions.length === 0
1365
- ? React.createElement("div", { className: "ssh-t-muted" }, t("noSessions"))
1366
- : sessions.map(function (s) {
1367
- const live = s.running && s.status === "connected";
1368
- const reconnecting = s.status === "reconnecting";
1369
- const statusLabel = reconnecting
1370
- ? t("reconnecting", { attempt: s.reconnect_attempt || 1, max: s.reconnect_max_attempts || 3 })
1371
- : (live ? t("connected") : t("disconnected"));
1372
- return React.createElement("div", { key: s.session_id, className: "ssh-t-row" },
1373
- React.createElement("div", { className: "ssh-t-row-main" },
1374
- React.createElement("div", { className: "ssh-t-name" }, s.title || s.session_id),
1375
- React.createElement("div", { className: "ssh-t-endpoint" },
1376
- (s.endpoint ? s.endpoint + " · " : "") + statusLabel + " · sftp=" + String(s.sftpEnabled),
1377
- ),
1378
- ),
1379
- React.createElement("div", { className: "ssh-t-actions" },
1380
- React.createElement(Btn, { primary: true, disabled: !live, onClick: function () { openCenter(s, "bash"); } }, t("terminal")),
1381
- React.createElement(Btn, { disabled: !live || !s.sftpEnabled, onClick: function () { openCenter(s, "sftp"); } }, t("sftp")),
1382
- !live && !reconnecting ? React.createElement(Btn, { primary: true, disabled: !!busy, onClick: function () {
1383
- run("reconnect", async function () {
1384
- try {
1385
- await api("reconnect", { sessionId: s.session_id, projectPathKey: projectPathKey });
1386
- } catch (e) {
1387
- const msg = String(e && e.message ? e.message : e);
1388
- if (/keyboard-interactive/i.test(msg)) throw new Error(t("kbiUnsupported"));
1389
- throw e;
1390
- }
1391
- });
1392
- } }, t("reconnect")) : null,
1393
- React.createElement(Btn, { danger: true, onClick: function () {
1394
- run("disc", async function () {
1395
- if (overlay && overlay.sessionId === s.session_id) setOverlay(null);
1396
- await api("disconnect", { sessionId: s.session_id, projectPathKey: projectPathKey });
1397
- });
1398
- } }, live ? t("disconnect") : t("closeSession")),
1399
- ),
1400
- );
1401
- }),
1402
- ) : null,
1403
- message ? React.createElement("div", { className: "ssh-t-ok" }, message) : null,
1404
- error ? React.createElement("div", { className: "ssh-t-err" }, error) : null,
1405
- React.createElement("div", { className: "ssh-t-muted" },
1406
- t("footerHint"),
1407
- ),
1408
- ),
1409
- );
1410
- }
1411
-
1412
- function TunnelRoot(props) {
1413
- const ctx = props.ctx;
1414
- // Re-render on DSH locale switches (better-sidebar pattern).
1415
- const localeKey = React.useSyncExternalStore(
1416
- React.useCallback(function (cb) {
1417
- if (!ctx.locale || typeof ctx.locale.subscribe !== "function") return function () {};
1418
- return ctx.locale.subscribe(cb);
1419
- }, [ctx]),
1420
- React.useCallback(function () {
1421
- try {
1422
- return ctx.locale && ctx.locale.getSnapshot ? ctx.locale.getSnapshot().active : activeLocale();
1423
- } catch (e) {
1424
- return activeLocale();
1425
- }
1426
- }, [ctx]),
1427
- function () { return "en"; },
1428
- );
1429
- return React.createElement(TunnelPanel, {
1430
- key: "ssh-tunnel-" + String(localeKey || "en"),
1431
- visible: props.visible,
1432
- scope: props.scope,
1433
- });
1434
- }
1435
-
1436
- const inject = ["betterSidebar", "locale"];
1437
- function apply(ctx) {
1438
- ensureStyles();
1439
- if (ctx.locale) {
1440
- attachLocale(ctx.locale);
1441
- ctx.effect(function () {
1442
- const offZh = ctx.locale.register(LOCALE_NS, "zh", zh);
1443
- const offEn = ctx.locale.register(LOCALE_NS, "en", en);
1444
- return function () {
1445
- try { offZh(); } catch (e) {}
1446
- try { offEn(); } catch (e) {}
1447
- };
1448
- }, "dsh-ssh-tunnel: dictionaries");
1449
- }
1450
- if (!ctx.betterSidebar) return;
1451
- ctx.effect(function () {
1452
- return ctx.betterSidebar.registerTab({
1453
- id: TAB_ID,
1454
- title: function () { return t("tabTitle"); },
1455
- icon: function (size) { return icon(size); },
1456
- order: 44,
1457
- single: true,
1458
- component: function (p) {
1459
- return React.createElement(TunnelRoot, {
1460
- ctx: ctx,
1461
- visible: p.visible,
1462
- scope: p.scope,
1463
- });
1464
- },
1465
- });
1466
- }, "dsh-ssh-tunnel: register tab");
1467
- }
1468
- exports.apply = apply;
1469
- exports.inject = inject;
1470
- return module.exports;
1471
- },
1472
- });
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-ssh-tunnel",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ const React = require("react");
8
+
9
+ // Lazy react-dom resolver (same module loader registry as react).
10
+ // Only needed for the body portal in CenterOverlay; optional on purpose.
11
+ let ReactDOMRef;
12
+ function getReactDOM() {
13
+ if (ReactDOMRef !== undefined) return ReactDOMRef;
14
+ try { ReactDOMRef = require("react-dom"); } catch (e) { ReactDOMRef = null; }
15
+ return ReactDOMRef;
16
+ }
17
+
18
+ // --- i18n (same pattern as dsh-better-sidebar) ---
19
+ const LOCALE_NS = "sshTunnel";
20
+ const zh = {
21
+ tabTitle: "SSH 隧道",
22
+ projectLabel: "项目: {path}",
23
+ projectUnbound: "(未绑定)",
24
+ refresh: "刷新",
25
+ tabGrants: "项目授权",
26
+ tabHosts: "主机库",
27
+ tabTunnel: "隧道会话",
28
+ needConfirm: "需要确认",
29
+ trustConnect: "信任并连接",
30
+ dismissPrompt: "不信任",
31
+ promptKeyUnknown: "未知主机指纹",
32
+ promptKeyChanged: "主机指纹已变更",
33
+ promptFpNew: "新指纹",
34
+ promptFpOld: "原指纹",
35
+ trustedNotConnected: "已保存信任,但未发起连接:",
36
+ grantsHint: "勾选后,当前项目下的对话可通过 SSHManager 与隧道使用这些主机。连接前须先完成项目授权(不会在连接时自动授权)。",
37
+ saveGrants: "保存项目授权",
38
+ newHost: "新建主机",
39
+ scanOpenSsh: "扫描 OpenSSH",
40
+ edit: "编辑",
41
+ delete: "删除",
42
+ deleteHostConfirm: "确定删除主机「{name}」?此操作不可撤销。",
43
+ import: "导入",
44
+ name: "名称 / 别名",
45
+ host: "Host",
46
+ port: "Port",
47
+ username: "Username",
48
+ authType: "认证方式",
49
+ authPrivateKey: "私钥",
50
+ authPassword: "密码",
51
+ privateKeyPath: "私钥路径",
52
+ privateKeyPem: "或粘贴 PEM",
53
+ passphrase: "私钥口令",
54
+ password: "密码",
55
+ save: "保存",
56
+ back: "返回",
57
+ sftpOnConnect: "连接时启用 SFTP",
58
+ connectSection: "连接",
59
+ connect: "连接",
60
+ needHostKey: "需要在提示中信任 host key",
61
+ sessionsSection: "会话(终端 / SFTP 在中央打开)",
62
+ noSessions: "暂无会话",
63
+ terminal: "终端",
64
+ sftp: "SFTP",
65
+ disconnect: "断开",
66
+ reconnect: "重连",
67
+ reconnecting: "重连中 ({attempt}/{max})",
68
+ disconnected: "已断开",
69
+ connected: "已连接",
70
+ sessionDropped: "会话已断开",
71
+ kbiUnsupported: "不支持键盘交互登录,请编辑主机改为密码或私钥",
72
+ kbiEditNotice: "该主机原为键盘交互认证,此方式已不再支持;请改选密码或私钥并重新输入凭据。",
73
+ closeSession: "关闭",
74
+ footerHint: "连接与授权在此侧栏;终端与 SFTP 在中央面板打开。",
75
+ bashTitle: "SSH 终端 · {name}",
76
+ close: "关闭",
77
+ bash: "Bash",
78
+ bashStatusReady: "connected · 直接在终端内输入,Ctrl+C 等快捷键可用",
79
+ bashStatusLoading: "loading",
80
+ bashStatusError: "error",
81
+ xtermLoadFailed: "无法加载 xterm",
82
+ transferring: "传输中…",
83
+ transferSkipped: "传输完成,跳过 {count} 个目录",
84
+ mkdirTitle: "新建文件夹",
85
+ mkdirLocation: "位置:{path}",
86
+ mkdirPlaceholder: "文件夹名称",
87
+ create: "创建",
88
+ renameTitle: "重命名",
89
+ confirmDeleteTitle: "确认删除",
90
+ confirmDeleteDesc: "将删除 {count} 项,此操作可能不可恢复。",
91
+ copiedPath: "已复制路径",
92
+ pathTitle: "路径",
93
+ localProject: "本地项目",
94
+ remoteDevice: "远端设备",
95
+ parentDir: "上级",
96
+ upload: "上传 →",
97
+ download: "← 下载",
98
+ sftpHint: "多选: Ctrl/⌘ 点击 · Shift 范围 · 拖拽跨栏传输 · 右键菜单",
99
+ open: "打开",
100
+ uploadToRemote: "上传到远端",
101
+ downloadToLocal: "下载到本地",
102
+ newFolder: "新建文件夹",
103
+ rename: "重命名",
104
+ copyPath: "复制路径",
105
+ ok: "确定",
106
+ cancel: "取消",
107
+ gotIt: "好的",
108
+ };
109
+ const en = {
110
+ tabTitle: "SSH Tunnel",
111
+ projectLabel: "Project: {path}",
112
+ projectUnbound: "(not bound)",
113
+ refresh: "Refresh",
114
+ tabGrants: "Project access",
115
+ tabHosts: "Hosts",
116
+ tabTunnel: "Sessions",
117
+ needConfirm: "Confirmation needed",
118
+ trustConnect: "Trust and connect",
119
+ dismissPrompt: "Dismiss",
120
+ promptKeyUnknown: "Unknown host key",
121
+ promptKeyChanged: "Host key changed",
122
+ promptFpNew: "New fingerprint",
123
+ promptFpOld: "Previous fingerprint",
124
+ trustedNotConnected: "Trust saved, but the connection was not attempted:",
125
+ grantsHint: "Checked hosts can be used by SSHManager and the tunnel for this project. Grant access before connecting (connect does not auto-authorize).",
126
+ saveGrants: "Save project access",
127
+ newHost: "New host",
128
+ scanOpenSsh: "Scan OpenSSH",
129
+ edit: "Edit",
130
+ delete: "Delete",
131
+ deleteHostConfirm: "Delete host \"{name}\"? This cannot be undone.",
132
+ import: "Import",
133
+ name: "Name / alias",
134
+ host: "Host",
135
+ port: "Port",
136
+ username: "Username",
137
+ authType: "Authentication",
138
+ authPrivateKey: "Private key",
139
+ authPassword: "Password",
140
+ privateKeyPath: "Private key path",
141
+ privateKeyPem: "Or paste PEM",
142
+ passphrase: "Key passphrase",
143
+ password: "Password",
144
+ save: "Save",
145
+ back: "Back",
146
+ sftpOnConnect: "Enable SFTP on connect",
147
+ connectSection: "Connect",
148
+ connect: "Connect",
149
+ needHostKey: "Trust the host key in the prompt first",
150
+ sessionsSection: "Sessions (terminal / SFTP open in the center)",
151
+ noSessions: "No sessions",
152
+ terminal: "Terminal",
153
+ sftp: "SFTP",
154
+ disconnect: "Disconnect",
155
+ reconnect: "Reconnect",
156
+ reconnecting: "Reconnecting ({attempt}/{max})",
157
+ disconnected: "Disconnected",
158
+ connected: "Connected",
159
+ sessionDropped: "Session disconnected",
160
+ kbiUnsupported: "Keyboard-interactive auth is not supported; edit the host to password or private key",
161
+ kbiEditNotice: "This host used keyboard-interactive auth, which is no longer supported. Pick password or private key and re-enter the credential.",
162
+ closeSession: "Close",
163
+ footerHint: "Use this sidebar for connect and access; terminal and SFTP open in the center panel.",
164
+ bashTitle: "SSH terminal · {name}",
165
+ close: "Close",
166
+ bash: "Bash",
167
+ bashStatusReady: "connected · type directly in the terminal (Ctrl+C etc.)",
168
+ bashStatusLoading: "loading",
169
+ bashStatusError: "error",
170
+ xtermLoadFailed: "Failed to load xterm",
171
+ transferring: "Transferring…",
172
+ transferSkipped: "Transfer finished; {count} skipped",
173
+ mkdirTitle: "New folder",
174
+ mkdirLocation: "Location: {path}",
175
+ mkdirPlaceholder: "Folder name",
176
+ create: "Create",
177
+ renameTitle: "Rename",
178
+ confirmDeleteTitle: "Confirm delete",
179
+ confirmDeleteDesc: "Delete {count} item(s)? This may not be recoverable.",
180
+ copiedPath: "Path copied",
181
+ pathTitle: "Path",
182
+ localProject: "Local project",
183
+ remoteDevice: "Remote",
184
+ parentDir: "Up",
185
+ upload: "Upload →",
186
+ download: "← Download",
187
+ sftpHint: "Multi-select: Ctrl/⌘ click · Shift range · drag across panes · right-click menu",
188
+ open: "Open",
189
+ uploadToRemote: "Upload to remote",
190
+ downloadToLocal: "Download to local",
191
+ newFolder: "New folder",
192
+ rename: "Rename",
193
+ copyPath: "Copy path",
194
+ ok: "OK",
195
+ cancel: "Cancel",
196
+ gotIt: "OK",
197
+ };
198
+ let localeService;
199
+ function attachLocale(service) {
200
+ localeService = service;
201
+ }
202
+ function activeLocale() {
203
+ const fromSvc = localeService && localeService.getSnapshot ? localeService.getSnapshot().active : undefined;
204
+ const raw = fromSvc || (typeof navigator !== "undefined" ? navigator.language : "") || "en";
205
+ return String(raw);
206
+ }
207
+ function isZh() {
208
+ return activeLocale().toLowerCase().startsWith("zh");
209
+ }
210
+ /** Translate a copy key; `{name}` placeholders from params (better-sidebar style). */
211
+ function t(key, params) {
212
+ const dict = isZh() ? zh : en;
213
+ let text = dict[key];
214
+ if (text === undefined) text = en[key] || zh[key] || key;
215
+ if (params !== undefined && params !== null) {
216
+ for (const [name, value] of Object.entries(params)) {
217
+ text = String(text).split("{" + name + "}").join(String(value));
218
+ }
219
+ }
220
+ return text;
221
+ }
222
+
223
+ const TAB_ID = "dsh-ssh-tunnel";
224
+ const API = "/dsh-ssh-tunnel/api";
225
+
226
+ // Errors carry the structured fields the host sends: `code` (API_ERROR_STATUS
227
+ // semantic) and `data` (full parsed body) so callers can react to
228
+ // interactive states (e.g. a host-key prompt arrives as 200 + ok:false +
229
+ // promptId) instead of parsing message strings.
230
+ async function api(method, body) {
231
+ const response = await fetch(API + "/" + method, {
232
+ method: "POST",
233
+ headers: { "content-type": "application/json" },
234
+ body: JSON.stringify(body || {}),
235
+ });
236
+ const text = await response.text();
237
+ let data;
238
+ try { data = text ? JSON.parse(text) : {}; }
239
+ catch (e) { throw new Error("bad JSON (" + response.status + "): " + text.slice(0, 200)); }
240
+ const failed = !response.ok || (data && data.ok === false);
241
+ if (failed) {
242
+ const err = new Error(String((data && data.error) || ("HTTP " + response.status)));
243
+ if (data && data.code) err.code = String(data.code);
244
+ err.data = data;
245
+ throw err;
246
+ }
247
+ return data;
248
+ }
249
+
250
+ // Injected vendor nodes are tracked so the caller's effect disposer can
251
+ // remove them again (no permanent <head> growth on plugin reload).
252
+ let xtermPromise = null;
253
+ const injectedVendor = [];
254
+ function loadScript(src) {
255
+ return new Promise(function (resolve, reject) {
256
+ const s = document.createElement("script");
257
+ s.src = src;
258
+ s.async = true;
259
+ s.onload = function () { resolve(); };
260
+ s.onerror = function () { s.remove(); reject(new Error("load fail " + src)); };
261
+ injectedVendor.push(s);
262
+ document.head.appendChild(s);
263
+ });
264
+ }
265
+ function loadCssOnce(href) {
266
+ if (document.querySelector('link[data-ssh-t="' + href + '"]')) return;
267
+ const l = document.createElement("link");
268
+ l.rel = "stylesheet";
269
+ l.href = href;
270
+ l.setAttribute("data-ssh-t", href);
271
+ injectedVendor.push(l);
272
+ document.head.appendChild(l);
273
+ }
274
+ // Vendor assets are same-origin files served by this plugin's host route
275
+ // (/dsh-ssh-tunnel/vendor/*). No CDN, no module-table dependency, and no
276
+ // require("@xterm/*") fallback — the module table never carries xterm, so
277
+ // that branch was dead code. A failed load retries on the next open
278
+ // (xtermPromise is reset below) instead of caching the failure forever.
279
+ async function ensureXterm() {
280
+ if (xtermPromise) return xtermPromise;
281
+ xtermPromise = (async function () {
282
+ try {
283
+ if (typeof document !== "undefined") {
284
+ loadCssOnce("/dsh-ssh-tunnel/vendor/xterm.css");
285
+ await loadScript("/dsh-ssh-tunnel/vendor/xterm.js");
286
+ await loadScript("/dsh-ssh-tunnel/vendor/addon-fit.js");
287
+ }
288
+ const Terminal = globalThis.Terminal || null;
289
+ const FitAddon = (globalThis.FitAddon && globalThis.FitAddon.FitAddon) || globalThis.FitAddon || null;
290
+ if (!Terminal) throw new Error(t("xtermLoadFailed"));
291
+ return { Terminal: Terminal, FitAddon: FitAddon, disposeVendor: disposeVendor };
292
+ } catch (e) {
293
+ xtermPromise = null; // allow a retry on the next open
294
+ throw e;
295
+ }
296
+ })();
297
+ return xtermPromise;
298
+ }
299
+ function disposeVendor() {
300
+ for (const node of injectedVendor.splice(0)) {
301
+ try { node.remove(); } catch (e) {}
302
+ }
303
+ }
304
+
305
+ function icon(size) {
306
+ const s = size || 16;
307
+ // 与包根 icon.svg 同一份图:插件列表磁贴与侧边栏共用同一张标。
308
+ return React.createElement("svg", {
309
+ width: s, height: s, viewBox: "0 0 36 36", fill: "none", "aria-hidden": true,
310
+ },
311
+ React.createElement("defs", null,
312
+ React.createElement("linearGradient", {
313
+ id: "dsh-ssh-tunnel-tab-host", x1: "18", y1: "6", x2: "18", y2: "30",
314
+ gradientUnits: "userSpaceOnUse",
315
+ },
316
+ React.createElement("stop", { stopColor: "#4E6EF2" }),
317
+ React.createElement("stop", { offset: "1", stopColor: "#1434B8" })),
318
+ React.createElement("linearGradient", {
319
+ id: "dsh-ssh-tunnel-tab-flow", x1: "14", y1: "18", x2: "22", y2: "18",
320
+ gradientUnits: "userSpaceOnUse",
321
+ },
322
+ React.createElement("stop", { stopColor: "#5CE1E6" }),
323
+ React.createElement("stop", { offset: "1", stopColor: "#38E298" }))),
324
+ React.createElement("g", { transform: "translate(18 18) scale(1.3) translate(-18 -18)" },
325
+ React.createElement("rect", { x: 7, y: 10, width: 7, height: 16, rx: 2, fill: "url(#dsh-ssh-tunnel-tab-host)" }),
326
+ React.createElement("circle", { cx: 10.5, cy: 14, r: 1, fill: "#5CE1E6" }),
327
+ React.createElement("circle", { cx: 10.5, cy: 18, r: 1, fill: "#FFFFFF", fillOpacity: 0.8 }),
328
+ React.createElement("rect", { x: 22, y: 10, width: 7, height: 16, rx: 2, fill: "url(#dsh-ssh-tunnel-tab-host)" }),
329
+ React.createElement("circle", { cx: 25.5, cy: 14, r: 1, fill: "#5CE1E6" }),
330
+ React.createElement("circle", { cx: 25.5, cy: 18, r: 1, fill: "#FFFFFF", fillOpacity: 0.8 }),
331
+ React.createElement("path", { d: "M14 15.5H22", stroke: "url(#dsh-ssh-tunnel-tab-flow)", strokeWidth: 2.2, strokeLinecap: "round" }),
332
+ React.createElement("path", { d: "M14 20.5H22", stroke: "url(#dsh-ssh-tunnel-tab-flow)", strokeWidth: 2.2, strokeLinecap: "round" }),
333
+ React.createElement("circle", { cx: 18, cy: 15.5, r: 1.3, fill: "#FFFFFF" }),
334
+ React.createElement("circle", { cx: 18, cy: 20.5, r: 1.3, fill: "#FFFFFF" })),
335
+ );
336
+ }
337
+
338
+ // Returns a disposer removing the injected <style>; apply() registers it
339
+ // through ctx.effect so a disabled plugin leaves no style rules behind.
340
+ function ensureStyles() {
341
+ if (typeof document === "undefined") return function () {};
342
+ // bump id when theme CSS changes so hard-refresh replaces rules
343
+ const STYLE_ID = "dsh-ssh-tunnel-style-v4";
344
+ const prev = document.getElementById("dsh-ssh-tunnel-style") || document.getElementById("dsh-ssh-tunnel-style-v2") || document.getElementById("dsh-ssh-tunnel-style-v3") || document.getElementById("dsh-ssh-tunnel-style-v4");
345
+ if (prev && prev.id === STYLE_ID && prev.getAttribute("data-rev") === "4") return;
346
+ if (prev) prev.remove();
347
+ const el = document.createElement("style");
348
+ el.id = STYLE_ID;
349
+ el.setAttribute("data-rev", "4");
350
+ el.textContent = [
351
+ /* Theme-only surfaces: no hard-coded black/gray panels */
352
+ ".ssh-t-root{display:flex;flex-direction:column;gap:14px;padding:14px 14px 18px;height:100%;overflow:auto;font-size:13px;line-height:1.45;color:var(--dsw-alias-label-primary);box-sizing:border-box;background:transparent;}",
353
+ ".ssh-t-head{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:10px;}",
354
+ ".ssh-t-title{font-weight:600;font-size:15px;letter-spacing:-0.01em;color:var(--dsw-alias-label-primary);}",
355
+ ".ssh-t-sub{color:var(--dsw-alias-label-secondary);font-size:12px;word-break:break-all;margin-top:3px;opacity:1;}",
356
+ ".ssh-t-tabs{display:flex;gap:4px;flex-wrap:wrap;padding:3px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;width:fit-content;}",
357
+ ".ssh-t-tab{padding:6px 12px;border-radius:999px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;font-weight:500;transition:background var(--ds-transition-duration-slow, .15s) ease,color .15s ease;}",
358
+ ".ssh-t-tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);}",
359
+ ".ssh-t-tab.on{background:var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));color:var(--dsw-alias-label-primary-inverted, #fff);}",
360
+ ".ssh-t-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:14px;background:var(--dsw-alias-bg-layer-1);}",
361
+ /* Rows: keep action buttons visible when endpoint (IPv6) is long */
362
+ ".ssh-t-row{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:10px 12px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);}",
363
+ ".ssh-t-row:last-child{border-bottom:none;padding-bottom:0;}",
364
+ ".ssh-t-row:first-child{padding-top:0;}",
365
+ ".ssh-t-row-main{flex:1 1 12rem;min-width:0;max-width:100%;}",
366
+ ".ssh-t-row-main strong,.ssh-t-row-main .ssh-t-name{display:block;font-weight:600;overflow-wrap:anywhere;word-break:break-word;}",
367
+ ".ssh-t-endpoint{display:block;color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:2px;overflow-wrap:anywhere;word-break:break-word;line-height:1.4;}",
368
+ ".ssh-t-row > .ssh-t-actions{flex:0 0 auto;margin-top:0;margin-left:auto;}",
369
+ ".ssh-t-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;align-items:center;}",
370
+ ".ssh-t-check span{min-width:0;flex:1;overflow-wrap:anywhere;word-break:break-word;}",
371
+ ".ssh-t-btn{padding:7px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2, transparent);color:var(--dsw-alias-label-primary);cursor:pointer;font-size:12px;font-weight:500;transition:background .15s,border-color .15s;}",
372
+ ".ssh-t-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2);}",
373
+ ".ssh-t-btn:disabled{opacity:.45;cursor:not-allowed;}",
374
+ ".ssh-t-btn.primary{background:var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));border-color:transparent;color:var(--dsw-alias-label-primary-inverted, #fff);}",
375
+ ".ssh-t-btn.primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover, var(--dsw-alias-button-primary-fill));}",
376
+ ".ssh-t-btn.danger{border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);}",
377
+ ".ssh-t-btn.danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 16%, transparent);}",
378
+ ".ssh-t-field{display:block;margin-bottom:12px;}",
379
+ ".ssh-t-label{display:block;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);margin-bottom:6px;letter-spacing:0;text-transform:none;}",
380
+ ".ssh-t-input,.ssh-t-select,.ssh-t-textarea{width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:13px;outline:none;transition:border-color .15s, box-shadow .15s;}",
381
+ ".ssh-t-input:focus,.ssh-t-select:focus,.ssh-t-textarea:focus{border-color:var(--dsw-alias-border-l4, var(--dsw-alias-brand-primary));box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent);}",
382
+ ".ssh-t-ok{color:var(--dsw-alias-state-success-primary, #3d9a5f);font-size:12px;}",
383
+ ".ssh-t-err{color:var(--dsw-alias-state-error-primary);font-size:12px;white-space:pre-wrap;padding:8px 10px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);border:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 28%, transparent);}",
384
+ ".ssh-t-muted{color:var(--dsw-alias-label-secondary);font-size:12px;}",
385
+ ".ssh-t-check{display:flex;align-items:center;gap:10px;margin:8px 0;cursor:pointer;padding:8px 10px;border-radius:8px;border:1px solid transparent;color:var(--dsw-alias-label-primary);}",
386
+ ".ssh-t-check:hover{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l1);}",
387
+ ".ssh-t-check input{accent-color:var(--dsw-alias-brand-primary);}",
388
+ ".ssh-t-prompt{border:1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #d97706) 40%, transparent);background:var(--dsw-alias-state-warn-tertiary, color-mix(in srgb, #d97706 12%, transparent));border-radius:12px;padding:12px 14px;color:var(--dsw-alias-label-primary);}",
389
+ /* Overlay follows theme; only terminal canvas stays dark for readability */
390
+ ".ssh-ov-root{position:fixed;inset:0;z-index:12000;display:flex;align-items:center;justify-content:center;padding:24px;background:color-mix(in srgb, var(--dsw-alias-bg-base) 35%, rgba(0,0,0,.45));backdrop-filter:blur(6px);}",
391
+ ".ssh-ov-panel{display:flex;flex-direction:column;position:relative;width:min(1180px,100%);height:min(840px,100%);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;box-shadow:0 18px 50px color-mix(in srgb, #000 35%, transparent);overflow:hidden;}",
392
+ ".ssh-ov-bar{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);flex:none;}",
393
+ ".ssh-ov-bar .ssh-t-title{flex:1;min-width:0;font-size:14px;color:var(--dsw-alias-label-primary);}",
394
+ ".ssh-ov-tabs{display:flex;gap:4px;flex:none;padding:3px;background:var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base));border-radius:999px;border:1px solid var(--dsw-alias-border-l1);}",
395
+ ".ssh-ov-tabs button{padding:5px 12px;border-radius:999px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;font-weight:500;}",
396
+ ".ssh-ov-tabs button.on{background:var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary));color:var(--dsw-alias-label-primary-inverted, #fff);}",
397
+ ".ssh-ov-body{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);}",
398
+ ".ssh-ov-xterm{flex:1;min-height:0;padding:10px;background:#0b0f14;}",
399
+ ".ssh-ov-xterm .xterm,.ssh-ov-xterm .xterm-viewport{height:100%;}",
400
+ ".ssh-sftp{flex:1;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-base);}",
401
+ ".ssh-sftp-panes{flex:1;min-height:0;display:grid;grid-template-columns:1fr 1fr;gap:0;}",
402
+ ".ssh-sftp-pane{display:flex;flex-direction:column;min-width:0;min-height:0;border-right:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);}",
403
+ ".ssh-sftp-pane:last-child{border-right:none;}",
404
+ ".ssh-sftp-pane-hd{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:10px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;background:var(--dsw-alias-bg-layer-1);}",
405
+ ".ssh-sftp-pane-hd strong{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);min-width:64px;}",
406
+ ".ssh-sftp-pane-hd input{flex:1;min-width:80px;}",
407
+ ".ssh-sftp-list{flex:1;min-height:0;overflow:auto;user-select:none;padding:6px;background:var(--dsw-alias-bg-base);}",
408
+ ".ssh-sftp-item{display:flex;align-items:center;gap:10px;padding:8px 10px;font-size:12px;cursor:default;border-radius:8px;margin-bottom:2px;color:var(--dsw-alias-label-primary);}",
409
+ ".ssh-sftp-item:hover{background:var(--dsw-alias-interactive-bg-hover);}",
410
+ ".ssh-sftp-item.sel{background:var(--dsw-alias-interactive-bg-active, color-mix(in srgb, var(--dsw-alias-brand-primary) 16%, transparent));outline:1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 40%, transparent);}",
411
+ ".ssh-sftp-item .nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
412
+ ".ssh-sftp-item .sz{color:var(--dsw-alias-label-tertiary);flex:none;font-variant-numeric:tabular-nums;font-size:11px;}",
413
+ ".ssh-ctx{position:fixed;z-index:13000;min-width:190px;background:var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1));border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:6px;box-shadow:0 12px 32px color-mix(in srgb, #000 25%, transparent);}",
414
+ ".ssh-ctx button{display:block;width:100%;text-align:left;padding:8px 10px;border:none;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;font-size:12px;border-radius:6px;}",
415
+ ".ssh-ctx button:hover{background:var(--dsw-alias-interactive-bg-hover);}",
416
+ ".ssh-ctx button.danger{color:var(--dsw-alias-state-error-primary);}",
417
+ ".ssh-ctx hr{border:none;border-top:1px solid var(--dsw-alias-border-l1);margin:4px 0;}",
418
+ ".ssh-modal-root{position:absolute;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;padding:20px;background:color-mix(in srgb, var(--dsw-alias-bg-base) 30%, rgba(0,0,0,.35));}",".ssh-modal{width:min(400px,100%);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:14px;box-shadow:0 16px 40px color-mix(in srgb,#000 28%,transparent);padding:16px 16px 14px;}",".ssh-modal-title{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0 0 6px;}",".ssh-modal-desc{font-size:12px;color:var(--dsw-alias-label-secondary);margin:0 0 12px;line-height:1.45;word-break:break-all;}",".ssh-modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;}",".ssh-status{padding:7px 14px;font-size:11px;color:var(--dsw-alias-label-secondary);border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);flex:none;}",
419
+ ].join("\n");
420
+ document.head.appendChild(el);
421
+ return function () {
422
+ try { el.remove(); } catch (e) {}
423
+ };
424
+ }
425
+
426
+ function Btn(props) {
427
+ const cls = "ssh-t-btn" + (props.primary ? " primary" : "") + (props.danger ? " danger" : "");
428
+ return React.createElement("button", { type: "button", className: cls, disabled: props.disabled, onClick: props.onClick }, props.children);
429
+ }
430
+ function Field(props) {
431
+ return React.createElement("label", { className: "ssh-t-field" },
432
+ React.createElement("span", { className: "ssh-t-label" }, props.label), props.children);
433
+ }
434
+ function emptyForm() {
435
+ return { id: "", name: "", host: "", port: 22, username: "", authType: "privateKey", privateKeyPath: "", password: "", privateKeyPem: "", passphrase: "" };
436
+ }
437
+ // Paths are dual-flavor: remote SFTP paths are POSIX, local paths on
438
+ // Windows carry backslashes. Split/join detect the flavor per path
439
+ // instead of assuming "/" everywhere (C-01).
440
+ function splitPath(p) {
441
+ return String(p || "").split(/[\\/]+/).filter(Boolean);
442
+ }
443
+ function parentOf(p) {
444
+ const parts = splitPath(p);
445
+ if (parts.length <= 1) {
446
+ // "C:\" style drive roots and "/" both have no parent
447
+ return /^[A-Za-z]:[\\/]?$/.test(String(p || "")) ? String(p) : "/";
448
+ }
449
+ const head = parts.slice(0, -1).join(p.indexOf("\\") >= 0 ? "\\" : "/");
450
+ // preserve the drive prefix ("C:" + "\x")
451
+ if (/^[A-Za-z]:$/.test(head)) return head + (p.indexOf("\\") >= 0 ? "\\" : "/");
452
+ return head || "/";
453
+ }
454
+ function joinPath(base, name) {
455
+ if (!base) return name;
456
+ const sep = String(base).indexOf("\\") >= 0 ? "\\" : "/";
457
+ if (base === "/" || /^[A-Za-z]:[\\/]?$/.test(String(base))) return base.replace(/[\\/]?$/, sep) + name;
458
+ return String(base).replace(/[\\/]+$/, "") + sep + name;
459
+ }
460
+ function formatSize(n) {
461
+ n = Number(n) || 0;
462
+ if (n < 1024) return n + " B";
463
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " K";
464
+ return (n / (1024 * 1024)).toFixed(1) + " M";
465
+ }
466
+
467
+
468
+ /** In-panel dialog (replaces window.prompt / confirm). */
469
+ function TextDialog(props) {
470
+ const open = props.open;
471
+ const title = props.title || "";
472
+ const description = props.description || "";
473
+ const mode = props.mode || "prompt"; // prompt | confirm | alert
474
+ const confirmLabel = props.confirmLabel || (mode === "confirm" ? t("ok") : mode === "alert" ? t("gotIt") : t("ok"));
475
+ const cancelLabel = props.cancelLabel || t("cancel");
476
+ const danger = !!props.danger;
477
+ const [value, setValue] = React.useState(props.defaultValue || "");
478
+ const inputRef = React.useRef(null);
479
+ React.useEffect(function () {
480
+ if (!open) return;
481
+ setValue(props.defaultValue || "");
482
+ const t = setTimeout(function () {
483
+ if (inputRef.current) {
484
+ inputRef.current.focus();
485
+ inputRef.current.select && inputRef.current.select();
486
+ }
487
+ }, 30);
488
+ return function () { clearTimeout(t); };
489
+ }, [open, props.defaultValue, props.token]);
490
+ if (!open) return null;
491
+ function submit() {
492
+ if (mode === "prompt") props.onConfirm && props.onConfirm(value);
493
+ else props.onConfirm && props.onConfirm(true);
494
+ }
495
+ function cancel() {
496
+ props.onCancel && props.onCancel();
497
+ }
498
+ return React.createElement("div", {
499
+ className: "ssh-modal-root",
500
+ onMouseDown: function (e) {
501
+ if (e.target === e.currentTarget) cancel();
502
+ },
503
+ },
504
+ React.createElement("div", {
505
+ className: "ssh-modal",
506
+ role: "dialog",
507
+ "aria-modal": "true",
508
+ onMouseDown: function (e) { e.stopPropagation(); },
509
+ },
510
+ React.createElement("div", { className: "ssh-modal-title" }, title),
511
+ description ? React.createElement("div", { className: "ssh-modal-desc" }, description) : null,
512
+ mode === "prompt"
513
+ ? React.createElement("input", {
514
+ ref: inputRef,
515
+ className: "ssh-t-input",
516
+ value: value,
517
+ placeholder: props.placeholder || "",
518
+ onChange: function (e) { setValue(e.target.value); },
519
+ onKeyDown: function (e) {
520
+ if (e.key === "Enter") submit();
521
+ if (e.key === "Escape") cancel();
522
+ },
523
+ })
524
+ : null,
525
+ React.createElement("div", { className: "ssh-modal-actions" },
526
+ mode !== "alert"
527
+ ? React.createElement(Btn, { onClick: cancel }, cancelLabel)
528
+ : null,
529
+ React.createElement(Btn, {
530
+ primary: !danger,
531
+ danger: danger,
532
+ onClick: submit,
533
+ }, confirmLabel),
534
+ ),
535
+ ),
536
+ );
537
+ }
538
+
539
+
540
+ /** Full-screen center overlay host */
541
+ function CenterOverlay(props) {
542
+ const rootEl = React.createElement("div", {
543
+ className: "ssh-ov-root",
544
+ onMouseDown: function (e) {
545
+ if (e.target === e.currentTarget && props.onClose) props.onClose();
546
+ },
547
+ }, React.createElement("div", { className: "ssh-ov-panel", role: "dialog", "aria-modal": "true" }, props.children));
548
+ // Portal to <body>: the overlay relies on position:fixed being
549
+ // viewport-relative. better-sidebar >=0.16 gives its panel
550
+ // containers `contain: layout` (and panels animate with transform),
551
+ // which makes them the containing block for fixed descendants — an
552
+ // inline-rendered overlay would then be trapped inside the narrow
553
+ // sidebar instead of covering the screen. Portaling removes that
554
+ // dependency; fall back to inline rendering if react-dom is absent.
555
+ const rd = getReactDOM();
556
+ if (rd && typeof rd.createPortal === "function" && typeof document !== "undefined" && document.body) {
557
+ return rd.createPortal(rootEl, document.body);
558
+ }
559
+ return rootEl;
560
+ }
561
+
562
+ /** LA-like SSH terminal: xterm fills center, type directly */
563
+ function CenterBash(props) {
564
+ const { sessionId, projectPathKey, title, onClose, onOpenSftp } = props;
565
+ const hostRef = React.useRef(null);
566
+ const termRef = React.useRef(null);
567
+ const fitRef = React.useRef(null);
568
+ // Cursor into the host's {text,seq} chunk log: the last seq we rendered.
569
+ const sinceRef = React.useRef(0);
570
+ const [err, setErr] = React.useState("");
571
+ const [status, setStatus] = React.useState("loading");
572
+
573
+ React.useEffect(function () {
574
+ let cancelled = false;
575
+ let timer = null;
576
+ let ro = null;
577
+ let pollDelay = 120;
578
+ let inFlight = false;
579
+ // Terminal conditions carry structured codes from the host
580
+ // (API_ERROR_STATUS): the session was closed, expired, or its host
581
+ // grant was revoked. Everything else is transient: show it and
582
+ // back off instead of hammering a sick session every 120ms.
583
+ const isTerminalCode = function (e) {
584
+ return !!e && (e.code === "not_found" || e.code === "gone" || e.code === "forbidden");
585
+ };
586
+ const poll = async function () {
587
+ if (cancelled || !termRef.current || inFlight) return;
588
+ inFlight = true;
589
+ try {
590
+ const r = await api("shellRead", {
591
+ sessionId, projectPathKey, since: sinceRef.current,
592
+ });
593
+ if (cancelled || !termRef.current) return;
594
+ if (r.dropped) {
595
+ // Ring eviction swallowed output between our cursor and the
596
+ // retained base — a plain chunk would silently skip lines.
597
+ // Repaint from the retained base instead.
598
+ const resync = await api("shellRead", {
599
+ sessionId, projectPathKey, since: Math.max(0, (r.baseSeq || 1) - 1),
600
+ });
601
+ if (cancelled || !termRef.current) return;
602
+ try { termRef.current.clear(); } catch (e) {}
603
+ if (resync.chunk) termRef.current.write(resync.chunk);
604
+ sinceRef.current = resync.since != null ? resync.since : sinceRef.current;
605
+ } else {
606
+ if (r.chunk) termRef.current.write(r.chunk);
607
+ // Cursor = last seq actually delivered (`since`), not the
608
+ // buffer tail (`seq`): a chunkTruncated response continues
609
+ // from where this chunk stopped.
610
+ if (r.since != null) sinceRef.current = r.since;
611
+ else if (r.seq != null) sinceRef.current = r.seq;
612
+ if (r.chunkTruncated) console.debug("[dsh-ssh-tunnel] shell chunk hit the 256KiB cap; continuing next tick");
613
+ }
614
+ setErr("");
615
+ pollDelay = 120;
616
+ } catch (e) {
617
+ if (cancelled) return;
618
+ const msg = String(e && e.message ? e.message : e);
619
+ if (isTerminalCode(e)) {
620
+ stopPolling();
621
+ setErr(msg);
622
+ setStatus("dropped");
623
+ return;
624
+ }
625
+ setErr(msg);
626
+ pollDelay = Math.min(pollDelay * 2, 2000);
627
+ } finally {
628
+ inFlight = false;
629
+ }
630
+ if (!cancelled) timer = setTimeout(poll, pollDelay);
631
+ };
632
+ const stopPolling = function () {
633
+ if (timer) { clearTimeout(timer); timer = null; }
634
+ };
635
+ (async function () {
636
+ try {
637
+ const mods = await ensureXterm();
638
+ if (cancelled || !hostRef.current) return;
639
+ if (!mods.Terminal) throw new Error(t("xtermLoadFailed"));
640
+ // disposeVendor on a previous close removed the stylesheet and
641
+ // the cached ensureXterm() promise never re-runs loadCssOnce —
642
+ // re-attach it here (idempotent via the data-ssh-t marker).
643
+ if (typeof document !== "undefined") loadCssOnce("/dsh-ssh-tunnel/vendor/xterm.css");
644
+ // First frame comes from shellOpen itself (it returns the whole
645
+ // buffer plus the seq cursor) — no second full-buffer fetch.
646
+ const open = await api("shellOpen", { sessionId, projectPathKey });
647
+ if (cancelled || !hostRef.current) return;
648
+ const term = new mods.Terminal({
649
+ cursorBlink: true,
650
+ fontSize: 14,
651
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
652
+ theme: { background: "#0b0f14", foreground: "#d7e0ea", cursor: "#d7e0ea" },
653
+ convertEol: true,
654
+ scrollback: 5000,
655
+ });
656
+ let fit = null;
657
+ if (mods.FitAddon) {
658
+ fit = new mods.FitAddon();
659
+ term.loadAddon(fit);
660
+ }
661
+ term.open(hostRef.current);
662
+ if (fit) { try { fit.fit(); } catch (e) {} }
663
+ term.focus();
664
+ termRef.current = term;
665
+ fitRef.current = fit;
666
+ if (open.output) term.write(open.output);
667
+ sinceRef.current = open.seq != null ? open.seq : 0;
668
+ term.onData(function (data) {
669
+ api("shellWrite", { sessionId, projectPathKey, data: data }).catch(function (e) {
670
+ setErr(String(e && e.message ? e.message : e));
671
+ });
672
+ });
673
+ const sendSize = function () {
674
+ try {
675
+ if (fitRef.current) fitRef.current.fit();
676
+ } catch (e) {}
677
+ if (!termRef.current) return;
678
+ api("shellResize", {
679
+ sessionId, projectPathKey,
680
+ cols: termRef.current.cols,
681
+ rows: termRef.current.rows,
682
+ }).catch(function () {});
683
+ };
684
+ sendSize();
685
+ if (typeof ResizeObserver !== "undefined" && hostRef.current) {
686
+ ro = new ResizeObserver(sendSize);
687
+ ro.observe(hostRef.current);
688
+ }
689
+ window.addEventListener("resize", sendSize);
690
+ termRef.current._onWin = sendSize;
691
+ termRef.current._vendorDispose = mods.disposeVendor;
692
+ setStatus("connected");
693
+ timer = setTimeout(poll, pollDelay);
694
+ } catch (e) {
695
+ if (!cancelled) {
696
+ const msg = String(e && e.message ? e.message : e);
697
+ setErr(msg);
698
+ setStatus(isTerminalCode(e) || /not running|disconnected/i.test(msg) ? "dropped" : "error");
699
+ }
700
+ }
701
+ })();
702
+ return function () {
703
+ cancelled = true;
704
+ stopPolling();
705
+ if (ro) try { ro.disconnect(); } catch (e) {}
706
+ if (termRef.current) {
707
+ if (termRef.current._onWin) window.removeEventListener("resize", termRef.current._onWin);
708
+ if (termRef.current._vendorDispose) try { termRef.current._vendorDispose(); } catch (e) {}
709
+ try { termRef.current.dispose(); } catch (e) {}
710
+ termRef.current = null;
711
+ }
712
+ };
713
+ }, [sessionId, projectPathKey]);
714
+
715
+ return React.createElement(CenterOverlay, { onClose: onClose },
716
+ React.createElement("div", { className: "ssh-ov-bar" },
717
+ React.createElement("div", { className: "ssh-t-title" }, t("bashTitle", { name: title || sessionId.slice(0, 8) })),
718
+ React.createElement("div", { className: "ssh-ov-tabs" },
719
+ React.createElement("button", { type: "button", className: "on" }, t("bash")),
720
+ React.createElement("button", { type: "button", onClick: onOpenSftp }, t("sftp")),
721
+ ),
722
+ React.createElement(Btn, { onClick: onClose }, t("close")),
723
+ ),
724
+ React.createElement("div", { className: "ssh-ov-body" },
725
+ React.createElement("div", { className: "ssh-ov-xterm", ref: hostRef }),
726
+ ),
727
+ React.createElement("div", { className: "ssh-status" }, (status === "connected" ? t("bashStatusReady") : status === "loading" ? t("bashStatusLoading") : status === "dropped" ? t("sessionDropped") : t("bashStatusError")) + (err ? " · " + err : "")),
728
+ );
729
+ }
730
+
731
+ function FileList(props) {
732
+ const { entries, selected, onSelect, onActivate, onContext } = props;
733
+ return React.createElement("div", { className: "ssh-sftp-list", onContextMenu: function (e) {
734
+ // empty area
735
+ if (e.target === e.currentTarget) {
736
+ e.preventDefault();
737
+ onContext && onContext(e, null);
738
+ }
739
+ } },
740
+ entries.map(function (ent) {
741
+ const sel = selected.indexOf(ent.path) >= 0;
742
+ return React.createElement("div", {
743
+ key: ent.path,
744
+ className: "ssh-sftp-item" + (sel ? " sel" : ""),
745
+ onClick: function (e) { onSelect(ent, e); },
746
+ onDoubleClick: function () { onActivate(ent); },
747
+ onContextMenu: function (e) {
748
+ e.preventDefault();
749
+ e.stopPropagation();
750
+ onContext && onContext(e, ent);
751
+ },
752
+ draggable: true,
753
+ onDragStart: function (e) {
754
+ const paths = selected.indexOf(ent.path) >= 0 ? selected : [ent.path];
755
+ e.dataTransfer.setData("application/x-dsh-sftp", JSON.stringify({
756
+ side: props.side,
757
+ paths: paths,
758
+ }));
759
+ e.dataTransfer.effectAllowed = "copyMove";
760
+ },
761
+ },
762
+ React.createElement("span", null, ent.isDirectory ? "📁" : "📄"),
763
+ React.createElement("span", { className: "nm" }, ent.name),
764
+ React.createElement("span", { className: "sz" }, ent.isDirectory ? "" : formatSize(ent.size)),
765
+ );
766
+ }),
767
+ );
768
+ }
769
+
770
+ /** LA-like dual-pane SFTP */
771
+ function CenterSftp(props) {
772
+ const { sessionId, projectPathKey, title, onClose, onOpenBash } = props;
773
+ const [localPath, setLocalPath] = React.useState(projectPathKey || "");
774
+ const [remotePath, setRemotePath] = React.useState("/");
775
+ const [localEntries, setLocalEntries] = React.useState([]);
776
+ const [remoteEntries, setRemoteEntries] = React.useState([]);
777
+ const [localSel, setLocalSel] = React.useState([]);
778
+ const [remoteSel, setRemoteSel] = React.useState([]);
779
+ const [status, setStatus] = React.useState("");
780
+ const [err, setErr] = React.useState("");
781
+ const [ctx, setCtx] = React.useState(null); // {x,y,side,entry|null}
782
+ const [dialog, setDialog] = React.useState(null); // in-panel modal config
783
+ const dialogToken = React.useRef(0);
784
+ const localAnchor = React.useRef(null);
785
+ const remoteAnchor = React.useRef(null);
786
+
787
+ function openDialog(cfg) {
788
+ dialogToken.current += 1;
789
+ return new Promise(function (resolve) {
790
+ setDialog(Object.assign({}, cfg, {
791
+ token: dialogToken.current,
792
+ _resolve: resolve,
793
+ }));
794
+ });
795
+ }
796
+ function finishDialog(result) {
797
+ setDialog(function (cur) {
798
+ if (cur && cur._resolve) cur._resolve(result);
799
+ return null;
800
+ });
801
+ }
802
+ function askText(opts) {
803
+ return openDialog({
804
+ mode: "prompt",
805
+ title: opts.title,
806
+ description: opts.description || "",
807
+ defaultValue: opts.defaultValue || "",
808
+ placeholder: opts.placeholder || "",
809
+ confirmLabel: opts.confirmLabel || t("ok"),
810
+ }).then(function (v) {
811
+ if (v == null) return null;
812
+ const s = String(v).trim();
813
+ return s ? s : null;
814
+ });
815
+ }
816
+ function askConfirm(opts) {
817
+ return openDialog({
818
+ mode: "confirm",
819
+ title: opts.title,
820
+ description: opts.description || "",
821
+ confirmLabel: opts.confirmLabel || t("ok"),
822
+ danger: !!opts.danger,
823
+ }).then(function (v) { return !!v; });
824
+ }
825
+ function askAlert(opts) {
826
+ return openDialog({
827
+ mode: "alert",
828
+ title: opts.title,
829
+ description: opts.description || "",
830
+ confirmLabel: opts.confirmLabel || t("gotIt"),
831
+ });
832
+ }
833
+
834
+ const refreshLocal = React.useCallback(async function (p) {
835
+ const path = p != null ? p : localPath;
836
+ const r = await api("localList", { projectPathKey, path: path });
837
+ setLocalPath(r.path);
838
+ setLocalEntries(r.entries || []);
839
+ setLocalSel([]);
840
+ }, [localPath, projectPathKey]);
841
+
842
+ const refreshRemote = React.useCallback(async function (p) {
843
+ const path = p != null ? p : remotePath;
844
+ const r = await api("sftpList", { sessionId, projectPathKey, path: path });
845
+ setRemotePath(r.path || path);
846
+ setRemoteEntries(r.entries || []);
847
+ setRemoteSel([]);
848
+ }, [remotePath, sessionId, projectPathKey]);
849
+
850
+ React.useEffect(function () {
851
+ refreshLocal(projectPathKey || "").catch(function (e) {
852
+ setErr(String(e && e.message ? e.message : e));
853
+ });
854
+ refreshRemote("/").catch(function (e) {
855
+ const msg = String(e && e.message ? e.message : e);
856
+ setErr(/not running|disconnected/i.test(msg) ? t("sessionDropped") + " · " + msg : msg);
857
+ });
858
+ }, [sessionId, projectPathKey]); // eslint-disable-line
859
+
860
+ function selectHandler(side) {
861
+ return function (ent, e) {
862
+ const list = side === "local" ? localEntries : remoteEntries;
863
+ const setSel = side === "local" ? setLocalSel : setRemoteSel;
864
+ const sel = side === "local" ? localSel : remoteSel;
865
+ const anchorRef = side === "local" ? localAnchor : remoteAnchor;
866
+ if (e.shiftKey && anchorRef.current) {
867
+ const paths = list.map(function (x) { return x.path; });
868
+ const a = paths.indexOf(anchorRef.current);
869
+ const b = paths.indexOf(ent.path);
870
+ if (a >= 0 && b >= 0) {
871
+ const lo = Math.min(a, b), hi = Math.max(a, b);
872
+ setSel(paths.slice(lo, hi + 1));
873
+ return;
874
+ }
875
+ }
876
+ if (e.metaKey || e.ctrlKey) {
877
+ if (sel.indexOf(ent.path) >= 0) setSel(sel.filter(function (p) { return p !== ent.path; }));
878
+ else setSel(sel.concat([ent.path]));
879
+ anchorRef.current = ent.path;
880
+ return;
881
+ }
882
+ setSel([ent.path]);
883
+ anchorRef.current = ent.path;
884
+ };
885
+ }
886
+
887
+ function activate(side) {
888
+ return function (ent) {
889
+ if (ent.isDirectory) {
890
+ if (side === "local") refreshLocal(ent.path).catch(function (e) { setErr(String(e.message || e)); });
891
+ else refreshRemote(ent.path).catch(function (e) { setErr(String(e.message || e)); });
892
+ }
893
+ };
894
+ }
895
+
896
+ async function transfer(direction, paths) {
897
+ // direction: 'upload' local->remote, 'download' remote->local
898
+ setErr("");
899
+ setStatus(t("transferring"));
900
+ let skipped = 0;
901
+ try {
902
+ for (let i = 0; i < paths.length; i++) {
903
+ const src = paths[i];
904
+ const name = splitPath(src).pop();
905
+ if (direction === "upload") {
906
+ // skip dirs for v1 simple file upload
907
+ const ent = localEntries.find(function (e) { return e.path === src; });
908
+ if (ent && ent.isDirectory) {
909
+ skipped++;
910
+ continue;
911
+ }
912
+ const remote = joinPath(remotePath, name);
913
+ await api("sftpUpload", {
914
+ sessionId, projectPathKey,
915
+ local_path: src,
916
+ remote_path: remote,
917
+ });
918
+ } else {
919
+ const ent = remoteEntries.find(function (e) { return e.path === src; });
920
+ if (ent && ent.isDirectory) {
921
+ skipped++;
922
+ continue;
923
+ }
924
+ const local = joinPath(localPath, name);
925
+ await api("sftpDownload", {
926
+ sessionId, projectPathKey,
927
+ remote_path: src,
928
+ local_path: local,
929
+ });
930
+ }
931
+ }
932
+ await refreshLocal(localPath);
933
+ await refreshRemote(remotePath);
934
+ // C-09: a skipped directory used to overwrite the status with a
935
+ // per-item line and vanish; surface it as a final summary instead.
936
+ setStatus(skipped > 0 ? t("transferSkipped", { count: skipped }) : "");
937
+ } catch (e) {
938
+ setErr(String(e && e.message ? e.message : e));
939
+ setStatus("");
940
+ }
941
+ }
942
+
943
+ function onDropPane(side) {
944
+ return function (e) {
945
+ e.preventDefault();
946
+ let raw = e.dataTransfer.getData("application/x-dsh-sftp");
947
+ if (!raw) return;
948
+ let payload;
949
+ try { payload = JSON.parse(raw); } catch (err) { return; }
950
+ if (!payload || !payload.paths) return;
951
+ if (payload.side === side) return;
952
+ if (payload.side === "local" && side === "remote") transfer("upload", payload.paths);
953
+ if (payload.side === "remote" && side === "local") transfer("download", payload.paths);
954
+ };
955
+ }
956
+
957
+ async function ctxAction(action) {
958
+ if (!ctx) return;
959
+ const side = ctx.side;
960
+ const entry = ctx.entry;
961
+ const sel = side === "local" ? (localSel.length ? localSel : (entry ? [entry.path] : [])) : (remoteSel.length ? remoteSel : (entry ? [entry.path] : []));
962
+ setCtx(null);
963
+ try {
964
+ if (action === "open" && entry) {
965
+ activate(side)(entry);
966
+ return;
967
+ }
968
+ if (action === "upload" && side === "local") {
969
+ await transfer("upload", sel);
970
+ return;
971
+ }
972
+ if (action === "download" && side === "remote") {
973
+ await transfer("download", sel);
974
+ return;
975
+ }
976
+ if (action === "mkdir") {
977
+ const name = await askText({
978
+ title: t("mkdirTitle"),
979
+ description: t("mkdirLocation", { path: side === "local" ? localPath : remotePath }),
980
+ placeholder: t("mkdirPlaceholder"),
981
+ confirmLabel: t("create"),
982
+ });
983
+ if (!name) return;
984
+ if (side === "local") {
985
+ await api("localMkdir", { projectPathKey, path: joinPath(localPath, name) });
986
+ await refreshLocal(localPath);
987
+ } else {
988
+ await api("sftpMkdir", { sessionId, projectPathKey, path: joinPath(remotePath, name) });
989
+ await refreshRemote(remotePath);
990
+ }
991
+ return;
992
+ }
993
+ if (action === "rename" && entry) {
994
+ const name = await askText({
995
+ title: t("renameTitle"),
996
+ description: entry.path,
997
+ defaultValue: entry.name,
998
+ confirmLabel: t("save"),
999
+ });
1000
+ if (!name || name === entry.name) return;
1001
+ const dest = joinPath(side === "local" ? localPath : remotePath, name);
1002
+ if (side === "local") {
1003
+ await api("localRename", { projectPathKey, from_path: entry.path, to_path: dest });
1004
+ await refreshLocal(localPath);
1005
+ } else {
1006
+ await api("sftpRename", { sessionId, projectPathKey, from_path: entry.path, to_path: dest });
1007
+ await refreshRemote(remotePath);
1008
+ }
1009
+ return;
1010
+ }
1011
+ if (action === "delete") {
1012
+ if (!sel.length) return;
1013
+ const ok = await askConfirm({
1014
+ title: t("confirmDeleteTitle"),
1015
+ description: t("confirmDeleteDesc", { count: sel.length }),
1016
+ confirmLabel: t("delete"),
1017
+ danger: true,
1018
+ });
1019
+ if (!ok) return;
1020
+ for (let i = 0; i < sel.length; i++) {
1021
+ if (side === "local") await api("localDelete", { projectPathKey, path: sel[i] });
1022
+ else await api("sftpDelete", { sessionId, projectPathKey, path: sel[i] });
1023
+ }
1024
+ if (side === "local") await refreshLocal(localPath);
1025
+ else await refreshRemote(remotePath);
1026
+ return;
1027
+ }
1028
+ if (action === "copyPath" && entry) {
1029
+ try {
1030
+ await navigator.clipboard.writeText(entry.path);
1031
+ setStatus(t("copiedPath"));
1032
+ } catch (e) {
1033
+ await askAlert({ title: t("pathTitle"), description: entry.path });
1034
+ }
1035
+ }
1036
+ } catch (e) {
1037
+ setErr(String(e && e.message ? e.message : e));
1038
+ }
1039
+ }
1040
+
1041
+ function pane(side) {
1042
+ const isLocal = side === "local";
1043
+ const path = isLocal ? localPath : remotePath;
1044
+ const setPath = isLocal ? setLocalPath : setRemotePath;
1045
+ const entries = isLocal ? localEntries : remoteEntries;
1046
+ const sel = isLocal ? localSel : remoteSel;
1047
+ const refresh = isLocal ? refreshLocal : refreshRemote;
1048
+ return React.createElement("div", {
1049
+ className: "ssh-sftp-pane",
1050
+ onDragOver: function (e) { e.preventDefault(); },
1051
+ onDrop: onDropPane(side),
1052
+ },
1053
+ React.createElement("div", { className: "ssh-sftp-pane-hd" },
1054
+ React.createElement("strong", null, isLocal ? t("localProject") : t("remoteDevice")),
1055
+ React.createElement(Btn, { onClick: function () { refresh(parentOf(path)).catch(function (e) { setErr(String(e.message || e)); }); } }, t("parentDir")),
1056
+ React.createElement(Btn, { onClick: function () { refresh(path).catch(function (e) { setErr(String(e.message || e)); }); } }, t("refresh")),
1057
+ React.createElement("input", {
1058
+ className: "ssh-t-input",
1059
+ value: path,
1060
+ onChange: function (e) { setPath(e.target.value); },
1061
+ onKeyDown: function (e) {
1062
+ if (e.key === "Enter") refresh(path).catch(function (err) { setErr(String(err.message || err)); });
1063
+ },
1064
+ }),
1065
+ ),
1066
+ React.createElement(FileList, {
1067
+ side: side,
1068
+ entries: entries,
1069
+ selected: sel,
1070
+ onSelect: selectHandler(side),
1071
+ onActivate: activate(side),
1072
+ onContext: function (e, ent) {
1073
+ // ensure selection includes entry
1074
+ if (ent) {
1075
+ const cur = isLocal ? localSel : remoteSel;
1076
+ if (cur.indexOf(ent.path) < 0) {
1077
+ if (isLocal) setLocalSel([ent.path]);
1078
+ else setRemoteSel([ent.path]);
1079
+ }
1080
+ }
1081
+ setCtx({ x: e.clientX, y: e.clientY, side: side, entry: ent });
1082
+ },
1083
+ }),
1084
+ );
1085
+ }
1086
+
1087
+ return React.createElement(CenterOverlay, { onClose: onClose },
1088
+ React.createElement("div", { className: "ssh-ov-bar" },
1089
+ React.createElement("div", { className: "ssh-t-title" }, t("sftp") + " · " + (title || sessionId.slice(0, 8))),
1090
+ React.createElement("div", { className: "ssh-ov-tabs" },
1091
+ React.createElement("button", { type: "button", onClick: onOpenBash }, t("bash")),
1092
+ React.createElement("button", { type: "button", className: "on" }, t("sftp")),
1093
+ ),
1094
+ React.createElement(Btn, {
1095
+ primary: true,
1096
+ disabled: !localSel.length,
1097
+ onClick: function () { transfer("upload", localSel); },
1098
+ }, t("upload")),
1099
+ React.createElement(Btn, {
1100
+ primary: true,
1101
+ disabled: !remoteSel.length,
1102
+ onClick: function () { transfer("download", remoteSel); },
1103
+ }, t("download")),
1104
+ React.createElement(Btn, { onClick: onClose }, t("close")),
1105
+ ),
1106
+ React.createElement("div", { className: "ssh-ov-body ssh-sftp" },
1107
+ React.createElement("div", { className: "ssh-sftp-panes" },
1108
+ pane("local"),
1109
+ pane("remote"),
1110
+ ),
1111
+ React.createElement("div", { className: "ssh-status" },
1112
+ (status || t("sftpHint")) + (err ? " · " + err : ""),
1113
+ ),
1114
+ ),
1115
+ ctx ? React.createElement("div", {
1116
+ className: "ssh-ctx",
1117
+ style: { left: ctx.x, top: ctx.y },
1118
+ onMouseDown: function (e) { e.stopPropagation(); },
1119
+ },
1120
+ ctx.entry ? React.createElement("button", { type: "button", onClick: function () { ctxAction("open"); } }, t("open")) : null,
1121
+ ctx.side === "local" ? React.createElement("button", { type: "button", onClick: function () { ctxAction("upload"); } }, t("uploadToRemote")) : null,
1122
+ ctx.side === "remote" ? React.createElement("button", { type: "button", onClick: function () { ctxAction("download"); } }, t("downloadToLocal")) : null,
1123
+ React.createElement("button", { type: "button", onClick: function () { ctxAction("mkdir"); } }, t("newFolder")),
1124
+ ctx.entry ? React.createElement("button", { type: "button", onClick: function () { ctxAction("rename"); } }, t("rename")) : null,
1125
+ ctx.entry ? React.createElement("button", { type: "button", onClick: function () { ctxAction("copyPath"); } }, t("copyPath")) : null,
1126
+ React.createElement("hr"),
1127
+ React.createElement("button", { type: "button", className: "danger", onClick: function () { ctxAction("delete"); } }, t("delete")),
1128
+ ) : null,
1129
+ ctx ? React.createElement("div", {
1130
+ style: { position: "fixed", inset: 0, zIndex: 12999 },
1131
+ onMouseDown: function () { setCtx(null); },
1132
+ onContextMenu: function (e) { e.preventDefault(); setCtx(null); },
1133
+ }) : null,
1134
+ React.createElement(TextDialog, {
1135
+ open: !!dialog,
1136
+ token: dialog && dialog.token,
1137
+ mode: dialog && dialog.mode,
1138
+ title: dialog && dialog.title,
1139
+ description: dialog && dialog.description,
1140
+ defaultValue: dialog && dialog.defaultValue,
1141
+ placeholder: dialog && dialog.placeholder,
1142
+ confirmLabel: dialog && dialog.confirmLabel,
1143
+ danger: dialog && dialog.danger,
1144
+ onCancel: function () { finishDialog(null); },
1145
+ onConfirm: function (v) { finishDialog(v); },
1146
+ }),
1147
+ );
1148
+ }
1149
+
1150
+ function TunnelPanel(props) {
1151
+ const visible = props.visible;
1152
+ const scope = props.scope || {};
1153
+ const [view, setView] = React.useState("tunnel");
1154
+ const [projectPathKey, setProjectPathKey] = React.useState("");
1155
+ const [sessionId, setSessionId] = React.useState(scope.sessionId || "");
1156
+ const [hosts, setHosts] = React.useState([]);
1157
+ const [granted, setGranted] = React.useState([]);
1158
+ const [sessions, setSessions] = React.useState([]);
1159
+ const [prompts, setPrompts] = React.useState([]);
1160
+ const [scan, setScan] = React.useState(null);
1161
+ const [form, setForm] = React.useState(emptyForm());
1162
+ const [sftpOn, setSftpOn] = React.useState(true);
1163
+ const [busy, setBusy] = React.useState("");
1164
+ const [message, setMessage] = React.useState("");
1165
+ const [error, setError] = React.useState("");
1166
+ // center overlay state
1167
+ const [overlay, setOverlay] = React.useState(null); // {sessionId, title, mode:'bash'|'sftp'}
1168
+ // in-panel dialog (same machinery as CenterSftp) — replaces window.confirm
1169
+ const [dialog, setDialog] = React.useState(null);
1170
+ const dialogToken = React.useRef(0);
1171
+ function openDialog(cfg) {
1172
+ dialogToken.current += 1;
1173
+ return new Promise(function (resolve) {
1174
+ setDialog(Object.assign({}, cfg, { token: dialogToken.current, _resolve: resolve }));
1175
+ });
1176
+ }
1177
+ function finishDialog(result) {
1178
+ setDialog(function (cur) {
1179
+ if (cur && cur._resolve) cur._resolve(result);
1180
+ return null;
1181
+ });
1182
+ }
1183
+ async function askConfirm(opts) {
1184
+ const v = await openDialog({
1185
+ mode: "confirm",
1186
+ title: opts.title,
1187
+ description: opts.description || "",
1188
+ confirmLabel: opts.confirmLabel || t("ok"),
1189
+ danger: !!opts.danger,
1190
+ });
1191
+ return !!v;
1192
+ }
1193
+
1194
+ // scope.cwd is the real scope key better-sidebar passes; repoRoot is
1195
+ // the accepted alias. workspacePath was never provided by any host.
1196
+ const cwdGuess = scope.cwd || scope.repoRoot || "";
1197
+
1198
+ const refresh = React.useCallback(async () => {
1199
+ setError("");
1200
+ const ctx = await api("getProjectContext", {
1201
+ sessionId: scope.sessionId || sessionId || "",
1202
+ cwd: cwdGuess || undefined,
1203
+ });
1204
+ const key = ctx.projectPathKey || cwdGuess || "";
1205
+ setProjectPathKey(key);
1206
+ if (ctx.sessionId) setSessionId(ctx.sessionId);
1207
+ const [h, g, s, p] = await Promise.all([
1208
+ api("listHosts"),
1209
+ api("getGrants", { projectPathKey: key }),
1210
+ api("listSessions", { projectPathKey: key }),
1211
+ api("listPrompts", { projectPathKey: key }),
1212
+ ]);
1213
+ setHosts(h.hosts || []);
1214
+ setGranted(g.hostIds || []);
1215
+ setSessions(s.sessions || []);
1216
+ setPrompts(p.prompts || []);
1217
+ }, [scope.sessionId, cwdGuess, sessionId]);
1218
+
1219
+ React.useEffect(() => {
1220
+ if (!visible) return;
1221
+ refresh().catch(function (e) { setError(String(e && e.message ? e.message : e)); });
1222
+ const t = setInterval(function () {
1223
+ if (!visible) return;
1224
+ // Background polls log instead of swallowing: a broken host route
1225
+ // should be diagnosable from the console (C-23) while the UI
1226
+ // keeps the last known state.
1227
+ api("listSessions", { projectPathKey: projectPathKey || cwdGuess })
1228
+ .then(function (s) { setSessions(s.sessions || []); })
1229
+ .catch(function (e) { console.warn("[dsh-ssh-tunnel] session poll failed:", e && e.message ? e.message : e); });
1230
+ api("listPrompts", { projectPathKey: projectPathKey || cwdGuess })
1231
+ .then(function (p) { setPrompts(p.prompts || []); })
1232
+ .catch(function (e) { console.warn("[dsh-ssh-tunnel] prompt poll failed:", e && e.message ? e.message : e); });
1233
+ }, 4000);
1234
+ return function () { clearInterval(t); };
1235
+ }, [visible, refresh, projectPathKey, cwdGuess]);
1236
+
1237
+ function patchForm(p) {
1238
+ setForm(function (prev) { return Object.assign({}, prev, p); });
1239
+ }
1240
+ async function run(name, fn) {
1241
+ setBusy(name); setMessage(""); setError("");
1242
+ try { await fn(); setMessage(""); await refresh(); }
1243
+ catch (e) { setError(String(e && e.message ? e.message : e)); }
1244
+ finally { setBusy(""); }
1245
+ }
1246
+ function toggleGrant(id) {
1247
+ setGranted(granted.includes(id) ? granted.filter(function (x) { return x !== id; }) : granted.concat([id]));
1248
+ }
1249
+ function openCenter(sess, mode) {
1250
+ setOverlay({
1251
+ sessionId: sess.session_id,
1252
+ title: sess.title || sess.endpoint || sess.session_id,
1253
+ mode: mode,
1254
+ });
1255
+ }
1256
+
1257
+ const tabs = [
1258
+ { id: "grants", label: t("tabGrants") },
1259
+ { id: "hosts", label: t("tabHosts") },
1260
+ { id: "tunnel", label: t("tabTunnel") },
1261
+ ];
1262
+
1263
+ const overlayEl = overlay
1264
+ ? (overlay.mode === "bash"
1265
+ ? React.createElement(CenterBash, {
1266
+ sessionId: overlay.sessionId,
1267
+ projectPathKey: projectPathKey,
1268
+ title: overlay.title,
1269
+ onClose: function () { setOverlay(null); },
1270
+ onOpenSftp: function () { setOverlay(Object.assign({}, overlay, { mode: "sftp" })); },
1271
+ })
1272
+ : React.createElement(CenterSftp, {
1273
+ sessionId: overlay.sessionId,
1274
+ projectPathKey: projectPathKey,
1275
+ title: overlay.title,
1276
+ onClose: function () { setOverlay(null); },
1277
+ onOpenBash: function () { setOverlay(Object.assign({}, overlay, { mode: "bash" })); },
1278
+ }))
1279
+ : null;
1280
+
1281
+ return React.createElement(React.Fragment, null,
1282
+ overlayEl,
1283
+ React.createElement("div", { className: "ssh-t-root" },
1284
+ React.createElement("div", { className: "ssh-t-head" },
1285
+ React.createElement("div", null,
1286
+ React.createElement("div", { className: "ssh-t-title" }, t("tabTitle")),
1287
+ React.createElement("div", { className: "ssh-t-sub" }, t("projectLabel", { path: projectPathKey || t("projectUnbound") })),
1288
+ ),
1289
+ React.createElement(Btn, { disabled: !!busy, onClick: function () {
1290
+ refresh().catch(function (e) { setError(String(e.message || e)); });
1291
+ } }, t("refresh")),
1292
+ ),
1293
+ React.createElement("div", { className: "ssh-t-tabs" },
1294
+ tabs.map(function (t) {
1295
+ return React.createElement("button", {
1296
+ key: t.id, type: "button",
1297
+ className: "ssh-t-tab" + (view === t.id || (view === "edit" && t.id === "hosts") ? " on" : ""),
1298
+ onClick: function () { setView(t.id); },
1299
+ }, t.label);
1300
+ }),
1301
+ ),
1302
+ prompts && prompts.length ? React.createElement("div", { className: "ssh-t-prompt" },
1303
+ React.createElement("div", { style: { fontWeight: 600, marginBottom: 6 } }, t("needConfirm")),
1304
+ prompts.map(function (p) {
1305
+ const changed = p.kind === "host_key_changed";
1306
+ const newFp = p.fingerprint
1307
+ ? ((p.fingerprintAlgo || "SHA256") + ":" + String(p.fingerprint))
1308
+ : "";
1309
+ const monoStyle = {
1310
+ marginTop: 6, wordBreak: "break-all",
1311
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: 11,
1312
+ };
1313
+ // Structured render: kind label + endpoint + old/new
1314
+ // fingerprints from fields. The host `message` string embeds
1315
+ // the new fingerprint too — showing both duplicated it.
1316
+ return React.createElement("div", { key: p.promptId, style: { marginBottom: 8 } },
1317
+ React.createElement("div", null,
1318
+ (changed ? t("promptKeyChanged") : t("promptKeyUnknown"))
1319
+ + " · " + p.host + ":" + (p.port || 22)),
1320
+ newFp
1321
+ ? React.createElement("div", { className: "ssh-t-muted", style: monoStyle }, t("promptFpNew") + ": " + newFp)
1322
+ : null,
1323
+ changed && p.previous
1324
+ ? React.createElement("div", { className: "ssh-t-muted", style: monoStyle }, t("promptFpOld") + ": SHA256:" + String(p.previous))
1325
+ : null,
1326
+ React.createElement("div", { className: "ssh-t-actions" },
1327
+ React.createElement(Btn, { primary: true, disabled: !!busy, onClick: function () {
1328
+ run("trust", async function () {
1329
+ const r = await api("answerPrompt", {
1330
+ promptId: p.promptId, trustHostKey: true, connectAfter: true,
1331
+ projectPathKey: projectPathKey, sftpEnabled: sftpOn,
1332
+ });
1333
+ // C-08: "trusted but not connected" must not look
1334
+ // like a failed trust — say both parts explicitly.
1335
+ if (r && r.trusted && r.connected === false && r.error) {
1336
+ throw new Error(t("trustedNotConnected") + " " + String(r.error));
1337
+ }
1338
+ if (r && r.connected === false && r.error) throw new Error(String(r.error));
1339
+ });
1340
+ } }, t("trustConnect")),
1341
+ React.createElement(Btn, { disabled: !!busy, onClick: function () {
1342
+ run("dismiss", async function () {
1343
+ // No trustHostKey: the host drops the prompt and
1344
+ // stores nothing.
1345
+ await api("answerPrompt", { promptId: p.promptId });
1346
+ });
1347
+ } }, t("dismissPrompt")),
1348
+ ),
1349
+ );
1350
+ }),
1351
+ ) : null,
1352
+ view === "grants" ? React.createElement("div", { className: "ssh-t-card" },
1353
+ React.createElement("div", { className: "ssh-t-muted", style: { marginBottom: 8 } }, t("grantsHint")),
1354
+ hosts.map(function (h) {
1355
+ return React.createElement("label", { key: h.id, className: "ssh-t-check" },
1356
+ React.createElement("input", {
1357
+ type: "checkbox", checked: granted.includes(h.id),
1358
+ onChange: function () { toggleGrant(h.id); },
1359
+ }),
1360
+ React.createElement("span", null,
1361
+ React.createElement("span", { className: "ssh-t-name" }, h.name || h.id),
1362
+ React.createElement("span", { className: "ssh-t-endpoint" },
1363
+ h.username + "@" + h.host + ":" + (h.port || 22) + " · " + h.credentialStatus,
1364
+ ),
1365
+ ),
1366
+ );
1367
+ }),
1368
+ React.createElement("div", { className: "ssh-t-actions" },
1369
+ React.createElement(Btn, { primary: true, disabled: !!busy || !projectPathKey, onClick: function () {
1370
+ run("grants", async function () {
1371
+ await api("setGrants", { projectPathKey: projectPathKey, hostIds: granted });
1372
+ });
1373
+ } }, t("saveGrants")),
1374
+ ),
1375
+ ) : null,
1376
+ view === "hosts" ? React.createElement("div", { className: "ssh-t-card" },
1377
+ React.createElement("div", { className: "ssh-t-actions", style: { marginTop: 0 } },
1378
+ React.createElement(Btn, { primary: true, onClick: function () { setForm(emptyForm()); setView("edit"); } }, t("newHost")),
1379
+ React.createElement(Btn, { disabled: !!busy, onClick: function () {
1380
+ run("scan", async function () { setScan(await api("scanOpenSsh")); });
1381
+ } }, t("scanOpenSsh")),
1382
+ ),
1383
+ hosts.map(function (h) {
1384
+ return React.createElement("div", { key: h.id, className: "ssh-t-row" },
1385
+ React.createElement("div", { className: "ssh-t-row-main" },
1386
+ React.createElement("div", { className: "ssh-t-name" }, h.name || h.id),
1387
+ React.createElement("div", { className: "ssh-t-endpoint", title: h.username + "@" + h.host + ":" + h.port },
1388
+ h.username + "@" + h.host + ":" + h.port,
1389
+ ),
1390
+ ),
1391
+ React.createElement("div", { className: "ssh-t-actions" },
1392
+ React.createElement(Btn, { onClick: function () {
1393
+ setForm({
1394
+ id: h.id, name: h.name || "", host: h.host || "", port: h.port || 22,
1395
+ username: h.username || "",
1396
+ // C-19: a legacy keyboard-interactive host must not look
1397
+ // like an ordinary password host. Keep the retirement
1398
+ // visible with an explicit banner on the edit form.
1399
+ authType: h.authType === "keyboardInteractive" ? "password" : (h.authType || "privateKey"),
1400
+ legacyKbi: h.authType === "keyboardInteractive",
1401
+ privateKeyPath: h.privateKeyPath || "", password: "", privateKeyPem: "", passphrase: "",
1402
+ });
1403
+ setView("edit");
1404
+ } }, t("edit")),
1405
+ React.createElement(Btn, { danger: true, onClick: async function () {
1406
+ const ok = await askConfirm({
1407
+ title: t("confirmDeleteTitle"),
1408
+ description: t("deleteHostConfirm", { name: h.name || h.id }),
1409
+ confirmLabel: t("delete"),
1410
+ danger: true,
1411
+ });
1412
+ if (!ok) return;
1413
+ run("del", async function () { await api("deleteHost", { id: h.id }); });
1414
+ } }, t("delete")),
1415
+ ),
1416
+ );
1417
+ }),
1418
+ scan ? React.createElement("div", { style: { marginTop: 12 } },
1419
+ (scan.configs || []).map(function (c, i) {
1420
+ return React.createElement("div", { key: i, className: "ssh-t-row" },
1421
+ React.createElement("div", { className: "ssh-t-row-main" },
1422
+ React.createElement("div", { className: "ssh-t-name" }, c.name || c.host),
1423
+ React.createElement("div", { className: "ssh-t-endpoint", title: c.host },
1424
+ (c.username ? c.username + "@" : "") + c.host + ":" + (c.port || 22),
1425
+ ),
1426
+ c.privateKeyPath
1427
+ ? React.createElement("div", { className: "ssh-t-endpoint" }, t("privateKeyPath") + ": " + c.privateKeyPath)
1428
+ : null,
1429
+ ),
1430
+ React.createElement("div", { className: "ssh-t-actions" },
1431
+ React.createElement(Btn, { onClick: function () {
1432
+ run("imp", async function () { await api("importScan", { entry: c }); });
1433
+ } }, t("import")),
1434
+ ),
1435
+ );
1436
+ }),
1437
+ ) : null,
1438
+ ) : null,
1439
+ view === "edit" ? React.createElement("div", { className: "ssh-t-card" },
1440
+ React.createElement(Field, { label: t("name") }, React.createElement("input", { className: "ssh-t-input", value: form.name, onChange: function (e) { patchForm({ name: e.target.value }); } })),
1441
+ React.createElement(Field, { label: t("host") }, React.createElement("input", { className: "ssh-t-input", value: form.host, onChange: function (e) { patchForm({ host: e.target.value }); } })),
1442
+ React.createElement(Field, { label: t("port") }, React.createElement("input", { className: "ssh-t-input", type: "number", value: form.port, onChange: function (e) { patchForm({ port: e.target.value }); } })),
1443
+ React.createElement(Field, { label: t("username") }, React.createElement("input", { className: "ssh-t-input", value: form.username, onChange: function (e) { patchForm({ username: e.target.value }); } })),
1444
+ React.createElement(Field, { label: t("authType") },
1445
+ React.createElement("select", { className: "ssh-t-select", value: form.authType, onChange: function (e) { patchForm({ authType: e.target.value, legacyKbi: false }); } },
1446
+ React.createElement("option", { value: "privateKey" }, t("authPrivateKey")),
1447
+ React.createElement("option", { value: "password" }, t("authPassword")),
1448
+ ),
1449
+ ),
1450
+ form.authType === "privateKey" ? React.createElement(React.Fragment, null,
1451
+ React.createElement(Field, { label: t("privateKeyPath") }, React.createElement("input", { className: "ssh-t-input", value: form.privateKeyPath, onChange: function (e) { patchForm({ privateKeyPath: e.target.value }); } })),
1452
+ React.createElement(Field, { label: t("privateKeyPem") }, React.createElement("textarea", { className: "ssh-t-textarea", rows: 3, value: form.privateKeyPem, onChange: function (e) { patchForm({ privateKeyPem: e.target.value }); } })),
1453
+ React.createElement(Field, { label: t("passphrase") }, React.createElement("input", { className: "ssh-t-input", type: "password", value: form.passphrase, onChange: function (e) { patchForm({ passphrase: e.target.value }); } })),
1454
+ ) : null,
1455
+ form.authType === "password" ? React.createElement(Field, { label: t("password") }, React.createElement("input", { className: "ssh-t-input", type: "password", value: form.password, onChange: function (e) { patchForm({ password: e.target.value }); } })) : null,
1456
+ form.legacyKbi ? React.createElement("div", { className: "ssh-t-err" }, t("kbiEditNotice")) : null,
1457
+ React.createElement("div", { className: "ssh-t-actions" },
1458
+ React.createElement(Btn, { primary: true, disabled: !!busy, onClick: function () {
1459
+ run("save", async function () {
1460
+ const payload = {
1461
+ id: form.id || undefined, name: form.name, host: form.host,
1462
+ port: Number(form.port) || 22, username: form.username, authType: form.authType,
1463
+ privateKeyPath: form.privateKeyPath,
1464
+ };
1465
+ if (form.password) payload.password = form.password;
1466
+ if (form.privateKeyPem) payload.privateKeyPem = form.privateKeyPem;
1467
+ if (form.passphrase) payload.passphrase = form.passphrase;
1468
+ await api("saveHost", { host: payload });
1469
+ setView("hosts");
1470
+ });
1471
+ } }, t("save")),
1472
+ React.createElement(Btn, { onClick: function () { setView("hosts"); } }, t("back")),
1473
+ ),
1474
+ ) : null,
1475
+ view === "tunnel" ? React.createElement("div", { className: "ssh-t-card" },
1476
+ React.createElement("label", { className: "ssh-t-check" },
1477
+ React.createElement("input", { type: "checkbox", checked: sftpOn, onChange: function (e) { setSftpOn(!!e.target.checked); } }),
1478
+ React.createElement("span", null, t("sftpOnConnect")),
1479
+ ),
1480
+ React.createElement("div", { style: { fontWeight: 600, margin: "8px 0" } }, t("connectSection")),
1481
+ hosts.filter(function (h) { return granted.includes(h.id); }).map(function (h) {
1482
+ return React.createElement("div", { key: h.id, className: "ssh-t-row" },
1483
+ React.createElement("div", { className: "ssh-t-row-main" },
1484
+ React.createElement("div", { className: "ssh-t-name" }, h.name || h.id),
1485
+ React.createElement("div", { className: "ssh-t-endpoint", title: h.username + "@" + h.host + ":" + (h.port || 22) },
1486
+ h.username + "@" + h.host + ":" + (h.port || 22),
1487
+ ),
1488
+ ),
1489
+ React.createElement("div", { className: "ssh-t-actions" },
1490
+ React.createElement(Btn, { primary: true, disabled: !!busy || !projectPathKey, onClick: function () {
1491
+ run("connect", async function () {
1492
+ if (h.authType === "keyboardInteractive") {
1493
+ throw new Error(t("kbiUnsupported"));
1494
+ }
1495
+ try {
1496
+ await api("connect", {
1497
+ hostId: h.id, projectPathKey: projectPathKey,
1498
+ sftpEnabled: sftpOn, trustHostKey: false,
1499
+ });
1500
+ } catch (e) {
1501
+ // A host-key prompt arrives as ok:false + promptId
1502
+ // (HTTP 200): surface it and refresh the card. The
1503
+ // fallback copy covers hosts that omit `message`.
1504
+ if (e && e.data && e.data.promptId) {
1505
+ setMessage(e.data.message || t("needHostKey"));
1506
+ setPrompts((await api("listPrompts", { projectPathKey: projectPathKey })).prompts || []);
1507
+ return;
1508
+ }
1509
+ throw e;
1510
+ }
1511
+ });
1512
+ } }, t("connect")),
1513
+ ),
1514
+ );
1515
+ }),
1516
+ React.createElement("div", { style: { fontWeight: 600, margin: "12px 0 6px" } }, t("sessionsSection")),
1517
+ sessions.length === 0
1518
+ ? React.createElement("div", { className: "ssh-t-muted" }, t("noSessions"))
1519
+ : sessions.map(function (s) {
1520
+ const live = s.running && s.status === "connected";
1521
+ const reconnecting = s.status === "reconnecting";
1522
+ const statusLabel = reconnecting
1523
+ ? t("reconnecting", { attempt: s.reconnect_attempt || 1, max: s.reconnect_max_attempts || 3 })
1524
+ : (live ? t("connected") : t("disconnected"));
1525
+ return React.createElement("div", { key: s.session_id, className: "ssh-t-row" },
1526
+ React.createElement("div", { className: "ssh-t-row-main" },
1527
+ React.createElement("div", { className: "ssh-t-name" }, s.title || s.session_id),
1528
+ React.createElement("div", { className: "ssh-t-endpoint" },
1529
+ (s.endpoint ? s.endpoint + " · " : "") + statusLabel + " · sftp=" + String(s.sftpEnabled),
1530
+ ),
1531
+ ),
1532
+ React.createElement("div", { className: "ssh-t-actions" },
1533
+ React.createElement(Btn, { primary: true, disabled: !live, onClick: function () { openCenter(s, "bash"); } }, t("terminal")),
1534
+ React.createElement(Btn, { disabled: !live || !s.sftpEnabled, onClick: function () { openCenter(s, "sftp"); } }, t("sftp")),
1535
+ !live && !reconnecting ? React.createElement(Btn, { primary: true, disabled: !!busy || !projectPathKey, onClick: function () {
1536
+ run("reconnect", async function () {
1537
+ try {
1538
+ await api("reconnect", { sessionId: s.session_id, projectPathKey: projectPathKey });
1539
+ } catch (e) {
1540
+ const msg = String(e && e.message ? e.message : e);
1541
+ if (e && e.data && e.data.promptId) {
1542
+ setMessage(e.data.message || t("needHostKey"));
1543
+ setPrompts((await api("listPrompts", { projectPathKey: projectPathKey })).prompts || []);
1544
+ return;
1545
+ }
1546
+ if (/keyboard-interactive/i.test(msg)) throw new Error(t("kbiUnsupported"));
1547
+ throw e;
1548
+ }
1549
+ });
1550
+ } }, t("reconnect")) : null,
1551
+ React.createElement(Btn, { danger: true, onClick: function () {
1552
+ run("disc", async function () {
1553
+ if (overlay && overlay.sessionId === s.session_id) setOverlay(null);
1554
+ await api("disconnect", { sessionId: s.session_id, projectPathKey: projectPathKey });
1555
+ });
1556
+ } }, live ? t("disconnect") : t("closeSession")),
1557
+ ),
1558
+ );
1559
+ }),
1560
+ ) : null,
1561
+ message ? React.createElement("div", { className: "ssh-t-ok" }, message) : null,
1562
+ error ? React.createElement("div", { className: "ssh-t-err" }, error) : null,
1563
+ React.createElement("div", { className: "ssh-t-muted" },
1564
+ t("footerHint"),
1565
+ ),
1566
+ React.createElement(TextDialog, {
1567
+ open: !!dialog,
1568
+ token: dialog && dialog.token,
1569
+ mode: dialog && dialog.mode,
1570
+ title: dialog && dialog.title,
1571
+ description: dialog && dialog.description,
1572
+ confirmLabel: dialog && dialog.confirmLabel,
1573
+ danger: dialog && dialog.danger,
1574
+ onCancel: function () { finishDialog(null); },
1575
+ onConfirm: function (v) { finishDialog(v); },
1576
+ }),
1577
+ ),
1578
+ );
1579
+ }
1580
+
1581
+ function TunnelRoot(props) {
1582
+ const ctx = props.ctx;
1583
+ // Re-render on DSH locale switches (better-sidebar pattern).
1584
+ const localeKey = React.useSyncExternalStore(
1585
+ React.useCallback(function (cb) {
1586
+ if (!ctx.locale || typeof ctx.locale.subscribe !== "function") return function () {};
1587
+ return ctx.locale.subscribe(cb);
1588
+ }, [ctx]),
1589
+ React.useCallback(function () {
1590
+ try {
1591
+ return ctx.locale && ctx.locale.getSnapshot ? ctx.locale.getSnapshot().active : activeLocale();
1592
+ } catch (e) {
1593
+ return activeLocale();
1594
+ }
1595
+ }, [ctx]),
1596
+ function () { return "en"; },
1597
+ );
1598
+ return React.createElement(TunnelPanel, {
1599
+ key: "ssh-tunnel-" + String(localeKey || "en"),
1600
+ visible: props.visible,
1601
+ scope: props.scope,
1602
+ });
1603
+ }
1604
+
1605
+ const inject = ["betterSidebar", "locale"];
1606
+ function apply(ctx) {
1607
+ ctx.effect(function () {
1608
+ return ensureStyles();
1609
+ }, "dsh-ssh-tunnel: styles");
1610
+ if (ctx.locale) {
1611
+ attachLocale(ctx.locale);
1612
+ ctx.effect(function () {
1613
+ const offZh = ctx.locale.register(LOCALE_NS, "zh", zh);
1614
+ const offEn = ctx.locale.register(LOCALE_NS, "en", en);
1615
+ return function () {
1616
+ try { offZh(); } catch (e) {}
1617
+ try { offEn(); } catch (e) {}
1618
+ };
1619
+ }, "dsh-ssh-tunnel: dictionaries");
1620
+ }
1621
+ if (!ctx.betterSidebar) return;
1622
+ ctx.effect(function () {
1623
+ return ctx.betterSidebar.registerTab({
1624
+ id: TAB_ID,
1625
+ title: function () { return t("tabTitle"); },
1626
+ icon: function (size) { return icon(size); },
1627
+ order: 44,
1628
+ single: true,
1629
+ component: function (p) {
1630
+ return React.createElement(TunnelRoot, {
1631
+ ctx: ctx,
1632
+ visible: p.visible,
1633
+ scope: p.scope,
1634
+ });
1635
+ },
1636
+ });
1637
+ }, "dsh-ssh-tunnel: register tab");
1638
+ }
1639
+ exports.apply = apply;
1640
+ exports.inject = inject;
1641
+ return module.exports;
1642
+ },
1643
+ });