@goodandready/dsh-remote-workspace 0.3.1 → 0.3.3

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,2179 +1,2322 @@
1
- // Remote Workspace Settings Card (settings.plugin.item) & Session Utility Chip.
2
- window.__ModuleLoader__.load({
3
- id: '@goodandready/dsh-remote-workspace',
4
- factory: (require) => {
5
- var module = { exports: {} }
6
- var exports = module.exports
7
- const React = require('react')
8
-
9
- const NS = 'dsh-remote-workspace'
10
-
11
- function ensureCss() {
12
- if (typeof document === 'undefined') return
13
- if (document.getElementById('drw-clinebot-full-css')) return
14
- const style = document.createElement('style')
15
- style.id = 'drw-clinebot-full-css'
16
- style.dataset.dshPlugin = NS
17
- style.textContent = `
18
- .drw-page{display:flex;flex-direction:column;gap:18px;padding:6px 0 24px;max-width:960px}
19
- .drw-header{display:flex;flex-direction:column;gap:8px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}
20
- .drw-header-top{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px}
21
- .drw-page-title{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px}
22
- .drw-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}
23
- .drw-header-badges{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
24
-
25
- .drw-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:14px;list-style:none}
26
- .drw-card-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}
27
- .drw-card-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}
28
-
29
- .drw-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
30
- .drw-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:12px}
31
- .drw-grid-4{display:grid;grid-template-columns:repeat(auto-fit, minmax(190px, 1fr));gap:10px}
32
-
33
- .drw-badge{font-size:11px;padding:3px 9px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500;font-family:monospace}
34
- .drw-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}
35
- .drw-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}
36
- .drw-badge-err{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}
37
-
38
- .drw-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;width:100%;box-sizing:border-box}
39
- .drw-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
40
- .drw-label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary);margin-bottom:2px}
41
- .drw-hint{font-size:11px;color:var(--dsw-alias-label-dimmed, var(--dsw-alias-label-secondary));line-height:1.3}
42
- .drw-field{display:flex;flex-direction:column;gap:3px;flex:1;min-width:180px}
43
-
44
- .drw-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
45
- .drw-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
46
- .drw-btn:disabled{opacity:0.6;cursor:not-allowed}
47
- .drw-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
48
- .drw-btn-primary:hover:not(:disabled){opacity:0.9}
49
- .drw-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
50
- .drw-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12);border-color:rgba(239,68,68,0.5)}
51
-
52
- .drw-segmented{display:inline-flex;border-radius:8px;padding:2px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);gap:2px}
53
- .drw-segmented-item{appearance:none;border:none;background:none;cursor:pointer;padding:5px 12px;font-size:12px;font-weight:500;border-radius:6px;color:var(--dsw-alias-label-secondary);transition:all .15s ease}
54
- .drw-segmented-item-active{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgba(0,0,0,0.08);font-weight:600}
55
-
56
- .drw-table{width:100%;border-collapse:collapse;margin-top:4px}
57
- .drw-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:600}
58
- .drw-table td{padding:9px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;color:var(--dsw-alias-label-primary)}
59
- .drw-table tr:hover{background:var(--dsw-alias-bg-layer-2)}
60
-
61
- .drw-stat-box{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:3px}
62
- .drw-stat-val{font-size:14px;font-weight:700;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
63
- .drw-stat-lbl{font-size:11px;color:var(--dsw-alias-label-secondary)}
64
- .drw-preview{padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-family:monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--dsw-alias-border-l2);line-height:1.4}
65
-
66
- .drw-browser-modal{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:12px;display:flex;flex-direction:column;gap:8px;max-height:280px}
67
- .drw-browser-list{overflow-y:auto;display:flex;flex-direction:column;gap:2px;max-height:190px;padding-right:4px}
68
- .drw-browser-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;cursor:pointer;font-size:13px;font-family:monospace}
69
- .drw-browser-item:hover{background:var(--dsw-alias-bg-layer-3)}
70
-
71
- .drw-tabs{display:flex;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:10px;margin-bottom:14px;overflow-x:auto}
72
- .drw-tab-btn{appearance:none;border:1px solid transparent;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);padding:6px 14px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .15s ease}
73
- .drw-tab-btn:hover{background:var(--dsw-alias-bg-layer-4);color:var(--dsw-alias-label-primary)}
74
- .drw-tab-btn.active{background:var(--dsw-alias-bg-layer-4);border-color:var(--dsw-alias-state-brand-primary);color:var(--dsw-alias-state-brand-primary)}
75
-
76
- .drw-term-window{background:#0d1117;border:1px solid #30363d;border-radius:10px;display:flex;flex-direction:column;height:340px;overflow:hidden;box-shadow:0 4px 14px rgba(0,0,0,0.25)}
77
- .drw-term-header{background:#161b22;padding:6px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #30363d;font-size:12px;color:#8b949e;font-family:monospace}
78
- .drw-term-body{flex:1;padding:10px 12px;overflow-y:auto;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12.5px;line-height:1.45;color:#c9d1d9;white-space:pre-wrap;word-break:break-all}
79
- .drw-term-footer{display:flex;background:#161b22;border-top:1px solid #30363d;padding:6px}
80
- .drw-term-input{flex:1;background:transparent;border:none;color:#58a6ff;font-family:monospace;font-size:13px;padding:4px 8px;outline:none}
81
-
82
- .drw-explorer-box{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:12px;display:flex;flex-direction:column;gap:10px}
83
- .drw-explorer-nav{display:flex;align-items:center;gap:8px;font-family:monospace;font-size:12px;background:var(--dsw-alias-bg-layer-1);padding:6px 10px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2)}
84
- .drw-file-list{display:flex;flex-direction:column;gap:3px;max-height:260px;overflow-y:auto}
85
- .drw-file-row{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-radius:6px;cursor:pointer;font-size:13px;font-family:monospace;transition:background .1s ease}
86
- .drw-file-row:hover{background:var(--dsw-alias-bg-layer-3)}
87
- .drw-file-meta{display:flex;align-items:center;gap:12px;color:var(--dsw-alias-label-secondary);font-size:11.5px}
88
-
89
- .drw-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px}
90
- .drw-modal-dialog{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;width:100%;max-width:800px;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 12px 32px rgba(0,0,0,0.4)}
91
- .drw-modal-hdr{padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);display:flex;align-items:center;justify-content:space-between;font-weight:600}
92
- .drw-modal-body{flex:1;padding:12px 16px;overflow-y:auto}
93
-
94
- .drw-health-bar{display:grid;grid-template-columns:repeat(auto-fit, minmax(130px, 1fr));gap:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;font-size:11.5px;font-family:monospace}
95
- .drw-health-item{display:flex;flex-direction:column;gap:2px}
96
- .drw-progress{height:6px;background:var(--dsw-alias-bg-layer-1);border-radius:3px;overflow:hidden}
97
- .drw-progress-fill{height:100%;border-radius:3px;transition:width .2s ease}
98
-
99
- .drw-container-row{display:grid;grid-template-columns:2fr 2fr 1.5fr 1.5fr auto;align-items:center;gap:10px;padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12.5px;font-family:monospace}
100
- .drw-container-row:hover{background:var(--dsw-alias-bg-layer-3)}
101
-
102
- .drw-env-table{width:100%;border-collapse:collapse}
103
- .drw-env-row{display:grid;grid-template-columns:220px 1fr 90px;gap:8px;padding:6px 0;align-items:center;border-bottom:1px solid var(--dsw-alias-border-l2)}
104
- .drw-alert-badge{padding:4px 8px;border-radius:6px;font-size:11px;font-family:monospace;display:flex;align-items:center;gap:6px}
105
- .drw-alert-critical{background:rgba(239,68,68,0.15);color:var(--dsw-alias-state-error-primary);border:1px solid rgba(239,68,68,0.4)}
106
- .drw-alert-warning{background:rgba(245,158,11,0.15);color:var(--dsw-alias-state-warning-primary);border:1px solid rgba(245,158,11,0.4)}
107
-
108
-
109
- `
110
- document.head.appendChild(style)
111
- }
112
-
113
- const en = {
114
- title: 'Remote Workspace',
115
- subtitle: 'Native SSH connections, SFTP file access, 3-way mirror sync, and port forwarding tunnels.',
116
- status: 'Ready',
117
-
118
- tabProfiles: '🖥️ Host Profiles',
119
-
120
- tabContainers: '🐳 Containers',
121
-
122
- tabEnv: '🗃️ .env Manager',
123
- btnImportSsh: 'Import ~/.ssh/config',
124
- envTitle: 'Remote Environment Variables (.env)',
125
- envKey: 'Key',
126
- envVal: 'Value',
127
- envSaveVar: 'Save',
128
- envShowVal: 'Show',
129
- envHideVal: 'Hide',
130
- tarSyncFast: '🚀 Fast Tarball Stream',
131
- diagnoseTitle: 'Quick Diagnostics',
132
- diagnosePorts: 'Open Ports',
133
- diagnoseOom: 'OOM Killer Check',
134
- diagnoseDisk: 'Disk Usage',
135
- alertLabel: 'Alerts ({count})',
136
-
137
- containersTitle: 'Remote Docker / Podman Engine',
138
- containerName: 'Container',
139
- containerImage: 'Image',
140
- containerStatus: 'Status',
141
- containerPorts: 'Ports',
142
- btnLogs: 'Logs',
143
- btnRestart: 'Restart',
144
- btnStop: 'Stop',
145
- noContainers: 'No active containers found or Docker daemon not running.',
146
- healthCpu: 'CPU: {val}%',
147
- healthMem: 'RAM: {val}% ({used}/{total} MB)',
148
- healthDisk: 'Disk: {val}%',
149
- healthUptime: 'Up: {val}',
150
- autoSyncTitle: 'Live Auto-Sync on Save',
151
- autoSyncDesc: 'Automatically push modified files to remote workspace on local save.',
152
- btnTransfer: 'Copy to Host...',
153
- transferTitle: 'Server-to-Server Direct Transfer',
154
- transferDestHost: 'Destination Server',
155
- transferDestPath: 'Destination File Path',
156
- transferBtnExec: 'Transfer Directly',
157
- transferSuccess: 'Successfully transferred {bytes} bytes to {host}:{path}',
158
- fJumpHost: 'Jump Host (Bastion)',
159
- fJumpHostNone: 'Direct Connection (No Jump Host)',
160
-
161
- tabTerminal: '⚡ Web Terminal',
162
- tabExplorer: '📂 File Explorer',
163
- tabTunnels: '🔗 Port Tunnels',
164
- tabSync: '🔄 Sync Station',
165
- vaultProtectedBadge: '🛡️ .env Vault Protected',
166
- termStart: 'Connect Terminal',
167
- termStop: 'Disconnect',
168
- termClear: 'Clear',
169
- termPlaceholder: 'Type command and press Enter (e.g. htop, ls, docker ps)...',
170
- termConnected: 'Connected to PTY session {id} on {host}',
171
- termDisconnected: 'Terminal disconnected.',
172
- explorerTitle: 'Remote Filesystem Explorer',
173
- filePreviewTitle: 'File Viewer: {path}',
174
- fileSize: '{size} KB',
175
- btnSaveFile: 'Save Changes',
176
- fileSaved: 'File saved successfully.',
177
- tunnelTraffic: 'RX: {rx} | TX: {tx}',
178
- tunnelUptime: 'Up: {sec}s',
179
- openLocalLink: '🌐 Open Local Port',
180
- badgeOnline: 'Active Host: {name} ({latency} ms)',
181
- badgeOffline: 'No active host',
182
- badgeTunnels: 'Tunnels: {count}',
183
- badgeKey: '🔑 Key Auth',
184
- badgePassword: '🔒 Password Auth',
185
- profilesTitle: '🖥️ Remote SSH Profiles',
186
- profilesDesc: 'Configure remote machines for code execution, SFTP browsing, and background mirror sync.',
187
- addBtn: '+ Add Host',
188
- nameCol: 'Name',
189
- hostCol: 'Host / User',
190
- portCol: 'Port',
191
- pathCol: 'Remote Workspace',
192
- actionsCol: 'Actions',
193
- noHosts: 'No remote hosts configured yet. Click "+ Add Host" to create your first connection.',
194
- activeBadge: 'Active',
195
- pinBtn: 'Pin Active',
196
- editBtn: 'Edit',
197
- delBtn: 'Delete',
198
- delConfirm: 'Delete host profile "{name}"?',
199
- formTitleNew: 'Add New Host Profile',
200
- formTitleEdit: 'Edit Host Profile',
201
- fName: 'Profile Name',
202
- fHost: 'Host / IP Address',
203
- fPort: 'Port',
204
- fUser: 'Username',
205
- fAuthType: 'Authentication Method',
206
- fAuthKey: 'SSH Private Key',
207
- fAuthPass: 'Password',
208
- fKeyPath: 'Private Key Path',
209
- fKeyPathHint: 'Path to ~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
210
- fPassphrase: 'Key Passphrase (optional)',
211
- fPassword: 'Account Password',
212
- fRemotePath: 'Remote Workspace Directory',
213
- fRemotePathHint: 'Absolute path on the server (e.g. /home/user/my-project)',
214
- fLocalMirror: 'Local Mirror Directory',
215
- fLocalMirrorHint: 'Local directory where files are mirrored for fast 3-way synchronization',
216
- btnBrowse: '📁 Browse Remote...',
217
- btnSelectThis: 'Select Directory',
218
- btnTest: '⚡ Test Connection (Ping)',
219
- btnSave: 'Save Profile',
220
- btnCancel: 'Cancel',
221
- testing: 'Testing Connection...',
222
- saving: 'Saving...',
223
- testOk: '✅ Connection OK! Latency: {latency} ms | OS: {os}',
224
- testFail: '❌ Connection Failed: {err}',
225
- diagTitle: '📊 Active Connection Diagnostics',
226
- diagDesc: 'Real-time telemetry and engine status for the currently selected remote machine.',
227
- diagProbeBtn: '⚡ Quick Probe',
228
- syncTitle: '🔄 Two-Way Workspace Mirror Sync',
229
- syncDesc: 'Fast 3-way conflict-aware file synchronization between local mirror and remote server.',
230
- pullBtn: '⬇️ Pull from Remote',
231
- pushBtn: '⬆️ Push to Remote',
232
- pulling: 'Pulling files…',
233
- pushing: 'Pushing files…',
234
- syncOk: 'Sync completed! Updated {count} file(s).',
235
- syncConflict: 'Conflict warning: {count} file(s) modified on both sides.',
236
- syncErr: 'Sync error: {err}',
237
- tunnelsTitle: '🔀 SSH Port Forwarding (Tunnels)',
238
- tunnelsDesc: 'Forward remote ports (e.g. databases, web servers, APIs) to your local machine via encrypted SSH tunnels.',
239
- tLocalPort: 'Local Port',
240
- tRemotePort: 'Remote Port',
241
- tOpenBtn: '+ Open Tunnel',
242
- tOpening: 'Opening…',
243
- tStopBtn: 'Stop',
244
- tNoTunnels: 'No active port forwarding tunnels.',
245
- tunnelsColId: 'Tunnel ID',
246
- tunnelsColRoute: 'Forwarding Route',
247
- tunnelsColActions: 'Actions',
248
- tOpenOk: 'Tunnel opened: 127.0.0.1:{localPort} -> Remote:{remotePort}',
249
- tOpenErr: 'Failed to open tunnel: {err}'
250
- }
251
-
252
- const zh = {
253
- title: '远程开发工作区',
254
- subtitle: '支持原生 SSH 连接、SFTP 文件访问、3-way 镜像同步与端口转发隧道。',
255
- status: '就绪',
256
-
257
- tabProfiles: '🖥️ 主机配置',
258
- tabContainers: '🐳 容器管理',
259
- tabEnv: '🗃️ 环境变量',
260
- btnImportSsh: '导入 ~/.ssh/config',
261
- envTitle: '远程环境变量配置 (.env)',
262
- envKey: '变量名',
263
- envVal: '变量值',
264
- envSaveVar: '保存',
265
- envShowVal: '显示',
266
- envHideVal: '隐藏',
267
- tarSyncFast: '🚀 快速 Tarball 流式同步',
268
- diagnoseTitle: '快速诊断',
269
- diagnosePorts: '开放端口',
270
- diagnoseOom: 'OOM 检查',
271
- diagnoseDisk: '磁盘分析',
272
- alertLabel: '告警 ({count})',
273
-
274
- containersTitle: '远程 Docker / Podman 引擎',
275
- containerName: '容器名',
276
- containerImage: '镜像',
277
- containerStatus: '运行状态',
278
- containerPorts: '端口映射',
279
- btnLogs: '日志',
280
- btnRestart: '重启',
281
- btnStop: '停止',
282
- noContainers: '未找到运行中的容器或 Docker 守护进程未启动。',
283
- healthCpu: 'CPU: {val}%',
284
- healthMem: '内存: {val}% ({used}/{total} MB)',
285
- healthDisk: '磁盘: {val}%',
286
- healthUptime: '运行: {val}',
287
- autoSyncTitle: '保存时自动增量同步',
288
- autoSyncDesc: '本地文件保存时,后台自动增量推送到远程对应工作目录。',
289
- btnTransfer: '跨主机传输...',
290
- transferTitle: '主机间直接流式传输',
291
- transferDestHost: '目标主机',
292
- transferDestPath: '目标文件路径',
293
- transferBtnExec: '立即直传',
294
- transferSuccess: '成功传输 {bytes} 字节到 {host}:{path}',
295
- fJumpHost: '跳板机 (Bastion)',
296
- fJumpHostNone: '直连模式 (无跳板机)',
297
-
298
- tabTerminal: '⚡ 网页终端',
299
- tabExplorer: '📂 文件管理',
300
- tabTunnels: '🔗 端口隧道',
301
- tabSync: '🔄 同步控制台',
302
- vaultProtectedBadge: '🛡️ .env 保管库加密保护',
303
- termStart: '连接终端',
304
- termStop: '断开连接',
305
- termClear: '清屏',
306
- termPlaceholder: '输入命令后回车 (如 htop, ls, docker ps)...',
307
- termConnected: '已连接至主机 {host} 的 PTY 会话 {id}',
308
- termDisconnected: '终端已断开。',
309
- explorerTitle: '远程文件系统浏览',
310
- filePreviewTitle: '文件查看器: {path}',
311
- fileSize: '{size} KB',
312
- btnSaveFile: '保存文件更改',
313
- fileSaved: '文件保存成功。',
314
- tunnelTraffic: '下行: {rx} | 上行: {tx}',
315
- tunnelUptime: '运行: {sec}秒',
316
- openLocalLink: '🌐 打开本地端口',
317
- badgeOnline: '当前主机: {name} ({latency} ms)',
318
- badgeOffline: '未选定活跃主机',
319
- badgeTunnels: '活动隧道: {count}',
320
- badgeKey: '🔑 私钥认证',
321
- badgePassword: '🔒 密码认证',
322
- profilesTitle: '🖥️ 远程 SSH 主机配置',
323
- profilesDesc: '配置用于命令执行、SFTP 浏览与后台镜像同步的远程服务器。',
324
- addBtn: '+ 添加主机',
325
- nameCol: '配置名称',
326
- hostCol: '主机 / 用户',
327
- portCol: '端口',
328
- pathCol: '远程工作区',
329
- actionsCol: '操作',
330
- noHosts: '暂无配置的主机。点击 "+ 添加主机" 建立首个连接。',
331
- activeBadge: '活动中',
332
- pinBtn: '设为默认',
333
- editBtn: '编辑',
334
- delBtn: '删除',
335
- delConfirm: '确认删除主机配置 "{name}" 吗?',
336
- formTitleNew: '添加新主机配置',
337
- formTitleEdit: '编辑主机配置',
338
- fName: '配置别名',
339
- fHost: '主机名 / IP 地址',
340
- fPort: '端口',
341
- fUser: '登录用户名',
342
- fAuthType: '身份认证方式',
343
- fAuthKey: 'SSH 私钥',
344
- fAuthPass: '账户密码',
345
- fKeyPath: '私钥文件路径',
346
- fKeyPathHint: '例如 ~/.ssh/id_rsa 或 /home/user/.ssh/id_ed25519',
347
- fPassphrase: '私钥口令 (可选)',
348
- fPassword: '登录密码',
349
- fRemotePath: '远程项目工作区目录',
350
- fRemotePathHint: '服务器上的绝对路径 (例如 /home/user/my-project)',
351
- fLocalMirror: '本地镜像同步目录',
352
- fLocalMirrorHint: '用于 3-way 差异比对与快速同步的本地镜像路径',
353
- btnBrowse: '📁 浏览远程目录...',
354
- btnSelectThis: '选择当前目录',
355
- btnTest: '⚡ 测试网络连通性 (Ping)',
356
- btnSave: '保存配置',
357
- btnCancel: '取消',
358
- testing: '正在测试连接...',
359
- saving: '正在保存...',
360
- testOk: '✅ 连接成功!延迟: {latency} ms | 系统: {os}',
361
- testFail: '❌ 连接失败: {err}',
362
- diagTitle: '📊 实时连接状态诊断',
363
- diagDesc: '查看当前选定主机的网络延迟、系统资源及底层服务运行指标。',
364
- diagProbeBtn: '⚡ 即时探测',
365
- syncTitle: '🔄 双向工作区镜像同步',
366
- syncDesc: '基于三方冲突感知的快速差异同步,支持本地与远程端双向推拉。',
367
- pullBtn: '⬇️ 拉取至本地 (Pull)',
368
- pushBtn: '⬆️ 推送至远程 (Push)',
369
- pulling: '正在拉取文件…',
370
- pushing: '正在推送文件…',
371
- syncOk: '同步完成!共更新 {count} 个文件。',
372
- syncConflict: '冲突警告: 检测到 {count} 个文件在两端均有修改。',
373
- syncErr: '同步异常: {err}',
374
- tunnelsTitle: '🔀 SSH 端口安全隧道',
375
- tunnelsDesc: '通过加密 SSH 隧道将远程端口 (如数据库、开发服务、API) 映射至本地访问。',
376
- tLocalPort: '本地监听端口',
377
- tRemotePort: '远程目标端口',
378
- tOpenBtn: '+ 建立隧道',
379
- tOpening: '正在建立…',
380
- tStopBtn: '关闭',
381
- tNoTunnels: '当前没有运行中的端口转发隧道。',
382
- tunnelsColId: '隧道标识',
383
- tunnelsColRoute: '转发路由',
384
- tunnelsColActions: '管理操作',
385
- tOpenOk: '隧道已启动: 127.0.0.1:{localPort} -> Remote:{remotePort}',
386
- tOpenErr: '启动隧道失败: {err}'
387
- }
388
-
389
- function makeT(dict, fallback) {
390
- return function t(key, vars) {
391
- let val = (dict && dict[key]) || (fallback && fallback[key]) || key
392
- if (vars && typeof val === 'string') {
393
- for (const k of Object.keys(vars)) {
394
- val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]))
395
- }
396
- }
397
- return val
398
- }
399
- }
400
-
401
- function FallbackChevron() {
402
- return React.createElement(
403
- 'svg',
404
- { width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': true },
405
- React.createElement('path', {
406
- d: 'M3.5 5.25L7 8.75L10.5 5.25',
407
- stroke: 'currentColor',
408
- strokeWidth: 1.5,
409
- strokeLinecap: 'round',
410
- strokeLinejoin: 'round',
411
- })
412
- )
413
- }
414
-
415
-
416
-
417
-
418
- // --- Sub-component: Remote Environment Tab (New in v0.3.1) ---
419
- function EnvTab(props) {
420
- const { activeProfile, t } = props
421
- const [envEntries, setEnvEntries] = React.useState([])
422
- const [loading, setLoading] = React.useState(false)
423
- const [visibleKeys, setVisibleKeys] = React.useState({})
424
- const [editValues, setEditValues] = React.useState({})
425
- const [msg, setMsg] = React.useState('')
426
-
427
- const loadEnv = async () => {
428
- if (!activeProfile) return
429
- setLoading(true)
430
- try {
431
- const res = await fetch('/dsh-remote-workspace/env/view', {
432
- method: 'POST',
433
- headers: { 'Content-Type': 'application/json' },
434
- body: JSON.stringify({ profileId: activeProfile.id })
435
- })
436
- const data = await res.json()
437
- if (data.ok && Array.isArray(data.entries)) {
438
- setEnvEntries(data.entries.filter((e) => e.type === 'var'))
439
- const initialEdits = {}
440
- data.entries.forEach((e) => {
441
- if (e.type === 'var') initialEdits[e.key] = e.value
442
- })
443
- setEditValues(initialEdits)
444
- }
445
- } catch (err) {
446
- setMsg(`Error loading .env: ${err.message}`)
447
- }
448
- setLoading(false)
449
- }
450
-
451
- React.useEffect(() => {
452
- loadEnv()
453
- }, [activeProfile?.id])
454
-
455
- const toggleVisible = (key) => {
456
- setVisibleKeys((prev) => ({ ...prev, [key]: !prev[key] }))
457
- }
458
-
459
- const saveVar = async (key) => {
460
- try {
461
- const res = await fetch('/dsh-remote-workspace/env/save', {
462
- method: 'POST',
463
- headers: { 'Content-Type': 'application/json' },
464
- body: JSON.stringify({
465
- profileId: activeProfile.id,
466
- key,
467
- value: editValues[key] || ''
468
- })
469
- })
470
- const data = await res.json()
471
- if (data.ok) {
472
- setMsg(`Saved ${key} successfully!`)
473
- setTimeout(() => setMsg(''), 3000)
474
- }
475
- } catch (err) {
476
- setMsg(`Save failed: ${err.message}`)
477
- }
478
- }
479
-
480
- return React.createElement(
481
- 'div',
482
- { style: { display: 'flex', flexDirection: 'column', gap: '14px' } },
483
- React.createElement(
484
- 'div',
485
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
486
- React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, t('envTitle')),
487
- React.createElement('button', { type: 'button', className: 'drw-btn', onClick: loadEnv, disabled: loading }, loading ? 'Loading...' : '🔄 Refresh')
488
- ),
489
- msg ? React.createElement('div', { className: 'drw-badge drw-badge-ok' }, msg) : null,
490
- React.createElement(
491
- 'div',
492
- { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', padding: '12px', background: 'var(--dsw-alias-bg-layer-2)' } },
493
- envEntries.length === 0
494
- ? React.createElement('div', { style: { textAlign: 'center', padding: '16px', color: 'var(--dsw-alias-label-secondary)' } }, 'No variables found in remote .env')
495
- : envEntries.map((e) =>
496
- React.createElement(
497
- 'div',
498
- { key: e.key, className: 'drw-env-row' },
499
- React.createElement('span', { style: { fontWeight: 600, fontFamily: 'monospace', fontSize: '12.5px' } }, e.key),
500
- React.createElement('input', {
501
- type: visibleKeys[e.key] ? 'text' : 'password',
502
- className: 'drw-input',
503
- value: editValues[e.key] !== undefined ? editValues[e.key] : e.value,
504
- onChange: (ev) => {
505
- const val = ev.target.value
506
- setEditValues((prev) => ({ ...prev, [e.key]: val }))
507
- }
508
- }),
509
- React.createElement(
510
- 'div',
511
- { style: { display: 'flex', gap: '4px', justifyContent: 'flex-end' } },
512
- React.createElement('button', { type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => toggleVisible(e.key) }, visibleKeys[e.key] ? t('envHideVal') : t('envShowVal')),
513
- React.createElement('button', { type: 'button', className: 'drw-btn drw-btn-primary', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => saveVar(e.key) }, t('envSaveVar'))
514
- )
515
- )
516
- )
517
- )
518
- )
519
- }
520
-
521
- // --- Sub-component: Docker Containers Tab ---
522
- function ContainersTab(props) {
523
- const { activeProfile, t } = props
524
- const [containers, setContainers] = React.useState([])
525
- const [loading, setLoading] = React.useState(false)
526
- const [logsModal, setLogsModal] = React.useState(null)
527
- const [logsContent, setLogsContent] = React.useState('')
528
- const [logsLoading, setLogsLoading] = React.useState(false)
529
-
530
- const loadContainers = async () => {
531
- if (!activeProfile) return
532
- setLoading(true)
533
- try {
534
- const res = await fetch(`/dsh-remote-workspace/docker/list?profileId=${activeProfile.id}`)
535
- const data = await res.json()
536
- if (data.ok && Array.isArray(data.containers)) {
537
- setContainers(data.containers)
538
- } else {
539
- setContainers([])
540
- }
541
- } catch (_) {}
542
- setLoading(false)
543
- }
544
-
545
- React.useEffect(() => {
546
- loadContainers()
547
- }, [activeProfile?.id])
548
-
549
- const handleAction = async (containerId, action) => {
550
- try {
551
- await fetch('/dsh-remote-workspace/docker/action', {
552
- method: 'POST',
553
- headers: { 'Content-Type': 'application/json' },
554
- body: JSON.stringify({ profileId: activeProfile.id, containerId, action })
555
- })
556
- await loadContainers()
557
- } catch (_) {}
558
- }
559
-
560
- const showLogs = async (containerId, name) => {
561
- setLogsModal({ id: containerId, name })
562
- setLogsLoading(true)
563
- try {
564
- const res = await fetch('/dsh-remote-workspace/docker/logs', {
565
- method: 'POST',
566
- headers: { 'Content-Type': 'application/json' },
567
- body: JSON.stringify({ profileId: activeProfile.id, containerId, tail: 150 })
568
- })
569
- const data = await res.json()
570
- setLogsContent(data.logs || '[No logs output]')
571
- } catch (err) {
572
- setLogsContent(`[Error: ${err.message}]`)
573
- }
574
- setLogsLoading(false)
575
- }
576
-
577
- return React.createElement(
578
- 'div',
579
- { style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
580
- React.createElement(
581
- 'div',
582
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
583
- React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, t('containersTitle')),
584
- React.createElement(
585
- 'button',
586
- {
587
- type: 'button',
588
- className: 'drw-btn',
589
- onClick: loadContainers,
590
- disabled: loading
591
- },
592
- loading ? 'Refreshing...' : '🔄 Refresh'
593
- )
594
- ),
595
- React.createElement(
596
- 'div',
597
- { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', overflow: 'hidden', background: 'var(--dsw-alias-bg-layer-2)' } },
598
- React.createElement(
599
- 'div',
600
- { className: 'drw-container-row', style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)', background: 'var(--dsw-alias-bg-layer-1)' } },
601
- React.createElement('span', null, t('containerName')),
602
- React.createElement('span', null, t('containerImage')),
603
- React.createElement('span', null, t('containerStatus')),
604
- React.createElement('span', null, t('containerPorts')),
605
- React.createElement('span', { style: { textAlign: 'right' } }, t('actionsCol'))
606
- ),
607
- containers.length === 0
608
- ? React.createElement('div', { style: { padding: '24px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)' } }, t('noContainers'))
609
- : containers.map((c) =>
610
- React.createElement(
611
- 'div',
612
- { key: c.id, className: 'drw-container-row' },
613
- React.createElement(
614
- 'div',
615
- { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
616
- React.createElement('span', { className: `drw-badge ${c.state === 'running' ? 'drw-badge-ok' : 'drw-badge-warn'}` }, c.state),
617
- React.createElement('span', { style: { fontWeight: 600 } }, c.names)
618
- ),
619
- React.createElement('span', { style: { opacity: 0.8, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, c.image),
620
- React.createElement('span', { style: { fontSize: '11px', opacity: 0.7 } }, c.status),
621
- React.createElement('span', { style: { fontSize: '11px', opacity: 0.7, overflow: 'hidden', textOverflow: 'ellipsis' } }, c.ports || '—'),
622
- React.createElement(
623
- 'div',
624
- { style: { display: 'flex', gap: '4px', justifyContent: 'flex-end' } },
625
- React.createElement(
626
- 'button',
627
- { type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => showLogs(c.id, c.names) },
628
- t('btnLogs')
629
- ),
630
- React.createElement(
631
- 'button',
632
- { type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => handleAction(c.id, 'restart') },
633
- t('btnRestart')
634
- ),
635
- c.state === 'running'
636
- ? React.createElement(
637
- 'button',
638
- { type: 'button', className: 'drw-btn drw-btn-danger', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => handleAction(c.id, 'stop') },
639
- t('btnStop')
640
- )
641
- : null
642
- )
643
- )
644
- )
645
- ),
646
- // Container Logs Modal
647
- logsModal
648
- ? React.createElement(
649
- 'div',
650
- { className: 'drw-modal-overlay', onClick: () => setLogsModal(null) },
651
- React.createElement(
652
- 'div',
653
- { className: 'drw-modal-dialog', onClick: (e) => e.stopPropagation() },
654
- React.createElement(
655
- 'div',
656
- { className: 'drw-modal-hdr' },
657
- React.createElement('span', { style: { fontFamily: 'monospace' } }, `Docker Logs: ${logsModal.name} (${logsModal.id})`),
658
- React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => setLogsModal(null) }, '✕')
659
- ),
660
- React.createElement(
661
- 'div',
662
- { className: 'drw-modal-body' },
663
- React.createElement('pre', { style: { background: '#0d1117', color: '#c9d1d9', padding: '12px', borderRadius: '8px', fontSize: '12px', maxHeight: '420px', overflowY: 'auto', whiteSpace: 'pre-wrap' } }, logsLoading ? 'Fetching logs...' : logsContent)
664
- )
665
- )
666
- )
667
- : null
668
- )
669
- }
670
-
671
- // --- Sub-component: Web Terminal Tab ---
672
- function TerminalTab(props) {
673
- const { activeProfile, t } = props
674
- const [connected, setConnected] = React.useState(false)
675
- const [sessionId, setSessionId] = React.useState(null)
676
- const [output, setOutput] = React.useState('')
677
- const [cmdInput, setCmdInput] = React.useState('')
678
- const [loading, setLoading] = React.useState(false)
679
- const termEndRef = React.useRef(null)
680
- const evtSourceRef = React.useRef(null)
681
-
682
- React.useEffect(() => {
683
- if (termEndRef.current) {
684
- termEndRef.current.scrollIntoView({ behavior: 'smooth' })
685
- }
686
- }, [output])
687
-
688
- React.useEffect(() => {
689
- return () => {
690
- if (evtSourceRef.current) {
691
- evtSourceRef.current.close()
692
- }
693
- }
694
- }, [])
695
-
696
- const handleConnect = async () => {
697
- if (!activeProfile) return
698
- setLoading(true)
699
- try {
700
- const res = await fetch('/dsh-remote-workspace/terminal/create', {
701
- method: 'POST',
702
- headers: { 'Content-Type': 'application/json' },
703
- body: JSON.stringify({ profileId: activeProfile.id, cols: 80, rows: 24 })
704
- })
705
- const data = await res.json()
706
- if (data.ok && data.sessionId) {
707
- setSessionId(data.sessionId)
708
- setConnected(true)
709
- setOutput((prev) => prev + `\n[+] ` + t('termConnected', { id: data.sessionId, host: activeProfile.host }) + `\n`)
710
-
711
- // Connect SSE stream
712
- const es = new EventSource(`/dsh-remote-workspace/terminal/stream?sessionId=${data.sessionId}`)
713
- evtSourceRef.current = es
714
- es.onmessage = (e) => {
715
- try {
716
- const parsed = JSON.parse(e.data)
717
- if (parsed.chunk) {
718
- setOutput((p) => p + parsed.chunk)
719
- }
720
- } catch (_) {}
721
- }
722
- es.onerror = () => {
723
- setOutput((p) => p + `\n[-] ` + t('termDisconnected') + `\n`)
724
- setConnected(false)
725
- es.close()
726
- }
727
- }
728
- } catch (err) {
729
- setOutput((p) => p + `\n[!] Connection error: ${err.message}\n`)
730
- }
731
- setLoading(false)
732
- }
733
-
734
- const handleDisconnect = async () => {
735
- if (sessionId) {
736
- try {
737
- await fetch('/dsh-remote-workspace/terminal/close', {
738
- method: 'POST',
739
- headers: { 'Content-Type': 'application/json' },
740
- body: JSON.stringify({ sessionId })
741
- })
742
- } catch (_) {}
743
- }
744
- if (evtSourceRef.current) {
745
- evtSourceRef.current.close()
746
- }
747
- setConnected(false)
748
- setSessionId(null)
749
- setOutput((p) => p + `\n[-] ` + t('termDisconnected') + `\n`)
750
- }
751
-
752
- const sendCommand = async (e) => {
753
- if (e) e.preventDefault()
754
- if (!cmdInput || !sessionId) return
755
- const toSend = cmdInput + '\n'
756
- setCmdInput('')
757
- try {
758
- await fetch('/dsh-remote-workspace/terminal/input', {
759
- method: 'POST',
760
- headers: { 'Content-Type': 'application/json' },
761
- body: JSON.stringify({ sessionId, data: toSend })
762
- })
763
- } catch (_) {}
764
- }
765
-
766
- const sendCtrlC = async () => {
767
- if (!sessionId) return
768
- try {
769
- await fetch('/dsh-remote-workspace/terminal/input', {
770
- method: 'POST',
771
- headers: { 'Content-Type': 'application/json' },
772
- body: JSON.stringify({ sessionId, data: '\x03' })
773
- })
774
- } catch (_) {}
775
- }
776
-
777
- return React.createElement(
778
- 'div',
779
- { style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
780
- React.createElement(
781
- 'div',
782
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
783
- React.createElement(
784
- 'div',
785
- { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
786
- React.createElement('span', { style: { fontWeight: 600, fontSize: '14px' } }, 'SSH PTY Shell:'),
787
- React.createElement(
788
- 'span',
789
- { className: `drw-badge ${connected ? 'drw-badge-ok' : 'drw-badge-warn'}` },
790
- connected ? `🟢 Online (${activeProfile?.host})` : '⚪ Idle'
791
- )
792
- ),
793
- React.createElement(
794
- 'div',
795
- { style: { display: 'flex', gap: '8px' } },
796
- !connected
797
- ? React.createElement(
798
- 'button',
799
- {
800
- type: 'button',
801
- className: 'drw-btn drw-btn-primary',
802
- disabled: !activeProfile || loading,
803
- onClick: handleConnect
804
- },
805
- loading ? 'Connecting...' : t('termStart')
806
- )
807
- : React.createElement(
808
- 'button',
809
- {
810
- type: 'button',
811
- className: 'drw-btn drw-btn-danger',
812
- onClick: handleDisconnect
813
- },
814
- t('termStop')
815
- ),
816
- connected
817
- ? React.createElement(
818
- 'button',
819
- {
820
- type: 'button',
821
- className: 'drw-btn',
822
- onClick: sendCtrlC
823
- },
824
- 'Ctrl+C'
825
- )
826
- : null,
827
- React.createElement(
828
- 'button',
829
- {
830
- type: 'button',
831
- className: 'drw-btn',
832
- onClick: () => setOutput('')
833
- },
834
- t('termClear')
835
- )
836
- )
837
- ),
838
- React.createElement(
839
- 'div',
840
- { className: 'drw-term-window' },
841
- React.createElement(
842
- 'div',
843
- { className: 'drw-term-header' },
844
- React.createElement('span', null, `terminal ~ ${activeProfile?.username || 'user'}@${activeProfile?.host || 'remote'}`),
845
- React.createElement('span', null, connected ? `Session: ${sessionId}` : 'Offline')
846
- ),
847
- React.createElement(
848
- 'div',
849
- { className: 'drw-term-body' },
850
- output || (activeProfile ? `[i] Ready to connect to ${activeProfile.username || 'root'}@${activeProfile.host}. Click 'Connect Terminal'.` : `[!] No active host profile selected. Configure and activate a profile first.`),
851
- React.createElement('div', { ref: termEndRef })
852
- ),
853
- connected
854
- ? React.createElement(
855
- 'form',
856
- { className: 'drw-term-footer', onSubmit: sendCommand },
857
- React.createElement('span', { style: { color: '#38bdf8', padding: '4px 6px', fontFamily: 'monospace' } }, '$'),
858
- React.createElement('input', {
859
- className: 'drw-term-input',
860
- type: 'text',
861
- placeholder: t('termPlaceholder'),
862
- value: cmdInput,
863
- onChange: (e) => setCmdInput(e.target.value)
864
- }),
865
- React.createElement(
866
- 'button',
867
- { type: 'submit', className: 'drw-btn', style: { padding: '2px 8px', fontSize: '11px' } },
868
- 'Send ↵'
869
- )
870
- )
871
- : null
872
- )
873
- )
874
- }
875
-
876
- // --- Sub-component: Remote File Explorer Tab ---
877
- function ExplorerTab(props) {
878
- const { activeProfile, t } = props
879
- const [currentPath, setCurrentPath] = React.useState(activeProfile?.remoteWorkspace || '/')
880
- const [entries, setEntries] = React.useState([])
881
- const [loading, setLoading] = React.useState(false)
882
- const [previewFile, setPreviewFile] = React.useState(null)
883
- const [previewContent, setPreviewContent] = React.useState('')
884
- const [editContent, setEditContent] = React.useState('')
885
- const [previewLoading, setPreviewLoading] = React.useState(false)
886
- const [saveStatus, setSaveStatus] = React.useState(null)
887
-
888
- const loadDir = async (dirPath) => {
889
- if (!activeProfile) return
890
- setLoading(true)
891
- try {
892
- const res = await fetch('/dsh-remote-workspace/browse', {
893
- method: 'POST',
894
- headers: { 'Content-Type': 'application/json' },
895
- body: JSON.stringify({ profileId: activeProfile.id, remotePath: dirPath })
896
- })
897
- const data = await res.json()
898
- if (data.ok && Array.isArray(data.entries)) {
899
- setCurrentPath(dirPath)
900
- setEntries(data.entries)
901
- }
902
- } catch (_) {}
903
- setLoading(false)
904
- }
905
-
906
- React.useEffect(() => {
907
- if (activeProfile) {
908
- loadDir(activeProfile.remoteWorkspace || '/')
909
- }
910
- }, [activeProfile?.id])
911
-
912
- const handleNavUp = () => {
913
- const parts = currentPath.split('/').filter(Boolean)
914
- parts.pop()
915
- const parent = '/' + parts.join('/')
916
- loadDir(parent || '/')
917
- }
918
-
919
- const openFile = async (entry) => {
920
- const filePath = currentPath.endsWith('/') ? currentPath + entry.filename : currentPath + '/' + entry.filename
921
- setPreviewLoading(true)
922
- setPreviewFile(filePath)
923
- setSaveStatus(null)
924
- try {
925
- const res = await fetch('/dsh-remote-workspace/file/view', {
926
- method: 'POST',
927
- headers: { 'Content-Type': 'application/json' },
928
- body: JSON.stringify({ profileId: activeProfile.id, filePath })
929
- })
930
- const data = await res.json()
931
- if (data.ok) {
932
- setPreviewContent(data.content)
933
- setEditContent(data.content)
934
- } else {
935
- setPreviewContent(`[Error loading file: ${data.error || 'Failed'}]`)
936
- setEditContent('')
937
- }
938
- } catch (err) {
939
- setPreviewContent(`[Error: ${err.message}]`)
940
- }
941
- setPreviewLoading(false)
942
- }
943
-
944
- const handleSaveFile = async () => {
945
- if (!previewFile || !activeProfile) return
946
- setSaveStatus('saving')
947
- try {
948
- const res = await fetch('/dsh-remote-workspace/file/save', {
949
- method: 'POST',
950
- headers: { 'Content-Type': 'application/json' },
951
- body: JSON.stringify({ profileId: activeProfile.id, filePath: previewFile, content: editContent })
952
- })
953
- const data = await res.json()
954
- if (data.ok) {
955
- setSaveStatus('saved')
956
- setPreviewContent(editContent)
957
- } else {
958
- setSaveStatus('error')
959
- }
960
- } catch (_) {
961
- setSaveStatus('error')
962
- }
963
- }
964
-
965
- return React.createElement(
966
- 'div',
967
- { className: 'drw-explorer-box' },
968
- React.createElement(
969
- 'div',
970
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
971
- React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, t('explorerTitle')),
972
- React.createElement(
973
- 'button',
974
- {
975
- type: 'button',
976
- className: 'drw-btn',
977
- onClick: () => loadDir(currentPath),
978
- disabled: loading
979
- },
980
- loading ? 'Refreshing...' : '🔄 Refresh'
981
- )
982
- ),
983
- React.createElement(
984
- 'div',
985
- { className: 'drw-explorer-nav' },
986
- React.createElement(
987
- 'button',
988
- {
989
- type: 'button',
990
- className: 'drw-btn',
991
- style: { padding: '2px 8px', fontSize: '11px' },
992
- onClick: handleNavUp,
993
- disabled: currentPath === '/' || loading
994
- },
995
- '⬆️ Up'
996
- ),
997
- React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, 'Path:'),
998
- React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-brand-primary)' } }, currentPath)
999
- ),
1000
- React.createElement(
1001
- 'div',
1002
- { className: 'drw-file-list' },
1003
- entries.length === 0
1004
- ? React.createElement('div', { style: { padding: '16px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)' } }, loading ? 'Reading directory...' : 'Directory is empty.')
1005
- : entries.map((e) =>
1006
- React.createElement(
1007
- 'div',
1008
- {
1009
- key: e.filename,
1010
- className: 'drw-file-row',
1011
- onClick: () => (e.isDirectory ? loadDir(currentPath.endsWith('/') ? currentPath + e.filename : currentPath + '/' + e.filename) : openFile(e))
1012
- },
1013
- React.createElement(
1014
- 'div',
1015
- { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
1016
- React.createElement('span', null, e.isDirectory ? '📁' : '📄'),
1017
- React.createElement('span', { style: { fontWeight: e.isDirectory ? 600 : 400 } }, e.filename)
1018
- ),
1019
- React.createElement(
1020
- 'div',
1021
- { className: 'drw-file-meta' },
1022
- e.size !== undefined ? React.createElement('span', null, `${Math.round(e.size / 1024)} KB`) : null,
1023
- e.permissions ? React.createElement('span', { style: { opacity: 0.6 } }, e.permissions) : null
1024
- )
1025
- )
1026
- )
1027
- ),
1028
- // File Preview / Edit Modal
1029
- previewFile
1030
- ? React.createElement(
1031
- 'div',
1032
- { className: 'drw-modal-overlay', onClick: () => setPreviewFile(null) },
1033
- React.createElement(
1034
- 'div',
1035
- { className: 'drw-modal-dialog', onClick: (e) => e.stopPropagation() },
1036
- React.createElement(
1037
- 'div',
1038
- { className: 'drw-modal-hdr' },
1039
- React.createElement('span', { style: { fontFamily: 'monospace' } }, t('filePreviewTitle', { path: previewFile })),
1040
- React.createElement(
1041
- 'button',
1042
- {
1043
- type: 'button',
1044
- className: 'drw-btn',
1045
- style: { padding: '2px 8px' },
1046
- onClick: () => setPreviewFile(null)
1047
- },
1048
- '✕'
1049
- )
1050
- ),
1051
- React.createElement(
1052
- 'div',
1053
- { className: 'drw-modal-body', style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
1054
- previewLoading
1055
- ? React.createElement('div', { style: { padding: '20px', textAlign: 'center' } }, 'Loading content...')
1056
- : React.createElement('textarea', {
1057
- style: {
1058
- width: '100%',
1059
- height: '320px',
1060
- background: '#0d1117',
1061
- color: '#c9d1d9',
1062
- fontFamily: 'monospace',
1063
- fontSize: '12px',
1064
- padding: '10px',
1065
- border: '1px solid #30363d',
1066
- borderRadius: '8px',
1067
- resize: 'vertical',
1068
- boxSizing: 'border-box'
1069
- },
1070
- value: editContent,
1071
- onChange: (e) => setEditContent(e.target.value)
1072
- }),
1073
- React.createElement(
1074
- 'div',
1075
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
1076
- React.createElement(
1077
- 'span',
1078
- { style: { fontSize: '12px', color: saveStatus === 'saved' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)' } },
1079
- saveStatus === 'saved' ? t('fileSaved') : saveStatus === 'error' ? 'Error saving file.' : ''
1080
- ),
1081
- React.createElement(
1082
- 'div',
1083
- { style: { display: 'flex', gap: '8px' } },
1084
- React.createElement(
1085
- 'button',
1086
- {
1087
- type: 'button',
1088
- className: 'drw-btn',
1089
- onClick: () => setPreviewFile(null)
1090
- },
1091
- 'Close'
1092
- ),
1093
- React.createElement(
1094
- 'button',
1095
- {
1096
- type: 'button',
1097
- className: 'drw-btn drw-btn-primary',
1098
- onClick: handleSaveFile,
1099
- disabled: saveStatus === 'saving'
1100
- },
1101
- saveStatus === 'saving' ? 'Saving...' : t('btnSaveFile')
1102
- )
1103
- )
1104
- )
1105
- )
1106
- )
1107
- )
1108
- : null
1109
- )
1110
- }
1111
-
1112
- function SettingsView(props) {
1113
- const [activeTab, setActiveTab] = React.useState('profiles')
1114
- const [healthData, setHealthData] = React.useState(null)
1115
- const [autoSync, setAutoSync] = React.useState(false)
1116
-
1117
- const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
1118
- const [profiles, setProfiles] = React.useState([])
1119
- const [activeId, setActiveId] = React.useState(null)
1120
- const [tunnels, setTunnels] = React.useState([])
1121
- const [editing, setEditing] = React.useState(null)
1122
- const [busy, setBusy] = React.useState(null)
1123
- const [testResult, setTestResult] = React.useState(null)
1124
- const [syncMsg, setSyncMsg] = React.useState(null)
1125
- const [tunnelMsg, setTunnelMsg] = React.useState(null)
1126
- const [showPassword, setShowPassword] = React.useState(false)
1127
-
1128
- // Tunnel creation inputs
1129
- const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
1130
- const [newTunnelRemote, setNewTunnelRemote] = React.useState('3000')
1131
-
1132
- // Remote Directory Browser State
1133
- const [browserOpen, setBrowserOpen] = React.useState(false)
1134
- const [browserPath, setBrowserPath] = React.useState('/')
1135
- const [browserEntries, setBrowserEntries] = React.useState([])
1136
- const [browserLoading, setBrowserLoading] = React.useState(false)
1137
-
1138
- const loadState = async () => {
1139
- try {
1140
- const res = await fetch('/dsh-remote-workspace/state')
1141
- if (res.ok) {
1142
- const data = await res.json()
1143
- if (data.ok) {
1144
- setProfiles(data.profiles || [])
1145
- setActiveId(data.activeId || null)
1146
- setTunnels(data.tunnels || [])
1147
- setAutoSync(Boolean(data.autoSync))
1148
- if (data.activeId) {
1149
- fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
1150
- }
1151
- }
1152
- }
1153
- } catch (_) {}
1154
- }
1155
-
1156
- React.useEffect(() => {
1157
- ensureCss()
1158
- loadState()
1159
- }, [])
1160
-
1161
- const handleSave = async (profile) => {
1162
- setBusy('saving')
1163
- try {
1164
- const res = await fetch('/dsh-remote-workspace/profiles/save', {
1165
- method: 'POST',
1166
- headers: { 'Content-Type': 'application/json' },
1167
- body: JSON.stringify(profile)
1168
- })
1169
- if (res.ok) {
1170
- setEditing(null)
1171
- setBrowserOpen(false)
1172
- setTestResult(null)
1173
- await loadState()
1174
- }
1175
- } catch (_) {}
1176
- setBusy(null)
1177
- }
1178
-
1179
- const handleDelete = async (id, name) => {
1180
- if (!confirm(t('delConfirm', { name: name || id }))) return
1181
- try {
1182
- const res = await fetch('/dsh-remote-workspace/profiles/delete', {
1183
- method: 'POST',
1184
- headers: { 'Content-Type': 'application/json' },
1185
- body: JSON.stringify({ id })
1186
- })
1187
- if (res.ok) {
1188
- if (editing && editing.id === id) setEditing(null)
1189
- await loadState()
1190
- }
1191
- } catch (_) {}
1192
- }
1193
-
1194
- const handleSetActive = async (id) => {
1195
- try {
1196
- const res = await fetch('/dsh-remote-workspace/profiles/active', {
1197
- method: 'POST',
1198
- headers: { 'Content-Type': 'application/json' },
1199
- body: JSON.stringify({ id })
1200
- })
1201
- if (res.ok) {
1202
- await loadState()
1203
- }
1204
- } catch (_) {}
1205
- }
1206
-
1207
- const handleTest = async (prof) => {
1208
- setBusy('testing')
1209
- setTestResult(null)
1210
- try {
1211
- const res = await fetch('/dsh-remote-workspace/test', {
1212
- method: 'POST',
1213
- headers: { 'Content-Type': 'application/json' },
1214
- body: JSON.stringify({ profile: prof })
1215
- })
1216
- const data = await res.json()
1217
- setTestResult(data)
1218
- } catch (err) {
1219
- setTestResult({ ok: false, error: err.message })
1220
- }
1221
- setBusy(null)
1222
- }
1223
-
1224
- const openDirectoryBrowser = async (initialPath) => {
1225
- setBrowserOpen(true)
1226
- const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
1227
- await fetchRemoteDir(target)
1228
- }
1229
-
1230
- const fetchRemoteDir = async (dirPath) => {
1231
- setBrowserLoading(true)
1232
- try {
1233
- const res = await fetch('/dsh-remote-workspace/browse', {
1234
- method: 'POST',
1235
- headers: { 'Content-Type': 'application/json' },
1236
- body: JSON.stringify({ profile: editing, remotePath: dirPath })
1237
- })
1238
- const data = await res.json()
1239
- if (data.ok) {
1240
- setBrowserPath(data.currentPath || dirPath)
1241
- setBrowserEntries(data.entries || [])
1242
- }
1243
- } catch (_) {}
1244
- setBrowserLoading(false)
1245
- }
1246
-
1247
- const handleSync = async (direction) => {
1248
- setBusy(`sync_${direction}`)
1249
- setSyncMsg(null)
1250
- try {
1251
- const res = await fetch('/dsh-remote-workspace/sync', {
1252
- method: 'POST',
1253
- headers: { 'Content-Type': 'application/json' },
1254
- body: JSON.stringify({ direction })
1255
- })
1256
- const data = await res.json()
1257
- if (data.ok) {
1258
- const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
1259
- if (data.conflicts && data.conflicts.length > 0) {
1260
- setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
1261
- } else {
1262
- setSyncMsg({ ok: true, text: t('syncOk', { count }) })
1263
- }
1264
- } else {
1265
- setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
1266
- }
1267
- } catch (err) {
1268
- setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
1269
- }
1270
- setBusy(null)
1271
- }
1272
-
1273
- const handleStartTunnel = async () => {
1274
- if (!activeId) return
1275
- setBusy('tunnel_open')
1276
- setTunnelMsg(null)
1277
- try {
1278
- const res = await fetch('/dsh-remote-workspace/tunnels/start', {
1279
- method: 'POST',
1280
- headers: { 'Content-Type': 'application/json' },
1281
- body: JSON.stringify({
1282
- profileId: activeId,
1283
- localPort: newTunnelLocal,
1284
- remotePort: newTunnelRemote
1285
- })
1286
- })
1287
- const data = await res.json()
1288
- if (data.ok) {
1289
- setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
1290
- await loadState()
1291
- } else {
1292
- setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
1293
- }
1294
- } catch (err) {
1295
- setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
1296
- }
1297
- setBusy(null)
1298
- }
1299
-
1300
- const handleStopTunnel = async (tunnelId) => {
1301
- try {
1302
- const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
1303
- method: 'POST',
1304
- headers: { 'Content-Type': 'application/json' },
1305
- body: JSON.stringify({ tunnelId })
1306
- })
1307
- if (res.ok) {
1308
- await loadState()
1309
- }
1310
- } catch (_) {}
1311
- }
1312
-
1313
- const activeProfile = profiles.find((p) => p.id === activeId)
1314
-
1315
- return React.createElement(
1316
- 'div',
1317
- { className: 'drw-page' },
1318
-
1319
- // Header with Live Badges (ClineBot Style)
1320
- React.createElement(
1321
- 'div',
1322
- { className: 'drw-header' },
1323
- React.createElement(
1324
- 'div',
1325
- { className: 'drw-header-top' },
1326
- React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
1327
- React.createElement(
1328
- 'div',
1329
- { className: 'drw-header-badges' },
1330
- activeProfile
1331
- ? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
1332
- : React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
1333
- activeProfile
1334
- ? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
1335
- : null,
1336
- React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
1337
- )
1338
- ),
1339
- React.createElement('div', { className: 'drw-page-sub' }, t('subtitle'))
1340
- ),
1341
-
1342
- // Section 1: SSH Profiles Table & Management
1343
- React.createElement(
1344
- 'div',
1345
- { className: 'drw-card' },
1346
- React.createElement(
1347
- 'div',
1348
- { className: 'drw-card-title' },
1349
- React.createElement('span', null, t('profilesTitle')),
1350
- !editing
1351
- ? React.createElement(
1352
- 'button',
1353
- {
1354
- type: 'button',
1355
- className: 'drw-btn drw-btn-primary',
1356
- onClick: () => {
1357
- setEditing({
1358
- id: 'prof_' + Math.random().toString(36).substr(2, 9),
1359
- name: 'New Server',
1360
- host: '',
1361
- port: 22,
1362
- username: 'root',
1363
- authType: 'key',
1364
- privateKeyPath: '~/.ssh/id_rsa',
1365
- passphrase: '',
1366
- password: '',
1367
- remoteWorkspace: '',
1368
- localMirrorPath: ''
1369
- })
1370
- setTestResult(null)
1371
- }
1372
- },
1373
- t('addBtn')
1374
- )
1375
- : null
1376
- ),
1377
- React.createElement('div', { className: 'drw-card-desc' }, t('profilesDesc')),
1378
-
1379
- // Interactive Editing Form
1380
- editing
1381
- ? React.createElement(
1382
- 'div',
1383
- {
1384
- style: {
1385
- padding: '16px',
1386
- borderRadius: '10px',
1387
- background: 'var(--dsw-alias-bg-layer-2)',
1388
- border: '1px solid var(--dsw-alias-border-l2)',
1389
- display: 'flex',
1390
- flexDirection: 'column',
1391
- gap: '14px'
1392
- }
1393
- },
1394
- React.createElement(
1395
- 'div',
1396
- { style: { fontWeight: 600, fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } },
1397
- editing.name ? t('formTitleEdit') : t('formTitleNew')
1398
- ),
1399
- React.createElement(
1400
- 'div',
1401
- { className: 'drw-grid-2' },
1402
- React.createElement(
1403
- 'div',
1404
- { className: 'drw-field' },
1405
- React.createElement('span', { className: 'drw-label' }, t('fName')),
1406
- React.createElement('input', {
1407
- className: 'drw-input',
1408
- value: editing.name || '',
1409
- onChange: (e) => setEditing({ ...editing, name: e.target.value })
1410
- })
1411
- ),
1412
- React.createElement(
1413
- 'div',
1414
- { className: 'drw-field' },
1415
- React.createElement('span', { className: 'drw-label' }, t('fHost')),
1416
- React.createElement('input', {
1417
- className: 'drw-input',
1418
- placeholder: '192.168.1.100 or server.domain.com',
1419
- value: editing.host || '',
1420
- onChange: (e) => setEditing({ ...editing, host: e.target.value })
1421
- })
1422
- ),
1423
- React.createElement(
1424
- 'div',
1425
- { className: 'drw-field' },
1426
- React.createElement('span', { className: 'drw-label' }, t('fPort')),
1427
- React.createElement('input', {
1428
- className: 'drw-input',
1429
- type: 'number',
1430
- value: editing.port || 22,
1431
- onChange: (e) => setEditing({ ...editing, port: parseInt(e.target.value, 10) || 22 })
1432
- })
1433
- ),
1434
- React.createElement(
1435
- 'div',
1436
- { className: 'drw-field' },
1437
- React.createElement('span', { className: 'drw-label' }, t('fUser')),
1438
- React.createElement('input', {
1439
- className: 'drw-input',
1440
- value: editing.username || '',
1441
- onChange: (e) => setEditing({ ...editing, username: e.target.value })
1442
- })
1443
- )
1444
- ),
1445
-
1446
- // Auth Method Segmented Switcher (Key vs Password)
1447
- React.createElement(
1448
- 'div',
1449
- { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
1450
- React.createElement('span', { className: 'drw-label' }, t('fAuthType')),
1451
- React.createElement(
1452
- 'div',
1453
- { className: 'drw-segmented' },
1454
- React.createElement(
1455
- 'button',
1456
- {
1457
- type: 'button',
1458
- className: `drw-segmented-item ${editing.authType !== 'password' ? 'drw-segmented-item-active' : ''}`,
1459
- onClick: () => setEditing({ ...editing, authType: 'key' })
1460
- },
1461
- '🔑 ' + t('fAuthKey')
1462
- ),
1463
- React.createElement(
1464
- 'button',
1465
- {
1466
- type: 'button',
1467
- className: `drw-segmented-item ${editing.authType === 'password' ? 'drw-segmented-item-active' : ''}`,
1468
- onClick: () => setEditing({ ...editing, authType: 'password' })
1469
- },
1470
- '🔒 ' + t('fAuthPass')
1471
- )
1472
- )
1473
- ),
1474
-
1475
- // Conditional Auth Inputs
1476
- editing.authType === 'password'
1477
- ? React.createElement(
1478
- 'div',
1479
- { className: 'drw-field' },
1480
- React.createElement('span', { className: 'drw-label' }, t('fPassword')),
1481
- React.createElement(
1482
- 'div',
1483
- { style: { display: 'flex', gap: '8px' } },
1484
- React.createElement('input', {
1485
- className: 'drw-input',
1486
- type: showPassword ? 'text' : 'password',
1487
- placeholder: '••••••••••••',
1488
- value: editing.password || '',
1489
- onChange: (e) => setEditing({ ...editing, password: e.target.value })
1490
- }),
1491
- React.createElement(
1492
- 'button',
1493
- {
1494
- type: 'button',
1495
- className: 'drw-btn',
1496
- style: { fontSize: '11px', whiteSpace: 'nowrap' },
1497
- onClick: () => setShowPassword((v) => !v)
1498
- },
1499
- showPassword ? 'Hide' : 'Show'
1500
- )
1501
- )
1502
- )
1503
- : React.createElement(
1504
- 'div',
1505
- { className: 'drw-grid-2' },
1506
- React.createElement(
1507
- 'div',
1508
- { className: 'drw-field' },
1509
- React.createElement('span', { className: 'drw-label' }, t('fKeyPath')),
1510
- React.createElement('input', {
1511
- className: 'drw-input',
1512
- placeholder: '~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
1513
- value: editing.privateKeyPath || '',
1514
- onChange: (e) => setEditing({ ...editing, privateKeyPath: e.target.value })
1515
- }),
1516
- React.createElement('span', { className: 'drw-hint' }, t('fKeyPathHint'))
1517
- ),
1518
- React.createElement(
1519
- 'div',
1520
- { className: 'drw-field' },
1521
- React.createElement('span', { className: 'drw-label' }, t('fPassphrase')),
1522
- React.createElement('input', {
1523
- className: 'drw-input',
1524
- type: 'password',
1525
- placeholder: 'Optional passphrase',
1526
- value: editing.passphrase || '',
1527
- onChange: (e) => setEditing({ ...editing, passphrase: e.target.value })
1528
- })
1529
- )
1530
- ),
1531
-
1532
- // Workspace Paths (Remote Workspace & Local Mirror)
1533
- React.createElement(
1534
- 'div',
1535
- { className: 'drw-grid-2' },
1536
- React.createElement(
1537
- 'div',
1538
- { className: 'drw-field' },
1539
- React.createElement('span', { className: 'drw-label' }, t('fRemotePath')),
1540
- React.createElement(
1541
- 'div',
1542
- { style: { display: 'flex', gap: '8px' } },
1543
- React.createElement('input', {
1544
- className: 'drw-input',
1545
- placeholder: '/home/user/project',
1546
- value: editing.remoteWorkspace || '',
1547
- onChange: (e) => setEditing({ ...editing, remoteWorkspace: e.target.value })
1548
- }),
1549
- React.createElement(
1550
- 'button',
1551
- {
1552
- type: 'button',
1553
- className: 'drw-btn',
1554
- style: { whiteSpace: 'nowrap', fontSize: '12px' },
1555
- disabled: !editing.host,
1556
- onClick: () => openDirectoryBrowser(editing.remoteWorkspace || '/')
1557
- },
1558
- t('btnBrowse')
1559
- )
1560
- ),
1561
- React.createElement('span', { className: 'drw-hint' }, t('fRemotePathHint'))
1562
- ),
1563
- React.createElement(
1564
- 'div',
1565
- { className: 'drw-field' },
1566
- React.createElement('span', { className: 'drw-label' }, t('fLocalMirror')),
1567
- React.createElement('input', {
1568
- className: 'drw-input',
1569
- placeholder: '~/.dsh/mirrors/my-project',
1570
- value: editing.localMirrorPath || '',
1571
- onChange: (e) => setEditing({ ...editing, localMirrorPath: e.target.value })
1572
- }),
1573
- React.createElement('span', { className: 'drw-hint' }, t('fLocalMirrorHint'))
1574
- )
1575
- ),
1576
-
1577
- // Directory Browser Modal
1578
- browserOpen
1579
- ? React.createElement(
1580
- 'div',
1581
- { className: 'drw-browser-modal' },
1582
- React.createElement(
1583
- 'div',
1584
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
1585
- React.createElement('span', { style: { fontSize: '12px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, `📂 ${browserPath}`),
1586
- React.createElement(
1587
- 'div',
1588
- { style: { display: 'flex', gap: '6px' } },
1589
- React.createElement(
1590
- 'button',
1591
- {
1592
- type: 'button',
1593
- className: 'drw-btn drw-btn-primary',
1594
- style: { fontSize: '11px', padding: '3px 8px' },
1595
- onClick: () => {
1596
- setEditing({ ...editing, remoteWorkspace: browserPath })
1597
- setBrowserOpen(false)
1598
- }
1599
- },
1600
- t('btnSelectThis')
1601
- ),
1602
- React.createElement(
1603
- 'button',
1604
- {
1605
- type: 'button',
1606
- className: 'drw-btn',
1607
- style: { fontSize: '11px', padding: '3px 8px' },
1608
- onClick: () => setBrowserOpen(false)
1609
- },
1610
- '✕'
1611
- )
1612
- )
1613
- ),
1614
- React.createElement(
1615
- 'div',
1616
- { className: 'drw-browser-list' },
1617
- browserLoading
1618
- ? React.createElement('div', { style: { padding: '8px', fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, 'Loading directory entries...')
1619
- : React.createElement(
1620
- React.Fragment,
1621
- null,
1622
- browserPath !== '/'
1623
- ? React.createElement(
1624
- 'div',
1625
- {
1626
- className: 'drw-browser-item',
1627
- onClick: () => {
1628
- const parent = browserPath.substring(0, browserPath.lastIndexOf('/')) || '/'
1629
- fetchRemoteDir(parent)
1630
- }
1631
- },
1632
- '📁 .. (parent directory)'
1633
- )
1634
- : null,
1635
- browserEntries
1636
- .filter((e) => e.isDirectory)
1637
- .map((e) =>
1638
- React.createElement(
1639
- 'div',
1640
- {
1641
- key: e.filename,
1642
- className: 'drw-browser-item',
1643
- onClick: () => {
1644
- const next = browserPath === '/' ? `/${e.filename}` : `${browserPath}/${e.filename}`
1645
- fetchRemoteDir(next)
1646
- }
1647
- },
1648
- `📁 ${e.filename}`
1649
- )
1650
- )
1651
- )
1652
- )
1653
- )
1654
- : null,
1655
-
1656
- // Form Action Buttons
1657
- React.createElement(
1658
- 'div',
1659
- { className: 'drw-row', style: { justifyContent: 'flex-end', marginTop: '6px' } },
1660
- React.createElement(
1661
- 'button',
1662
- {
1663
- type: 'button',
1664
- className: 'drw-btn',
1665
- disabled: !editing.host || !!busy,
1666
- onClick: () => handleTest(editing)
1667
- },
1668
- busy === 'testing' ? t('testing') : t('btnTest')
1669
- ),
1670
- React.createElement(
1671
- 'button',
1672
- {
1673
- type: 'button',
1674
- className: 'drw-btn',
1675
- onClick: () => {
1676
- setEditing(null)
1677
- setBrowserOpen(false)
1678
- setTestResult(null)
1679
- }
1680
- },
1681
- t('btnCancel')
1682
- ),
1683
- React.createElement(
1684
- 'button',
1685
- {
1686
- type: 'button',
1687
- className: 'drw-btn drw-btn-primary',
1688
- disabled: !editing.host || !!busy,
1689
- onClick: () => handleSave(editing)
1690
- },
1691
- busy === 'saving' ? t('saving') : t('btnSave')
1692
- )
1693
- ),
1694
- testResult
1695
- ? React.createElement(
1696
- 'div',
1697
- {
1698
- className: 'drw-preview',
1699
- style: {
1700
- borderColor: testResult.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
1701
- }
1702
- },
1703
- testResult.ok
1704
- ? t('testOk', { latency: testResult.latencyMs ?? testResult.latency ?? 0, os: testResult.remoteOs || testResult.os || 'Linux' })
1705
- : t('testFail', { err: testResult.error || 'Connection timed out' })
1706
- )
1707
- : null
1708
- )
1709
- : null,
1710
-
1711
- // Profiles List Table
1712
- profiles.length > 0
1713
- ? React.createElement(
1714
- 'table',
1715
- { className: 'drw-table' },
1716
- React.createElement(
1717
- 'thead',
1718
- null,
1719
- React.createElement(
1720
- 'tr',
1721
- null,
1722
- React.createElement('th', null, t('nameCol')),
1723
- React.createElement('th', null, t('hostCol')),
1724
- React.createElement('th', null, t('portCol')),
1725
- React.createElement('th', null, t('pathCol')),
1726
- React.createElement('th', { style: { textAlign: 'right' } }, t('actionsCol'))
1727
- )
1728
- ),
1729
- React.createElement(
1730
- 'tbody',
1731
- null,
1732
- profiles.map((p) => {
1733
- const isAct = p.id === activeId
1734
- return React.createElement(
1735
- 'tr',
1736
- { key: p.id },
1737
- React.createElement(
1738
- 'td',
1739
- null,
1740
- React.createElement('span', { style: { fontWeight: 600 } }, p.name || 'Server'),
1741
- React.createElement(
1742
- 'span',
1743
- { className: 'drw-badge', style: { marginLeft: '8px' } },
1744
- p.authType === 'password' ? '🔒 pwd' : '🔑 key'
1745
- ),
1746
- isAct
1747
- ? React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginLeft: '6px' } }, t('activeBadge'))
1748
- : null
1749
- ),
1750
- React.createElement('td', { style: { fontFamily: 'monospace' } }, `${p.username || 'root'}@${p.host}`),
1751
- React.createElement('td', { style: { fontFamily: 'monospace' } }, p.port || 22),
1752
- React.createElement('td', { style: { fontFamily: 'monospace', fontSize: '12px' } }, p.remoteWorkspace || '—'),
1753
- React.createElement(
1754
- 'td',
1755
- { style: { textAlign: 'right' } },
1756
- React.createElement(
1757
- 'div',
1758
- { style: { display: 'inline-flex', gap: '6px' } },
1759
- !isAct
1760
- ? React.createElement(
1761
- 'button',
1762
- {
1763
- type: 'button',
1764
- className: 'drw-btn',
1765
- style: { fontSize: '11px', padding: '3px 8px' },
1766
- onClick: () => handleSetActive(p.id)
1767
- },
1768
- t('pinBtn')
1769
- )
1770
- : null,
1771
- React.createElement(
1772
- 'button',
1773
- {
1774
- type: 'button',
1775
- className: 'drw-btn',
1776
- style: { fontSize: '11px', padding: '3px 8px' },
1777
- onClick: () => {
1778
- setEditing({ ...p })
1779
- setTestResult(null)
1780
- }
1781
- },
1782
- t('editBtn')
1783
- ),
1784
- React.createElement(
1785
- 'button',
1786
- {
1787
- type: 'button',
1788
- className: 'drw-btn drw-btn-danger',
1789
- style: { fontSize: '11px', padding: '3px 8px' },
1790
- onClick: () => handleDelete(p.id, p.name)
1791
- },
1792
- t('delBtn')
1793
- )
1794
- )
1795
- )
1796
- )
1797
- })
1798
- )
1799
- )
1800
- : !editing
1801
- ? React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('noHosts'))
1802
- : null
1803
- ),
1804
-
1805
- // Section 2: Active Connection Diagnostics (ClineBot Card 4 style)
1806
- activeProfile
1807
- ? React.createElement(
1808
- 'div',
1809
- { className: 'drw-card' },
1810
- React.createElement(
1811
- 'div',
1812
- { className: 'drw-card-title' },
1813
- React.createElement('span', null, t('diagTitle')),
1814
- React.createElement(
1815
- 'button',
1816
- {
1817
- type: 'button',
1818
- className: 'drw-btn',
1819
- style: { fontSize: '11px', padding: '3px 9px' },
1820
- disabled: busy === 'testing',
1821
- onClick: () => handleTest(activeProfile)
1822
- },
1823
- busy === 'testing' ? t('testing') : t('diagProbeBtn')
1824
- )
1825
- ),
1826
- React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
1827
- React.createElement(
1828
- 'div',
1829
- { className: 'drw-grid-4' },
1830
- React.createElement(
1831
- 'div',
1832
- { className: 'drw-stat-box' },
1833
- React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
1834
- React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
1835
- ),
1836
- React.createElement(
1837
- 'div',
1838
- { className: 'drw-stat-box' },
1839
- React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
1840
- React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
1841
- ),
1842
- React.createElement(
1843
- 'div',
1844
- { className: 'drw-stat-box' },
1845
- React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
1846
- React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
1847
- ),
1848
- React.createElement(
1849
- 'div',
1850
- { className: 'drw-stat-box' },
1851
- React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
1852
- React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
1853
- )
1854
- )
1855
- )
1856
- : null,
1857
-
1858
- // Section 3: Mirror Sync (ClineBot Action Card style)
1859
- activeProfile
1860
- ? React.createElement(
1861
- 'div',
1862
- { className: 'drw-card' },
1863
- React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
1864
- React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
1865
-
1866
- React.createElement(
1867
- 'div',
1868
- { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)', marginBottom: '12px' } },
1869
- React.createElement(
1870
- 'div',
1871
- { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
1872
- React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, t('autoSyncTitle')),
1873
- React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('autoSyncDesc'))
1874
- ),
1875
- React.createElement(
1876
- 'button',
1877
- {
1878
- type: 'button',
1879
- className: `drw-btn ${autoSync ? 'drw-btn-primary' : ''}`,
1880
- style: { padding: '4px 12px', fontSize: '12px' },
1881
- onClick: async () => {
1882
- const next = !autoSync
1883
- try {
1884
- const res = await fetch('/dsh-remote-workspace/watcher/toggle', {
1885
- method: 'POST',
1886
- headers: { 'Content-Type': 'application/json' },
1887
- body: JSON.stringify({ enabled: next })
1888
- })
1889
- const d = await res.json()
1890
- if (d.ok) setAutoSync(d.autoSync)
1891
- } catch (_) {}
1892
- }
1893
- },
1894
- autoSync ? '⚡ Active (ON)' : '⚪ Off'
1895
- )
1896
- ),
1897
-
1898
- React.createElement(
1899
- 'div',
1900
- { className: 'drw-row' },
1901
- React.createElement(
1902
- 'button',
1903
- {
1904
- type: 'button',
1905
- className: 'drw-btn',
1906
- disabled: !!busy || !activeProfile.remoteWorkspace || !activeProfile.localMirrorPath,
1907
- onClick: () => handleSync('pull')
1908
- },
1909
- busy === 'sync_pull' ? t('pulling') : t('pullBtn')
1910
- ),
1911
- React.createElement(
1912
- 'button',
1913
- {
1914
- type: 'button',
1915
- className: 'drw-btn',
1916
- disabled: !!busy || !activeProfile.remoteWorkspace || !activeProfile.localMirrorPath,
1917
- onClick: () => handleSync('push')
1918
- },
1919
- busy === 'sync_push' ? t('pushing') : t('pushBtn')
1920
- )
1921
- ),
1922
- syncMsg
1923
- ? React.createElement(
1924
- 'div',
1925
- {
1926
- className: 'drw-preview',
1927
- style: {
1928
- borderColor: syncMsg.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
1929
- }
1930
- },
1931
- syncMsg.text
1932
- )
1933
- : null
1934
- )
1935
- : null,
1936
-
1937
- // Section 4: SSH Port Forwarding (Tunnels Manager)
1938
- React.createElement(
1939
- 'div',
1940
- { className: 'drw-card' },
1941
- React.createElement('div', { className: 'drw-card-title' }, t('tunnelsTitle')),
1942
- React.createElement('div', { className: 'drw-card-desc' }, t('tunnelsDesc')),
1943
-
1944
- // Start tunnel inline row
1945
- activeProfile
1946
- ? React.createElement(
1947
- 'div',
1948
- { className: 'drw-row', style: { alignItems: 'flex-end', background: 'var(--dsw-alias-bg-layer-2)', padding: '10px 12px', borderRadius: '8px' } },
1949
- React.createElement(
1950
- 'div',
1951
- { style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
1952
- React.createElement('span', { className: 'drw-label' }, t('tLocalPort')),
1953
- React.createElement('input', {
1954
- className: 'drw-input',
1955
- style: { width: '120px' },
1956
- type: 'number',
1957
- value: newTunnelLocal,
1958
- onChange: (e) => setNewTunnelLocal(e.target.value)
1959
- })
1960
- ),
1961
- React.createElement('span', { style: { paddingBottom: '8px', color: 'var(--dsw-alias-label-secondary)' } }, '→'),
1962
- React.createElement(
1963
- 'div',
1964
- { style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
1965
- React.createElement('span', { className: 'drw-label' }, t('tRemotePort')),
1966
- React.createElement('input', {
1967
- className: 'drw-input',
1968
- style: { width: '120px' },
1969
- type: 'number',
1970
- value: newTunnelRemote,
1971
- onChange: (e) => setNewTunnelRemote(e.target.value)
1972
- })
1973
- ),
1974
- React.createElement(
1975
- 'button',
1976
- {
1977
- type: 'button',
1978
- className: 'drw-btn drw-btn-primary',
1979
- disabled: busy === 'tunnel_open',
1980
- onClick: handleStartTunnel
1981
- },
1982
- busy === 'tunnel_open' ? t('tOpening') : t('tOpenBtn')
1983
- )
1984
- )
1985
- : null,
1986
-
1987
- tunnelMsg
1988
- ? React.createElement(
1989
- 'div',
1990
- {
1991
- className: 'drw-preview',
1992
- style: {
1993
- borderColor: tunnelMsg.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
1994
- }
1995
- },
1996
- tunnelMsg.text
1997
- )
1998
- : null,
1999
-
2000
- // Tunnels Table
2001
- tunnels.length > 0
2002
- ? React.createElement(
2003
- 'table',
2004
- { className: 'drw-table' },
2005
- React.createElement(
2006
- 'thead',
2007
- null,
2008
- React.createElement(
2009
- 'tr',
2010
- null,
2011
- React.createElement('th', null, t('tunnelsColId')),
2012
- React.createElement('th', null, t('tunnelsColRoute')),
2013
- React.createElement('th', { style: { textAlign: 'right' } }, t('tunnelsColActions'))
2014
- )
2015
- ),
2016
- React.createElement(
2017
- 'tbody',
2018
- null,
2019
- tunnels.map((tun) =>
2020
- React.createElement(
2021
- 'tr',
2022
- { key: tun.id },
2023
- React.createElement('td', { style: { fontFamily: 'monospace', fontSize: '12px' } }, tun.id),
2024
- React.createElement(
2025
- 'td',
2026
- null,
2027
- React.createElement('span', { className: 'drw-badge drw-badge-ok' }, `127.0.0.1:${tun.localPort}`),
2028
- ' → ',
2029
- React.createElement('span', { className: 'drw-badge' }, `${tun.targetHost || '127.0.0.1'}:${tun.remotePort}`)
2030
- ),
2031
- React.createElement(
2032
- 'td',
2033
- { style: { textAlign: 'right' } },
2034
- React.createElement(
2035
- 'button',
2036
- {
2037
- type: 'button',
2038
- className: 'drw-btn drw-btn-danger',
2039
- style: { fontSize: '11px', padding: '3px 8px' },
2040
- onClick: () => handleStopTunnel(tun.id)
2041
- },
2042
- t('tStopBtn')
2043
- )
2044
- )
2045
- )
2046
- )
2047
- )
2048
- )
2049
- : React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('tNoTunnels'))
2050
- )
2051
- )
2052
- }
2053
-
2054
- // Accordion item for Settings -> Plugins section
2055
- function PluginCard(props) {
2056
- const [open, setOpen] = React.useState(false)
2057
- const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
2058
-
2059
- React.useEffect(() => {
2060
- ensureCss()
2061
- }, [])
2062
-
2063
- return React.createElement(
2064
- 'li',
2065
- { className: 'drw-card', style: { listStyle: 'none', marginBottom: '12px' } },
2066
- React.createElement(
2067
- 'button',
2068
- {
2069
- type: 'button',
2070
- style: {
2071
- background: 'none',
2072
- border: 'none',
2073
- cursor: 'pointer',
2074
- display: 'flex',
2075
- alignItems: 'center',
2076
- width: '100%',
2077
- padding: 0,
2078
- textAlign: 'left',
2079
- },
2080
- 'aria-expanded': open,
2081
- onClick: () => setOpen((v) => !v),
2082
- },
2083
- React.createElement('span', { style: { fontSize: '20px', marginRight: '12px' } }, '🌐'),
2084
- React.createElement(
2085
- 'div',
2086
- { style: { flex: 1 } },
2087
- React.createElement('div', { style: { fontWeight: 600, fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, t('title')),
2088
- React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
2089
- ),
2090
- React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginRight: '14px' } }, t('status')),
2091
- React.createElement(
2092
- 'span',
2093
- { style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
2094
- React.createElement(FallbackChevron)
2095
- )
2096
- ),
2097
- open
2098
- ? React.createElement(
2099
- 'div',
2100
- { style: { marginTop: '16px', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '16px' } },
2101
- React.createElement(SettingsView, props)
2102
- )
2103
- : null
2104
- )
2105
- }
2106
-
2107
- function RemoteWorkspaceChip(props) {
2108
- ensureCss()
2109
- const [activeHost, setActiveHost] = React.useState(null)
2110
-
2111
- React.useEffect(() => {
2112
- fetch('/dsh-remote-workspace/state')
2113
- .then((r) => r.json())
2114
- .then((d) => {
2115
- if (d.ok && d.activeId && d.profiles) {
2116
- const f = d.profiles.find((p) => p.id === d.activeId)
2117
- if (f) setActiveHost(f.name || f.host)
2118
- }
2119
- })
2120
- .catch(() => {})
2121
- }, [])
2122
-
2123
- return React.createElement(
2124
- 'div',
2125
- {
2126
- style: {
2127
- display: 'flex',
2128
- alignItems: 'center',
2129
- gap: '6px',
2130
- padding: '2px 8px',
2131
- fontSize: '11px',
2132
- fontFamily: 'monospace',
2133
- borderRadius: '999px',
2134
- border: '1px solid var(--dsw-alias-border-l2)',
2135
- background: 'var(--dsw-alias-bg-layer-1)',
2136
- color: activeHost ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
2137
- cursor: 'pointer'
2138
- }
2139
- },
2140
- React.createElement('span', null, '🌐'),
2141
- React.createElement('span', null, activeHost ? `SSH: ${activeHost}` : 'SSH: Local')
2142
- )
2143
- }
2144
-
2145
- function apply(ctx) {
2146
- if (ctx.locale && typeof ctx.locale.register === 'function') {
2147
- try { ctx.locale.register(NS, { en, zh }) } catch (_) {}
2148
- }
2149
-
2150
- if (ctx.slots && typeof ctx.slots.inject === 'function') {
2151
- ctx.slots.inject('settings.plugin.item', () =>
2152
- ctx.slots.register(
2153
- {
2154
- name: 'settings.plugin.item',
2155
- key: NS,
2156
- locale: NS,
2157
- inject: () => ({ ctx })
2158
- },
2159
- (props) => React.createElement(PluginCard, Object.assign({}, props, { ctx }))
2160
- )
2161
- )
2162
-
2163
- ctx.slots.inject('conversation.session.header.utilities', () =>
2164
- ctx.slots.register(
2165
- {
2166
- name: 'conversation.session.header.utilities',
2167
- id: '@goodandready/dsh-remote-workspace',
2168
- order: 30
2169
- },
2170
- (props) => React.createElement(RemoteWorkspaceChip, props)
2171
- )
2172
- )
2173
- }
2174
- }
2175
-
2176
- module.exports = { apply, inject: ['slots', 'locale'] }
2177
- return module.exports
2178
- }
2179
- })
1
+ // Remote Workspace Settings Card (settings.plugin.item) & Session Utility Chip.
2
+ window.__ModuleLoader__.load({
3
+ id: '@goodandready/dsh-remote-workspace',
4
+ factory: (require) => {
5
+ var module = { exports: {} }
6
+ var exports = module.exports
7
+ const React = require('react')
8
+
9
+ const NS = 'dsh-remote-workspace'
10
+
11
+ function ensureCss() {
12
+ if (typeof document === 'undefined') return
13
+ if (document.getElementById('drw-clinebot-full-css')) return
14
+ const style = document.createElement('style')
15
+ style.id = 'drw-clinebot-full-css'
16
+ style.dataset.dshPlugin = NS
17
+ style.textContent = `
18
+ .drw-page{display:flex;flex-direction:column;gap:18px;padding:6px 0 24px;max-width:960px}
19
+ .drw-header{display:flex;flex-direction:column;gap:8px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}
20
+ .drw-header-top{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px}
21
+ .drw-page-title{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px}
22
+ .drw-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}
23
+ .drw-header-badges{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
24
+
25
+ .drw-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:14px;list-style:none}
26
+ .drw-card-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}
27
+ .drw-card-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}
28
+
29
+ .drw-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
30
+ .drw-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:12px}
31
+ .drw-grid-4{display:grid;grid-template-columns:repeat(auto-fit, minmax(190px, 1fr));gap:10px}
32
+
33
+ .drw-badge{font-size:11px;padding:3px 9px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500;font-family:monospace}
34
+ .drw-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-bg, var(--dsw-alias-bg-layer-2))}
35
+ .drw-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:var(--dsw-alias-state-warning-bg, var(--dsw-alias-bg-layer-2))}
36
+ .drw-badge-err{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-bg, var(--dsw-alias-bg-layer-2))}
37
+
38
+ .drw-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;width:100%;box-sizing:border-box}
39
+ .drw-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
40
+ .drw-label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary);margin-bottom:2px}
41
+ .drw-hint{font-size:11px;color:var(--dsw-alias-label-dimmed, var(--dsw-alias-label-secondary));line-height:1.3}
42
+ .drw-field{display:flex;flex-direction:column;gap:3px;flex:1;min-width:180px}
43
+
44
+ .drw-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
45
+ .drw-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}
46
+ .drw-btn:disabled{opacity:0.6;cursor:not-allowed}
47
+ .drw-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
48
+ .drw-btn-primary:hover:not(:disabled){opacity:0.9}
49
+ .drw-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-border, var(--dsw-alias-border-l2))}
50
+ .drw-btn-danger:hover:not(:disabled){background:var(--dsw-alias-state-error-bg, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-state-error-border, var(--dsw-alias-border-l2))}
51
+
52
+ .drw-segmented{display:inline-flex;border-radius:8px;padding:2px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);gap:2px}
53
+ .drw-segmented-item{appearance:none;border:none;background:none;cursor:pointer;padding:5px 12px;font-size:12px;font-weight:500;border-radius:6px;color:var(--dsw-alias-label-secondary);transition:all .15s ease}
54
+ .drw-segmented-item-active{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);font-weight:600}
55
+
56
+ .drw-table{width:100%;border-collapse:collapse;margin-top:4px}
57
+ .drw-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:600}
58
+ .drw-table td{padding:9px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;color:var(--dsw-alias-label-primary)}
59
+ .drw-table tr:hover{background:var(--dsw-alias-bg-layer-2)}
60
+
61
+ .drw-stat-box{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:3px}
62
+ .drw-stat-val{font-size:14px;font-weight:700;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
63
+ .drw-stat-lbl{font-size:11px;color:var(--dsw-alias-label-secondary)}
64
+ .drw-preview{padding:10px 12px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);font-family:monospace;font-size:12px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--dsw-alias-border-l2);line-height:1.4}
65
+
66
+ .drw-browser-modal{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:12px;display:flex;flex-direction:column;gap:8px;max-height:280px}
67
+ .drw-browser-list{overflow-y:auto;display:flex;flex-direction:column;gap:2px;max-height:190px;padding-right:4px}
68
+ .drw-browser-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;cursor:pointer;font-size:13px;font-family:monospace}
69
+ .drw-browser-item:hover{background:var(--dsw-alias-bg-layer-3)}
70
+
71
+ .drw-tabs{display:flex;gap:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:10px;margin-bottom:14px;overflow-x:auto}
72
+ .drw-tab-btn{appearance:none;border:1px solid transparent;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);padding:6px 14px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .15s ease}
73
+ .drw-tab-btn:hover{background:var(--dsw-alias-bg-layer-4);color:var(--dsw-alias-label-primary)}
74
+ .drw-tab-btn.active{background:var(--dsw-alias-bg-layer-4);border-color:var(--dsw-alias-state-brand-primary);color:var(--dsw-alias-state-brand-primary)}
75
+
76
+ .drw-term-window{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;display:flex;flex-direction:column;height:340px;overflow:hidden;box-shadow:0 4px 14px var(--dsw-alias-border-l1)}
77
+ .drw-term-header{background:var(--dsw-alias-bg-layer-2);padding:6px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12px;color:var(--dsw-alias-label-secondary);font-family:monospace}
78
+ .drw-term-body{flex:1;padding:10px 12px;overflow-y:auto;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12.5px;line-height:1.45;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-all}
79
+ .drw-term-footer{display:flex;background:var(--dsw-alias-bg-layer-2);border-top:1px solid var(--dsw-alias-border-l2);padding:6px}
80
+ .drw-term-input{flex:1;background:transparent;border:none;color:var(--dsw-alias-link-primary, var(--dsw-alias-label-primary));font-family:monospace;font-size:13px;padding:4px 8px;outline:none}
81
+
82
+ .drw-explorer-box{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:12px;display:flex;flex-direction:column;gap:10px}
83
+ .drw-explorer-nav{display:flex;align-items:center;gap:8px;font-family:monospace;font-size:12px;background:var(--dsw-alias-bg-layer-1);padding:6px 10px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2)}
84
+ .drw-file-list{display:flex;flex-direction:column;gap:3px;max-height:260px;overflow-y:auto}
85
+ .drw-file-row{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-radius:6px;cursor:pointer;font-size:13px;font-family:monospace;transition:background .1s ease}
86
+ .drw-file-row:hover{background:var(--dsw-alias-bg-layer-3)}
87
+ .drw-file-meta{display:flex;align-items:center;gap:12px;color:var(--dsw-alias-label-secondary);font-size:11.5px}
88
+
89
+ .drw-modal-overlay{position:fixed;inset:0;background:var(--dsw-alias-mask, var(--dsw-alias-bg-layer-1));z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px}
90
+ .drw-modal-dialog{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;width:100%;max-width:800px;max-height:85vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 12px 32px var(--dsw-alias-border-l1)}
91
+ .drw-modal-hdr{padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2);display:flex;align-items:center;justify-content:space-between;font-weight:600}
92
+ .drw-modal-body{flex:1;padding:12px 16px;overflow-y:auto}
93
+
94
+ .drw-health-bar{display:grid;grid-template-columns:repeat(auto-fit, minmax(130px, 1fr));gap:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;font-size:11.5px;font-family:monospace}
95
+ .drw-health-item{display:flex;flex-direction:column;gap:2px}
96
+ .drw-progress{height:6px;background:var(--dsw-alias-bg-layer-1);border-radius:3px;overflow:hidden}
97
+ .drw-progress-fill{height:100%;border-radius:3px;transition:width .2s ease}
98
+
99
+ .drw-container-row{display:grid;grid-template-columns:2fr 2fr 1.5fr 1.5fr auto;align-items:center;gap:10px;padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12.5px;font-family:monospace}
100
+ .drw-container-row:hover{background:var(--dsw-alias-bg-layer-3)}
101
+
102
+ .drw-env-table{width:100%;border-collapse:collapse}
103
+ .drw-env-row{display:grid;grid-template-columns:220px 1fr 90px;gap:8px;padding:6px 0;align-items:center;border-bottom:1px solid var(--dsw-alias-border-l2)}
104
+ .drw-alert-badge{padding:4px 8px;border-radius:6px;font-size:11px;font-family:monospace;display:flex;align-items:center;gap:6px}
105
+ .drw-alert-critical{background:var(--dsw-alias-state-error-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-border, var(--dsw-alias-border-l2))}
106
+ .drw-alert-warning{background:var(--dsw-alias-state-warning-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-state-warning-border, var(--dsw-alias-border-l2))}
107
+
108
+
109
+ `
110
+ document.head.appendChild(style)
111
+ }
112
+
113
+ const en = {
114
+ title: 'Remote Workspace',
115
+ subtitle: 'Native SSH connections, SFTP file access, 3-way mirror sync, and port forwarding tunnels.',
116
+ status: 'Ready',
117
+
118
+ tabProfiles: '🖥️ Host Profiles',
119
+
120
+ tabContainers: '🐳 Containers',
121
+
122
+ tabEnv: '🗃️ .env Manager',
123
+ btnImportSsh: 'Import ~/.ssh/config',
124
+ envTitle: 'Remote Environment Variables (.env)',
125
+ envKey: 'Key',
126
+ envVal: 'Value',
127
+ envSaveVar: 'Save',
128
+ envShowVal: 'Show',
129
+ envHideVal: 'Hide',
130
+ tarSyncFast: '🚀 Fast Tarball Stream',
131
+ diagnoseTitle: 'Quick Diagnostics',
132
+ diagnosePorts: 'Open Ports',
133
+ diagnoseOom: 'OOM Killer Check',
134
+ diagnoseDisk: 'Disk Usage',
135
+ alertLabel: 'Alerts ({count})',
136
+
137
+ containersTitle: 'Remote Docker / Podman Engine',
138
+ containerName: 'Container',
139
+ containerImage: 'Image',
140
+ containerStatus: 'Status',
141
+ containerPorts: 'Ports',
142
+ btnLogs: 'Logs',
143
+ btnRestart: 'Restart',
144
+ btnStop: 'Stop',
145
+ noContainers: 'No active containers found or Docker daemon not running.',
146
+ healthCpu: 'CPU: {val}%',
147
+ healthMem: 'RAM: {val}% ({used}/{total} MB)',
148
+ healthDisk: 'Disk: {val}%',
149
+ healthUptime: 'Up: {val}',
150
+ autoSyncTitle: 'Live Auto-Sync on Save',
151
+ autoSyncDesc: 'Automatically push modified files to remote workspace on local save.',
152
+ btnTransfer: 'Copy to Host...',
153
+ transferTitle: 'Server-to-Server Direct Transfer',
154
+ transferDestHost: 'Destination Server',
155
+ transferDestPath: 'Destination File Path',
156
+ transferBtnExec: 'Transfer Directly',
157
+ transferSuccess: 'Successfully transferred {bytes} bytes to {host}:{path}',
158
+ fJumpHost: 'Jump Host (Bastion)',
159
+ fJumpHostNone: 'Direct Connection (No Jump Host)',
160
+
161
+ tabTerminal: '⚡ Web Terminal',
162
+ tabExplorer: '📂 File Explorer',
163
+ tabTunnels: '🔗 Port Tunnels',
164
+ tabSync: '🔄 Sync Station',
165
+ vaultProtectedBadge: '🛡️ .env Vault Protected',
166
+ termStart: 'Connect Terminal',
167
+ termStop: 'Disconnect',
168
+ termClear: 'Clear',
169
+ termPlaceholder: 'Type command and press Enter (e.g. htop, ls, docker ps)...',
170
+ termConnected: 'Connected to PTY session {id} on {host}',
171
+ termDisconnected: 'Terminal disconnected.',
172
+ explorerTitle: 'Remote Filesystem Explorer',
173
+ filePreviewTitle: 'File Viewer: {path}',
174
+ fileSize: '{size} KB',
175
+ btnSaveFile: 'Save Changes',
176
+ fileSaved: 'File saved successfully.',
177
+ tunnelTraffic: 'RX: {rx} | TX: {tx}',
178
+ tunnelUptime: 'Up: {sec}s',
179
+ openLocalLink: '🌐 Open Local Port',
180
+ badgeOnline: 'Active Host: {name} ({latency} ms)',
181
+ badgeOffline: 'No active host',
182
+ badgeTunnels: 'Tunnels: {count}',
183
+ badgeKey: '🔑 Key Auth',
184
+ badgePassword: '🔒 Password Auth',
185
+ profilesTitle: '🖥️ Remote SSH Profiles',
186
+ profilesDesc: 'Configure remote machines for code execution, SFTP browsing, and background mirror sync.',
187
+ addBtn: '+ Add Host',
188
+ nameCol: 'Name',
189
+ hostCol: 'Host / User',
190
+ portCol: 'Port',
191
+ pathCol: 'Remote Workspace',
192
+ actionsCol: 'Actions',
193
+ noHosts: 'No remote hosts configured yet. Click "+ Add Host" to create your first connection.',
194
+ activeBadge: 'Active',
195
+ pinBtn: 'Pin Active',
196
+ editBtn: 'Edit',
197
+ delBtn: 'Delete',
198
+ delConfirm: 'Delete host profile "{name}"?',
199
+ formTitleNew: 'Add New Host Profile',
200
+ formTitleEdit: 'Edit Host Profile',
201
+ fName: 'Profile Name',
202
+ fHost: 'Host / IP Address',
203
+ fPort: 'Port',
204
+ fUser: 'Username',
205
+ fAuthType: 'Authentication Method',
206
+ fAuthKey: 'SSH Private Key',
207
+ fAuthPass: 'Password',
208
+ fKeyPath: 'Private Key Path',
209
+ fKeyPathHint: 'Path to ~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
210
+ fPassphrase: 'Key Passphrase (optional)',
211
+ fPassword: 'Account Password',
212
+ fRemotePath: 'Remote Workspace Directory',
213
+ fRemotePathHint: 'Absolute path on the server (e.g. /home/user/my-project)',
214
+ fLocalMirror: 'Local Mirror Directory',
215
+ fLocalMirrorHint: 'Local directory where files are mirrored for fast 3-way synchronization',
216
+ btnBrowse: '📁 Browse Remote...',
217
+ btnSelectThis: 'Select Directory',
218
+ btnTest: '⚡ Test Connection (Ping)',
219
+ btnSave: 'Save Profile',
220
+ btnCancel: 'Cancel',
221
+ testing: 'Testing Connection...',
222
+ saving: 'Saving...',
223
+ testOk: '✅ Connection OK! Latency: {latency} ms | OS: {os}',
224
+ testFail: '❌ Connection Failed: {err}',
225
+ diagTitle: '📊 Active Connection Diagnostics',
226
+ diagDesc: 'Real-time telemetry and engine status for the currently selected remote machine.',
227
+ diagProbeBtn: '⚡ Quick Probe',
228
+ syncTitle: '🔄 Two-Way Workspace Mirror Sync',
229
+ syncDesc: 'Fast 3-way conflict-aware file synchronization between local mirror and remote server.',
230
+ pullBtn: '⬇️ Pull from Remote',
231
+ pushBtn: '⬆️ Push to Remote',
232
+ pulling: 'Pulling files…',
233
+ pushing: 'Pushing files…',
234
+ syncOk: 'Sync completed! Updated {count} file(s).',
235
+ syncConflict: 'Conflict warning: {count} file(s) modified on both sides.',
236
+ syncErr: 'Sync error: {err}',
237
+ tunnelsTitle: '🔀 SSH Port Forwarding (Tunnels)',
238
+ tunnelsDesc: 'Forward remote ports (e.g. databases, web servers, APIs) to your local machine via encrypted SSH tunnels.',
239
+ tLocalPort: 'Local Port',
240
+ tRemotePort: 'Remote Port',
241
+ tOpenBtn: '+ Open Tunnel',
242
+ tOpening: 'Opening…',
243
+ tStopBtn: 'Stop',
244
+ tNoTunnels: 'No active port forwarding tunnels.',
245
+ tunnelsColId: 'Tunnel ID',
246
+ tunnelsColRoute: 'Forwarding Route',
247
+ tunnelsColActions: 'Actions',
248
+ tOpenOk: 'Tunnel opened: 127.0.0.1:{localPort} -> Remote:{remotePort}',
249
+ tOpenErr: 'Failed to open tunnel: {err}'
250
+ }
251
+
252
+ const zh = {
253
+ title: '远程开发工作区',
254
+ subtitle: '支持原生 SSH 连接、SFTP 文件访问、3-way 镜像同步与端口转发隧道。',
255
+ status: '就绪',
256
+
257
+ tabProfiles: '🖥️ 主机配置',
258
+ tabContainers: '🐳 容器管理',
259
+ tabEnv: '🗃️ 环境变量',
260
+ btnImportSsh: '导入 ~/.ssh/config',
261
+ envTitle: '远程环境变量配置 (.env)',
262
+ envKey: '变量名',
263
+ envVal: '变量值',
264
+ envSaveVar: '保存',
265
+ envShowVal: '显示',
266
+ envHideVal: '隐藏',
267
+ tarSyncFast: '🚀 快速 Tarball 流式同步',
268
+ diagnoseTitle: '快速诊断',
269
+ diagnosePorts: '开放端口',
270
+ diagnoseOom: 'OOM 检查',
271
+ diagnoseDisk: '磁盘分析',
272
+ alertLabel: '告警 ({count})',
273
+
274
+ containersTitle: '远程 Docker / Podman 引擎',
275
+ containerName: '容器名',
276
+ containerImage: '镜像',
277
+ containerStatus: '运行状态',
278
+ containerPorts: '端口映射',
279
+ btnLogs: '日志',
280
+ btnRestart: '重启',
281
+ btnStop: '停止',
282
+ noContainers: '未找到运行中的容器或 Docker 守护进程未启动。',
283
+ healthCpu: 'CPU: {val}%',
284
+ healthMem: '内存: {val}% ({used}/{total} MB)',
285
+ healthDisk: '磁盘: {val}%',
286
+ healthUptime: '运行: {val}',
287
+ autoSyncTitle: '保存时自动增量同步',
288
+ autoSyncDesc: '本地文件保存时,后台自动增量推送到远程对应工作目录。',
289
+ btnTransfer: '跨主机传输...',
290
+ transferTitle: '主机间直接流式传输',
291
+ transferDestHost: '目标主机',
292
+ transferDestPath: '目标文件路径',
293
+ transferBtnExec: '立即直传',
294
+ transferSuccess: '成功传输 {bytes} 字节到 {host}:{path}',
295
+ fJumpHost: '跳板机 (Bastion)',
296
+ fJumpHostNone: '直连模式 (无跳板机)',
297
+
298
+ tabTerminal: '⚡ 网页终端',
299
+ tabExplorer: '📂 文件管理',
300
+ tabTunnels: '🔗 端口隧道',
301
+ tabSync: '🔄 同步控制台',
302
+ vaultProtectedBadge: '🛡️ .env 保管库加密保护',
303
+ termStart: '连接终端',
304
+ termStop: '断开连接',
305
+ termClear: '清屏',
306
+ termPlaceholder: '输入命令后回车 (如 htop, ls, docker ps)...',
307
+ termConnected: '已连接至主机 {host} 的 PTY 会话 {id}',
308
+ termDisconnected: '终端已断开。',
309
+ explorerTitle: '远程文件系统浏览',
310
+ filePreviewTitle: '文件查看器: {path}',
311
+ fileSize: '{size} KB',
312
+ btnSaveFile: '保存文件更改',
313
+ fileSaved: '文件保存成功。',
314
+ tunnelTraffic: '下行: {rx} | 上行: {tx}',
315
+ tunnelUptime: '运行: {sec}秒',
316
+ openLocalLink: '🌐 打开本地端口',
317
+ badgeOnline: '当前主机: {name} ({latency} ms)',
318
+ badgeOffline: '未选定活跃主机',
319
+ badgeTunnels: '活动隧道: {count}',
320
+ badgeKey: '🔑 私钥认证',
321
+ badgePassword: '🔒 密码认证',
322
+ profilesTitle: '🖥️ 远程 SSH 主机配置',
323
+ profilesDesc: '配置用于命令执行、SFTP 浏览与后台镜像同步的远程服务器。',
324
+ addBtn: '+ 添加主机',
325
+ nameCol: '配置名称',
326
+ hostCol: '主机 / 用户',
327
+ portCol: '端口',
328
+ pathCol: '远程工作区',
329
+ actionsCol: '操作',
330
+ noHosts: '暂无配置的主机。点击 "+ 添加主机" 建立首个连接。',
331
+ activeBadge: '活动中',
332
+ updaterTitle: '插件更新',
333
+ currentVersion: '当前版本',
334
+ latestVersion: 'npm 最新版',
335
+ checkUpdate: '检查更新',
336
+ checking: '正在检查...',
337
+ updateNow: '立即更新',
338
+ updating: '正在更新...',
339
+ upToDate: '已是最新版本',
340
+ updateAvailable: '发现新版本',
341
+ restartRequired: '更新已成功安装,请重启 DSH 使其生效。',
342
+ updateFailed: '更新失败',
343
+ pinBtn: '设为默认',
344
+ editBtn: '编辑',
345
+ delBtn: '删除',
346
+ delConfirm: '确认删除主机配置 "{name}" 吗?',
347
+ formTitleNew: '添加新主机配置',
348
+ formTitleEdit: '编辑主机配置',
349
+ fName: '配置别名',
350
+ fHost: '主机名 / IP 地址',
351
+ fPort: '端口',
352
+ fUser: '登录用户名',
353
+ fAuthType: '身份认证方式',
354
+ fAuthKey: 'SSH 私钥',
355
+ fAuthPass: '账户密码',
356
+ fKeyPath: '私钥文件路径',
357
+ fKeyPathHint: '例如 ~/.ssh/id_rsa 或 /home/user/.ssh/id_ed25519',
358
+ fPassphrase: '私钥口令 (可选)',
359
+ fPassword: '登录密码',
360
+ fRemotePath: '远程项目工作区目录',
361
+ fRemotePathHint: '服务器上的绝对路径 (例如 /home/user/my-project)',
362
+ fLocalMirror: '本地镜像同步目录',
363
+ fLocalMirrorHint: '用于 3-way 差异比对与快速同步的本地镜像路径',
364
+ btnBrowse: '📁 浏览远程目录...',
365
+ btnSelectThis: '选择当前目录',
366
+ btnTest: '⚡ 测试网络连通性 (Ping)',
367
+ btnSave: '保存配置',
368
+ btnCancel: '取消',
369
+ testing: '正在测试连接...',
370
+ saving: '正在保存...',
371
+ testOk: '✅ 连接成功!延迟: {latency} ms | 系统: {os}',
372
+ testFail: '❌ 连接失败: {err}',
373
+ diagTitle: '📊 实时连接状态诊断',
374
+ diagDesc: '查看当前选定主机的网络延迟、系统资源及底层服务运行指标。',
375
+ diagProbeBtn: '⚡ 即时探测',
376
+ syncTitle: '🔄 双向工作区镜像同步',
377
+ syncDesc: '基于三方冲突感知的快速差异同步,支持本地与远程端双向推拉。',
378
+ pullBtn: '⬇️ 拉取至本地 (Pull)',
379
+ pushBtn: '⬆️ 推送至远程 (Push)',
380
+ pulling: '正在拉取文件…',
381
+ pushing: '正在推送文件…',
382
+ syncOk: '同步完成!共更新 {count} 个文件。',
383
+ syncConflict: '冲突警告: 检测到 {count} 个文件在两端均有修改。',
384
+ syncErr: '同步异常: {err}',
385
+ tunnelsTitle: '🔀 SSH 端口安全隧道',
386
+ tunnelsDesc: '通过加密 SSH 隧道将远程端口 (如数据库、开发服务、API) 映射至本地访问。',
387
+ tLocalPort: '本地监听端口',
388
+ tRemotePort: '远程目标端口',
389
+ tOpenBtn: '+ 建立隧道',
390
+ tOpening: '正在建立…',
391
+ tStopBtn: '关闭',
392
+ tNoTunnels: '当前没有运行中的端口转发隧道。',
393
+ tunnelsColId: '隧道标识',
394
+ tunnelsColRoute: '转发路由',
395
+ tunnelsColActions: '管理操作',
396
+ tOpenOk: '隧道已启动: 127.0.0.1:{localPort} -> Remote:{remotePort}',
397
+ tOpenErr: '启动隧道失败: {err}'
398
+ }
399
+
400
+ function makeT(dict, fallback) {
401
+ return function t(key, vars) {
402
+ let val = (dict && dict[key]) || (fallback && fallback[key]) || key
403
+ if (vars && typeof val === 'string') {
404
+ for (const k of Object.keys(vars)) {
405
+ val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]))
406
+ }
407
+ }
408
+ return val
409
+ }
410
+ }
411
+
412
+ let ChevronIcon = null;
413
+ try {
414
+ const primitives = require('@deepseek-ai/dsh-client-ui-primitives');
415
+ ChevronIcon = primitives && primitives.IconChevronDownOutline14;
416
+ } catch (err) {
417
+ ChevronIcon = null;
418
+ }
419
+
420
+ function FallbackChevron() {
421
+ return React.createElement(
422
+ 'svg',
423
+ { width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': true },
424
+ React.createElement('path', {
425
+ d: 'M3.5 5.25L7 8.75L10.5 5.25',
426
+ stroke: 'currentColor',
427
+ strokeWidth: 1.5,
428
+ strokeLinecap: 'round',
429
+ strokeLinejoin: 'round',
430
+ })
431
+ )
432
+ }
433
+
434
+
435
+
436
+
437
+ // --- Sub-component: Remote Environment Tab (New in v0.3.1) ---
438
+
439
+ function EnvTab(props) {
440
+ const { activeProfile, t } = props
441
+ const [envEntries, setEnvEntries] = React.useState([])
442
+ const [loading, setLoading] = React.useState(false)
443
+ const [visibleKeys, setVisibleKeys] = React.useState({})
444
+ const [editValues, setEditValues] = React.useState({})
445
+ const [msg, setMsg] = React.useState('')
446
+
447
+ const loadEnv = async () => {
448
+ if (!activeProfile) return
449
+ setLoading(true)
450
+ try {
451
+ const res = await fetch('/dsh-remote-workspace/env/view', {
452
+ method: 'POST',
453
+ headers: { 'Content-Type': 'application/json' },
454
+ body: JSON.stringify({ profileId: activeProfile.id })
455
+ })
456
+ const data = await res.json()
457
+ if (data.ok && Array.isArray(data.entries)) {
458
+ setEnvEntries(data.entries.filter((e) => e.type === 'var'))
459
+ const initialEdits = {}
460
+ data.entries.forEach((e) => {
461
+ if (e.type === 'var') initialEdits[e.key] = e.value
462
+ })
463
+ setEditValues(initialEdits)
464
+ }
465
+ } catch (err) {
466
+ setMsg(`Error loading .env: ${err.message}`)
467
+ }
468
+ setLoading(false)
469
+ }
470
+
471
+ React.useEffect(() => {
472
+ loadEnv()
473
+ }, [activeProfile?.id])
474
+
475
+ const toggleVisible = (key) => {
476
+ setVisibleKeys((prev) => ({ ...prev, [key]: !prev[key] }))
477
+ }
478
+
479
+ const saveVar = async (key) => {
480
+ try {
481
+ const res = await fetch('/dsh-remote-workspace/env/save', {
482
+ method: 'POST',
483
+ headers: { 'Content-Type': 'application/json' },
484
+ body: JSON.stringify({
485
+ profileId: activeProfile.id,
486
+ key,
487
+ value: editValues[key] || ''
488
+ })
489
+ })
490
+ const data = await res.json()
491
+ if (data.ok) {
492
+ setMsg(`Saved ${key} successfully!`)
493
+ setTimeout(() => setMsg(''), 3000)
494
+ }
495
+ } catch (err) {
496
+ setMsg(`Save failed: ${err.message}`)
497
+ }
498
+ }
499
+
500
+ return React.createElement(
501
+ 'div',
502
+ { style: { display: 'flex', flexDirection: 'column', gap: '14px' } },
503
+ React.createElement(
504
+ 'div',
505
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
506
+ React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, t('envTitle')),
507
+ React.createElement('button', { type: 'button', className: 'drw-btn', onClick: loadEnv, disabled: loading }, loading ? 'Loading...' : '🔄 Refresh')
508
+ ),
509
+ msg ? React.createElement('div', { className: 'drw-badge drw-badge-ok' }, msg) : null,
510
+ React.createElement(
511
+ 'div',
512
+ { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', padding: '12px', background: 'var(--dsw-alias-bg-layer-2)' } },
513
+ envEntries.length === 0
514
+ ? React.createElement('div', { style: { textAlign: 'center', padding: '16px', color: 'var(--dsw-alias-label-secondary)' } }, 'No variables found in remote .env')
515
+ : envEntries.map((e) =>
516
+ React.createElement(
517
+ 'div',
518
+ { key: e.key, className: 'drw-env-row' },
519
+ React.createElement('span', { style: { fontWeight: 600, fontFamily: 'monospace', fontSize: '12.5px' } }, e.key),
520
+ React.createElement('input', {
521
+ type: visibleKeys[e.key] ? 'text' : 'password',
522
+ className: 'drw-input',
523
+ value: editValues[e.key] !== undefined ? editValues[e.key] : e.value,
524
+ onChange: (ev) => {
525
+ const val = ev.target.value
526
+ setEditValues((prev) => ({ ...prev, [e.key]: val }))
527
+ }
528
+ }),
529
+ React.createElement(
530
+ 'div',
531
+ { style: { display: 'flex', gap: '4px', justifyContent: 'flex-end' } },
532
+ React.createElement('button', { type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => toggleVisible(e.key) }, visibleKeys[e.key] ? t('envHideVal') : t('envShowVal')),
533
+ React.createElement('button', { type: 'button', className: 'drw-btn drw-btn-primary', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => saveVar(e.key) }, t('envSaveVar'))
534
+ )
535
+ )
536
+ )
537
+ )
538
+ )
539
+ }
540
+
541
+ // --- Sub-component: Docker Containers Tab ---
542
+
543
+ function ContainersTab(props) {
544
+ const { activeProfile, t } = props
545
+ const [containers, setContainers] = React.useState([])
546
+ const [loading, setLoading] = React.useState(false)
547
+ const [logsModal, setLogsModal] = React.useState(null)
548
+ const [logsContent, setLogsContent] = React.useState('')
549
+ const [logsLoading, setLogsLoading] = React.useState(false)
550
+
551
+ const loadContainers = async () => {
552
+ if (!activeProfile) return
553
+ setLoading(true)
554
+ try {
555
+ const res = await fetch(`/dsh-remote-workspace/docker/list?profileId=${activeProfile.id}`)
556
+ const data = await res.json()
557
+ if (data.ok && Array.isArray(data.containers)) {
558
+ setContainers(data.containers)
559
+ } else {
560
+ setContainers([])
561
+ }
562
+ } catch (err) { /* best-effort cleanup */ }
563
+ setLoading(false)
564
+ }
565
+
566
+ React.useEffect(() => {
567
+ loadContainers()
568
+ }, [activeProfile?.id])
569
+
570
+ const handleAction = async (containerId, action) => {
571
+ try {
572
+ await fetch('/dsh-remote-workspace/docker/action', {
573
+ method: 'POST',
574
+ headers: { 'Content-Type': 'application/json' },
575
+ body: JSON.stringify({ profileId: activeProfile.id, containerId, action })
576
+ })
577
+ await loadContainers()
578
+ } catch (err) { /* best-effort cleanup */ }
579
+ }
580
+
581
+ const showLogs = async (containerId, name) => {
582
+ setLogsModal({ id: containerId, name })
583
+ setLogsLoading(true)
584
+ try {
585
+ const res = await fetch('/dsh-remote-workspace/docker/logs', {
586
+ method: 'POST',
587
+ headers: { 'Content-Type': 'application/json' },
588
+ body: JSON.stringify({ profileId: activeProfile.id, containerId, tail: 150 })
589
+ })
590
+ const data = await res.json()
591
+ setLogsContent(data.logs || '[No logs output]')
592
+ } catch (err) {
593
+ setLogsContent(`[Error: ${err.message}]`)
594
+ }
595
+ setLogsLoading(false)
596
+ }
597
+
598
+ return React.createElement(
599
+ 'div',
600
+ { style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
601
+ React.createElement(
602
+ 'div',
603
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
604
+ React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, t('containersTitle')),
605
+ React.createElement(
606
+ 'button',
607
+ {
608
+ type: 'button',
609
+ className: 'drw-btn',
610
+ onClick: loadContainers,
611
+ disabled: loading
612
+ },
613
+ loading ? 'Refreshing...' : '🔄 Refresh'
614
+ )
615
+ ),
616
+ React.createElement(
617
+ 'div',
618
+ { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', overflow: 'hidden', background: 'var(--dsw-alias-bg-layer-2)' } },
619
+ React.createElement(
620
+ 'div',
621
+ { className: 'drw-container-row', style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)', background: 'var(--dsw-alias-bg-layer-1)' } },
622
+ React.createElement('span', null, t('containerName')),
623
+ React.createElement('span', null, t('containerImage')),
624
+ React.createElement('span', null, t('containerStatus')),
625
+ React.createElement('span', null, t('containerPorts')),
626
+ React.createElement('span', { style: { textAlign: 'right' } }, t('actionsCol'))
627
+ ),
628
+ containers.length === 0
629
+ ? React.createElement('div', { style: { padding: '24px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)' } }, t('noContainers'))
630
+ : containers.map((c) =>
631
+ React.createElement(
632
+ 'div',
633
+ { key: c.id, className: 'drw-container-row' },
634
+ React.createElement(
635
+ 'div',
636
+ { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
637
+ React.createElement('span', { className: `drw-badge ${c.state === 'running' ? 'drw-badge-ok' : 'drw-badge-warn'}` }, c.state),
638
+ React.createElement('span', { style: { fontWeight: 600 } }, c.names)
639
+ ),
640
+ React.createElement('span', { style: { opacity: 0.8, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, c.image),
641
+ React.createElement('span', { style: { fontSize: '11px', opacity: 0.7 } }, c.status),
642
+ React.createElement('span', { style: { fontSize: '11px', opacity: 0.7, overflow: 'hidden', textOverflow: 'ellipsis' } }, c.ports || '—'),
643
+ React.createElement(
644
+ 'div',
645
+ { style: { display: 'flex', gap: '4px', justifyContent: 'flex-end' } },
646
+ React.createElement(
647
+ 'button',
648
+ { type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => showLogs(c.id, c.names) },
649
+ t('btnLogs')
650
+ ),
651
+ React.createElement(
652
+ 'button',
653
+ { type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => handleAction(c.id, 'restart') },
654
+ t('btnRestart')
655
+ ),
656
+ c.state === 'running'
657
+ ? React.createElement(
658
+ 'button',
659
+ { type: 'button', className: 'drw-btn drw-btn-danger', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => handleAction(c.id, 'stop') },
660
+ t('btnStop')
661
+ )
662
+ : null
663
+ )
664
+ )
665
+ )
666
+ ),
667
+ // Container Logs Modal
668
+ logsModal
669
+ ? React.createElement(
670
+ 'div',
671
+ { className: 'drw-modal-overlay', onClick: () => setLogsModal(null) },
672
+ React.createElement(
673
+ 'div',
674
+ { className: 'drw-modal-dialog', onClick: (e) => e.stopPropagation() },
675
+ React.createElement(
676
+ 'div',
677
+ { className: 'drw-modal-hdr' },
678
+ React.createElement('span', { style: { fontFamily: 'monospace' } }, `Docker Logs: ${logsModal.name} (${logsModal.id})`),
679
+ React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => setLogsModal(null) }, '✕')
680
+ ),
681
+ React.createElement(
682
+ 'div',
683
+ { className: 'drw-modal-body' },
684
+ React.createElement('pre', { style: { background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)', padding: '12px', borderRadius: '8px', fontSize: '12px', maxHeight: '420px', overflowY: 'auto', whiteSpace: 'pre-wrap' } }, logsLoading ? 'Fetching logs...' : logsContent)
685
+ )
686
+ )
687
+ )
688
+ : null
689
+ )
690
+ }
691
+
692
+ // --- Sub-component: Web Terminal Tab ---
693
+
694
+ function TerminalTab(props) {
695
+ const { activeProfile, t } = props
696
+ const [connected, setConnected] = React.useState(false)
697
+ const [sessionId, setSessionId] = React.useState(null)
698
+ const [output, setOutput] = React.useState('')
699
+ const [cmdInput, setCmdInput] = React.useState('')
700
+ const [loading, setLoading] = React.useState(false)
701
+ const termEndRef = React.useRef(null)
702
+ const evtSourceRef = React.useRef(null)
703
+
704
+ React.useEffect(() => {
705
+ if (termEndRef.current) {
706
+ termEndRef.current.scrollIntoView({ behavior: 'smooth' })
707
+ }
708
+ }, [output])
709
+
710
+ React.useEffect(() => {
711
+ return () => {
712
+ if (evtSourceRef.current) {
713
+ evtSourceRef.current.close()
714
+ }
715
+ }
716
+ }, [])
717
+
718
+ const handleConnect = async () => {
719
+ if (!activeProfile) return
720
+ setLoading(true)
721
+ try {
722
+ const res = await fetch('/dsh-remote-workspace/terminal/create', {
723
+ method: 'POST',
724
+ headers: { 'Content-Type': 'application/json' },
725
+ body: JSON.stringify({ profileId: activeProfile.id, cols: 80, rows: 24 })
726
+ })
727
+ const data = await res.json()
728
+ if (data.ok && data.sessionId) {
729
+ setSessionId(data.sessionId)
730
+ setConnected(true)
731
+ setOutput((prev) => prev + `\n[+] ` + t('termConnected', { id: data.sessionId, host: activeProfile.host }) + `\n`)
732
+
733
+ // Connect SSE stream
734
+ const es = new EventSource(`/dsh-remote-workspace/terminal/stream?sessionId=${data.sessionId}`)
735
+ evtSourceRef.current = es
736
+ es.onmessage = (e) => {
737
+ try {
738
+ const parsed = JSON.parse(e.data)
739
+ if (parsed.chunk) {
740
+ setOutput((p) => p + parsed.chunk)
741
+ }
742
+ } catch (err) { /* best-effort cleanup */ }
743
+ }
744
+ es.onerror = () => {
745
+ setOutput((p) => p + `\n[-] ` + t('termDisconnected') + `\n`)
746
+ setConnected(false)
747
+ es.close()
748
+ }
749
+ }
750
+ } catch (err) {
751
+ setOutput((p) => p + `\n[!] Connection error: ${err.message}\n`)
752
+ }
753
+ setLoading(false)
754
+ }
755
+
756
+ const handleDisconnect = async () => {
757
+ if (sessionId) {
758
+ try {
759
+ await fetch('/dsh-remote-workspace/terminal/close', {
760
+ method: 'POST',
761
+ headers: { 'Content-Type': 'application/json' },
762
+ body: JSON.stringify({ sessionId })
763
+ })
764
+ } catch (err) { /* best-effort cleanup */ }
765
+ }
766
+ if (evtSourceRef.current) {
767
+ evtSourceRef.current.close()
768
+ }
769
+ setConnected(false)
770
+ setSessionId(null)
771
+ setOutput((p) => p + `\n[-] ` + t('termDisconnected') + `\n`)
772
+ }
773
+
774
+ const sendCommand = async (e) => {
775
+ if (e) e.preventDefault()
776
+ if (!cmdInput || !sessionId) return
777
+ const toSend = cmdInput + '\n'
778
+ setCmdInput('')
779
+ try {
780
+ await fetch('/dsh-remote-workspace/terminal/input', {
781
+ method: 'POST',
782
+ headers: { 'Content-Type': 'application/json' },
783
+ body: JSON.stringify({ sessionId, data: toSend })
784
+ })
785
+ } catch (err) { /* best-effort cleanup */ }
786
+ }
787
+
788
+ const sendCtrlC = async () => {
789
+ if (!sessionId) return
790
+ try {
791
+ await fetch('/dsh-remote-workspace/terminal/input', {
792
+ method: 'POST',
793
+ headers: { 'Content-Type': 'application/json' },
794
+ body: JSON.stringify({ sessionId, data: '\x03' })
795
+ })
796
+ } catch (err) { /* best-effort cleanup */ }
797
+ }
798
+
799
+ return React.createElement(
800
+ 'div',
801
+ { style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
802
+ React.createElement(
803
+ 'div',
804
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
805
+ React.createElement(
806
+ 'div',
807
+ { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
808
+ React.createElement('span', { style: { fontWeight: 600, fontSize: '14px' } }, 'SSH PTY Shell:'),
809
+ React.createElement(
810
+ 'span',
811
+ { className: `drw-badge ${connected ? 'drw-badge-ok' : 'drw-badge-warn'}` },
812
+ connected ? `🟢 Online (${activeProfile?.host})` : '⚪ Idle'
813
+ )
814
+ ),
815
+ React.createElement(
816
+ 'div',
817
+ { style: { display: 'flex', gap: '8px' } },
818
+ !connected
819
+ ? React.createElement(
820
+ 'button',
821
+ {
822
+ type: 'button',
823
+ className: 'drw-btn drw-btn-primary',
824
+ disabled: !activeProfile || loading,
825
+ onClick: handleConnect
826
+ },
827
+ loading ? 'Connecting...' : t('termStart')
828
+ )
829
+ : React.createElement(
830
+ 'button',
831
+ {
832
+ type: 'button',
833
+ className: 'drw-btn drw-btn-danger',
834
+ onClick: handleDisconnect
835
+ },
836
+ t('termStop')
837
+ ),
838
+ connected
839
+ ? React.createElement(
840
+ 'button',
841
+ {
842
+ type: 'button',
843
+ className: 'drw-btn',
844
+ onClick: sendCtrlC
845
+ },
846
+ 'Ctrl+C'
847
+ )
848
+ : null,
849
+ React.createElement(
850
+ 'button',
851
+ {
852
+ type: 'button',
853
+ className: 'drw-btn',
854
+ onClick: () => setOutput('')
855
+ },
856
+ t('termClear')
857
+ )
858
+ )
859
+ ),
860
+ React.createElement(
861
+ 'div',
862
+ { className: 'drw-term-window' },
863
+ React.createElement(
864
+ 'div',
865
+ { className: 'drw-term-header' },
866
+ React.createElement('span', null, `terminal ~ ${activeProfile?.username || 'user'}@${activeProfile?.host || 'remote'}`),
867
+ React.createElement('span', null, connected ? `Session: ${sessionId}` : 'Offline')
868
+ ),
869
+ React.createElement(
870
+ 'div',
871
+ { className: 'drw-term-body' },
872
+ output || (activeProfile ? `[i] Ready to connect to ${activeProfile.username || 'root'}@${activeProfile.host}. Click 'Connect Terminal'.` : `[!] No active host profile selected. Configure and activate a profile first.`),
873
+ React.createElement('div', { ref: termEndRef })
874
+ ),
875
+ connected
876
+ ? React.createElement(
877
+ 'form',
878
+ { className: 'drw-term-footer', onSubmit: sendCommand },
879
+ React.createElement('span', { style: { color: 'var(--dsw-alias-link-primary)', padding: '4px 6px', fontFamily: 'monospace' } }, '$'),
880
+ React.createElement('input', {
881
+ className: 'drw-term-input',
882
+ type: 'text',
883
+ placeholder: t('termPlaceholder'),
884
+ value: cmdInput,
885
+ onChange: (e) => setCmdInput(e.target.value)
886
+ }),
887
+ React.createElement(
888
+ 'button',
889
+ { type: 'submit', className: 'drw-btn', style: { padding: '2px 8px', fontSize: '11px' } },
890
+ 'Send ↵'
891
+ )
892
+ )
893
+ : null
894
+ )
895
+ )
896
+ }
897
+
898
+ // --- Sub-component: Remote File Explorer Tab ---
899
+
900
+ function ExplorerTab(props) {
901
+ const { activeProfile, t } = props
902
+ const [currentPath, setCurrentPath] = React.useState(activeProfile?.remoteWorkspace || '/')
903
+ const [entries, setEntries] = React.useState([])
904
+ const [loading, setLoading] = React.useState(false)
905
+ const [previewFile, setPreviewFile] = React.useState(null)
906
+ const [previewContent, setPreviewContent] = React.useState('')
907
+ const [editContent, setEditContent] = React.useState('')
908
+ const [previewLoading, setPreviewLoading] = React.useState(false)
909
+ const [saveStatus, setSaveStatus] = React.useState(null)
910
+
911
+ const loadDir = async (dirPath) => {
912
+ if (!activeProfile) return
913
+ setLoading(true)
914
+ try {
915
+ const res = await fetch('/dsh-remote-workspace/browse', {
916
+ method: 'POST',
917
+ headers: { 'Content-Type': 'application/json' },
918
+ body: JSON.stringify({ profileId: activeProfile.id, remotePath: dirPath })
919
+ })
920
+ const data = await res.json()
921
+ if (data.ok && Array.isArray(data.entries)) {
922
+ setCurrentPath(dirPath)
923
+ setEntries(data.entries)
924
+ }
925
+ } catch (err) { /* best-effort cleanup */ }
926
+ setLoading(false)
927
+ }
928
+
929
+ React.useEffect(() => {
930
+ if (activeProfile) {
931
+ loadDir(activeProfile.remoteWorkspace || '/')
932
+ }
933
+ }, [activeProfile?.id])
934
+
935
+ const handleNavUp = () => {
936
+ const parts = currentPath.split('/').filter(Boolean)
937
+ parts.pop()
938
+ const parent = '/' + parts.join('/')
939
+ loadDir(parent || '/')
940
+ }
941
+
942
+ const openFile = async (entry) => {
943
+ const filePath = currentPath.endsWith('/') ? currentPath + entry.filename : currentPath + '/' + entry.filename
944
+ setPreviewLoading(true)
945
+ setPreviewFile(filePath)
946
+ setSaveStatus(null)
947
+ try {
948
+ const res = await fetch('/dsh-remote-workspace/file/view', {
949
+ method: 'POST',
950
+ headers: { 'Content-Type': 'application/json' },
951
+ body: JSON.stringify({ profileId: activeProfile.id, filePath })
952
+ })
953
+ const data = await res.json()
954
+ if (data.ok) {
955
+ setPreviewContent(data.content)
956
+ setEditContent(data.content)
957
+ } else {
958
+ setPreviewContent(`[Error loading file: ${data.error || 'Failed'}]`)
959
+ setEditContent('')
960
+ }
961
+ } catch (err) {
962
+ setPreviewContent(`[Error: ${err.message}]`)
963
+ }
964
+ setPreviewLoading(false)
965
+ }
966
+
967
+ const handleSaveFile = async () => {
968
+ if (!previewFile || !activeProfile) return
969
+ setSaveStatus('saving')
970
+ try {
971
+ const res = await fetch('/dsh-remote-workspace/file/save', {
972
+ method: 'POST',
973
+ headers: { 'Content-Type': 'application/json' },
974
+ body: JSON.stringify({ profileId: activeProfile.id, filePath: previewFile, content: editContent })
975
+ })
976
+ const data = await res.json()
977
+ if (data.ok) {
978
+ setSaveStatus('saved')
979
+ setPreviewContent(editContent)
980
+ } else {
981
+ setSaveStatus('error')
982
+ }
983
+ } catch (_) {
984
+ setSaveStatus('error')
985
+ }
986
+ }
987
+
988
+ return React.createElement(
989
+ 'div',
990
+ { className: 'drw-explorer-box' },
991
+ React.createElement(
992
+ 'div',
993
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
994
+ React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, t('explorerTitle')),
995
+ React.createElement(
996
+ 'button',
997
+ {
998
+ type: 'button',
999
+ className: 'drw-btn',
1000
+ onClick: () => loadDir(currentPath),
1001
+ disabled: loading
1002
+ },
1003
+ loading ? 'Refreshing...' : '🔄 Refresh'
1004
+ )
1005
+ ),
1006
+ React.createElement(
1007
+ 'div',
1008
+ { className: 'drw-explorer-nav' },
1009
+ React.createElement(
1010
+ 'button',
1011
+ {
1012
+ type: 'button',
1013
+ className: 'drw-btn',
1014
+ style: { padding: '2px 8px', fontSize: '11px' },
1015
+ onClick: handleNavUp,
1016
+ disabled: currentPath === '/' || loading
1017
+ },
1018
+ '⬆️ Up'
1019
+ ),
1020
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, 'Path:'),
1021
+ React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-brand-primary)' } }, currentPath)
1022
+ ),
1023
+ React.createElement(
1024
+ 'div',
1025
+ { className: 'drw-file-list' },
1026
+ entries.length === 0
1027
+ ? React.createElement('div', { style: { padding: '16px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)' } }, loading ? 'Reading directory...' : 'Directory is empty.')
1028
+ : entries.map((e) =>
1029
+ React.createElement(
1030
+ 'div',
1031
+ {
1032
+ key: e.filename,
1033
+ className: 'drw-file-row',
1034
+ onClick: () => (e.isDirectory ? loadDir(currentPath.endsWith('/') ? currentPath + e.filename : currentPath + '/' + e.filename) : openFile(e))
1035
+ },
1036
+ React.createElement(
1037
+ 'div',
1038
+ { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
1039
+ React.createElement('span', null, e.isDirectory ? '📁' : '📄'),
1040
+ React.createElement('span', { style: { fontWeight: e.isDirectory ? 600 : 400 } }, e.filename)
1041
+ ),
1042
+ React.createElement(
1043
+ 'div',
1044
+ { className: 'drw-file-meta' },
1045
+ e.size !== undefined ? React.createElement('span', null, `${Math.round(e.size / 1024)} KB`) : null,
1046
+ e.permissions ? React.createElement('span', { style: { opacity: 0.6 } }, e.permissions) : null
1047
+ )
1048
+ )
1049
+ )
1050
+ ),
1051
+ // File Preview / Edit Modal
1052
+ previewFile
1053
+ ? React.createElement(
1054
+ 'div',
1055
+ { className: 'drw-modal-overlay', onClick: () => setPreviewFile(null) },
1056
+ React.createElement(
1057
+ 'div',
1058
+ { className: 'drw-modal-dialog', onClick: (e) => e.stopPropagation() },
1059
+ React.createElement(
1060
+ 'div',
1061
+ { className: 'drw-modal-hdr' },
1062
+ React.createElement('span', { style: { fontFamily: 'monospace' } }, t('filePreviewTitle', { path: previewFile })),
1063
+ React.createElement(
1064
+ 'button',
1065
+ {
1066
+ type: 'button',
1067
+ className: 'drw-btn',
1068
+ style: { padding: '2px 8px' },
1069
+ onClick: () => setPreviewFile(null)
1070
+ },
1071
+ '✕'
1072
+ )
1073
+ ),
1074
+ React.createElement(
1075
+ 'div',
1076
+ { className: 'drw-modal-body', style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
1077
+ previewLoading
1078
+ ? React.createElement('div', { style: { padding: '20px', textAlign: 'center' } }, 'Loading content...')
1079
+ : React.createElement('textarea', {
1080
+ style: {
1081
+ width: '100%',
1082
+ height: '320px',
1083
+ background: 'var(--dsw-alias-bg-layer-1)',
1084
+ color: 'var(--dsw-alias-label-primary)',
1085
+ fontFamily: 'monospace',
1086
+ fontSize: '12px',
1087
+ padding: '10px',
1088
+ border: '1px solid var(--dsw-alias-border-l2)',
1089
+ borderRadius: '8px',
1090
+ resize: 'vertical',
1091
+ boxSizing: 'border-box'
1092
+ },
1093
+ value: editContent,
1094
+ onChange: (e) => setEditContent(e.target.value)
1095
+ }),
1096
+ React.createElement(
1097
+ 'div',
1098
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
1099
+ React.createElement(
1100
+ 'span',
1101
+ { style: { fontSize: '12px', color: saveStatus === 'saved' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)' } },
1102
+ saveStatus === 'saved' ? t('fileSaved') : saveStatus === 'error' ? 'Error saving file.' : ''
1103
+ ),
1104
+ React.createElement(
1105
+ 'div',
1106
+ { style: { display: 'flex', gap: '8px' } },
1107
+ React.createElement(
1108
+ 'button',
1109
+ {
1110
+ type: 'button',
1111
+ className: 'drw-btn',
1112
+ onClick: () => setPreviewFile(null)
1113
+ },
1114
+ 'Close'
1115
+ ),
1116
+ React.createElement(
1117
+ 'button',
1118
+ {
1119
+ type: 'button',
1120
+ className: 'drw-btn drw-btn-primary',
1121
+ onClick: handleSaveFile,
1122
+ disabled: saveStatus === 'saving'
1123
+ },
1124
+ saveStatus === 'saving' ? 'Saving...' : t('btnSaveFile')
1125
+ )
1126
+ )
1127
+ )
1128
+ )
1129
+ )
1130
+ )
1131
+ : null
1132
+ )
1133
+ }
1134
+
1135
+ function SettingsView(props) {
1136
+ const [activeTab, setActiveTab] = React.useState('profiles')
1137
+ const [healthData, setHealthData] = React.useState(null)
1138
+ const [autoSync, setAutoSync] = React.useState(false)
1139
+
1140
+ const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
1141
+ const [profiles, setProfiles] = React.useState([])
1142
+ const [activeId, setActiveId] = React.useState(null)
1143
+ const [tunnels, setTunnels] = React.useState([])
1144
+ const [editing, setEditing] = React.useState(null)
1145
+ const [busy, setBusy] = React.useState(null)
1146
+ const [testResult, setTestResult] = React.useState(null)
1147
+ const [syncMsg, setSyncMsg] = React.useState(null)
1148
+ const [tunnelMsg, setTunnelMsg] = React.useState(null)
1149
+ const [showPassword, setShowPassword] = React.useState(false)
1150
+
1151
+ // Tunnel creation inputs
1152
+ const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
1153
+ const [newTunnelRemote, setNewTunnelRemote] = React.useState('3000')
1154
+
1155
+ // Remote Directory Browser State
1156
+ const [browserOpen, setBrowserOpen] = React.useState(false)
1157
+ const [browserPath, setBrowserPath] = React.useState('/')
1158
+ const [browserEntries, setBrowserEntries] = React.useState([])
1159
+ const [browserLoading, setBrowserLoading] = React.useState(false)
1160
+
1161
+ const loadState = async () => {
1162
+ try {
1163
+ const res = await fetch('/dsh-remote-workspace/state')
1164
+ if (res.ok) {
1165
+ const data = await res.json()
1166
+ if (data.ok) {
1167
+ setProfiles(data.profiles || [])
1168
+ setActiveId(data.activeId || null)
1169
+ setTunnels(data.tunnels || [])
1170
+ setAutoSync(Boolean(data.autoSync))
1171
+ if (data.activeId) {
1172
+ fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
1173
+ }
1174
+ }
1175
+ }
1176
+ } catch (err) { /* best-effort cleanup */ }
1177
+ }
1178
+
1179
+ React.useEffect(() => {
1180
+ ensureCss()
1181
+ loadState()
1182
+ }, [])
1183
+
1184
+ const handleSave = async (profile) => {
1185
+ setBusy('saving')
1186
+ try {
1187
+ const res = await fetch('/dsh-remote-workspace/profiles/save', {
1188
+ method: 'POST',
1189
+ headers: { 'Content-Type': 'application/json' },
1190
+ body: JSON.stringify(profile)
1191
+ })
1192
+ if (res.ok) {
1193
+ setEditing(null)
1194
+ setBrowserOpen(false)
1195
+ setTestResult(null)
1196
+ await loadState()
1197
+ }
1198
+ } catch (err) { /* best-effort cleanup */ }
1199
+ setBusy(null)
1200
+ }
1201
+
1202
+ const handleDelete = async (id, name) => {
1203
+ if (!confirm(t('delConfirm', { name: name || id }))) return
1204
+ try {
1205
+ const res = await fetch('/dsh-remote-workspace/profiles/delete', {
1206
+ method: 'POST',
1207
+ headers: { 'Content-Type': 'application/json' },
1208
+ body: JSON.stringify({ id })
1209
+ })
1210
+ if (res.ok) {
1211
+ if (editing && editing.id === id) setEditing(null)
1212
+ await loadState()
1213
+ }
1214
+ } catch (err) { /* best-effort cleanup */ }
1215
+ }
1216
+
1217
+ const handleSetActive = async (id) => {
1218
+ try {
1219
+ const res = await fetch('/dsh-remote-workspace/profiles/active', {
1220
+ method: 'POST',
1221
+ headers: { 'Content-Type': 'application/json' },
1222
+ body: JSON.stringify({ id })
1223
+ })
1224
+ if (res.ok) {
1225
+ await loadState()
1226
+ }
1227
+ } catch (err) { /* best-effort cleanup */ }
1228
+ }
1229
+
1230
+ const handleTest = async (prof) => {
1231
+ setBusy('testing')
1232
+ setTestResult(null)
1233
+ try {
1234
+ const res = await fetch('/dsh-remote-workspace/test', {
1235
+ method: 'POST',
1236
+ headers: { 'Content-Type': 'application/json' },
1237
+ body: JSON.stringify({ profile: prof })
1238
+ })
1239
+ const data = await res.json()
1240
+ setTestResult(data)
1241
+ } catch (err) {
1242
+ setTestResult({ ok: false, error: err.message })
1243
+ }
1244
+ setBusy(null)
1245
+ }
1246
+
1247
+ const openDirectoryBrowser = async (initialPath) => {
1248
+ setBrowserOpen(true)
1249
+ const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
1250
+ await fetchRemoteDir(target)
1251
+ }
1252
+
1253
+ const fetchRemoteDir = async (dirPath) => {
1254
+ setBrowserLoading(true)
1255
+ try {
1256
+ const res = await fetch('/dsh-remote-workspace/browse', {
1257
+ method: 'POST',
1258
+ headers: { 'Content-Type': 'application/json' },
1259
+ body: JSON.stringify({ profile: editing, remotePath: dirPath })
1260
+ })
1261
+ const data = await res.json()
1262
+ if (data.ok) {
1263
+ setBrowserPath(data.currentPath || dirPath)
1264
+ setBrowserEntries(data.entries || [])
1265
+ }
1266
+ } catch (err) { /* best-effort cleanup */ }
1267
+ setBrowserLoading(false)
1268
+ }
1269
+
1270
+ const handleSync = async (direction) => {
1271
+ setBusy(`sync_${direction}`)
1272
+ setSyncMsg(null)
1273
+ try {
1274
+ const res = await fetch('/dsh-remote-workspace/sync', {
1275
+ method: 'POST',
1276
+ headers: { 'Content-Type': 'application/json' },
1277
+ body: JSON.stringify({ direction })
1278
+ })
1279
+ const data = await res.json()
1280
+ if (data.ok) {
1281
+ const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
1282
+ if (data.conflicts && data.conflicts.length > 0) {
1283
+ setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
1284
+ } else {
1285
+ setSyncMsg({ ok: true, text: t('syncOk', { count }) })
1286
+ }
1287
+ } else {
1288
+ setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
1289
+ }
1290
+ } catch (err) {
1291
+ setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
1292
+ }
1293
+ setBusy(null)
1294
+ }
1295
+
1296
+ const handleStartTunnel = async () => {
1297
+ if (!activeId) return
1298
+ setBusy('tunnel_open')
1299
+ setTunnelMsg(null)
1300
+ try {
1301
+ const res = await fetch('/dsh-remote-workspace/tunnels/start', {
1302
+ method: 'POST',
1303
+ headers: { 'Content-Type': 'application/json' },
1304
+ body: JSON.stringify({
1305
+ profileId: activeId,
1306
+ localPort: newTunnelLocal,
1307
+ remotePort: newTunnelRemote
1308
+ })
1309
+ })
1310
+ const data = await res.json()
1311
+ if (data.ok) {
1312
+ setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
1313
+ await loadState()
1314
+ } else {
1315
+ setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
1316
+ }
1317
+ } catch (err) {
1318
+ setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
1319
+ }
1320
+ setBusy(null)
1321
+ }
1322
+
1323
+ const handleStopTunnel = async (tunnelId) => {
1324
+ try {
1325
+ const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
1326
+ method: 'POST',
1327
+ headers: { 'Content-Type': 'application/json' },
1328
+ body: JSON.stringify({ tunnelId })
1329
+ })
1330
+ if (res.ok) {
1331
+ await loadState()
1332
+ }
1333
+ } catch (err) { /* best-effort cleanup */ }
1334
+ }
1335
+
1336
+ const activeProfile = profiles.find((p) => p.id === activeId)
1337
+
1338
+ return React.createElement(
1339
+ 'div',
1340
+ { className: 'drw-page' },
1341
+
1342
+ // Header with Live Badges (ClineBot Style)
1343
+ React.createElement(
1344
+ 'div',
1345
+ { className: 'drw-header' },
1346
+ React.createElement(
1347
+ 'div',
1348
+ { className: 'drw-header-top' },
1349
+ React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
1350
+ React.createElement(
1351
+ 'div',
1352
+ { className: 'drw-header-badges' },
1353
+ activeProfile
1354
+ ? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
1355
+ : React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
1356
+ activeProfile
1357
+ ? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
1358
+ : null,
1359
+ React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
1360
+ )
1361
+ ),
1362
+ React.createElement('div', { className: 'drw-page-sub' }, t('subtitle'))
1363
+ ),
1364
+
1365
+ // Section 1: SSH Profiles Table & Management
1366
+ React.createElement(
1367
+ 'div',
1368
+ { className: 'drw-card' },
1369
+ React.createElement(
1370
+ 'div',
1371
+ { className: 'drw-card-title' },
1372
+ React.createElement('span', null, t('profilesTitle')),
1373
+ !editing
1374
+ ? React.createElement(
1375
+ 'button',
1376
+ {
1377
+ type: 'button',
1378
+ className: 'drw-btn drw-btn-primary',
1379
+ onClick: () => {
1380
+ setEditing({
1381
+ id: 'prof_' + Math.random().toString(36).substr(2, 9),
1382
+ name: 'New Server',
1383
+ host: '',
1384
+ port: 22,
1385
+ username: 'root',
1386
+ authType: 'key',
1387
+ privateKeyPath: '~/.ssh/id_rsa',
1388
+ passphrase: '',
1389
+ password: '',
1390
+ remoteWorkspace: '',
1391
+ localMirrorPath: ''
1392
+ })
1393
+ setTestResult(null)
1394
+ }
1395
+ },
1396
+ t('addBtn')
1397
+ )
1398
+ : null
1399
+ ),
1400
+ React.createElement('div', { className: 'drw-card-desc' }, t('profilesDesc')),
1401
+
1402
+ // Interactive Editing Form
1403
+ editing
1404
+ ? React.createElement(
1405
+ 'div',
1406
+ {
1407
+ style: {
1408
+ padding: '16px',
1409
+ borderRadius: '10px',
1410
+ background: 'var(--dsw-alias-bg-layer-2)',
1411
+ border: '1px solid var(--dsw-alias-border-l2)',
1412
+ display: 'flex',
1413
+ flexDirection: 'column',
1414
+ gap: '14px'
1415
+ }
1416
+ },
1417
+ React.createElement(
1418
+ 'div',
1419
+ { style: { fontWeight: 600, fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } },
1420
+ editing.name ? t('formTitleEdit') : t('formTitleNew')
1421
+ ),
1422
+ React.createElement(
1423
+ 'div',
1424
+ { className: 'drw-grid-2' },
1425
+ React.createElement(
1426
+ 'div',
1427
+ { className: 'drw-field' },
1428
+ React.createElement('span', { className: 'drw-label' }, t('fName')),
1429
+ React.createElement('input', {
1430
+ className: 'drw-input',
1431
+ value: editing.name || '',
1432
+ onChange: (e) => setEditing({ ...editing, name: e.target.value })
1433
+ })
1434
+ ),
1435
+ React.createElement(
1436
+ 'div',
1437
+ { className: 'drw-field' },
1438
+ React.createElement('span', { className: 'drw-label' }, t('fHost')),
1439
+ React.createElement('input', {
1440
+ className: 'drw-input',
1441
+ placeholder: '192.168.1.100 or server.domain.com',
1442
+ value: editing.host || '',
1443
+ onChange: (e) => setEditing({ ...editing, host: e.target.value })
1444
+ })
1445
+ ),
1446
+ React.createElement(
1447
+ 'div',
1448
+ { className: 'drw-field' },
1449
+ React.createElement('span', { className: 'drw-label' }, t('fPort')),
1450
+ React.createElement('input', {
1451
+ className: 'drw-input',
1452
+ type: 'number',
1453
+ value: editing.port || 22,
1454
+ onChange: (e) => setEditing({ ...editing, port: parseInt(e.target.value, 10) || 22 })
1455
+ })
1456
+ ),
1457
+ React.createElement(
1458
+ 'div',
1459
+ { className: 'drw-field' },
1460
+ React.createElement('span', { className: 'drw-label' }, t('fUser')),
1461
+ React.createElement('input', {
1462
+ className: 'drw-input',
1463
+ value: editing.username || '',
1464
+ onChange: (e) => setEditing({ ...editing, username: e.target.value })
1465
+ })
1466
+ )
1467
+ ),
1468
+
1469
+ // Auth Method Segmented Switcher (Key vs Password)
1470
+ React.createElement(
1471
+ 'div',
1472
+ { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
1473
+ React.createElement('span', { className: 'drw-label' }, t('fAuthType')),
1474
+ React.createElement(
1475
+ 'div',
1476
+ { className: 'drw-segmented' },
1477
+ React.createElement(
1478
+ 'button',
1479
+ {
1480
+ type: 'button',
1481
+ className: `drw-segmented-item ${editing.authType !== 'password' ? 'drw-segmented-item-active' : ''}`,
1482
+ onClick: () => setEditing({ ...editing, authType: 'key' })
1483
+ },
1484
+ '🔑 ' + t('fAuthKey')
1485
+ ),
1486
+ React.createElement(
1487
+ 'button',
1488
+ {
1489
+ type: 'button',
1490
+ className: `drw-segmented-item ${editing.authType === 'password' ? 'drw-segmented-item-active' : ''}`,
1491
+ onClick: () => setEditing({ ...editing, authType: 'password' })
1492
+ },
1493
+ '🔒 ' + t('fAuthPass')
1494
+ )
1495
+ )
1496
+ ),
1497
+
1498
+ // Conditional Auth Inputs
1499
+ editing.authType === 'password'
1500
+ ? React.createElement(
1501
+ 'div',
1502
+ { className: 'drw-field' },
1503
+ React.createElement('span', { className: 'drw-label' }, t('fPassword')),
1504
+ React.createElement(
1505
+ 'div',
1506
+ { style: { display: 'flex', gap: '8px' } },
1507
+ React.createElement('input', {
1508
+ className: 'drw-input',
1509
+ type: showPassword ? 'text' : 'password',
1510
+ placeholder: '••••••••••••',
1511
+ value: editing.password || '',
1512
+ onChange: (e) => setEditing({ ...editing, password: e.target.value })
1513
+ }),
1514
+ React.createElement(
1515
+ 'button',
1516
+ {
1517
+ type: 'button',
1518
+ className: 'drw-btn',
1519
+ style: { fontSize: '11px', whiteSpace: 'nowrap' },
1520
+ onClick: () => setShowPassword((v) => !v)
1521
+ },
1522
+ showPassword ? 'Hide' : 'Show'
1523
+ )
1524
+ )
1525
+ )
1526
+ : React.createElement(
1527
+ 'div',
1528
+ { className: 'drw-grid-2' },
1529
+ React.createElement(
1530
+ 'div',
1531
+ { className: 'drw-field' },
1532
+ React.createElement('span', { className: 'drw-label' }, t('fKeyPath')),
1533
+ React.createElement('input', {
1534
+ className: 'drw-input',
1535
+ placeholder: '~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
1536
+ value: editing.privateKeyPath || '',
1537
+ onChange: (e) => setEditing({ ...editing, privateKeyPath: e.target.value })
1538
+ }),
1539
+ React.createElement('span', { className: 'drw-hint' }, t('fKeyPathHint'))
1540
+ ),
1541
+ React.createElement(
1542
+ 'div',
1543
+ { className: 'drw-field' },
1544
+ React.createElement('span', { className: 'drw-label' }, t('fPassphrase')),
1545
+ React.createElement('input', {
1546
+ className: 'drw-input',
1547
+ type: 'password',
1548
+ placeholder: 'Optional passphrase',
1549
+ value: editing.passphrase || '',
1550
+ onChange: (e) => setEditing({ ...editing, passphrase: e.target.value })
1551
+ })
1552
+ )
1553
+ ),
1554
+
1555
+ // Workspace Paths (Remote Workspace & Local Mirror)
1556
+ React.createElement(
1557
+ 'div',
1558
+ { className: 'drw-grid-2' },
1559
+ React.createElement(
1560
+ 'div',
1561
+ { className: 'drw-field' },
1562
+ React.createElement('span', { className: 'drw-label' }, t('fRemotePath')),
1563
+ React.createElement(
1564
+ 'div',
1565
+ { style: { display: 'flex', gap: '8px' } },
1566
+ React.createElement('input', {
1567
+ className: 'drw-input',
1568
+ placeholder: '/home/user/project',
1569
+ value: editing.remoteWorkspace || '',
1570
+ onChange: (e) => setEditing({ ...editing, remoteWorkspace: e.target.value })
1571
+ }),
1572
+ React.createElement(
1573
+ 'button',
1574
+ {
1575
+ type: 'button',
1576
+ className: 'drw-btn',
1577
+ style: { whiteSpace: 'nowrap', fontSize: '12px' },
1578
+ disabled: !editing.host,
1579
+ onClick: () => openDirectoryBrowser(editing.remoteWorkspace || '/')
1580
+ },
1581
+ t('btnBrowse')
1582
+ )
1583
+ ),
1584
+ React.createElement('span', { className: 'drw-hint' }, t('fRemotePathHint'))
1585
+ ),
1586
+ React.createElement(
1587
+ 'div',
1588
+ { className: 'drw-field' },
1589
+ React.createElement('span', { className: 'drw-label' }, t('fLocalMirror')),
1590
+ React.createElement('input', {
1591
+ className: 'drw-input',
1592
+ placeholder: '~/.dsh/mirrors/my-project',
1593
+ value: editing.localMirrorPath || '',
1594
+ onChange: (e) => setEditing({ ...editing, localMirrorPath: e.target.value })
1595
+ }),
1596
+ React.createElement('span', { className: 'drw-hint' }, t('fLocalMirrorHint'))
1597
+ )
1598
+ ),
1599
+
1600
+ // Directory Browser Modal
1601
+ browserOpen
1602
+ ? React.createElement(
1603
+ 'div',
1604
+ { className: 'drw-browser-modal' },
1605
+ React.createElement(
1606
+ 'div',
1607
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
1608
+ React.createElement('span', { style: { fontSize: '12px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, `📂 ${browserPath}`),
1609
+ React.createElement(
1610
+ 'div',
1611
+ { style: { display: 'flex', gap: '6px' } },
1612
+ React.createElement(
1613
+ 'button',
1614
+ {
1615
+ type: 'button',
1616
+ className: 'drw-btn drw-btn-primary',
1617
+ style: { fontSize: '11px', padding: '3px 8px' },
1618
+ onClick: () => {
1619
+ setEditing({ ...editing, remoteWorkspace: browserPath })
1620
+ setBrowserOpen(false)
1621
+ }
1622
+ },
1623
+ t('btnSelectThis')
1624
+ ),
1625
+ React.createElement(
1626
+ 'button',
1627
+ {
1628
+ type: 'button',
1629
+ className: 'drw-btn',
1630
+ style: { fontSize: '11px', padding: '3px 8px' },
1631
+ onClick: () => setBrowserOpen(false)
1632
+ },
1633
+ '✕'
1634
+ )
1635
+ )
1636
+ ),
1637
+ React.createElement(
1638
+ 'div',
1639
+ { className: 'drw-browser-list' },
1640
+ browserLoading
1641
+ ? React.createElement('div', { style: { padding: '8px', fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, 'Loading directory entries...')
1642
+ : React.createElement(
1643
+ React.Fragment,
1644
+ null,
1645
+ browserPath !== '/'
1646
+ ? React.createElement(
1647
+ 'div',
1648
+ {
1649
+ className: 'drw-browser-item',
1650
+ onClick: () => {
1651
+ const parent = browserPath.substring(0, browserPath.lastIndexOf('/')) || '/'
1652
+ fetchRemoteDir(parent)
1653
+ }
1654
+ },
1655
+ '📁 .. (parent directory)'
1656
+ )
1657
+ : null,
1658
+ browserEntries
1659
+ .filter((e) => e.isDirectory)
1660
+ .map((e) =>
1661
+ React.createElement(
1662
+ 'div',
1663
+ {
1664
+ key: e.filename,
1665
+ className: 'drw-browser-item',
1666
+ onClick: () => {
1667
+ const next = browserPath === '/' ? `/${e.filename}` : `${browserPath}/${e.filename}`
1668
+ fetchRemoteDir(next)
1669
+ }
1670
+ },
1671
+ `📁 ${e.filename}`
1672
+ )
1673
+ )
1674
+ )
1675
+ )
1676
+ )
1677
+ : null,
1678
+
1679
+ // Form Action Buttons
1680
+ React.createElement(
1681
+ 'div',
1682
+ { className: 'drw-row', style: { justifyContent: 'flex-end', marginTop: '6px' } },
1683
+ React.createElement(
1684
+ 'button',
1685
+ {
1686
+ type: 'button',
1687
+ className: 'drw-btn',
1688
+ disabled: !editing.host || !!busy,
1689
+ onClick: () => handleTest(editing)
1690
+ },
1691
+ busy === 'testing' ? t('testing') : t('btnTest')
1692
+ ),
1693
+ React.createElement(
1694
+ 'button',
1695
+ {
1696
+ type: 'button',
1697
+ className: 'drw-btn',
1698
+ onClick: () => {
1699
+ setEditing(null)
1700
+ setBrowserOpen(false)
1701
+ setTestResult(null)
1702
+ }
1703
+ },
1704
+ t('btnCancel')
1705
+ ),
1706
+ React.createElement(
1707
+ 'button',
1708
+ {
1709
+ type: 'button',
1710
+ className: 'drw-btn drw-btn-primary',
1711
+ disabled: !editing.host || !!busy,
1712
+ onClick: () => handleSave(editing)
1713
+ },
1714
+ busy === 'saving' ? t('saving') : t('btnSave')
1715
+ )
1716
+ ),
1717
+ testResult
1718
+ ? React.createElement(
1719
+ 'div',
1720
+ {
1721
+ className: 'drw-preview',
1722
+ style: {
1723
+ borderColor: testResult.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
1724
+ }
1725
+ },
1726
+ testResult.ok
1727
+ ? t('testOk', { latency: testResult.latencyMs ?? testResult.latency ?? 0, os: testResult.remoteOs || testResult.os || 'Linux' })
1728
+ : t('testFail', { err: testResult.error || 'Connection timed out' })
1729
+ )
1730
+ : null
1731
+ )
1732
+ : null,
1733
+
1734
+ // Profiles List Table
1735
+ profiles.length > 0
1736
+ ? React.createElement(
1737
+ 'table',
1738
+ { className: 'drw-table' },
1739
+ React.createElement(
1740
+ 'thead',
1741
+ null,
1742
+ React.createElement(
1743
+ 'tr',
1744
+ null,
1745
+ React.createElement('th', null, t('nameCol')),
1746
+ React.createElement('th', null, t('hostCol')),
1747
+ React.createElement('th', null, t('portCol')),
1748
+ React.createElement('th', null, t('pathCol')),
1749
+ React.createElement('th', { style: { textAlign: 'right' } }, t('actionsCol'))
1750
+ )
1751
+ ),
1752
+ React.createElement(
1753
+ 'tbody',
1754
+ null,
1755
+ profiles.map((p) => {
1756
+ const isAct = p.id === activeId
1757
+ return React.createElement(
1758
+ 'tr',
1759
+ { key: p.id },
1760
+ React.createElement(
1761
+ 'td',
1762
+ null,
1763
+ React.createElement('span', { style: { fontWeight: 600 } }, p.name || 'Server'),
1764
+ React.createElement(
1765
+ 'span',
1766
+ { className: 'drw-badge', style: { marginLeft: '8px' } },
1767
+ p.authType === 'password' ? '🔒 pwd' : '🔑 key'
1768
+ ),
1769
+ isAct
1770
+ ? React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginLeft: '6px' } }, t('activeBadge'))
1771
+ : null
1772
+ ),
1773
+ React.createElement('td', { style: { fontFamily: 'monospace' } }, `${p.username || 'root'}@${p.host}`),
1774
+ React.createElement('td', { style: { fontFamily: 'monospace' } }, p.port || 22),
1775
+ React.createElement('td', { style: { fontFamily: 'monospace', fontSize: '12px' } }, p.remoteWorkspace || '—'),
1776
+ React.createElement(
1777
+ 'td',
1778
+ { style: { textAlign: 'right' } },
1779
+ React.createElement(
1780
+ 'div',
1781
+ { style: { display: 'inline-flex', gap: '6px' } },
1782
+ !isAct
1783
+ ? React.createElement(
1784
+ 'button',
1785
+ {
1786
+ type: 'button',
1787
+ className: 'drw-btn',
1788
+ style: { fontSize: '11px', padding: '3px 8px' },
1789
+ onClick: () => handleSetActive(p.id)
1790
+ },
1791
+ t('pinBtn')
1792
+ )
1793
+ : null,
1794
+ React.createElement(
1795
+ 'button',
1796
+ {
1797
+ type: 'button',
1798
+ className: 'drw-btn',
1799
+ style: { fontSize: '11px', padding: '3px 8px' },
1800
+ onClick: () => {
1801
+ setEditing({ ...p })
1802
+ setTestResult(null)
1803
+ }
1804
+ },
1805
+ t('editBtn')
1806
+ ),
1807
+ React.createElement(
1808
+ 'button',
1809
+ {
1810
+ type: 'button',
1811
+ className: 'drw-btn drw-btn-danger',
1812
+ style: { fontSize: '11px', padding: '3px 8px' },
1813
+ onClick: () => handleDelete(p.id, p.name)
1814
+ },
1815
+ t('delBtn')
1816
+ )
1817
+ )
1818
+ )
1819
+ )
1820
+ })
1821
+ )
1822
+ )
1823
+ : !editing
1824
+ ? React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('noHosts'))
1825
+ : null
1826
+ ),
1827
+
1828
+ // Section 2: Active Connection Diagnostics (ClineBot Card 4 style)
1829
+ activeProfile
1830
+ ? React.createElement(
1831
+ 'div',
1832
+ { className: 'drw-card' },
1833
+ React.createElement(
1834
+ 'div',
1835
+ { className: 'drw-card-title' },
1836
+ React.createElement('span', null, t('diagTitle')),
1837
+ React.createElement(
1838
+ 'button',
1839
+ {
1840
+ type: 'button',
1841
+ className: 'drw-btn',
1842
+ style: { fontSize: '11px', padding: '3px 9px' },
1843
+ disabled: busy === 'testing',
1844
+ onClick: () => handleTest(activeProfile)
1845
+ },
1846
+ busy === 'testing' ? t('testing') : t('diagProbeBtn')
1847
+ )
1848
+ ),
1849
+ React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
1850
+ React.createElement(
1851
+ 'div',
1852
+ { className: 'drw-grid-4' },
1853
+ React.createElement(
1854
+ 'div',
1855
+ { className: 'drw-stat-box' },
1856
+ React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
1857
+ React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
1858
+ ),
1859
+ React.createElement(
1860
+ 'div',
1861
+ { className: 'drw-stat-box' },
1862
+ React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
1863
+ React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
1864
+ ),
1865
+ React.createElement(
1866
+ 'div',
1867
+ { className: 'drw-stat-box' },
1868
+ React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
1869
+ React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
1870
+ ),
1871
+ React.createElement(
1872
+ 'div',
1873
+ { className: 'drw-stat-box' },
1874
+ React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
1875
+ React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
1876
+ )
1877
+ )
1878
+ )
1879
+ : null,
1880
+
1881
+ // Section 3: Mirror Sync (ClineBot Action Card style)
1882
+ activeProfile
1883
+ ? React.createElement(
1884
+ 'div',
1885
+ { className: 'drw-card' },
1886
+ React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
1887
+ React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
1888
+
1889
+ React.createElement(
1890
+ 'div',
1891
+ { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)', marginBottom: '12px' } },
1892
+ React.createElement(
1893
+ 'div',
1894
+ { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
1895
+ React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, t('autoSyncTitle')),
1896
+ React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('autoSyncDesc'))
1897
+ ),
1898
+ React.createElement(
1899
+ 'button',
1900
+ {
1901
+ type: 'button',
1902
+ className: `drw-btn ${autoSync ? 'drw-btn-primary' : ''}`,
1903
+ style: { padding: '4px 12px', fontSize: '12px' },
1904
+ onClick: async () => {
1905
+ const next = !autoSync
1906
+ try {
1907
+ const res = await fetch('/dsh-remote-workspace/watcher/toggle', {
1908
+ method: 'POST',
1909
+ headers: { 'Content-Type': 'application/json' },
1910
+ body: JSON.stringify({ enabled: next })
1911
+ })
1912
+ const d = await res.json()
1913
+ if (d.ok) setAutoSync(d.autoSync)
1914
+ } catch (err) { /* best-effort cleanup */ }
1915
+ }
1916
+ },
1917
+ autoSync ? '⚡ Active (ON)' : '⚪ Off'
1918
+ )
1919
+ ),
1920
+
1921
+ React.createElement(
1922
+ 'div',
1923
+ { className: 'drw-row' },
1924
+ React.createElement(
1925
+ 'button',
1926
+ {
1927
+ type: 'button',
1928
+ className: 'drw-btn',
1929
+ disabled: !!busy || !activeProfile.remoteWorkspace || !activeProfile.localMirrorPath,
1930
+ onClick: () => handleSync('pull')
1931
+ },
1932
+ busy === 'sync_pull' ? t('pulling') : t('pullBtn')
1933
+ ),
1934
+ React.createElement(
1935
+ 'button',
1936
+ {
1937
+ type: 'button',
1938
+ className: 'drw-btn',
1939
+ disabled: !!busy || !activeProfile.remoteWorkspace || !activeProfile.localMirrorPath,
1940
+ onClick: () => handleSync('push')
1941
+ },
1942
+ busy === 'sync_push' ? t('pushing') : t('pushBtn')
1943
+ )
1944
+ ),
1945
+ syncMsg
1946
+ ? React.createElement(
1947
+ 'div',
1948
+ {
1949
+ className: 'drw-preview',
1950
+ style: {
1951
+ borderColor: syncMsg.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
1952
+ }
1953
+ },
1954
+ syncMsg.text
1955
+ )
1956
+ : null
1957
+ )
1958
+ : null,
1959
+
1960
+ // Section 4: SSH Port Forwarding (Tunnels Manager)
1961
+ React.createElement(
1962
+ 'div',
1963
+ { className: 'drw-card' },
1964
+ React.createElement('div', { className: 'drw-card-title' }, t('tunnelsTitle')),
1965
+ React.createElement('div', { className: 'drw-card-desc' }, t('tunnelsDesc')),
1966
+
1967
+ // Start tunnel inline row
1968
+ activeProfile
1969
+ ? React.createElement(
1970
+ 'div',
1971
+ { className: 'drw-row', style: { alignItems: 'flex-end', background: 'var(--dsw-alias-bg-layer-2)', padding: '10px 12px', borderRadius: '8px' } },
1972
+ React.createElement(
1973
+ 'div',
1974
+ { style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
1975
+ React.createElement('span', { className: 'drw-label' }, t('tLocalPort')),
1976
+ React.createElement('input', {
1977
+ className: 'drw-input',
1978
+ style: { width: '120px' },
1979
+ type: 'number',
1980
+ value: newTunnelLocal,
1981
+ onChange: (e) => setNewTunnelLocal(e.target.value)
1982
+ })
1983
+ ),
1984
+ React.createElement('span', { style: { paddingBottom: '8px', color: 'var(--dsw-alias-label-secondary)' } }, '→'),
1985
+ React.createElement(
1986
+ 'div',
1987
+ { style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
1988
+ React.createElement('span', { className: 'drw-label' }, t('tRemotePort')),
1989
+ React.createElement('input', {
1990
+ className: 'drw-input',
1991
+ style: { width: '120px' },
1992
+ type: 'number',
1993
+ value: newTunnelRemote,
1994
+ onChange: (e) => setNewTunnelRemote(e.target.value)
1995
+ })
1996
+ ),
1997
+ React.createElement(
1998
+ 'button',
1999
+ {
2000
+ type: 'button',
2001
+ className: 'drw-btn drw-btn-primary',
2002
+ disabled: busy === 'tunnel_open',
2003
+ onClick: handleStartTunnel
2004
+ },
2005
+ busy === 'tunnel_open' ? t('tOpening') : t('tOpenBtn')
2006
+ )
2007
+ )
2008
+ : null,
2009
+
2010
+ tunnelMsg
2011
+ ? React.createElement(
2012
+ 'div',
2013
+ {
2014
+ className: 'drw-preview',
2015
+ style: {
2016
+ borderColor: tunnelMsg.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
2017
+ }
2018
+ },
2019
+ tunnelMsg.text
2020
+ )
2021
+ : null,
2022
+
2023
+ // Tunnels Table
2024
+ tunnels.length > 0
2025
+ ? React.createElement(
2026
+ 'table',
2027
+ { className: 'drw-table' },
2028
+ React.createElement(
2029
+ 'thead',
2030
+ null,
2031
+ React.createElement(
2032
+ 'tr',
2033
+ null,
2034
+ React.createElement('th', null, t('tunnelsColId')),
2035
+ React.createElement('th', null, t('tunnelsColRoute')),
2036
+ React.createElement('th', { style: { textAlign: 'right' } }, t('tunnelsColActions'))
2037
+ )
2038
+ ),
2039
+ React.createElement(
2040
+ 'tbody',
2041
+ null,
2042
+ tunnels.map((tun) =>
2043
+ React.createElement(
2044
+ 'tr',
2045
+ { key: tun.id },
2046
+ React.createElement('td', { style: { fontFamily: 'monospace', fontSize: '12px' } }, tun.id),
2047
+ React.createElement(
2048
+ 'td',
2049
+ null,
2050
+ React.createElement('span', { className: 'drw-badge drw-badge-ok' }, `127.0.0.1:${tun.localPort}`),
2051
+ ' → ',
2052
+ React.createElement('span', { className: 'drw-badge' }, `${tun.targetHost || '127.0.0.1'}:${tun.remotePort}`)
2053
+ ),
2054
+ React.createElement(
2055
+ 'td',
2056
+ { style: { textAlign: 'right' } },
2057
+ React.createElement(
2058
+ 'button',
2059
+ {
2060
+ type: 'button',
2061
+ className: 'drw-btn drw-btn-danger',
2062
+ style: { fontSize: '11px', padding: '3px 8px' },
2063
+ onClick: () => handleStopTunnel(tun.id)
2064
+ },
2065
+ t('tStopBtn')
2066
+ )
2067
+ )
2068
+ )
2069
+ )
2070
+ )
2071
+ )
2072
+ : React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('tNoTunnels'))
2073
+ )
2074
+ )
2075
+ }
2076
+
2077
+ // Accordion item for Settings -> Plugins section
2078
+
2079
+ function UpdaterSection(props) {
2080
+ const t = props.t || ((k) => k);
2081
+ const [updateStatus, setUpdateStatus] = React.useState(null);
2082
+ const [loading, setLoading] = React.useState(false);
2083
+ const [updating, setUpdating] = React.useState(false);
2084
+ const [msg, setMsg] = React.useState(null);
2085
+
2086
+ const checkUpdate = React.useCallback(async () => {
2087
+ setLoading(true);
2088
+ setMsg(null);
2089
+ try {
2090
+ const res = await fetch('/dsh-remote-workspace/update', {
2091
+ headers: { accept: 'application/json' },
2092
+ cache: 'no-store'
2093
+ });
2094
+ if (!res.ok) throw new Error('HTTP ' + res.status);
2095
+ const data = await res.json();
2096
+ setUpdateStatus(data);
2097
+ } catch (e) {
2098
+ setMsg({ ok: false, text: (t('updateFailed') || 'Failed: ') + (e?.message || String(e)) });
2099
+ } finally {
2100
+ setLoading(false);
2101
+ }
2102
+ }, [t]);
2103
+
2104
+ const doUpdate = async () => {
2105
+ setUpdating(true);
2106
+ setMsg(null);
2107
+ try {
2108
+ const res = await fetch('/dsh-remote-workspace/update', {
2109
+ method: 'POST',
2110
+ headers: {
2111
+ 'x-dsh-plugin-update': '1',
2112
+ 'content-type': 'application/json'
2113
+ }
2114
+ });
2115
+ const data = await res.json();
2116
+ if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status));
2117
+ setUpdateStatus(data);
2118
+ if (data.restartRequired) {
2119
+ setMsg({ ok: true, text: t('restartRequired') });
2120
+ }
2121
+ } catch (e) {
2122
+ setMsg({ ok: false, text: (t('updateFailed') || 'Update failed: ') + (e?.message || String(e)) });
2123
+ } finally {
2124
+ setUpdating(false);
2125
+ }
2126
+ };
2127
+
2128
+ React.useEffect(() => {
2129
+ checkUpdate();
2130
+ }, [checkUpdate]);
2131
+
2132
+ return React.createElement(
2133
+ 'div',
2134
+ {
2135
+ style: {
2136
+ display: 'flex',
2137
+ alignItems: 'center',
2138
+ justifyContent: 'space-between',
2139
+ padding: '10px 14px',
2140
+ marginBottom: '14px',
2141
+ borderRadius: '8px',
2142
+ background: 'var(--dsw-alias-bg-layer-2)',
2143
+ border: '1px solid var(--dsw-alias-border-l2)',
2144
+ fontSize: '13px'
2145
+ }
2146
+ },
2147
+ React.createElement(
2148
+ 'div',
2149
+ { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
2150
+ React.createElement('span', null, '📦'),
2151
+ React.createElement(
2152
+ 'div',
2153
+ null,
2154
+ React.createElement('span', { style: { fontWeight: 500 } }, 'v' + (updateStatus?.currentVersion || '0.3.1')),
2155
+ updateStatus?.latestVersion && updateStatus.latestVersion !== updateStatus.currentVersion
2156
+ ? React.createElement('span', { style: { marginLeft: '8px', color: 'var(--dsw-alias-state-warning-text)' } }, '→ v' + updateStatus.latestVersion)
2157
+ : null,
2158
+ msg
2159
+ ? React.createElement('div', { style: { fontSize: '11px', marginTop: '3px', color: msg.ok ? 'var(--dsw-alias-state-success-text)' : 'var(--dsw-alias-state-error-text)' } }, msg.text)
2160
+ : null
2161
+ )
2162
+ ),
2163
+ React.createElement(
2164
+ 'div',
2165
+ { style: { display: 'flex', gap: '8px' } },
2166
+ updateStatus?.updateAvailable
2167
+ ? React.createElement(
2168
+ 'button',
2169
+ {
2170
+ type: 'button',
2171
+ className: 'drw-btn drw-btn-primary',
2172
+ disabled: updating,
2173
+ onClick: doUpdate,
2174
+ style: { fontSize: '12px', padding: '4px 10px' }
2175
+ },
2176
+ updating ? t('updating') : t('updateNow')
2177
+ )
2178
+ : React.createElement(
2179
+ 'button',
2180
+ {
2181
+ type: 'button',
2182
+ className: 'drw-btn',
2183
+ disabled: loading,
2184
+ onClick: checkUpdate,
2185
+ style: { fontSize: '12px', padding: '4px 10px' }
2186
+ },
2187
+ loading ? t('checking') : (updateStatus?.updateAvailable === false ? t('upToDate') : t('checkUpdate'))
2188
+ )
2189
+ )
2190
+ );
2191
+ }
2192
+
2193
+ function PluginCard(props) {
2194
+ const [open, setOpen] = React.useState(false)
2195
+ const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
2196
+
2197
+ React.useEffect(() => {
2198
+ ensureCss()
2199
+ }, [])
2200
+
2201
+ return React.createElement(
2202
+ 'li',
2203
+ { className: 'drw-card', style: { listStyle: 'none', marginBottom: '12px' } },
2204
+ React.createElement(
2205
+ 'button',
2206
+ {
2207
+ type: 'button',
2208
+ style: {
2209
+ background: 'none',
2210
+ border: 'none',
2211
+ cursor: 'pointer',
2212
+ display: 'flex',
2213
+ alignItems: 'center',
2214
+ width: '100%',
2215
+ padding: 0,
2216
+ textAlign: 'left',
2217
+ },
2218
+ 'aria-expanded': open,
2219
+ onClick: () => setOpen((v) => !v),
2220
+ },
2221
+ React.createElement('span', { style: { fontSize: '20px', marginRight: '12px' } }, '🌐'),
2222
+ React.createElement(
2223
+ 'div',
2224
+ { style: { flex: 1 } },
2225
+ React.createElement('div', { style: { fontWeight: 600, fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, t('title')),
2226
+ React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
2227
+ ),
2228
+ React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginRight: '14px' } }, t('status')),
2229
+ React.createElement(
2230
+ 'span',
2231
+ { style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
2232
+ React.createElement(FallbackChevron)
2233
+ )
2234
+ ),
2235
+ open
2236
+ ? React.createElement(
2237
+ 'div',
2238
+ { style: { marginTop: '16px', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '16px' } },
2239
+ React.createElement(React.Fragment, null, React.createElement(UpdaterSection, props), React.createElement(SettingsView, props))
2240
+ )
2241
+ : null
2242
+ )
2243
+ }
2244
+
2245
+ function RemoteWorkspaceChip(props) {
2246
+ ensureCss()
2247
+ const [activeHost, setActiveHost] = React.useState(null)
2248
+
2249
+ React.useEffect(() => {
2250
+ fetch('/dsh-remote-workspace/state')
2251
+ .then((r) => r.json())
2252
+ .then((d) => {
2253
+ if (d.ok && d.activeId && d.profiles) {
2254
+ const f = d.profiles.find((p) => p.id === d.activeId)
2255
+ if (f) setActiveHost(f.name || f.host)
2256
+ }
2257
+ })
2258
+ .catch(() => {})
2259
+ }, [])
2260
+
2261
+ return React.createElement(
2262
+ 'div',
2263
+ {
2264
+ style: {
2265
+ display: 'flex',
2266
+ alignItems: 'center',
2267
+ gap: '6px',
2268
+ padding: '2px 8px',
2269
+ fontSize: '11px',
2270
+ fontFamily: 'monospace',
2271
+ borderRadius: '999px',
2272
+ border: '1px solid var(--dsw-alias-border-l2)',
2273
+ background: 'var(--dsw-alias-bg-layer-1)',
2274
+ color: activeHost ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
2275
+ cursor: 'pointer'
2276
+ }
2277
+ },
2278
+ React.createElement('span', null, '🌐'),
2279
+ React.createElement('span', null, activeHost ? `SSH: ${activeHost}` : 'SSH: Local')
2280
+ )
2281
+ }
2282
+
2283
+ function apply(ctx) {
2284
+ if (ctx.locale && typeof ctx.locale.register === 'function') {
2285
+ if (typeof ctx.effect === 'function') {
2286
+ ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-remote-workspace: dictionaries');
2287
+ } else {
2288
+ ctx.locale.register(NS, { en, zh });
2289
+ }
2290
+ }
2291
+
2292
+ if (ctx.slots && typeof ctx.slots.inject === 'function') {
2293
+ ctx.slots.inject('settings.plugin.item', () =>
2294
+ ctx.slots.register(
2295
+ {
2296
+ name: 'settings.plugin.item',
2297
+ key: NS,
2298
+ locale: NS,
2299
+ inject: () => ({ ctx })
2300
+ },
2301
+ (props) => React.createElement(PluginCard, Object.assign({}, props, { ctx }))
2302
+ )
2303
+ )
2304
+
2305
+ ctx.slots.inject('conversation.session.header.utilities', () =>
2306
+ ctx.slots.register(
2307
+ {
2308
+ name: 'conversation.session.header.utilities',
2309
+ id: '@goodandready/dsh-remote-workspace',
2310
+ order: 30
2311
+ },
2312
+ (props) => React.createElement(RemoteWorkspaceChip, props)
2313
+ )
2314
+ )
2315
+ }
2316
+ }
2317
+
2318
+ module.exports = { apply, inject: ['slots', 'locale'] }
2319
+ return module.exports
2320
+
2321
+ },
2322
+ })