dsh-linked-workspace 0.8.1 → 0.8.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.3 — 2026-11-11
4
+
5
+ Second review round: follow-ups to the 0.8.2 paging and preview work.
6
+
7
+ - **Preview overlay (medium)**: the sanitizer now strips inline `style` attributes as well. A document containing `<div style="position:fixed;inset:0;z-index:99999;background:#fff">` used to cover the whole DSH window and could impersonate a login page (click-jacking / phishing without any script).
8
+ - **Paging survives refresh (medium)**: the local tree refreshes every 4 seconds; it now re-requests only the number of entries already loaded instead of resetting to the first page, so “Load more” results no longer disappear after up to 4 s.
9
+ - **Paging offset with hidden files (medium)**: “Load more” uses the raw server offset, so with hidden files toggled off entries are neither duplicated (200 duplicates before) nor skipped; the remaining count follows the same offset.
10
+ - **Directory listing cache (medium)**: `fs.list` caches a listing for 5 seconds, so paging a huge directory reads it once instead of re-running a full `listDir` (20,000 entries: ~10.4 s per call). Writes, mkdir, rename, move, delete, copy and transfers invalidate the cache, and `limit` is clamped server-side to 1..5000.
11
+ - **Partial copy cleanup (low)**: if a directory copy fails midway (for example on a >64 MB file), the partial target is removed and the error is `FS_INCOMPLETE`; `FS_TOO_LARGE` now only means an actual size cap.
12
+ - **Connection reuse (low)**: `HostPool` keys in-flight connects by host *and* authority, so an address change cannot attach an old connection to a new request; a connection replaced while connecting is disposed instead of leaked, and `setHosts` clears stale pending entries.
13
+ - **Right-sidebar requests (low)**: the linked-workspace tree source de-duplicates in-flight loads instead of sending the same request twice.
14
+ - **Large pickers and PDF export (low)**: the local/remote directory pickers render at most 500 entries plus a “type the path directly” note; markdown-to-PDF export stops after 999 name suffixes with an explicit error instead of looping forever.
15
+ - **Tests**: fixed two self-inconsistent assertions in `test/host-api.mjs` (a folder left behind made a later create return 409, and an already-existing folder was expected to answer 400); the suite now passes all 28 steps.
16
+
17
+ ## 0.8.2 — 2026-11-11
18
+
19
+ Three-day review round: security, data-integrity and large-directory fixes.
20
+
21
+ - **Document preview XSS (high)**: the preview sanitizer now normalizes control characters, whitespace and zero-width characters before checking a URL scheme, so `jav\tascript:` links (and `vbscript:`, `file:`, `blob:`, `data:text/html`) can no longer run in the GUI. Only `http(s)`, `mailto`, `tel`, relative URLs and bitmap `data:image/*` sources are kept; SVG data URLs stay blocked.
22
+ - **Mirror containment on Windows**: `realpathSync.native` is used for the containment check, so a directory junction can no longer make `.remote` (or a mirrored file) point outside the workspace. Linked `.local`/`.remote` entries that are themselves links are removed as links, never followed.
23
+ - **No silent truncation when copying**: copying a remote file larger than 64 MB, or reading one incompletely, now fails with an explicit error instead of writing a truncated copy. The `linked_workspace` read tool no longer mirrors a truncated payload into `.remote`.
24
+ - **Large local files**: `local.read` reads only the requested head of the file instead of loading the whole thing into memory, so peeking at multi-gigabyte files no longer allocates them (or fails with `ERR_FS_FILE_TOO_LARGE`).
25
+ - **Non-empty directory delete from the tool**: `linked_workspace` `delete` now removes directories recursively, matching the UI, instead of failing on SFTP `rmdir`.
26
+ - **Paged directory listings**: `fs.list` and `local.list` return `limit`/`offset` with `total` and `hasMore` (default 500, max 5000); both trees render a “Load more (N left)” row. A 20,000-entry folder no longer freezes the UI (it used to render all 20,012 rows).
27
+ - **Deep sync trees**: the mirror sync marks a job truncated when it stops at the depth limit, instead of reporting success with missing files.
28
+ - **Accessibility**: visible `:focus-visible` rings for tree rows, inputs, buttons and tabs; `role="treeitem"`/`aria-level`/`aria-selected`/`aria-expanded` on tree rows with ←/→ collapse/expand; the add-workspace dialog focuses its first field, traps Tab, and closes on Escape.
29
+ - **Excel preview honesty**: `read_xlsx` reports `totalRow`/`totalCol`/`truncated`, and the grid notes when it shows only the first 200 rows × 40 columns or the first 20 worksheets.
30
+ - **Build and host fixes**: the client build fails loudly when a `src/parts/*` file is missing; the dead branch in `linked.update` is gone; `HostPool` reuses one in-flight connection per host instead of leaking a duplicate on concurrent first connects.
31
+
32
+ Earlier unreleased work included in this version:
33
+
34
+ - Fixed linked-directory context-menu creation on both the remote and local trees: New folder and New file are direct menu actions, so the scrollable menu no longer clips the creation submenu.
35
+ - CSV, HTML, SVG and other preview formats now create ordinary empty files; only Markdown, PDF, DOCX, XLSX and PPTX use document templates.
36
+ - Creation keeps the name dialog open on failure, displays the error and prevents repeated submissions while pending. New files and folders reject existing names with HTTP 409 instead of replacing or silently reusing them, on both remote and local trees.
37
+
3
38
  ## 0.8.1 — 2026-11-09
