dsh-edit-turn 0.5.0 → 0.5.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/README.md CHANGED
@@ -61,7 +61,9 @@ dsh plugin --profile web add /path/to/dsh-edit-turn
61
61
  编辑器里,消息带的图片与文件**各显示成一个芯片**(缩略图或文件名 + 大小 + 删除按钮):不碰就原样保留,点删除就是不要它了;旁边还有一个「**添加附件**」(这条消息本来没有附件时也能用)。缩略图由本插件的只读路由按需取回;取不回来、或那些字节不是图片,芯片退回显示名字。**只有当这个部署没有附件存储时**,编辑器才会改回旧的那句话——「这条消息包含图片或文件附件,改写会丢弃它们,只保留文字」——并且不出现添加入口。
62
62
  **一字不改就按保存,什么都不会发生。** 编辑器预填的就是这条消息**现在**的文本,所以「打开编辑器、不输入、直接保存」等于没提出任何修改:宿主不写任何事件(不落替换、不开合成轮次、不追加回答),编辑器照常关闭、**一条提示都不显示**(0.2.20 起;这条提示此前压在输入框的占位文字上)。会话日志、轮次导轨、轨迹视图都保持原样——此前它照样落一条替换(一次会话里三条),改回答时还会多出一整个轮次(用户只发了两条消息,轨迹视图里却有四轮)。**「重跑」不受这条守卫影响**:文本没变但用户点的是「重跑」,宿主同样不写,重跑照常发起(用户的意图就是让这一轮重新生成)。
63
63
 
64
- **编辑模型的回答**:悬停任意一条模型回复,同样会出现编辑入口。编辑器上方多一个**块选择器**:一条回复同时带思维链和正文时,可以选「思维链」或「正文」,只改选中的那一块。改完点「保存」——记录只追加一条 `plugin:dsh-edit-turn/patch`,投影在模型下一次折叠时生效;**这条回答仍在原来的位置,后面的轮次原样不动**,工具调用和其他块也不动。转录里正文会就地显示改后的文字;思维链被改时会出现一条注记,展开就能看到模型实际读到的内容。
64
+ **术语**:编辑器里的「**思维链**」「**正文**」「**全部**」三个界面叫**页**;「全部」页里的「思维链」「正文」条目叫**子页**(也叫**块**)。页与子页指的是同一份内容,所以**在任一页里改动、切到别的页再切回来,改过的内容都还在**(只有关掉编辑器才会丢);子页之间也是同一份草稿。
65
+
66
+ **编辑模型的回答**:悬停任意一条模型回复,同样会出现编辑入口。编辑器上方多一个**页选择器**:一条回复同时带思维链和正文时,可以选「思维链」「正文」页,或切到「全部」页一次看完整轮。改完点「保存」——记录只追加一条 `plugin:dsh-edit-turn/patch`,投影在模型下一次折叠时生效;**这条回答仍在原来的位置,后面的轮次原样不动**,工具调用和其他块也不动。转录里正文会就地显示改后的文字;思维链被改时会出现一条注记,展开就能看到模型实际读到的内容。
65
67
 
66
68
  在退回重放路径的宿主上(或显式 `replyMode: replay`),行为仍是 0.2.21 的:整条回答被替换,后面的轮次回退再逐事件重放;这种模式下只能改正文,工具调用与思维链无法保留(编辑器会提示)。
67
69
 
@@ -252,8 +254,77 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
252
254
 
253
255
  **「点了保存没反应」先看这里**:记录里有没有 `apply`。没有 → 点击丢在浏览器半边(此前的成因是宿主在 mousedown/mouseup 之间重建了行,按钮已改为 pointerdown 激活);有 `apply` 但 `ok:false` → 宿主拒绝了,`code` 就是原因(`busy`/`stale`/...)。
254
256
 
