dsh-remote 0.8.16 → 0.8.18
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/README.md +29 -48
- package/README.zh.md +25 -36
- package/cordis.patch.yml +5 -55
- package/lib/client.js +187 -6
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://github.com/topics/dsh-plugin)
|
|
10
10
|
|
|
11
|
+

|
|
12
|
+
|
|
11
13
|
**Remote-work assistant for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH).**
|
|
12
14
|
|
|
13
15
|
Manage several SSH machines, then pick a **remote workspace** (or a **local** one) and let the agent operate right there without leaving the harness — listing files, reading code, running builds & commands over the remote host, and keeping that remote directory mirrored into a real local workspace object.
|
|
@@ -18,7 +20,7 @@ The harness Web UI intentionally binds `127.0.0.1` (the CLI rejects `--host 0.0.
|
|
|
18
20
|
|
|
19
21
|
Settings → **远程工作区** — a multi-machine SSH registry (add / edit / delete / set-current, password stored locally):
|
|
20
22
|
|
|
21
|
-
<img src="
|
|
23
|
+
<img src="docs/ui-settings-panel.png" alt="dsh-remote settings — multi-machine registry (light theme, host scrubbed)" width="720"/>
|
|
22
24
|
|
|
23
25
|
The native **"Add workspace" / "Select workspace"** flow — a centered modal, two tabs, opens on **本机 (local)**; switch to **远程 (remote)**:
|
|
24
26
|
|
|
@@ -26,7 +28,7 @@ The native **"Add workspace" / "Select workspace"** flow — a centered modal, t
|
|
|
26
28
|
|
|
27
29
|
Real capture (host scrubbed to a placeholder):
|
|
28
30
|
|
|
29
|
-
<img src="
|
|
31
|
+
<img src="docs/ui-picker-panel.png" alt="dsh-remote workspace picker — real dialog; 本机 (local) tab; 远程 machine select + prefilled root path + autocomplete" width="720"/>
|
|
30
32
|
|
|
31
33
|
---
|
|
32
34
|
|
|
@@ -72,10 +74,8 @@ core or require a listening Web server:
|
|
|
72
74
|
`sidebar.right.pane.tab` seat. It reuses the existing explorer/editor and
|
|
73
75
|
gives remote files their own session-scoped resource addresses, rather than
|
|
74
76
|
sending remote paths to the local Files viewer.
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
retain the existing `/dsh-remote/*` routes and sidebar composition, including
|
|
78
|
-
the standalone-sidebar deduplication guard.
|
|
77
|
+
- `dsh-better-sidebar` is not bundled. Web hosts may install it separately;
|
|
78
|
+
official Desktop uses the native right-sidebar integration instead.
|
|
79
79
|
|
|
80
80
|
Validation so far covers Host startup, Desktop IPC JSON requests, read-only
|
|
81
81
|
SSH connection/list/read, and opening the settings/import UI. Native file-tab
|
|
@@ -97,48 +97,29 @@ build allowlist or automatically approve dependency scripts.
|
|
|
97
97
|
dsh plugin add dsh-remote # add the bundle
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
([dsh-better-sidebar](https://www.npmjs.com/package/dsh-better-sidebar)) is
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
>
|
|
120
|
-
>
|
|
121
|
-
>
|
|
122
|
-
>
|
|
123
|
-
> On an older harness either stay on **0.8.14**, or keep the host half by
|
|
124
|
-
> disabling the embedded sidebar row in your profile's `cordis.patch.yml`
|
|
125
|
-
> (verified working on 0.1.0-rc.8 — the `rw_*` tools keep working, the sidebar
|
|
126
|
-
> UI is what you give up):
|
|
127
|
-
>
|
|
128
|
-
> ```yaml
|
|
129
|
-
> - id: dsh-remote-sidebar
|
|
130
|
-
> disabled: true
|
|
131
|
-
> ```
|
|
132
|
-
>
|
|
133
|
-
> (and do not list a standalone `dsh-better-sidebar` bundle either).
|
|
134
|
-
|
|
135
|
-
> **Requires the profile's pnpm linker to be `hoisted`** (the DSH profile
|
|
136
|
-
> default, `nodeLinker: hoisted` in `pnpm-workspace.yaml`). The loader resolves
|
|
137
|
-
> plugin packages from the profile root, so the sidebar must be reachable in
|
|
138
|
-
> the top-level `node_modules`. If your `pnpm-workspace.yaml` was rewritten
|
|
139
|
-
> without `nodeLinker: hoisted`, add it back (`nodeLinker: hoisted`) and run
|
|
140
|
-
> `pnpm install` once — otherwise the embedded sidebar row fails with
|
|
141
|
-
> `Cannot find package 'dsh-better-sidebar'`.
|
|
100
|
+
Since **v0.8.18**, `dsh-remote` installs and mounts only itself. The Web sidebar
|
|
101
|
+
([dsh-better-sidebar](https://www.npmjs.com/package/dsh-better-sidebar)) is
|
|
102
|
+
optional and is no longer a dependency or an automatically mounted row. This
|
|
103
|
+
keeps the SSH tools and settings UI independent from a particular sidebar
|
|
104
|
+
implementation.
|
|
105
|
+
|
|
106
|
+
To add the optional Web remote-file explorer/editor, install both bundles:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
dsh plugin add dsh-remote
|
|
110
|
+
dsh plugin add dsh-better-sidebar
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
When the standalone sidebar service is present, `dsh-remote` discovers it
|
|
114
|
+
dynamically and registers its remote explorer/editor tabs. Without it, all
|
|
115
|
+
`rw_*` tools, the settings UI, sync, audit log, and port forwarding continue to
|
|
116
|
+
work. Official Desktop uses its native right-sidebar seats and does not need
|
|
117
|
+
`dsh-better-sidebar`.
|
|
118
|
+
|
|
119
|
+
> **Upgrading from 0.7.2–0.8.17:** upgrading to 0.8.18 removes the embedded
|
|
120
|
+
> sidebar dependency and mount. Install `dsh-better-sidebar` separately only if
|
|
121
|
+
> you still want that Web UI. Any old profile override for
|
|
122
|
+
> `id: dsh-remote-sidebar` can be removed because that row no longer exists.
|
|
142
123
|
|
|
143
124
|
(or `npm install dsh-remote` + add `- id: dsh-remote / name: dsh-remote` in `cordis.patch.yml`).
|
|
144
125
|
|
package/README.zh.md
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
`dsh-app:` 通道承载请求,鉴权仍由宿主负责,不启动 Web Server。
|
|
14
14
|
- 通过 `sidebarRightTabs` 和 `sidebar.right.pane.tab` 提供原生“远程文件”入口,
|
|
15
15
|
复用原来的文件树与编辑器,不把远端路径传给本地文件预览器。
|
|
16
|
-
-
|
|
17
|
-
|
|
16
|
+
- `dsh-better-sidebar` 不再内置;Web 版可以单独安装,官方 Desktop 则使用
|
|
17
|
+
原生右侧栏集成。
|
|
18
18
|
|
|
19
19
|
已验证 Host 启动、IPC 请求、真实 SSH 的只读连接/目录列表/文本读取,以及设置页和
|
|
20
20
|
测试 SSH 配置的导入。文件标签的完整 UI 操作、编辑/同步、多机器并行会话,以及旧 Web
|
|
@@ -28,6 +28,8 @@ Desktop 安装器还可能要求明确配置 `ssh2` / `cpu-features` 可选构
|
|
|
28
28
|
[](LICENSE)
|
|
29
29
|
[](https://github.com/topics/dsh-plugin)
|
|
30
30
|
|
|
31
|
+

|
|
32
|
+
|
|
31
33
|
**为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)打造的远程工作助手。**
|
|
32
34
|
|
|
33
35
|
维护多台 SSH 机器,然后在「选择工作区」时选一个**远程工作区**(或**本地工作区**),Agent 就能在不离开 harness 的情况下直接操作——列文件、读代码、在远程主机上跑构建/命令,并把远程目录镜像成一个真实的本地工作区对象。
|
|
@@ -38,7 +40,7 @@ DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.
|
|
|
38
40
|
|
|
39
41
|
设置 → **远程工作区** —— 多机 SSH 列表(增/删/改/设为当前,密码本地保存、不回显):
|
|
40
42
|
|
|
41
|
-
<img src="
|
|
43
|
+
<img src="docs/ui-settings-panel.png" alt="dsh-remote 设置页 — 多机列表(浅色主题,主机已打码)" width="720"/>
|
|
42
44
|
|
|
43
45
|
原生 **「Add workspace / 选择工作区」** 流程 —— **居中弹窗**、两个 tab,**默认落在「本机」**;切到 **「远程」**:
|
|
44
46
|
|
|
@@ -46,7 +48,7 @@ DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.
|
|
|
46
48
|
|
|
47
49
|
真实截图(机器已打码为占位):
|
|
48
50
|
|
|
49
|
-
<img src="
|
|
51
|
+
<img src="docs/ui-picker-panel.png" alt="dsh-remote 工作区选择 — 真实弹窗;默认本机 tab;远程:机器下拉 + 预填根路径 + 自动补全" width="720"/>
|
|
50
52
|
|
|
51
53
|
---
|
|
52
54
|
|
|
@@ -74,38 +76,25 @@ DSH 的 Web 界面刻意只监听 `127.0.0.1`(CLI 为安全拒绝 `--host 0.0.
|
|
|
74
76
|
dsh plugin add dsh-remote # 添加 bundle
|
|
75
77
|
```
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
([dsh-better-sidebar](https://www.npmjs.com/package/dsh-better-sidebar)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
>
|
|
94
|
-
>
|
|
95
|
-
>
|
|
96
|
-
> ```yaml
|
|
97
|
-
> - id: dsh-remote-sidebar
|
|
98
|
-
> disabled: true
|
|
99
|
-
> ```
|
|
100
|
-
>
|
|
101
|
-
> (同时不要再单独列出 `dsh-better-sidebar` bundle。)
|
|
102
|
-
|
|
103
|
-
> **要求 profile 的 pnpm linker 为 `hoisted`**(DSH profile 默认,
|
|
104
|
-
> `pnpm-workspace.yaml` 里 `nodeLinker: hoisted`)。loader 从 profile 根解析
|
|
105
|
-
> 插件包,侧边栏必须能在顶层 `node_modules` 被解析到。如果你的
|
|
106
|
-
> `pnpm-workspace.yaml` 被重写丢掉了 `nodeLinker: hoisted`,请补回并执行一次
|
|
107
|
-
> `pnpm install` —— 否则内嵌侧边栏行会报
|
|
108
|
-
> `Cannot find package 'dsh-better-sidebar'`。
|
|
79
|
+
从 **v0.8.18** 起,`dsh-remote` 只安装并挂载自身。Web 侧边栏
|
|
80
|
+
([dsh-better-sidebar](https://www.npmjs.com/package/dsh-better-sidebar))
|
|
81
|
+
改为可选,不再是依赖,也不会被自动挂载。这样 SSH 工具和设置页不再被某个侧边栏
|
|
82
|
+
实现的版本/API 变化拖垮。
|
|
83
|
+
|
|
84
|
+
如需 Web 版远程文件浏览/编辑,请显式安装两个 bundle:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
dsh plugin add dsh-remote
|
|
88
|
+
dsh plugin add dsh-better-sidebar
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
独立侧边栏 service 存在时,`dsh-remote` 会动态发现它并注册远程文件 tab;
|
|
92
|
+
不安装时,`rw_*` 工具、设置页、同步、审计日志和端口转发均照常工作。
|
|
93
|
+
官方 Desktop 使用原生右侧栏,不需要安装 `dsh-better-sidebar`。
|
|
94
|
+
|
|
95
|
+
> **从 0.7.2–0.8.17 升级:** 升到 0.8.18 后,内嵌侧边栏依赖和挂载会消失。
|
|
96
|
+
> 只有仍需要 Web 侧边栏 UI 时才单独安装 `dsh-better-sidebar`。旧 profile 里针对
|
|
97
|
+
> `id: dsh-remote-sidebar` 的覆盖可以删除,因为这行已不存在。
|
|
109
98
|
|
|
110
99
|
(或 `npm install dsh-remote`,再在 `cordis.patch.yml` 加 `- id: dsh-remote / name: dsh-remote`。)
|
|
111
100
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,59 +1,9 @@
|
|
|
1
|
-
# dsh-remote bundle patch: inserts this plugin into a profile's layer stack.
|
|
2
|
-
# Applied when a profile lists this bundle; rows reference the package by name so
|
|
3
|
-
# Node resolution finds the installed code.
|
|
1
|
+
# dsh-remote bundle patch: inserts only this plugin into a profile's layer stack.
|
|
4
2
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# row uses a DEDICATED id (`dsh-remote-sidebar`)
|
|
10
|
-
# and a guard so it backs off when another row mounts `dsh-better-sidebar`
|
|
11
|
-
# (e.g. the user installed the sidebar bundle on its own, or an aggregate
|
|
12
|
-
# bundle owns it) — two enabled mounts would both register /sidebar/api and
|
|
13
|
-
# fail the whole plugin tree at boot.
|
|
14
|
-
#
|
|
15
|
-
# Order-independence (issue #12): the guard must NOT depend on
|
|
16
|
-
# `ctx.loader.entries()` creation order. The loader creates entries in bundle
|
|
17
|
-
# order and each row's disabled expression is evaluated at creation time, when
|
|
18
|
-
# only rows created BEFORE it are visible — so with
|
|
19
|
-
# `bundles: [..., "dsh-remote", "dsh-better-sidebar", ...]` (standalone sidebar
|
|
20
|
-
# installed after dsh-remote, the normal `dsh plugin add` append order) both
|
|
21
|
-
# rows saw no other sidebar mount and BOTH started, crashing boot with
|
|
22
|
-
# `duplicate prefix route "/sidebar/api"`.
|
|
23
|
-
#
|
|
24
|
-
# Instead we scan the COMPOSED PATCH STACK — the include entry's
|
|
25
|
-
# `config.patches` (every bundle layer in `dsh.profile.bundles` order, then the
|
|
26
|
-
# profile/home/overlay layers) — for any OTHER row that mounts
|
|
27
|
-
# `dsh-better-sidebar`. That data is fully materialized before any row guard
|
|
28
|
-
# evaluates, so the outcome is identical no matter where the standalone bundle
|
|
29
|
-
# sits in the bundle list. It is also pure data: it never reads another row's
|
|
30
|
-
# `disabled` (reading another row's disabled expression would recursively
|
|
31
|
-
# re-enter the loader — dsh-better-sidebar's own row carries one too, so the
|
|
32
|
-
# two would ping-pong), so no recursion is possible. The old entries()-based
|
|
33
|
-
# check is kept as a belt-and-suspenders fallback for rows not visible in the
|
|
34
|
-
# patch stack (e.g. an entry injected at runtime by HMR or another plugin).
|
|
3
|
+
# dsh-better-sidebar is deliberately not mounted here and is not a dependency.
|
|
4
|
+
# Users who want that optional Web UI can install its bundle separately;
|
|
5
|
+
# lib/client.js discovers the betterSidebar service dynamically and adds the
|
|
6
|
+
# remote explorer/editor tabs when it is present.
|
|
35
7
|
- insert:
|
|
36
8
|
- id: dsh-remote
|
|
37
9
|
name: 'dsh-remote'
|
|
38
|
-
- id: dsh-remote-sidebar
|
|
39
|
-
name: 'dsh-better-sidebar'
|
|
40
|
-
disabled: !!js >-
|
|
41
|
-
(function () {
|
|
42
|
-
// Inspect the core row's explicit disabled flag, not service presence:
|
|
43
|
-
// a Web server that simply has not started yet must still be awaited.
|
|
44
|
-
// Do not evaluate another sidebar's disabled guard (issue #12).
|
|
45
|
-
if ([...ctx.loader.entries()].some((e) => e.options.name === '@deepseek-ai/dsh-host-webserver' && e.options.disabled === true)) return true
|
|
46
|
-
try {
|
|
47
|
-
const inc = ctx.loader && ctx.loader.resolve && ctx.loader.resolve('include')
|
|
48
|
-
const patches = inc && inc.subtree && inc.subtree.config && inc.subtree.config.patches
|
|
49
|
-
if (Array.isArray(patches)) {
|
|
50
|
-
for (const p of patches) {
|
|
51
|
-
if (!p || !Array.isArray(p.insert)) continue
|
|
52
|
-
for (const row of p.insert) {
|
|
53
|
-
if (row && row.name === 'dsh-better-sidebar' && row.id !== 'dsh-remote-sidebar') return true
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} catch (e) {}
|
|
58
|
-
return [...ctx.loader.entries()].some((e) => e.options.name === 'dsh-better-sidebar' && e.options.id !== 'dsh-remote-sidebar')
|
|
59
|
-
})()
|
package/lib/client.js
CHANGED
|
@@ -1491,11 +1491,177 @@ window.__ModuleLoader__.load({
|
|
|
1491
1491
|
React.createElement('path', { d: 'M3.5 1.5a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V5.2a1 1 0 0 0-.29-.7L10.5 1.79a1 1 0 0 0-.7-.29H3.5Zm1.5 3h2.25a.75.75 0 0 1 0 1.5H5a.75.75 0 0 1 0-1.5ZM5 9.5h6a.75.75 0 0 1 0 1.5H5a.75.75 0 0 1 0-1.5Zm0 2.25h4a.75.75 0 0 1 0 1.5H5a.75.75 0 0 1 0-1.5Z' }),
|
|
1492
1492
|
)
|
|
1493
1493
|
|
|
1494
|
-
//
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
|
|
1498
|
-
|
|
1494
|
+
// Issue #32: the remote tree's expansion list travels on the TAB (`meta`),
|
|
1495
|
+
// never on better-sidebar's session-shared `expanded` array. Best-effort:
|
|
1496
|
+
// an older sidebar without tab meta simply forgets the expansion on reload.
|
|
1497
|
+
const TAB_EXPANDED_META = 'remoteExpanded'
|
|
1498
|
+
/** The tab's persisted expansion list, or null when the tab records none
|
|
1499
|
+
* (so a caller can tell "never expanded" from "expanded then collapsed
|
|
1500
|
+
* everything"). */
|
|
1501
|
+
function readTabExpanded(tab) {
|
|
1502
|
+
try {
|
|
1503
|
+
const meta = tab && tab.meta
|
|
1504
|
+
if (!meta || typeof meta !== 'object' || !(TAB_EXPANDED_META in meta)) return null
|
|
1505
|
+
const list = meta[TAB_EXPANDED_META]
|
|
1506
|
+
return Array.isArray(list) ? list.filter((p) => typeof p === 'string') : []
|
|
1507
|
+
} catch { return null }
|
|
1508
|
+
}
|
|
1509
|
+
/** Fallback store when the sidebar predates tab meta (no persistence then,
|
|
1510
|
+
* but the tree still opens and closes within the page's lifetime). */
|
|
1511
|
+
const tabExpandedFallback = new Map()
|
|
1512
|
+
function tabExpandedOf(tab) {
|
|
1513
|
+
const fromMeta = readTabExpanded(tab)
|
|
1514
|
+
if (fromMeta !== null) return fromMeta
|
|
1515
|
+
const id = tab && tab.id
|
|
1516
|
+
return (id && tabExpandedFallback.get(id)) || []
|
|
1517
|
+
}
|
|
1518
|
+
function toggleTabExpanded(ctx, tab, path) {
|
|
1519
|
+
const cur = tabExpandedOf(tab)
|
|
1520
|
+
const next = cur.includes(path) ? cur.filter((p) => p !== path) : [...cur, path]
|
|
1521
|
+
const id = tab && tab.id
|
|
1522
|
+
if (id) tabExpandedFallback.set(id, next)
|
|
1523
|
+
try {
|
|
1524
|
+
if (!ctx || !tab || !id || typeof ctx.get !== 'function') return next
|
|
1525
|
+
const bs = ctx.get('betterSidebar')
|
|
1526
|
+
if (!bs || typeof bs.updateTab !== 'function') return next
|
|
1527
|
+
const meta = tab.meta && typeof tab.meta === 'object' ? tab.meta : {}
|
|
1528
|
+
bs.updateTab(id, { meta: { ...meta, [TAB_EXPANDED_META]: next } })
|
|
1529
|
+
} catch { /* expansion memory is best-effort */ }
|
|
1530
|
+
return next
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// Issue #32 healing, kept pure so it is directly testable: an older
|
|
1534
|
+
// dsh-remote pushed REMOTE paths into better-sidebar's session-shared
|
|
1535
|
+
// `expanded` set through onToggleDir. That set is PERSISTED (localStorage
|
|
1536
|
+
// `dsh-sidebar:v1:<sessionId>`) and the built-in local Files tree loads
|
|
1537
|
+
// every entry through a LOCAL fs.realpath, so those remote paths kept
|
|
1538
|
+
// failing with ENOENT until the set was cleaned.
|
|
1539
|
+
//
|
|
1540
|
+
// Return exactly the entries this tree owns: the current remote root and
|
|
1541
|
+
// anything under it. That range is remote-only by construction (the root
|
|
1542
|
+
// comes from resolve-mirror), so a legitimate local directory cannot
|
|
1543
|
+
// match. A LOCAL MIRROR path is never evicted — on a host whose own home is
|
|
1544
|
+
// an ancestor of the remote root (a macOS host driving a macOS remote) the
|
|
1545
|
+
// mirror under $DSH_HOME/remote-workspaces would otherwise fall inside it.
|
|
1546
|
+
//
|
|
1547
|
+
// On a WINDOWS host the sweep is broader and complete: a POSIX-absolute
|
|
1548
|
+
// entry (`/home/...`) can never be a valid local path there (Windows paths
|
|
1549
|
+
// are drive- or UNC-rooted), which is exactly why the bug only bit Windows.
|
|
1550
|
+
// That also cleans pollution left by an earlier remote root, which no
|
|
1551
|
+
// per-root rule could find.
|
|
1552
|
+
function isPosixAbsolutePath(p) {
|
|
1553
|
+
return typeof p === 'string' && p.startsWith('/') && !p.startsWith('//')
|
|
1554
|
+
}
|
|
1555
|
+
function windowsHost() {
|
|
1556
|
+
try {
|
|
1557
|
+
const nav = typeof navigator !== 'undefined' ? navigator : null
|
|
1558
|
+
if (!nav) return false
|
|
1559
|
+
if (nav.userAgentData && typeof nav.userAgentData.platform === 'string') {
|
|
1560
|
+
return /win/i.test(nav.userAgentData.platform)
|
|
1561
|
+
}
|
|
1562
|
+
if (typeof nav.platform === 'string' && nav.platform) return /win/i.test(nav.platform)
|
|
1563
|
+
return typeof nav.userAgent === 'string' && /windows/i.test(nav.userAgent)
|
|
1564
|
+
} catch { return false }
|
|
1565
|
+
}
|
|
1566
|
+
function strayRemoteSharedPaths(root, shared, hostIsWindows = windowsHost()) {
|
|
1567
|
+
if (!Array.isArray(shared) || !shared.length) return []
|
|
1568
|
+
let prefix = ''
|
|
1569
|
+
if (typeof root === 'string' && root !== '' && root !== '/') prefix = root.replace(/[\\/]+$/, '')
|
|
1570
|
+
const isMirror = (p) => p.includes('/remote-workspaces/') || p.includes('\\remote-workspaces\\')
|
|
1571
|
+
return shared.filter((p) => {
|
|
1572
|
+
if (typeof p !== 'string' || p === '' || isMirror(p)) return false
|
|
1573
|
+
if (prefix !== '' && (p === prefix || p === root ||
|
|
1574
|
+
p.startsWith(prefix + '/') || p.startsWith(prefix + '\\'))) return true
|
|
1575
|
+
return hostIsWindows && isPosixAbsolutePath(p)
|
|
1576
|
+
})
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Issue #32: the remote tree must NOT use better-sidebar's `expanded` /
|
|
1580
|
+
// `onToggleDir` props. That pair is the SESSION-shared expansion set, which
|
|
1581
|
+
// the built-in local Files tree also reads and loads as local directories
|
|
1582
|
+
// through /sidebar/api fs.tree. Feeding it remote paths (/home/...) made the
|
|
1583
|
+
// core tree run a LOCAL fs.realpath on them; on win32 a POSIX path counts as
|
|
1584
|
+
// absolute (drive-rooted), so it resolved to `D:\home\...` and failed with
|
|
1585
|
+
// ENOENT. Own the expansion here and persist it on the tab instead.
|
|
1586
|
+
//
|
|
1587
|
+
// Because that shared set is PERSISTED, an already-affected user stays
|
|
1588
|
+
// broken across reloads, so the stray entries are also migrated eagerly (see
|
|
1589
|
+
// migratePollutedSidebarState) and healed again from the tab (below).
|
|
1590
|
+
const SIDEBAR_STATE_PREFIX = 'dsh-sidebar:v1:'
|
|
1591
|
+
/**
|
|
1592
|
+
* Strip remote paths a pre-fix dsh-remote left in PERSISTED sidebar state,
|
|
1593
|
+
* before the built-in Files tree loads them. Only entries that cannot be a
|
|
1594
|
+
* valid local path on this host are removed (see strayRemoteSharedPaths).
|
|
1595
|
+
* Best-effort: any storage failure just leaves the tab-level healing to do
|
|
1596
|
+
* the job on the next load.
|
|
1597
|
+
*
|
|
1598
|
+
* @param storage - a Storage-like object (window.localStorage).
|
|
1599
|
+
* @param hostIsWindows - overrides host detection (tests).
|
|
1600
|
+
* @returns how many stray entries were removed.
|
|
1601
|
+
*/
|
|
1602
|
+
function migratePollutedSidebarState(storage, hostIsWindows = windowsHost()) {
|
|
1603
|
+
if (!storage || typeof storage.length !== 'number') return 0
|
|
1604
|
+
let cleaned = 0
|
|
1605
|
+
try {
|
|
1606
|
+
const keys = []
|
|
1607
|
+
for (let i = 0; i < storage.length; i++) {
|
|
1608
|
+
const key = storage.key(i)
|
|
1609
|
+
if (key && key.startsWith(SIDEBAR_STATE_PREFIX)) keys.push(key)
|
|
1610
|
+
}
|
|
1611
|
+
for (const key of keys) {
|
|
1612
|
+
const raw = storage.getItem(key)
|
|
1613
|
+
if (typeof raw !== 'string' || raw === '') continue
|
|
1614
|
+
let parsed
|
|
1615
|
+
try { parsed = JSON.parse(raw) } catch { continue }
|
|
1616
|
+
if (parsed === null || typeof parsed !== 'object' || !Array.isArray(parsed.expanded)) continue
|
|
1617
|
+
const stray = strayRemoteSharedPaths('', parsed.expanded, hostIsWindows)
|
|
1618
|
+
if (!stray.length) continue
|
|
1619
|
+
const drop = new Set(stray)
|
|
1620
|
+
parsed.expanded = parsed.expanded.filter((p) => !drop.has(p))
|
|
1621
|
+
storage.setItem(key, JSON.stringify(parsed))
|
|
1622
|
+
cleaned += stray.length
|
|
1623
|
+
}
|
|
1624
|
+
} catch { /* storage unavailable: the tab-level healing still runs */ }
|
|
1625
|
+
return cleaned
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// `evictStray` heals state an older dsh-remote already polluted: the tree
|
|
1629
|
+
// calls it once it knows its remote root, and it drops exactly those paths
|
|
1630
|
+
// from the shared set. It is a plain function (not an effect) so the rule is
|
|
1631
|
+
// directly testable.
|
|
1632
|
+
function RemoteExplorerTabOwned(props) {
|
|
1633
|
+
const [expanded, setExpanded] = React.useState(tabExpandedOf(props.tab))
|
|
1634
|
+
const sharedExpanded = Array.isArray(props.expanded) ? props.expanded : []
|
|
1635
|
+
const evictShared = typeof props.onToggleDir === 'function' ? props.onToggleDir : null
|
|
1636
|
+
// Heal without needing the remote root: on a Windows host a POSIX-absolute
|
|
1637
|
+
// entry can never be a valid local path, so the sweep is complete there.
|
|
1638
|
+
// (The root-scoped sweep for the session's own remote tree runs inside the
|
|
1639
|
+
// tab, which is where the resolved root becomes known.)
|
|
1640
|
+
const sharedKey = sharedExpanded.join('\u0000')
|
|
1641
|
+
React.useEffect(() => {
|
|
1642
|
+
if (!evictShared) return
|
|
1643
|
+
const stray = strayRemoteSharedPaths('', sharedKey ? sharedKey.split('\u0000') : [])
|
|
1644
|
+
for (const p of stray) evictShared(p)
|
|
1645
|
+
}, [sharedKey, evictShared])
|
|
1646
|
+
// Explicitly drop the framework's session-shared pair from the child.
|
|
1647
|
+
const owned = { ...props }
|
|
1648
|
+
delete owned.expanded
|
|
1649
|
+
delete owned.onToggleDir
|
|
1650
|
+
return React.createElement(RemoteExplorerTab, {
|
|
1651
|
+
...owned,
|
|
1652
|
+
evictStray: (root) => {
|
|
1653
|
+
if (!evictShared || !sharedExpanded.length) return []
|
|
1654
|
+
const stray = strayRemoteSharedPaths(root, sharedExpanded)
|
|
1655
|
+
for (const p of stray) evictShared(p)
|
|
1656
|
+
return stray
|
|
1657
|
+
},
|
|
1658
|
+
expanded,
|
|
1659
|
+
onToggleDir: (path) => setExpanded(toggleTabExpanded(props.ctx, props.tab, path)),
|
|
1660
|
+
})
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Tree-style remote file explorer. Data is listed live from the remote host
|
|
1664
|
+
// over /dsh-remote/ls, rooted at this session's remote workspace dir.
|
|
1499
1665
|
function RemoteExplorerTab(props) {
|
|
1500
1666
|
const [status, setStatus] = React.useState(null)
|
|
1501
1667
|
const [root, setRoot] = React.useState('') // remote root path for this session
|
|
@@ -1627,6 +1793,13 @@ window.__ModuleLoader__.load({
|
|
|
1627
1793
|
React.useEffect(() => { if (props.visible) refreshStatus({ visible: true }) }, [props.visible])
|
|
1628
1794
|
// Load dirs as they get expanded (keep already-loaded content).
|
|
1629
1795
|
React.useEffect(() => { if (root) for (const dir of expanded) loadDir(dir, { keep: true }) }, [root, expanded, loadDir])
|
|
1796
|
+
// Issue #32 healing: once the remote root is known, drop the paths an
|
|
1797
|
+
// older dsh-remote left in better-sidebar's PERSISTED session-shared
|
|
1798
|
+
// expansion set (the built-in local tree was loading them through a local
|
|
1799
|
+
// fs.realpath and failing). See strayRemoteSharedPaths().
|
|
1800
|
+
React.useEffect(() => {
|
|
1801
|
+
if (root && typeof props.evictStray === 'function') props.evictStray(root)
|
|
1802
|
+
}, [root, props.evictStray])
|
|
1630
1803
|
// ── Context-menu portal: append to document.body so the sidebar's
|
|
1631
1804
|
// overflow/transform cannot clip it. Pure DOM, no React portal needed.
|
|
1632
1805
|
React.useEffect(() => {
|
|
@@ -1917,6 +2090,13 @@ window.__ModuleLoader__.load({
|
|
|
1917
2090
|
function registerSidebarIntegration(ctx) {
|
|
1918
2091
|
const bs = (ctx && ctx.get && ctx.get('betterSidebar')) || null
|
|
1919
2092
|
if (!bs || typeof bs.registerTab !== 'function') return
|
|
2093
|
+
// Issue #32 migration: drop the remote paths an older dsh-remote left in
|
|
2094
|
+
// PERSISTED sidebar state, BEFORE the built-in Files tree loads them
|
|
2095
|
+
// through its local fs guard. Runs once per activation; a failure is
|
|
2096
|
+
// harmless because the tab-level healing repeats it.
|
|
2097
|
+
try {
|
|
2098
|
+
migratePollutedSidebarState(typeof window !== 'undefined' ? window.localStorage : null)
|
|
2099
|
+
} catch { /* best-effort migration */ }
|
|
1920
2100
|
const disposers = []
|
|
1921
2101
|
try {
|
|
1922
2102
|
disposers.push(bs.registerTab({
|
|
@@ -1925,7 +2105,8 @@ window.__ModuleLoader__.load({
|
|
|
1925
2105
|
icon: (size) => React.createElement('span', { style: { fontSize: (size || 14), lineHeight: 1 } }, '🌐'),
|
|
1926
2106
|
order: 55,
|
|
1927
2107
|
single: true,
|
|
1928
|
-
|
|
2108
|
+
// Issue #32: own the expansion state (see RemoteExplorerTabOwned).
|
|
2109
|
+
component: RemoteExplorerTabOwned,
|
|
1929
2110
|
}))
|
|
1930
2111
|
disposers.push(bs.registerTab({
|
|
1931
2112
|
id: SIDEBAR_FILE_ID,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive/proxy jump), pick a remote workspace, operate on it with 21 rw_* tools (incl. rw_edit/rw_stat/rw_mkdir/rw_remove/rw_move/rw_forward), conflict-aware mirror sync, port forwarding, side-bar remote file editing, audit log, keychain passwords.",
|
|
3
|
+
"version": "0.8.18",
|
|
4
|
+
"description": "Remote-work assistant for DeepSeek Harness: connect SSH (password/key/agent/keyboard-interactive/proxy jump), pick a remote workspace, operate on it with 21 rw_* tools (incl. rw_edit/rw_stat/rw_mkdir/rw_remove/rw_move/rw_forward), conflict-aware mirror sync, port forwarding, optional side-bar remote file editing, audit log, keychain passwords.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "flymysql <flyphp@outlook.com>",
|
|
7
7
|
"homepage": "https://github.com/flymysql/dsh-remote",
|
|
@@ -66,7 +66,6 @@
|
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"ssh2": "^1.16.0",
|
|
68
68
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
69
|
-
"dsh-better-sidebar": "^0.18.1",
|
|
70
69
|
"iconv-lite": "^0.6.3"
|
|
71
70
|
},
|
|
72
71
|
"devDependencies": {
|