dsh-remote-workspaces 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/src/client.js ADDED
@@ -0,0 +1,756 @@
1
+ /**
2
+ * Browser half of the SSH remote workspace plugin:
3
+ *
4
+ * 1. A "远程工作区" settings section — a multi-machine SSH registry (add /
5
+ * edit / delete / test) with `~/.ssh/config` as a one-click form-fill
6
+ * convenience. NO remote browsing here.
7
+ *
8
+ * 2. A composed directory-flow picker registered into the harness's two
9
+ * workspace-add holes (`conversation.hero.workspace.directoryFlow` and
10
+ * `sidebar.workspaces.directoryFlow`) at a lower priority so it shadows the
11
+ * native chooser and offers BOTH "本地文件夹" (delegates to the host
12
+ * chooser) and "远程目录" (pick a machine → browse the remote → open it as
13
+ * a remote workspace → hand the anchor path back through `onPicked`).
14
+ *
15
+ * Served verbatim as a classic script, so it self-registers through
16
+ * `window.__ModuleLoader__` in factory form (no ESM import/export); `react` is
17
+ * a platform seed word resolved via `require("react")`.
18
+ */
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Remote contract (must match src/index.js). Parameters carry strict codecs
22
+ // with a pass-through `parse` (the client `$mount` face rejects `src-json`).
23
+ // ---------------------------------------------------------------------------
24
+ var PACKAGE = 'dsh-remote-workspaces'
25
+ var NAMESPACE = 'remoteWorkspaces'
26
+
27
+ var JSON_CODEC = Object.freeze({
28
+ mode: 'strict',
29
+ typeSymbol: 'JsonValue',
30
+ schema: Object.freeze({
31
+ parse: function (value) { return value },
32
+ }),
33
+ })
34
+
35
+ function jsonParameter(name) {
36
+ return { name: name, wire: name, source: 'json', codec: JSON_CODEC }
37
+ }
38
+
39
+ function invocation(method, parameters) {
40
+ return {
41
+ id: NAMESPACE + '/' + method,
42
+ service: NAMESPACE,
43
+ namespace: NAMESPACE,
44
+ method: method,
45
+ invocation: { kind: 'direct' },
46
+ parameters: parameters || [],
47
+ result: JSON_CODEC,
48
+ }
49
+ }
50
+
51
+ var INVOCATIONS = [
52
+ invocation('listMachines'),
53
+ invocation('saveMachine', [jsonParameter('machine')]),
54
+ invocation('deleteMachine', [jsonParameter('id')]),
55
+ invocation('listSshAliases'),
56
+ invocation('sshAliasDetail', [jsonParameter('alias')]),
57
+ invocation('testConnection', [jsonParameter('machine')]),
58
+ invocation('listRemoteDir', [jsonParameter('machine'), jsonParameter('path')]),
59
+ invocation('openRemoteWorkspace', [jsonParameter('machine'), jsonParameter('path')]),
60
+ ]
61
+
62
+ function unwrapRemote(res) {
63
+ if (res === undefined || res === null) return { ok: false, error: '无响应' }
64
+ if (res.ok === false) {
65
+ var e = res.error
66
+ return { ok: false, error: e && e.message ? e.message : '调用失败' }
67
+ }
68
+ return res.value || { ok: false, error: '空结果' }
69
+ }
70
+
71
+ window.__ModuleLoader__.load({
72
+ id: PACKAGE,
73
+ factory: (require) => {
74
+ var module = { exports: {} }
75
+ var exports = module.exports
76
+ var React = require('react')
77
+
78
+ var sectionStyle = { padding: 16, fontSize: 14, lineHeight: 1.6, maxWidth: 820 }
79
+ var labelStyle = { color: 'var(--dsw-alias-label-secondary, #888)', margin: 0, fontSize: 12.5 }
80
+ var monoStyle = { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', fontSize: 12.5 }
81
+ var btnStyle = { padding: '5px 12px', cursor: 'pointer' }
82
+ var inputStyle = { padding: '5px 8px', fontSize: 13, width: '100%', boxSizing: 'border-box' }
83
+ var dangerColor = 'var(--dsw-alias-danger, #c00)'
84
+ var successColor = 'var(--dsw-alias-success, #0a0)'
85
+ var borderColor = 'var(--dsw-alias-border, #333)'
86
+ var chipStyle = { display: 'inline-block', padding: '1px 8px', borderRadius: 10, background: 'rgba(127,127,127,0.18)', fontSize: 11.5, lineHeight: '18px' }
87
+
88
+ function pathBasename(p) {
89
+ if (!p) return ''
90
+ var s = String(p).replace(/[\\/]+$/, '')
91
+ var idx = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'))
92
+ return idx >= 0 ? s.slice(idx + 1) : s
93
+ }
94
+
95
+ // =========================================================================
96
+ // Settings section: machine registry only (no remote browsing).
97
+ // =========================================================================
98
+ function SshWorkspaceSection(props) {
99
+ var getRemote = props.getRemote
100
+ var mount = props.mount
101
+
102
+ var machinesState = React.useState([])
103
+ var machines = machinesState[0]
104
+ var setMachines = machinesState[1]
105
+ var loadingState = React.useState(true)
106
+ var loading = loadingState[0]
107
+ var setLoading = loadingState[1]
108
+ var errorState = React.useState(null)
109
+ var error = errorState[0]
110
+ var setError = errorState[1]
111
+ var remoteState = React.useState(null)
112
+ var remote = remoteState[0]
113
+ var setRemote = remoteState[1]
114
+ var mountErrorState = React.useState(null)
115
+ var mountError = mountErrorState[0]
116
+ var setMountError = mountErrorState[1]
117
+ var formState = React.useState(null)
118
+ var form = formState[0]
119
+ var setForm = formState[1]
120
+ var aliasesState = React.useState(null)
121
+ var aliases = aliasesState[0]
122
+ var setAliases = aliasesState[1]
123
+ var showAliasesState = React.useState(false)
124
+ var showAliases = showAliasesState[0]
125
+ var setShowAliases = showAliasesState[1]
126
+ var resultsState = React.useState({})
127
+ var results = resultsState[0]
128
+ var setResults = resultsState[1]
129
+ var expandedHostsState = React.useState({})
130
+ var expandedHosts = expandedHostsState[0]
131
+ var setExpandedHosts = expandedHostsState[1]
132
+ var deleteTargetState = React.useState(null)
133
+ var deleteTarget = deleteTargetState[0]
134
+ var setDeleteTarget = deleteTargetState[1]
135
+ var deletingState = React.useState(false)
136
+ var deleting = deletingState[0]
137
+ var setDeleting = deletingState[1]
138
+
139
+ React.useEffect(function () {
140
+ var alive = true
141
+ mount.then(
142
+ function () {
143
+ if (!alive) return
144
+ var ns = getRemote()
145
+ setRemote(ns)
146
+ ns.listMachines().then(
147
+ function (res) {
148
+ var b = unwrapRemote(res)
149
+ if (!alive) return
150
+ if (b.ok) setMachines(b.machines || [])
151
+ else setError(b.error || '加载主机失败')
152
+ setLoading(false)
153
+ },
154
+ function (err) {
155
+ if (!alive) return
156
+ setError(err && err.message ? err.message : String(err))
157
+ setLoading(false)
158
+ },
159
+ )
160
+ },
161
+ function (err) { if (alive) setMountError(err && err.message ? err.message : String(err)) },
162
+ )
163
+ return function () { alive = false }
164
+ }, [])
165
+
166
+ function refreshMachines() {
167
+ setLoading(true)
168
+ setError(null)
169
+ remote.listMachines().then(
170
+ function (res) {
171
+ var b = unwrapRemote(res)
172
+ setLoading(false)
173
+ if (b.ok) setMachines(b.machines || [])
174
+ else setError(b.error || '加载主机失败')
175
+ },
176
+ function (err) {
177
+ setLoading(false)
178
+ setError(err && err.message ? err.message : String(err))
179
+ },
180
+ )
181
+ }
182
+
183
+ function openNew() {
184
+ setForm({ isNew: true, machine: { id: undefined, alias: '', host: '', port: '', user: '', identityFile: '', hasPassword: false } })
185
+ }
186
+ function openEdit(machine) { setForm({ isNew: false, machine: machine }) }
187
+ function closeForm() { setForm(null) }
188
+
189
+ function doSave(values) {
190
+ var payload = {
191
+ id: form.isNew ? undefined : form.machine.id,
192
+ alias: values.alias,
193
+ host: values.host,
194
+ port: values.port,
195
+ user: values.user,
196
+ identityFile: values.identityFile,
197
+ password: values.password === '' ? undefined : values.password,
198
+ passphrase: values.passphrase === '' ? undefined : values.passphrase,
199
+ }
200
+ remote.saveMachine(payload).then(
201
+ function (res) {
202
+ var b = unwrapRemote(res)
203
+ if (b.ok) { closeForm(); refreshMachines() }
204
+ else setError(b.error || '保存失败')
205
+ },
206
+ function (err) { setError(err && err.message ? err.message : String(err)) },
207
+ )
208
+ }
209
+
210
+ function doDelete(machine) {
211
+ setDeleteTarget(machine)
212
+ }
213
+
214
+ function closeDelete() {
215
+ if (!deleting) setDeleteTarget(null)
216
+ }
217
+
218
+ function confirmDelete() {
219
+ if (!remote || !deleteTarget || deleting) return
220
+ setDeleting(true)
221
+ var machine = deleteTarget
222
+ remote.deleteMachine(machine.id).then(
223
+ function () {
224
+ setDeleting(false)
225
+ setDeleteTarget(null)
226
+ refreshMachines()
227
+ },
228
+ function (err) {
229
+ setDeleting(false)
230
+ setError(err && err.message ? err.message : String(err))
231
+ },
232
+ )
233
+ }
234
+
235
+ function doTest(machine) {
236
+ var key = machine.id
237
+ setResults(function (prev) { var next = Object.assign({}, prev); next[key] = { testing: true }; return next })
238
+ remote.testConnection(machine).then(
239
+ function (res) {
240
+ setResults(function (prev) { var next = Object.assign({}, prev); next[key] = unwrapRemote(res); return next })
241
+ },
242
+ function (err) {
243
+ setResults(function (prev) {
244
+ var next = Object.assign({}, prev)
245
+ next[key] = { ok: false, error: err && err.message ? err.message : String(err) }
246
+ return next
247
+ })
248
+ },
249
+ )
250
+ }
251
+
252
+ function toggleAliases() {
253
+ var next = !showAliases
254
+ setShowAliases(next)
255
+ if (next && aliases === null) {
256
+ remote.listSshAliases().then(
257
+ function (res) {
258
+ var b = unwrapRemote(res)
259
+ if (b.ok) setAliases(b.aliases || [])
260
+ else setAliases([])
261
+ },
262
+ function () { setAliases([]) },
263
+ )
264
+ }
265
+ }
266
+
267
+ function fillFromAlias(alias) {
268
+ remote.sshAliasDetail(alias).then(
269
+ function (res) {
270
+ var b = unwrapRemote(res)
271
+ if (b.ok) setForm({ isNew: true, machine: b.machine })
272
+ else setError(b.error || '读取别名失败')
273
+ },
274
+ function (err) { setError(err && err.message ? err.message : String(err)) },
275
+ )
276
+ }
277
+
278
+ function toggleHost(id) {
279
+ setExpandedHosts(function (prev) {
280
+ var n = Object.assign({}, prev)
281
+ n[id] = prev[id] === false // default open → first click collapses
282
+ return n
283
+ })
284
+ }
285
+
286
+ return React.createElement(
287
+ 'div',
288
+ { style: sectionStyle },
289
+ React.createElement('div', { style: { fontWeight: 600, fontSize: 16, marginBottom: 8 } }, '远程工作区'),
290
+ React.createElement('p', { style: { margin: '0 0 12px' } },
291
+ '管理 SSH 主机与已打开的远程工作区。添加远程工作区请在侧边栏「添加工作区」里选「远程目录」。'),
292
+ mountError !== null
293
+ ? React.createElement('p', { style: { color: dangerColor, margin: '0 0 12px' } }, 'Remote 命名空间挂载失败:' + mountError)
294
+ : null,
295
+ error !== null
296
+ ? React.createElement('p', { style: { color: dangerColor, margin: '0 0 12px' } }, error)
297
+ : null,
298
+ React.createElement('div', { style: { display: 'flex', gap: 8, marginBottom: 12 } },
299
+ React.createElement('button', { type: 'button', onClick: openNew, disabled: !remote, style: btnStyle }, '添加主机'),
300
+ React.createElement('button', { type: 'button', onClick: toggleAliases, disabled: !remote, style: btnStyle }, '从 ~/.ssh/config 导入'),
301
+ ),
302
+ showAliases
303
+ ? React.createElement('div', { style: { border: '1px solid ' + borderColor, borderRadius: 6, padding: 10, marginBottom: 12 } },
304
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: 6 } }, '选择要填充到表单的别名'),
305
+ aliases === null
306
+ ? React.createElement('div', { style: labelStyle }, '读取中…')
307
+ : aliases.length === 0
308
+ ? React.createElement('div', { style: labelStyle }, '未找到 ~/.ssh/config 或其中没有 Host 别名')
309
+ : React.createElement('div', {},
310
+ aliases.map(function (alias) {
311
+ return React.createElement('span', { key: alias, onClick: function () { fillFromAlias(alias) }, style: Object.assign({}, monoStyle, { display: 'inline-block', padding: '3px 8px', margin: '0 6px 6px 0', border: '1px solid ' + borderColor, borderRadius: 4, cursor: 'pointer' }) }, alias)
312
+ }),
313
+ ),
314
+ )
315
+ : null,
316
+ form !== null
317
+ ? React.createElement(FormPanel, { key: (form.machine.alias || '') + '|' + (form.machine.host || '') + '|' + (form.machine.id || 'new'), form: form, onSave: doSave, onCancel: closeForm })
318
+ : null,
319
+ loading
320
+ ? React.createElement('div', { style: labelStyle }, '加载中…')
321
+ : machines.length === 0
322
+ ? React.createElement('div', { style: labelStyle }, '还没有主机,点「添加主机」配置一台。')
323
+ : React.createElement('div', { style: { marginTop: 4 } },
324
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 } },
325
+ React.createElement('div', { style: { fontWeight: 600 } }, '主机(' + machines.length + ')'),
326
+ ),
327
+ machines.map(function (machine) {
328
+ var open = expandedHosts[machine.id] !== false
329
+ return MachineRow(
330
+ machine,
331
+ results[machine.id],
332
+ open,
333
+ function () { toggleHost(machine.id) },
334
+ function () { doTest(machine) },
335
+ function () { openEdit(machine) },
336
+ function () { doDelete(machine) },
337
+ )
338
+ }),
339
+ ),
340
+ deleteTarget !== null
341
+ ? React.createElement('div', { style: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000 } },
342
+ React.createElement('div', { style: { background: 'var(--dsw-alias-bg, #1c1c1c)', border: '1px solid ' + borderColor, borderRadius: 8, padding: 16, width: 440, maxWidth: '90vw', color: 'var(--dsw-alias-text, #ddd)' } },
343
+ React.createElement('div', { style: { fontWeight: 600, fontSize: 15, marginBottom: 10 } }, '删除主机'),
344
+ React.createElement('p', { style: { margin: '0 0 12px', lineHeight: 1.6 } },
345
+ '确定删除主机「' + (deleteTarget.alias || deleteTarget.host) + '」吗?(远端数据不受影响)'),
346
+ React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } },
347
+ React.createElement('button', { type: 'button', onClick: closeDelete, disabled: deleting, style: btnStyle }, '取消'),
348
+ React.createElement('button', { type: 'button', onClick: confirmDelete, disabled: deleting, style: Object.assign({}, btnStyle, { background: dangerColor, color: '#fff', borderColor: dangerColor }) }, deleting ? '删除中…' : '删除'),
349
+ ),
350
+ ),
351
+ )
352
+ : null,
353
+ )
354
+ }
355
+
356
+ function MachineRow(machine, result, open, onToggle, onTest, onEdit, onDelete) {
357
+ var summary = [machine.alias || '(未命名)']
358
+ if (machine.host) summary.push(machine.host)
359
+ if (machine.user) summary.push('@' + machine.user)
360
+ if (machine.port) summary.push(':' + machine.port)
361
+ return React.createElement(
362
+ 'div',
363
+ { style: { borderTop: '1px solid ' + borderColor, padding: '8px 0' } },
364
+ React.createElement('div', { onClick: onToggle, style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' } },
365
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary, #888)', width: 16, flexShrink: 0 } }, open ? '▾' : '▸'),
366
+ React.createElement('span', { style: Object.assign({}, monoStyle, { fontWeight: 600, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }) }, summary.join(' ')),
367
+ React.createElement('span', { style: { flex: 1 } }),
368
+ React.createElement('button', { type: 'button', onClick: function (e) { e.stopPropagation(); onTest() }, style: btnStyle }, '测试连接'),
369
+ React.createElement('button', { type: 'button', onClick: function (e) { e.stopPropagation(); onEdit() }, style: btnStyle }, '编辑'),
370
+ React.createElement('button', { type: 'button', onClick: function (e) { e.stopPropagation(); onDelete() }, style: btnStyle }, '删除'),
371
+ ),
372
+ React.createElement('div', { style: { display: 'flex', gap: 6, marginTop: 4, flexWrap: 'wrap', paddingLeft: 24 } },
373
+ machine.identityFile
374
+ ? React.createElement('span', { title: '私钥:' + machine.identityFile, style: chipStyle }, '私钥 ' + pathBasename(machine.identityFile))
375
+ : null,
376
+ machine.hasPassword
377
+ ? React.createElement('span', { title: '登录密码已保存在本地', style: chipStyle }, '密码已保存')
378
+ : null,
379
+ machine.hasPassphrase
380
+ ? React.createElement('span', { title: '私钥口令已保存在本地,连接时会自动使用', style: chipStyle }, '口令已保存')
381
+ : null,
382
+ ),
383
+ result && result.testing
384
+ ? React.createElement('div', { style: Object.assign({}, labelStyle, { paddingLeft: 24, marginTop: 2 }) }, '测试中…')
385
+ : result && result.ok === true
386
+ ? React.createElement('div', { style: { color: successColor, margin: 0, paddingLeft: 24, marginTop: 2 } }, '已连接(' + result.ms + 'ms)')
387
+ : result
388
+ ? React.createElement('div', { style: { color: dangerColor, margin: 0, paddingLeft: 24, marginTop: 2 } }, result.error || '连接失败')
389
+ : null,
390
+ )
391
+ }
392
+
393
+ function FormPanel(props) {
394
+ var initial = props.form.machine || {}
395
+ var valuesState = React.useState({
396
+ alias: initial.alias || '',
397
+ host: initial.host || '',
398
+ port: initial.port || '',
399
+ user: initial.user || '',
400
+ identityFile: initial.identityFile || '',
401
+ password: '',
402
+ passphrase: '',
403
+ })
404
+ var values = valuesState[0]
405
+ var setValues = valuesState[1]
406
+
407
+ function set(field) {
408
+ return function (e) {
409
+ var next = Object.assign({}, values)
410
+ next[field] = e.target.value
411
+ setValues(next)
412
+ }
413
+ }
414
+
415
+ function submit(e) {
416
+ e.preventDefault()
417
+ props.onSave(values)
418
+ }
419
+
420
+ function field(label, name, type, placeholder) {
421
+ return React.createElement('div', { style: { marginBottom: 8 } },
422
+ React.createElement('div', { style: labelStyle }, label),
423
+ React.createElement('input', { type: type || 'text', value: values[name], onChange: set(name), placeholder: placeholder || '', style: inputStyle }),
424
+ )
425
+ }
426
+
427
+ return React.createElement(
428
+ 'form',
429
+ { onSubmit: submit, style: { border: '1px solid ' + borderColor, borderRadius: 6, padding: 12, marginBottom: 12 } },
430
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, props.form.isNew ? '添加主机' : '编辑主机'),
431
+ field('别名(alias)', 'alias', 'text', '如 dev'),
432
+ field('主机地址(host)', 'host', 'text', '如 192.168.1.10 或 example.com'),
433
+ field('端口(port)', 'port', 'number', '默认 22'),
434
+ field('用户名(user)', 'user', 'text', '默认当前用户'),
435
+ field('私钥路径(identityFile)', 'identityFile', 'text', '如 ~/.ssh/id_rsa,留空用默认密钥'),
436
+ field('密码(password)', 'password', 'password', props.form.isNew ? '可选' : '留空保持不变,输入则替换'),
437
+ field('私钥口令(passphrase)', 'passphrase', 'password', props.form.isNew ? '可选' : '留空保持不变'),
438
+ React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 4 } },
439
+ React.createElement('button', { type: 'submit', style: btnStyle }, '保存'),
440
+ React.createElement('button', { type: 'button', onClick: props.onCancel, style: btnStyle }, '取消'),
441
+ ),
442
+ )
443
+ }
444
+
445
+ // =========================================================================
446
+ // Composed directory-flow picker (workspace add): local + remote.
447
+ // =========================================================================
448
+ function joinPosix(base, name) {
449
+ if (base === '' || base === undefined || base === null) return name
450
+ if (base === '/') return '/' + name
451
+ return base.replace(/\/+$/, '') + '/' + name
452
+ }
453
+
454
+ function parentPosix(path) {
455
+ if (path === '' || path === undefined || path === null || path === '/') return '/'
456
+ var s = path.replace(/\/+$/, '')
457
+ if (s === '') return '/'
458
+ var idx = s.lastIndexOf('/')
459
+ return idx <= 0 ? '/' : s.slice(0, idx)
460
+ }
461
+
462
+ // One remote directory/file row: directories render first with a folder
463
+ // icon and are clickable; files render below with a muted style.
464
+ function EntryRow(props) {
465
+ var entry = props.entry
466
+ var onOpen = props.onOpen
467
+ var hoverState = React.useState(false)
468
+ var hover = hoverState[0]
469
+ var setHover = hoverState[1]
470
+ var rowStyle = {
471
+ display: 'flex',
472
+ alignItems: 'center',
473
+ gap: 8,
474
+ padding: '4px 8px',
475
+ borderRadius: 4,
476
+ cursor: entry.dir ? 'pointer' : 'default',
477
+ background: hover ? 'rgba(127,127,127,0.16)' : 'transparent',
478
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
479
+ fontSize: 13,
480
+ color: entry.dir ? 'var(--dsw-alias-text, #ddd)' : 'var(--dsw-alias-label-secondary, #888)',
481
+ }
482
+ return React.createElement(
483
+ 'div',
484
+ {
485
+ style: rowStyle,
486
+ onClick: entry.dir ? function () { onOpen(entry.name) } : undefined,
487
+ onMouseEnter: function () { setHover(true) },
488
+ onMouseLeave: function () { setHover(false) },
489
+ title: entry.dir ? entry.name + '/' : entry.name,
490
+ },
491
+ React.createElement('span', { style: { width: 18, textAlign: 'center', flexShrink: 0 } }, entry.dir ? '📁' : '📄'),
492
+ React.createElement('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, entry.name),
493
+ entry.dir ? React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary, #888)', flexShrink: 0 } }, '/') : null,
494
+ )
495
+ }
496
+
497
+ function RemoteDirectoryFlow(props) {
498
+ var open = props.open
499
+ var busy = props.busy
500
+ var onPicked = props.onPicked
501
+ var onCancel = props.onCancel
502
+ var onError = props.onError
503
+ var getRemote = props.getRemote
504
+ var pickLocal = props.pickLocal
505
+
506
+ var modeState = React.useState('remote')
507
+ var mode = modeState[0]
508
+ var setMode = modeState[1]
509
+ var machinesState = React.useState([])
510
+ var machines = machinesState[0]
511
+ var setMachines = machinesState[1]
512
+ var machinesLoadedState = React.useState(false)
513
+ var machinesLoaded = machinesLoadedState[0]
514
+ var setMachinesLoaded = machinesLoadedState[1]
515
+ var selectedState = React.useState(null)
516
+ var selected = selectedState[0]
517
+ var setSelected = selectedState[1]
518
+ var browseState = React.useState(null)
519
+ var browse = browseState[0]
520
+ var setBrowse = browseState[1]
521
+ var openingState = React.useState(false)
522
+ var opening = openingState[0]
523
+ var setOpening = openingState[1]
524
+ var localPickingState = React.useState(false)
525
+ var localPicking = localPickingState[0]
526
+ var setLocalPicking = localPickingState[1]
527
+ var pathInputState = React.useState('')
528
+ var pathInput = pathInputState[0]
529
+ var setPathInput = pathInputState[1]
530
+
531
+ // Reset per open edge, then load machines.
532
+ React.useEffect(function () {
533
+ if (!open) return
534
+ setMode('remote')
535
+ setMachines([])
536
+ setMachinesLoaded(false)
537
+ setSelected(null)
538
+ setBrowse(null)
539
+ setOpening(false)
540
+ setLocalPicking(false)
541
+ var ns = getRemote()
542
+ if (!ns) return
543
+ ns.listMachines().then(
544
+ function (res) {
545
+ var b = unwrapRemote(res)
546
+ if (b.ok) setMachines(b.machines || [])
547
+ setMachinesLoaded(true)
548
+ },
549
+ function () { setMachinesLoaded(true) },
550
+ )
551
+ }, [open])
552
+
553
+ // Keep the path input in sync with the browsed directory (but not while
554
+ // the user is editing it).
555
+ React.useEffect(function () {
556
+ if (browse && browse.path !== undefined && browse.path !== null) setPathInput(browse.path)
557
+ else setPathInput('')
558
+ }, [browse && browse.path])
559
+
560
+ function pickMachine(machine) {
561
+ setSelected(machine)
562
+ setBrowse(null)
563
+ loadEntries(machine, '')
564
+ }
565
+
566
+ function loadEntries(machine, path) {
567
+ setBrowse({ machine: machine, path: path, loading: true, error: null, entries: [] })
568
+ getRemote().listRemoteDir(machine, path).then(
569
+ function (res) {
570
+ var b = unwrapRemote(res)
571
+ setBrowse(function (prev) {
572
+ if (!prev || prev.machine !== machine) return prev
573
+ if (b.ok) {
574
+ // The host returns the resolved absolute path (home expanded),
575
+ // so "上一级" can walk past home all the way up to `/`.
576
+ var resolved = b.path !== undefined && b.path !== null && b.path !== '' ? b.path : path
577
+ return { machine: machine, path: resolved, loading: false, error: null, entries: b.entries || [] }
578
+ }
579
+ return { machine: machine, path: path, loading: false, error: b.error || '列出目录失败', entries: [] }
580
+ })
581
+ },
582
+ function (err) {
583
+ setBrowse(function (prev) {
584
+ if (!prev || prev.machine !== machine) return prev
585
+ return { machine: machine, path: path, loading: false, error: err && err.message ? err.message : String(err), entries: [] }
586
+ })
587
+ },
588
+ )
589
+ }
590
+
591
+ function navigate(name) {
592
+ loadEntries(browse.machine, joinPosix(browse.path, name))
593
+ }
594
+ function goUp() {
595
+ if (!browse || !browse.path) return
596
+ var parent = parentPosix(browse.path)
597
+ if (parent === browse.path) return
598
+ loadEntries(browse.machine, parent)
599
+ }
600
+ function jumpTo() {
601
+ if (!selected) return
602
+ loadEntries(selected, pathInput)
603
+ }
604
+
605
+ function doLocal() {
606
+ if (localPicking) return
607
+ setLocalPicking(true)
608
+ pickLocal().then(
609
+ function (path) {
610
+ if (path === null || path === undefined || path === '') onCancel()
611
+ else onPicked(path)
612
+ },
613
+ function (err) {
614
+ onError(err && err.message ? err.message : String(err))
615
+ },
616
+ )
617
+ }
618
+
619
+ function doOpenRemote() {
620
+ if (opening || !selected || !browse) return
621
+ setOpening(true)
622
+ getRemote().openRemoteWorkspace(selected, browse.path).then(
623
+ function (res) {
624
+ var b = unwrapRemote(res)
625
+ if (b.ok && b.localDir) onPicked(b.localDir)
626
+ else onError(b.error || '打开远程工作区失败')
627
+ },
628
+ function (err) { onError(err && err.message ? err.message : String(err)) },
629
+ )
630
+ }
631
+
632
+ if (!open) return null
633
+
634
+ return React.createElement(
635
+ 'div',
636
+ { style: { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10000 } },
637
+ React.createElement(
638
+ 'div',
639
+ { style: { background: 'var(--dsw-alias-bg, #1c1c1c)', border: '1px solid ' + borderColor, borderRadius: 8, padding: 16, width: 560, maxWidth: '92vw', maxHeight: '82vh', overflow: 'auto', color: 'var(--dsw-alias-text, #ddd)' } },
640
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', marginBottom: 12 } },
641
+ React.createElement('span', { style: { fontWeight: 600, fontSize: 15 } }, '打开文件夹'),
642
+ React.createElement('span', { style: { flex: 1 } }),
643
+ React.createElement('button', { type: 'button', onClick: onCancel, style: btnStyle }, '取消'),
644
+ ),
645
+ React.createElement('div', { style: { display: 'flex', gap: 8, marginBottom: 12 } },
646
+ React.createElement('button', { type: 'button', onClick: function () { setMode('local') }, style: Object.assign({}, btnStyle, mode === 'local' ? { border: '2px solid ' + successColor } : {}) }, '本地文件夹'),
647
+ React.createElement('button', { type: 'button', onClick: function () { setMode('remote') }, style: Object.assign({}, btnStyle, mode === 'remote' ? { border: '2px solid ' + successColor } : {}) }, '远程目录'),
648
+ ),
649
+ mode === 'local'
650
+ ? React.createElement('div', {},
651
+ React.createElement('p', { style: { margin: '0 0 12px' } }, '在本机打开系统文件夹选择器,选取一个本地目录作为工作区。'),
652
+ React.createElement('button', { type: 'button', onClick: doLocal, disabled: localPicking || busy, style: btnStyle }, localPicking ? '等待选择…' : '选择本地文件夹'),
653
+ )
654
+ : React.createElement('div', {},
655
+ React.createElement('div', { style: { marginBottom: 8 } }, '选择 SSH 主机(在「设置 → 远程工作区」中配置):'),
656
+ machinesLoaded && machines.length === 0
657
+ ? React.createElement('div', { style: { color: dangerColor, margin: '0 0 8px' } }, '还没有配置主机,请先到「设置 → 远程工作区」添加。')
658
+ : React.createElement('select', {
659
+ value: selected ? selected.id : '',
660
+ onChange: function (e) {
661
+ var id = e.target.value
662
+ var m = machines.find(function (x) { return x.id === id })
663
+ if (m) pickMachine(m)
664
+ },
665
+ style: Object.assign({}, inputStyle, { marginBottom: 8 }),
666
+ },
667
+ React.createElement('option', { value: '' }, '选择主机…'),
668
+ machines.map(function (m) {
669
+ return React.createElement('option', { key: m.id, value: m.id }, (m.alias || m.host) + ' (' + m.host + (m.user ? '@' + m.user : '') + ')')
670
+ }),
671
+ ),
672
+ selected
673
+ ? React.createElement('div', { style: { border: '1px solid ' + borderColor, borderRadius: 6, padding: 10 } },
674
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 } },
675
+ React.createElement('input', {
676
+ type: 'text',
677
+ value: pathInput,
678
+ onChange: function (e) { setPathInput(e.target.value) },
679
+ onKeyDown: function (e) { if (e.key === 'Enter') jumpTo() },
680
+ placeholder: '输入绝对路径,或 ~ 回到主目录',
681
+ spellCheck: false,
682
+ style: Object.assign({}, monoStyle, { flex: 1, minWidth: 0, padding: '5px 8px', fontSize: 13, boxSizing: 'border-box' }),
683
+ }),
684
+ React.createElement('button', { type: 'button', onClick: jumpTo, disabled: !browse || browse.loading, style: btnStyle }, '跳转'),
685
+ React.createElement('button', { type: 'button', onClick: goUp, disabled: !browse || browse.loading || !browse.path || browse.path === '/', style: btnStyle }, '上一级'),
686
+ ),
687
+ browse && browse.loading
688
+ ? React.createElement('div', { style: labelStyle }, '加载中…')
689
+ : browse && browse.error
690
+ ? React.createElement('div', { style: { color: dangerColor, margin: 0 } }, browse.error)
691
+ : browse
692
+ ? React.createElement('div', { style: { maxHeight: 220, overflow: 'auto', border: '1px solid ' + borderColor, borderRadius: 6 } },
693
+ (browse.entries || []).slice().sort(function (a, b) {
694
+ if (a.dir !== b.dir) return a.dir ? -1 : 1
695
+ var an = a.name.toLowerCase()
696
+ var bn = b.name.toLowerCase()
697
+ return an < bn ? -1 : an > bn ? 1 : 0
698
+ }).map(function (e) {
699
+ return React.createElement(EntryRow, { key: (e.dir ? 'd:' : 'f:') + e.name, entry: e, onOpen: navigate })
700
+ }),
701
+ )
702
+ : null,
703
+ React.createElement('div', { style: { marginTop: 10 } },
704
+ React.createElement('button', { type: 'button', onClick: doOpenRemote, disabled: opening || busy || (browse && browse.loading), style: btnStyle },
705
+ opening ? '打开中…' : '打开此目录'),
706
+ ),
707
+ )
708
+ : null,
709
+ ),
710
+ ),
711
+ )
712
+ }
713
+
714
+ exports.inject = ['slots', 'remote', 'uiWorkspace']
715
+
716
+ exports.apply = function apply(ctx) {
717
+ var mount = ctx.remote.$mount({ package: PACKAGE, descriptors: INVOCATIONS })
718
+ var getRemote = function () { return ctx.get('remote.' + NAMESPACE) }
719
+
720
+ // Settings section: machines + open remote workspaces (grouped by host).
721
+ ctx.slots.inject('settings.section', function () {
722
+ return ctx.slots.register(
723
+ { name: 'settings.section', id: 'dsh-remote-workspaces', order: 100, label: '远程工作区' },
724
+ function () {
725
+ return React.createElement(SshWorkspaceSection, {
726
+ mount: mount,
727
+ getRemote: getRemote,
728
+ })
729
+ },
730
+ )
731
+ })
732
+
733
+ // Composed workspace-add picker (shadows the native chooser at a lower priority).
734
+ var flowInjected = function () {
735
+ return {
736
+ getRemote: getRemote,
737
+ pickLocal: function () { return ctx.uiWorkspace.pickDirectory() },
738
+ }
739
+ }
740
+ ctx.slots.inject('conversation.hero.workspace.directoryFlow', function () {
741
+ return ctx.slots.inject('sidebar.workspaces.directoryFlow', function* () {
742
+ yield ctx.slots.register(
743
+ { name: 'conversation.hero.workspace.directoryFlow', inject: flowInjected, priority: -1 },
744
+ RemoteDirectoryFlow,
745
+ )
746
+ yield ctx.slots.register(
747
+ { name: 'sidebar.workspaces.directoryFlow', inject: flowInjected, priority: -1 },
748
+ RemoteDirectoryFlow,
749
+ )
750
+ })
751
+ })
752
+ }
753
+
754
+ return module.exports
755
+ },
756
+ })