257
+ ### 为什么没有 `replyMode: replace`(把回复也走 surface 替换)
258
+
259
+ 试过:像 `dsh-chat-thinking-editor` 那样 append 一条新的 `assistant/message`、用 replace op 遮蔽旧的那条,让宿主原生渲染。**当前宿主(0.2.1-rc.2)的 surface 校验直接拒绝**,两条独立规则:
260
+
261
+ 1. `session event "assistant/message" carries an invalid replace surfaceOp` —— replace op 的键必须是 `{op, startSeq, endSeq}`(对方的代码用 `{start, end}`)
262
+ 2. `assistant/message embeds its source stream and cannot carry sourceEventSeqs`,而同一校验又要求 `surface replace: sourceEventSeqs must include every shadowed surface node; missing N` —— 两条互相矛盾 ⇒ **assistant 消息不能被另一条 assistant 消息替换**
263
+
264
+ ### 为什么没有走 `conversation.chat.reasoning.body` 槽位渲染
265
+
266
+ 也试过:注册官方 `conversation.chat.reasoning.body`(assistant-step 的子槽,`kind: single`、`scope: session`,宿主默认实现 `priority: 100`,并转交工厂槽 `conversation.chat.reasoning.content` 画 markdown),用它原生渲染改写后的思维链。
267
+
268
+ 实测(真页面,2026-10-22):**注册成功但拿不到槽位**——
269
+
270
+ - 注入回调确实执行(在 `document.documentElement.dataset` 上留了标记,读到 `1`)
271
+ - 组件从不渲染:`[data-dshet-reason-body]` 计数恒为 0
272
+ - `priority: 200` / `priority: 50` / 不传(由 runner 分配负值)三种都试过,宿主默认实现始终是渲染者
273
+ - 没有出现任何「registration refused」告警,说明注册被接受、但 `single` 槽位仍由宿主持有
274
+
275
+ 结论:**当前版本下第三方插件无法接管这个 single 子槽位**。所以「原生渲染」的两条路都封着:assistant 替换被 session 校验拒绝;思维链正文槽位不可顶替。界面侧只能继续用本插件的 DOM 补丁(已验证可用:`[data-dshet-patch]` 落在 `_text_*` 思维链节点上,内容为投影后的文本)。
276
+
277
+ 要真正摆脱 DOM 补丁,需要上游给投影一个原生渲染口,或者允许 assistant 替换、或允许插件 shadow 一个 single 子槽位。
278
+
255
279
  ### 更新日志
256
280
 
