@goodandready/dsh-remote-workspace 0.1.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/LICENSE +21 -0
- package/README.md +238 -0
- package/README.ru.md +238 -0
- package/README.zh.md +238 -0
- package/cordis.patch.yml +3 -0
- package/docs/design/DESIGN.md +62 -0
- package/docs/plans/findings.md +7 -0
- package/docs/plans/progress.md +14 -0
- package/docs/plans/task_plan.md +26 -0
- package/lib/client.js +966 -0
- package/lib/index.js +116 -0
- package/lib/mirror-sync-service.js +180 -0
- package/lib/remote-fs-service.js +117 -0
- package/lib/routes.js +164 -0
- package/lib/ssh-service.js +176 -0
- package/lib/tools.js +138 -0
- package/lib/tunnel-service.js +88 -0
- package/lib/types.js +29 -0
- package/package.json +75 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,966 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@goodandready/dsh-remote-workspace',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} }
|
|
5
|
+
var exports = module.exports
|
|
6
|
+
const React = require('react')
|
|
7
|
+
|
|
8
|
+
const NS = 'dsh-remote-workspace'
|
|
9
|
+
|
|
10
|
+
function ensureCss() {
|
|
11
|
+
if (typeof document === 'undefined') return
|
|
12
|
+
if (document.getElementById('drw-clinebot-style')) return
|
|
13
|
+
const style = document.createElement('style')
|
|
14
|
+
style.id = 'drw-clinebot-style'
|
|
15
|
+
style.dataset.dshPlugin = NS
|
|
16
|
+
style.textContent = `
|
|
17
|
+
.drw-page{display:flex;flex-direction:column;gap:18px;padding:6px 0 20px;max-width:960px}
|
|
18
|
+
.drw-header{display:flex;flex-direction:column;gap:6px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}
|
|
19
|
+
.drw-page-title{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px}
|
|
20
|
+
.drw-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.4}
|
|
21
|
+
|
|
22
|
+
.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:12px;list-style:none}
|
|
23
|
+
.drw-card-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}
|
|
24
|
+
.drw-card-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}
|
|
25
|
+
|
|
26
|
+
.drw-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
|
|
27
|
+
.drw-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:12px}
|
|
28
|
+
|
|
29
|
+
.drw-badge{font-size:11px;padding:3px 8px;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}
|
|
30
|
+
.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)}
|
|
31
|
+
.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)}
|
|
32
|
+
.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)}
|
|
33
|
+
|
|
34
|
+
.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}
|
|
35
|
+
.drw-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}
|
|
36
|
+
.drw-label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary);margin-bottom:2px}
|
|
37
|
+
.drw-field{display:flex;flex-direction:column;gap:3px;flex:1;min-width:180px}
|
|
38
|
+
|
|
39
|
+
.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}
|
|
40
|
+
.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))}
|
|
41
|
+
.drw-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
|
|
42
|
+
.drw-btn-primary:hover:not(:disabled){opacity:0.9}
|
|
43
|
+
.drw-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}
|
|
44
|
+
.drw-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12);border-color:rgba(239,68,68,0.5)}
|
|
45
|
+
|
|
46
|
+
.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}
|
|
47
|
+
.drw-segmented-item{appearance:none;border:none;background:none;cursor:pointer;padding:4px 12px;font-size:12px;font-weight:500;border-radius:6px;color:var(--dsw-alias-label-secondary);transition:all .15s ease}
|
|
48
|
+
.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}
|
|
49
|
+
|
|
50
|
+
.drw-table{width:100%;border-collapse:collapse;margin-top:6px}
|
|
51
|
+
.drw-table th{text-align:left;font-size:12px;color:var(--dsw-alias-label-secondary);padding:6px 8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-weight:600}
|
|
52
|
+
.drw-table td{padding:8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;color:var(--dsw-alias-label-primary)}
|
|
53
|
+
.drw-table tr:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
54
|
+
|
|
55
|
+
.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}
|
|
56
|
+
.drw-stat-val{font-size:15px;font-weight:700;color:var(--dsw-alias-label-primary)}
|
|
57
|
+
.drw-stat-lbl{font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
58
|
+
.drw-preview{padding:10px;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)}
|
|
59
|
+
|
|
60
|
+
.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:260px}
|
|
61
|
+
.drw-browser-list{overflow-y:auto;display:flex;flex-direction:column;gap:2px;max-height:180px;padding-right:4px}
|
|
62
|
+
.drw-browser-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;cursor:pointer;font-size:13px;font-family:monospace}
|
|
63
|
+
.drw-browser-item:hover{background:var(--dsw-alias-bg-layer-3)}
|
|
64
|
+
`
|
|
65
|
+
document.head.appendChild(style)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const en = {
|
|
69
|
+
title: 'Remote Workspace',
|
|
70
|
+
subtitle: 'Manage SSH connections, SFTP sync, and tunnels',
|
|
71
|
+
status: 'Ready',
|
|
72
|
+
activeBadge: 'Active',
|
|
73
|
+
setActiveBtn: 'Set Active',
|
|
74
|
+
pinned: 'Pinned Active Host',
|
|
75
|
+
profilesTitle: '🖥️ Remote SSH Profiles',
|
|
76
|
+
profilesDesc: 'Configure remote hosts for code execution, SFTP file access, and mirror syncing.',
|
|
77
|
+
addBtn: '+ Add Host',
|
|
78
|
+
nameCol: 'Name',
|
|
79
|
+
hostCol: 'Host / User',
|
|
80
|
+
portCol: 'Port',
|
|
81
|
+
pathCol: 'Remote Path',
|
|
82
|
+
actionsCol: 'Actions',
|
|
83
|
+
noHosts: 'No remote hosts configured yet. Click "+ Add Host" to create one.',
|
|
84
|
+
formTitleNew: 'Add New Host Profile',
|
|
85
|
+
formTitleEdit: 'Edit Host Profile',
|
|
86
|
+
fName: 'Profile Name',
|
|
87
|
+
fHost: 'Host / IP Address',
|
|
88
|
+
fPort: 'Port',
|
|
89
|
+
fUser: 'Username',
|
|
90
|
+
fAuthType: 'Authentication Method',
|
|
91
|
+
fAuthKey: 'SSH Private Key',
|
|
92
|
+
fAuthPass: 'Password',
|
|
93
|
+
fKeyPath: 'Key Path',
|
|
94
|
+
fPassphrase: 'Key Passphrase (optional)',
|
|
95
|
+
fPassword: 'Password',
|
|
96
|
+
fRemotePath: 'Remote Project Directory',
|
|
97
|
+
fRemotePathHint: '(Optional now, can be chosen later while working)',
|
|
98
|
+
btnBrowse: '📁 Browse Remote...',
|
|
99
|
+
btnSelectThis: 'Select Current Directory',
|
|
100
|
+
btnTest: '⚡ Test Connection',
|
|
101
|
+
btnSave: 'Save Profile',
|
|
102
|
+
btnCancel: 'Cancel',
|
|
103
|
+
btnDelete: 'Delete',
|
|
104
|
+
testing: 'Testing Connection...',
|
|
105
|
+
saving: 'Saving...',
|
|
106
|
+
diagTitle: '📊 Active Connection Diagnostics',
|
|
107
|
+
diagDesc: 'Live connection probe to the currently selected remote machine.',
|
|
108
|
+
syncTitle: '🔄 Mirror Sync & Port Tunnels',
|
|
109
|
+
syncDesc: 'Synchronize local workspace mirror or check forwarded background port tunnels.',
|
|
110
|
+
pullBtn: '⬇️ Pull from Remote',
|
|
111
|
+
pushBtn: '⬆️ Push to Remote',
|
|
112
|
+
syncSuccess: 'Sync completed successfully',
|
|
113
|
+
syncFailed: 'Sync encountered conflicts or errors: '
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ru = {
|
|
117
|
+
title: 'Удалённое рабочее пространство',
|
|
118
|
+
subtitle: 'Управление подключениями по SSH, синхронизацией SFTP и туннелями',
|
|
119
|
+
status: 'Готово',
|
|
120
|
+
activeBadge: 'Активен',
|
|
121
|
+
setActiveBtn: 'Сделать активным',
|
|
122
|
+
pinned: 'Активный хост',
|
|
123
|
+
profilesTitle: '🖥️ Профили SSH серверов',
|
|
124
|
+
profilesDesc: 'Настройка удалённых хостов для выполнения команд, SFTP доступа и синхронизации.',
|
|
125
|
+
addBtn: '+ Добавить хост',
|
|
126
|
+
nameCol: 'Имя',
|
|
127
|
+
hostCol: 'Хост / Пользователь',
|
|
128
|
+
portCol: 'Порт',
|
|
129
|
+
pathCol: 'Удалённый путь',
|
|
130
|
+
actionsCol: 'Действия',
|
|
131
|
+
noHosts: 'Удалённые хосты ещё не настроены. Нажмите «+ Добавить хост».',
|
|
132
|
+
formTitleNew: 'Добавление нового SSH хоста',
|
|
133
|
+
formTitleEdit: 'Редактирование профиля хоста',
|
|
134
|
+
fName: 'Название подключения',
|
|
135
|
+
fHost: 'Хост / IP-адрес',
|
|
136
|
+
fPort: 'Порт',
|
|
137
|
+
fUser: 'Имя пользователя',
|
|
138
|
+
fAuthType: 'Способ авторизации',
|
|
139
|
+
fAuthKey: 'SSH-ключ',
|
|
140
|
+
fAuthPass: 'Пароль',
|
|
141
|
+
fKeyPath: 'Путь к ключу',
|
|
142
|
+
fPassphrase: 'Кодовая фраза ключа (если есть)',
|
|
143
|
+
fPassword: 'Пароль учетной записи',
|
|
144
|
+
fRemotePath: 'Удалённая папка проекта',
|
|
145
|
+
fRemotePathHint: '(Не обязательно сейчас — можно выбрать позже при работе)',
|
|
146
|
+
btnBrowse: '📁 Выбрать на сервере…',
|
|
147
|
+
btnSelectThis: 'Выбрать эту папку',
|
|
148
|
+
btnTest: '⚡ Проверить связь (Ping)',
|
|
149
|
+
btnSave: 'Сохранить профиль',
|
|
150
|
+
btnCancel: 'Отмена',
|
|
151
|
+
btnDelete: 'Удалить',
|
|
152
|
+
testing: 'Проверка соединения…',
|
|
153
|
+
saving: 'Сохранение…',
|
|
154
|
+
diagTitle: '📊 Диагностика активного подключения',
|
|
155
|
+
diagDesc: 'Проверка доступности и измерение задержки активного сервера.',
|
|
156
|
+
syncTitle: '🔄 Синхронизация зеркала и туннели',
|
|
157
|
+
syncDesc: 'Ручная синхронизация файлов с удалённым сервером и статус проброса портов.',
|
|
158
|
+
pullBtn: '⬇️ Скачать с сервера (Pull)',
|
|
159
|
+
pushBtn: '⬆️ Загрузить на сервер (Push)',
|
|
160
|
+
syncSuccess: 'Синхронизация успешно выполнена',
|
|
161
|
+
syncFailed: 'Ошибка синхронизации: '
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function makeT(dict, fallback) {
|
|
165
|
+
return function t(key) {
|
|
166
|
+
return (dict && dict[key]) || (fallback && fallback[key]) || key
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function FallbackChevron() {
|
|
171
|
+
return React.createElement(
|
|
172
|
+
'svg',
|
|
173
|
+
{ width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': true },
|
|
174
|
+
React.createElement('path', {
|
|
175
|
+
d: 'M3.5 5.25L7 8.75L10.5 5.25',
|
|
176
|
+
stroke: 'currentColor',
|
|
177
|
+
strokeWidth: 1.5,
|
|
178
|
+
strokeLinecap: 'round',
|
|
179
|
+
strokeLinejoin: 'round',
|
|
180
|
+
})
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function SettingsView(props) {
|
|
185
|
+
const t = props.locale === 'ru' ? makeT(ru, en) : makeT(en, ru)
|
|
186
|
+
const [profiles, setProfiles] = React.useState([])
|
|
187
|
+
const [activeId, setActiveId] = React.useState(null)
|
|
188
|
+
const [editing, setEditing] = React.useState(null) // null or object
|
|
189
|
+
const [busy, setBusy] = React.useState(null)
|
|
190
|
+
const [testResult, setTestResult] = React.useState(null)
|
|
191
|
+
const [syncMsg, setSyncMsg] = React.useState(null)
|
|
192
|
+
|
|
193
|
+
// Remote Directory Browser State
|
|
194
|
+
const [browserOpen, setBrowserOpen] = React.useState(false)
|
|
195
|
+
const [browserPath, setBrowserPath] = React.useState('/')
|
|
196
|
+
const [browserEntries, setBrowserEntries] = React.useState([])
|
|
197
|
+
const [browserLoading, setBrowserLoading] = React.useState(false)
|
|
198
|
+
|
|
199
|
+
const loadState = async () => {
|
|
200
|
+
try {
|
|
201
|
+
const res = await fetch('/dsh-remote-workspace/state')
|
|
202
|
+
if (res.ok) {
|
|
203
|
+
const data = await res.json()
|
|
204
|
+
if (data.ok) {
|
|
205
|
+
setProfiles(data.profiles || [])
|
|
206
|
+
setActiveId(data.activeId || null)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch (_) {}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
React.useEffect(() => {
|
|
213
|
+
ensureCss()
|
|
214
|
+
loadState()
|
|
215
|
+
}, [])
|
|
216
|
+
|
|
217
|
+
const handleSave = async (profile) => {
|
|
218
|
+
setBusy('saving')
|
|
219
|
+
try {
|
|
220
|
+
const res = await fetch('/dsh-remote-workspace/profiles/save', {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
headers: { 'Content-Type': 'application/json' },
|
|
223
|
+
body: JSON.stringify(profile)
|
|
224
|
+
})
|
|
225
|
+
if (res.ok) {
|
|
226
|
+
setEditing(null)
|
|
227
|
+
setBrowserOpen(false)
|
|
228
|
+
await loadState()
|
|
229
|
+
}
|
|
230
|
+
} catch (_) {}
|
|
231
|
+
setBusy(null)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const handleDelete = async (id) => {
|
|
235
|
+
try {
|
|
236
|
+
const res = await fetch('/dsh-remote-workspace/profiles/delete', {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: { 'Content-Type': 'application/json' },
|
|
239
|
+
body: JSON.stringify({ id })
|
|
240
|
+
})
|
|
241
|
+
if (res.ok) {
|
|
242
|
+
if (editing && editing.id === id) setEditing(null)
|
|
243
|
+
await loadState()
|
|
244
|
+
}
|
|
245
|
+
} catch (_) {}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const handleSetActive = async (id) => {
|
|
249
|
+
try {
|
|
250
|
+
const res = await fetch('/dsh-remote-workspace/profiles/active', {
|
|
251
|
+
method: 'POST',
|
|
252
|
+
headers: { 'Content-Type': 'application/json' },
|
|
253
|
+
body: JSON.stringify({ id })
|
|
254
|
+
})
|
|
255
|
+
if (res.ok) {
|
|
256
|
+
await loadState()
|
|
257
|
+
}
|
|
258
|
+
} catch (_) {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const handleTest = async (prof) => {
|
|
262
|
+
setBusy('testing')
|
|
263
|
+
setTestResult(null)
|
|
264
|
+
try {
|
|
265
|
+
const res = await fetch('/dsh-remote-workspace/test', {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
268
|
+
body: JSON.stringify({ profile: prof })
|
|
269
|
+
})
|
|
270
|
+
const data = await res.json()
|
|
271
|
+
setTestResult(data)
|
|
272
|
+
} catch (err) {
|
|
273
|
+
setTestResult({ ok: false, error: err.message })
|
|
274
|
+
}
|
|
275
|
+
setBusy(null)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const openDirectoryBrowser = async (initialPath) => {
|
|
279
|
+
setBrowserOpen(true)
|
|
280
|
+
const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
|
|
281
|
+
await fetchRemoteDir(target)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const fetchRemoteDir = async (dirPath) => {
|
|
285
|
+
setBrowserLoading(true)
|
|
286
|
+
try {
|
|
287
|
+
const res = await fetch('/dsh-remote-workspace/browse', {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: { 'Content-Type': 'application/json' },
|
|
290
|
+
body: JSON.stringify({ profile: editing, remotePath: dirPath })
|
|
291
|
+
})
|
|
292
|
+
const data = await res.json()
|
|
293
|
+
if (data.ok) {
|
|
294
|
+
setBrowserPath(data.currentPath || dirPath)
|
|
295
|
+
setBrowserEntries(data.entries || [])
|
|
296
|
+
}
|
|
297
|
+
} catch (_) {}
|
|
298
|
+
setBrowserLoading(false)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const handleSync = async (direction) => {
|
|
302
|
+
setBusy(`sync_${direction}`)
|
|
303
|
+
setSyncMsg(null)
|
|
304
|
+
try {
|
|
305
|
+
const res = await fetch('/dsh-remote-workspace/sync', {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
headers: { 'Content-Type': 'application/json' },
|
|
308
|
+
body: JSON.stringify({ profileId: activeId, direction })
|
|
309
|
+
})
|
|
310
|
+
const data = await res.json()
|
|
311
|
+
if (data.ok) {
|
|
312
|
+
setSyncMsg({ ok: true, text: t('syncSuccess') })
|
|
313
|
+
} else {
|
|
314
|
+
setSyncMsg({ ok: false, text: t('syncFailed') + (data.error || 'Conflicts detected') })
|
|
315
|
+
}
|
|
316
|
+
} catch (err) {
|
|
317
|
+
setSyncMsg({ ok: false, text: err.message })
|
|
318
|
+
}
|
|
319
|
+
setBusy(null)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const activeProfile = profiles.find((p) => p.id === activeId)
|
|
323
|
+
|
|
324
|
+
return React.createElement(
|
|
325
|
+
'div',
|
|
326
|
+
{ className: 'drw-page' },
|
|
327
|
+
|
|
328
|
+
// Section 1: Profiles Management (ClineBot style)
|
|
329
|
+
React.createElement(
|
|
330
|
+
'div',
|
|
331
|
+
{ className: 'drw-card' },
|
|
332
|
+
React.createElement(
|
|
333
|
+
'div',
|
|
334
|
+
{ className: 'drw-card-title' },
|
|
335
|
+
React.createElement('span', null, t('profilesTitle')),
|
|
336
|
+
!editing
|
|
337
|
+
? React.createElement(
|
|
338
|
+
'button',
|
|
339
|
+
{
|
|
340
|
+
type: 'button',
|
|
341
|
+
className: 'drw-btn drw-btn-primary',
|
|
342
|
+
onClick: () =>
|
|
343
|
+
setEditing({
|
|
344
|
+
id: 'prof_' + Math.random().toString(36).substr(2, 9),
|
|
345
|
+
name: 'New Server',
|
|
346
|
+
host: '',
|
|
347
|
+
port: 22,
|
|
348
|
+
username: 'root',
|
|
349
|
+
authType: 'key',
|
|
350
|
+
privateKeyPath: '~/.ssh/id_rsa',
|
|
351
|
+
passphrase: '',
|
|
352
|
+
password: '',
|
|
353
|
+
remoteWorkspace: '',
|
|
354
|
+
localMirrorPath: ''
|
|
355
|
+
})
|
|
356
|
+
},
|
|
357
|
+
t('addBtn')
|
|
358
|
+
)
|
|
359
|
+
: null
|
|
360
|
+
),
|
|
361
|
+
React.createElement('div', { className: 'drw-card-desc' }, t('profilesDesc')),
|
|
362
|
+
|
|
363
|
+
// Editing Form
|
|
364
|
+
editing
|
|
365
|
+
? React.createElement(
|
|
366
|
+
'div',
|
|
367
|
+
{
|
|
368
|
+
style: {
|
|
369
|
+
padding: '14px',
|
|
370
|
+
borderRadius: '8px',
|
|
371
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
372
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
373
|
+
display: 'flex',
|
|
374
|
+
flexDirection: 'column',
|
|
375
|
+
gap: '12px'
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
React.createElement('div', { style: { fontWeight: 600, fontSize: '14px' } }, editing.name ? t('formTitleEdit') : t('formTitleNew')),
|
|
379
|
+
React.createElement(
|
|
380
|
+
'div',
|
|
381
|
+
{ className: 'drw-grid-2' },
|
|
382
|
+
React.createElement(
|
|
383
|
+
'div',
|
|
384
|
+
{ className: 'drw-field' },
|
|
385
|
+
React.createElement('span', { className: 'drw-label' }, t('fName')),
|
|
386
|
+
React.createElement('input', {
|
|
387
|
+
className: 'drw-input',
|
|
388
|
+
value: editing.name || '',
|
|
389
|
+
onChange: (e) => setEditing({ ...editing, name: e.target.value })
|
|
390
|
+
})
|
|
391
|
+
),
|
|
392
|
+
React.createElement(
|
|
393
|
+
'div',
|
|
394
|
+
{ className: 'drw-field' },
|
|
395
|
+
React.createElement('span', { className: 'drw-label' }, t('fHost')),
|
|
396
|
+
React.createElement('input', {
|
|
397
|
+
className: 'drw-input',
|
|
398
|
+
placeholder: '192.168.1.100 or domain.com',
|
|
399
|
+
value: editing.host || '',
|
|
400
|
+
onChange: (e) => setEditing({ ...editing, host: e.target.value })
|
|
401
|
+
})
|
|
402
|
+
),
|
|
403
|
+
React.createElement(
|
|
404
|
+
'div',
|
|
405
|
+
{ className: 'drw-field' },
|
|
406
|
+
React.createElement('span', { className: 'drw-label' }, t('fPort')),
|
|
407
|
+
React.createElement('input', {
|
|
408
|
+
className: 'drw-input',
|
|
409
|
+
type: 'number',
|
|
410
|
+
value: editing.port || 22,
|
|
411
|
+
onChange: (e) => setEditing({ ...editing, port: parseInt(e.target.value, 10) || 22 })
|
|
412
|
+
})
|
|
413
|
+
),
|
|
414
|
+
React.createElement(
|
|
415
|
+
'div',
|
|
416
|
+
{ className: 'drw-field' },
|
|
417
|
+
React.createElement('span', { className: 'drw-label' }, t('fUser')),
|
|
418
|
+
React.createElement('input', {
|
|
419
|
+
className: 'drw-input',
|
|
420
|
+
value: editing.username || '',
|
|
421
|
+
onChange: (e) => setEditing({ ...editing, username: e.target.value })
|
|
422
|
+
})
|
|
423
|
+
)
|
|
424
|
+
),
|
|
425
|
+
|
|
426
|
+
// Auth Method Segmented Switcher (Key vs Password)
|
|
427
|
+
React.createElement(
|
|
428
|
+
'div',
|
|
429
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
|
|
430
|
+
React.createElement('span', { className: 'drw-label' }, t('fAuthType')),
|
|
431
|
+
React.createElement(
|
|
432
|
+
'div',
|
|
433
|
+
{ className: 'drw-segmented' },
|
|
434
|
+
React.createElement(
|
|
435
|
+
'button',
|
|
436
|
+
{
|
|
437
|
+
type: 'button',
|
|
438
|
+
className: `drw-segmented-item ${editing.authType !== 'password' ? 'drw-segmented-item-active' : ''}`,
|
|
439
|
+
onClick: () => setEditing({ ...editing, authType: 'key' })
|
|
440
|
+
},
|
|
441
|
+
'🔑 ' + t('fAuthKey')
|
|
442
|
+
),
|
|
443
|
+
React.createElement(
|
|
444
|
+
'button',
|
|
445
|
+
{
|
|
446
|
+
type: 'button',
|
|
447
|
+
className: `drw-segmented-item ${editing.authType === 'password' ? 'drw-segmented-item-active' : ''}`,
|
|
448
|
+
onClick: () => setEditing({ ...editing, authType: 'password' })
|
|
449
|
+
},
|
|
450
|
+
'🔒 ' + t('fAuthPass')
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
),
|
|
454
|
+
|
|
455
|
+
// Conditional Auth Inputs
|
|
456
|
+
editing.authType === 'password'
|
|
457
|
+
? React.createElement(
|
|
458
|
+
'div',
|
|
459
|
+
{ className: 'drw-field' },
|
|
460
|
+
React.createElement('span', { className: 'drw-label' }, t('fPassword')),
|
|
461
|
+
React.createElement('input', {
|
|
462
|
+
className: 'drw-input',
|
|
463
|
+
type: 'password',
|
|
464
|
+
placeholder: '••••••••••••',
|
|
465
|
+
value: editing.password || '',
|
|
466
|
+
onChange: (e) => setEditing({ ...editing, password: e.target.value })
|
|
467
|
+
})
|
|
468
|
+
)
|
|
469
|
+
: React.createElement(
|
|
470
|
+
'div',
|
|
471
|
+
{ className: 'drw-grid-2' },
|
|
472
|
+
React.createElement(
|
|
473
|
+
'div',
|
|
474
|
+
{ className: 'drw-field' },
|
|
475
|
+
React.createElement('span', { className: 'drw-label' }, t('fKeyPath')),
|
|
476
|
+
React.createElement('input', {
|
|
477
|
+
className: 'drw-input',
|
|
478
|
+
placeholder: '~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
|
|
479
|
+
value: editing.privateKeyPath || '',
|
|
480
|
+
onChange: (e) => setEditing({ ...editing, privateKeyPath: e.target.value })
|
|
481
|
+
})
|
|
482
|
+
),
|
|
483
|
+
React.createElement(
|
|
484
|
+
'div',
|
|
485
|
+
{ className: 'drw-field' },
|
|
486
|
+
React.createElement('span', { className: 'drw-label' }, t('fPassphrase')),
|
|
487
|
+
React.createElement('input', {
|
|
488
|
+
className: 'drw-input',
|
|
489
|
+
type: 'password',
|
|
490
|
+
placeholder: 'Optional passphrase',
|
|
491
|
+
value: editing.passphrase || '',
|
|
492
|
+
onChange: (e) => setEditing({ ...editing, passphrase: e.target.value })
|
|
493
|
+
})
|
|
494
|
+
)
|
|
495
|
+
),
|
|
496
|
+
|
|
497
|
+
// Remote Project Directory (Optional, with live server browse)
|
|
498
|
+
React.createElement(
|
|
499
|
+
'div',
|
|
500
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
501
|
+
React.createElement(
|
|
502
|
+
'div',
|
|
503
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
504
|
+
React.createElement('span', { className: 'drw-label' }, t('fRemotePath')),
|
|
505
|
+
React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)' } }, t('fRemotePathHint'))
|
|
506
|
+
),
|
|
507
|
+
React.createElement(
|
|
508
|
+
'div',
|
|
509
|
+
{ style: { display: 'flex', gap: '8px' } },
|
|
510
|
+
React.createElement('input', {
|
|
511
|
+
className: 'drw-input',
|
|
512
|
+
placeholder: '/var/www/my-project or ~/projects/app (can leave empty for now)',
|
|
513
|
+
value: editing.remoteWorkspace || '',
|
|
514
|
+
onChange: (e) => setEditing({ ...editing, remoteWorkspace: e.target.value })
|
|
515
|
+
}),
|
|
516
|
+
React.createElement(
|
|
517
|
+
'button',
|
|
518
|
+
{
|
|
519
|
+
type: 'button',
|
|
520
|
+
className: 'drw-btn',
|
|
521
|
+
disabled: !editing.host,
|
|
522
|
+
onClick: () => openDirectoryBrowser(editing.remoteWorkspace || '/')
|
|
523
|
+
},
|
|
524
|
+
t('btnBrowse')
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
),
|
|
528
|
+
|
|
529
|
+
// Remote Directory Browser Modal/Inline Tree
|
|
530
|
+
browserOpen
|
|
531
|
+
? React.createElement(
|
|
532
|
+
'div',
|
|
533
|
+
{ className: 'drw-browser-modal' },
|
|
534
|
+
React.createElement(
|
|
535
|
+
'div',
|
|
536
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
537
|
+
React.createElement('span', { style: { fontSize: '12px', fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, `📂 ${browserPath}`),
|
|
538
|
+
React.createElement(
|
|
539
|
+
'div',
|
|
540
|
+
{ style: { display: 'flex', gap: '6px' } },
|
|
541
|
+
React.createElement(
|
|
542
|
+
'button',
|
|
543
|
+
{
|
|
544
|
+
type: 'button',
|
|
545
|
+
className: 'drw-btn drw-btn-primary',
|
|
546
|
+
style: { fontSize: '11px', padding: '3px 8px' },
|
|
547
|
+
onClick: () => {
|
|
548
|
+
setEditing({ ...editing, remoteWorkspace: browserPath })
|
|
549
|
+
setBrowserOpen(false)
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
t('btnSelectThis')
|
|
553
|
+
),
|
|
554
|
+
React.createElement(
|
|
555
|
+
'button',
|
|
556
|
+
{
|
|
557
|
+
type: 'button',
|
|
558
|
+
className: 'drw-btn',
|
|
559
|
+
style: { fontSize: '11px', padding: '3px 8px' },
|
|
560
|
+
onClick: () => setBrowserOpen(false)
|
|
561
|
+
},
|
|
562
|
+
'✕'
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
),
|
|
566
|
+
React.createElement(
|
|
567
|
+
'div',
|
|
568
|
+
{ className: 'drw-browser-list' },
|
|
569
|
+
browserLoading
|
|
570
|
+
? React.createElement('div', { style: { padding: '8px', fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } }, 'Loading directory entries...')
|
|
571
|
+
: React.createElement(
|
|
572
|
+
React.Fragment,
|
|
573
|
+
null,
|
|
574
|
+
browserPath !== '/'
|
|
575
|
+
? React.createElement(
|
|
576
|
+
'div',
|
|
577
|
+
{
|
|
578
|
+
className: 'drw-browser-item',
|
|
579
|
+
onClick: () => {
|
|
580
|
+
const parent = browserPath.substring(0, browserPath.lastIndexOf('/')) || '/'
|
|
581
|
+
fetchRemoteDir(parent)
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
'📁 .. (parent directory)'
|
|
585
|
+
)
|
|
586
|
+
: null,
|
|
587
|
+
browserEntries
|
|
588
|
+
.filter((e) => e.isDirectory)
|
|
589
|
+
.map((e) =>
|
|
590
|
+
React.createElement(
|
|
591
|
+
'div',
|
|
592
|
+
{
|
|
593
|
+
key: e.filename,
|
|
594
|
+
className: 'drw-browser-item',
|
|
595
|
+
onClick: () => {
|
|
596
|
+
const next = browserPath === '/' ? `/${e.filename}` : `${browserPath}/${e.filename}`
|
|
597
|
+
fetchRemoteDir(next)
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
`📁 ${e.filename}`
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
: null,
|
|
607
|
+
|
|
608
|
+
// Form Action Buttons
|
|
609
|
+
React.createElement(
|
|
610
|
+
'div',
|
|
611
|
+
{ className: 'drw-row', style: { justifyContent: 'flex-end', marginTop: '6px' } },
|
|
612
|
+
React.createElement(
|
|
613
|
+
'button',
|
|
614
|
+
{
|
|
615
|
+
type: 'button',
|
|
616
|
+
className: 'drw-btn',
|
|
617
|
+
disabled: !editing.host || !!busy,
|
|
618
|
+
onClick: () => handleTest(editing)
|
|
619
|
+
},
|
|
620
|
+
busy === 'testing' ? t('testing') : t('btnTest')
|
|
621
|
+
),
|
|
622
|
+
React.createElement(
|
|
623
|
+
'button',
|
|
624
|
+
{
|
|
625
|
+
type: 'button',
|
|
626
|
+
className: 'drw-btn',
|
|
627
|
+
onClick: () => {
|
|
628
|
+
setEditing(null)
|
|
629
|
+
setBrowserOpen(false)
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
t('btnCancel')
|
|
633
|
+
),
|
|
634
|
+
React.createElement(
|
|
635
|
+
'button',
|
|
636
|
+
{
|
|
637
|
+
type: 'button',
|
|
638
|
+
className: 'drw-btn drw-btn-primary',
|
|
639
|
+
disabled: !editing.host || !!busy,
|
|
640
|
+
onClick: () => handleSave(editing)
|
|
641
|
+
},
|
|
642
|
+
busy === 'saving' ? t('saving') : t('btnSave')
|
|
643
|
+
)
|
|
644
|
+
),
|
|
645
|
+
testResult
|
|
646
|
+
? React.createElement(
|
|
647
|
+
'div',
|
|
648
|
+
{
|
|
649
|
+
className: 'drw-preview',
|
|
650
|
+
style: {
|
|
651
|
+
borderColor: testResult.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
testResult.ok
|
|
655
|
+
? `✅ Connection OK! Latency: ${testResult.latencyMs} ms | OS: ${testResult.remoteOs || 'Unknown'}`
|
|
656
|
+
: `❌ Connection Failed: ${testResult.error || 'Connection timed out'}`
|
|
657
|
+
)
|
|
658
|
+
: null
|
|
659
|
+
)
|
|
660
|
+
: null,
|
|
661
|
+
|
|
662
|
+
// Profiles Table
|
|
663
|
+
profiles.length > 0
|
|
664
|
+
? React.createElement(
|
|
665
|
+
'table',
|
|
666
|
+
{ className: 'drw-table' },
|
|
667
|
+
React.createElement(
|
|
668
|
+
'thead',
|
|
669
|
+
null,
|
|
670
|
+
React.createElement(
|
|
671
|
+
'tr',
|
|
672
|
+
null,
|
|
673
|
+
React.createElement('th', null, t('nameCol')),
|
|
674
|
+
React.createElement('th', null, t('hostCol')),
|
|
675
|
+
React.createElement('th', null, t('portCol')),
|
|
676
|
+
React.createElement('th', null, t('pathCol')),
|
|
677
|
+
React.createElement('th', { style: { textAlign: 'right' } }, t('actionsCol'))
|
|
678
|
+
)
|
|
679
|
+
),
|
|
680
|
+
React.createElement(
|
|
681
|
+
'tbody',
|
|
682
|
+
null,
|
|
683
|
+
profiles.map((p) => {
|
|
684
|
+
const isAct = p.id === activeId
|
|
685
|
+
return React.createElement(
|
|
686
|
+
'tr',
|
|
687
|
+
{ key: p.id },
|
|
688
|
+
React.createElement(
|
|
689
|
+
'td',
|
|
690
|
+
null,
|
|
691
|
+
React.createElement('span', { style: { fontWeight: 600 } }, p.name),
|
|
692
|
+
React.createElement(
|
|
693
|
+
'span',
|
|
694
|
+
{ className: 'drw-badge', style: { marginLeft: '8px' } },
|
|
695
|
+
p.authType === 'password' ? '🔒 pwd' : '🔑 key'
|
|
696
|
+
),
|
|
697
|
+
isAct
|
|
698
|
+
? React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginLeft: '6px' } }, t('activeBadge'))
|
|
699
|
+
: null
|
|
700
|
+
),
|
|
701
|
+
React.createElement('td', null, `${p.username}@${p.host}`),
|
|
702
|
+
React.createElement('td', null, p.port || 22),
|
|
703
|
+
React.createElement('td', null, p.remoteWorkspace || '— (not set)'),
|
|
704
|
+
React.createElement(
|
|
705
|
+
'td',
|
|
706
|
+
{ style: { textAlign: 'right' } },
|
|
707
|
+
React.createElement(
|
|
708
|
+
'div',
|
|
709
|
+
{ className: 'drw-row', style: { justifyContent: 'flex-end' } },
|
|
710
|
+
!isAct
|
|
711
|
+
? React.createElement(
|
|
712
|
+
'button',
|
|
713
|
+
{
|
|
714
|
+
type: 'button',
|
|
715
|
+
className: 'drw-btn',
|
|
716
|
+
style: { fontSize: '11px', padding: '3px 8px' },
|
|
717
|
+
onClick: () => handleSetActive(p.id)
|
|
718
|
+
},
|
|
719
|
+
t('setActiveBtn')
|
|
720
|
+
)
|
|
721
|
+
: null,
|
|
722
|
+
React.createElement(
|
|
723
|
+
'button',
|
|
724
|
+
{
|
|
725
|
+
type: 'button',
|
|
726
|
+
className: 'drw-btn',
|
|
727
|
+
style: { fontSize: '11px', padding: '3px 8px' },
|
|
728
|
+
onClick: () => {
|
|
729
|
+
setEditing({ ...p })
|
|
730
|
+
setTestResult(null)
|
|
731
|
+
setBrowserOpen(false)
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
'✏️'
|
|
735
|
+
),
|
|
736
|
+
React.createElement(
|
|
737
|
+
'button',
|
|
738
|
+
{
|
|
739
|
+
type: 'button',
|
|
740
|
+
className: 'drw-btn drw-btn-danger',
|
|
741
|
+
style: { fontSize: '11px', padding: '3px 8px' },
|
|
742
|
+
onClick: () => handleDelete(p.id)
|
|
743
|
+
},
|
|
744
|
+
'🗑️'
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
)
|
|
748
|
+
)
|
|
749
|
+
})
|
|
750
|
+
)
|
|
751
|
+
)
|
|
752
|
+
: !editing
|
|
753
|
+
? React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('noHosts'))
|
|
754
|
+
: null
|
|
755
|
+
),
|
|
756
|
+
|
|
757
|
+
// Section 2: Active Connection Diagnostics (ClineBot Card 4 style)
|
|
758
|
+
activeProfile
|
|
759
|
+
? React.createElement(
|
|
760
|
+
'div',
|
|
761
|
+
{ className: 'drw-card' },
|
|
762
|
+
React.createElement('div', { className: 'drw-card-title' }, t('diagTitle')),
|
|
763
|
+
React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
|
|
764
|
+
React.createElement(
|
|
765
|
+
'div',
|
|
766
|
+
{ className: 'drw-grid-2' },
|
|
767
|
+
React.createElement(
|
|
768
|
+
'div',
|
|
769
|
+
{ className: 'drw-stat-box' },
|
|
770
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name),
|
|
771
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'Host: ' + activeProfile.host)
|
|
772
|
+
),
|
|
773
|
+
React.createElement(
|
|
774
|
+
'div',
|
|
775
|
+
{ className: 'drw-stat-box' },
|
|
776
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '— (select during work)'),
|
|
777
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace Directory')
|
|
778
|
+
),
|
|
779
|
+
React.createElement(
|
|
780
|
+
'div',
|
|
781
|
+
{ className: 'drw-stat-box' },
|
|
782
|
+
React.createElement('div', { className: 'drw-stat-val' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth'),
|
|
783
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'User: ' + activeProfile.username)
|
|
784
|
+
),
|
|
785
|
+
React.createElement(
|
|
786
|
+
'div',
|
|
787
|
+
{ className: 'drw-stat-box' },
|
|
788
|
+
React.createElement('div', { className: 'drw-stat-val' }, 'Ready'),
|
|
789
|
+
React.createElement('div', { className: 'drw-stat-lbl' }, 'DSH Remote Engine Status')
|
|
790
|
+
)
|
|
791
|
+
)
|
|
792
|
+
)
|
|
793
|
+
: null,
|
|
794
|
+
|
|
795
|
+
// Section 3: Mirror Sync (ClineBot Action Card style)
|
|
796
|
+
activeProfile && activeProfile.remoteWorkspace
|
|
797
|
+
? React.createElement(
|
|
798
|
+
'div',
|
|
799
|
+
{ className: 'drw-card' },
|
|
800
|
+
React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
|
|
801
|
+
React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
|
|
802
|
+
React.createElement(
|
|
803
|
+
'div',
|
|
804
|
+
{ className: 'drw-row' },
|
|
805
|
+
React.createElement(
|
|
806
|
+
'button',
|
|
807
|
+
{
|
|
808
|
+
type: 'button',
|
|
809
|
+
className: 'drw-btn',
|
|
810
|
+
disabled: !!busy,
|
|
811
|
+
onClick: () => handleSync('pull')
|
|
812
|
+
},
|
|
813
|
+
busy === 'sync_pull' ? 'Pulling...' : t('pullBtn')
|
|
814
|
+
),
|
|
815
|
+
React.createElement(
|
|
816
|
+
'button',
|
|
817
|
+
{
|
|
818
|
+
type: 'button',
|
|
819
|
+
className: 'drw-btn',
|
|
820
|
+
disabled: !!busy,
|
|
821
|
+
onClick: () => handleSync('push')
|
|
822
|
+
},
|
|
823
|
+
busy === 'sync_push' ? 'Pushing...' : t('pushBtn')
|
|
824
|
+
)
|
|
825
|
+
),
|
|
826
|
+
syncMsg
|
|
827
|
+
? React.createElement(
|
|
828
|
+
'div',
|
|
829
|
+
{
|
|
830
|
+
className: 'drw-preview',
|
|
831
|
+
style: {
|
|
832
|
+
borderColor: syncMsg.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)'
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
syncMsg.text
|
|
836
|
+
)
|
|
837
|
+
: null
|
|
838
|
+
)
|
|
839
|
+
: null
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Accordion item for Plugins section
|
|
844
|
+
function PluginCard(props) {
|
|
845
|
+
const [open, setOpen] = React.useState(false)
|
|
846
|
+
const t = props.locale === 'ru' ? makeT(ru, en) : makeT(en, ru)
|
|
847
|
+
|
|
848
|
+
React.useEffect(() => {
|
|
849
|
+
ensureCss()
|
|
850
|
+
}, [])
|
|
851
|
+
|
|
852
|
+
return React.createElement(
|
|
853
|
+
'li',
|
|
854
|
+
{ className: 'drw-card', style: { listStyle: 'none', marginBottom: '12px' } },
|
|
855
|
+
React.createElement(
|
|
856
|
+
'button',
|
|
857
|
+
{
|
|
858
|
+
type: 'button',
|
|
859
|
+
style: {
|
|
860
|
+
background: 'none',
|
|
861
|
+
border: 'none',
|
|
862
|
+
cursor: 'pointer',
|
|
863
|
+
display: 'flex',
|
|
864
|
+
alignItems: 'center',
|
|
865
|
+
width: '100%',
|
|
866
|
+
padding: 0,
|
|
867
|
+
textAlign: 'left',
|
|
868
|
+
},
|
|
869
|
+
'aria-expanded': open,
|
|
870
|
+
onClick: () => setOpen((v) => !v),
|
|
871
|
+
},
|
|
872
|
+
React.createElement('span', { style: { fontSize: '20px', marginRight: '12px' } }, '🌐'),
|
|
873
|
+
React.createElement(
|
|
874
|
+
'div',
|
|
875
|
+
{ style: { flex: 1 } },
|
|
876
|
+
React.createElement('div', { style: { fontWeight: 600, fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, t('title')),
|
|
877
|
+
React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
|
|
878
|
+
),
|
|
879
|
+
React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginRight: '14px' } }, t('status')),
|
|
880
|
+
React.createElement(
|
|
881
|
+
'span',
|
|
882
|
+
{ style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
|
|
883
|
+
React.createElement(FallbackChevron)
|
|
884
|
+
)
|
|
885
|
+
),
|
|
886
|
+
open
|
|
887
|
+
? React.createElement('div', { style: { marginTop: '16px', borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: '16px' } },
|
|
888
|
+
React.createElement(SettingsView, props)
|
|
889
|
+
)
|
|
890
|
+
: null
|
|
891
|
+
)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function RemoteWorkspaceChip(props) {
|
|
895
|
+
ensureCss()
|
|
896
|
+
const [activeHost, setActiveHost] = React.useState(null)
|
|
897
|
+
|
|
898
|
+
React.useEffect(() => {
|
|
899
|
+
fetch('/dsh-remote-workspace/state')
|
|
900
|
+
.then((r) => r.json())
|
|
901
|
+
.then((d) => {
|
|
902
|
+
if (d.ok && d.activeId && d.profiles) {
|
|
903
|
+
const f = d.profiles.find((p) => p.id === d.activeId)
|
|
904
|
+
if (f) setActiveHost(f.name || f.host)
|
|
905
|
+
}
|
|
906
|
+
})
|
|
907
|
+
.catch(() => {})
|
|
908
|
+
}, [])
|
|
909
|
+
|
|
910
|
+
return React.createElement(
|
|
911
|
+
'div',
|
|
912
|
+
{
|
|
913
|
+
style: {
|
|
914
|
+
display: 'flex',
|
|
915
|
+
alignItems: 'center',
|
|
916
|
+
gap: '6px',
|
|
917
|
+
padding: '2px 8px',
|
|
918
|
+
fontSize: '11px',
|
|
919
|
+
fontFamily: 'monospace',
|
|
920
|
+
borderRadius: '999px',
|
|
921
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
922
|
+
background: 'var(--dsw-alias-bg-layer-1)',
|
|
923
|
+
color: activeHost ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
924
|
+
cursor: 'pointer'
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
React.createElement('span', null, '🌐'),
|
|
928
|
+
React.createElement('span', null, activeHost ? `SSH: ${activeHost}` : 'SSH: Local')
|
|
929
|
+
)
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function apply(ctx) {
|
|
933
|
+
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
934
|
+
try { ctx.locale.register(NS, { en, ru }) } catch (_) {}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (ctx.slots && typeof ctx.slots.inject === 'function') {
|
|
938
|
+
ctx.slots.inject('settings.plugin.item', () =>
|
|
939
|
+
ctx.slots.register(
|
|
940
|
+
{
|
|
941
|
+
name: 'settings.plugin.item',
|
|
942
|
+
key: NS,
|
|
943
|
+
locale: NS,
|
|
944
|
+
inject: () => ({ ctx })
|
|
945
|
+
},
|
|
946
|
+
(props) => React.createElement(PluginCard, Object.assign({}, props, { ctx }))
|
|
947
|
+
)
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
ctx.slots.inject('conversation.session.header.utilities', () =>
|
|
951
|
+
ctx.slots.register(
|
|
952
|
+
{
|
|
953
|
+
name: 'conversation.session.header.utilities',
|
|
954
|
+
id: '@goodandready/dsh-remote-workspace',
|
|
955
|
+
order: 30
|
|
956
|
+
},
|
|
957
|
+
(props) => React.createElement(RemoteWorkspaceChip, props)
|
|
958
|
+
)
|
|
959
|
+
)
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
|
|
964
|
+
return module.exports
|
|
965
|
+
}
|
|
966
|
+
})
|