dsh-remote 0.8.63 → 0.8.65

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 +92 -30
  2. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -55,6 +55,10 @@ window.__ModuleLoader__.load({
55
55
  'settings.badgeKey': '私钥',
56
56
  'settings.openWorkspace': '选目录',
57
57
  'settings.openWorkspaceHint': '在这台机器上选一个远程目录作为工作区',
58
+ 'settings.recentWorkspacesFor': '最近工作区({n})',
59
+ 'settings.recentWorkspacesHint': '这台机器用过的远程目录,点开直接打开',
60
+ 'settings.openWorkspaceHere': '打开',
61
+ 'settings.openWorkspaceOnHint': '在 {host} 上打开这个目录(会创建/复用本地镜像)',
58
62
  'settings.workspaceOpened': '已打开远程工作区:{path}',
59
63
  'settings.addMachineManual': '手动添加(密码 / 私钥 / 跳板机)',
60
64
  // 列表的搜索、排序与连接状态(机器一多就要用)。
@@ -143,8 +147,6 @@ window.__ModuleLoader__.load({
143
147
  'settings.testConn': '测试连接',
144
148
  'settings.saving': '保存中…',
145
149
  'settings.save': '保存',
146
- 'settings.recentWorkspaces': '当前机器最近工作区(一键切换)',
147
- 'settings.switch': '切换',
148
150
  'settings.forwards': '端口转发(SSH 隧道)',
149
151
  'settings.fwdStop': '停止',
150
152
  'settings.fwdStart': '启动',
@@ -223,7 +225,6 @@ window.__ModuleLoader__.load({
223
225
  'settings.secretStoreRefused': '密码未能写入系统密钥库({msg}),已放弃保存该密码(不会以明文落盘)。修复密钥库后重新保存,或把 credentialFallback 设为 plain 以接受明文回退。',
224
226
  'settings.cleared': '已取消当前远程机 — 本地会话不再自动使用远程上下文',
225
227
  'settings.filledSsh': '已从 ~/.ssh/config 填入(仅引用私钥路径,不读取内容)',
226
- 'settings.workspaceSwitched': '已切换到工作区 {path}',
227
228
  'settings.connOk': '连接成功',
228
229
  'settings.connOkWith': '连接成功({user}@{host},{latency}ms){shell}',
229
230
  'settings.connFail': '连接失败',
@@ -382,6 +383,10 @@ window.__ModuleLoader__.load({
382
383
  'settings.badgeKey': 'key',
383
384
  'settings.openWorkspace': 'Folder',
384
385
  'settings.openWorkspaceHint': 'Pick a remote folder on this machine as the workspace',
386
+ 'settings.recentWorkspacesFor': 'Recent folders ({n})',
387
+ 'settings.recentWorkspacesHint': 'Remote folders this machine used before — open one directly',
388
+ 'settings.openWorkspaceHere': 'Open',
389
+ 'settings.openWorkspaceOnHint': 'Open this folder on {host} (creates or reuses the local mirror)',
385
390
  'settings.workspaceOpened': 'Opened remote workspace: {path}',
386
391
  'settings.addMachineManual': 'Add manually (password / key / jump host)',
387
392
  'settings.hostFilter': 'Search hosts (name / address / user)',
@@ -463,8 +468,6 @@ window.__ModuleLoader__.load({
463
468
  'settings.testConn': 'Test connection',
464
469
  'settings.saving': 'Saving…',
465
470
  'settings.save': 'Save',
466
- 'settings.recentWorkspaces': 'Recent workspaces of the current machine (one-click switch)',
467
- 'settings.switch': 'Switch',
468
471
  'settings.forwards': 'Port forwarding (SSH tunnels)',
469
472
  'settings.fwdStop': 'Stop',
470
473
  'settings.fwdStart': 'Start',
@@ -543,7 +546,6 @@ window.__ModuleLoader__.load({
543
546
  'settings.secretStoreRefused': 'The OS keychain refused the password ({msg}); the password was NOT saved (it will not be written to disk in plaintext). Repair the keychain and save again, or set credentialFallback to plain to accept the plaintext fallback.',
544
547
  'settings.cleared': 'Cleared current remote — local sessions no longer use remote context automatically',
545
548
  'settings.filledSsh': 'Filled from ~/.ssh/config (only references the private key path, never reads its content)',
546
- 'settings.workspaceSwitched': 'Switched to workspace {path}',
547
549
  'settings.connOk': 'Connection successful',
548
550
  'settings.connOkWith': 'Connection successful ({user}@{host}, {latency}ms){shell}',
549
551
  'settings.connFail': 'Connection failed',
@@ -1009,7 +1011,6 @@ window.__ModuleLoader__.load({
1009
1011
  const [forwards, setForwards] = React.useState([])
1010
1012
  const [fwdForm, setFwdForm] = React.useState({ listenPort: '', targetHost: '127.0.0.1', targetPort: '', direction: 'local', autoStart: false })
1011
1013
  const [auditLines, setAuditLines] = React.useState([])
1012
- const [workspaces, setWorkspaces] = React.useState([])
1013
1014
  const [advOpen, setAdvOpen] = React.useState(false) // 高级配置折叠
1014
1015
  // Collapsed by default: the forwards panel sits BELOW the host list and used to
1015
1016
  // push the update/audit cards far down the page for a feature most sessions
@@ -1046,6 +1047,8 @@ window.__ModuleLoader__.load({
1046
1047
  const [manualOpen, setManualOpen] = React.useState(false)
1047
1048
  const [dirPick, setDirPick] = React.useState(null) // { machineId } while the picker is open
1048
1049
  const [hostFilter, setHostFilter] = React.useState('')
1050
+ // Which machine rows have their 最近工作区 list expanded (issue #73: host → folders).
1051
+ const [openWs, setOpenWs] = React.useState({})
1049
1052
  // session's machine", which the host resolves from the session binding.
1050
1053
  React.useEffect(() => { ensureChromeCss() }, [])
1051
1054
 
@@ -1105,11 +1108,25 @@ window.__ModuleLoader__.load({
1105
1108
 
1106
1109
  const refresh = () => api('GET', '/dsh-remote/machines').then((r) => {
1107
1110
  setMachines(r.machines || []); setCurrentId(r.currentId || '')
1108
- const cur = (r.machines || []).find((m) => m.id === r.currentId)
1109
- setWorkspaces((cur && cur.recentWorkspaces) || [])
1110
1111
  })
1111
1112
  React.useEffect(() => { refresh() }, [])
1112
1113
 
1114
+ // Issue #73 follow-up: the settings page must notice changes made ELSEWHERE —
1115
+ // an agent calling rw_connect, or another tab switching machines — without the
1116
+ // user reloading. A light poll of the registry (a local JSON read) plus a
1117
+ // refresh on window focus; the ssh-config list is re-read on focus only, since
1118
+ // that one touches the filesystem.
1119
+ React.useEffect(() => {
1120
+ const poll = () => { refresh().catch(() => {}) }
1121
+ const timer = setInterval(poll, 5000)
1122
+ const onFocus = () => { poll(); loadSshHosts() }
1123
+ try { window.addEventListener('focus', onFocus) } catch { /* headless host */ }
1124
+ return () => {
1125
+ clearInterval(timer)
1126
+ try { window.removeEventListener('focus', onFocus) } catch { /* headless host */ }
1127
+ }
1128
+ }, [])
1129
+
1113
1130
  // ── update state ────────────────────────────────────────────────────────
1114
1131
  const [upd, setUpd] = React.useState(null) // { current, latest, updateAvailable, updateMode, updatedMarker }
1115
1132
  const [updBusy, setUpdBusy] = React.useState(false)
@@ -1403,10 +1420,25 @@ window.__ModuleLoader__.load({
1403
1420
  .finally(() => setConnectingKey(''))
1404
1421
  }
1405
1422
 
1406
- const setWorkspaceNow = (p) => {
1407
- api('POST', '/dsh-remote/workspace', { path: p })
1408
- .then(() => setMsg(tr('settings.workspaceSwitched', { path: p })))
1423
+ /**
1424
+ * Open a folder this machine used before — one click from the row's
1425
+ * 「最近工作区」 list (issue #73: the VSCode shape is host → folders).
1426
+ *
1427
+ * Goes through /mirror rather than /workspace: /workspace only changes the
1428
+ * ACTIVE machine's workspace, while the user may be clicking a folder that
1429
+ * belongs to a machine they are not on. /mirror re-asserts the machine, creates
1430
+ * (or reuses) the mirror and persists the choice on that machine.
1431
+ */
1432
+ const openWorkspaceOn = (machineId, path) => {
1433
+ setBusy(true); setErr(''); setMsg('')
1434
+ api('POST', '/dsh-remote/mirror', { path, machineId })
1435
+ .then((r) => {
1436
+ setCurrentId((r && r.currentId) || machineId)
1437
+ setMsg(tr('settings.workspaceOpened', { path }))
1438
+ refresh()
1439
+ })
1409
1440
  .catch((e) => setErr(String((e && e.message) || e)))
1441
+ .finally(() => setBusy(false))
1410
1442
  }
1411
1443
 
1412
1444
  // ── forwards panel ────────────────────────────────────────────────────
@@ -1575,7 +1607,31 @@ window.__ModuleLoader__.load({
1575
1607
  (m.sshConfigResolved && m.sshConfigResolved.warnings && m.sshConfigResolved.warnings.length)
1576
1608
  ? React.createElement('div', { style: { fontSize: 11, color: T.warn, marginTop: 2 } }, tr('settings.sshResolvedWarn', { msg: m.sshConfigResolved.warnings.join(' ') }))
1577
1609
  : null,
1578
- rowResult('machine:' + m.id)),
1610
+ rowResult('machine:' + m.id),
1611
+ // 最近工作区挂在这一行下面(host → folders,issue #73):点一下就在这台
1612
+ // 机器上打开那个目录(走 /mirror,会建/复用镜像并记住选择)。
1613
+ (Array.isArray(m.recentWorkspaces) && m.recentWorkspaces.length)
1614
+ ? React.createElement('div', { style: { marginTop: 4 } },
1615
+ React.createElement('button', {
1616
+ type: 'button', className: 'dsh-rw-section-btn',
1617
+ style: { fontSize: 11, padding: 0 },
1618
+ title: tr('settings.recentWorkspacesHint'),
1619
+ onClick: () => setOpenWs((prev) => ({ ...prev, [m.id]: !prev[m.id] })),
1620
+ }, React.createElement('span', { style: { opacity: 0.7, width: 10, display: 'inline-block' } }, openWs[m.id] ? '▾' : '▸'),
1621
+ tr('settings.recentWorkspacesFor', { n: m.recentWorkspaces.length })),
1622
+ openWs[m.id]
1623
+ ? React.createElement('div', { style: { marginTop: 2, marginLeft: 16 } },
1624
+ m.recentWorkspaces.map((w, wi) => React.createElement('div', { key: w + wi, style: { display: 'flex', gap: 6, alignItems: 'center', padding: '1px 0' } },
1625
+ React.createElement('code', { style: { flex: 1, fontSize: 11, fontFamily: 'monospace', opacity: 0.85, wordBreak: 'break-all' } }, w),
1626
+ React.createElement('button', {
1627
+ type: 'button', className: 'dsh-rw-btn',
1628
+ style: { ...buttonS, fontSize: 11, padding: '1px 8px', whiteSpace: 'nowrap' },
1629
+ disabled: busy,
1630
+ title: tr('settings.openWorkspaceOnHint', { host: m.name }),
1631
+ onClick: () => openWorkspaceOn(m.id, w),
1632
+ }, tr('settings.openWorkspaceHere')))))
1633
+ : null)
1634
+ : null),
1579
1635
  React.createElement('div', { style: { display: 'flex', gap: 6, flexShrink: 0 } },
1580
1636
  React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => startEdit(m) }, tr('settings.edit')),
1581
1637
  React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => del(m.id) }, tr('settings.delete')),
@@ -1723,12 +1779,8 @@ window.__ModuleLoader__.load({
1723
1779
  ),
1724
1780
  ) : null,
1725
1781
  ),
1726
- // 快捷切换工作区(该机器的最近工作区)
1727
- workspaces.length ? React.createElement('div', { style: boxS },
1728
- React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.recentWorkspaces')),
1729
- workspaces.map((w, i) => React.createElement('div', { key: w + i, style: { display: 'flex', gap: 8, alignItems: 'center', padding: '4px 0' } },
1730
- React.createElement('code', { style: { flex: 1, fontSize: 12, fontFamily: 'monospace' } }, w),
1731
- React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px' }, onClick: () => setWorkspaceNow(w) }, tr('settings.switch'))))) : null,
1782
+ // 最近工作区现在挂在各自的机器行下面(issue #73:host → folders 的树),
1783
+ // 这个「只显示当前机器」的卡片就没有意义了。
1732
1784
  // 远程 DSH 界面(issue #46)
1733
1785
  React.createElement('div', { style: boxS },
1734
1786
  sectionToggle(webAttachOpen, setWebAttachOpen, tr('settings.webAttach')),
@@ -1941,7 +1993,11 @@ window.__ModuleLoader__.load({
1941
1993
  : React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.auditEmpty')))
1942
1994
  : React.createElement('div', { style: { fontSize: 11, opacity: 0.55 } }, String(auditLines.length)),
1943
1995
  ),
1944
- err ? React.createElement('div', { style: { color: T.danger, fontSize: 13 } }, err) : null,
1996
+ // Panel-level messages. They used to live ONLY inside the manual form, which is
1997
+ // collapsed by default now — so "已打开远程工作区 …" and a failed 连接 were
1998
+ // invisible after clicking something in the list.
1999
+ msg ? React.createElement('div', { style: { color: T.ok, fontSize: 12, lineHeight: 1.5 } }, msg) : null,
2000
+ err ? React.createElement('div', { style: { color: T.danger, fontSize: 13, lineHeight: 1.5 } }, err) : null,
1945
2001
  React.createElement('div', { style: boxS },
1946
2002
  React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.update')),
1947
2003
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', fontSize: 12 } },
@@ -1992,17 +2048,18 @@ window.__ModuleLoader__.load({
1992
2048
  // 选目录 on a machine that is NOT current must browse THAT machine, not
1993
2049
  // whichever one happens to be current.
1994
2050
  initialMachineId: dirPick.machineId,
1995
- onPicked: (picked) => {
2051
+ onPicked: (localMirror, remotePath) => {
1996
2052
  const target = dirPick.machineId
1997
2053
  setDirPick(null)
1998
- if (!picked) return
1999
- api('POST', '/dsh-remote/mirror', { path: picked, machineId: target })
2000
- .then((r) => {
2001
- setMsg(tr('settings.workspaceOpened', { path: picked }))
2002
- refresh()
2003
- if (r && r.localMirror) setWorkspaces((w) => (w.includes(picked) ? w : [picked, ...w]))
2004
- })
2005
- .catch((e) => setErr(String((e && e.message) || e)))
2054
+ // The picker already created the mirror; posting /mirror again would
2055
+ // send a local path as if it were remote (see commitPath).
2056
+ const shown = remotePath || localMirror
2057
+ if (!shown) return
2058
+ setCurrentId(target)
2059
+ setMsg(tr('settings.workspaceOpened', { path: shown }))
2060
+ // The mirror list is per machine now, so re-reading the registry is
2061
+ // what makes the new folder appear under that machine's row.
2062
+ refresh()
2006
2063
  },
2007
2064
  onCancel: () => setDirPick(null),
2008
2065
  })
@@ -2458,7 +2515,12 @@ window.__ModuleLoader__.load({
2458
2515
  // instead of trusting whichever happens to be active.
2459
2516
  ensureCurrent(machineId)
2460
2517
  .then(() => api('POST', '/dsh-remote/mirror', { path: target, machineId }))
2461
- .then((res) => (res && res.localMirror ? onPicked(res.localMirror) : setErr((res && res.error) || '')))
2518
+ // `onPicked(localMirror, remotePath)`: the mirror is ALREADY created here.
2519
+ // The sidebar opens the local mirror (1st arg); a caller that only wants to
2520
+ // report/continue from the remote path uses the 2nd. Handing back only the
2521
+ // local path made the settings page post /mirror a SECOND time with a
2522
+ // Windows path as if it were remote ("not a directory (or unreachable)").
2523
+ .then((res) => (res && res.localMirror ? onPicked(res.localMirror, res.path || target) : setErr((res && res.error) || '')))
2462
2524
  .catch((e) => setErr(String((e && e.message) || e)))
2463
2525
  }
2464
2526
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote",
3
- "version": "0.8.63",
3
+ "version": "0.8.65",
4
4
  "description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive, multi-hop ProxyJump), pick a remote workspace, operate on it with 22 rw_* tools, conflict-aware mirror sync, an embedded PTY terminal, one-click remote dsh deploy + web UI attach, port forwarding, host grouping, remote sandbox fencing, audit log, keychain passwords.",
5
5
  "license": "MIT",
6
6
  "author": "flymysql <flyphp@outlook.com>",
@@ -92,7 +92,7 @@
92
92
  "semver": "^7.8.5"
93
93
  },
94
94
  "scripts": {
95
- "test": "node --import ./test/setup-telemetry-off.mjs --test --test-timeout=40000 --test-force-exit",
95
+ "test": "node --import ./test/setup-telemetry-off.mjs --test --test-timeout=40000 --test-force-exit --test-concurrency=8",
96
96
  "test:coverage": "node scripts/coverage-union.mjs",
97
97
  "test:coverage:gate": "node scripts/coverage-union.mjs --gate 70"
98
98
  }