4
39
 
5
40
  - **Image zoom**: the image preview gains zoom-out/zoom-in buttons, a percentage readout, “Fit to window” and “1:1” controls, plus `Ctrl/Cmd + wheel` zoom.
package/README.en.md CHANGED
@@ -119,6 +119,35 @@ Old “Remote directories” tabs are restored after refresh through hidden lega
119
119
 
120
120
  See [CHANGELOG.md](CHANGELOG.md) for the full version history.
121
121
 
122
+ ### 0.8.3
123
+
124
+ Follow-up fixes to the 0.8.2 paging and preview work (second review round):
125
+
126
+ - Document previews now also drop inline `style` attributes, so a `position:fixed` overlay can no longer cover the whole app and impersonate a login page (click-jacking/phishing surface).
127
+ - Tree refreshes (the local tree every 4 s) re-request only the entries already loaded, so “Load more” results are no longer reset to the first page.
128
+ - With hidden files toggled, “Load more” uses the true server offset: entries are neither duplicated nor skipped, and the “N left” label is computed from that offset.
129
+ - `fs.list` gained a 5-second listing cache: paging reads the directory once, and any write, mkdir, rename, move, delete, copy or transfer invalidates it.
130
+ - A directory copy that fails midway removes the partial copy and reports `FS_INCOMPLETE`; `FS_TOO_LARGE` is now reserved for real size limits.
131
+ - Concurrent first connects are keyed by host *and* address, so an address change cannot hand the old connection to the new request, and replaced connections are released.
132
+ - The right-sidebar tree source de-duplicates in-flight loads instead of issuing the same request twice.
133
+ - Directory pickers render at most 500 entries and suggest typing the path; PDF export stops after 999 name suffixes with an explicit error instead of looping forever.
134
+ - Tests: two self-inconsistent assertions in `test/host-api.mjs` were fixed (a leftover folder made a later create return 409, and an existing name was expected to return 400); all 28 steps now pass.
135
+
136
+ ### 0.8.2
137
+
138
+ Security, data-integrity and large-directory fixes from the three-day review round:
139
+
140
+ - The document-preview sanitizer normalizes control characters, whitespace and zero-width characters before checking a URL scheme, so `jav\tascript:`, `vbscript:`, `file:` and `data:text/html` links no longer run in the GUI; only `http(s)`, `mailto`, `tel`, relative URLs and bitmap `data:image` sources are kept.
141
+ - Windows junctions can no longer escape the `.remote` containment check (`realpathSync.native`); linked `.local`/`.remote` entries are removed as links, never followed.
142
+ - Copying a remote file over 64 MB, or reading one incompletely, now fails loudly instead of writing a truncated copy; the read tool no longer mirrors truncated payloads.
143
+ - `local.read` reads only the requested head instead of loading the whole file into memory.
144
+ - The `linked_workspace` `delete` action removes non-empty directories recursively.
145
+ - Directory listings are paged (500 by default, 5000 max, with `total`/`hasMore`) and both trees render a “Load more” row, so a 20,000-entry folder no longer freezes the UI.
146
+ - Mirror sync marks a job truncated at the depth limit instead of reporting success with files missing.
147
+ - Accessibility: `:focus-visible` rings, `treeitem`/`aria-level`/`aria-expanded` semantics, `←`/`→` collapse and expand, and an add-workspace dialog that focuses its first field, traps Tab and closes on Escape.
148
+ - Excel previews report `totalRow`/`totalCol`/`truncated` and note when the grid shows only the first 200 rows × 40 columns or the first 20 worksheets.
149
+ - The client build fails when a `src/parts/*` file is missing; the dead `linked.update` branch is gone; `HostPool` reuses one in-flight connection per host.
150
+
122
151
  ### 0.7.0
