dsh-plugin-workbench 0.0.12 → 0.0.14
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 +27 -0
- package/README.en.md +180 -0
- package/README.md +20 -9
- package/docs/workbench-webgui.png +0 -0
- package/lib/client.js +84 -6
- package/lib/client.js.map +1 -1
- package/lib/index.js +31 -1
- package/package.json +13 -3
- package/src/client/FilePreview.tsx +106 -6
- package/src/index.ts +21 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.14] - 2026-08-29
|
|
4
|
+
|
|
5
|
+
- 文档:对标生态头部插件优化 README(首屏新增「装完你会看到」;布局补丁话术改为开箱即用);
|
|
6
|
+
- 元数据:npm description 重写(突出可编辑/右键文件操作/@引用),keywords 7 → 15;
|
|
7
|
+
- 新增英文版 README.en.md 与双语切换行。
|
|
8
|
+
## [0.0.13] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **预览分栏宽度拖拽:一拖就骤然缩小到最窄,之后无法拖动、无法复原**。根因:
|
|
13
|
+
拖拽计算"预览最大宽度"时用 `handle.parentElement` 测量中间列宽度,但 slot 系统把
|
|
14
|
+
slot 内容包在一层 `display: contents` 的包装元素里——该包装层不参与布局,
|
|
15
|
+
`getBoundingClientRect().width` 恒为 **0**,于是
|
|
16
|
+
`max = max(240, 0 - 240) = 240`,第一次 `pointermove` 就把宽度钳到最小宽度 240,
|
|
17
|
+
之后每次移动都被钳在 240,表现为骤然缩小且拖不回来(100% 复现,与窗口/鼠标操作
|
|
18
|
+
无关)。修复:`laidOutParent()` 跳过所有 `display: contents` 层,取真正参与布局的
|
|
19
|
+
flex 容器(中间列)再测量;持久化宽度恢复时同样用它校准。
|
|
20
|
+
- 拖拽健壮性(防同类"卡死"):`setPointerCapture` 保证 `pointerup` 在指针移出窗口后
|
|
21
|
+
也能派发并清理监听器;监听 `pointercancel`;每次 `pointerdown` 防御性移除上一次
|
|
22
|
+
残留的监听器;组件卸载时清理窗口监听器;拖拽中每步实时重测中间列宽度。
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **预览宽度持久化**:拖拽后的分栏宽度写入 localStorage,刷新/重开会话后恢复
|
|
27
|
+
(自动按当前窗宽 clamp 在有效区间内)。
|
|
28
|
+
|
|
29
|
+
|
|
3
30
|
## [0.0.12] - 2026-08-25
|
|
4
31
|
|
|
5
32
|
- 相关插件段新增 dsh-plugin-windows-guard(Windows 环境防坑守则 skill 插件,互相引流)。
|
package/README.en.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
[中文](./README.md) | **English**
|
|
2
|
+
|
|
3
|
+
# dsh-plugin-workbench
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
**A VS Code-style workbench that edits files directly** — not a read-only preview: file tree + editable code preview
|
|
12
|
+
(syntax highlighting, tabs, line-number gutter) + right-click file operations (new / rename / delete / copy / cut /
|
|
13
|
+
paste / open in system / reveal in file explorer) + inline image preview, with state saved independently per workspace.
|
|
14
|
+
Once installed, the DSH web GUI becomes a lightweight code editor.
|
|
15
|
+
|
|
16
|
+
> ✅ **Works out of the box**: since 0.0.9 the layout patch is fully automated — it takes effect right after install and
|
|
17
|
+
> restart; if a DSH upgrade overwrites the bundle, the plugin patches it back automatically, with no manual steps
|
|
18
|
+
> required (manual action is only needed if the anchors break, see the "Configuration" section).
|
|
19
|
+
|
|
20
|
+
What you'll see after installing:
|
|
21
|
+
|
|
22
|
+
- A **file tree sidebar** on the left (lazy loading + 2-second auto-refresh, expansion state kept per workspace);
|
|
23
|
+
- `@relative-path` mentions in messages turn into **clickable links** that open a preview in the workbench;
|
|
24
|
+
- **Drag files from the left panel into the chat area** and the path is inserted into the input box automatically.
|
|
25
|
+
|
|
26
|
+
## Screenshot
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
> Real Web GUI screenshot.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- File tree: lazy loading, 2-second auto-refresh, expansion state kept per workspace
|
|
35
|
+
- File icons: colored badges for common code formats / emoji for images, audio & video, archives, etc.; expanded and
|
|
36
|
+
collapsed directories are visually distinguished
|
|
37
|
+
- Editable preview: transparent textarea layered over syntax highlighting, save with `Ctrl+S`/`Cmd+S`;
|
|
38
|
+
Markdown renders a preview by default (one-click switch between source and rendered view); prose formats like
|
|
39
|
+
.txt and very large code files automatically fall back to plain-text editing — fast to load, no UI jank
|
|
40
|
+
- **Disk change sync**: when an open file is modified externally (e.g. saved by an agent or another editor),
|
|
41
|
+
clean tabs reload automatically, while tabs with unsaved edits show a "⟳" badge (click to reload)
|
|
42
|
+
- **Line-number gutter**: logical line numbers along the editor's left edge, locked in alignment with text scrolling
|
|
43
|
+
(works in both plain-text and highlighted modes)
|
|
44
|
+
- **Image preview**: png/jpg/gif/webp/avif/svg and more render inline directly (same-origin byte route, 20MB limit)
|
|
45
|
+
- **Context menu** (VS Code style): right-click on files, folders, **any blank space in the panel, or the panel
|
|
46
|
+
header** — new file, new folder, rename, delete (recursive, with confirmation), copy path, copy / cut / paste,
|
|
47
|
+
refresh, open in system, select all, undo,
|
|
48
|
+
reveal in file explorer (a file is revealed selected in its folder, a folder opens directly;
|
|
49
|
+
Windows `explorer` / macOS `open`, automatically translated under WSL)
|
|
50
|
+
- **Reference files with @ in messages**: the file context-menu item "Reference with @" inserts
|
|
51
|
+
`@workspace-relative-path` into the chat input box; once sent, `@relative-path` mentions in the message render as
|
|
52
|
+
hyperlinks (click to open a preview in the workbench). The syntax is `@` + a workspace-relative path; any other
|
|
53
|
+
`@text` is always displayed as-is and has no special meaning
|
|
54
|
+
- **Drag files into the conversation**: drag a file or a multi-selection from the left panel anywhere into the chat
|
|
55
|
+
area and the path text is inserted at the input box cursor; dropping onto a directory row in the left panel is
|
|
56
|
+
still a move operation
|
|
57
|
+
- **Click anywhere to deselect**: clicking anywhere outside the tree (conversation / other panels / left panel
|
|
58
|
+
header) clears the selection
|
|
59
|
+
- **Copy / cut / paste**: Ctrl/Cmd+C/X/V (or the context menu), with cross-workspace paste support;
|
|
60
|
+
when the target already contains an item with the same name you are asked whether to overwrite, just like the
|
|
61
|
+
system file manager; the clipboard is cleared automatically after a successful cut (move)
|
|
62
|
+
- **Batch operations**: Ctrl/Cmd+click and Ctrl+A multi-select, delete selected items in bulk via the context menu or
|
|
63
|
+
the Delete key, drag the whole group to move it
|
|
64
|
+
- **Undoable operations**: the "↩" button at the top right or Ctrl+Z undoes the most recent operation (kept per
|
|
65
|
+
workspace, up to 30 entries): copy (removes the copy), cut/drag move (moves back to the original place), rename,
|
|
66
|
+
create, delete; deletion is an undoable delete (files are moved instantly into a hidden `.dsh-trash`, no bytes
|
|
67
|
+
copied)
|
|
68
|
+
- **Reveal in file explorer**: a context-menu item (equivalent to VS Code's Reveal in File Explorer) that locates the
|
|
69
|
+
file/folder in the system file manager (Windows Explorer, macOS Finder, etc.), executed via the `reveal` endpoint of
|
|
70
|
+
the loopback RPC
|
|
71
|
+
- Word wrap: one-click soft-wrap toggle in the tab bar (wrapping at the display layer only, file content untouched) or
|
|
72
|
+
horizontal scrolling for long lines; the preference persists
|
|
73
|
+
- Tabs: multiple files, drag to reorder, collapse/pop out; one-click light/dark theme switching
|
|
74
|
+
- Syntax highlighting: highlight.js (JS/TS, Python, JSON, HTML, CSS, Shell, and more;
|
|
75
|
+
Markdown renders a preview by default, switchable to source)
|
|
76
|
+
- Markdown rendering: markdown-it (raw HTML is always escaped, never executed); relative-path images are displayed
|
|
77
|
+
inline via the same-origin route `/dsh-plugin-files/raw/<path>`
|
|
78
|
+
- Disk changes: the host watches open files with `fs.watch` (watching the parent directory, surviving atomic renames);
|
|
79
|
+
changes are pushed over SSE at `/dsh-plugin-files/events`, and clean tabs sync automatically
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
No environment variables or config files needed; the layout patch is fully automated (the plugin automatically detects
|
|
84
|
+
and re-runs `scripts/patch-layout.mjs` at startup — idempotent, non-blocking; written against
|
|
85
|
+
`dsh-client-ui-layout@0.1.0-rc.6`):
|
|
86
|
+
- **Behavior preferences** (word wrap, light/dark theme, tab layout, file tree expansion state) persist per workspace
|
|
87
|
+
and need no manual configuration;
|
|
88
|
+
- **File operation channel**: via the loopback RPC `/dsh-plugin-files`; write operations are executed explicitly under
|
|
89
|
+
`danger-full-access`, with no external configuration options.
|
|
90
|
+
|
|
91
|
+
## Installation
|
|
92
|
+
|
|
93
|
+
> The layout patch is written against `dsh-client-ui-layout@0.1.0-rc.6`; other versions require updating the anchors.
|
|
94
|
+
> Since 0.0.9 the plugin detects and re-applies the patch automatically at startup (see the "Configuration" section
|
|
95
|
+
> for details).
|
|
96
|
+
|
|
97
|
+
```powershell
|
|
98
|
+
# npm (recommended)
|
|
99
|
+
dsh plugin --profile web add dsh-plugin-workbench
|
|
100
|
+
# or GitHub
|
|
101
|
+
dsh plugin --profile web add github:Pasumao/dsh-plugin-workbench
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Install from source (local development / debugging):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
git clone https://github.com/Pasumao/dsh-plugin-workbench.git
|
|
108
|
+
cd dsh-plugin-workbench
|
|
109
|
+
pnpm install
|
|
110
|
+
pnpm run build # produces lib/index.js and lib/client.js
|
|
111
|
+
# mount into the profile via link:
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
After installing, apply the layout patch and restart:
|
|
115
|
+
|
|
116
|
+
```powershell
|
|
117
|
+
node node_modules/dsh-plugin-workbench/scripts/patch-layout.mjs
|
|
118
|
+
# restart dsh web
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
> Since 0.0.9 the plugin automatically detects and re-applies the layout patch at startup — no need to run it manually
|
|
122
|
+
> after DSH upgrades; run the command above manually only if the automatic re-patching fails (broken anchors).
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```powershell
|
|
127
|
+
pnpm install
|
|
128
|
+
pnpm run build # produces lib/index.js (host) and lib/client.js (browser)
|
|
129
|
+
pnpm run typecheck
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Uninstall
|
|
133
|
+
|
|
134
|
+
```powershell
|
|
135
|
+
dsh plugin --profile web remove dsh-plugin-workbench
|
|
136
|
+
# and restore the layout bundle (patches/layout.backup/client.js.orig → dsh-client-ui-layout/lib/client.js)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Notes
|
|
140
|
+
|
|
141
|
+
- The `/dsh-plugin-files` RPC channel is loopback-only; write operations are executed explicitly under
|
|
142
|
+
`danger-full-access`; the context menu's create/rename/delete go through the same channel too (loopback trust,
|
|
143
|
+
same as saving in the editor)
|
|
144
|
+
- Image preview goes through the same-origin route `/dsh-plugin-files/raw/<path>`: it only responds to image
|
|
145
|
+
extensions, resolves the path via `ctx.fs.resolve → stat` first (sandbox-consistent path resolution) before reading
|
|
146
|
+
bytes, with a 20MB limit
|
|
147
|
+
- DSH upgrades overwrite the layout bundle; since 0.0.9 the plugin detects and re-applies the patch automatically at
|
|
148
|
+
startup, so there is no need to re-run it manually; if the anchors break, `scripts/patch-layout.mjs` must be updated
|
|
149
|
+
- This repository does not include DSH build artifacts
|
|
150
|
+
|
|
151
|
+
## Contributing
|
|
152
|
+
|
|
153
|
+
See [CONTRIBUTING](./CONTRIBUTING.md) and [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md); the changelog is in
|
|
154
|
+
[CHANGELOG](./CHANGELOG.md).
|
|
155
|
+
|
|
156
|
+
## Related plugins
|
|
157
|
+
|
|
158
|
+
This plugin is part of **Pasumao's dsh plugin ecosystem**; the published plugins in the same series can be used
|
|
159
|
+
alongside it:
|
|
160
|
+
|
|
161
|
+
| Plugin (npm) | GitHub | Description |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| [dsh-notify](https://www.npmjs.com/package/dsh-notify) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-notify) | Native Windows notifications + system tray |
|
|
164
|
+
| [dsh-plugin-choice-refresh](https://www.npmjs.com/package/dsh-plugin-choice-refresh) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-choice-refresh) | Choice enhancements: regenerate options / more options |
|
|
165
|
+
| [dsh-plugin-dev-kb](https://www.npmjs.com/package/dsh-plugin-dev-kb) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-dev-kb) | Plugin development knowledge base (full mirror of the official docs + skill) |
|
|
166
|
+
| [dsh-plugin-image-tools](https://www.npmjs.com/package/dsh-plugin-image-tools) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-image-tools) | Image choice cards + inline images in replies + image intake for text-only models |
|
|
167
|
+
| [dsh-plugin-table-zoom](https://www.npmjs.com/package/dsh-plugin-table-zoom) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-table-zoom) | Floating window for long chat tables + one-click Markdown copy |
|
|
168
|
+
| [dsh-plugin-windows-guard](https://www.npmjs.com/package/dsh-plugin-windows-guard) | [GitHub repo](https://github.com/Pasumao/dsh-plugin-windows-guard) | Windows environment safeguards: guideline skills + mojibake detection / dangerous-write interception / encoding diagnosis & repair |
|
|
169
|
+
|
|
170
|
+
> For the remaining plugins in the series, see [Pasumao · dsh plugins](https://github.com/Pasumao); if you find them useful, a ⭐ on GitHub is always welcome.
|
|
171
|
+
|
|
172
|
+
## AI generation statement
|
|
173
|
+
|
|
174
|
+
Part of the source code and documentation was generated with AI assistance (DeepSeek Harness) and has been
|
|
175
|
+
human-reviewed and verified on a live system; permission-related logic has been re-reviewed against the principle of
|
|
176
|
+
least privilege.
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
[MIT](./LICENSE)
|
package/README.md
CHANGED
|
@@ -6,13 +6,27 @@
|
|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
9
|
+
[**中文**](./README.md) | [English](./README.en.md)
|
|
10
|
+
|
|
9
11
|
**能直接改文件的 VS Code 风格工作台**——不是只读预览:文件树 + 可编辑代码预览
|
|
10
12
|
(语法高亮、标签页、行号栏)+ 右键文件操作(新建 / 重命名 / 删除 / 复制 / 剪切 /
|
|
11
13
|
粘贴 / 在系统中打开 / 在资源管理器打开)+ 图片内联预览,每个工作区独立保存状态。
|
|
12
14
|
装上之后,DSH 网页就是一个轻量代码编辑器。
|
|
13
15
|
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
+
> ✅ **开箱即用**:0.0.9 起布局补丁全自动——装完重启即生效;DSH 升级覆盖 bundle
|
|
17
|
+
> 后插件会自动补回,无需任何手动操作(仅锚点失效时才需手动,见「配置」节)。
|
|
18
|
+
|
|
19
|
+
装完你会看到:
|
|
20
|
+
|
|
21
|
+
- 左侧多出**文件树侧栏**(懒加载 + 2 秒自动刷新,每工作区独立展开状态);
|
|
22
|
+
- 消息里的 `@相对路径` 变成**可点击链接**,点开直接在工作台预览;
|
|
23
|
+
- 把文件从左栏**拖进聊天区**,路径自动插入输入框。
|
|
24
|
+
|
|
25
|
+
## 效果图
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
> 真实 Web GUI 截图。
|
|
16
30
|
|
|
17
31
|
## 功能
|
|
18
32
|
|
|
@@ -58,12 +72,9 @@
|
|
|
58
72
|
|
|
59
73
|
## 配置
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
(幂等、非阻塞;针对 `dsh-client-ui-layout@0.1.0-rc.6` 编写)。DSH 升级覆盖
|
|
65
|
-
bundle 后插件会自动补回;若自动重打失败(锚点失效),可手动运行
|
|
66
|
-
`node node_modules/dsh-plugin-workbench/scripts/patch-layout.mjs`;
|
|
75
|
+
无需环境变量或配置文件;布局补丁全自动(插件启动时自动检测并重跑
|
|
76
|
+
`scripts/patch-layout.mjs`,幂等、非阻塞;针对 `dsh-client-ui-layout@0.1.0-rc.6`
|
|
77
|
+
编写):
|
|
67
78
|
- **行为偏好**(自动换行、亮暗主题、标签布局、文件树展开状态)按工作区持久化,无需手动配置;
|
|
68
79
|
- **文件操作通道**:经 loopback RPC `/dsh-plugin-files`,写操作显式以 `danger-full-access` 执行,无外部配置项。
|
|
69
80
|
|
|
@@ -140,7 +151,7 @@ dsh plugin --profile web remove dsh-plugin-workbench
|
|
|
140
151
|
| [dsh-plugin-dev-kb](https://www.npmjs.com/package/dsh-plugin-dev-kb) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-dev-kb) | 插件开发知识库(官方文档完整镜像 + 技能) |
|
|
141
152
|
| [dsh-plugin-image-tools](https://www.npmjs.com/package/dsh-plugin-image-tools) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-image-tools) | 图片选择卡 + 回复内嵌图片 + 盲模型收图 |
|
|
142
153
|
| [dsh-plugin-table-zoom](https://www.npmjs.com/package/dsh-plugin-table-zoom) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-table-zoom) | 聊天长表格浮窗查看 + 一键复制 Markdown |
|
|
143
|
-
| [dsh-plugin-windows-guard](https://www.npmjs.com/package/dsh-plugin-windows-guard) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-windows-guard) | Windows
|
|
154
|
+
| [dsh-plugin-windows-guard](https://www.npmjs.com/package/dsh-plugin-windows-guard) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-windows-guard) | Windows 环境防坑:守则技能 + 乱码检测 / 危险写拦截 / 编码诊断修复 |
|
|
144
155
|
|
|
145
156
|
> 本系列其余插件见 [Pasumao · dsh 插件](https://github.com/Pasumao);觉得好用欢迎到 GitHub 点 ⭐。
|
|
146
157
|
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -15324,6 +15324,20 @@ window.__ModuleLoader__.load({
|
|
|
15324
15324
|
const PREVIEW_MIN = 240;
|
|
15325
15325
|
const CHAT_MIN = 240;
|
|
15326
15326
|
const PREVIEW_TOO_LARGE_LABEL = "512KB";
|
|
15327
|
+
/** Persisted split width (px) — survives reloads so a drag is never lost. */
|
|
15328
|
+
const PREVIEW_WIDTH_KEY = "dsh-plugin-workbench:preview-width";
|
|
15329
|
+
/** Read the persisted preview width; `null` means "use the default 55%". */
|
|
15330
|
+
function storedPreviewWidth() {
|
|
15331
|
+
try {
|
|
15332
|
+
if (typeof window === "undefined") return null;
|
|
15333
|
+
const raw = window.localStorage.getItem(PREVIEW_WIDTH_KEY);
|
|
15334
|
+
if (raw === null) return null;
|
|
15335
|
+
const px = Number(raw);
|
|
15336
|
+
return Number.isFinite(px) && px > 0 ? px : null;
|
|
15337
|
+
} catch {
|
|
15338
|
+
return null;
|
|
15339
|
+
}
|
|
15340
|
+
}
|
|
15327
15341
|
/** Same-origin raw-bytes route registered by the host half (see src/index.ts). */
|
|
15328
15342
|
const RAW_PREFIX = "/dsh-plugin-files/raw";
|
|
15329
15343
|
/** Same-origin SSE endpoint pushed by the host half (see src/index.ts). */
|
|
@@ -15385,6 +15399,22 @@ window.__ModuleLoader__.load({
|
|
|
15385
15399
|
function clamp(value, min, max) {
|
|
15386
15400
|
return Math.min(max, Math.max(min, value));
|
|
15387
15401
|
}
|
|
15402
|
+
/**
|
|
15403
|
+
* Nearest ancestor that actually takes part in layout. The slot system wraps
|
|
15404
|
+
* each slot's content in a `display: contents` element: children still join
|
|
15405
|
+
* the OUTER flex row, but the wrapper itself reports a 0×0 bounding rect.
|
|
15406
|
+
* Measuring that (the old `handle.parentElement`) made `max` collapse to
|
|
15407
|
+
* `PREVIEW_MIN` on the first move — the pane jumped to its minimum width and
|
|
15408
|
+
* could never be dragged back out. Skip every `display: contents` layer.
|
|
15409
|
+
*/
|
|
15410
|
+
function laidOutParent(el) {
|
|
15411
|
+
let node = el?.parentElement ?? null;
|
|
15412
|
+
while (node !== null) {
|
|
15413
|
+
if (getComputedStyle(node).display !== "contents") return node;
|
|
15414
|
+
node = node.parentElement;
|
|
15415
|
+
}
|
|
15416
|
+
return null;
|
|
15417
|
+
}
|
|
15388
15418
|
/** Parent directory of a path ('C:/a/b.md' → 'C:/a'; '' when there is none). */
|
|
15389
15419
|
function dirnameOf(path) {
|
|
15390
15420
|
const idx = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
@@ -15436,6 +15466,8 @@ window.__ModuleLoader__.load({
|
|
|
15436
15466
|
const highlightRef = (0, react.useRef)(null);
|
|
15437
15467
|
const textareaRef = (0, react.useRef)(null);
|
|
15438
15468
|
const gutterRef = (0, react.useRef)(null);
|
|
15469
|
+
const dragMoveRef = (0, react.useRef)(() => void 0);
|
|
15470
|
+
const dragUpRef = (0, react.useRef)(() => void 0);
|
|
15439
15471
|
const refresh = (0, react.useCallback)(() => bump((v) => v + 1), []);
|
|
15440
15472
|
const tabsRef = (0, react.useRef)(tabs);
|
|
15441
15473
|
(0, react.useEffect)(() => {
|
|
@@ -15625,26 +15657,72 @@ window.__ModuleLoader__.load({
|
|
|
15625
15657
|
document.body.style.removeProperty("--dsh-preview-width");
|
|
15626
15658
|
};
|
|
15627
15659
|
}, [isOpen]);
|
|
15660
|
+
(0, react.useEffect)(() => {
|
|
15661
|
+
const saved = storedPreviewWidth();
|
|
15662
|
+
if (saved === null) return;
|
|
15663
|
+
const preview = previewRef.current;
|
|
15664
|
+
const center = laidOutParent(preview);
|
|
15665
|
+
if (preview === null || preview === void 0 || center === null || center === void 0) return;
|
|
15666
|
+
const centerWidth = center.getBoundingClientRect().width;
|
|
15667
|
+
const max = Math.max(PREVIEW_MIN, centerWidth - CHAT_MIN);
|
|
15668
|
+
setPreviewWidth(clamp(saved, PREVIEW_MIN, max));
|
|
15669
|
+
}, [isOpen]);
|
|
15670
|
+
(0, react.useEffect)(() => () => {
|
|
15671
|
+
window.removeEventListener("pointermove", dragMoveRef.current);
|
|
15672
|
+
window.removeEventListener("pointerup", dragUpRef.current);
|
|
15673
|
+
window.removeEventListener("pointercancel", dragUpRef.current);
|
|
15674
|
+
}, []);
|
|
15675
|
+
/**
|
|
15676
|
+
* Start a split-width drag. Robustness notes (the "pane suddenly shrinks
|
|
15677
|
+
* and freezes" bug class):
|
|
15678
|
+
*
|
|
15679
|
+
* - Pointer capture reroutes every later pointer event to the handle, so
|
|
15680
|
+
* `pointerup` fires EVEN when the mouse is released outside the window.
|
|
15681
|
+
* Without it the up event is lost, `onUp` never runs, and the leftover
|
|
15682
|
+
* `onMove` keeps rewriting the width from its stale baseline on every
|
|
15683
|
+
* mouse move anywhere on the page — that is the "cannot drag / cannot
|
|
15684
|
+
* restore" state.
|
|
15685
|
+
* - `pointercancel` is cleaned up too (browser steals the pointer, e.g. a
|
|
15686
|
+
* tablet palm or an OS gesture).
|
|
15687
|
+
* - Pointerdown defensively removes any previous listeners first, so even a
|
|
15688
|
+
* capture-less leftover cannot survive into a second drag.
|
|
15689
|
+
* - The max is re-measured every move: the chat minimum is relative to the
|
|
15690
|
+
* CURRENT center column, which can change while the drag is in flight.
|
|
15691
|
+
*/
|
|
15628
15692
|
const onHandleDown = (0, react.useCallback)((e) => {
|
|
15629
15693
|
e.preventDefault();
|
|
15630
15694
|
const handle = handleRef.current;
|
|
15631
15695
|
const preview = previewRef.current;
|
|
15632
15696
|
if (handle === null || preview === null) return;
|
|
15633
|
-
const center = handle
|
|
15697
|
+
const center = laidOutParent(handle);
|
|
15634
15698
|
if (center === null) return;
|
|
15635
15699
|
const startX = e.clientX;
|
|
15636
15700
|
const startWidth = preview.getBoundingClientRect().width;
|
|
15637
|
-
const centerWidth = center.getBoundingClientRect().width;
|
|
15638
|
-
const max = Math.max(PREVIEW_MIN, centerWidth - CHAT_MIN);
|
|
15639
15701
|
const onMove = (ev) => {
|
|
15640
|
-
|
|
15702
|
+
const centerWidth = center.getBoundingClientRect().width;
|
|
15703
|
+
const max = Math.max(PREVIEW_MIN, centerWidth - CHAT_MIN);
|
|
15704
|
+
const width = clamp(startWidth + ev.clientX - startX, PREVIEW_MIN, max);
|
|
15705
|
+
setPreviewWidth(width);
|
|
15706
|
+
try {
|
|
15707
|
+
window.localStorage.setItem(PREVIEW_WIDTH_KEY, String(width));
|
|
15708
|
+
} catch {}
|
|
15641
15709
|
};
|
|
15642
15710
|
const onUp = () => {
|
|
15643
|
-
window.removeEventListener("pointermove",
|
|
15644
|
-
window.removeEventListener("pointerup",
|
|
15711
|
+
window.removeEventListener("pointermove", dragMoveRef.current);
|
|
15712
|
+
window.removeEventListener("pointerup", dragUpRef.current);
|
|
15713
|
+
window.removeEventListener("pointercancel", dragUpRef.current);
|
|
15645
15714
|
};
|
|
15715
|
+
window.removeEventListener("pointermove", dragMoveRef.current);
|
|
15716
|
+
window.removeEventListener("pointerup", dragUpRef.current);
|
|
15717
|
+
window.removeEventListener("pointercancel", dragUpRef.current);
|
|
15718
|
+
dragMoveRef.current = onMove;
|
|
15719
|
+
dragUpRef.current = onUp;
|
|
15646
15720
|
window.addEventListener("pointermove", onMove);
|
|
15647
15721
|
window.addEventListener("pointerup", onUp);
|
|
15722
|
+
window.addEventListener("pointercancel", onUp);
|
|
15723
|
+
try {
|
|
15724
|
+
handle.setPointerCapture(e.pointerId);
|
|
15725
|
+
} catch {}
|
|
15648
15726
|
}, []);
|
|
15649
15727
|
const onSave = (0, react.useCallback)(async () => {
|
|
15650
15728
|
if (active === void 0) return;
|