dsh-quote-to-chat
在 DSH Web GUI 里选中回复中的一段文字 → 弹出浮动工具条:把片段作为引用块写进输入框草稿、就这段内容开一个侧边线程,或直接复制。就像 Codex / ChatGPT 桌面端那种选中文本后的操作条。

English · 零运行时依赖 · 自测零依赖(node --test)
安装
# 从 GitHub 装(推荐)
dsh plugin --profile web add github:QIN-SMART/dsh-quote-to-chat
# 从 npm 装(发布后)
dsh plugin --profile web add dsh-quote-to-chat
装完刷新一次浏览器页面即可,不需要重启 dsh web。
开发本仓库时用本地链接(路径换成你自己的克隆位置):
git clone https://github.com/QIN-SMART/dsh-quote-to-chat
dsh plugin --profile web add "link:$PWD/dsh-quote-to-chat"
可选依赖:dsh-better-sidebar 提供侧边对话线程。没装它时「侧边提问」整个动作自动隐藏,其余功能不受影响。
用法
| 操作 | 结果 |
|---|---|
| 在对话正文里选中一段文字 | 选区上方弹出 添加到对话 | 侧边提问 | 复制 |
| 点添加到对话 | 草稿插入 > 选中内容 + 一个空行;光标回到输入框;工具条提示「已添加到对话」 |
| 点侧边提问 | 右栏展开一个侧边对话线程(继承当前会话上下文),引用预填进它的输入框、光标在末尾,接着写问题回车即可 |
| 点复制 | 选中原文进剪贴板,提示「已复制」 |
| Escape / 点别处 / 滚动到选区失效 | 工具条收起 |
| 在输入框里选中文字 | 不弹(避免自己跟自己打架) |
| 选中的是代码块 | 自动写成 ```lang 围栏,不塞进引用块 |
| 选中的是普通段落 | 写成 > 每行 的 blockquote;多段选中按块级换行还原 |
没装 dsh-better-sidebar |
「侧边提问」这一条整条隐藏,不留点了没用的按钮 |
工具条用 DSH 原生菜单材质(--dsw-specific-menu + --dsw-menu-backdrop-filter + --dsw-elevation-prominent),浅色/深色主题自动跟随:
点「添加到对话」后,草稿里出现引用块,光标回到输入框:


为什么不是直接照抄原生菜单的 58% 透明
原生菜单的填充色 --dsw-specific-menu 只有 58% 不透明(浅色 #f8f9fa94、深色 #43454a73)。那是给「浮在平面背景上的下拉菜单」设计的;而选区工具条恰恰浮在密集正文上,单独用它当底会让前后两层互相穿透 —— 第一版就是这个毛病,实测计算值:
第一版:background-color = rgba(248, 249, 250, 0.58) ← 正文笔画直接压在标签上
theme 里其实有一个 94% 的 macOS 版本(#f8f9faf0),但它的选择器是 html[data-platform=darwin] body,而这个构建从不设置 data-platform(实测 html 与 body 上都没有该属性)—— 那条规则是死代码,原生菜单自己也没吃到。
现在的做法:把原生填充铺在一层不透明的应用层色上。
background-color: var(--dsw-alias-bg-layer-1, #fff);
background-image: linear-gradient(var(--dsw-specific-menu, …), var(--dsw-specific-menu, …));
backdrop-filter: var(--dsw-menu-backdrop-filter, none); /* 保留,将来填充变透可自动回落 */
色相仍是原生菜单色(浅色合成 ≈ #fbfbfc,深色 ≈ #313235),但任何内容都不会再透上来。实测计算值变成 background-color = rgb(255,255,255)(alpha = 1)+ 一层 58% 的原生填充,命中测试栈顶是插件、下一层才是对话正文 —— 两层都清楚。
「侧边提问」是怎么接的
DSH 本体没有侧边线程,dsh-better-sidebar 提供 Codex 风格的「侧边对话」。所以这一条做成可选依赖:
- 开线程走它的公开服务:
ctx.get('betterSidebar').openTab({ type: 'sidechat' }, { sessionId })。用ctx.get而不是inject,是 DSH 里读「可选依赖」的正确姿势(缺了返回undefined,不会抛)。右栏已经开着侧边对话时复用它,不为每次点击都造新线程。 - 写引用走 DOM:它的输入框是标准 React 受控
<textarea>,用原生 value setter + 冒泡input事件让 React 的onChange收到(比伪造键盘事件可靠)。它没有公开的「存草稿」API(sidechat.start/prompt只能发消息),而自动替用户发一条消息会凭空烧掉一个回合。 - 兜底:5 秒内等不到输入框(比如 tab 类型被关掉了),就把引用放进剪贴板 + 明确提示「侧边对话已打开,引用已复制」—— 绝不静默失败。
判定输入框时排除了 xterm 的 helper <textarea>(1×1、挪到屏幕外)与隐藏 tab 里的元素,所以不会把终端输入框当成侧边对话。
结构
package.json dsh.bundle.patch + dsh.client.platform=web;ModuleLoader id 必须等于包名
cordis.patch.yml 启用一个 Loader 条目(客户端 bundle 只对启用的条目发布)
index.mjs 宿主半边:惰性,只打一行日志
lib/client.js 全部行为:选区识别、工具条、写回 composer、侧边提问
test/verify.mjs 离线断言(真实 bundle + mock DOM)
tools/real-ui-check.mjs 真机断言(CDP + 无头 Chrome + 真实 GUI)
docs/ 合成数据的截图
浏览器半边只注册一个插槽条目:conversation.input.overlay 里一个 display:none 锚点 span。它的唯一作用是拿到该会话的 inputActions,并把「哪个 DOM 子树属于这个会话」交给工具条 —— 浮层本体是纯 DOM,直接挂 document.body。选中哪个会话的文本就写回哪个会话的输入框(多会话并存时按「最近的共同容器」判定,无法唯一确定时宁可提示「找不到对应的输入框」也不写错地方)。
落地依据(全部来自 DSH 0.2.0-rc.1 的真实契约)
| 能力 | 依据 |
|---|---|
| 消息正文可选中 | dsh-client-ui-chat bundle 里 user-select:none 只出现在一处;正文是普通 DOM 文本 |
| 不与现有功能撞车 | 该 bundle 里 getSelection / selectionchange 出现 0 次 —— 原生没有任何选区弹层 |
| 能拿到「写入输入框」的正式 API | ui-conversation 装配时 ctx.uiSession.provide({ hooks:['conversation','input'], props:['inputActions'] }),每个会话 scope 的插槽组件都会收到 inputActions |
| 插入语义 | inputActions.captureInsertion() 给出带 draftRev 的位置;insertText(text, span) 落到 Lexical;draftRev 不匹配或正在提交时返回 false(不会破坏草稿) |
| 会话归属 | 消息节点带 data-chat-turn;正文容器是 [data-slot="conversation.session"];composer 是 [data-slot="conversation.composer.bar"];Lexical 宿主固定输出 div[contenteditable="true"][role="textbox"] —— 全程不碰构建期哈希类名 |
| 浮层 portal | 官方 dsh-client-ui-message-feedback 的浮层注释写明 Modal 与 Toast 都 portal 到 document.body |
| 选区属于哪个会话 | data-conversation-session(ui-conversation 自己的 Escape 处理就是这么取的) |
| 右侧栏自动展开 | ctx.sidebarRight.openTab 的契约:「内容用户看不见就不算打开」,开 tab 与展开右栏是同一步 |
配置
界面里没有设置项(保持克制)。调试句柄:
__dshQuoteToChat.config() // { enabled, format, attribution, minChars }
__dshQuoteToChat.setConfig({ format: 'plain' }) // auto | quote | fenced | plain
__dshQuoteToChat.setConfig({ attribution: true }) // 引用块后补一行「(引用自第 N 轮回复)」
__dshQuoteToChat.setConfig({ minChars: 0 }) // 少于 N 字符不弹
__dshQuoteToChat.setConfig({ enabled: false }) // 整体关掉
__dshQuoteToChat.state() // { open, text, turn, instances, hasActions, sideChat, config }
__dshQuoteToChat.insert('一段话') // 绕过选区直接写引用(自动化用)
__dshQuoteToChat.sideChat() // { available, composers, drafts }
配置存 localStorage['dsh.quote-to-chat.v1'],白名单字段,坏数据一律回落默认值。
验证
node --test test/verify.mjs # 离线断言:模块形状、材质不变量、引用格式化、
# 定位与七类不弹条件、多会话归属、三个动作、侧边对话各分支、dispose
node tools/real-ui-check.mjs # 真机断言:CDP 驱动真实 GUI(真实选区、真实写回),自动读启动 token
真机检查覆盖:client bundle 是否出现在启动图、插件是否 materialize、会话桥是否拿到 inputActions、真实选区能否弹出、材质 alpha 是否 = 1(浅色/深色)、命中测试栈顺序、草稿是否逐字节一致、侧边线程是否真的开出且引用已预填、以及脚本自己开出来的 tab 是否被清理干净。
已知边界
- 侧边提问依赖
dsh-better-sidebar:没装时该动作自动隐藏。写草稿走它的输入框 DOM(它没有公开的 draft API),它若大改输入框实现,这一条会退化成「剪贴板兜底」而不是静默失败。 - 列表项、表格等非段落结构:选中后按块级换行还原,不补
-/|之类的结构标记(DSH 用 CSS 画列表符号,DOM 里没有字符)。 - 只覆盖对话正文:侧边栏、文档预览、轨迹视图里选中不弹(刻意的克制)。
- 引用是纯文本:不是 DSH 的原子引用 chip(那需要
ctx.inputTriggers注册 source + 宿主侧serializeReference)。 - 只支持 Web:
dsh.client.platform = web。
后续可以加
- 把选中片段做成原子引用 chip(
ctx.inputTriggers+ 宿主serializeReference),可点击预览、可整体删除。 - 侧边提问反过来:右栏已开着某个侧边线程时,给一个「提升为顶层会话」的快捷键。
- 选区来源标注(
data-chat-turn已经有了,只差一个默认开关)。 - 配置上移到宿主
Config(ctx.configForms.get('quote-to-chat')),跨浏览器同步。
卸载
dsh plugin --profile web remove dsh-quote-to-chat
License
MIT — 见 LICENSE。
No comments yet. Be the first to write one.