dsh-ssh-tunnel 1.0.0

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 ADDED
@@ -0,0 +1,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
+ 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
+ });