@goodandready/dsh-remote-workspace 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1,1278 +1,1991 @@
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
- document.head.appendChild(style)
72
- }
73
-
74
- const en = {
75
- title: 'Remote Workspace',
76
- subtitle: 'Native SSH connections, SFTP file access, 3-way mirror sync, and port forwarding tunnels.',
77
- status: 'Ready',
78
- badgeOnline: 'Active Host: {name} ({latency} ms)',
79
- badgeOffline: 'No active host',
80
- badgeTunnels: 'Tunnels: {count}',
81
- badgeKey: '🔑 Key Auth',
82
- badgePassword: '🔒 Password Auth',
83
- profilesTitle: '🖥️ Remote SSH Profiles',
84
- profilesDesc: 'Configure remote machines for code execution, SFTP browsing, and background mirror sync.',
85
- addBtn: '+ Add Host',
86
- nameCol: 'Name',
87
- hostCol: 'Host / User',
88
- portCol: 'Port',
89
- pathCol: 'Remote Workspace',
90
- actionsCol: 'Actions',
91
- noHosts: 'No remote hosts configured yet. Click "+ Add Host" to create your first connection.',
92
- activeBadge: 'Active',
93
- pinBtn: 'Pin Active',
94
- editBtn: 'Edit',
95
- delBtn: 'Delete',
96
- delConfirm: 'Delete host profile "{name}"?',
97
- formTitleNew: 'Add New Host Profile',
98
- formTitleEdit: 'Edit Host Profile',
99
- fName: 'Profile Name',
100
- fHost: 'Host / IP Address',
101
- fPort: 'Port',
102
- fUser: 'Username',
103
- fAuthType: 'Authentication Method',
104
- fAuthKey: 'SSH Private Key',
105
- fAuthPass: 'Password',
106
- fKeyPath: 'Private Key Path',
107
- fKeyPathHint: 'Path to ~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
108
- fPassphrase: 'Key Passphrase (optional)',
109
- fPassword: 'Account Password',
110
- fRemotePath: 'Remote Workspace Directory',
111
- fRemotePathHint: 'Absolute path on the server (e.g. /home/user/my-project)',
112
- fLocalMirror: 'Local Mirror Directory',
113
- fLocalMirrorHint: 'Local directory where files are mirrored for fast 3-way synchronization',
114
- btnBrowse: '📁 Browse Remote...',
115
- btnSelectThis: 'Select Directory',
116
- btnTest: '⚡ Test Connection (Ping)',
117
- btnSave: 'Save Profile',
118
- btnCancel: 'Cancel',
119
- testing: 'Testing Connection...',
120
- saving: 'Saving...',
121
- testOk: '✅ Connection OK! Latency: {latency} ms | OS: {os}',
122
- testFail: '❌ Connection Failed: {err}',
123
- diagTitle: '📊 Active Connection Diagnostics',
124
- diagDesc: 'Real-time telemetry and engine status for the currently selected remote machine.',
125
- diagProbeBtn: '⚡ Quick Probe',
126
- syncTitle: '🔄 Two-Way Workspace Mirror Sync',
127
- syncDesc: 'Fast 3-way conflict-aware file synchronization between local mirror and remote server.',
128
- pullBtn: '⬇️ Pull from Remote',
129
- pushBtn: '⬆️ Push to Remote',
130
- pulling: 'Pulling files…',
131
- pushing: 'Pushing files…',
132
- syncOk: 'Sync completed! Updated {count} file(s).',
133
- syncConflict: 'Conflict warning: {count} file(s) modified on both sides.',
134
- syncErr: 'Sync error: {err}',
135
- tunnelsTitle: '🔀 SSH Port Forwarding (Tunnels)',
136
- tunnelsDesc: 'Forward remote ports (e.g. databases, web servers, APIs) to your local machine via encrypted SSH tunnels.',
137
- tLocalPort: 'Local Port',
138
- tRemotePort: 'Remote Port',
139
- tOpenBtn: '+ Open Tunnel',
140
- tOpening: 'Opening…',
141
- tStopBtn: 'Stop',
142
- tNoTunnels: 'No active port forwarding tunnels.',
143
- tunnelsColId: 'Tunnel ID',
144
- tunnelsColRoute: 'Forwarding Route',
145
- tunnelsColActions: 'Actions',
146
- tOpenOk: 'Tunnel opened: 127.0.0.1:{localPort} -> Remote:{remotePort}',
147
- tOpenErr: 'Failed to open tunnel: {err}'
148
- }
149
-
150
- const ru = {
151
- title: 'Удалённое рабочее пространство',
152
- subtitle: 'SSH-подключения, доступ к файлам по SFTP, 3-way синхронизация зеркала и проброс портов.',
153
- status: 'Готово',
154
- badgeOnline: 'Активный хост: {name} ({latency} мс)',
155
- badgeOffline: 'Хост не выбран',
156
- badgeTunnels: 'Туннелей: {count}',
157
- badgeKey: '🔑 Авторизация по ключу',
158
- badgePassword: '🔒 Авторизация по паролю',
159
- profilesTitle: '🖥️ Профили SSH серверов',
160
- profilesDesc: 'Настройка удалённых серверов для запуска команд, SFTP-навигации и синхронизации зеркала.',
161
- addBtn: '+ Добавить хост',
162
- nameCol: 'Имя',
163
- hostCol: 'Хост / Пользователь',
164
- portCol: 'Порт',
165
- pathCol: 'Удалённый путь',
166
- actionsCol: 'Действия',
167
- noHosts: 'Удалённые хосты ещё не настроены. Нажмите «+ Добавить хост» для создания подключения.',
168
- activeBadge: 'Активен',
169
- pinBtn: 'Сделать активным',
170
- editBtn: 'Править',
171
- delBtn: 'Удалить',
172
- delConfirm: 'Удалить профиль хоста «{name}»?',
173
- formTitleNew: 'Добавление нового SSH хоста',
174
- formTitleEdit: 'Редактирование профиля хоста',
175
- fName: 'Название подключения',
176
- fHost: 'Хост / IP-адрес',
177
- fPort: 'Порт',
178
- fUser: 'Имя пользователя',
179
- fAuthType: 'Способ авторизации',
180
- fAuthKey: 'SSH-ключ',
181
- fAuthPass: 'Пароль',
182
- fKeyPath: 'Путь к приватному ключу',
183
- fKeyPathHint: 'Путь к ~/.ssh/id_rsa или /home/user/.ssh/id_ed25519',
184
- fPassphrase: 'Кодовая фраза ключа (если есть)',
185
- fPassword: 'Пароль учётной записи',
186
- fRemotePath: 'Удалённая папка проекта',
187
- fRemotePathHint: 'Абсолютный путь на сервере (например, /home/user/project)',
188
- fLocalMirror: 'Локальная папка зеркала',
189
- fLocalMirrorHint: 'Локальная папка для быстрого зеркалирования и бесконфликтной 3-way синхронизации',
190
- btnBrowse: '📁 Обзор на сервере…',
191
- btnSelectThis: 'Выбрать папку',
192
- btnTest: '⚡ Проверить связь (Ping)',
193
- btnSave: 'Сохранить профиль',
194
- btnCancel: 'Отмена',
195
- testing: 'Проверка соединения…',
196
- saving: 'Сохранение…',
197
- testOk: '✅ Соединение успешно! Задержка: {latency} мс | ОС: {os}',
198
- testFail: '❌ Ошибка подключения: {err}',
199
- diagTitle: '📊 Диагностика активного подключения',
200
- diagDesc: 'Телеметрия в реальном времени и состояние подсистемы для выбранного сервера.',
201
- diagProbeBtn: '⚡ Опросить сервер',
202
- syncTitle: '🔄 Двусторонняя синхронизация зеркала',
203
- syncDesc: 'Бесконфликтная 3-way синхронизация файлов между локальным зеркалом и сервером.',
204
- pullBtn: '⬇️ Скачать с сервера (Pull)',
205
- pushBtn: '⬆️ Загрузить на сервер (Push)',
206
- pulling: 'Скачивание файлов…',
207
- pushing: 'Загрузка файлов…',
208
- syncOk: 'Синхронизация завершена! Обновлено файлов: {count}.',
209
- syncConflict: 'Внимание: конфликт изменений в {count} файлах.',
210
- syncErr: 'Ошибка синхронизации: {err}',
211
- tunnelsTitle: '🔀 SSH-туннели (Port Forwarding)',
212
- tunnelsDesc: 'Проброс удалённых портов (базы данных, dev-серверы, API) на локальный компьютер через зашифрованный канал.',
213
- tLocalPort: 'Локальный порт',
214
- tRemotePort: 'Удалённый порт',
215
- tOpenBtn: '+ Открыть туннель',
216
- tOpening: 'Открытие…',
217
- tStopBtn: 'Закрыть',
218
- tNoTunnels: 'Нет активных туннелей проброса портов.',
219
- tunnelsColId: 'ID туннеля',
220
- tunnelsColRoute: 'Маршрут перенаправления',
221
- tunnelsColActions: 'Действия',
222
- tOpenOk: 'Туннель запущен: 127.0.0.1:{localPort} -> Remote:{remotePort}',
223
- tOpenErr: 'Не удалось запустить туннель: {err}'
224
- }
225
-
226
- function makeT(dict, fallback) {
227
- return function t(key, vars) {
228
- let val = (dict && dict[key]) || (fallback && fallback[key]) || key
229
- if (vars && typeof val === 'string') {
230
- for (const k of Object.keys(vars)) {
231
- val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]))
232
- }
233
- }
234
- return val
235
- }
236
- }
237
-
238
- function FallbackChevron() {
239
- return React.createElement(
240
- 'svg',
241
- { width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': true },
242
- React.createElement('path', {
243
- d: 'M3.5 5.25L7 8.75L10.5 5.25',
244
- stroke: 'currentColor',
245
- strokeWidth: 1.5,
246
- strokeLinecap: 'round',
247
- strokeLinejoin: 'round',
248
- })
249
- )
250
- }
251
-
252
- function SettingsView(props) {
253
- const t = props.locale === 'ru' ? makeT(ru, en) : makeT(en, ru)
254
- const [profiles, setProfiles] = React.useState([])
255
- const [activeId, setActiveId] = React.useState(null)
256
- const [tunnels, setTunnels] = React.useState([])
257
- const [editing, setEditing] = React.useState(null)
258
- const [busy, setBusy] = React.useState(null)
259
- const [testResult, setTestResult] = React.useState(null)
260
- const [syncMsg, setSyncMsg] = React.useState(null)
261
- const [tunnelMsg, setTunnelMsg] = React.useState(null)
262
- const [showPassword, setShowPassword] = React.useState(false)
263
-
264
- // Tunnel creation inputs
265
- const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
266
- const [newTunnelRemote, setNewTunnelRemote] = React.useState('3000')
267
-
268
- // Remote Directory Browser State
269
- const [browserOpen, setBrowserOpen] = React.useState(false)
270
- const [browserPath, setBrowserPath] = React.useState('/')
271
- const [browserEntries, setBrowserEntries] = React.useState([])
272
- const [browserLoading, setBrowserLoading] = React.useState(false)
273
-
274
- const loadState = async () => {
275
- try {
276
- const res = await fetch('/dsh-remote-workspace/state')
277
- if (res.ok) {
278
- const data = await res.json()
279
- if (data.ok) {
280
- setProfiles(data.profiles || [])
281
- setActiveId(data.activeId || null)
282
- setTunnels(data.tunnels || [])
283
- }
284
- }
285
- } catch (_) {}
286
- }
287
-
288
- React.useEffect(() => {
289
- ensureCss()
290
- loadState()
291
- }, [])
292
-
293
- const handleSave = async (profile) => {
294
- setBusy('saving')
295
- try {
296
- const res = await fetch('/dsh-remote-workspace/profiles/save', {
297
- method: 'POST',
298
- headers: { 'Content-Type': 'application/json' },
299
- body: JSON.stringify(profile)
300
- })
301
- if (res.ok) {
302
- setEditing(null)
303
- setBrowserOpen(false)
304
- setTestResult(null)
305
- await loadState()
306
- }
307
- } catch (_) {}
308
- setBusy(null)
309
- }
310
-
311
- const handleDelete = async (id, name) => {
312
- if (!confirm(t('delConfirm', { name: name || id }))) return
313
- try {
314
- const res = await fetch('/dsh-remote-workspace/profiles/delete', {
315
- method: 'POST',
316
- headers: { 'Content-Type': 'application/json' },
317
- body: JSON.stringify({ id })
318
- })
319
- if (res.ok) {
320
- if (editing && editing.id === id) setEditing(null)
321
- await loadState()
322
- }
323
- } catch (_) {}
324
- }
325
-
326
- const handleSetActive = async (id) => {
327
- try {
328
- const res = await fetch('/dsh-remote-workspace/profiles/active', {
329
- method: 'POST',
330
- headers: { 'Content-Type': 'application/json' },
331
- body: JSON.stringify({ id })
332
- })
333
- if (res.ok) {
334
- await loadState()
335
- }
336
- } catch (_) {}
337
- }
338
-
339
- const handleTest = async (prof) => {
340
- setBusy('testing')
341
- setTestResult(null)
342
- try {
343
- const res = await fetch('/dsh-remote-workspace/test', {
344
- method: 'POST',
345
- headers: { 'Content-Type': 'application/json' },
346
- body: JSON.stringify({ profile: prof })
347
- })
348
- const data = await res.json()
349
- setTestResult(data)
350
- } catch (err) {
351
- setTestResult({ ok: false, error: err.message })
352
- }
353
- setBusy(null)
354
- }
355
-
356
- const openDirectoryBrowser = async (initialPath) => {
357
- setBrowserOpen(true)
358
- const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
359
- await fetchRemoteDir(target)
360
- }
361
-
362
- const fetchRemoteDir = async (dirPath) => {
363
- setBrowserLoading(true)
364
- try {
365
- const res = await fetch('/dsh-remote-workspace/browse', {
366
- method: 'POST',
367
- headers: { 'Content-Type': 'application/json' },
368
- body: JSON.stringify({ profile: editing, remotePath: dirPath })
369
- })
370
- const data = await res.json()
371
- if (data.ok) {
372
- setBrowserPath(data.currentPath || dirPath)
373
- setBrowserEntries(data.entries || [])
374
- }
375
- } catch (_) {}
376
- setBrowserLoading(false)
377
- }
378
-
379
- const handleSync = async (direction) => {
380
- setBusy(`sync_${direction}`)
381
- setSyncMsg(null)
382
- try {
383
- const res = await fetch('/dsh-remote-workspace/sync', {
384
- method: 'POST',
385
- headers: { 'Content-Type': 'application/json' },
386
- body: JSON.stringify({ direction })
387
- })
388
- const data = await res.json()
389
- if (data.ok) {
390
- const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
391
- if (data.conflicts && data.conflicts.length > 0) {
392
- setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
393
- } else {
394
- setSyncMsg({ ok: true, text: t('syncOk', { count }) })
395
- }
396
- } else {
397
- setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
398
- }
399
- } catch (err) {
400
- setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
401
- }
402
- setBusy(null)
403
- }
404
-
405
- const handleStartTunnel = async () => {
406
- if (!activeId) return
407
- setBusy('tunnel_open')
408
- setTunnelMsg(null)
409
- try {
410
- const res = await fetch('/dsh-remote-workspace/tunnels/start', {
411
- method: 'POST',
412
- headers: { 'Content-Type': 'application/json' },
413
- body: JSON.stringify({
414
- profileId: activeId,
415
- localPort: newTunnelLocal,
416
- remotePort: newTunnelRemote
417
- })
418
- })
419
- const data = await res.json()
420
- if (data.ok) {
421
- setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
422
- await loadState()
423
- } else {
424
- setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
425
- }
426
- } catch (err) {
427
- setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
428
- }
429
- setBusy(null)
430
- }
431
-
432
- const handleStopTunnel = async (tunnelId) => {
433
- try {
434
- const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
435
- method: 'POST',
436
- headers: { 'Content-Type': 'application/json' },
437
- body: JSON.stringify({ tunnelId })
438
- })
439
- if (res.ok) {
440
- await loadState()
441
- }
442
- } catch (_) {}
443
- }
444
-
445
- const activeProfile = profiles.find((p) => p.id === activeId)
446
-
447
- return React.createElement(
448
- 'div',
449
- { className: 'drw-page' },
450
-
451
- // Header with Live Badges (ClineBot Style)
452
- React.createElement(
453
- 'div',
454
- { className: 'drw-header' },
455
- React.createElement(
456
- 'div',
457
- { className: 'drw-header-top' },
458
- React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
459
- React.createElement(
460
- 'div',
461
- { className: 'drw-header-badges' },
462
- activeProfile
463
- ? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
464
- : React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
465
- activeProfile
466
- ? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
467
- : null,
468
- React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
469
- )
470
- ),
471
- React.createElement('div', { className: 'drw-page-sub' }, t('subtitle'))
472
- ),
473
-
474
- // Section 1: SSH Profiles Table & Management
475
- React.createElement(
476
- 'div',
477
- { className: 'drw-card' },
478
- React.createElement(
479
- 'div',
480
- { className: 'drw-card-title' },
481
- React.createElement('span', null, t('profilesTitle')),
482
- !editing
483
- ? React.createElement(
484
- 'button',
485
- {
486
- type: 'button',
487
- className: 'drw-btn drw-btn-primary',
488
- onClick: () => {
489
- setEditing({
490
- id: 'prof_' + Math.random().toString(36).substr(2, 9),
491
- name: 'New Server',
492
- host: '',
493
- port: 22,
494
- username: 'root',
495
- authType: 'key',
496
- privateKeyPath: '~/.ssh/id_rsa',
497
- passphrase: '',
498
- password: '',
499
- remoteWorkspace: '',
500
- localMirrorPath: ''
501
- })
502
- setTestResult(null)
503
- }
504
- },
505
- t('addBtn')
506
- )
507
- : null
508
- ),
509
- React.createElement('div', { className: 'drw-card-desc' }, t('profilesDesc')),
510
-
511
- // Interactive Editing Form
512
- editing
513
- ? React.createElement(
514
- 'div',
515
- {
516
- style: {
517
- padding: '16px',
518
- borderRadius: '10px',
519
- background: 'var(--dsw-alias-bg-layer-2)',
520
- border: '1px solid var(--dsw-alias-border-l2)',
521
- display: 'flex',
522
- flexDirection: 'column',
523
- gap: '14px'
524
- }
525
- },
526
- React.createElement(
527
- 'div',
528
- { style: { fontWeight: 600, fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } },
529
- editing.name ? t('formTitleEdit') : t('formTitleNew')
530
- ),
531
- React.createElement(
532
- 'div',
533
- { className: 'drw-grid-2' },
534
- React.createElement(
535
- 'div',
536
- { className: 'drw-field' },
537
- React.createElement('span', { className: 'drw-label' }, t('fName')),
538
- React.createElement('input', {
539
- className: 'drw-input',
540
- value: editing.name || '',
541
- onChange: (e) => setEditing({ ...editing, name: e.target.value })
542
- })
543
- ),
544
- React.createElement(
545
- 'div',
546
- { className: 'drw-field' },
547
- React.createElement('span', { className: 'drw-label' }, t('fHost')),
548
- React.createElement('input', {
549
- className: 'drw-input',
550
- placeholder: '192.168.1.100 or server.domain.com',
551
- value: editing.host || '',
552
- onChange: (e) => setEditing({ ...editing, host: e.target.value })
553
- })
554
- ),
555
- React.createElement(
556
- 'div',
557
- { className: 'drw-field' },
558
- React.createElement('span', { className: 'drw-label' }, t('fPort')),
559
- React.createElement('input', {
560
- className: 'drw-input',
561
- type: 'number',
562
- value: editing.port || 22,
563
- onChange: (e) => setEditing({ ...editing, port: parseInt(e.target.value, 10) || 22 })
564
- })
565
- ),
566
- React.createElement(
567
- 'div',
568
- { className: 'drw-field' },
569
- React.createElement('span', { className: 'drw-label' }, t('fUser')),
570
- React.createElement('input', {
571
- className: 'drw-input',
572
- value: editing.username || '',
573
- onChange: (e) => setEditing({ ...editing, username: e.target.value })
574
- })
575
- )
576
- ),
577
-
578
- // Auth Method Segmented Switcher (Key vs Password)
579
- React.createElement(
580
- 'div',
581
- { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
582
- React.createElement('span', { className: 'drw-label' }, t('fAuthType')),
583
- React.createElement(
584
- 'div',
585
- { className: 'drw-segmented' },
586
- React.createElement(
587
- 'button',
588
- {
589
- type: 'button',
590
- className: `drw-segmented-item ${editing.authType !== 'password' ? 'drw-segmented-item-active' : ''}`,
591
- onClick: () => setEditing({ ...editing, authType: 'key' })
592
- },
593
- '🔑 ' + t('fAuthKey')
594
- ),
595
- React.createElement(
596
- 'button',
597
- {
598
- type: 'button',
599
- className: `drw-segmented-item ${editing.authType === 'password' ? 'drw-segmented-item-active' : ''}`,
600
- onClick: () => setEditing({ ...editing, authType: 'password' })
601
- },
602
- '🔒 ' + t('fAuthPass')
603
- )
604
- )
605
- ),
606
-
607
- // Conditional Auth Inputs
608
- editing.authType === 'password'
609
- ? React.createElement(
610
- 'div',
611
- { className: 'drw-field' },
612
- React.createElement('span', { className: 'drw-label' }, t('fPassword')),
613
- React.createElement(
614
- 'div',
615
- { style: { display: 'flex', gap: '8px' } },
616
- React.createElement('input', {
617
- className: 'drw-input',
618
- type: showPassword ? 'text' : 'password',
619
- placeholder: '••••••••••••',
620
- value: editing.password || '',
621
- onChange: (e) => setEditing({ ...editing, password: e.target.value })
622
- }),
623
- React.createElement(
624
- 'button',
625
- {
626
- type: 'button',
627
- className: 'drw-btn',
628
- style: { fontSize: '11px', whiteSpace: 'nowrap' },
629
- onClick: () => setShowPassword((v) => !v)
630
- },
631
- showPassword ? 'Hide' : 'Show'
632
- )
633
- )
634
- )
635
- : React.createElement(
636
- 'div',
637
- { className: 'drw-grid-2' },
638
- React.createElement(
639
- 'div',
640
- { className: 'drw-field' },
641
- React.createElement('span', { className: 'drw-label' }, t('fKeyPath')),
642
- React.createElement('input', {
643
- className: 'drw-input',
644
- placeholder: '~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
645
- value: editing.privateKeyPath || '',
646
- onChange: (e) => setEditing({ ...editing, privateKeyPath: e.target.value })
647
- }),
648
- React.createElement('span', { className: 'drw-hint' }, t('fKeyPathHint'))
649
- ),
650
- React.createElement(
651
- 'div',
652
- { className: 'drw-field' },
653
- React.createElement('span', { className: 'drw-label' }, t('fPassphrase')),
654
- React.createElement('input', {
655
- className: 'drw-input',
656
- type: 'password',
657
- placeholder: 'Optional passphrase',
658
- value: editing.passphrase || '',
659
- onChange: (e) => setEditing({ ...editing, passphrase: e.target.value })
660
- })
661
- )
662
- ),
663
-
664
- // Workspace Paths (Remote Workspace & Local Mirror)
665
- React.createElement(
666
- 'div',
667
- { className: 'drw-grid-2' },
668
- React.createElement(
669
- 'div',
670
- { className: 'drw-field' },
671
- React.createElement('span', { className: 'drw-label' }, t('fRemotePath')),
672
- React.createElement(
673
- 'div',
674
- { style: { display: 'flex', gap: '8px' } },
675
- React.createElement('input', {
676
- className: 'drw-input',
677
- placeholder: '/home/user/project',
678
- value: editing.remoteWorkspace || '',
679
- onChange: (e) => setEditing({ ...editing, remoteWorkspace: e.target.value })
680
- }),
681
- React.createElement(
682
- 'button',
683
- {
684
- type: 'button',
685
- className: 'drw-btn',
686
- style: { whiteSpace: 'nowrap', fontSize: '12px' },
687
- disabled: !editing.host,
688
- onClick: () => openDirectoryBrowser(editing.remoteWorkspace || '/')
689
- },
690
- t('btnBrowse')
691
- )
692
- ),
693
- React.createElement('span', { className: 'drw-hint' }, t('fRemotePathHint'))
694
- ),
695
- React.createElement(
696
- 'div',
697
- { className: 'drw-field' },
698
- React.createElement('span', { className: 'drw-label' }, t('fLocalMirror')),
699
- React.createElement('input', {
700
- className: 'drw-input',
701
- placeholder: '~/.dsh/mirrors/my-project',
702
- value: editing.localMirrorPath || '',
703
- onChange: (e) => setEditing({ ...editing, localMirrorPath: e.target.value })
704
- }),
705
- React.createElement('span', { className: 'drw-hint' }, t('fLocalMirrorHint'))
706
- )
707
- ),
708
-
709
- // Directory Browser Modal
710
- browserOpen
711
- ? React.createElement(
712
- 'div',
713
- { className: 'drw-browser-modal' },
714
- React.createElement(
715
- 'div',
716
- { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
717
- React.createElement('span', { style: { fontSize: '12px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, `📂 ${browserPath}`),
718
- React.createElement(
719
- 'div',
720
- { style: { display: 'flex', gap: '6px' } },
721
- React.createElement(
722
- 'button',
723
- {
724
- type: 'button',
725
- className: 'drw-btn drw-btn-primary',
726
- style: { fontSize: '11px', padding: '3px 8px' },
727
- onClick: () => {
728
- setEditing({ ...editing, remoteWorkspace: browserPath })
729
- setBrowserOpen(false)
730
- }
731
- },
732
- t('btnSelectThis')
733
- ),
734
- React.createElement(
735
- 'button',
736
- {
737
- type: 'button',
738
- className: 'drw-btn',
739
- style: { fontSize: '11px', padding: '3px 8px' },
740
- onClick: () => setBrowserOpen(false)
741
- },
742
- '✕'
743
- )
744
- )
745
- ),
746
- React.createElement(
747
- 'div',
748
- { className: 'drw-browser-list' },
749
- browserLoading
750
- ? React.createElement('div', { style: { padding: '8px', fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, 'Loading directory entries...')
751
- : React.createElement(
752
- React.Fragment,
753
- null,
754
- browserPath !== '/'
755
- ? React.createElement(
756
- 'div',
757
- {
758
- className: 'drw-browser-item',
759
- onClick: () => {
760
- const parent = browserPath.substring(0, browserPath.lastIndexOf('/')) || '/'
761
- fetchRemoteDir(parent)
762
- }
763
- },
764
- '📁 .. (parent directory)'
765
- )
766
- : null,
767
- browserEntries
768
- .filter((e) => e.isDirectory)
769
- .map((e) =>
770
- React.createElement(
771
- 'div',
772
- {
773
- key: e.filename,
774
- className: 'drw-browser-item',
775
- onClick: () => {
776
- const next = browserPath === '/' ? `/${e.filename}` : `${browserPath}/${e.filename}`
777
- fetchRemoteDir(next)
778
- }
779
- },
780
- `📁 ${e.filename}`
781
- )
782
- )
783
- )
784
- )
785
- )
786
- : null,
787
-
788
- // Form Action Buttons
789
- React.createElement(
790
- 'div',
791
- { className: 'drw-row', style: { justifyContent: 'flex-end', marginTop: '6px' } },
792
- React.createElement(
793
- 'button',
794
- {
795
- type: 'button',
796
- className: 'drw-btn',
797
- disabled: !editing.host || !!busy,
798
- onClick: () => handleTest(editing)
799
- },
800
- busy === 'testing' ? t('testing') : t('btnTest')
801
- ),
802
- React.createElement(
803
- 'button',
804
- {
805
- type: 'button',
806
- className: 'drw-btn',
807
- onClick: () => {
808
- setEditing(null)
809
- setBrowserOpen(false)
810
- setTestResult(null)
811
- }
812
- },
813
- t('btnCancel')
814
- ),
815
- React.createElement(
816
- 'button',
817
- {
818
- type: 'button',
819
- className: 'drw-btn drw-btn-primary',
820
- disabled: !editing.host || !!busy,
821
- onClick: () => handleSave(editing)
822
- },
823
- busy === 'saving' ? t('saving') : t('btnSave')
824
- )
825
- ),
826
- testResult
827
- ? React.createElement(
828
- 'div',
829
- {
830
- className: 'drw-preview',
831
- style: {
832
- borderColor: testResult.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
833
- }
834
- },
835
- testResult.ok
836
- ? t('testOk', { latency: testResult.latencyMs ?? testResult.latency ?? 0, os: testResult.remoteOs || testResult.os || 'Linux' })
837
- : t('testFail', { err: testResult.error || 'Connection timed out' })
838
- )
839
- : null
840
- )
841
- : null,
842
-
843
- // Profiles List Table
844
- profiles.length > 0
845
- ? React.createElement(
846
- 'table',
847
- { className: 'drw-table' },
848
- React.createElement(
849
- 'thead',
850
- null,
851
- React.createElement(
852
- 'tr',
853
- null,
854
- React.createElement('th', null, t('nameCol')),
855
- React.createElement('th', null, t('hostCol')),
856
- React.createElement('th', null, t('portCol')),
857
- React.createElement('th', null, t('pathCol')),
858
- React.createElement('th', { style: { textAlign: 'right' } }, t('actionsCol'))
859
- )
860
- ),
861
- React.createElement(
862
- 'tbody',
863
- null,
864
- profiles.map((p) => {
865
- const isAct = p.id === activeId
866
- return React.createElement(
867
- 'tr',
868
- { key: p.id },
869
- React.createElement(
870
- 'td',
871
- null,
872
- React.createElement('span', { style: { fontWeight: 600 } }, p.name || 'Server'),
873
- React.createElement(
874
- 'span',
875
- { className: 'drw-badge', style: { marginLeft: '8px' } },
876
- p.authType === 'password' ? '🔒 pwd' : '🔑 key'
877
- ),
878
- isAct
879
- ? React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginLeft: '6px' } }, t('activeBadge'))
880
- : null
881
- ),
882
- React.createElement('td', { style: { fontFamily: 'monospace' } }, `${p.username || 'root'}@${p.host}`),
883
- React.createElement('td', { style: { fontFamily: 'monospace' } }, p.port || 22),
884
- React.createElement('td', { style: { fontFamily: 'monospace', fontSize: '12px' } }, p.remoteWorkspace || '—'),
885
- React.createElement(
886
- 'td',
887
- { style: { textAlign: 'right' } },
888
- React.createElement(
889
- 'div',
890
- { style: { display: 'inline-flex', gap: '6px' } },
891
- !isAct
892
- ? React.createElement(
893
- 'button',
894
- {
895
- type: 'button',
896
- className: 'drw-btn',
897
- style: { fontSize: '11px', padding: '3px 8px' },
898
- onClick: () => handleSetActive(p.id)
899
- },
900
- t('pinBtn')
901
- )
902
- : null,
903
- React.createElement(
904
- 'button',
905
- {
906
- type: 'button',
907
- className: 'drw-btn',
908
- style: { fontSize: '11px', padding: '3px 8px' },
909
- onClick: () => {
910
- setEditing({ ...p })
911
- setTestResult(null)
912
- }
913
- },
914
- t('editBtn')
915
- ),
916
- React.createElement(
917
- 'button',
918
- {
919
- type: 'button',
920
- className: 'drw-btn drw-btn-danger',
921
- style: { fontSize: '11px', padding: '3px 8px' },
922
- onClick: () => handleDelete(p.id, p.name)
923
- },
924
- t('delBtn')
925
- )
926
- )
927
- )
928
- )
929
- })
930
- )
931
- )
932
- : !editing
933
- ? React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('noHosts'))
934
- : null
935
- ),
936
-
937
- // Section 2: Active Connection Diagnostics (ClineBot Card 4 style)
938
- activeProfile
939
- ? React.createElement(
940
- 'div',
941
- { className: 'drw-card' },
942
- React.createElement(
943
- 'div',
944
- { className: 'drw-card-title' },
945
- React.createElement('span', null, t('diagTitle')),
946
- React.createElement(
947
- 'button',
948
- {
949
- type: 'button',
950
- className: 'drw-btn',
951
- style: { fontSize: '11px', padding: '3px 9px' },
952
- disabled: busy === 'testing',
953
- onClick: () => handleTest(activeProfile)
954
- },
955
- busy === 'testing' ? t('testing') : t('diagProbeBtn')
956
- )
957
- ),
958
- React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
959
- React.createElement(
960
- 'div',
961
- { className: 'drw-grid-4' },
962
- React.createElement(
963
- 'div',
964
- { className: 'drw-stat-box' },
965
- React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
966
- React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
967
- ),
968
- React.createElement(
969
- 'div',
970
- { className: 'drw-stat-box' },
971
- React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
972
- React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
973
- ),
974
- React.createElement(
975
- 'div',
976
- { className: 'drw-stat-box' },
977
- React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
978
- React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
979
- ),
980
- React.createElement(
981
- 'div',
982
- { className: 'drw-stat-box' },
983
- React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
984
- React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
985
- )
986
- )
987
- )
988
- : null,
989
-
990
- // Section 3: Mirror Sync (ClineBot Action Card style)
991
- activeProfile
992
- ? React.createElement(
993
- 'div',
994
- { className: 'drw-card' },
995
- React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
996
- React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
997
- React.createElement(
998
- 'div',
999
- { className: 'drw-row' },
1000
- React.createElement(
1001
- 'button',
1002
- {
1003
- type: 'button',
1004
- className: 'drw-btn',
1005
- disabled: !!busy || !activeProfile.remoteWorkspace || !activeProfile.localMirrorPath,
1006
- onClick: () => handleSync('pull')
1007
- },
1008
- busy === 'sync_pull' ? t('pulling') : t('pullBtn')
1009
- ),
1010
- React.createElement(
1011
- 'button',
1012
- {
1013
- type: 'button',
1014
- className: 'drw-btn',
1015
- disabled: !!busy || !activeProfile.remoteWorkspace || !activeProfile.localMirrorPath,
1016
- onClick: () => handleSync('push')
1017
- },
1018
- busy === 'sync_push' ? t('pushing') : t('pushBtn')
1019
- )
1020
- ),
1021
- syncMsg
1022
- ? React.createElement(
1023
- 'div',
1024
- {
1025
- className: 'drw-preview',
1026
- style: {
1027
- borderColor: syncMsg.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
1028
- }
1029
- },
1030
- syncMsg.text
1031
- )
1032
- : null
1033
- )
1034
- : null,
1035
-
1036
- // Section 4: SSH Port Forwarding (Tunnels Manager)
1037
- React.createElement(
1038
- 'div',
1039
- { className: 'drw-card' },
1040
- React.createElement('div', { className: 'drw-card-title' }, t('tunnelsTitle')),
1041
- React.createElement('div', { className: 'drw-card-desc' }, t('tunnelsDesc')),
1042
-
1043
- // Start tunnel inline row
1044
- activeProfile
1045
- ? React.createElement(
1046
- 'div',
1047
- { className: 'drw-row', style: { alignItems: 'flex-end', background: 'var(--dsw-alias-bg-layer-2)', padding: '10px 12px', borderRadius: '8px' } },
1048
- React.createElement(
1049
- 'div',
1050
- { style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
1051
- React.createElement('span', { className: 'drw-label' }, t('tLocalPort')),
1052
- React.createElement('input', {
1053
- className: 'drw-input',
1054
- style: { width: '120px' },
1055
- type: 'number',
1056
- value: newTunnelLocal,
1057
- onChange: (e) => setNewTunnelLocal(e.target.value)
1058
- })
1059
- ),
1060
- React.createElement('span', { style: { paddingBottom: '8px', color: 'var(--dsw-alias-label-secondary)' } }, '→'),
1061
- React.createElement(
1062
- 'div',
1063
- { style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
1064
- React.createElement('span', { className: 'drw-label' }, t('tRemotePort')),
1065
- React.createElement('input', {
1066
- className: 'drw-input',
1067
- style: { width: '120px' },
1068
- type: 'number',
1069
- value: newTunnelRemote,
1070
- onChange: (e) => setNewTunnelRemote(e.target.value)
1071
- })
1072
- ),
1073
- React.createElement(
1074
- 'button',
1075
- {
1076
- type: 'button',
1077
- className: 'drw-btn drw-btn-primary',
1078
- disabled: busy === 'tunnel_open',
1079
- onClick: handleStartTunnel
1080
- },
1081
- busy === 'tunnel_open' ? t('tOpening') : t('tOpenBtn')
1082
- )
1083
- )
1084
- : null,
1085
-
1086
- tunnelMsg
1087
- ? React.createElement(
1088
- 'div',
1089
- {
1090
- className: 'drw-preview',
1091
- style: {
1092
- borderColor: tunnelMsg.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
1093
- }
1094
- },
1095
- tunnelMsg.text
1096
- )
1097
- : null,
1098
-
1099
- // Tunnels Table
1100
- tunnels.length > 0
1101
- ? React.createElement(
1102
- 'table',
1103
- { className: 'drw-table' },
1104
- React.createElement(
1105
- 'thead',
1106
- null,
1107
- React.createElement(
1108
- 'tr',
1109
- null,
1110
- React.createElement('th', null, t('tunnelsColId')),
1111
- React.createElement('th', null, t('tunnelsColRoute')),
1112
- React.createElement('th', { style: { textAlign: 'right' } }, t('tunnelsColActions'))
1113
- )
1114
- ),
1115
- React.createElement(
1116
- 'tbody',
1117
- null,
1118
- tunnels.map((tun) =>
1119
- React.createElement(
1120
- 'tr',
1121
- { key: tun.id },
1122
- React.createElement('td', { style: { fontFamily: 'monospace', fontSize: '12px' } }, tun.id),
1123
- React.createElement(
1124
- 'td',
1125
- null,
1126
- React.createElement('span', { className: 'drw-badge drw-badge-ok' }, `127.0.0.1:${tun.localPort}`),
1127
- ' → ',
1128
- React.createElement('span', { className: 'drw-badge' }, `${tun.targetHost || '127.0.0.1'}:${tun.remotePort}`)
1129
- ),
1130
- React.createElement(
1131
- 'td',
1132
- { style: { textAlign: 'right' } },
1133
- React.createElement(
1134
- 'button',
1135
- {
1136
- type: 'button',
1137
- className: 'drw-btn drw-btn-danger',
1138
- style: { fontSize: '11px', padding: '3px 8px' },
1139
- onClick: () => handleStopTunnel(tun.id)
1140
- },
1141
- t('tStopBtn')
1142
- )
1143
- )
1144
- )
1145
- )
1146
- )
1147
- )
1148
- : React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('tNoTunnels'))
1149
- )
1150
- )
1151
- }
1152
-
1153
- // Accordion item for Settings -> Plugins section
1154
- function PluginCard(props) {
1155
- const [open, setOpen] = React.useState(false)
1156
- const t = props.locale === 'ru' ? makeT(ru, en) : makeT(en, ru)
1157
-
1158
- React.useEffect(() => {
1159
- ensureCss()
1160
- }, [])
1161
-
1162
- return React.createElement(
1163
- 'li',
1164
- { className: 'drw-card', style: { listStyle: 'none', marginBottom: '12px' } },
1165
- React.createElement(
1166
- 'button',
1167
- {
1168
- type: 'button',
1169
- style: {
1170
- background: 'none',
1171
- border: 'none',
1172
- cursor: 'pointer',
1173
- display: 'flex',
1174
- alignItems: 'center',
1175
- width: '100%',
1176
- padding: 0,
1177
- textAlign: 'left',
1178
- },
1179
- 'aria-expanded': open,
1180
- onClick: () => setOpen((v) => !v),
1181
- },
1182
- React.createElement('span', { style: { fontSize: '20px', marginRight: '12px' } }, '🌐'),
1183
- React.createElement(
1184
- 'div',
1185
- { style: { flex: 1 } },
1186
- React.createElement('div', { style: { fontWeight: 600, fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, t('title')),
1187
- React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
1188
- ),
1189
- React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginRight: '14px' } }, t('status')),
1190
- React.createElement(
1191
- 'span',
1192
- { style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
1193
- React.createElement(FallbackChevron)
1194
- )
1195
- ),
1196
- open
1197
- ? React.createElement(
1198
- 'div',
1199
- { style: { marginTop: '16px', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '16px' } },
1200
- React.createElement(SettingsView, props)
1201
- )
1202
- : null
1203
- )
1204
- }
1205
-
1206
- function RemoteWorkspaceChip(props) {
1207
- ensureCss()
1208
- const [activeHost, setActiveHost] = React.useState(null)
1209
-
1210
- React.useEffect(() => {
1211
- fetch('/dsh-remote-workspace/state')
1212
- .then((r) => r.json())
1213
- .then((d) => {
1214
- if (d.ok && d.activeId && d.profiles) {
1215
- const f = d.profiles.find((p) => p.id === d.activeId)
1216
- if (f) setActiveHost(f.name || f.host)
1217
- }
1218
- })
1219
- .catch(() => {})
1220
- }, [])
1221
-
1222
- return React.createElement(
1223
- 'div',
1224
- {
1225
- style: {
1226
- display: 'flex',
1227
- alignItems: 'center',
1228
- gap: '6px',
1229
- padding: '2px 8px',
1230
- fontSize: '11px',
1231
- fontFamily: 'monospace',
1232
- borderRadius: '999px',
1233
- border: '1px solid var(--dsw-alias-border-l2)',
1234
- background: 'var(--dsw-alias-bg-layer-1)',
1235
- color: activeHost ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
1236
- cursor: 'pointer'
1237
- }
1238
- },
1239
- React.createElement('span', null, '🌐'),
1240
- React.createElement('span', null, activeHost ? `SSH: ${activeHost}` : 'SSH: Local')
1241
- )
1242
- }
1243
-
1244
- function apply(ctx) {
1245
- if (ctx.locale && typeof ctx.locale.register === 'function') {
1246
- try { ctx.locale.register(NS, { en, ru }) } catch (_) {}
1247
- }
1248
-
1249
- if (ctx.slots && typeof ctx.slots.inject === 'function') {
1250
- ctx.slots.inject('settings.plugin.item', () =>
1251
- ctx.slots.register(
1252
- {
1253
- name: 'settings.plugin.item',
1254
- key: NS,
1255
- locale: NS,
1256
- inject: () => ({ ctx })
1257
- },
1258
- (props) => React.createElement(PluginCard, Object.assign({}, props, { ctx }))
1259
- )
1260
- )
1261
-
1262
- ctx.slots.inject('conversation.session.header.utilities', () =>
1263
- ctx.slots.register(
1264
- {
1265
- name: 'conversation.session.header.utilities',
1266
- id: '@goodandready/dsh-remote-workspace',
1267
- order: 30
1268
- },
1269
- (props) => React.createElement(RemoteWorkspaceChip, props)
1270
- )
1271
- )
1272
- }
1273
- }
1274
-
1275
- module.exports = { apply, inject: ['slots', 'locale'] }
1276
- return module.exports
1277
- }
1278
- })
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
+ `
103
+ document.head.appendChild(style)
104
+ }
105
+
106
+ const en = {
107
+ title: 'Remote Workspace',
108
+ subtitle: 'Native SSH connections, SFTP file access, 3-way mirror sync, and port forwarding tunnels.',
109
+ status: 'Ready',
110
+
111
+ tabProfiles: '🖥️ Host Profiles',
112
+
113
+ tabContainers: '🐳 Containers',
114
+ containersTitle: 'Remote Docker / Podman Engine',
115
+ containerName: 'Container',
116
+ containerImage: 'Image',
117
+ containerStatus: 'Status',
118
+ containerPorts: 'Ports',
119
+ btnLogs: 'Logs',
120
+ btnRestart: 'Restart',
121
+ btnStop: 'Stop',
122
+ noContainers: 'No active containers found or Docker daemon not running.',
123
+ healthCpu: 'CPU: {val}%',
124
+ healthMem: 'RAM: {val}% ({used}/{total} MB)',
125
+ healthDisk: 'Disk: {val}%',
126
+ healthUptime: 'Up: {val}',
127
+ autoSyncTitle: 'Live Auto-Sync on Save',
128
+ autoSyncDesc: 'Automatically push modified files to remote workspace on local save.',
129
+ btnTransfer: 'Copy to Host...',
130
+ transferTitle: 'Server-to-Server Direct Transfer',
131
+ transferDestHost: 'Destination Server',
132
+ transferDestPath: 'Destination File Path',
133
+ transferBtnExec: 'Transfer Directly',
134
+ transferSuccess: 'Successfully transferred {bytes} bytes to {host}:{path}',
135
+ fJumpHost: 'Jump Host (Bastion)',
136
+ fJumpHostNone: 'Direct Connection (No Jump Host)',
137
+
138
+ tabTerminal: '⚡ Web Terminal',
139
+ tabExplorer: '📂 File Explorer',
140
+ tabTunnels: '🔗 Port Tunnels',
141
+ tabSync: '🔄 Sync Station',
142
+ vaultProtectedBadge: '🛡️ .env Vault Protected',
143
+ termStart: 'Connect Terminal',
144
+ termStop: 'Disconnect',
145
+ termClear: 'Clear',
146
+ termPlaceholder: 'Type command and press Enter (e.g. htop, ls, docker ps)...',
147
+ termConnected: 'Connected to PTY session {id} on {host}',
148
+ termDisconnected: 'Terminal disconnected.',
149
+ explorerTitle: 'Remote Filesystem Explorer',
150
+ filePreviewTitle: 'File Viewer: {path}',
151
+ fileSize: '{size} KB',
152
+ btnSaveFile: 'Save Changes',
153
+ fileSaved: 'File saved successfully.',
154
+ tunnelTraffic: 'RX: {rx} | TX: {tx}',
155
+ tunnelUptime: 'Up: {sec}s',
156
+ openLocalLink: '🌐 Open Local Port',
157
+ badgeOnline: 'Active Host: {name} ({latency} ms)',
158
+ badgeOffline: 'No active host',
159
+ badgeTunnels: 'Tunnels: {count}',
160
+ badgeKey: '🔑 Key Auth',
161
+ badgePassword: '🔒 Password Auth',
162
+ profilesTitle: '🖥️ Remote SSH Profiles',
163
+ profilesDesc: 'Configure remote machines for code execution, SFTP browsing, and background mirror sync.',
164
+ addBtn: '+ Add Host',
165
+ nameCol: 'Name',
166
+ hostCol: 'Host / User',
167
+ portCol: 'Port',
168
+ pathCol: 'Remote Workspace',
169
+ actionsCol: 'Actions',
170
+ noHosts: 'No remote hosts configured yet. Click "+ Add Host" to create your first connection.',
171
+ activeBadge: 'Active',
172
+ pinBtn: 'Pin Active',
173
+ editBtn: 'Edit',
174
+ delBtn: 'Delete',
175
+ delConfirm: 'Delete host profile "{name}"?',
176
+ formTitleNew: 'Add New Host Profile',
177
+ formTitleEdit: 'Edit Host Profile',
178
+ fName: 'Profile Name',
179
+ fHost: 'Host / IP Address',
180
+ fPort: 'Port',
181
+ fUser: 'Username',
182
+ fAuthType: 'Authentication Method',
183
+ fAuthKey: 'SSH Private Key',
184
+ fAuthPass: 'Password',
185
+ fKeyPath: 'Private Key Path',
186
+ fKeyPathHint: 'Path to ~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
187
+ fPassphrase: 'Key Passphrase (optional)',
188
+ fPassword: 'Account Password',
189
+ fRemotePath: 'Remote Workspace Directory',
190
+ fRemotePathHint: 'Absolute path on the server (e.g. /home/user/my-project)',
191
+ fLocalMirror: 'Local Mirror Directory',
192
+ fLocalMirrorHint: 'Local directory where files are mirrored for fast 3-way synchronization',
193
+ btnBrowse: '📁 Browse Remote...',
194
+ btnSelectThis: 'Select Directory',
195
+ btnTest: '⚡ Test Connection (Ping)',
196
+ btnSave: 'Save Profile',
197
+ btnCancel: 'Cancel',
198
+ testing: 'Testing Connection...',
199
+ saving: 'Saving...',
200
+ testOk: '✅ Connection OK! Latency: {latency} ms | OS: {os}',
201
+ testFail: '❌ Connection Failed: {err}',
202
+ diagTitle: '📊 Active Connection Diagnostics',
203
+ diagDesc: 'Real-time telemetry and engine status for the currently selected remote machine.',
204
+ diagProbeBtn: '⚡ Quick Probe',
205
+ syncTitle: '🔄 Two-Way Workspace Mirror Sync',
206
+ syncDesc: 'Fast 3-way conflict-aware file synchronization between local mirror and remote server.',
207
+ pullBtn: '⬇️ Pull from Remote',
208
+ pushBtn: '⬆️ Push to Remote',
209
+ pulling: 'Pulling files…',
210
+ pushing: 'Pushing files…',
211
+ syncOk: 'Sync completed! Updated {count} file(s).',
212
+ syncConflict: 'Conflict warning: {count} file(s) modified on both sides.',
213
+ syncErr: 'Sync error: {err}',
214
+ tunnelsTitle: '🔀 SSH Port Forwarding (Tunnels)',
215
+ tunnelsDesc: 'Forward remote ports (e.g. databases, web servers, APIs) to your local machine via encrypted SSH tunnels.',
216
+ tLocalPort: 'Local Port',
217
+ tRemotePort: 'Remote Port',
218
+ tOpenBtn: '+ Open Tunnel',
219
+ tOpening: 'Opening…',
220
+ tStopBtn: 'Stop',
221
+ tNoTunnels: 'No active port forwarding tunnels.',
222
+ tunnelsColId: 'Tunnel ID',
223
+ tunnelsColRoute: 'Forwarding Route',
224
+ tunnelsColActions: 'Actions',
225
+ tOpenOk: 'Tunnel opened: 127.0.0.1:{localPort} -> Remote:{remotePort}',
226
+ tOpenErr: 'Failed to open tunnel: {err}'
227
+ }
228
+
229
+ const ru = {
230
+ title: 'Удалённое рабочее пространство',
231
+ subtitle: 'SSH-подключения, доступ к файлам по SFTP, 3-way синхронизация зеркала и проброс портов.',
232
+ status: 'Готово',
233
+ badgeOnline: 'Активный хост: {name} ({latency} мс)',
234
+ badgeOffline: 'Хост не выбран',
235
+ badgeTunnels: 'Туннелей: {count}',
236
+ badgeKey: '🔑 Авторизация по ключу',
237
+ badgePassword: '🔒 Авторизация по паролю',
238
+ profilesTitle: '🖥️ Профили SSH серверов',
239
+ profilesDesc: 'Настройка удалённых серверов для запуска команд, SFTP-навигации и синхронизации зеркала.',
240
+ addBtn: '+ Добавить хост',
241
+ nameCol: 'Имя',
242
+ hostCol: 'Хост / Пользователь',
243
+ portCol: 'Порт',
244
+ pathCol: 'Удалённый путь',
245
+ actionsCol: 'Действия',
246
+ noHosts: 'Удалённые хосты ещё не настроены. Нажмите «+ Добавить хост» для создания подключения.',
247
+ activeBadge: 'Активен',
248
+ pinBtn: 'Сделать активным',
249
+ editBtn: 'Править',
250
+ delBtn: 'Удалить',
251
+ delConfirm: 'Удалить профиль хоста «{name}»?',
252
+ formTitleNew: 'Добавление нового SSH хоста',
253
+ formTitleEdit: 'Редактирование профиля хоста',
254
+ fName: 'Название подключения',
255
+ fHost: 'Хост / IP-адрес',
256
+ fPort: 'Порт',
257
+ fUser: 'Имя пользователя',
258
+ fAuthType: 'Способ авторизации',
259
+ fAuthKey: 'SSH-ключ',
260
+ fAuthPass: 'Пароль',
261
+ fKeyPath: 'Путь к приватному ключу',
262
+ fKeyPathHint: 'Путь к ~/.ssh/id_rsa или /home/user/.ssh/id_ed25519',
263
+ fPassphrase: 'Кодовая фраза ключа (если есть)',
264
+ fPassword: 'Пароль учётной записи',
265
+ fRemotePath: 'Удалённая папка проекта',
266
+ fRemotePathHint: 'Абсолютный путь на сервере (например, /home/user/project)',
267
+ fLocalMirror: 'Локальная папка зеркала',
268
+ fLocalMirrorHint: 'Локальная папка для быстрого зеркалирования и бесконфликтной 3-way синхронизации',
269
+ btnBrowse: '📁 Обзор на сервере…',
270
+ btnSelectThis: 'Выбрать папку',
271
+ btnTest: '⚡ Проверить связь (Ping)',
272
+ btnSave: 'Сохранить профиль',
273
+ btnCancel: 'Отмена',
274
+ testing: 'Проверка соединения…',
275
+ saving: 'Сохранение…',
276
+ testOk: '✅ Соединение успешно! Задержка: {latency} мс | ОС: {os}',
277
+ testFail: '❌ Ошибка подключения: {err}',
278
+ diagTitle: '📊 Диагностика активного подключения',
279
+ diagDesc: 'Телеметрия в реальном времени и состояние подсистемы для выбранного сервера.',
280
+ diagProbeBtn: '⚡ Опросить сервер',
281
+ syncTitle: '🔄 Двусторонняя синхронизация зеркала',
282
+ syncDesc: 'Бесконфликтная 3-way синхронизация файлов между локальным зеркалом и сервером.',
283
+ pullBtn: '⬇️ Скачать с сервера (Pull)',
284
+ pushBtn: '⬆️ Загрузить на сервер (Push)',
285
+ pulling: 'Скачивание файлов…',
286
+ pushing: 'Загрузка файлов…',
287
+ syncOk: 'Синхронизация завершена! Обновлено файлов: {count}.',
288
+ syncConflict: 'Внимание: конфликт изменений в {count} файлах.',
289
+ syncErr: 'Ошибка синхронизации: {err}',
290
+ tunnelsTitle: '🔀 SSH-туннели (Port Forwarding)',
291
+ tunnelsDesc: 'Проброс удалённых портов (базы данных, dev-серверы, API) на локальный компьютер через зашифрованный канал.',
292
+ tLocalPort: 'Локальный порт',
293
+ tRemotePort: 'Удалённый порт',
294
+ tOpenBtn: '+ Открыть туннель',
295
+ tOpening: 'Открытие…',
296
+ tStopBtn: 'Закрыть',
297
+ tNoTunnels: 'Нет активных туннелей проброса портов.',
298
+ tunnelsColId: 'ID туннеля',
299
+ tunnelsColRoute: 'Маршрут перенаправления',
300
+ tunnelsColActions: 'Действия',
301
+ tOpenOk: 'Туннель запущен: 127.0.0.1:{localPort} -> Remote:{remotePort}',
302
+ tOpenErr: 'Не удалось запустить туннель: {err}'
303
+ }
304
+
305
+ function makeT(dict, fallback) {
306
+ return function t(key, vars) {
307
+ let val = (dict && dict[key]) || (fallback && fallback[key]) || key
308
+ if (vars && typeof val === 'string') {
309
+ for (const k of Object.keys(vars)) {
310
+ val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]))
311
+ }
312
+ }
313
+ return val
314
+ }
315
+ }
316
+
317
+ function FallbackChevron() {
318
+ return React.createElement(
319
+ 'svg',
320
+ { width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': true },
321
+ React.createElement('path', {
322
+ d: 'M3.5 5.25L7 8.75L10.5 5.25',
323
+ stroke: 'currentColor',
324
+ strokeWidth: 1.5,
325
+ strokeLinecap: 'round',
326
+ strokeLinejoin: 'round',
327
+ })
328
+ )
329
+ }
330
+
331
+
332
+
333
+ // --- Sub-component: Docker Containers Tab ---
334
+ function ContainersTab(props) {
335
+ const { activeProfile, t } = props
336
+ const [containers, setContainers] = React.useState([])
337
+ const [loading, setLoading] = React.useState(false)
338
+ const [logsModal, setLogsModal] = React.useState(null)
339
+ const [logsContent, setLogsContent] = React.useState('')
340
+ const [logsLoading, setLogsLoading] = React.useState(false)
341
+
342
+ const loadContainers = async () => {
343
+ if (!activeProfile) return
344
+ setLoading(true)
345
+ try {
346
+ const res = await fetch(`/dsh-remote-workspace/docker/list?profileId=${activeProfile.id}`)
347
+ const data = await res.json()
348
+ if (data.ok && Array.isArray(data.containers)) {
349
+ setContainers(data.containers)
350
+ } else {
351
+ setContainers([])
352
+ }
353
+ } catch (_) {}
354
+ setLoading(false)
355
+ }
356
+
357
+ React.useEffect(() => {
358
+ loadContainers()
359
+ }, [activeProfile?.id])
360
+
361
+ const handleAction = async (containerId, action) => {
362
+ try {
363
+ await fetch('/dsh-remote-workspace/docker/action', {
364
+ method: 'POST',
365
+ headers: { 'Content-Type': 'application/json' },
366
+ body: JSON.stringify({ profileId: activeProfile.id, containerId, action })
367
+ })
368
+ await loadContainers()
369
+ } catch (_) {}
370
+ }
371
+
372
+ const showLogs = async (containerId, name) => {
373
+ setLogsModal({ id: containerId, name })
374
+ setLogsLoading(true)
375
+ try {
376
+ const res = await fetch('/dsh-remote-workspace/docker/logs', {
377
+ method: 'POST',
378
+ headers: { 'Content-Type': 'application/json' },
379
+ body: JSON.stringify({ profileId: activeProfile.id, containerId, tail: 150 })
380
+ })
381
+ const data = await res.json()
382
+ setLogsContent(data.logs || '[No logs output]')
383
+ } catch (err) {
384
+ setLogsContent(`[Error: ${err.message}]`)
385
+ }
386
+ setLogsLoading(false)
387
+ }
388
+
389
+ return React.createElement(
390
+ 'div',
391
+ { style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
392
+ React.createElement(
393
+ 'div',
394
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
395
+ React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, t('containersTitle')),
396
+ React.createElement(
397
+ 'button',
398
+ {
399
+ type: 'button',
400
+ className: 'drw-btn',
401
+ onClick: loadContainers,
402
+ disabled: loading
403
+ },
404
+ loading ? 'Refreshing...' : '🔄 Refresh'
405
+ )
406
+ ),
407
+ React.createElement(
408
+ 'div',
409
+ { style: { border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', overflow: 'hidden', background: 'var(--dsw-alias-bg-layer-2)' } },
410
+ React.createElement(
411
+ 'div',
412
+ { className: 'drw-container-row', style: { fontWeight: 600, color: 'var(--dsw-alias-label-secondary)', background: 'var(--dsw-alias-bg-layer-1)' } },
413
+ React.createElement('span', null, t('containerName')),
414
+ React.createElement('span', null, t('containerImage')),
415
+ React.createElement('span', null, t('containerStatus')),
416
+ React.createElement('span', null, t('containerPorts')),
417
+ React.createElement('span', { style: { textAlign: 'right' } }, t('actionsCol'))
418
+ ),
419
+ containers.length === 0
420
+ ? React.createElement('div', { style: { padding: '24px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)' } }, t('noContainers'))
421
+ : containers.map((c) =>
422
+ React.createElement(
423
+ 'div',
424
+ { key: c.id, className: 'drw-container-row' },
425
+ React.createElement(
426
+ 'div',
427
+ { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
428
+ React.createElement('span', { className: `drw-badge ${c.state === 'running' ? 'drw-badge-ok' : 'drw-badge-warn'}` }, c.state),
429
+ React.createElement('span', { style: { fontWeight: 600 } }, c.names)
430
+ ),
431
+ React.createElement('span', { style: { opacity: 0.8, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, c.image),
432
+ React.createElement('span', { style: { fontSize: '11px', opacity: 0.7 } }, c.status),
433
+ React.createElement('span', { style: { fontSize: '11px', opacity: 0.7, overflow: 'hidden', textOverflow: 'ellipsis' } }, c.ports || '—'),
434
+ React.createElement(
435
+ 'div',
436
+ { style: { display: 'flex', gap: '4px', justifyContent: 'flex-end' } },
437
+ React.createElement(
438
+ 'button',
439
+ { type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => showLogs(c.id, c.names) },
440
+ t('btnLogs')
441
+ ),
442
+ React.createElement(
443
+ 'button',
444
+ { type: 'button', className: 'drw-btn', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => handleAction(c.id, 'restart') },
445
+ t('btnRestart')
446
+ ),
447
+ c.state === 'running'
448
+ ? React.createElement(
449
+ 'button',
450
+ { type: 'button', className: 'drw-btn drw-btn-danger', style: { padding: '2px 6px', fontSize: '11px' }, onClick: () => handleAction(c.id, 'stop') },
451
+ t('btnStop')
452
+ )
453
+ : null
454
+ )
455
+ )
456
+ )
457
+ ),
458
+ // Container Logs Modal
459
+ logsModal
460
+ ? React.createElement(
461
+ 'div',
462
+ { className: 'drw-modal-overlay', onClick: () => setLogsModal(null) },
463
+ React.createElement(
464
+ 'div',
465
+ { className: 'drw-modal-dialog', onClick: (e) => e.stopPropagation() },
466
+ React.createElement(
467
+ 'div',
468
+ { className: 'drw-modal-hdr' },
469
+ React.createElement('span', { style: { fontFamily: 'monospace' } }, `Docker Logs: ${logsModal.name} (${logsModal.id})`),
470
+ React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => setLogsModal(null) }, '✕')
471
+ ),
472
+ React.createElement(
473
+ 'div',
474
+ { className: 'drw-modal-body' },
475
+ 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)
476
+ )
477
+ )
478
+ )
479
+ : null
480
+ )
481
+ }
482
+
483
+ // --- Sub-component: Web Terminal Tab ---
484
+ function TerminalTab(props) {
485
+ const { activeProfile, t } = props
486
+ const [connected, setConnected] = React.useState(false)
487
+ const [sessionId, setSessionId] = React.useState(null)
488
+ const [output, setOutput] = React.useState('')
489
+ const [cmdInput, setCmdInput] = React.useState('')
490
+ const [loading, setLoading] = React.useState(false)
491
+ const termEndRef = React.useRef(null)
492
+ const evtSourceRef = React.useRef(null)
493
+
494
+ React.useEffect(() => {
495
+ if (termEndRef.current) {
496
+ termEndRef.current.scrollIntoView({ behavior: 'smooth' })
497
+ }
498
+ }, [output])
499
+
500
+ React.useEffect(() => {
501
+ return () => {
502
+ if (evtSourceRef.current) {
503
+ evtSourceRef.current.close()
504
+ }
505
+ }
506
+ }, [])
507
+
508
+ const handleConnect = async () => {
509
+ if (!activeProfile) return
510
+ setLoading(true)
511
+ try {
512
+ const res = await fetch('/dsh-remote-workspace/terminal/create', {
513
+ method: 'POST',
514
+ headers: { 'Content-Type': 'application/json' },
515
+ body: JSON.stringify({ profileId: activeProfile.id, cols: 80, rows: 24 })
516
+ })
517
+ const data = await res.json()
518
+ if (data.ok && data.sessionId) {
519
+ setSessionId(data.sessionId)
520
+ setConnected(true)
521
+ setOutput((prev) => prev + `\n[+] ` + t('termConnected', { id: data.sessionId, host: activeProfile.host }) + `\n`)
522
+
523
+ // Connect SSE stream
524
+ const es = new EventSource(`/dsh-remote-workspace/terminal/stream?sessionId=${data.sessionId}`)
525
+ evtSourceRef.current = es
526
+ es.onmessage = (e) => {
527
+ try {
528
+ const parsed = JSON.parse(e.data)
529
+ if (parsed.chunk) {
530
+ setOutput((p) => p + parsed.chunk)
531
+ }
532
+ } catch (_) {}
533
+ }
534
+ es.onerror = () => {
535
+ setOutput((p) => p + `\n[-] ` + t('termDisconnected') + `\n`)
536
+ setConnected(false)
537
+ es.close()
538
+ }
539
+ }
540
+ } catch (err) {
541
+ setOutput((p) => p + `\n[!] Connection error: ${err.message}\n`)
542
+ }
543
+ setLoading(false)
544
+ }
545
+
546
+ const handleDisconnect = async () => {
547
+ if (sessionId) {
548
+ try {
549
+ await fetch('/dsh-remote-workspace/terminal/close', {
550
+ method: 'POST',
551
+ headers: { 'Content-Type': 'application/json' },
552
+ body: JSON.stringify({ sessionId })
553
+ })
554
+ } catch (_) {}
555
+ }
556
+ if (evtSourceRef.current) {
557
+ evtSourceRef.current.close()
558
+ }
559
+ setConnected(false)
560
+ setSessionId(null)
561
+ setOutput((p) => p + `\n[-] ` + t('termDisconnected') + `\n`)
562
+ }
563
+
564
+ const sendCommand = async (e) => {
565
+ if (e) e.preventDefault()
566
+ if (!cmdInput || !sessionId) return
567
+ const toSend = cmdInput + '\n'
568
+ setCmdInput('')
569
+ try {
570
+ await fetch('/dsh-remote-workspace/terminal/input', {
571
+ method: 'POST',
572
+ headers: { 'Content-Type': 'application/json' },
573
+ body: JSON.stringify({ sessionId, data: toSend })
574
+ })
575
+ } catch (_) {}
576
+ }
577
+
578
+ const sendCtrlC = async () => {
579
+ if (!sessionId) return
580
+ try {
581
+ await fetch('/dsh-remote-workspace/terminal/input', {
582
+ method: 'POST',
583
+ headers: { 'Content-Type': 'application/json' },
584
+ body: JSON.stringify({ sessionId, data: '\x03' })
585
+ })
586
+ } catch (_) {}
587
+ }
588
+
589
+ return React.createElement(
590
+ 'div',
591
+ { style: { display: 'flex', flexDirection: 'column', gap: '12px' } },
592
+ React.createElement(
593
+ 'div',
594
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
595
+ React.createElement(
596
+ 'div',
597
+ { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
598
+ React.createElement('span', { style: { fontWeight: 600, fontSize: '14px' } }, 'SSH PTY Shell:'),
599
+ React.createElement(
600
+ 'span',
601
+ { className: `drw-badge ${connected ? 'drw-badge-ok' : 'drw-badge-warn'}` },
602
+ connected ? `🟢 Online (${activeProfile?.host})` : '⚪ Idle'
603
+ )
604
+ ),
605
+ React.createElement(
606
+ 'div',
607
+ { style: { display: 'flex', gap: '8px' } },
608
+ !connected
609
+ ? React.createElement(
610
+ 'button',
611
+ {
612
+ type: 'button',
613
+ className: 'drw-btn drw-btn-primary',
614
+ disabled: !activeProfile || loading,
615
+ onClick: handleConnect
616
+ },
617
+ loading ? 'Connecting...' : t('termStart')
618
+ )
619
+ : React.createElement(
620
+ 'button',
621
+ {
622
+ type: 'button',
623
+ className: 'drw-btn drw-btn-danger',
624
+ onClick: handleDisconnect
625
+ },
626
+ t('termStop')
627
+ ),
628
+ connected
629
+ ? React.createElement(
630
+ 'button',
631
+ {
632
+ type: 'button',
633
+ className: 'drw-btn',
634
+ onClick: sendCtrlC
635
+ },
636
+ 'Ctrl+C'
637
+ )
638
+ : null,
639
+ React.createElement(
640
+ 'button',
641
+ {
642
+ type: 'button',
643
+ className: 'drw-btn',
644
+ onClick: () => setOutput('')
645
+ },
646
+ t('termClear')
647
+ )
648
+ )
649
+ ),
650
+ React.createElement(
651
+ 'div',
652
+ { className: 'drw-term-window' },
653
+ React.createElement(
654
+ 'div',
655
+ { className: 'drw-term-header' },
656
+ React.createElement('span', null, `terminal ~ ${activeProfile?.username || 'user'}@${activeProfile?.host || 'remote'}`),
657
+ React.createElement('span', null, connected ? `Session: ${sessionId}` : 'Offline')
658
+ ),
659
+ React.createElement(
660
+ 'div',
661
+ { className: 'drw-term-body' },
662
+ 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.`),
663
+ React.createElement('div', { ref: termEndRef })
664
+ ),
665
+ connected
666
+ ? React.createElement(
667
+ 'form',
668
+ { className: 'drw-term-footer', onSubmit: sendCommand },
669
+ React.createElement('span', { style: { color: '#38bdf8', padding: '4px 6px', fontFamily: 'monospace' } }, '$'),
670
+ React.createElement('input', {
671
+ className: 'drw-term-input',
672
+ type: 'text',
673
+ placeholder: t('termPlaceholder'),
674
+ value: cmdInput,
675
+ onChange: (e) => setCmdInput(e.target.value)
676
+ }),
677
+ React.createElement(
678
+ 'button',
679
+ { type: 'submit', className: 'drw-btn', style: { padding: '2px 8px', fontSize: '11px' } },
680
+ 'Send ↵'
681
+ )
682
+ )
683
+ : null
684
+ )
685
+ )
686
+ }
687
+
688
+ // --- Sub-component: Remote File Explorer Tab ---
689
+ function ExplorerTab(props) {
690
+ const { activeProfile, t } = props
691
+ const [currentPath, setCurrentPath] = React.useState(activeProfile?.remoteWorkspace || '/')
692
+ const [entries, setEntries] = React.useState([])
693
+ const [loading, setLoading] = React.useState(false)
694
+ const [previewFile, setPreviewFile] = React.useState(null)
695
+ const [previewContent, setPreviewContent] = React.useState('')
696
+ const [editContent, setEditContent] = React.useState('')
697
+ const [previewLoading, setPreviewLoading] = React.useState(false)
698
+ const [saveStatus, setSaveStatus] = React.useState(null)
699
+
700
+ const loadDir = async (dirPath) => {
701
+ if (!activeProfile) return
702
+ setLoading(true)
703
+ try {
704
+ const res = await fetch('/dsh-remote-workspace/browse', {
705
+ method: 'POST',
706
+ headers: { 'Content-Type': 'application/json' },
707
+ body: JSON.stringify({ profileId: activeProfile.id, remotePath: dirPath })
708
+ })
709
+ const data = await res.json()
710
+ if (data.ok && Array.isArray(data.entries)) {
711
+ setCurrentPath(dirPath)
712
+ setEntries(data.entries)
713
+ }
714
+ } catch (_) {}
715
+ setLoading(false)
716
+ }
717
+
718
+ React.useEffect(() => {
719
+ if (activeProfile) {
720
+ loadDir(activeProfile.remoteWorkspace || '/')
721
+ }
722
+ }, [activeProfile?.id])
723
+
724
+ const handleNavUp = () => {
725
+ const parts = currentPath.split('/').filter(Boolean)
726
+ parts.pop()
727
+ const parent = '/' + parts.join('/')
728
+ loadDir(parent || '/')
729
+ }
730
+
731
+ const openFile = async (entry) => {
732
+ const filePath = currentPath.endsWith('/') ? currentPath + entry.filename : currentPath + '/' + entry.filename
733
+ setPreviewLoading(true)
734
+ setPreviewFile(filePath)
735
+ setSaveStatus(null)
736
+ try {
737
+ const res = await fetch('/dsh-remote-workspace/file/view', {
738
+ method: 'POST',
739
+ headers: { 'Content-Type': 'application/json' },
740
+ body: JSON.stringify({ profileId: activeProfile.id, filePath })
741
+ })
742
+ const data = await res.json()
743
+ if (data.ok) {
744
+ setPreviewContent(data.content)
745
+ setEditContent(data.content)
746
+ } else {
747
+ setPreviewContent(`[Error loading file: ${data.error || 'Failed'}]`)
748
+ setEditContent('')
749
+ }
750
+ } catch (err) {
751
+ setPreviewContent(`[Error: ${err.message}]`)
752
+ }
753
+ setPreviewLoading(false)
754
+ }
755
+
756
+ const handleSaveFile = async () => {
757
+ if (!previewFile || !activeProfile) return
758
+ setSaveStatus('saving')
759
+ try {
760
+ const res = await fetch('/dsh-remote-workspace/file/save', {
761
+ method: 'POST',
762
+ headers: { 'Content-Type': 'application/json' },
763
+ body: JSON.stringify({ profileId: activeProfile.id, filePath: previewFile, content: editContent })
764
+ })
765
+ const data = await res.json()
766
+ if (data.ok) {
767
+ setSaveStatus('saved')
768
+ setPreviewContent(editContent)
769
+ } else {
770
+ setSaveStatus('error')
771
+ }
772
+ } catch (_) {
773
+ setSaveStatus('error')
774
+ }
775
+ }
776
+
777
+ return React.createElement(
778
+ 'div',
779
+ { className: 'drw-explorer-box' },
780
+ React.createElement(
781
+ 'div',
782
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
783
+ React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, t('explorerTitle')),
784
+ React.createElement(
785
+ 'button',
786
+ {
787
+ type: 'button',
788
+ className: 'drw-btn',
789
+ onClick: () => loadDir(currentPath),
790
+ disabled: loading
791
+ },
792
+ loading ? 'Refreshing...' : '🔄 Refresh'
793
+ )
794
+ ),
795
+ React.createElement(
796
+ 'div',
797
+ { className: 'drw-explorer-nav' },
798
+ React.createElement(
799
+ 'button',
800
+ {
801
+ type: 'button',
802
+ className: 'drw-btn',
803
+ style: { padding: '2px 8px', fontSize: '11px' },
804
+ onClick: handleNavUp,
805
+ disabled: currentPath === '/' || loading
806
+ },
807
+ '⬆️ Up'
808
+ ),
809
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, 'Path:'),
810
+ React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-brand-primary)' } }, currentPath)
811
+ ),
812
+ React.createElement(
813
+ 'div',
814
+ { className: 'drw-file-list' },
815
+ entries.length === 0
816
+ ? React.createElement('div', { style: { padding: '16px', textAlign: 'center', color: 'var(--dsw-alias-label-secondary)' } }, loading ? 'Reading directory...' : 'Directory is empty.')
817
+ : entries.map((e) =>
818
+ React.createElement(
819
+ 'div',
820
+ {
821
+ key: e.filename,
822
+ className: 'drw-file-row',
823
+ onClick: () => (e.isDirectory ? loadDir(currentPath.endsWith('/') ? currentPath + e.filename : currentPath + '/' + e.filename) : openFile(e))
824
+ },
825
+ React.createElement(
826
+ 'div',
827
+ { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
828
+ React.createElement('span', null, e.isDirectory ? '📁' : '📄'),
829
+ React.createElement('span', { style: { fontWeight: e.isDirectory ? 600 : 400 } }, e.filename)
830
+ ),
831
+ React.createElement(
832
+ 'div',
833
+ { className: 'drw-file-meta' },
834
+ e.size !== undefined ? React.createElement('span', null, `${Math.round(e.size / 1024)} KB`) : null,
835
+ e.permissions ? React.createElement('span', { style: { opacity: 0.6 } }, e.permissions) : null
836
+ )
837
+ )
838
+ )
839
+ ),
840
+ // File Preview / Edit Modal
841
+ previewFile
842
+ ? React.createElement(
843
+ 'div',
844
+ { className: 'drw-modal-overlay', onClick: () => setPreviewFile(null) },
845
+ React.createElement(
846
+ 'div',
847
+ { className: 'drw-modal-dialog', onClick: (e) => e.stopPropagation() },
848
+ React.createElement(
849
+ 'div',
850
+ { className: 'drw-modal-hdr' },
851
+ React.createElement('span', { style: { fontFamily: 'monospace' } }, t('filePreviewTitle', { path: previewFile })),
852
+ React.createElement(
853
+ 'button',
854
+ {
855
+ type: 'button',
856
+ className: 'drw-btn',
857
+ style: { padding: '2px 8px' },
858
+ onClick: () => setPreviewFile(null)
859
+ },
860
+ '✕'
861
+ )
862
+ ),
863
+ React.createElement(
864
+ 'div',
865
+ { className: 'drw-modal-body', style: { display: 'flex', flexDirection: 'column', gap: '10px' } },
866
+ previewLoading
867
+ ? React.createElement('div', { style: { padding: '20px', textAlign: 'center' } }, 'Loading content...')
868
+ : React.createElement('textarea', {
869
+ style: {
870
+ width: '100%',
871
+ height: '320px',
872
+ background: '#0d1117',
873
+ color: '#c9d1d9',
874
+ fontFamily: 'monospace',
875
+ fontSize: '12px',
876
+ padding: '10px',
877
+ border: '1px solid #30363d',
878
+ borderRadius: '8px',
879
+ resize: 'vertical',
880
+ boxSizing: 'border-box'
881
+ },
882
+ value: editContent,
883
+ onChange: (e) => setEditContent(e.target.value)
884
+ }),
885
+ React.createElement(
886
+ 'div',
887
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
888
+ React.createElement(
889
+ 'span',
890
+ { style: { fontSize: '12px', color: saveStatus === 'saved' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)' } },
891
+ saveStatus === 'saved' ? t('fileSaved') : saveStatus === 'error' ? 'Error saving file.' : ''
892
+ ),
893
+ React.createElement(
894
+ 'div',
895
+ { style: { display: 'flex', gap: '8px' } },
896
+ React.createElement(
897
+ 'button',
898
+ {
899
+ type: 'button',
900
+ className: 'drw-btn',
901
+ onClick: () => setPreviewFile(null)
902
+ },
903
+ 'Close'
904
+ ),
905
+ React.createElement(
906
+ 'button',
907
+ {
908
+ type: 'button',
909
+ className: 'drw-btn drw-btn-primary',
910
+ onClick: handleSaveFile,
911
+ disabled: saveStatus === 'saving'
912
+ },
913
+ saveStatus === 'saving' ? 'Saving...' : t('btnSaveFile')
914
+ )
915
+ )
916
+ )
917
+ )
918
+ )
919
+ )
920
+ : null
921
+ )
922
+ }
923
+
924
+ function SettingsView(props) {
925
+ const [activeTab, setActiveTab] = React.useState('profiles')
926
+ const [healthData, setHealthData] = React.useState(null)
927
+ const [autoSync, setAutoSync] = React.useState(false)
928
+
929
+ const t = props.locale === 'ru' ? makeT(ru, en) : makeT(en, ru)
930
+ const [profiles, setProfiles] = React.useState([])
931
+ const [activeId, setActiveId] = React.useState(null)
932
+ const [tunnels, setTunnels] = React.useState([])
933
+ const [editing, setEditing] = React.useState(null)
934
+ const [busy, setBusy] = React.useState(null)
935
+ const [testResult, setTestResult] = React.useState(null)
936
+ const [syncMsg, setSyncMsg] = React.useState(null)
937
+ const [tunnelMsg, setTunnelMsg] = React.useState(null)
938
+ const [showPassword, setShowPassword] = React.useState(false)
939
+
940
+ // Tunnel creation inputs
941
+ const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
942
+ const [newTunnelRemote, setNewTunnelRemote] = React.useState('3000')
943
+
944
+ // Remote Directory Browser State
945
+ const [browserOpen, setBrowserOpen] = React.useState(false)
946
+ const [browserPath, setBrowserPath] = React.useState('/')
947
+ const [browserEntries, setBrowserEntries] = React.useState([])
948
+ const [browserLoading, setBrowserLoading] = React.useState(false)
949
+
950
+ const loadState = async () => {
951
+ try {
952
+ const res = await fetch('/dsh-remote-workspace/state')
953
+ if (res.ok) {
954
+ const data = await res.json()
955
+ if (data.ok) {
956
+ setProfiles(data.profiles || [])
957
+ setActiveId(data.activeId || null)
958
+ setTunnels(data.tunnels || [])
959
+ setAutoSync(Boolean(data.autoSync))
960
+ if (data.activeId) {
961
+ fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
962
+ }
963
+ }
964
+ }
965
+ } catch (_) {}
966
+ }
967
+
968
+ React.useEffect(() => {
969
+ ensureCss()
970
+ loadState()
971
+ }, [])
972
+
973
+ const handleSave = async (profile) => {
974
+ setBusy('saving')
975
+ try {
976
+ const res = await fetch('/dsh-remote-workspace/profiles/save', {
977
+ method: 'POST',
978
+ headers: { 'Content-Type': 'application/json' },
979
+ body: JSON.stringify(profile)
980
+ })
981
+ if (res.ok) {
982
+ setEditing(null)
983
+ setBrowserOpen(false)
984
+ setTestResult(null)
985
+ await loadState()
986
+ }
987
+ } catch (_) {}
988
+ setBusy(null)
989
+ }
990
+
991
+ const handleDelete = async (id, name) => {
992
+ if (!confirm(t('delConfirm', { name: name || id }))) return
993
+ try {
994
+ const res = await fetch('/dsh-remote-workspace/profiles/delete', {
995
+ method: 'POST',
996
+ headers: { 'Content-Type': 'application/json' },
997
+ body: JSON.stringify({ id })
998
+ })
999
+ if (res.ok) {
1000
+ if (editing && editing.id === id) setEditing(null)
1001
+ await loadState()
1002
+ }
1003
+ } catch (_) {}
1004
+ }
1005
+
1006
+ const handleSetActive = async (id) => {
1007
+ try {
1008
+ const res = await fetch('/dsh-remote-workspace/profiles/active', {
1009
+ method: 'POST',
1010
+ headers: { 'Content-Type': 'application/json' },
1011
+ body: JSON.stringify({ id })
1012
+ })
1013
+ if (res.ok) {
1014
+ await loadState()
1015
+ }
1016
+ } catch (_) {}
1017
+ }
1018
+
1019
+ const handleTest = async (prof) => {
1020
+ setBusy('testing')
1021
+ setTestResult(null)
1022
+ try {
1023
+ const res = await fetch('/dsh-remote-workspace/test', {
1024
+ method: 'POST',
1025
+ headers: { 'Content-Type': 'application/json' },
1026
+ body: JSON.stringify({ profile: prof })
1027
+ })
1028
+ const data = await res.json()
1029
+ setTestResult(data)
1030
+ } catch (err) {
1031
+ setTestResult({ ok: false, error: err.message })
1032
+ }
1033
+ setBusy(null)
1034
+ }
1035
+
1036
+ const openDirectoryBrowser = async (initialPath) => {
1037
+ setBrowserOpen(true)
1038
+ const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
1039
+ await fetchRemoteDir(target)
1040
+ }
1041
+
1042
+ const fetchRemoteDir = async (dirPath) => {
1043
+ setBrowserLoading(true)
1044
+ try {
1045
+ const res = await fetch('/dsh-remote-workspace/browse', {
1046
+ method: 'POST',
1047
+ headers: { 'Content-Type': 'application/json' },
1048
+ body: JSON.stringify({ profile: editing, remotePath: dirPath })
1049
+ })
1050
+ const data = await res.json()
1051
+ if (data.ok) {
1052
+ setBrowserPath(data.currentPath || dirPath)
1053
+ setBrowserEntries(data.entries || [])
1054
+ }
1055
+ } catch (_) {}
1056
+ setBrowserLoading(false)
1057
+ }
1058
+
1059
+ const handleSync = async (direction) => {
1060
+ setBusy(`sync_${direction}`)
1061
+ setSyncMsg(null)
1062
+ try {
1063
+ const res = await fetch('/dsh-remote-workspace/sync', {
1064
+ method: 'POST',
1065
+ headers: { 'Content-Type': 'application/json' },
1066
+ body: JSON.stringify({ direction })
1067
+ })
1068
+ const data = await res.json()
1069
+ if (data.ok) {
1070
+ const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
1071
+ if (data.conflicts && data.conflicts.length > 0) {
1072
+ setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
1073
+ } else {
1074
+ setSyncMsg({ ok: true, text: t('syncOk', { count }) })
1075
+ }
1076
+ } else {
1077
+ setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
1078
+ }
1079
+ } catch (err) {
1080
+ setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
1081
+ }
1082
+ setBusy(null)
1083
+ }
1084
+
1085
+ const handleStartTunnel = async () => {
1086
+ if (!activeId) return
1087
+ setBusy('tunnel_open')
1088
+ setTunnelMsg(null)
1089
+ try {
1090
+ const res = await fetch('/dsh-remote-workspace/tunnels/start', {
1091
+ method: 'POST',
1092
+ headers: { 'Content-Type': 'application/json' },
1093
+ body: JSON.stringify({
1094
+ profileId: activeId,
1095
+ localPort: newTunnelLocal,
1096
+ remotePort: newTunnelRemote
1097
+ })
1098
+ })
1099
+ const data = await res.json()
1100
+ if (data.ok) {
1101
+ setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
1102
+ await loadState()
1103
+ } else {
1104
+ setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
1105
+ }
1106
+ } catch (err) {
1107
+ setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
1108
+ }
1109
+ setBusy(null)
1110
+ }
1111
+
1112
+ const handleStopTunnel = async (tunnelId) => {
1113
+ try {
1114
+ const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
1115
+ method: 'POST',
1116
+ headers: { 'Content-Type': 'application/json' },
1117
+ body: JSON.stringify({ tunnelId })
1118
+ })
1119
+ if (res.ok) {
1120
+ await loadState()
1121
+ }
1122
+ } catch (_) {}
1123
+ }
1124
+
1125
+ const activeProfile = profiles.find((p) => p.id === activeId)
1126
+
1127
+ return React.createElement(
1128
+ 'div',
1129
+ { className: 'drw-page' },
1130
+
1131
+ // Header with Live Badges (ClineBot Style)
1132
+ React.createElement(
1133
+ 'div',
1134
+ { className: 'drw-header' },
1135
+ React.createElement(
1136
+ 'div',
1137
+ { className: 'drw-header-top' },
1138
+ React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
1139
+ React.createElement(
1140
+ 'div',
1141
+ { className: 'drw-header-badges' },
1142
+ activeProfile
1143
+ ? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
1144
+ : React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
1145
+ activeProfile
1146
+ ? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
1147
+ : null,
1148
+ React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
1149
+ )
1150
+ ),
1151
+ React.createElement('div', { className: 'drw-page-sub' }, t('subtitle'))
1152
+ ),
1153
+
1154
+ // Section 1: SSH Profiles Table & Management
1155
+ React.createElement(
1156
+ 'div',
1157
+ { className: 'drw-card' },
1158
+ React.createElement(
1159
+ 'div',
1160
+ { className: 'drw-card-title' },
1161
+ React.createElement('span', null, t('profilesTitle')),
1162
+ !editing
1163
+ ? React.createElement(
1164
+ 'button',
1165
+ {
1166
+ type: 'button',
1167
+ className: 'drw-btn drw-btn-primary',
1168
+ onClick: () => {
1169
+ setEditing({
1170
+ id: 'prof_' + Math.random().toString(36).substr(2, 9),
1171
+ name: 'New Server',
1172
+ host: '',
1173
+ port: 22,
1174
+ username: 'root',
1175
+ authType: 'key',
1176
+ privateKeyPath: '~/.ssh/id_rsa',
1177
+ passphrase: '',
1178
+ password: '',
1179
+ remoteWorkspace: '',
1180
+ localMirrorPath: ''
1181
+ })
1182
+ setTestResult(null)
1183
+ }
1184
+ },
1185
+ t('addBtn')
1186
+ )
1187
+ : null
1188
+ ),
1189
+ React.createElement('div', { className: 'drw-card-desc' }, t('profilesDesc')),
1190
+
1191
+ // Interactive Editing Form
1192
+ editing
1193
+ ? React.createElement(
1194
+ 'div',
1195
+ {
1196
+ style: {
1197
+ padding: '16px',
1198
+ borderRadius: '10px',
1199
+ background: 'var(--dsw-alias-bg-layer-2)',
1200
+ border: '1px solid var(--dsw-alias-border-l2)',
1201
+ display: 'flex',
1202
+ flexDirection: 'column',
1203
+ gap: '14px'
1204
+ }
1205
+ },
1206
+ React.createElement(
1207
+ 'div',
1208
+ { style: { fontWeight: 600, fontSize: '14px', color: 'var(--dsw-alias-label-primary)' } },
1209
+ editing.name ? t('formTitleEdit') : t('formTitleNew')
1210
+ ),
1211
+ React.createElement(
1212
+ 'div',
1213
+ { className: 'drw-grid-2' },
1214
+ React.createElement(
1215
+ 'div',
1216
+ { className: 'drw-field' },
1217
+ React.createElement('span', { className: 'drw-label' }, t('fName')),
1218
+ React.createElement('input', {
1219
+ className: 'drw-input',
1220
+ value: editing.name || '',
1221
+ onChange: (e) => setEditing({ ...editing, name: e.target.value })
1222
+ })
1223
+ ),
1224
+ React.createElement(
1225
+ 'div',
1226
+ { className: 'drw-field' },
1227
+ React.createElement('span', { className: 'drw-label' }, t('fHost')),
1228
+ React.createElement('input', {
1229
+ className: 'drw-input',
1230
+ placeholder: '192.168.1.100 or server.domain.com',
1231
+ value: editing.host || '',
1232
+ onChange: (e) => setEditing({ ...editing, host: e.target.value })
1233
+ })
1234
+ ),
1235
+ React.createElement(
1236
+ 'div',
1237
+ { className: 'drw-field' },
1238
+ React.createElement('span', { className: 'drw-label' }, t('fPort')),
1239
+ React.createElement('input', {
1240
+ className: 'drw-input',
1241
+ type: 'number',
1242
+ value: editing.port || 22,
1243
+ onChange: (e) => setEditing({ ...editing, port: parseInt(e.target.value, 10) || 22 })
1244
+ })
1245
+ ),
1246
+ React.createElement(
1247
+ 'div',
1248
+ { className: 'drw-field' },
1249
+ React.createElement('span', { className: 'drw-label' }, t('fUser')),
1250
+ React.createElement('input', {
1251
+ className: 'drw-input',
1252
+ value: editing.username || '',
1253
+ onChange: (e) => setEditing({ ...editing, username: e.target.value })
1254
+ })
1255
+ )
1256
+ ),
1257
+
1258
+ // Auth Method Segmented Switcher (Key vs Password)
1259
+ React.createElement(
1260
+ 'div',
1261
+ { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
1262
+ React.createElement('span', { className: 'drw-label' }, t('fAuthType')),
1263
+ React.createElement(
1264
+ 'div',
1265
+ { className: 'drw-segmented' },
1266
+ React.createElement(
1267
+ 'button',
1268
+ {
1269
+ type: 'button',
1270
+ className: `drw-segmented-item ${editing.authType !== 'password' ? 'drw-segmented-item-active' : ''}`,
1271
+ onClick: () => setEditing({ ...editing, authType: 'key' })
1272
+ },
1273
+ '🔑 ' + t('fAuthKey')
1274
+ ),
1275
+ React.createElement(
1276
+ 'button',
1277
+ {
1278
+ type: 'button',
1279
+ className: `drw-segmented-item ${editing.authType === 'password' ? 'drw-segmented-item-active' : ''}`,
1280
+ onClick: () => setEditing({ ...editing, authType: 'password' })
1281
+ },
1282
+ '🔒 ' + t('fAuthPass')
1283
+ )
1284
+ )
1285
+ ),
1286
+
1287
+ // Conditional Auth Inputs
1288
+ editing.authType === 'password'
1289
+ ? React.createElement(
1290
+ 'div',
1291
+ { className: 'drw-field' },
1292
+ React.createElement('span', { className: 'drw-label' }, t('fPassword')),
1293
+ React.createElement(
1294
+ 'div',
1295
+ { style: { display: 'flex', gap: '8px' } },
1296
+ React.createElement('input', {
1297
+ className: 'drw-input',
1298
+ type: showPassword ? 'text' : 'password',
1299
+ placeholder: '••••••••••••',
1300
+ value: editing.password || '',
1301
+ onChange: (e) => setEditing({ ...editing, password: e.target.value })
1302
+ }),
1303
+ React.createElement(
1304
+ 'button',
1305
+ {
1306
+ type: 'button',
1307
+ className: 'drw-btn',
1308
+ style: { fontSize: '11px', whiteSpace: 'nowrap' },
1309
+ onClick: () => setShowPassword((v) => !v)
1310
+ },
1311
+ showPassword ? 'Hide' : 'Show'
1312
+ )
1313
+ )
1314
+ )
1315
+ : React.createElement(
1316
+ 'div',
1317
+ { className: 'drw-grid-2' },
1318
+ React.createElement(
1319
+ 'div',
1320
+ { className: 'drw-field' },
1321
+ React.createElement('span', { className: 'drw-label' }, t('fKeyPath')),
1322
+ React.createElement('input', {
1323
+ className: 'drw-input',
1324
+ placeholder: '~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
1325
+ value: editing.privateKeyPath || '',
1326
+ onChange: (e) => setEditing({ ...editing, privateKeyPath: e.target.value })
1327
+ }),
1328
+ React.createElement('span', { className: 'drw-hint' }, t('fKeyPathHint'))
1329
+ ),
1330
+ React.createElement(
1331
+ 'div',
1332
+ { className: 'drw-field' },
1333
+ React.createElement('span', { className: 'drw-label' }, t('fPassphrase')),
1334
+ React.createElement('input', {
1335
+ className: 'drw-input',
1336
+ type: 'password',
1337
+ placeholder: 'Optional passphrase',
1338
+ value: editing.passphrase || '',
1339
+ onChange: (e) => setEditing({ ...editing, passphrase: e.target.value })
1340
+ })
1341
+ )
1342
+ ),
1343
+
1344
+ // Workspace Paths (Remote Workspace & Local Mirror)
1345
+ React.createElement(
1346
+ 'div',
1347
+ { className: 'drw-grid-2' },
1348
+ React.createElement(
1349
+ 'div',
1350
+ { className: 'drw-field' },
1351
+ React.createElement('span', { className: 'drw-label' }, t('fRemotePath')),
1352
+ React.createElement(
1353
+ 'div',
1354
+ { style: { display: 'flex', gap: '8px' } },
1355
+ React.createElement('input', {
1356
+ className: 'drw-input',
1357
+ placeholder: '/home/user/project',
1358
+ value: editing.remoteWorkspace || '',
1359
+ onChange: (e) => setEditing({ ...editing, remoteWorkspace: e.target.value })
1360
+ }),
1361
+ React.createElement(
1362
+ 'button',
1363
+ {
1364
+ type: 'button',
1365
+ className: 'drw-btn',
1366
+ style: { whiteSpace: 'nowrap', fontSize: '12px' },
1367
+ disabled: !editing.host,
1368
+ onClick: () => openDirectoryBrowser(editing.remoteWorkspace || '/')
1369
+ },
1370
+ t('btnBrowse')
1371
+ )
1372
+ ),
1373
+ React.createElement('span', { className: 'drw-hint' }, t('fRemotePathHint'))
1374
+ ),
1375
+ React.createElement(
1376
+ 'div',
1377
+ { className: 'drw-field' },
1378
+ React.createElement('span', { className: 'drw-label' }, t('fLocalMirror')),
1379
+ React.createElement('input', {
1380
+ className: 'drw-input',
1381
+ placeholder: '~/.dsh/mirrors/my-project',
1382
+ value: editing.localMirrorPath || '',
1383
+ onChange: (e) => setEditing({ ...editing, localMirrorPath: e.target.value })
1384
+ }),
1385
+ React.createElement('span', { className: 'drw-hint' }, t('fLocalMirrorHint'))
1386
+ )
1387
+ ),
1388
+
1389
+ // Directory Browser Modal
1390
+ browserOpen
1391
+ ? React.createElement(
1392
+ 'div',
1393
+ { className: 'drw-browser-modal' },
1394
+ React.createElement(
1395
+ 'div',
1396
+ { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
1397
+ React.createElement('span', { style: { fontSize: '12px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, `📂 ${browserPath}`),
1398
+ React.createElement(
1399
+ 'div',
1400
+ { style: { display: 'flex', gap: '6px' } },
1401
+ React.createElement(
1402
+ 'button',
1403
+ {
1404
+ type: 'button',
1405
+ className: 'drw-btn drw-btn-primary',
1406
+ style: { fontSize: '11px', padding: '3px 8px' },
1407
+ onClick: () => {
1408
+ setEditing({ ...editing, remoteWorkspace: browserPath })
1409
+ setBrowserOpen(false)
1410
+ }
1411
+ },
1412
+ t('btnSelectThis')
1413
+ ),
1414
+ React.createElement(
1415
+ 'button',
1416
+ {
1417
+ type: 'button',
1418
+ className: 'drw-btn',
1419
+ style: { fontSize: '11px', padding: '3px 8px' },
1420
+ onClick: () => setBrowserOpen(false)
1421
+ },
1422
+ '✕'
1423
+ )
1424
+ )
1425
+ ),
1426
+ React.createElement(
1427
+ 'div',
1428
+ { className: 'drw-browser-list' },
1429
+ browserLoading
1430
+ ? React.createElement('div', { style: { padding: '8px', fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, 'Loading directory entries...')
1431
+ : React.createElement(
1432
+ React.Fragment,
1433
+ null,
1434
+ browserPath !== '/'
1435
+ ? React.createElement(
1436
+ 'div',
1437
+ {
1438
+ className: 'drw-browser-item',
1439
+ onClick: () => {
1440
+ const parent = browserPath.substring(0, browserPath.lastIndexOf('/')) || '/'
1441
+ fetchRemoteDir(parent)
1442
+ }
1443
+ },
1444
+ '📁 .. (parent directory)'
1445
+ )
1446
+ : null,
1447
+ browserEntries
1448
+ .filter((e) => e.isDirectory)
1449
+ .map((e) =>
1450
+ React.createElement(
1451
+ 'div',
1452
+ {
1453
+ key: e.filename,
1454
+ className: 'drw-browser-item',
1455
+ onClick: () => {
1456
+ const next = browserPath === '/' ? `/${e.filename}` : `${browserPath}/${e.filename}`
1457
+ fetchRemoteDir(next)
1458
+ }
1459
+ },
1460
+ `📁 ${e.filename}`
1461
+ )
1462
+ )
1463
+ )
1464
+ )
1465
+ )
1466
+ : null,
1467
+
1468
+ // Form Action Buttons
1469
+ React.createElement(
1470
+ 'div',
1471
+ { className: 'drw-row', style: { justifyContent: 'flex-end', marginTop: '6px' } },
1472
+ React.createElement(
1473
+ 'button',
1474
+ {
1475
+ type: 'button',
1476
+ className: 'drw-btn',
1477
+ disabled: !editing.host || !!busy,
1478
+ onClick: () => handleTest(editing)
1479
+ },
1480
+ busy === 'testing' ? t('testing') : t('btnTest')
1481
+ ),
1482
+ React.createElement(
1483
+ 'button',
1484
+ {
1485
+ type: 'button',
1486
+ className: 'drw-btn',
1487
+ onClick: () => {
1488
+ setEditing(null)
1489
+ setBrowserOpen(false)
1490
+ setTestResult(null)
1491
+ }
1492
+ },
1493
+ t('btnCancel')
1494
+ ),
1495
+ React.createElement(
1496
+ 'button',
1497
+ {
1498
+ type: 'button',
1499
+ className: 'drw-btn drw-btn-primary',
1500
+ disabled: !editing.host || !!busy,
1501
+ onClick: () => handleSave(editing)
1502
+ },
1503
+ busy === 'saving' ? t('saving') : t('btnSave')
1504
+ )
1505
+ ),
1506
+ testResult
1507
+ ? React.createElement(
1508
+ 'div',
1509
+ {
1510
+ className: 'drw-preview',
1511
+ style: {
1512
+ borderColor: testResult.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
1513
+ }
1514
+ },
1515
+ testResult.ok
1516
+ ? t('testOk', { latency: testResult.latencyMs ?? testResult.latency ?? 0, os: testResult.remoteOs || testResult.os || 'Linux' })
1517
+ : t('testFail', { err: testResult.error || 'Connection timed out' })
1518
+ )
1519
+ : null
1520
+ )
1521
+ : null,
1522
+
1523
+ // Profiles List Table
1524
+ profiles.length > 0
1525
+ ? React.createElement(
1526
+ 'table',
1527
+ { className: 'drw-table' },
1528
+ React.createElement(
1529
+ 'thead',
1530
+ null,
1531
+ React.createElement(
1532
+ 'tr',
1533
+ null,
1534
+ React.createElement('th', null, t('nameCol')),
1535
+ React.createElement('th', null, t('hostCol')),
1536
+ React.createElement('th', null, t('portCol')),
1537
+ React.createElement('th', null, t('pathCol')),
1538
+ React.createElement('th', { style: { textAlign: 'right' } }, t('actionsCol'))
1539
+ )
1540
+ ),
1541
+ React.createElement(
1542
+ 'tbody',
1543
+ null,
1544
+ profiles.map((p) => {
1545
+ const isAct = p.id === activeId
1546
+ return React.createElement(
1547
+ 'tr',
1548
+ { key: p.id },
1549
+ React.createElement(
1550
+ 'td',
1551
+ null,
1552
+ React.createElement('span', { style: { fontWeight: 600 } }, p.name || 'Server'),
1553
+ React.createElement(
1554
+ 'span',
1555
+ { className: 'drw-badge', style: { marginLeft: '8px' } },
1556
+ p.authType === 'password' ? '🔒 pwd' : '🔑 key'
1557
+ ),
1558
+ isAct
1559
+ ? React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginLeft: '6px' } }, t('activeBadge'))
1560
+ : null
1561
+ ),
1562
+ React.createElement('td', { style: { fontFamily: 'monospace' } }, `${p.username || 'root'}@${p.host}`),
1563
+ React.createElement('td', { style: { fontFamily: 'monospace' } }, p.port || 22),
1564
+ React.createElement('td', { style: { fontFamily: 'monospace', fontSize: '12px' } }, p.remoteWorkspace || '—'),
1565
+ React.createElement(
1566
+ 'td',
1567
+ { style: { textAlign: 'right' } },
1568
+ React.createElement(
1569
+ 'div',
1570
+ { style: { display: 'inline-flex', gap: '6px' } },
1571
+ !isAct
1572
+ ? React.createElement(
1573
+ 'button',
1574
+ {
1575
+ type: 'button',
1576
+ className: 'drw-btn',
1577
+ style: { fontSize: '11px', padding: '3px 8px' },
1578
+ onClick: () => handleSetActive(p.id)
1579
+ },
1580
+ t('pinBtn')
1581
+ )
1582
+ : null,
1583
+ React.createElement(
1584
+ 'button',
1585
+ {
1586
+ type: 'button',
1587
+ className: 'drw-btn',
1588
+ style: { fontSize: '11px', padding: '3px 8px' },
1589
+ onClick: () => {
1590
+ setEditing({ ...p })
1591
+ setTestResult(null)
1592
+ }
1593
+ },
1594
+ t('editBtn')
1595
+ ),
1596
+ React.createElement(
1597
+ 'button',
1598
+ {
1599
+ type: 'button',
1600
+ className: 'drw-btn drw-btn-danger',
1601
+ style: { fontSize: '11px', padding: '3px 8px' },
1602
+ onClick: () => handleDelete(p.id, p.name)
1603
+ },
1604
+ t('delBtn')
1605
+ )
1606
+ )
1607
+ )
1608
+ )
1609
+ })
1610
+ )
1611
+ )
1612
+ : !editing
1613
+ ? React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('noHosts'))
1614
+ : null
1615
+ ),
1616
+
1617
+ // Section 2: Active Connection Diagnostics (ClineBot Card 4 style)
1618
+ activeProfile
1619
+ ? React.createElement(
1620
+ 'div',
1621
+ { className: 'drw-card' },
1622
+ React.createElement(
1623
+ 'div',
1624
+ { className: 'drw-card-title' },
1625
+ React.createElement('span', null, t('diagTitle')),
1626
+ React.createElement(
1627
+ 'button',
1628
+ {
1629
+ type: 'button',
1630
+ className: 'drw-btn',
1631
+ style: { fontSize: '11px', padding: '3px 9px' },
1632
+ disabled: busy === 'testing',
1633
+ onClick: () => handleTest(activeProfile)
1634
+ },
1635
+ busy === 'testing' ? t('testing') : t('diagProbeBtn')
1636
+ )
1637
+ ),
1638
+ React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
1639
+ React.createElement(
1640
+ 'div',
1641
+ { className: 'drw-grid-4' },
1642
+ React.createElement(
1643
+ 'div',
1644
+ { className: 'drw-stat-box' },
1645
+ React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
1646
+ React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
1647
+ ),
1648
+ React.createElement(
1649
+ 'div',
1650
+ { className: 'drw-stat-box' },
1651
+ React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
1652
+ React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
1653
+ ),
1654
+ React.createElement(
1655
+ 'div',
1656
+ { className: 'drw-stat-box' },
1657
+ React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
1658
+ React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
1659
+ ),
1660
+ React.createElement(
1661
+ 'div',
1662
+ { className: 'drw-stat-box' },
1663
+ React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
1664
+ React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
1665
+ )
1666
+ )
1667
+ )
1668
+ : null,
1669
+
1670
+ // Section 3: Mirror Sync (ClineBot Action Card style)
1671
+ activeProfile
1672
+ ? React.createElement(
1673
+ 'div',
1674
+ { className: 'drw-card' },
1675
+ React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
1676
+ React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
1677
+
1678
+ React.createElement(
1679
+ 'div',
1680
+ { 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' } },
1681
+ React.createElement(
1682
+ 'div',
1683
+ { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
1684
+ React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, t('autoSyncTitle')),
1685
+ React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('autoSyncDesc'))
1686
+ ),
1687
+ React.createElement(
1688
+ 'button',
1689
+ {
1690
+ type: 'button',
1691
+ className: `drw-btn ${autoSync ? 'drw-btn-primary' : ''}`,
1692
+ style: { padding: '4px 12px', fontSize: '12px' },
1693
+ onClick: async () => {
1694
+ const next = !autoSync
1695
+ try {
1696
+ const res = await fetch('/dsh-remote-workspace/watcher/toggle', {
1697
+ method: 'POST',
1698
+ headers: { 'Content-Type': 'application/json' },
1699
+ body: JSON.stringify({ enabled: next })
1700
+ })
1701
+ const d = await res.json()
1702
+ if (d.ok) setAutoSync(d.autoSync)
1703
+ } catch (_) {}
1704
+ }
1705
+ },
1706
+ autoSync ? '⚡ Active (ON)' : '⚪ Off'
1707
+ )
1708
+ ),
1709
+
1710
+ React.createElement(
1711
+ 'div',
1712
+ { className: 'drw-row' },
1713
+ React.createElement(
1714
+ 'button',
1715
+ {
1716
+ type: 'button',
1717
+ className: 'drw-btn',
1718
+ disabled: !!busy || !activeProfile.remoteWorkspace || !activeProfile.localMirrorPath,
1719
+ onClick: () => handleSync('pull')
1720
+ },
1721
+ busy === 'sync_pull' ? t('pulling') : t('pullBtn')
1722
+ ),
1723
+ React.createElement(
1724
+ 'button',
1725
+ {
1726
+ type: 'button',
1727
+ className: 'drw-btn',
1728
+ disabled: !!busy || !activeProfile.remoteWorkspace || !activeProfile.localMirrorPath,
1729
+ onClick: () => handleSync('push')
1730
+ },
1731
+ busy === 'sync_push' ? t('pushing') : t('pushBtn')
1732
+ )
1733
+ ),
1734
+ syncMsg
1735
+ ? React.createElement(
1736
+ 'div',
1737
+ {
1738
+ className: 'drw-preview',
1739
+ style: {
1740
+ borderColor: syncMsg.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
1741
+ }
1742
+ },
1743
+ syncMsg.text
1744
+ )
1745
+ : null
1746
+ )
1747
+ : null,
1748
+
1749
+ // Section 4: SSH Port Forwarding (Tunnels Manager)
1750
+ React.createElement(
1751
+ 'div',
1752
+ { className: 'drw-card' },
1753
+ React.createElement('div', { className: 'drw-card-title' }, t('tunnelsTitle')),
1754
+ React.createElement('div', { className: 'drw-card-desc' }, t('tunnelsDesc')),
1755
+
1756
+ // Start tunnel inline row
1757
+ activeProfile
1758
+ ? React.createElement(
1759
+ 'div',
1760
+ { className: 'drw-row', style: { alignItems: 'flex-end', background: 'var(--dsw-alias-bg-layer-2)', padding: '10px 12px', borderRadius: '8px' } },
1761
+ React.createElement(
1762
+ 'div',
1763
+ { style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
1764
+ React.createElement('span', { className: 'drw-label' }, t('tLocalPort')),
1765
+ React.createElement('input', {
1766
+ className: 'drw-input',
1767
+ style: { width: '120px' },
1768
+ type: 'number',
1769
+ value: newTunnelLocal,
1770
+ onChange: (e) => setNewTunnelLocal(e.target.value)
1771
+ })
1772
+ ),
1773
+ React.createElement('span', { style: { paddingBottom: '8px', color: 'var(--dsw-alias-label-secondary)' } }, '→'),
1774
+ React.createElement(
1775
+ 'div',
1776
+ { style: { display: 'flex', flexDirection: 'column', gap: '3px' } },
1777
+ React.createElement('span', { className: 'drw-label' }, t('tRemotePort')),
1778
+ React.createElement('input', {
1779
+ className: 'drw-input',
1780
+ style: { width: '120px' },
1781
+ type: 'number',
1782
+ value: newTunnelRemote,
1783
+ onChange: (e) => setNewTunnelRemote(e.target.value)
1784
+ })
1785
+ ),
1786
+ React.createElement(
1787
+ 'button',
1788
+ {
1789
+ type: 'button',
1790
+ className: 'drw-btn drw-btn-primary',
1791
+ disabled: busy === 'tunnel_open',
1792
+ onClick: handleStartTunnel
1793
+ },
1794
+ busy === 'tunnel_open' ? t('tOpening') : t('tOpenBtn')
1795
+ )
1796
+ )
1797
+ : null,
1798
+
1799
+ tunnelMsg
1800
+ ? React.createElement(
1801
+ 'div',
1802
+ {
1803
+ className: 'drw-preview',
1804
+ style: {
1805
+ borderColor: tunnelMsg.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
1806
+ }
1807
+ },
1808
+ tunnelMsg.text
1809
+ )
1810
+ : null,
1811
+
1812
+ // Tunnels Table
1813
+ tunnels.length > 0
1814
+ ? React.createElement(
1815
+ 'table',
1816
+ { className: 'drw-table' },
1817
+ React.createElement(
1818
+ 'thead',
1819
+ null,
1820
+ React.createElement(
1821
+ 'tr',
1822
+ null,
1823
+ React.createElement('th', null, t('tunnelsColId')),
1824
+ React.createElement('th', null, t('tunnelsColRoute')),
1825
+ React.createElement('th', { style: { textAlign: 'right' } }, t('tunnelsColActions'))
1826
+ )
1827
+ ),
1828
+ React.createElement(
1829
+ 'tbody',
1830
+ null,
1831
+ tunnels.map((tun) =>
1832
+ React.createElement(
1833
+ 'tr',
1834
+ { key: tun.id },
1835
+ React.createElement('td', { style: { fontFamily: 'monospace', fontSize: '12px' } }, tun.id),
1836
+ React.createElement(
1837
+ 'td',
1838
+ null,
1839
+ React.createElement('span', { className: 'drw-badge drw-badge-ok' }, `127.0.0.1:${tun.localPort}`),
1840
+ ' → ',
1841
+ React.createElement('span', { className: 'drw-badge' }, `${tun.targetHost || '127.0.0.1'}:${tun.remotePort}`)
1842
+ ),
1843
+ React.createElement(
1844
+ 'td',
1845
+ { style: { textAlign: 'right' } },
1846
+ React.createElement(
1847
+ 'button',
1848
+ {
1849
+ type: 'button',
1850
+ className: 'drw-btn drw-btn-danger',
1851
+ style: { fontSize: '11px', padding: '3px 8px' },
1852
+ onClick: () => handleStopTunnel(tun.id)
1853
+ },
1854
+ t('tStopBtn')
1855
+ )
1856
+ )
1857
+ )
1858
+ )
1859
+ )
1860
+ )
1861
+ : React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('tNoTunnels'))
1862
+ )
1863
+ )
1864
+ }
1865
+
1866
+ // Accordion item for Settings -> Plugins section
1867
+ function PluginCard(props) {
1868
+ const [open, setOpen] = React.useState(false)
1869
+ const t = props.locale === 'ru' ? makeT(ru, en) : makeT(en, ru)
1870
+
1871
+ React.useEffect(() => {
1872
+ ensureCss()
1873
+ }, [])
1874
+
1875
+ return React.createElement(
1876
+ 'li',
1877
+ { className: 'drw-card', style: { listStyle: 'none', marginBottom: '12px' } },
1878
+ React.createElement(
1879
+ 'button',
1880
+ {
1881
+ type: 'button',
1882
+ style: {
1883
+ background: 'none',
1884
+ border: 'none',
1885
+ cursor: 'pointer',
1886
+ display: 'flex',
1887
+ alignItems: 'center',
1888
+ width: '100%',
1889
+ padding: 0,
1890
+ textAlign: 'left',
1891
+ },
1892
+ 'aria-expanded': open,
1893
+ onClick: () => setOpen((v) => !v),
1894
+ },
1895
+ React.createElement('span', { style: { fontSize: '20px', marginRight: '12px' } }, '🌐'),
1896
+ React.createElement(
1897
+ 'div',
1898
+ { style: { flex: 1 } },
1899
+ React.createElement('div', { style: { fontWeight: 600, fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, t('title')),
1900
+ React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
1901
+ ),
1902
+ React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginRight: '14px' } }, t('status')),
1903
+ React.createElement(
1904
+ 'span',
1905
+ { style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
1906
+ React.createElement(FallbackChevron)
1907
+ )
1908
+ ),
1909
+ open
1910
+ ? React.createElement(
1911
+ 'div',
1912
+ { style: { marginTop: '16px', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '16px' } },
1913
+ React.createElement(SettingsView, props)
1914
+ )
1915
+ : null
1916
+ )
1917
+ }
1918
+
1919
+ function RemoteWorkspaceChip(props) {
1920
+ ensureCss()
1921
+ const [activeHost, setActiveHost] = React.useState(null)
1922
+
1923
+ React.useEffect(() => {
1924
+ fetch('/dsh-remote-workspace/state')
1925
+ .then((r) => r.json())
1926
+ .then((d) => {
1927
+ if (d.ok && d.activeId && d.profiles) {
1928
+ const f = d.profiles.find((p) => p.id === d.activeId)
1929
+ if (f) setActiveHost(f.name || f.host)
1930
+ }
1931
+ })
1932
+ .catch(() => {})
1933
+ }, [])
1934
+
1935
+ return React.createElement(
1936
+ 'div',
1937
+ {
1938
+ style: {
1939
+ display: 'flex',
1940
+ alignItems: 'center',
1941
+ gap: '6px',
1942
+ padding: '2px 8px',
1943
+ fontSize: '11px',
1944
+ fontFamily: 'monospace',
1945
+ borderRadius: '999px',
1946
+ border: '1px solid var(--dsw-alias-border-l2)',
1947
+ background: 'var(--dsw-alias-bg-layer-1)',
1948
+ color: activeHost ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
1949
+ cursor: 'pointer'
1950
+ }
1951
+ },
1952
+ React.createElement('span', null, '🌐'),
1953
+ React.createElement('span', null, activeHost ? `SSH: ${activeHost}` : 'SSH: Local')
1954
+ )
1955
+ }
1956
+
1957
+ function apply(ctx) {
1958
+ if (ctx.locale && typeof ctx.locale.register === 'function') {
1959
+ try { ctx.locale.register(NS, { en, ru }) } catch (_) {}
1960
+ }
1961
+
1962
+ if (ctx.slots && typeof ctx.slots.inject === 'function') {
1963
+ ctx.slots.inject('settings.plugin.item', () =>
1964
+ ctx.slots.register(
1965
+ {
1966
+ name: 'settings.plugin.item',
1967
+ key: NS,
1968
+ locale: NS,
1969
+ inject: () => ({ ctx })
1970
+ },
1971
+ (props) => React.createElement(PluginCard, Object.assign({}, props, { ctx }))
1972
+ )
1973
+ )
1974
+
1975
+ ctx.slots.inject('conversation.session.header.utilities', () =>
1976
+ ctx.slots.register(
1977
+ {
1978
+ name: 'conversation.session.header.utilities',
1979
+ id: '@goodandready/dsh-remote-workspace',
1980
+ order: 30
1981
+ },
1982
+ (props) => React.createElement(RemoteWorkspaceChip, props)
1983
+ )
1984
+ )
1985
+ }
1986
+ }
1987
+
1988
+ module.exports = { apply, inject: ['slots', 'locale'] }
1989
+ return module.exports
1990
+ }
1991
+ })