281
+ **(工作区)** —— 会话 id 校验放宽 + 空会话按空状态处理。
282
+
283
+ - **会话 id 不再只认 UUID**:命名会话(例如 `bili-whale`,`~/.dsh/storages/<profile>/sessions/bili-whale.json`)是完全合法的会话,但这里的校验只接受 UUID 形式,于是对它的每个请求都被 400 拒掉 —— 面板上就是「状态加载失败,稍后重试。」。现在按 DSH 自己的语法(宿主侧 schema 就是 `z.string().min(1)`)放宽为 `/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/`,仍然挡住 `/`、`\`、`..`、空白与控制字符。
284
+ - **查不到会话按空状态处理**:没有日志的会话(刚建的命名会话)返回空状态而不是 404 —— 面板正常加载、显示"没有可操作的内容",日志一出现下一次刷新就接上。
285
+
286
+
287
+ **0.5.3** —— 编辑框随内容长高;隐藏兜底不再吞掉第三方插件的按钮。
288
+
289
+ - **换页不再丢草稿**:以前「正文」页写的内容只存在单块草稿里、「全部」页的子页各自另存一份,而且切页时草稿会被重置成记录原文 —— 于是「正文」页写个 1、切到「全部」页,子页里没有这个 1;切回来也没了。现在三页共用**一份按块索引的草稿**(`正文` 页与它在「全部」页里的子页是同一个块、同一个 key),任何一页里打的字,切到别的页、再切回来都还在;保存时发出的也是草稿(真页面复验:正文页写 `1` → 全部页正文子页显示 `1` → 在全部页改思维链子页 → 切回正文页仍是 `1` → 切到思维链页是 `thought draft` → 再切回正文页仍是 `1`)。
290
+
291
+ - **往下滚自动弹回顶部(主要问题)**:测量每个块的 textarea 要把它塌成 `height: auto`,整个文档随之变矮,浏览器就把滚动条的 `scrollTop` 夹到新的上限(≈0)—— 高度随后虽然恢复,**位置已经丢了**。宿主在流式回复时会不停重绘,所以表现为「滚到下面自动回上去」。实测:滚到底(`scrollTop 847 = max 847`)后触发一次布局,`scrollTop` 变 **0**。修法:新增 `withScrollKept(scroller, work)`,测量前后保存/恢复滚动位置;「全部」页的整体重测、逐块撑高、单块模式的测量全部走它。修后同样操作 `scrollTop` 保持 **847**。
292
+ - **最后一行滚不到**:同一次「设成 `auto` 再读 `scrollHeight`」的读取**永远少一行**(换行在写完样式之后才落定),元素被钉在偏小高度且 `overflow:hidden`,外层滚动条按这个值算高度 → 最后一行既看不见也滚不到(实测:一次读 2392、二次读 2415,行高 23px)。修法:抽出 `settleAreaHeight()`,重置后**读两次**再落定。
293
+ - **真页面复验**:正文灌到 1200 / 3000 / 6000 / 12000 字符,`clipped` 全为 0、滚动条都能到底、最后一行完整可见且停在底栏上方 8px;滚到底后连续两次布局,位置不动。
294
+ - **改写后的提示词气泡跟随 Markdown 渲染**:装了 `dsh-markdown-bubble` 时,改写后那条气泡(`.dshet-revision`)不再固定成纯文本,而是走宿主自己的 `MarkdownText`(用 `react-dom/client` 挂载 —— 宿主自己的包也是这么用的,已实测可行)。判据是那个插件自己的存在标记 `globalThis.__DSH_MARKDOWN_BUBBLE__`:**装了才渲染 Markdown,没装就保持纯文本**,因为没装时宿主画的其它用户气泡就是纯文本,只有跟着一致才不会出现「改写过的和没改写过的长得不一样」。理由与上游建议见 [deepseek-harness#10704](https://github.com/deepseek-ai/deepseek-harness/discussions/10704)。
295
+ - **编辑时框跟着内容长高**:以前框高在打开那一刻定死(「一轮最大」还带缓存),打字变多也不长 —— 内容超出后只能靠框内滚动条。现在:**打字变多就长高,删字就变矮,但不会比「打开时」更矮**(这道地板属于这一次编辑,切页也保留,保存或点叉才结束)。方向看空间:下面够就在下方长,下面不够就把底边钉在输入框卡片上沿、往上长(实测底边 680 / 输入框卡片上沿 688,留 8px 边距);整段都装不下就铺满对话区上下边界、由框内滚动。三档「高度」偏好只决定**打开时**多大,编辑中的跟随规则三档一致(设置页的说明文案已同步)。
296
+ - **隐藏兜底会吞掉第三方插件的按钮(issue #1,静默失败)**:折叠一条被改写的消息时,那段兜底是「除我自己的三个类名外,兄弟节点一律隐藏」。`dsh-rewind-pro` 这类插件把图标注入到同一条用户消息里(动作条旁边 / 插槽旁边),于是被一并隐藏 —— DOM 里在、点得到、屏幕上 `display:none`,**没有任何报错**,自动化测试(`querySelectorAll` 数得到元素)也全绿,所以很难发现。现在只隐藏**装着消息正文**的那个子树:兄弟节点里不含正文的一律不动(`messageContentRoot` 找不到宿主形状时仍退回旧行为,免得改写后的消息留在屏幕上)。回归测试里注入两个第三方节点(动作条旁、插槽旁),断言它们保持可见、且不带本插件的折叠标记 —— 改回旧写法该测试转红。
297
+ - **补上 `error.session-empty` 的中英文案**:宿主半部对「还没有内容的会话」会报这个 code,客户端字典里缺它,静态校验(`verify:client`)一直在红。
298
+ - **单块页(思维链 / 正文 / 用户输入)滚不动**:真因不是滚轮,而是**滚动位置活不过下一次布局** —— 给 textarea 赋 `value` 会把它的滚动位置扔回顶部(Chromium 实测:赋**同一个**字符串位置不变,赋**另一个**字符串位置归零,再赋回原值也回不来)。而「一轮最大」的高度测量正是把**同一轮其它块**的文字写进这个输入框来量的,于是每次**缓存未命中**的布局(第一次布局、切页、以及一轮里块还在变的时候——也就是模型流式输出期间)都会把读者扔回顶部。修法:测量前后保存/恢复 `scrollTop`。真页面复验:用户输入页灌 3000 字 → 滚到 400 → 强制重新测量(改宽度 + 输入)→ 仍是 **400**。
299
+ - **思维链页保存后立即渲染**(两层原因):① 宿主对一条思维链会保留**多份拷贝** —— 折叠时那一行留在 DOM 里但 `display:none`,展开面板时再渲染一份**可见的**正文;而补丁逻辑只要「记住的那个元素还连在文档里」就认为已经打过补丁并 `continue`,于是**用户正在看的那一份**始终是记录原文,直到整页刷新(刷新会从日志重新折叠,宿主自己就画对了)。现在这一支会**把所有拷贝一起打上**(记住的元素、所有带标记的元素、所有文本等于原文/上一次文本的元素,含 `findReasoningElements` 这个复数版搜索),实测同一次保存后 `data-dshet-patch` 出现两份 `335:0`。② 一条回复的块列表是**整轮**的(宿主把同一轮里每一步的思维链和正文都挂在回答行上,每块带自己的 `seq`),于是同一份列表里会有多个 `index` 相同的块(第一步的思维链和第四步的思维链都是 `index 0`)。以前两处都只按 `index` 找块:保存后本地那条「乐观」补丁还按**回答行的 seq** 记,渲染时又用 `find(index === …)` 命中**第一个**同 index 的块 —— 两处都拿错了 `original`,于是补丁在 DOM 里匹配不到任何东西,思维链正文要等刷新才变。现在补丁按**块自己的 seq** 记,查块按 **(seq, index)** 查,并顺着块找到**承载它的那条回复条目**(后一步的块住在以回答行 seq 为键的条目里)。
300
+ - **测试**:`npm test` **254/254**(新增六例:补丁只落在目标步的思维链上;保存后思维链正文立即变;后开的思考面板同样显示改写;测量后 textarea 的滚动位置不变;编辑时随内容长高、删字只矮回打开时的高度;下方没空间时向上长而不压输入框 —— 六例在改回旧写法时都会失败)。(新增五例:换页保留草稿;模拟「第一次读少一行」的 textarea 断言最终高度;模拟浏览器夹滚动条断言测量后位置不变;装了 markdown 插件时改写气泡走宿主渲染器、没装时保持纯文本 —— 把修法还原成旧写法时都会失败)。
301
+
302
+ **0.5.2** —— 回复编辑不再等这一轮回复结束;编辑器跟着当前皮肤走。
303
+
304
+ - **回复编辑不再被「正在回复中」挡住**:就地记录(patch)只 append 一条 `ignorable` 事件,不改任何 surface 节点,所以它可以在 agent 还在写的时候落盘 —— 模型下一次折叠就读到。宿主侧只在**会移动 surface** 的路径上保留忙时拒绝:改写提示词(surface replace)和 replay 兜底(遮蔽窗口 + 重放尾部)。客户端同步:回复就地编辑不再禁用保存、不再显示「该会话正在回复中」横幅;提示词编辑与 replay 路径照旧拦下并说明。
305
+ - **测试**:回复编辑在未闭合的 turn 中保存成功、提示词改写仍被拒、整轮多块在忙时保存成功。
306
+
307
+ - **颜色/圆角/阴影改用主题令牌**:文字、次级文字、三级文字、描边、主色、圆角(`--dsw-radius-panel/xl/lg/md/sm`)、阴影(`--dsw-shadow-lv3`)都读当前主题的令牌,取不到时退回本插件原来的值;悬停底色用 `color-mix` 从主题墨色算出来。
308
+ - **面板底色取皮肤的“色相”,不取它的透明**:皮肤把界面画在插画上时会把表面色做成半透明(靠自己的文字描边保证可读),插件元素没有描边。现在运行时探测 `--dsw-alias-bg-layer-1/2`:把它的**颜色**原样拿来、透明度强制为 1,于是「中秋」这类**深色插画皮肤**得到的是同色系的深色面板,而不是插件自带的浅色面板(这类皮肤不设 `data-ds-dark-theme`,旧代码只能退回浅色)。
309
+ - **调色板挂在 `body` 上**:主题令牌声明在 `body`,而自定义属性是在**定义处**作用域内做替换的 —— 原来挂在 `:root` 上,所有 `var(--dsw-*)` 都静默取了 fallback(实测:改了等于没改)。
310
+ - **就地编辑器不再刷白底**:`pageBackground()` 原来读 `body` 背景,插画皮肤下 `body` 是透明的 → 退回 `#ffffff`,于是深色皮肤上出现一个白框。现在优先用已解析的 `--dshet-panel`(按上面规则一定不透明)。
311
+ - **改写后的提示词气泡用皮肤自己的气泡色**(`--dsw-specific-bubble`),圆角用 `--dsw-radius-xl`,与真实用户气泡一致。
312
+ - **测试**:`npm test` **241/241**、`npm run verify:contract` **96** 项、`npm run verify:client` **192** 项。
313
+
314
+ **0.5.1** —— 思维链就地画出来、保存不再“点不动”、同一个块可以反复编辑。
315
+
316
+ - **删掉「思维链已改写(模型可见)」卡片**:reasoning 正文只要在 DOM 里就就地改写;没有正文时不再插入任何占位卡片。
317
+ - **无限次编辑**:记录「上一次画上去的文本」,第二、第三次编辑同一个块时仍能找到元素并更新。
318
+ - **projection 缺失不再 500**:宿主用 `... was removed or replaced` 拒绝时,单块回复保存自动降级走 replay 路径;「全部」多块保存返回 `projection-missing`,提示重启 DSH。
319
+ - **忙时不再假死**:客户端从 `/state` 读 `busy`,会话还在回复时直接禁用保存并显示整条提示「该会话正在回复中…」,每 3 秒复查一次,回复结束自动解除。
320
+ - **失败提示变醒目**:in-place 的 `.dshet-error` 从底栏左侧小字改成整条横幅(左右 8px、底 52px、带边框/阴影)。
321
+ - **确认步不再死循环**:保存键处理函数改读实时 controller 状态;确认步给按钮加可见强调圈;`/apply` 增加 20 秒超时,宿主挂住时恢复按钮并报错。
322
+ - **从「全部」切回单块不再残留**:选择器切回某一块时没有把 `allBlocks` 置回 false,重绘又画成「全部」视图、选择器却写着「正文」(两个输入框 + 单块选择)。现在切回单块会明确退出全部视图,`allBlocks` 也进了编辑器重建标记;选到不存在的块时同样退出全部视图并保留原先选中的块。
323
+ - **思考行找对了**:官方把 reasoning 画在**它自己那一行**(flow key 带 `,reasoning` 分组),而按 seq 找到的行是**正文行** —— 旧代码只在那一行里找,所以思维链改写永远落不到 DOM(正文改写正常,因为正文就在那一行)。现在思维链的搜索改为**在整个会话里找“文本等于记录原文”的最深节点**,并和其它块一样登记 `data-dshet-patch`(可回退)。
324
+ - **折叠的思考也能看到改写**:思考面板收起时正文根本不在 DOM 里,而头部那行 `思考 · <截断片段>` 是宿主按**记录的原文**画的 —— 于是改了模型上下文、界面却像没生效。现在保存后连**头部片段**一起按改写后的文本重画(保留宿主的省略号与截断长度、不碰它自己的标签)。
325
+ - **长内容不再被底栏压住**:输入框高度原来只减了顶栏(`height - offsetTop - 8`),底部 40px 落在控制带下面 —— 框内能滚动,但最后几行永远被 X/保存那条带子盖住。现在按底栏实际高度(`offsetHeight`,取不到时用 46px 常量)预留,输入框正好停在控制带上方;内容更高时在框内滚动。
326
+ - **测试**:`npm test` **235/235**、`npm run verify:contract` **96** 项、`npm run verify:client` **184** 项。
327
+
257
328
  **0.5.0** —— 从「一个气泡的编辑器」长成「整轮可编辑 + 可导航的文档」。
258
329
 
259
330
  - **整轮所有块**:一条回答里每个 step 的 reasoning 与每个 text 都进 picker,同类自动编号(思维链 1 / 正文 1 …),每块带自己的 seq,保存时 patch 到它所在的消息。