dsh-ide-pane 0.0.0-stage → 0.11.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zhuyuelei
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,185 @@
1
- # Temporary Holding Version
1
+ # dsh-ide-pane
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ DSH 右侧栏的「代码」面板:在当前会话里浏览、编辑、运行工作区文件,并在 Agent 改动文件后给出可读的差异。
4
+
5
+ 它不试图复制一套小号 VS Code;重点是回答一个会话语境里的问题:**Agent 到底改了什么,我现在的保存会不会覆盖它。**
6
+
7
+ ## 能力
8
+
9
+ | 能力 | 当前行为 |
10
+ |---|---|
11
+ | 文件树 | 懒加载目录,目录在前、文件在后,按名称排序 |
12
+ | 源码视图 | 行号、Python / JS / CSS / Markdown 轻量语法着色、横向滚动 |
13
+ | 编辑 | 「编辑」进入文本框,`Ctrl+S` 保存;保存后自动重读 |
14
+ | 差异视图 | 永远对比「这一次观察到的内容」和「上一次观察到的内容」;有变化时标题栏出现「差异 •」,按行标 `+` / `-`,未变部分折叠成「··· N 行未变 ···」。关掉再点开、切走再回来、刷新页面,对比都还在 |
15
+ | 会话版本 | 树上给 Agent 改过的文件(及其所在目录)打 `●`;打开这样的文件,代码区上方出现版本条,逐条列出这次会话里的每次改动,选一条就只看那一次的前后差异 |
16
+ | 版本保护 | 打开文件时记录内容 hash;如果 Agent 在编辑期间改了文件,保存会拒绝覆盖并给出「看差异 / 载入新版本 / 保留草稿」选择 |
17
+ | 运行 | 走插件自己的 `text/event-stream` 路由:输出边产生边显示,颜色和 `\r` 都保留,可「■ 停止」 |
18
+ | 选区引用 | 编辑时选择一段文本,生成 `@file:start-end`;按钮把引用插入当前会话输入框,输入框版本变化时回退为复制 |
19
+ | 拖拽与复制 | 文件行可以拖到对话框,松手即插入 `@路径` 引用;右键菜单提供「引用到对话 / 复制路径 / 复制内容 / 打开」 |
20
+ | 跟随改动 | 打开文件每 2 秒比一次版本号:优先用第一方 `workspaceFiles.stat`,失败或不可用时退回插件自己的内容 hash;两者都失败会明确提示「无法检测文件变化」,不静默假死 |
21
+ | 入口 | 右侧栏「+」中的「代码」;`Ctrl+Alt+I`;或**在对话里点文件引用**——直接打开这个面板并定位到该文件 |
22
+
23
+ **断点、单步、变量检查仍未实现。** 这需要 Node CDP 或完整 DAP 客户端,是独立于文件面板的调试子系统。
24
+
25
+ ## 差异怎么算
26
+
27
+ `diffLines(before, after)` 先裁掉公共前缀和后缀 —— 这正是「Agent 改了三行」的常见情况,代价接近线性。剩下的中段走 LCS 表;超过 250000 格的预算就退化成「整块被替换」,因为整文件重写不值得一张二次表,而说「这段被替换了」依然是真话。
28
+
29
+ 渲染时只保留每处改动上下各两行,其余折叠成「··· N 行未变 ···」,所以两千行的文件也不会刷出两千个节点。
30
+
31
+ 两条差异通路共用同一个渲染器:
32
+
33
+ - **与上一次比较**:每次读到文件内容都记一条「当前文本 + 上一次文本」。显示差异时比的就是这两者,所以关掉差异、切走标签、刷新页面都不会丢失——它存在存储里,不在视图状态里。
34
+ - **保存冲突**:Host 返回冲突时,「看差异」对比你的草稿和磁盘内容,`−` 是草稿、`+` 是磁盘。
35
+
36
+ ### 版本记录存在哪
37
+
38
+ `localStorage`,键为 `dsh-ide-pane/base/v1/<sessionId>/<规范化路径>`,值 `{ text, prev, at }`。
39
+
40
+ - `text` 是最近一次观察到的内容,`prev` 是它之前的那一版;
41
+ - 路径先规范化(统一成 `/`),所以从文件树点进来(`D:\a\b.py`)和从对话引用点进来(`D:/a/b.py`)共享同一条记录;
42
+ - 每个文件最多存 200 KB,超出就不记(大文件对比也没有意义);
43
+ - 最多保留 40 个文件,超了按 `at` 淘汰最旧的;
44
+ - `localStorage` 不可用时退化为内存 `Map`,当次会话内仍然有效。
45
+
46
+ 规则只有一条:**内容变了才推进**。重复读到同样的内容不动记录,因此最后一次真实改动会一直保留着,点多少次「差异」都是一样的对比。想主动忘掉它,点「清除对比」。
47
+
48
+ 第一次打开某个文件时 `prev` 为空,没有可比对象——这是这套机制的信息边界,不是 bug。
49
+
50
+ ## 三条通路
51
+
52
+ | 能力 | 通路 | 原因 |
53
+ |---|---|---|
54
+ | 读取 / 文件变化 | 第一方 `remote.workspaceFiles` | 使用会话 workspace 作用域与服务端越界检查 |
55
+ | 写入 / 运行 / 内容版本 | 插件受限 HTTP 路由 | 第一方 workspace namespace 只读;第三方 Remote 需要 Typert 构建期生成 |
56
+ | 会话版本 | `ctx.sessions.get(id).snapshotEvents()` → 插件受限路由 | 会话日志是第一方只读事实,插件不能写、只能折叠 |
57
+ | 选区引用 | 侧栏 slot 的标准 prop `inputActions` | 直接走宿主自己的 Lexical 编辑器,插入是一次可撤销的编辑 |
58
+
59
+ ### 运行输出为什么是流式的
60
+
61
+ `/dsh-ide-pane/runStream` 是一条 `text/event-stream` 路由:宿主每收到一块输出就 `res.write` 一帧,浏览器用 `response.body.getReader()` 增量读取,所以输出边产生边显示。中止 fetch 会关闭响应,宿主在 `res.on('close')` 里 `SIGKILL` 子进程——这就是「停止」按钮。
62
+
63
+ 帧格式:
64
+
65
+ ```text
66
+ {type:'start', command}
67
+ {type:'out'|'err', text}
68
+ {type:'end', code, ok, ms, command}
69
+ {type:'fail', error}
70
+ ```
71
+
72
+ **一个关键的环境变量**:`PYTHONUNBUFFERED=1`。Python 在非 TTY 下默认块缓冲 stdout,没有它,脚本输出会在退出时一次性到达——管道明明是通的,看起来却像没有流式。`PYTHONIOENCODING=utf-8` / `PYTHONUTF8=1` 则保证中文不被管道边界切坏。
73
+
74
+ 浏览器侧还有两件清理:
75
+
76
+ - `sanitizeTerminal` 只保留 SGR(颜色),丢掉光标移动、擦除、OSC——纯文本视图无法兑现这些;
77
+ - `applyCarriageReturns` 把 `\r` 按终端语义折叠(本行重写),进度条才是一行变化而不是一列快照。先归一 `\r\n`,否则行尾那个 `\r` 会把整行清空。
78
+
79
+ ### 拖到对话框为什么会变成引用
80
+
81
+ `dragstart` 里把 `text/plain` 设成 `@路径`(含空格时用 `@"…"`)。输入框是宿主自己的 Lexical 编辑器,它本来就会把拖进来的 `text/plain` 插到落点上——所以最朴素的做法就能用。
82
+
83
+ 但「能插入」和「一定插入」是两件事:外壳可能给拖放加了自己的遮罩,或者编辑器在落点判断上挑剔。所以另外在 `document` 上挂一个捕获阶段的 `drop` 监听:只要这次拖拽**是从本面板发起的**(模块级 `draggingReference` 标记),就由面板自己调用输入框的 `insertText`,并 `preventDefault` 吃掉事件,避免文字被插入两次。落回面板内部则视为取消。
84
+
85
+ `insertText` 失败时回退到剪贴板,和选区引用共用一条路径。
86
+
87
+ ### 对话里点文件为什么能进这个面板
88
+
89
+ 侧栏的 tab 类型登记表在**跨 kind** 的范围内给候选类型排序:先比 `priority` 档位,再比命中 pattern 的长度。DSH 自带的文本预览登记为 `fallback`(最低档),这个插件登记为默认的 `extension`,所以同一个 `dsh-resource://file/…` 地址会被本插件抢到。
90
+
91
+ 具体做法是给自己的 `ide` kind 加上:
92
+
93
+ ```js
94
+ patterns: ['dsh-resource://file/[\\s\\S]+'],
95
+ canOpen: (address) => 是文本类扩展名,
96
+ title: (address) => 文件名,
97
+ ```
98
+
99
+ `canOpen` 是必需的:图片、PDF、Office 文档也是文件地址,抢过来会把字节当源码渲染。只有已知的文本扩展名(以及 Makefile、`.env` 这类无扩展名/点文件)才接管。
100
+
101
+ 地址格式是 `dsh-resource://file/session/<sessionId>/<每段单独 percent-encoded 的路径>`,解析后既能拿到路径也能拿到会话 id——所以从别的会话点文件也能读对。
102
+
103
+ 想换回自带的文本预览:把 `client.js` 里的 `CLAIM_FILE_TABS` 改成 `false`,它就不再声明 pattern。
104
+
105
+ ### 会话版本从哪来
106
+
107
+ `edits` 只读会话日志,把 `tool/call` 里真正写文件的调用挑出来:
108
+
109
+ | 形态 | 识别方式 | 能给出什么 |
110
+ |---|---|---|
111
+ | 片段替换 | `old_string`/`new_string`,或 `old_str`/`new_str` | 完整的前后片段,可做真实 diff |
112
+ | 整体写入 | `content`(或写入类工具的 `text`) | 只有写入后的内容;旧内容不在日志里 |
113
+ | 只提到路径 | 写入类工具名 + `file_path` | 只够在树上打标,没有差异 |
114
+
115
+ 摘要不带正文,树上轮询它很便宜;只有真的点开某一条时才用 `editText` 从单个事件里取正文。
116
+
117
+ 折叠是增量的:缓存记住已折叠的最大 `seq`,下次只读新尾部。日志 `seq` 的基准(0 起还是 1 起)没有写进契约,所以代码不猜——用包含下界的读取,再靠 `event.seq < from` 过滤;fork 或日志重置让水位倒退时自动全量重扫。
118
+
119
+ 单个文件超过 12 条改动时,版本条只显示最近 12 条,更早的折成「更早 N 条」。
120
+
121
+ 注意这里**不是**「完整文件的第 N 版」:片段替换只记录被替换的那一段,所以能精确说「这一次改了什么」,但给不出历史上某个时刻的整份文件。人工在面板里的保存也不在日志里——第三方插件写不进去。
122
+
123
+ Host 路由位于 `POST /dsh-ide-pane/*`:
124
+
125
+ ```text
126
+ stat { root, path } → { ok, exists, version, bytes }
127
+ write { root, path, text, baseVersion? } → { ok, version, bytes } | { conflict, currentVersion, currentText }
128
+ run { root, path, command? } → { ok, code, stdout, stderr, ms, command }
129
+ edits { sessionId } → { ok, seq, files: { path: [edit] } }
130
+ editText { sessionId, seq } → { ok, kind, path, old, new }
131
+ ```
132
+
133
+ 路径必须落在 `root` 内。请求还必须带 `x-dsh-ide-pane: 1`,并且 `Origin` 的 host 必须与当前 DSH 主机相同。
134
+
135
+ 内容版本是插件自己算的 sha1 前 16 位,因为第一方 `stat` 报的版本号 Host 这边复现不出来;两端用同一个函数才能判断「这个文件还是我打开时那份吗」。
136
+
137
+ ## 结构
138
+
139
+ ```text
140
+ index.js Host:受限 stat/write/run 路由 + 会话日志折叠(edits/editText)
141
+ client.js 文件树、改动脉标、版本条、查看编辑、差异、选区引用、运行与冲突 UI
142
+ test/smoke.mjs Client 注册契约
143
+ test/host.mjs 真实 HTTP:版本、冲突、写、跑、护栏、越界、会话版本与增量折叠
144
+ test/render.mjs 迷你 Hook:文件、引用、版本化保存、运行、Agent 差异、冲突差异、版本条
145
+ ```
146
+
147
+ ## 测试
148
+
149
+ ```bash
150
+ node --check index.js
151
+ node --check client.js
152
+ node test/smoke.mjs
153
+ node test/host.mjs
154
+ node test/render.mjs
155
+ ```
156
+
157
+ `test/render.mjs` 也直接断言了 `diffLines`、`selectedLines`、`lineReference` 这三个纯函数,包括相同文本、单行修改、插入、删除、整文件重写和带空格路径的引用格式。
158
+
159
+ ## 安装 / 更新
160
+
161
+ 从 npm 安装:
162
+
163
+ ```bash
164
+ dsh plugin add dsh-ide-pane
165
+ ```
166
+
167
+ 从本地源码开发(改完立刻生效):
168
+
169
+ ```bash
170
+ dsh plugin add link:D:/path/to/dsh-ide-pane
171
+ ```
172
+
173
+ Host 半边可通过禁用再启用 `include:dsh-ide-pane` 重新挂载;Client 代码改变后需刷新当前 DSH 页面。
174
+
175
+ ## 已知限制
176
+
177
+ - 单文件最多渲染 2000 行。
178
+ - 文件树根取自 Session `cwd`;不是绝对目录时编辑和运行不可用。
179
+ - 「运行」不是终端:能流式、能停、保留颜色与 `\r`,但没有 stdin、没有光标定位、没有全屏 TUI。交互式脚本和 vim/top 这类程序请用侧栏终端。
180
+ - 差异对比的是「上一次观察到的内容」,不是 git HEAD:面板不做版本库对比,也不读取未打开过的历史内容。
181
+ - 会话版本是「每一次改动」,不是「完整文件的第 N 版」:片段替换只带被替换的那一段,整体写入没有旧内容。人工保存也不在会话日志里。
182
+ - 树上只标记**当前会话日志里**出现过的文件;别的会话、别的工具(终端里 `git checkout` 之类)改的文件不会被标记。
183
+ - 一个会话最多折叠 400 次写文件调用,超出的更早改动不再计入。
184
+ - `@file:start-end` 中的行范围是给模型和人看的文本后缀;当前第一方文件引用解析器只把 `@file` 本体作为原子引用。
185
+ - 没有断点调试。