123
152
 
124
153
  Single-click tree navigation with preview tabs and double-click pinning, remote/local terminal buttons with `cd`, VSCode-like shortcuts, unsaved-close prompts, and an auto-save switch under Settings → Connections.
package/README.md CHANGED
@@ -114,6 +114,35 @@ dsh plugin --profile "$DSH_PROFILE" add "file:$PWD/dsh-linked-workspace"
114
114
 
115
115
  已打开的旧右侧栏「远程目录」标签页会在刷新后自动恢复(插件保留了两个隐藏的旧 tab 类型别名),标签标题也会自动改成「联动工作区目录」,新开的标签使用新的 `dsh-linked-workspace` 类型。
116
116
 
117
+ ### 0.8.3 变化
118
+
119
+ 针对 0.8.2 分页与预览的后续修复(第二轮评审):
120
+
121
+ - 文档预览同时删除内联 `style`,`position:fixed` 全屏覆盖层不再能把整页伪装成登录页(点击劫持/钓鱼面)。
122
+ - 目录树刷新(本地树每 4 秒)只重取已加载的条目数,不再把「加载更多」的结果打回第一页。
123
+ - 含隐藏文件时「加载更多」用服务端真实偏移,条目不再重复或遗漏;「剩余 N 项」按真实偏移计算。
124
+ - `fs.list` 增加 5 秒列表缓存:翻页只读一次目录;写、建、改名、移动、删除、复制、传输后立即失效。
125
+ - 目录复制中途失败会清理已复制的半成品并返回 `FS_INCOMPLETE`(原 `FS_TOO_LARGE` 只用于真实超限)。
126
+ - 并发首次连接按「主机 + 地址」复用,主地址变化时不再把旧连接交给新地址请求,被替换的连接会释放。
127
+ - 右侧栏树的数据源增加请求去重,重复加载不再重复发请求。
128
+ - 目录选择器最多渲染 500 项并提示可直接输入路径;导出 PDF 的同名后缀最多尝试 999 次,超限报错而不是无限循环。
129
+ - 测试:修复 `test/host-api.mjs` 自身两处断言缺陷(重复创建文件夹残留、把已存在当 400),全套 28 步通过。
130
+
131
+ ### 0.8.2 变化
132
+
133
+ 安全、数据完整性与大目录体验修复(三日评审轮):
134
+
135
+ - 文档预览净化器先归一控制字符/空白/零宽字符再判 scheme,`jav\tascript:`、`vbscript:`、`file:`、`data:text/html` 不再可执行;只保留 http(s)/mailto/tel、相对链接与位图 `data:image`。
136
+ - Windows junction 不再能绕过 `.remote` 包含性校验(改用 `realpathSync.native`);链接目录只删链接本身。
137
+ - 远端复制 >64 MB 或读取不完整时直接报错,不再静默写入截断副本;工具读文件只在未截断时同步到 `.remote`。
138
+ - `local.read` 只读取所需头部,不再把超大文件整体读入内存。
139
+ - 工具 `delete` 可递归删除非空远端目录。
140
+ - 目录列表分页(默认 500、最大 5000,返回 `total`/`hasMore`),树底「加载更多」;2 万项目录不再卡死。
141
+ - 同步深度超限会标记为截断,不再把漏文件当成功。
142
+ - 可访问性:`:focus-visible` 焦点环、树行 `treeitem`/`aria-level`/`aria-expanded`、`←`/`→` 折叠展开、添加工作区弹窗聚焦首字段 + Tab 循环 + Esc 关闭。
143
+ - Excel 预览返回 `totalRow`/`totalCol`/`truncated`,并提示「仅显示前 200 行 × 40 列」「仅显示前 20 个工作表」。
144
+ - 客户端构建缺 `src/parts/*` 文件时直接失败;移除 `linked.update` 死分支;`HostPool` 并发首连复用同一连接。
145
+
117
146
  ### 0.5.11 变化
