- Home
- Plugins
- UI Enhancements
- dsh-annotation
dsh-annotation
zengxiaolou/dsh-annotation
Annotate quoted answer text in the transcript: each selection keeps its quote plus a note of your own, notes pile up as chips in the composer and travel with the message, and two or more also get a chip above the input to jump back or clear them. The chips need dsh-paste-chip installed; without it each note is a plain text line in the draft.
Install
dsh plugin --profile web add github:zengxiaolou/dsh-annotationREADME
dsh-annotation

划选对话里的回答文字 → 就地写一句注释 → 每条进草稿成为一枚 注释 · N 胶囊,多条一起随消息发出;
两条以上时,输入框上方会多一个 N 条注释 胶囊用来跳回原文 / 全部清除。
效果

示意图(按主题令牌渲染):划选回答里的文字 → 就地写一句批注 → 批注成为
注释 · N胶囊随消息发出;两条以上时输入框上方出现N 条注释胶囊,可跳回原文或全部清除。
交互
- 在任意回答里划选一段文字。
- 浮出的评论框里可以写备注,也可以留空(纯引用)。回车保存,Esc 取消。
- 选中的文字就地保留淡色底纹,末尾挂一个编号圆点;点圆点可以再编辑那条注释。
- 两条以上时,输入框上方出现
N 条注释胶囊:悬停或点击才浮出详情卡(逐条删除、全部清除、点条目跳回原文),平时不占位。只有一条时不出——草稿里那枚注释 · 1已经说清楚了,多一个「1 条注释」只是把同一件事说两遍。 - 注释以单行文本写进草稿,自己按回车或点发送即可带上,不需要额外按钮。
为什么只有一个注释时不弹 N 条注释(v0.1.6)
那个胶囊原本是「注释只活在面板里」时的唯一入口。现在注释都进了草稿(每枚 注释 · N 都能点开
卡片、✕ 取消),它的不可替代能力只剩两件:
| 能力 | 草稿胶囊能做吗 |
|---|---|
| 跳回原文(注释可能在上千行之上) | 不能:点开是正文预览卡 |
| 全部清除 | 不能:只能逐枚 ✕ |
这两件在「只有一条」时都不需要(高亮就在眼前、清空无意义),所以门槛设在 2 条: 一条不占位,两条以上才提供跳转与清空。
草稿里长什么样
每条注释一行,紧跟在你自己写的文字后面:
〔注释 1〕「你要的是旧版「六根线跳动」的」|备注:这句按旧版改
草稿是唯一事实来源:你在输入框里删掉这一行,胶囊里的那条也会同步消失;消息发出去之后同样自动清理。
实现要点
- 浮层挂在
shell.overlay(整帧浮层,压在对话滚动容器之上),高亮用Range.getClientRects()计算、绝对定位覆盖绘制,不改动被选中的 DOM,所以回答流式重绘时不会破坏原文。 - 胶囊挂在
conversation.input.dock(输入框上方一整条)。 - 发送走
inputActions.captureInsertion() / insertText(),失败时回退setDraft(),最后submit()。 - 摘录保留选区的富文本(只放行
code/strong/em/b/i/span/br,其余标签降级为纯文本),所以行内代码在列表里仍然是代码样式;用户备注保持纯文本。 - 注释的 Range 失效(换会话、消息重渲染、被压缩)时自动丢弃,不会留下指向空气的高亮。
已知边界
- 按 Enter 或点自带发送键的消息不会带上注释——没有 pre-submit 钩子可用,必须在胶囊里点「带注释发送」。
- 注释只存在于当前页面内存,刷新即消失。
- 划选必须落在非输入区域;composer 里的选区会被忽略。
卸载
在 Plugins 设置里停用 dsh-annotation,或从 profile 移除该 bundle。
会话归属(v0.1.1)
所有状态都按发起划词的那个对话分桶,不再是一份全局单例:
| 状态 | 归属 | 表现 |
|---|---|---|
注释 / 高亮 / N 条注释 胶囊 | 选区所在会话 | 切到别的会话看不到,也数不到 |
| 评论框、选区工具条 | 选区所在会话 | 侧栏、菜单、其他列里的划词不弹工具条 |
| 旁问弹窗与旁问卡片 | 发起它的会话 | 切走即隐藏,切回仍在;不会出现在别人的对话里 |
| 侧边对话标签与写入 | 发起它的会话 | openTab 带该会话的 scope,写入前校验侧栏作用域 |
| 侧栏诊断卡 | 发起它的会话 | 只在出问题的那条对话里 |
会话归属不是猜出来的,也不依赖 id 字符串格式:
- 选区:每个 layer 挂在某一篇对话里,从自己的 host 节点向上找最近的对话正文根
(
[data-conversation-content])。选区落在这篇正文里才认领,会话 id 一律取 slot props 的sessionId;侧栏、菜单、别的会话里的划词不会被认领。 - 旁问卡片:由
conversation.chat.turnTail传入的sessionId决定,不是本会话就渲染null。 - 旁问请求:
POST /dsh-annotation/ask带的是真正发起旁问的会话,fork 上下文不会串。
侧边对话
选中文字 → 点「侧边对话」→ 把选中内容(含已排队的注释)复制到剪贴板,并直接开一个
侧边对话标签页(调用 betterSidebar.openTab({ type: 'sidechat' }, { sessionId }),
线程由 better-sidebar 自己创建),随后把选中内容(含注释)直接写进它的输入框。
写入方式:等待侧栏挂载后,只在本文会话作用域内找 class 含 sidechat 的
contenteditable(右栏还挂着编辑器页、以及其他会话停靠的标签,不能按全页抓第一个),
聚焦后用 execCommand('insertText') 写入;若编辑器拒绝,则退化为派发一个 paste 事件。
两条都失败时剪贴板里仍有内容,手动粘贴即可,诊断卡落在发起它的那条对话里。
为什么不能用接口写入:dsh-better-sidebar 的服务只暴露 openTab / closeTab / registerTab 这些面板管理方法,线程创建与发消息走它内部的 ctx.connection RPC,
没有对外接口,插件无法代发。
旁问(第三个选项)
选中文字 → 点「旁问」→ 弹出模态:标题「旁问这段内容」、可展开的「引用原文」、 「你想了解什么?」输入框,底部是 取消 / 解释一下 / 发送旁问。
发送后模态关闭,回答以卡片形式出现在消息流里(挂在
conversation.chat.turnTail),不是浮在角落:卡片头部是问题、可折叠引用、
下方正文按增量流式追加(POST /dsh-annotation/ask 返回 NDJSON,每帧
{delta},收尾 {done})。卡片只属于发起旁问的那个会话。
host 侧用 subagents.getProvider('fork') 起一次性子会话、下
tools.restrict({allow: []}) 禁用全部工具,回答完即 dispose,不写主会话历史。
这条 fork 流水线参考 @michengai/dsh-btw(Apache-2.0),见 NOTICE。
安装
# 在 DSH 里以本地 bundle 安装(会写进当前 profile 并即时生效)
plugin_manager install_bundle /绝对路径/dsh-annotation
安装后划选任意回答文字即可看到工具栏。
多条注释与草稿(v0.1.3)
注入 dsh-paste-chip 时,每条注释是一枚自己的胶囊(标签 注释 · N),正文存在 paste-chip
里,发送时展开成 〔注释 N〕「…」。多条注释各自一枚,互不覆盖。
两条通道的存活判据不同,别再混用(混用就是 v0.1.2 的「新增一条,前一条就没了」):
| 通道 | 块正文在哪 | 存活判据 | 改编号 | 删单条 |
|---|---|---|---|---|
| 胶囊(有 ref) | paste-chip 存储 | 草稿里还有这枚 ref | paste-chip.replace 改正文 + 整份草稿改标签 | 整份草稿重写,去掉这一枚 |
| 纯文本(没装 paste-chip) | 草稿文本 | 草稿里还有这段正文 | 文本替换 | 文本删除 |
| 被动折叠(有 paste-chip 但通道失败) | paste-chip 存储,无 ref | 标签里的正文前 12 字 | 改不动,保留旧编号 | 按标签找到那枚再重写 |
取胶囊服务必须走 ctx.get('pasteChip'):cordis 里直接读没在 inject 声明的服务会抛
cannot get property ... without inject,ctx.pasteChip 永远是 undefined(v0.1.2 的通道
其实一次都没生效过,全部退回纯文本再被 paste-chip 被动折叠)。
插入点要换算成 detect 坐标:胶囊在 clipboard 投影里是整段显示文本,在 detect 投影里只占
1 个字符,直接拿 draft.length 当插入位置会越界。
选区工具条的落点(v0.1.5)
工具条原来固定落在选区下方 top = end.bottom + 12。选中对话最下面那几行时,它掉到输入框
下面(被输入框盖住或直接出视口),表现是「这个高度选不了、注释不了」。
现在先算一条可用下界:视口内、选区下方、横向与选区重叠的输入框顶边(取不到就退到视口底线), 再决定落点:
| 情况 | 落点 |
|---|---|
| 选区下方放得下 | 选区下 10px(原行为,避免和另一条停在选区上方的工具栏打架) |
| 下方被输入框/视口挡住 | 翻到选区上方 10px |
| 上下都放不下(视口极矮) | 夹进可用区间,不跑出屏幕 |
详情卡片的 ✕=取消这条注释(v0.1.4)
输入框里那枚 注释 · N 胶囊,点一下打开的是 dsh-paste-chip 的常驻预览卡片(正文 + 字数 +
「复制全文」)。卡片右上角现在有 ✕,点它就是取消这条注释:对话里的高亮、草稿里的
这枚胶囊一起消失,剩下的注释重新编号。
只认 ✕。这三种都不是「关闭」,注释留着:
| 动作 | 事件 reason | 结果 |
|---|---|---|
| 点 ✕ | close | 删掉这条注释(高亮 + 草稿胶囊),并弹一条「已取消这条注释」 |
| Esc / 点卡片外部 | dismiss | 只收起卡片 |
点另一枚 注释 · N 的卡片 | switch | 换预览,旧的注释不动 |
| 插件自己删掉注释 | removed | 顺手收掉卡片,避免留一张写着已删内容的卡 |
订阅走 ctx.get('pasteChip').watchPreview(fn):服务必须用 ctx.get 取,cordis 上下文里
直接读没在 inject 声明的服务会抛 without inject。服务实例换了(热更新)会自动退旧订新。
已知限制
- 整份草稿重写一律走
setDraft({ text, references }):setDraft(纯字符串)会把草稿里 已有的胶囊全部抹掉(Host 的语义),包括用户自己粘贴的那些。 - 被动折叠那条通道里注释编号改不动:删掉前面一条后,剩下的胶囊仍显示旧编号(比改坏标签、 让注释被当成已删除要好)。
- 旁问的答案目前是「收尾时整段推送」,尚未做到逐字增量:
activation.result只给最终结果。 - 侧边对话只能打开标签页并复制内容;better-sidebar 没有对外写入线程草稿的接口。写入失败时 15 秒后弹出诊断卡片,卡片只落在发起它的那条对话里。
- 注释与旁问都是页面内存态:刷新即失,不跨设备、不落盘。会话分桶只解决「同一次会话里串到别人对话」的问题。
- 自检:
node test/boot.test.mjs验证模块求值、slot 装配、✕ 关闭即取消这条注释,并用一个按 Host 语义写的 paste-chip 替身跑「两条注释并存 → 逐条删除 → 编号跟随」和降级通道;真实渲染与交互 仍需重载 DSH 后人工验收。
许可
本项目以 Apache License 2.0 发布,见 LICENSE。
lib/index.js 的旁问 host 流水线改编自 @michengai/dsh-btw
(Copyright 2026 MichengAI,Apache-2.0),其许可原文见 LICENSE-dsh-btw,
改动说明与署名见 NOTICE。依 Apache-2.0 第 4 条,这两份文件必须随代码一起分发。
Related plugins
dsh-web (dsh-task-board)
zhu1090093659/dsh-web
dsh-web (dsh-web-all)
zhu1090093659/dsh-web
dsh-web-ui (dsh-task-board)
zhu1090093659/dsh-web-ui
dsh-web-ui (dsh-web-ui-all)
zhu1090093659/dsh-web-ui