dsh-remote 0.3.1 → 0.3.2

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.
Files changed (2) hide show
  1. package/lib/client.js +238 -248
  2. package/package.json +5 -2
package/lib/client.js CHANGED
@@ -1,249 +1,239 @@
1
- // dsh-remote — client half.
2
- // Settings → 远程工作区: enter remote host/login (password or key path), connect,
3
- // browse the remote filesystem, and pick a directory as the active remote workspace.
4
- // All data flows through same-origin JSON routes on the harness `webServer`
5
- // (/dsh-remote/status, /connect, /ls, /workspace). The host half owns the SSH pool,
6
- // so no credentials live in the browser beyond what the form posts to loopback.
7
- //
8
- // Client entries must be classic scripts registered via window.__ModuleLoader__.load
9
- // ({ id, factory }); the factory receives a synchronous `require`.
10
- window.__ModuleLoader__.load({
11
- id: 'dsh-remote',
12
- factory: (require) => {
13
- var module = { exports: {} }
14
- var exports = module.exports
15
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
16
-
17
- const React = require('react')
18
- const name = 'dsh-remote'
19
-
20
- async function api(method, path, body) {
21
- const opts = { method, headers: {} }
22
- if (body) {
23
- opts.headers['Content-Type'] = 'application/json'
24
- opts.body = JSON.stringify(body)
25
- }
26
- const res = await fetch(path, opts)
27
- const data = await res.json().catch(() => ({}))
28
- if (!res.ok) throw new Error((data && (data.error || data.message)) || 'HTTP ' + res.status)
29
- return data
30
- }
31
-
32
- function RemoteWorkspacePage() {
33
- const [status, setStatus] = React.useState(null)
34
- const [form, setForm] = React.useState({ host: '', port: '22', username: '', password: '', privateKeyPath: '' })
35
- const [busy, setBusy] = React.useState(false)
36
- const [browse, setBrowse] = React.useState({ path: '', out: '' })
37
- const [error, setError] = React.useState('')
38
- const [okMsg, setOkMsg] = React.useState('')
39
-
40
- const loadStatus = () => {
41
- api('GET', '/dsh-remote/status')
42
- .then(setStatus)
43
- .catch((err) => setError(String((err && err.message) || err)))
44
- }
45
- React.useEffect(() => {
46
- loadStatus()
47
- }, [])
48
-
49
- const setField = (k) => (ev) => setForm({ ...form, [k]: ev.target.value })
50
-
51
- const connect = () => {
52
- setBusy(true)
53
- setError('')
54
- setOkMsg('')
55
- api('POST', '/dsh-remote/connect', {
56
- host: form.host.trim(),
57
- port: Number(form.port) || 22,
58
- username: form.username.trim() || 'root',
59
- password: form.password || undefined,
60
- privateKeyPath: form.privateKeyPath.trim() || undefined,
61
- })
62
- .then((res) => {
63
- setStatus(res)
64
- setOkMsg(res.ok ? '已连接 ✓ 可浏览远程目录。' : '')
65
- if (res && res.workspace) setBrowse({ path: res.workspace, out: '' })
66
- })
67
- .catch((err) => setError(String((err && err.message) || err)))
68
- .finally(() => setBusy(false))
69
- }
70
-
71
- const doBrowse = (p) => {
72
- const target = p !== undefined ? p : browse.path.trim()
73
- setBusy(true)
74
- setError('')
75
- api('GET', '/dsh-remote/ls?path=' + encodeURIComponent(target || '/'))
76
- .then((res) => setBrowse({ path: target, out: res.text }))
77
- .catch((err) => setError(String((err && err.message) || err)))
78
- .finally(() => setBusy(false))
79
- }
80
-
81
- const setWorkspace = () => {
82
- const p = browse.path.trim()
83
- if (!p) return
84
- setBusy(true)
85
- setError('')
86
- setOkMsg('')
87
- api('POST', '/dsh-remote/workspace', { path: p })
88
- .then((res) => {
89
- setStatus(res)
90
- setOkMsg('远程工作区已设为 ' + p)
91
- })
92
- .catch((err) => setError(String((err && err.message) || err)))
93
- .finally(() => setBusy(false))
94
- }
95
-
96
- const row = (t, v, action, key) =>
97
- React.createElement(
98
- 'div',
99
- { key },
100
- React.createElement('label', { style: { fontSize: '12px', opacity: 0.75 } }, t + ': '),
101
- v,
102
- action || null,
103
- )
104
-
105
- return React.createElement(
106
- 'div',
107
- { style: { padding: '16px', display: 'flex', flexDirection: 'column', gap: '14px', maxWidth: 720 } },
108
- React.createElement('div', { style: { fontWeight: 600, fontSize: 15 } }, '远程工作区(dsh-remote)'),
109
- React.createElement('div', { style: { fontSize: '12px', opacity: 0.8 } },
110
- '输入远程主机与登录方式,连接后在远程文件系统上浏览并选择工作区目录;agent 会用 remote_list_dir / remote_read_file / remote_exec 在所选远程工作区内操作。'),
111
- status
112
- ? React.createElement('div', { style: { display: 'flex', gap: '14px', flexWrap: 'wrap', fontSize: '13px' } },
113
- React.createElement('span', null, '远程: ', React.createElement('code', null, (status.username || '?') + '@' + (status.host || '?'))),
114
- React.createElement('span', null, '已连接: ', React.createElement('code', null, status.connected ? '是' : '否')),
115
- React.createElement('span', null, '远程工作区: ', React.createElement('code', null, status.workspace || '(未设置)')),
116
- )
117
- : null,
118
- React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' } },
119
- row('主机', React.createElement('input', { value: form.host, onChange: setField('host'), placeholder: 'IP 或 hostname', style: { flex: 1 } }), null, 'f1'),
120
- row('端口', React.createElement('input', { value: form.port, onChange: setField('port'), placeholder: '22', style: { width: 60 } }), null, 'f2'),
121
- row('用户', React.createElement('input', { value: form.username, onChange: setField('username'), placeholder: 'root', style: { flex: 1 } }), null, 'f3'),
122
- row('私钥路径', React.createElement('input', { value: form.privateKeyPath, onChange: setField('privateKeyPath'), placeholder: '如 C:/Users/me/.ssh/id_rsa(留空用默认 key)', style: { flex: 1 } }), null, 'f4'),
123
- row('密码', React.createElement('input', { type: 'password', value: form.password, onChange: setField('password'), placeholder: '密码(SSH 无 key 时用;不进日志)', style: { flex: 1 } }), null, 'f5'),
124
- ),
125
- React.createElement('button', { onClick: connect, disabled: busy }, busy ? '连接中…' : '连接远程'),
126
- React.createElement('div', { style: { display: 'flex', gap: '8px' } },
127
- React.createElement('input', { value: browse.path, onChange: (ev) => setBrowse({ ...browse, path: ev.target.value }), placeholder: '浏览/作为工作区的路径,如 /home/dev/project', style: { flex: 1 } }),
128
- React.createElement('button', { onClick: () => doBrowse(), disabled: busy }, '列目录'),
129
- React.createElement('button', { onClick: setWorkspace, disabled: busy || !browse.path.trim() }, '设为远程工作区'),
130
- ),
131
- error ? React.createElement('div', { style: { color: '#e06c75', whiteSpace: 'pre-wrap', fontSize: 13 } }, error) : null,
132
- okMsg ? React.createElement('div', { style: { color: '#4caf7d', fontSize: 13 } }, okMsg) : null,
133
- browse.out
134
- ? React.createElement('pre', { style: { fontSize: 12, background: 'rgba(128,128,128,0.08)', padding: 10, borderRadius: 6, maxHeight: 320, overflow: 'auto' } }, browse.out)
135
- : null,
136
- )
137
- }
138
-
139
- // ── Remote directory picker: fills ui-workspace's directory-flow holes ──
140
- // Adapter that lets the native "Add workspace" flow pick a REMOTE directory.
141
- // It browses via /dsh-remote/ls (host SFTP) and, on confirm, asks the host to
142
- // create a real LOCAL mirror (/dsh-remote/mirror) so the DSH workspaceRegistry
143
- // (which requires fs.realpath on a local dir) can adopt it with onPicked(mirror).
144
- function RemoteDirectoryFlow(props) {
145
- const { open, busy, onPicked, onCancel } = props
146
- const [path, setPath] = React.useState('')
147
- const [entries, setEntries] = React.useState(null)
148
- const [err, setErr] = React.useState('')
149
- const [loading, setLoading] = React.useState(false)
150
- React.useEffect(() => {
151
- if (!open) return
152
- setEntries(null)
153
- setErr('')
154
- setLoading(true)
155
- api('GET', '/dsh-remote/ls?path=' + encodeURIComponent(''))
156
- .then((res) => {
157
- setPath(res && res.path ? res.path : '')
158
- setEntries(res && res.text ? res.text : '(empty)')
159
- })
160
- .catch((e) => setErr(String((e && e.message) || e)))
161
- .finally(() => setLoading(false))
162
- }, [open])
163
-
164
- const browse = (p) => {
165
- const target = p !== undefined ? p : path
166
- setLoading(true)
167
- setErr('')
168
- api('GET', '/dsh-remote/ls?path=' + encodeURIComponent(target || ''))
169
- .then((res) => {
170
- setPath(target)
171
- setEntries(res.text)
172
- })
173
- .catch((e) => setErr(String((e && e.message) || e)))
174
- .finally(() => setLoading(false))
175
- }
176
-
177
- const goUp = () => {
178
- const idx = String(path).replace(/\/+$/, '').lastIndexOf('/')
179
- browse(idx <= 0 ? '/' : String(path).slice(0, idx))
180
- }
181
-
182
- const pick = () => {
183
- if (busy) return
184
- setErr('')
185
- api('POST', '/dsh-remote/mirror', { path })
186
- .then((res) => {
187
- if (res && res.localMirror) onPicked(res.localMirror)
188
- else if (res && res.error) setErr(res.error)
189
- })
190
- .catch((e) => setErr(String((e && e.message) || e)))
191
- }
192
-
193
- if (!open) return null
194
- return React.createElement(
195
- 'div',
196
- { style: { padding: 16, minWidth: 420, maxWidth: 640 } },
197
- React.createElement('div', { style: { fontWeight: 600, marginBottom: 8 } }, '远程工作区目录'),
198
- React.createElement(
199
- 'div',
200
- { style: { display: 'flex', gap: 6, marginBottom: 8 } },
201
- React.createElement('input', {
202
- value: path,
203
- onChange: (e) => setPath(e.target.value),
204
- placeholder: '远程路径,如 /home/dev/project',
205
- style: { flex: 1 },
206
- }),
207
- React.createElement('button', { onClick: () => browse(), disabled: loading }, '列目录'),
208
- React.createElement('button', { onClick: goUp, disabled: loading }, '上一级'),
209
- ),
210
- err ? React.createElement('div', { style: { color: '#e06c75', marginBottom: 6, fontSize: 12 } }, err) : null,
211
- React.createElement(
212
- 'pre',
213
- { style: { fontSize: 12, background: 'rgba(128,128,128,0.08)', padding: 8, borderRadius: 6, maxHeight: 260, overflow: 'auto', whiteSpace: 'pre-wrap' } },
214
- loading ? '加载中…' : entries || '(未连接远程,先连接或用 rw_connect)',
215
- ),
216
- React.createElement(
217
- 'div',
218
- { style: { display: 'flex', gap: 8, marginTop: 8, justifyContent: 'flex-end' } },
219
- React.createElement('button', { onClick: onCancel, style: { background: 'transparent' } }, '取消'),
220
- React.createElement('button', { onClick: pick, disabled: busy || !path.trim() }, busy ? '本地镜像中…' : '设为远程工作区'),
221
- ),
222
- )
223
- }
224
-
225
- function apply(ctx) {
226
- const slots = ctx.get('slots')
227
- if (slots === undefined) return
228
- slots.inject('settings.section', () =>
229
- slots.register(
230
- { name: 'settings.section', id: 'dsh-remote', order: 40, label: () => '远程工作区' },
231
- () => React.createElement(RemoteWorkspacePage, null),
232
- ),
233
- )
234
- // fill ui-workspace's two directory-flow holes with the remote picker
235
- slots.inject(
236
- 'conversation.hero.workspace.directoryFlow',
237
- () =>
238
- slots.inject('sidebar.workspaces.directoryFlow', function* () {
239
- yield slots.register({ name: 'conversation.hero.workspace.directoryFlow', id: 'dsh-remote', priority: -100 }, RemoteDirectoryFlow)
240
- yield slots.register({ name: 'sidebar.workspaces.directoryFlow', id: 'dsh-remote', priority: -100 }, RemoteDirectoryFlow)
241
- }),
242
- )
243
- }
244
-
245
- exports.name = name
246
- exports.apply = apply
247
- return module.exports
248
- },
1
+ // dsh-remote — client half.
2
+ // Settings → 远程工作区 + a unified workspace directory picker that fills
3
+ // ui-workspace's two directory-flow holes (sidebar + conversation hero).
4
+ //
5
+ // The picker has two tabs, styled to match the harness:
6
+ // • 本机 — drives the SAME local Host backend as the official picker
7
+ // (ctx.workspaces.listDirectory), so local dirs still work.
8
+ // • 远程 — browses the remote over /dsh-remote/ls (SFTP); on confirm it asks
9
+ // the host to build a real LOCAL mirror (/dsh-remote/mirror) then
10
+ // onPicked(localMirror) so the DSH workspaceRegistry adopts the local dir.
11
+ //
12
+ // Client entries must be classic scripts registered via window.__ModuleLoader__.load
13
+ // ({ id, factory }); the factory receives a synchronous `require`.
14
+ window.__ModuleLoader__.load({
15
+ id: 'dsh-remote',
16
+ factory: (require) => {
17
+ var module = { exports: {} }
18
+ var exports = module.exports
19
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
20
+
21
+ const React = require('react')
22
+ const name = 'dsh-remote'
23
+
24
+ async function api(method, path, body) {
25
+ const opts = { method, headers: {} }
26
+ if (body) {
27
+ opts.headers['Content-Type'] = 'application/json'
28
+ opts.body = JSON.stringify(body)
29
+ }
30
+ const res = await fetch(path, opts)
31
+ const data = await res.json().catch(() => ({}))
32
+ if (!res.ok) throw new Error((data && (data.error || data.message)) || 'HTTP ' + res.status)
33
+ return data
34
+ }
35
+
36
+ const T = { bg: 'rgba(128,128,128,0.07)', border: 'rgba(128,128,128,0.35)', danger: '#e06c75', ok: '#4caf7d', radius: 8 }
37
+
38
+ const inputS = { flex: 1, padding: '6px 10px', borderRadius: T.radius, border: '1px solid ' + T.border, background: T.bg }
39
+ const buttonS = { padding: '6px 12px', borderRadius: T.radius, border: '1px solid ' + T.border, background: T.bg, cursor: 'pointer' }
40
+
41
+ // ── Settings → 远程工作区 ────────────────────────────────────────────────
42
+ function RemoteWorkspacePage() {
43
+ const [status, setStatus] = React.useState(null)
44
+ const [form, setForm] = React.useState({ host: '', port: '22', username: '', password: '', privateKeyPath: '' })
45
+ const [busy, setBusy] = React.useState(false)
46
+ const [browse, setBrowse] = React.useState({ path: '', out: '' })
47
+ const [error, setError] = React.useState('')
48
+ const [okMsg, setOkMsg] = React.useState('')
49
+
50
+ const loadStatus = () => api('GET', '/dsh-remote/status').then(setStatus).catch((e) => setError(String((e && e.message) || e)))
51
+ React.useEffect(() => { loadStatus() }, [])
52
+
53
+ const setField = (k) => (ev) => setForm({ ...form, [k]: ev.target.value })
54
+
55
+ const connect = () => {
56
+ setBusy(true); setError(''); setOkMsg('')
57
+ api('POST', '/dsh-remote/connect', {
58
+ host: form.host.trim(), port: Number(form.port) || 22, username: form.username.trim() || 'root',
59
+ password: form.password || undefined, privateKeyPath: form.privateKeyPath.trim() || undefined,
60
+ })
61
+ .then((res) => { setStatus(res); if (res && res.workspace) setBrowse({ path: res.workspace, out: '' }); setOkMsg(res.ok ? '已连接 ✓ 可浏览远程目录。' : '') })
62
+ .catch((e) => setError(String((e && e.message) || e)))
63
+ .finally(() => setBusy(false))
64
+ }
65
+
66
+ const doBrowse = (p) => {
67
+ const target = p !== undefined ? p : browse.path.trim()
68
+ setBusy(true); setError('')
69
+ api('GET', '/dsh-remote/ls?path=' + encodeURIComponent(target || ''))
70
+ .then((res) => setBrowse({ path: target, out: res.text }))
71
+ .catch((e) => setError(String((e && e.message) || e)))
72
+ .finally(() => setBusy(false))
73
+ }
74
+
75
+ const setWorkspace = () => {
76
+ const p = browse.path.trim(); if (!p) return
77
+ setBusy(true); setError(''); setOkMsg('')
78
+ api('POST', '/dsh-remote/workspace', { path: p })
79
+ .then((res) => { setStatus(res); setOkMsg('远程工作区已设为 ' + p) })
80
+ .catch((e) => setError(String((e && e.message) || e)))
81
+ .finally(() => setBusy(false))
82
+ }
83
+
84
+ const row = (t, v, k) => React.createElement(
85
+ 'div', { key: k },
86
+ React.createElement('label', { style: { fontSize: 12, opacity: 0.75 } }, t + ': '), v,
87
+ )
88
+
89
+ return React.createElement(
90
+ 'div',
91
+ { style: { padding: 16, display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 720 } },
92
+ React.createElement('div', { style: { fontWeight: 600, fontSize: 15 } }, '远程工作区(dsh-remote)'),
93
+ React.createElement('div', { style: { fontSize: 12, opacity: 0.8 } },
94
+ '连接远程主机后即可用 rw_* 工具操作;选中的远程目录会镜像成一个本地工作区(可与本地工作区并存)。'),
95
+ status
96
+ ? React.createElement('div', { style: { display: 'flex', gap: 14, flexWrap: 'wrap', fontSize: 13 } },
97
+ React.createElement('span', null, '远程: ', React.createElement('code', null, (status.username || '?') + '@' + (status.host || '?'))),
98
+ React.createElement('span', null, '连接: ', React.createElement('code', null, status.connected ? '是' : '否')),
99
+ React.createElement('span', null, '工作区: ', React.createElement('code', null, status.workspace || '(未设置)')),
100
+ status.localMirror ? React.createElement('span', null, '本地镜像: ', React.createElement('code', null, status.localMirror)) : null,
101
+ )
102
+ : null,
103
+ React.createElement('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 } },
104
+ row('主机', React.createElement('input', { value: form.host, onChange: setField('host'), placeholder: 'IP 或 hostname', style: inputS }), 'h'),
105
+ row('端口', React.createElement('input', { value: form.port, onChange: setField('port'), placeholder: '22', style: { ...inputS, width: 70 } }), 'p'),
106
+ row('用户', React.createElement('input', { value: form.username, onChange: setField('username'), placeholder: 'root', style: inputS }), 'u'),
107
+ row('私钥路径', React.createElement('input', { value: form.privateKeyPath, onChange: setField('privateKeyPath'), placeholder: '如 C:/Users/me/.ssh/id_rsa', style: inputS }), 'k'),
108
+ row('密码', React.createElement('input', { type: 'password', value: form.password, onChange: setField('password'), placeholder: 'SSH 无 key 时用', style: inputS }), 'w'),
109
+ ),
110
+ React.createElement('button', { onClick: connect, disabled: busy, style: { ...buttonS, alignSelf: 'flex-start' } }, busy ? '连接中…' : '连接远程'),
111
+ React.createElement('div', { style: { display: 'flex', gap: 8 } },
112
+ React.createElement('input', { value: browse.path, onChange: (e) => setBrowse({ ...browse, path: e.target.value }), placeholder: '远程路径,如 /home/dev/project', style: inputS }),
113
+ React.createElement('button', { onClick: () => doBrowse(), disabled: busy, style: buttonS }, '列目录'),
114
+ React.createElement('button', { onClick: setWorkspace, disabled: busy || !browse.path.trim(), style: buttonS }, '设为远程工作区'),
115
+ ),
116
+ error ? React.createElement('div', { style: { color: T.danger, whiteSpace: 'pre-wrap', fontSize: 13 } }, error) : null,
117
+ okMsg ? React.createElement('div', { style: { color: T.ok, fontSize: 13 } }, okMsg) : null,
118
+ browse.out
119
+ ? React.createElement('pre', { style: { fontSize: 12, background: T.bg, padding: 10, borderRadius: T.radius, maxHeight: 320, overflow: 'auto', whiteSpace: 'pre-wrap' } }, browse.out)
120
+ : null,
121
+ )
122
+ }
123
+
124
+ // ── Unified workspace directory picker (fills the directory-flow holes) ──
125
+ let LOCAL_WORKSPACES = null // ctx.workspaces, bound in apply()
126
+
127
+ function parseLs(text) {
128
+ const out = []
129
+ for (const ln of String(text || '').split('\n')) {
130
+ const m = ln.match(/^(d[-r][rwx-]{8})\s+[^ ]+\s+[^ ]+\s+[^ ]+\s+[^ ]+\s+\d{4,}-\d{2}-\d{2}([T ]+\S+)?\s+(.+)$/)
131
+ if (!m) continue
132
+ out.push((m[1].charAt(0) === 'd' ? '📁' : '📄') + m[3])
133
+ }
134
+ if (!out.length && String(text || '').trim()) out.push(...String(text).trim().split('\n').slice(1).map((l) => '📄 ' + l.trim()).filter(Boolean))
135
+ return out
136
+ }
137
+
138
+ function DirPicker(props) {
139
+ const { open, busy, onPicked, onCancel } = props
140
+ const [tab, setTab] = React.useState('local')
141
+ const [path, setPath] = React.useState('')
142
+ const [items, setItems] = React.useState(null)
143
+ const [err, setErr] = React.useState('')
144
+ const [loading, setLoading] = React.useState(false)
145
+
146
+ const load = (target, useLocal) => {
147
+ const p = target !== undefined ? target : path
148
+ setLoading(true); setErr('')
149
+ if (useLocal) {
150
+ const w = LOCAL_WORKSPACES || {}
151
+ const list = w.listDirectory || (() => Promise.reject(new Error('本机目录服务不可用')))
152
+ Promise.resolve(list(p || undefined)).then((lv) => {
153
+ const names = (lv && lv.entries) || (lv && lv.children) || (lv && lv.list) || []
154
+ setPath(p || (lv && lv.path) || '')
155
+ setItems((Array.isArray(names) ? names : []).map((e) => (e && e.name ? e.name : String(e))))
156
+ }).catch((e) => setErr(String((e && e.message) || e))).finally(() => setLoading(false))
157
+ } else {
158
+ api('GET', '/dsh-remote/ls?path=' + encodeURIComponent(p || '')).then((res) => {
159
+ setPath(res && res.path ? res.path : p)
160
+ setItems(parseLs(res && res.text))
161
+ }).catch((e) => setErr(String((e && e.message) || e))).finally(() => setLoading(false))
162
+ }
163
+ }
164
+
165
+ React.useEffect(() => { if (open) load('', tab === 'local') }, [open])
166
+
167
+ const switchTab = (t) => { setTab(t); setPath(''); setItems(null); setErr(''); load('', t === 'local') }
168
+ const goUp = () => {
169
+ const sep = tab === 'local' ? '\\' : '/'
170
+ const idx = String(path).replace(/[\\/]+$/, '').lastIndexOf(sep)
171
+ const parent = idx <= 0 ? (tab === 'local' ? (path.slice(0, 3) || 'C:\\') : '/') : String(path).slice(0, idx)
172
+ load(parent, tab === 'local')
173
+ }
174
+ const enter = (name) => {
175
+ const sep = tab === 'local' ? '\\' : '/'
176
+ const np = path === '' || path.endsWith(sep) ? path + name : path + sep + name
177
+ load(np, tab === 'local')
178
+ }
179
+ const pickCurrent = () => {
180
+ if (loading || busy) return
181
+ if (tab === 'local') { onPicked(path); return }
182
+ api('POST', '/dsh-remote/mirror', { path })
183
+ .then((res) => (res && res.localMirror ? onPicked(res.localMirror) : setErr((res && res.error) || '')))
184
+ .catch((e) => setErr(String((e && e.message) || e)))
185
+ }
186
+
187
+ if (!open) return null
188
+ const isDir = (label) => String(label || '').charAt(0) === '📁'
189
+ const cell = { ...buttonS, textAlign: 'center' }
190
+ const active = { ...cell, fontWeight: 700 }
191
+ return React.createElement(
192
+ 'div', { style: { padding: 14, minWidth: 440, maxWidth: 680 } },
193
+ React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 8 } },
194
+ React.createElement('button', { onClick: () => switchTab('local'), style: tab === 'local' ? active : cell }, '本机'),
195
+ React.createElement('button', { onClick: () => switchTab('remote'), style: tab === 'remote' ? active : cell }, '远程'),
196
+ ),
197
+ React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 8 } },
198
+ React.createElement('input', { value: path, onChange: (e) => setPath(e.target.value), placeholder: tab === 'local' ? '本机目录路径' : '远程路径,如 /home/dev/project', style: inputS }),
199
+ React.createElement('button', { onClick: () => load(path, tab === 'local'), style: buttonS }, '载入'),
200
+ React.createElement('button', { onClick: goUp, style: buttonS }, '上一级'),
201
+ ),
202
+ err ? React.createElement('div', { style: { color: T.danger, marginBottom: 6, fontSize: 12 } }, err) : null,
203
+ React.createElement(
204
+ 'div', { style: { border: '1px solid ' + T.border, borderRadius: T.radius, padding: 8, maxHeight: 280, overflow: 'auto', fontSize: 13 } },
205
+ loading
206
+ ? '加载中…'
207
+ : (items && items.length
208
+ ? items.slice(0, 400).map((n, i) => React.createElement('div', { key: i, onClick: () => (isDir(n) ? enter(n.slice(2)) : null), style: { padding: '3px 6px', borderRadius: 4, cursor: isDir(n) ? 'pointer' : 'default', color: isDir(n) ? T.ok : 'inherit' } }, isDir(n) ? n : ' ' + n))
209
+ : React.createElement('div', { style: { opacity: 0.6 } }, items === null ? '(输入路径后点「载入」)' : '(空)')),
210
+ ),
211
+ React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 8, justifyContent: 'flex-end' } },
212
+ React.createElement('button', { onClick: onCancel, style: { background: 'transparent' } }, '取消'),
213
+ React.createElement('button', { onClick: pickCurrent, disabled: loading || busy || !path.trim(), style: buttonS }, busy ? '处理中…' : tab === 'local' ? '选用此目录' : '设为远程工作区'),
214
+ ),
215
+ )
216
+ }
217
+
218
+ function apply(ctx) {
219
+ const slots = ctx.get('slots')
220
+ if (slots === undefined) return
221
+ LOCAL_WORKSPACES = (ctx && ctx.workspaces) || null
222
+ slots.inject('settings.section', () =>
223
+ slots.register({ name: 'settings.section', id: 'dsh-remote', priority: 40, label: () => '远程工作区' }, () => React.createElement(RemoteWorkspacePage, null)),
224
+ )
225
+ slots.inject(
226
+ 'conversation.hero.workspace.directoryFlow',
227
+ () =>
228
+ slots.inject('sidebar.workspaces.directoryFlow', function* () {
229
+ yield slots.register({ name: 'conversation.hero.workspace.directoryFlow', id: 'dsh-remote', priority: -100 }, DirPicker)
230
+ yield slots.register({ name: 'sidebar.workspaces.directoryFlow', id: 'dsh-remote', priority: -100 }, DirPicker)
231
+ }),
232
+ )
233
+ }
234
+
235
+ exports.name = name
236
+ exports.apply = apply
237
+ return module.exports
238
+ },
249
239
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Remote-work assistant for DeepSeek Harness: connect SSH (password or key), pick a remote workspace, operate on it with rw_pick_workspace / rw_list_dir / rw_read_file / rw_exec / rw_sync tools, and mirror it to a real local directory (SFTP) so the DSH native workspace can adopt it.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -30,7 +30,8 @@
30
30
  },
31
31
  "client": {
32
32
  "inject": [
33
- "@deepseek-ai/dsh-client-runtime"
33
+ "@deepseek-ai/dsh-client-runtime",
34
+ "@deepseek-ai/dsh-client-ui-workspace"
34
35
  ],
35
36
  "platform": "web"
36
37
  }
@@ -40,6 +41,8 @@
40
41
  "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
41
42
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
42
43
  "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
44
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
45
+ "@deepseek-ai/dsh-client-ui-workspace": "^0.1.0-rc.6",
43
46
  "@deepseek-ai/cordis": "^4.0.1"
44
47
  },
45
48
  "dependencies": {