118
147
 
119
148
  远程目录的只读文件现在能看到代码内容;设置里「打开工作区」会跳转并关闭设置,远程目录「打开」会打开对应远程标签。
package/docs/API.md CHANGED
@@ -58,12 +58,12 @@ relative to the scope root, or absolute inside it.
58
58
  | `workspaces.remove` | `{ id }` | `{}` |
59
59
  | `workspaces.rename` | `{ id, name }` | `{ workspace }` |
60
60
  | `workspaces.test` | `{ id }` | `{ ok, latencyMs?, resolvedPath?, error? }` |
61
- | `fs.list` | `{ workspaceId?/linkedId?, path }` | `{ path, entries: RemoteEntry[] }` |
61
+ | `fs.list` | `{ workspaceId?/linkedId?, path, limit?, offset? }` | `{ path, entries: RemoteEntry[], total, offset, limit, hasMore }` (default 500, max 5000). Listings are cached for 5 s for `offset > 0`; writes, mkdir, rename, move, delete, copy and transfers invalidate the cache. |
62
62
  | `fs.read` | `{ workspaceId?/linkedId?, path, maxBytes? }` | `FileRead` |
63
63
  | `media.read` | `{ workspaceId?/linkedId?, path }` | `{ path, size, mtime, mime, base64 }` (PNG/JPG/GIF/WebP/BMP/ICO/AVIF/SVG, max 24 MB) |
64
- | `fs.write` | `{ workspaceId?/linkedId?, path, content, expectedMtime? }` | `{ path, size, mtime }` |
64
+ | `fs.write` | `{ workspaceId?/linkedId?, path, content, expectedMtime?, createOnly? }` | `{ path, size, mtime }` (`createOnly: true`: existing target → 409) |
65
65
  | `fs.stat` | `{ workspaceId?/linkedId?, path }` | `{ path, relative, type, size, mtime, mode }` |
66
- | `fs.mkdir` | `{ workspaceId?/linkedId?, path }` | `{}` |
66
+ | `fs.mkdir` | `{ workspaceId?/linkedId?, path, createOnly? }` | `{}` (`createOnly: true`: existing target → 409) |
67
67
  | `fs.rename` | `{ workspaceId?/linkedId?, from, to }` | `{}` |
68
68
  | `fs.move` | `{ workspaceId?/linkedId?, path, targetDir? }` | `{ path }` (same remote tree; collision → 409) |
69
69
  | `fs.delete` | `{ workspaceId?/linkedId?, path }` | `{}` |
@@ -71,7 +71,7 @@ relative to the scope root, or absolute inside it.
71
71
  | `settings.get` | `{}` | `{ editorTheme, editorThemeFamily, editorThemeMode, fileOpenMode, autoSave }` |
72
72
  | `settings.update` | `{ editorTheme?, editorThemeFamily?, editorThemeMode?, fileOpenMode?, autoSave? }` | `{ editorTheme, editorThemeFamily, editorThemeMode, fileOpenMode, autoSave }` |
73
73
  | `local.browse` | `{ path? }` | `{ path, parent, home, entries: [{ name, path }] }` |
74
- | `local.mkdir` | `{ path, name }` | `{ path }` |
74
+ | `local.mkdir` | `{ path, name }` | `{ path }` (existing target → 409) |
75
75
  | `linked.list` | `{}` | `{ links: LinkInfo[] }` (each link carries `running`) |
