zengxiaolou/dsh-annotation

dsh-annotation

划选回答文字就地加注释:保留原文引用加自己写的备注,每条在草稿里变成一枚胶囊,多条一起随消息发出;两条以上时输入框上方会多一个胶囊用来跳回原文或全部清除。胶囊需另装 dsh-paste-chip,否则每条注释在草稿里是一行纯文本。

待分类deepseekharness

dsh-annotation

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

效果

示意图(按主题令牌渲染):划选回答里的文字 → 就地写一句批注 → 批注成为 注释 · N 胶囊随消息发出;两条以上时输入框上方出现 N 条注释 胶囊,可跳回原文或全部清除。

交互

  1. 在任意回答里划选一段文字。
  2. 浮出的评论框里可以写备注,也可以留空(纯引用)。回车保存,Esc 取消。
  3. 选中的文字就地保留淡色底纹,末尾挂一个编号圆点;点圆点可以再编辑那条注释。
  4. 两条以上时,输入框上方出现 N 条注释 胶囊:悬停或点击才浮出详情卡(逐条删除、全部清除、点条目跳回原文),平时不占位。只有一条时不出——草稿里那枚 注释 · 1 已经说清楚了,多一个「1 条注释」只是把同一件事说两遍。
  5. 注释以单行文本写进草稿,自己按回车或点发送即可带上,不需要额外按钮。

为什么只有一个注释时不弹 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 条,这两份文件必须随代码一起分发。