76
76
  | `linked.create` | `{ hostId, remotePath, localPath, name?, mirror? }` | `{ link, workspaceId: string|null }` |
77
77
  | `linked.update` | `{ id, name?, mirror? }` | `{ link }` |
@@ -79,7 +79,7 @@ relative to the scope root, or absolute inside it.
79
79
  | `linked.sync` | `{ id, maxFiles?, maxBytes? }` | `{ job: SyncJob }` (starts a background job) |
80
80
  | `linked.syncStatus` | `{ id }` | `{ job: SyncJob | null }` |
81
81
  | `linked.syncCancel` | `{ id }` | `{ cancelled: boolean }` |
82
- | `local.list` | `{ path }` | `{ path, entries: [{ name, path, type, size, mtime }] }` |
82
+ | `local.list` | `{ path, limit?, offset? }` | `{ path, entries: [{ name, path, type, size, mtime }], total, offset, limit, hasMore }` |
83
83
  | `local.openExternal` | `{ path }` | `{ opened, path }` |
84
84
  | `terminal.open` | `{ workspaceId?/linkedId?, side: 'remote'\|'local' }` | `{ opened, side, path, host? }` |
85
85
  | `fs.openExternal` | `{ workspaceId?/linkedId?, path }` | `{ opened, localPath, size }` |
@@ -102,7 +102,7 @@ relative to the scope root, or absolute inside it.
102
102
  | `local.move` | `{ path, targetDir?, root? }` | `{ path, type }` (same local tree; collision → 409) |
103
103
  | `local.delete` | `{ path, root? }` | `{}` (recursive) |
104
104
  | `local.copy` | `{ path, targetDir? }` | `{ path, type }` |
105
- | `local.newFile` | `{ path }` | `{ path }` |
105
+ | `local.newFile` | `{ path }` | `{ path }` (existing target → 409) |
106
106
  | `local.chmod` | `{ path, mode }` | `{ path, mode }` |
107
107
  | `local.edit` | `{ path, app? }` | `{ path }` (opens Notepad) |
108
108
 
package/docs/VERIFY.md CHANGED
@@ -89,6 +89,8 @@ Host code (lib/) reloads live with `plugin_manager set_plugin include:linked-wor
89
89
  19. 编辑器:内部内容面板不显示路径/保存头部(Ctrl+S 保存);修改未保存时文件标签名前出现灰色圆点;行号、语法高亮(字符串/注释/关键字有颜色)、主题配色、
90
90
  远端并发修改 → 冲突提示(重新加载/强制保存)、二进制/大文件只读;底部状态栏完整可见(不再被视口裁切)。
91
91
 
92
+ 20. **右键新建回归**:远端目录和本地目录的文件夹均右击后,菜单中直接出现「新建文件夹」「新建文件」(不依赖向右展开的子菜单);分别在根目录和嵌套目录创建文件夹、`.txt`、`.csv`、`.html`、`.svg`、`.png`,应创建空普通文件。`.md`、`.pdf`、`.docx`、`.xlsx`、`.pptx` 仍创建对应模板并打开;重复名称应留在命名对话框并显示错误,不得覆盖已有文件。
93
+
92
94
  ## 联动会话(模型工具)
93
95
 
94
96
  1. 在联动工作区里新建会话,问模型「这个联动工作区的远端根目录是什么」→ 它应调用 `linked_workspace` 的 `list`/`info`。
@@ -132,3 +134,25 @@ Host code (lib/) reloads live with `plugin_manager set_plugin include:linked-wor
132
134
  4. 本地与远端各打开一个 `.html`/`.htm`:内容面板以 iframe 渲染页面,状态栏显示 HTML、大小和「网页脚本已禁用」。
133
135
  5. HTML 内联样式与静态标签正常显示;页面内脚本不执行,链接不会跳出 DSH 主界面。
134
136
  6. 读取失败的 HTML(权限/过大/二进制)显示错误提示和重试按钮。
137
+
138
+ ## 安全、分页与可访问性(0.8.2)
139
+
140
+ 1. **预览链接净化**:在本地或远端放一个 Markdown 文件,内容含 `<a href="jav&#9;ascript:document.title='X'">click</a>` 与 `[example](https://example.com)`;预览后点第一个链接:页面不应跳转、标题不变、DSH 界面不被替换;第二个链接仍指向 `https://example.com`。`vbscript:`、`file:`、`data:text/html`、`data:image/svg+xml` 链接同样应被移除。
141
+ 2. **大目录分页**:造一个 20 000 项的目录(`1..20000 | % { New-Item -ItemType File "f$_.txt" }`),在目录树展开它:应只出现 500 行和「加载更多(剩余 19500 项)」,页面不卡顿、可滚动、可点其他文件;点「加载更多」追加 500 行并更新剩余计数,滚到底部后可继续加载。
142
+ 3. **键盘操作**:Tab 到目录树行应看到明显焦点环;聚焦树行时 `←` 收起、`→` 展开目录;读屏应能听到 treeitem 层级与展开状态。
143
+ 4. **添加工作区弹窗**:打开后焦点直接在第一个输入框;连按 Tab 焦点在弹窗内循环,不会跑到背景页面;`Esc` 关闭弹窗。
144
+ 5. **大文件窥视**:打开一个 >2 GB 的本地文件(只读预览):应显示「文件过大,仅显示前 512 KB」而不是报错或长时间卡住;内存不随文件大小增长。
145
+ 6. **复制上限**:在远端复制一个 >64 MB 的文本/二进制文件:应提示「超过 64 MB,无法复制」,目标目录不得出现被截断的副本。
146
+ 7. **非空目录删除(工具)**:让 agent 用 `linked_workspace` 的 `delete` 删除一个含文件的远端目录:应递归删除成功,不再报 SFTP `Failure`。
147
+ 8. **Excel 截断提示**:打开一个 >200 行或 >40 列的 xlsx:表格上方应出现「仅显示前 200 行 × 40 列」;工作表超过 20 个时出现「仅显示前 20 个工作表」。
148
+
149
+ ## 分页续修与预览覆盖(0.8.3)
150
+
151
+ 1. **预览覆盖层**:放一个 Markdown 文件,内容为 `<div style="position:fixed;inset:0;z-index:99999;background:#fff;padding:24px">FAKE-LOGIN-PAGE</div>` 加一行普通文字;预览后不应出现全屏白底覆盖层,`div` 上不应残留 `style` 属性,页面其余部分照常可点。
152
+ 2. **加载更多不被刷新清空**:在本地树展开一个 >500 项的目录,点「加载更多」后**静置 10 秒**(跨过两次 4 秒刷新):行数与剩余计数应保持不变,不回退到 500 行。
153
+ 3. **隐藏文件下的分页**:目录中同时放 300 个 `.hidden*.txt` 与 600 个 `visible*.txt`;先关闭「显示隐藏的文件」再展开该目录,点「加载更多」:可见条目不应重复、不应遗漏,最终数量为 600,剩余计数归零。
154
+ 4. **目录选择器上限**:进入一个含 600+ 子目录的父目录:列表面板最多显示 500 项,并提示「目录项过多,仅显示前 500 项;可直接在输入框中输入路径」;在输入框直接键入子路径仍可选择。
155
+ 5. **翻页性能**:对 20 000 项的远端目录连续点两次「加载更多」:第二次起应明显快于首次(5 秒内命中列表缓存),且界面不卡顿。
156
+ 6. **复制失败清理(开发核验)**:复制一个内含 >64 MB 文件的目录:应报错且目标目录不留半成品(可用 `git status` 之外的目录列表确认)。
157
+ 7. **导出 PDF 上限(开发核验)**:同目录放 999 个同名 `<name>.pdf` 后再次导出同名的 Markdown:应提示「同名 PDF 过多,请先清理目录中的旧导出」,不进入无限循环。
158
+ 8. **host-api 套件**:在有 SSH 主机与 DSH 运行时 Node 的机器上跑 `node test/host-api.mjs`(本机系统 Node 24.13 删除中日韩字符目录会崩溃,用 DSH 自带 Electron Node 执行):应为 28 步全部通过。