dsh-client-ui-quote 0.0.0-stage → 0.10.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 +21 -0
- package/README.md +356 -2
- package/cordis.patch.yml +9 -0
- package/icon.svg +18 -0
- package/lib/client.js +3432 -0
- package/lib/index.js +12 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +60 -4
- package/test/capsule.html +299 -0
- package/test/capsule.mjs +1055 -0
- package/test/check.mjs +307 -0
- package/test/dom.mjs +415 -0
- package/test/panel.mjs +421 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-client-ui-quote contributors
|
|
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,357 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-client-ui-quote
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> 在 DSH 里**选中一句话**,浮出「评论 / 添加到对话 / 侧边对话」——把选区以 DSH 原生**引用芯片**插进输入框,或写进右侧栏那条独立侧边对话的输入框 —— 按 Kimi Code 的交互实现的 DSH 客户端插件。
|
|
4
|
+
|
|
5
|
+
选中正文 → 浮层动作 → 引用变成输入框里的行内胶囊(不是纯文本),评论也放在同一颗芯片里;发送之后,引用和评论**合成一颗胶囊**留在消息里,不会摊成一屏 `>` 行。第三项「侧边对话」则把引用写进右侧栏一条**独立的侧边线程的输入框**(一条主会话一条线程,反复点就反复追加),问什么由你写,主会话一点都不动。评论框里**引用正文本身也可编辑**(选多了当场裁);从**文件预览或终端**划选的引用以**围栏代码块**的线格式发给模型,模型看到的是真正的代码块。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
| 动作 | 行为 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| **评论** | 浮层就地变成评论框(`写一句评论…` + `取消` / `添加到对话`)。评论框里**引用正文也在自己的输入框里、可以改**——选多了就当场裁掉。`Enter` 提交、`Esc` 关闭、引用或评论为空时确认按钮禁用;输入法组合输入不会误触发。 |
|
|
12
|
+
| **添加到对话** | 直接把引用加进输入框,不弹评论框。 |
|
|
13
|
+
| **侧边对话** | 把引用写进一条**独立的侧边对话**(dsh-better-sidebar 的侧边线程)的输入框:线程继承主会话完整上下文、独立运行、不污染主会话,页签自动在右侧栏打开,**什么都不发送**——你写好自己的问题再发。同一条主会话只开一条线程,之后每次点都在同一条线程的草稿后面接着追加。鲸鱼蓝(`#4d6bfe`)的图标是它在浮层里的标识。 |
|
|
14
|
+
| **键盘直达** | `Alt+Q` 引用 / `Alt+C` 评论 / `Alt+B` 侧边对话——同一个浮层的三个动作,不弹浮层直接办。走 DSH 的快捷键服务注册,所以出现在设置里、可改键、与宿主键位冲突会在注册时报错;没有选区时返回 `pass`,按键不会被吞掉。 |
|
|
15
|
+
| **跳回原文** | 悬停卡片底部的 `跳回原文`:把引用当初所在的那条消息**停到视野上方 24px**(和宿主自己跳轮次一样的位置)并闪一下。坐标(消息行锚点 + 轮次)在插入时记下、按引用原文归一化成键存进 `localStorage`,因此跨刷新仍在。只在**引用自己那条会话的面板里**找,不跨会话误跳;被虚拟列表藏起来的行会让位给同轮次的可见行;压根没挂载的行靠滚动搜索一屏一屏地找出来——先往上找(引用总在比它更早的消息里),一路到顶还没找到就从最新处再来一遍;都找不到就明说(`原文不在当前视图里`)。 |
|
|
16
|
+
| **引用清单** | 右侧栏的一个**页面**:本会话发出去的引用与评论的清单。搜索框按引用/评论过滤、点一条跳回原文、`导出 Markdown` 落一个文件、历史没读完时可 `加载更早的消息`。它作为**页面类型**注册进右侧栏(`ctx.sidebarRightTabs.register`),并在指南页留下一个胶囊作入口;`Alt+L` 同样能打开。数据来自会话自己的事件日志(`ctx.sessions.retain` → `session.eventSource`),不依赖屏幕上有没有那条消息。 |
|
|
17
|
+
| **侧边线程回流** | 同一个页面底部是这条会话的**侧边线程**:显示它最后问的那个问题与消息条数,并给两个动作——**`带回主会话`** 把线程最后「问题 + 答案」写成主会话输入框里的草稿(**不发送**,你自己按发送),**`导出 Markdown`** 把整条线程落成一个文件。线程内容同样从它自己的事件日志读;没有线程时这一节只说明「还没有侧边线程」。 |
|
|
18
|
+
| **改这条引用** | 悬停卡片里的 `改这条引用`:把那条引用当初的**引用与评论都原样放回评论框**(引用正文、评论各占一个输入框,都能改),你改完按 `⏎` 就发出一条**新的**引用(DSH 没有编辑已发送消息的 API,这是唯一诚实的做法)。旧胶囊随即挂上 **`已修订`** 标记,卡片里直接显示改后的评论;**连正文一起改过的**,卡片里也把改后的原文亮出来。标记按引用原文记在本机,跨刷新仍在。 |
|
|
19
|
+
- 引用以 DSH 原生行内原子节点 `reference-chip` 插入(带图标的胶囊);走「评论」时评论一起放进这颗芯片(芯片标签显示为 `引用摘要 · 评论摘要`),引用与评论始终是一体,草稿里不会散成两段。
|
|
20
|
+
- **从文件预览或终端划选的引用是代码**:线格式不再是纯块引用,而是 `> ❝ ``` ` / `> 代码行` / `> ``` ` —— 模型看到的是真正的围栏代码块;胶囊与悬停卡片随之换等宽字体、按代码的空白排版,引用清单导出时也落成 ``` 围栏而非 `>`。围栏只是线格式(传输时剥掉),胶囊上显示的仍是原始代码文本;正文里本身就有 ``` 时改用 `~~~` 围栏。
|
|
21
|
+
- **引用正文可以编辑**:评论框上半格是引用自己的输入框(评论在下半格),划选带进了提示符噪音或多选了几行,当场裁掉再发;「改这条引用」时旧正文回填同一格,改完随新引用发出。
|
|
22
|
+
- 发送时由 DSH 回调本插件取「模型形态」:**`> ❝ …` 引用行 + 同一个块里的 `> ❞ …` 评论行**。消息里那段块会被就地换成 Kimi 那样的一颗胶囊
|
|
23
|
+
(`❝ 引用摘要 │ 评论摘要`,鼠标移上去就能读到引用与评论的全文、点开可展开、悬停出现 `复制`),你自己写的话保持原样,模型收到的仍是完整引用文本。
|
|
24
|
+
- 浮层位置贴着选区**下方**(下方放不下时才挪到上方),配色在运行时从输入卡片 / 正文取样,深浅色主题都适配;中文/英文词条随界面语言切换。
|
|
25
|
+
- 芯片通路不可用时(拿不到 DSH 的输入触发服务或会话输入服务)自动退回纯文本形态:直接写入 `> ❝ …`(评论接 `> ❞ …`)引用块。
|
|
26
|
+
|
|
27
|
+
## 安装
|
|
28
|
+
|
|
29
|
+
要求:DSH ≥ 0.2.0-rc.2 的 **web / 桌面** 客户端(`package.json` 里 `engines.dsh` 声明的就是实测版本;插件运行在渲染进程里,headless profile 没有 UI)。
|
|
30
|
+
|
|
31
|
+
### 方式一:link 安装(推荐)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
git clone https://github.com/BangBang-03/dsh-client-ui-quote.git
|
|
35
|
+
dsh plugin --profile desktop add link:<clone-path>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
DSH 会把包记进 profile 的依赖并加入组合包列表,装完在侧边栏 **插件** 页能看到「选中引用 (dsh-client-ui-quote)」,可随时开关。
|
|
39
|
+
|
|
40
|
+
### 方式二:手工注册为 profile 组合包
|
|
41
|
+
1. 把本仓库放到任意目录;
|
|
42
|
+
2. 编辑 `<DSH_HOME>/profiles/<profile>/package.json`:
|
|
43
|
+
- `dependencies` 增加 `"dsh-client-ui-quote": "link:<绝对路径>"`(Windows 上用正斜杠,如 `link:C:/path/to/dsh-client-ui-quote`);
|
|
44
|
+
- `dsh.profile.bundles` 追加 `"dsh-client-ui-quote"`;
|
|
45
|
+
3. 重启 DSH(profile 打开 `patchReload` 时也会自动重载)。
|
|
46
|
+
|
|
47
|
+
**不需要手写 loader 条目**:包自带的 `cordis.patch.yml` 就是组合包层补丁,会插入
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
- insert:
|
|
51
|
+
- id: ui-quote
|
|
52
|
+
name: dsh-client-ui-quote
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 方式三:npm 包(发布通道已就位,待 NPM_TOKEN)
|
|
56
|
+
|
|
57
|
+
`package.json` 已经是可发布状态:`private` 已移除、`files` 白名单只带 `lib` / `locale` / `test` / `icon.svg` / `cordis.patch.yml` / 文档,`prepublishOnly` 会在发布前跑完 253 项离线断言,`npm pack --dry-run` 可核对内容(0.10.0 = 14 个文件 / 97.6 kB)。
|
|
58
|
+
|
|
59
|
+
**发布有两条路**(都只差一个 npm Access Token):
|
|
60
|
+
|
|
61
|
+
1. **GitHub Actions(推荐)**:`.github/workflows/publish.yml` 已就位——往仓库里配一个 secret `NPM_TOKEN`(npmjs.com → Access Tokens → Generate New Token,选 **Automation** 类型;或 `gh secret set NPM_TOKEN --repo BangBang-03/dsh-client-ui-quote`),然后推 `v*` 标签或在 Actions 页手动 Run workflow 即发布。之后的版本:改版本号 → 打 `vX.Y.Z` 标签推上去,发布全自动。
|
|
62
|
+
2. **本机 npm**:`npm login && npm publish`——注意本机直连 `registry.npmjs.org` 不可达(系统 DNS 返回污染地址、真实 Cloudflare IP 也握手超时),要挂一个能通 npmjs 的代理再发。
|
|
63
|
+
|
|
64
|
+
装:`dsh plugin --profile desktop add dsh-client-ui-quote`。npmjs 侧发布后 npmmirror 一般 10 分钟内同步,DSH 若配置了镜像源即可国内直装。
|
|
65
|
+
|
|
66
|
+
在那之前请用方式一或方式二。
|
|
67
|
+
|
|
68
|
+
### 卸载
|
|
69
|
+
|
|
70
|
+
从 `dsh.profile.bundles` 与 `dependencies` 里移除该条目,重启 DSH(link 安装的包目录可以直接删)。
|
|
71
|
+
|
|
72
|
+
## 使用
|
|
73
|
+
|
|
74
|
+
1. 在会话正文、**文件预览**或**终端**里**拖动选中**一段文字(后两者没有自己的会话 id,引用会落进当前屏幕上那条会话,并且以**围栏代码块**的线格式发给模型;输入框内的选区属于改稿,不触发)。
|
|
75
|
+
2. 选区**下方**浮出浮层(下方空间不够时才改到上方),选择 **评论**、**添加到对话** 或 **侧边对话**。同一组动作也有键位:**`Alt+Q`** 直接引用、**`Alt+C`** 直接开评论框、**`Alt+B`** 直接开侧边对话(都在设置里可改;没有选区时按键原样让给其它快捷键)。
|
|
76
|
+
3. 前两个动作把引用放进输入框(评论模式下引用与评论在同一颗芯片里;**评论框上半格是引用正文本身,可以当场裁剪**),页面选区被清除、焦点交给输入框,可以接着写正式提问。
|
|
77
|
+
4. **侧边对话**写的是另一条会话的输入框:右侧栏打开该线程的页签(标题取引用首行),引用以纯块引用形态落在它的草稿里,**不发送**;你接着在这条线程里写自己的问题、自己按发送。同一条主会话只会有一条侧边线程,第二次点第三项就把新引用接在草稿后面。同一会话上一次请求还在进行时,再次点击会并入第一次,不会开出两条重复线程。
|
|
78
|
+
5. 浮层收起条件:单击别处、选区清空、按 `Esc`、滚动会话、窗口尺寸变化,以及动作完成之后。双击选词不会把浮层关掉。
|
|
79
|
+
6. 发送出去以后,消息里那段块会显示成一颗**胶囊**:收起时是单行 `❝ 引用摘要 │ 评论摘要`,**鼠标移上去**会浮出一张卡片显示完整的引用与评论
|
|
80
|
+
(输入框里那颗芯片同样可以悬停查看),点一下就在原地展开成全文,胶囊上悬停出现 `复制`。
|
|
81
|
+
你自己写的话照旧显示在它下面;一条消息里引用了几处,就会出现几颗胶囊,中间的原话留在原来的位置。
|
|
82
|
+
7. 悬停卡片底部有 **`跳回原文`**:只对那些**本插件看着被选中**的引用出现(插入时记下了它所在的消息行 `data-chat-anchor-key` 与轮次),点一下就把那条消息**停到视野上方 24px**——和你用宿主自己的「跳到某一轮」是同一个落点——并闪烁一下。搜索只在这条引用**自己的会话面板**里进行(同一个锚点键在别的会话里也有时不会误跳),被 `hidden` 藏起来的行不算目标、会让位给同轮次的可见行,还没被虚拟列表挂载的行会一屏一屏地滚出来找——**先往上**(引用总在更早的消息里),到顶仍未找到就从最新处**再来一遍**;这套落点是宿主自己的做法:读 `[data-conversation-scroll]` 的 `scrollTop` 再连补几帧(虚拟列表挂载邻居时会改写偏移,一次性的 `scrollIntoView` 会落短)。真找不到时给一条 toast(`原文不在当前视图里`),而不是留一个点了没反应的按钮。
|
|
83
|
+
8. **引用清单**:右侧栏的「引用清单」页把这条会话发出去的所有引用与评论列成一张表——**搜索框**按引用或评论过滤,点某一条就**跳回原文**(和悬停卡片上的按钮同一套落点),右上角**导出 Markdown** 存成文件;历史还没读完时列表上方会出现 **`加载更早的消息`**。入口有两个:右侧栏默认的**指南页**里有它的胶囊(点一下就在当前栏打开),或按 **`Alt+L`**(同样在设置里可改)。
|
|
84
|
+
9. **侧边线程回流**:同一个页面底部是这条会话的侧边线程——显示它最后问的那个问题与消息条数。**`带回主会话`** 把线程最后的「问题 + 答案」写成主会话输入框里的草稿(**不发送**,你自己按发送;输入框里已有的内容会保留,回流内容接在下面);**`导出 Markdown`** 把整条线程按顺序落成一个文件。没有线程时这一节只说明「这条会话还没有侧边线程」。
|
|
85
|
+
10. **改这条引用**:悬停一张胶囊卡片,点 **`改这条引用`** —— 评论框把那条引用当初的**引用与评论都填回来**(各自一个输入框,都能改),你改完按 `⏎`(或点「添加到对话」)。发出的是一条**新的**引用:DSH 的消息是只追加的,没有编辑 API,所以「修改」只能这样表达。旧胶囊会立刻挂上 **`已修订`** 标记,它的卡片里显示改后的评论;连正文一起改过的,卡片里也显示改后的原文。标记按引用原文记在本机(`localStorage`),刷新后仍在。
|
|
86
|
+
|
|
87
|
+
## 工作原理
|
|
88
|
+
|
|
89
|
+
DSH 的输入框是 Lexical,没有 Kimi 的 `quote` 节点,但有一个通用的行内原子节点 `reference-chip`(`@文件` / `@会话` 提及用的就是它),而且**插件可以拥有自己的 chip**:
|
|
90
|
+
|
|
91
|
+
- 插件注册一个 reference source:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
ctx.get("inputTriggers").registerSource({
|
|
95
|
+
trigger: "#", name: "quote",
|
|
96
|
+
candidates: () => [],
|
|
97
|
+
codec: { clipboardText: (ref) => ref, serialize: (ref) => Promise.resolve(String(ref)) }
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
发送时 DSH 按 chip 的 `source` 找到 owner,调用 `codec.serialize(ref)` 取模型形态并替换进草稿。找不到 owner 会用
|
|
102
|
+
`slash: no serializer for reference source "…"` 拒绝整条发送,所以插件在插入 chip 前先确保注册成功,注册不到就整体退回文本形态。
|
|
103
|
+
- 放入 chip 走会话输入服务:`ctx.get("conversation").input.shell(sessionId).insertReference(ref, span)`,其中
|
|
104
|
+
`ref = { source, ref, label, appearance, clipboardText }`。`ref` 对 host 不透明,因此引用正文(`> …`)直接放在 `ref` 里,
|
|
105
|
+
codec 保持无状态,草稿刷新后 chip 依然能还原。`appearance` 用 `session`(DSH 只认 `session` / `file` / `folder`),标签是引用正文的单行截断(48 字以内)。
|
|
106
|
+
- 浮层是一个挂到 `document.body` 的单例元素,注册在 `conversation.input.overlay` 槽位(会话作用域)的组件渲染 `null`,
|
|
107
|
+
在 `useEffect` 里装配原生 DOM 事件:`selectionchange`(防抖)/ `pointerup` / `keyup` 读选区,`scroll` / `resize` / `Esc` / 外部点击收起。
|
|
108
|
+
- 每个会话把自己最新的 `inputActions` 注册进模块级表,浮层只对已注册会话的选区生效,插入时一定写进当前会话的输入框。
|
|
109
|
+
- 插件 ctx 存进模块级变量,chip 通路用它惰性解析 `inputTriggers` 与 `conversation` 两个服务:apply 时服务还没就绪也没关系,第一次插入时再试。
|
|
110
|
+
|
|
111
|
+
### 发送后的胶囊(transcript 侧)
|
|
112
|
+
|
|
113
|
+
发送出去的用户消息在 DSH 里是**纯文本**:`projectUserText` 只把 `@…` / `/技能` 变成行内 chip,其余原样渲染成
|
|
114
|
+
`white-space: pre-wrap` 的文本,`> ` 不会变成引用样式 —— 一屏 `>` 行就是这么来的。要做成 Kimi 那样一颗胶囊,
|
|
115
|
+
可用的路只有三条,前两条都被排除了:
|
|
116
|
+
|
|
117
|
+
- **换成原生 chip**:`@路径` 会渲染成文件 chip,但它是「蓝色文字 + 图标」,没有底/圆角,标签会被
|
|
118
|
+
`split(/[\\/]/).at(-1)` 砍成 basename,点击还会调 `openFile()` 去开一个并不存在的路径;`@[标签](dsh-session:…)`
|
|
119
|
+
的 session 线格式会被 host 的 `parseSessionReferenceText` 解析并在格式非法时抛 `SESSION_REFERENCE_INVALID_REFERENCE`,
|
|
120
|
+
合法的又会真的去抓取被引用会话的 transcript(`MAX_REFERENCES = 3`)—— 语义不对。
|
|
121
|
+
- **做成附件**:`fileCard` 是原生卡片,但附件只能由粘贴/拖拽上传通路产生(发的是上传回执,模型要靠工具去读),
|
|
122
|
+
引用正文反而进不了提示词。
|
|
123
|
+
- **插件自己画**(本插件采用):`conversation.chat.node` 是按节点类型路由的 keyed 槽(`user` / `steering` / `context` … 由核心拥有),
|
|
124
|
+
没有「每条消息一个钩子」的位置,所以胶囊画在 transcript 之上:
|
|
125
|
+
|
|
126
|
+
- 序列化时在块首留**固有标记** `❝`(引用)与 `❞`(评论):`> ❝ 第一行` / `> 后续行` / `> ❞ 评论` 共用一个块 —— 它是刷新、重开之后
|
|
127
|
+
唯一还能认出「这段是引用 + 评论」的东西;一个块只对应一颗胶囊,所以芯片后面打的字不会被卷进评论里;
|
|
128
|
+
- **文件预览/终端来源的引用**(`readSelection` 分得清选区落在哪个 host)序列化成 `> ❝ ``` ` / `> 代码行` / `> ``` `:
|
|
129
|
+
模型读到块引用里一个真正的围栏代码块;`splitQuoteRuns` 解析时把首行的围栏(和收尾那一行)剥掉,胶囊拿到的仍是原始代码文本,
|
|
130
|
+
渲染加 `data-code="true"`(等宽、`pre`、展开时横向滚动),卡片加 `dshq-pop-code`,侧边对话落进子线程草稿的则是不带标记的裸围栏;
|
|
131
|
+
正文本身含 ``` 时序列化自动换 `~~~` 围栏。没有围栏的块照旧按普通引用解析,新旧消息互不影响;
|
|
132
|
+
- 客户端插件用 `MutationObserver` 找「只含一个文本子节点、内部有 `❝`、位于 `[data-conversation-content]` 里」的那个 run,
|
|
133
|
+
把整条消息拆成 `前文 / 引用 / 中间文字 / 引用 / 后文` 这样一串分段(`splitQuoteRuns`),给原 run 加 `display:none`,
|
|
134
|
+
在它**前面**按顺序插入自己的元素:每段引用一颗胶囊,每段用户自己的话一个 `span.dshq-body`(保持 `pre-wrap`)——
|
|
135
|
+
所以一条消息里引用了几处,就会出现几颗胶囊,中间的原话留在原来的位置;
|
|
136
|
+
- 全程只隐藏、绝不搬移或删除 React 的节点;胶囊按 run 记账(`Map<run, {text, nodes}>`),文本没变就复用、变了就地重建、
|
|
137
|
+
消息被删就把胶囊一起撤掉,React 重渲染不会出现第二颗胶囊;
|
|
138
|
+
- 胶囊点击展开/收起(有选区时不触发),悬停出现 `复制`:收起时是单行 `❝ 引用… │ 评论…`(两端超出都省略),展开后两段各自换行;
|
|
139
|
+
复制的是 Kimi 存 `display_text` 时的同一写法 `引用 · 评论`。样式用 `data-plugin="dsh-client-ui-quote"` 的 `<style>` 注入,
|
|
140
|
+
配色取 `currentColor` 的 `color-mix`,无边框无阴影,深浅主题都不用额外适配。
|
|
141
|
+
- 收起状态只有一行、读不全,所以**悬停会浮出一张卡片**(`#dsh-quote-popover`:单例、`position: fixed`、跟在锚点下方、贴底自动翻到上方、
|
|
142
|
+
左右按视口夹取、鼠标从锚点移到卡片上不会把它关掉):读全文不必先点开。**输入框里的芯片也接同一张卡片** ——
|
|
143
|
+
芯片标签本来就被 `chipLabel` 截断过(引用 30 字 + 评论 22 字),插件按「标签 → 引用 / 评论」建索引,在 `[data-composer-card]`
|
|
144
|
+
下认出这颗芯片后给它挂上同一张卡片,并打上 `data-dshq-chip="1"` 记账,卸载时摘掉钩子与索引。
|
|
145
|
+
芯片标签在**每次悬停时重新解析**:React 下一条引用常常复用同一个芯片节点(节点没变、标签变了),
|
|
146
|
+
所以卡片不会拿着挂载那一刻的旧内容;标签也对不上索引时宁可不出卡片,绝不显示别的引用的内容(芯片自带的 `❝` 前缀会被忽略)。
|
|
147
|
+
卡片跟着锚点活:锚点一旦离开文档(发消息会把芯片拿走,而被移除的节点不会再发 `mouseleave`)卡片立刻收起,
|
|
148
|
+
React 重渲染换掉胶囊时同理 —— 不会留下一张浮在空处的卡片。
|
|
149
|
+
|
|
150
|
+
### 侧边对话(第三项)
|
|
151
|
+
|
|
152
|
+
第三项是本插件自己的扩展(Kimi 的浮层只有两项),融合了三家 agent 同类组件的优点:**Kimi Code 的 `/btw` 侧边聊天**——一键在侧边栏开一条继承主会话上下文的线程;**Qoder 的「在侧边任务中提问」**——同一会话进行中的创建请求自动并入第一次,失败只弹一条提示,绝不大张旗鼓;**WorkBuddy 的划选引用**——引用是线程里持久的内容,而不是一次性的草稿附件。与前两家不同的是**问什么由你来写**:引用只落进那条线程的输入框,不会被当成第一条消息替你发出去。样式上第三项用 DeepSeek 的鲸鱼蓝(`#4d6bfe`)画图标与悬停色,白底浅色主题是主基调,深色主题同一色相。
|
|
153
|
+
|
|
154
|
+
- **可用性探测**:`ctx.get("betterSidebar")` 存在、`getTab("sidechat")` 有注册、`isTabEnabled("sidechat")` 为真,三项齐备才渲染第三项;缺任何一项就整个隐藏——与「芯片通路不可用时退回纯文本」同一个哲学。
|
|
155
|
+
- **开线程**:`POST /sidebar/api/sidechat.start`,载荷 `{ sessionId, question: "" }`。**空 question 是关键**:better-sidebar 对非空 question 会调 `admitFirstContact` 把它当成线程的第一条消息发出去,而空 question 只把主会话的上下文快照挂到子线程上、一条消息都不发——引用于是不会替你提问,而「线程继承主会话上下文」仍然成立。
|
|
156
|
+
- **开页签**:拿到 `{ childId }` 后调 `betterSidebar.openTab({ type: "sidechat", id: "sidechat:<childId>", title: 引用首行, meta: { threadId: childId } }, { sessionId })`。`meta` 自 0.12.0 起是文档化的种子通道,会一路透传成原生页签的导航参数;`SideChatView` 正是按 `meta.threadId` 绑定线程——页签于是精确落在这条线程上,而不是又开一个空线程。
|
|
157
|
+
- **写进输入框**:页签打开后,`ctx.get("conversation").input.shell(childId)` 拿到子会话的输入框外壳(`SessionInputShell`),用 `setDraft(...)` 把引用的**纯块引用**形态(`>` 逐行前缀,不带 `❝` 标记——那两个标记是主会话胶囊增强器的识别符,侧边线程用不上)写成草稿:输入框里已经有你写的内容时,引用接在下面、空行隔开;**不发送,也不碰别的东西**。子会话的输入框要等页签挂载后才存在(`shell(id)` 在此之前会抛「resolved no binding」),所以这里按 200ms 轮询等它出现;外壳在而 `setDraft` 不在(版本对不上)则立刻放弃,不空等 5 秒。
|
|
158
|
+
- **一条主会话一条线程**:线程 id 按主会话记住——优先读 better-sidebar 自己写在 `dsh-sidebar:v1:sidechat-thread` 里的绑定(页签绑上哪条线程它就记哪条),其次读本插件自己的 `dsh-client-ui-quote:v1:sidechat-thread`。第二次点第三项不再新建线程,只把新引用接进同一条线程的输入框。
|
|
159
|
+
- **防重复**:同一会话的请求还在途中时,再次点击直接并入第一次(模块级 `Map<sessionId, Promise>` 记账,结算后自行清除)。
|
|
160
|
+
- **失败路径**:路由拒绝(better-sidebar 未挂载、父会话未运行等)或 `openTab` 抛错 → toast「侧边对话没有打开,请重试。」;线程已开、草稿没能写进去 → toast「侧边对话已打开,但引用没能写进它的输入框」。
|
|
161
|
+
|
|
162
|
+
### 引用清单(右侧栏页面)
|
|
163
|
+
|
|
164
|
+
引用清单是这套能力里唯一**把引用当对象**的地方:它把一条会话发出去的引用与评论列成一张可搜索、可跳转、可导出的表。
|
|
165
|
+
|
|
166
|
+
- **数据来自会话自己的事件日志**,不是屏幕上的 DOM:`ctx.sessions.retain(sessionId, { source: "quoteList" })` → `await reference.ready` → `reference.binding.session` → `session.eventSource.getSnapshot().entries`,筛 `event.type === "user/message"`,取 `event.data.content` 里的文本块(注意 `user/message` 的记录就在 `event.data` 上,不是 `event.data.message`),再用 transcript 侧同一个 `splitQuoteRuns` 把 `> ❝ …` 块拆出来。读完立刻 `reference.release()`——保留一个永不放手的引用会把会话钉在内存里。因为读的是日志而不是 DOM,**屏幕上没有那条消息也照样列得出来**。
|
|
167
|
+
- **历史是分页的**:首屏只带最近若干条消息,`session.getSnapshot().hasMore` 为真时列表上方出现 `加载更早的消息`,点它调 `session.loadOlder()` 再重读一遍。
|
|
168
|
+
- **跳回原文**复用悬停卡片那一套(同一个 `jumpToSource`):只有当初被本插件看着选中的引用才有坐标,所以只有那些行可点(`data-dshq-row="jumpable"`),其余行只是可读的文本。
|
|
169
|
+
- **导出**走浏览器:`URL.createObjectURL(new Blob([md], { type: "text/markdown;charset=utf-8" }))` + 一个带 `download` 的 `<a>`——宿主没有给客户端插件任何写盘的接口,这是 DSH 里现成的做法(`dsh-session-log-export` 的客户端半边就是这么下载的)。没有 `Blob` 时退回 `data:` URL。
|
|
170
|
+
- **注册形状**(两侧都必须声明,id 是同一个):`ctx.sidebarRightTabs.register({ id: "dsh-client-ui-quote/list", kind: "quote-list", title, guide: [{ id: "quotes", commandId: "ui-quote.list", order: 20, title, description }] })` 声明**页面类型**(页面类型不写 `patterns`,按 kind 打开),`ctx.slots.register({ name: "sidebar.right.pane.tab", key: "dsh-client-ui-quote/list" }, Body)` 提供**页体**,标题座位 `sidebar.right.pane.tab.title`(同一个 key)提供页签文字。`guide` 数组是发现入口:右侧栏默认页就是指南页,里面每个 `guide` 条目一个胶囊,点开即 `openTab(kind)`。打开动作是 `ctx.get("sidebarRight").openTab("quote-list")`,`Alt+L` 与指南胶囊都走它。
|
|
171
|
+
- **页体拿到的 props**:座位注入的 `inject: (sessionId) => ({ sessionId, load, loadOlder, jump, save, loadThread, back, exportThread })`,页体本身只负责渲染——解析、过滤、Markdown、单行标签都是可离线断言的纯函数。
|
|
172
|
+
- **侧边线程回流**(页面底部那一节):线程 id 从线程记忆里反向查出主会话(本插件的 `dsh-client-ui-quote:v1:sidechat-thread` 与 better-sidebar 的 `dsh-sidebar:v1:sidechat-thread` 都是「主会话 → 线程」的表,这里倒着读);线程内容用同一个 `withSession` 读取(`retain` → `ready` → `binding.session.eventSource` → `release`)。`带回主会话` 取线程**最后一条提问**与它之后的**最后一条回答**(提问以 `>` 块引用形式带上,超过 4000 字截断并标注),写进主会话的草稿——用的是第三项那条链路同一个 `prefillDraft`,所以也是「**写草稿、不发送**」,草稿里原有内容保留、回流内容空行接在下面。`导出 Markdown` 把整条线程每条消息按顺序落成文件。
|
|
173
|
+
|
|
174
|
+
### 与 Kimi Code 的对应关系
|
|
175
|
+
|
|
176
|
+
参照物是 Kimi Code 1.0.4 桌面端 bundle。Kimi 的浮层是 teleport 到 body 的 `.sab`(selection action bar),**一个元素两种模式**:
|
|
177
|
+
`menu` 模式两行(`评论` 切到评论模式、`添加到对话` 直接出引用),`comment` 模式为文本域 + `取消` / `添加到对话 ⏎`;
|
|
178
|
+
动作经 `onAction` → `insertComposerQuote(quote, comment, source)` 写入 composer,而 Kimi 的引用是 composer schema 里的行内原子节点 `quote`
|
|
179
|
+
(DOM 形如 `span.quote-pill[data-quote-text][data-quote-comment]`)。对照 Kimi Code 自己的会话记录能看到这个节点的真实形状:
|
|
180
|
+
**引用与评论是同一个 Tiptap 节点的 `attrs.text` / `attrs.comment`**(`{"type":"quote","attrs":{"text":…,"comment":…}}`,引用和评论从不拆成两段),
|
|
181
|
+
发给模型时展开成「`> 引用` + 空行 + 评论」,而它在会话里显示、存储用的 `display_text` 就是 `引用 · 评论`。
|
|
182
|
+
本插件逐条对应到 DSH:`.sab` → 本文的单例浮层,`quote` 节点 → DSH 的 `reference-chip`(两半都放进同一颗芯片的 `ref` 与标签),
|
|
183
|
+
`insertComposerQuote` → `registerSource` + `insertReference` + `codec.serialize`,发送后留在消息里的 `quote-pill` →
|
|
184
|
+
transcript 侧的 `.dshq-cap` 胶囊:一颗胶囊同时装引用与评论,`复制` 出来的也是 `引用 · 评论`(见上一节)。
|
|
185
|
+
浮层的第三项不在对应关系里——`.sab` 的 `menu` 模式只有两行,侧边对话是本插件自己的扩展(见上一节)。
|
|
186
|
+
|
|
187
|
+
## 与同类插件的差异
|
|
188
|
+
|
|
189
|
+
判断依据是市场全量目录(`https://awesome-dsh-plugin.com/plugins.json`,2026-10-10 快照:**4471 条**,字段含 stars / downloads / capabilities),不是印象。**划选/引用/批注这一带一共只有 85 条**——小赛道,但很挤:「划选浮层 + 加进输入框」这个形态本身已经饱和(下面几个邻居都做得到)。
|
|
190
|
+
|
|
191
|
+
| 同类 | 它做什么 | 本插件多出来的部分 |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| `tr1v3r/dsh-quote-followup`、`sumomok/quote-message` | 选区 → **原生 reference chip** 进输入框 | 芯片机制不是差异点(它们也用同一个 API)。差异在芯片**之后** |
|
|
194
|
+
| `sunnystarye-ui/dsh-plugin-text-quote`、`choco9527/dsh-add-to-chat` | 选中 → 带注释的引用 → 进输入框;发送后可看注释 | 引用与评论**同一颗胶囊**、悬停卡片、`跳回原文`、清单页、线程回流 |
|
|
195
|
+
| `latte03/dsh-select-quote` | 原文旁虚线高亮 + 序号角标 + 摘要可查看/跳回 | 它把批注挂在下一条消息上;本插件是**胶囊模型**,且坐标按引用原文归一化后跨刷新仍在 |
|
|
196
|
+
| `dsh-sidenote`(11★)、`dsh-side-chat`、`side-chat-plus-plus`、`dsh-side-branch` 等 16 条侧聊 | 划选 → 在侧边线程里**提问/作答** | **只把引用写进线程输入框、不替你发送**:16 条里没有第二条同样语义;线程被记住并复用,`Alt+L` 那一页还能把线程结论带回主会话 |
|
|
197
|
+
|
|
198
|
+
全库计数里几个**零**(同一份快照的关键词探针):
|
|
199
|
+
|
|
200
|
+
- **「引用 + 评论合成单一可交互胶囊」:0 条** —— 本插件的 `❝ 引用摘要 │ 评论摘要` 仍是空位。
|
|
201
|
+
- **「编辑已发送的批注」:0 条** —— 所有人都是「写→发→再看」。DSH 目前**没有**任何编辑已发送消息的 API(侦察确认:`editMessage`/`updateMessage`/`rewriteMessage` 全树 0 命中,只有尚未进入会话的队列条目可改,见「已知限制」),所以这一格本插件也没填,留待宿主开口。
|
|
202
|
+
- **消息内联操作条**仅 2 条、**消息内评论**仅 2 条:发送后还在消息里「就地操作引用」的,几乎没有。
|
|
203
|
+
|
|
204
|
+
本插件相对同类的**可验证**优势集中在四件事上,都有离线断言兜底:
|
|
205
|
+
|
|
206
|
+
1. **语义而不是机制**:引用与评论是同一颗节点(对齐 Kimi 的 `quote` 节点 `{text, comment}`),一颗芯片 → 一个块 → 一颗胶囊 → 一张卡片 → 一次复制;机制(reference chip、DOM 增强)谁都能抄,这条链子不好抄。
|
|
207
|
+
2. **引用是带坐标的活对象**:`跳回原文` 在自己的会话面板里找行、按宿主的方式对齐到 24px,行被虚拟列表藏起来或没挂载都能找回来;坐标不随引用文本发给模型(不污染 prompt)。
|
|
208
|
+
3. **引用有下落**:清单页(搜索 / 跳转 / 导出 Markdown)+ 侧边线程回流(结论写回主会话草稿 / 导出线程),这两件事在 4471 条里没有对应物。
|
|
209
|
+
4. **工程化**:253 项离线断言(含真跑 `sessions.retain` / `setDraft` / 下载链路)、每个宿主契约都在 README 里写明出处、每个版本都有可回读的 release。同类里多数是一两百行、没有测试的插件。
|
|
210
|
+
|
|
211
|
+
反过来也要说清楚:**本插件不做的**——不做翻译/解释类选区动作(81 条同类在做)、不做多选合并引用(7 条有)、不做选区折叠/行内 diff 这类渲染花样。它只把「引用」这一件事做穿。
|
|
212
|
+
|
|
213
|
+
## 声明(package.json)
|
|
214
|
+
|
|
215
|
+
| 字段 | 作用 |
|
|
216
|
+
| --- | --- |
|
|
217
|
+
| `dsh.bundle.patch` | 指向 `cordis.patch.yml`:作为**组合包层补丁**插入 `ui-quote` 这一行 loader 条目。 |
|
|
218
|
+
| `dsh.client.platform: "web"` | 声明浏览器侧 bundle。host 还接受 `dsh.client.inject`(声明依赖的客户端包)、`external`(列非基线模块请求,默认 `[]`)、`immediately`(是否随启动立即实例化,默认 `false`);本插件只用平台基线模块,故都不声明。 |
|
|
219
|
+
| `exports["./client"]` | 必须导出:host 用 `clientExportOf(pkg.exports)` 找 bundle。 |
|
|
220
|
+
| `exports["./package.json"]` | 必须导出:插件页元数据要解析 `<pkg>/package.json`。 |
|
|
221
|
+
| `exports["./locale/*.json"]` | 必须导出:host 通过 ESM 解析器逐个读语言文件。 |
|
|
222
|
+
| `icon` | 卡片图标。**相对路径**(不能绝对路径、不能带协议)、SVG/PNG/JPEG/WebP、≤ 256 KiB、realpath 后必须留在包目录内;host 读成 `data:` URL 交给插件页。 |
|
|
223
|
+
| `locale/<lang>.json` | `{"meta": {"title": …, "description": …}}`。`locale/en.json` 是英文锚点(host 以它所在目录为准扫描同目录 `*.json`,语言名取自文件名);缺 `en.json` 就没词条,`en` 回退到 `package.json` 的 `name` / `description`。 |
|
|
224
|
+
| `engines.dsh`(清单根) | 声明兼容的 DSH 版本范围。当前 host 不校验它(声明式)。 |
|
|
225
|
+
| `dsh.manifestVersion: 1` | 清单格式号,同样只作文档。 |
|
|
226
|
+
| `peerDependencies` | **故意留空**:`@deepseek-ai/dsh*` 的 peer 范围是唯一被强制校验的兼容性字段,范围不满足时 preflight 会在组合期把该行 `disabled`(需要 `dsh plugin allow-version` 或 profile 的 `compatibility.json` 豁免)。本插件只 require 平台基线模块 `react`,不需要 peer。 |
|
|
227
|
+
|
|
228
|
+
`npm test` 会逐条复核上面这些:补丁文件、三个导出、图标类型与体积、中英词条、以及「没有会被强制校验的 peer」。
|
|
229
|
+
|
|
230
|
+
## 结构
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
package.json DSH 插件清单(bundle patch + client 半 + icon + locale 导出)
|
|
234
|
+
icon.svg 插件页卡片图标(相对路径、≤ 256 KiB 的 SVG)
|
|
235
|
+
cordis.patch.yml 组合包层补丁:插入 ui-quote 这个 loader 条目
|
|
236
|
+
lib/index.js host 半:只声明 apply(),不提供服务
|
|
237
|
+
lib/client.js 全部功能:vendor-CJS 工厂 + 浮层(三项动作)+ 输入框芯片 + 发送后胶囊 + 侧边对话接线 + 引用清单页
|
|
238
|
+
locale/zh.json 插件页卡片文案(中文)
|
|
239
|
+
locale/en.json 插件页卡片文案(英文)
|
|
240
|
+
test/check.mjs 离线自检(清单 + 组件契约)
|
|
241
|
+
test/dom.mjs 给离线自检用的最小 DOM / MutationObserver 垫片
|
|
242
|
+
test/capsule.mjs 离线自检(发送后胶囊 + 悬停卡片 + 跳回原文 + 键位 + 改这条引用 + 代码引用,159 项断言)
|
|
243
|
+
test/panel.mjs 离线自检(引用清单:解析 / 搜索 / Markdown / 读取 / 导出 / 页面注册 + 侧边线程回流,94 项断言)
|
|
244
|
+
test/capsule.html 真浏览器里跑同一套胶囊断言(可选)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
功能完全在浏览器侧:`lib/client.js` 用 `window.__ModuleLoader__.load({ id, factory })` 注册,只 `require("react")`
|
|
248
|
+
(平台基线模块),不依赖 react-dom、CSS 文件或 host 半参与序列化;浮层样式是运行时注入的内联 `<style>`,
|
|
249
|
+
胶囊样式是 `<style data-plugin="dsh-client-ui-quote">`。
|
|
250
|
+
|
|
251
|
+
## 自检
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
node --check lib/client.js
|
|
255
|
+
npm test # = node test/check.mjs && node test/capsule.mjs && node test/panel.mjs
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`test/check.mjs` 用桩 `window.__ModuleLoader__` / 桩 `react` / 桩 host ctx 加载 bundle,断言模块 id、服务声明、
|
|
259
|
+
槽位注册(`conversation.input.overlay` / `quote-selection` / locale `ui-quote`)、两套词条(含侧边对话的中英标签)、
|
|
260
|
+
「组件渲染 null」,以及 reference source 只注册一次、`codec.serialize` 原样返回模型形态、菜单候选保持为空。
|
|
261
|
+
它还断言侧边对话的可用性探测:没有 `betterSidebar` 时返回 null、页签未注册或被停用时返回 null、齐备时返回服务本身,
|
|
262
|
+
外加 `sideQuestion`(纯块引用形态)与 `sideChatTitle`(首行 + 芯片式截断)两个纯函数,以及线程记忆与草稿预填那四个函数
|
|
263
|
+
(`storedSideThread` / `rememberSideThread` / `displayedThreadId` / `prefillDraft`)在导出面上确实存在。
|
|
264
|
+
它还会按 host 读取清单的方式复核 `package.json`:`dsh.manifestVersion` / `dsh.client.platform` / `engines.dsh` 的位置、
|
|
265
|
+
`dsh.bundle.patch` 指向的补丁文件确实插入了 `ui-quote`、四个 `exports`、图标(相对路径 + 类型 + ≤ 256 KiB)、
|
|
266
|
+
`locale/*.json` 的 `meta.title` / `meta.description`,以及「没有会被 preflight 拒绝的 `@deepseek-ai/dsh*` peer」。
|
|
267
|
+
|
|
268
|
+
`test/capsule.mjs` 在 `test/dom.mjs` 的最小 DOM 上真跑一遍胶囊:把 `check.mjs` 那套桩 ctx 换成会记账的版本,塞进一条
|
|
269
|
+
「用户文字 + 序列化引用」的 `span.plainRun`,然后断言 —— 每个引用 run 恰好一颗胶囊、原 run 只是 `display:none` 且仍在文档里、
|
|
270
|
+
用户自己的话另起 `span.dshq-body`、胶囊里是引用原文(有评论时评论在同一颗胶囊里且不会多出一颗)、`复制` 写出 `引用 · 评论`、
|
|
271
|
+
React 重渲染不会出现第二颗、引用变了就地重建、消息被删胶囊一起消失、卸载后 run 与样式表都恢复、手写的 `> ` 引用块不被误伤;
|
|
272
|
+
悬停胶囊或输入框芯片时浮出卡片(两段全文都在、贴底自动翻到上方、左右夹取不越界、离开后收起、锚点被移除 / 发出去后卡片跟着收起、
|
|
273
|
+
卸载后卡片消失且芯片钩子摘除、输入框里两颗芯片各显示自己的引用、节点被复用后显示当前那一条、标签对不上时不出卡片);
|
|
274
|
+
菜单纵向位置(默认在选区下方、贴底改到上方、比视口还高时仍留在视口内);
|
|
275
|
+
一条消息里引用两处时,两条引用各得一颗胶囊、两颗之间的原话仍在原位、相邻两条引用也不会被吞掉;
|
|
276
|
+
**代码引用**整条线格式(预览/终端来源序列化成块引用内的围栏、正文自带 ``` 时换 `~~~`、解析时围栏剥干净逐字还原、普通引用逐字 round-trip、
|
|
277
|
+
代码胶囊挂 `data-code` 且卡片走 `dshq-pop-code` 等宽排版、侧边线程落裸围栏);
|
|
278
|
+
侧边对话整条链路在桩上真跑一遍——`sidechat.start` 的 URL / POST 形态与载荷(sessionId + **空 question**,也就是「什么都不发」)、
|
|
279
|
+
返回 `{childId}` 后 `openTab` 恰好一次且 `meta.threadId` 绑对线程、引用被 `setDraft` 写进那条线程的输入框(草稿里已有内容时接在下面、空行隔开)、
|
|
280
|
+
第二次点击复用记住的线程而不再创建、并发第二次点击并入第一次、外壳只暴露 `actions.setDraft` 时同样写得进去、
|
|
281
|
+
路由拒绝与 `openTab` 抛错都只落一条 toast 且绝不开页签、外壳没有 `setDraft` 时另落一条 toast、
|
|
282
|
+
线程记忆(自己的 map、host 的绑定优先、空 id 只忘自己的)、空引用不发请求;
|
|
283
|
+
跳回原文的坐标与落点(按引用原文归一化折叠与截断成键、坐标落进 `localStorage`、按 `data-chat-anchor-key` 在**引用自己那条会话的面板**里找到行、停到视野上方 24px 并闪一下、
|
|
284
|
+
同一个锚点键出现在别的会话面板里时不被误跳、被 `hidden` 藏起来的行让位给同轮次的可见行、还没挂载的行靠滚动搜索一屏一屏找出来并落在它身上、往上找到顶后从最新处再来一遍也能找到、都找不到时给 toast);
|
|
285
|
+
三个键位的注册形状(id 前缀、Alt 默认值、平台键表、区域 `page` + `editable`、没有选区时 `resolve` 返回 `pass`、注册返回 disposer);
|
|
286
|
+
**改这条引用**整条链路:挂载 `conversation.input.overlay` 座位(桩 react 按挂载语义跑 effect,于是会话真的注册进插件)、
|
|
287
|
+
卡片给出 `跳回原文` 与 `改这条引用` 两个动作、点后者把栏切到评论模式并**预填旧评论与旧正文**、
|
|
288
|
+
按确认后 `insertReference` 恰好一次(新芯片的 `ref` 带改后的评论)、旧引用被记为「已修订」并落进 `localStorage`、
|
|
289
|
+
旧胶囊挂上 `已修订` 徽标、它的卡片显示改后的评论(连正文改过的连改后原文一起显示)、空修订不算修订、
|
|
290
|
+
修订不改写引用原文本身、清空引用正文时确认按钮禁用、改后的正文随新芯片发出(共 159 项)。
|
|
291
|
+
`test/capsule.html` 是同一套断言的真浏览器版,用 `file://` 打开即可(页面会把结果写进标题与 `#probe-out`)。
|
|
292
|
+
|
|
293
|
+
`test/panel.mjs` 单独跑引用清单页与侧边线程回流(94 项):只认**持久**的 `user/message`(`transient`、助手消息、`data: null` 都跳过)、
|
|
294
|
+
引用按原文逐行拆出来而评论跟着同一条记录、最新一条排最前、空查询保持全量、引用与评论都能被搜到且忽略大小写、
|
|
295
|
+
Markdown 的标题/编号/块引用/评论行/时间戳与空列表兜底、超长引用在标题里被截断、非法时间导出为空;
|
|
296
|
+
读取侧用桩 `sessions.retain` 真跑一遍——读出两行且 `hasMore` 透传、**读完必须 release**(记录 `source: "quoteList"`)、
|
|
297
|
+
`loadOlder()` 被调用后重读、`retain` 抛错变成 `{ ok: false }` 而不是异常、解析不出 binding 时是 `no-session`;
|
|
298
|
+
导出断言下载文件名与 blob URL;注册侧断言页面类型(id/kind/title/guide 条目与 `commandId`)、页体的 keyed 座位与 `inject(sessionId)` 的四个能力、
|
|
299
|
+
标题座位、面板样式表真的进了 `<head>`、`openQuoteList()` 用 kind 打开以及宿主拒绝时不抛异常、
|
|
300
|
+
`Alt+L` 那个命令**没有选区也返回 handled**、四个命令的 id 依次是 `ui-quote.quote/comment/sidechat/list`,
|
|
301
|
+
以及单行的 markup(有坐标 → `jumpable` 且点击回调收到整行,无坐标 → `plain`)与页体外壳(标题、搜索框占位符、底部的侧边线程一节)。
|
|
302
|
+
侧边线程那一节另有 35 项:`sessionMessagesFromEntries` 认两种角色的文本存放位置(用户消息在 `event.data`、助手在 `event.data.message`)、工具结果与空消息不算、
|
|
303
|
+
顺序与 seq/时间透传;`threadConclusion` 取「最后一条提问 + 它之后的最后一条回答」、提问写成 `>` 块引用、只有提问/只有回答/空线程的三种退化、
|
|
304
|
+
超长截断并标注;`threadMarkdown` 的标题/编号/角色/时间与空线程兜底;`mainSessionForThread` 反向查两张线程记忆表(含 better-sidebar 的对象形态);
|
|
305
|
+
`readSessionMessages` 真读一遍且 release;`loadThreadSummary` 给出问题与条数、没有线程时 `threadId` 为 null;
|
|
306
|
+
`bringThreadBack` 把结论写成**草稿**(原有草稿保留、空行隔开)、外壳没有 `setDraft` 时如实失败、没有线程时不乱写;
|
|
307
|
+
`exportThread` 的文件名与整条线程内容、以及 MIME 是 Markdown。
|
|
308
|
+
|
|
309
|
+
合计 **253 项**(check.mjs 的契约断言 + capsule 159 + panel 94),`npm test` 全绿。
|
|
310
|
+
|
|
311
|
+
## 已知限制
|
|
312
|
+
|
|
313
|
+
- **侧边对话依赖两个半公开接口**:dsh-better-sidebar(实测 0.25.0;`meta` 种子通道自 0.12.0、原生右侧栏承载自 0.19.0 起可用)
|
|
314
|
+
的 `POST /sidebar/api/sidechat.start`(本插件靠「空 `question`」拿到一条**什么都不发**的新线程),
|
|
315
|
+
以及 DSH 客户端自己的输入框外壳 `SessionInputShell.setDraft`(把引用写成子会话的草稿)。
|
|
316
|
+
未安装、被禁用、或它的「侧边对话」页签在侧边卡片设置里被关掉时,浮层第三项**整个不渲染**;
|
|
317
|
+
两个接口升级后若失效都不会静默:开不了线程 → toast「侧边对话没有打开,请重试。」,写得进不去草稿 → toast「侧边对话已打开,但引用没能写进它的输入框」;
|
|
318
|
+
线程创建要求父会话处于运行态(历史归档会话会收到 409,同样落 toast)。
|
|
319
|
+
- **「改这条引用」是追加,不是原地编辑**:DSH 的 transcript 只追加(侦察确认全树没有 `editMessage` / `updateMessage` / `rewriteMessage` / 回收 API,只有**尚未进入会话**的队列条目可改)。所以修订发出一条新引用(评论、引用正文都可改),旧胶囊只挂 `已修订` 标记——标记存在**本机** `localStorage`(`dsh-client-ui-quote:v1:quote-revised`,按引用原文归一键、上限 60 条),因此不会跨标签页、跨设备同步,也不进入模型上下文;模型看到的是那条改后的新引用。正文改过后键仍是旧原文:`已修订` 挂在**旧**胶囊上(它的卡片里显示改后的原文与评论),改后的新胶囊不挂标。
|
|
320
|
+
- **代码引用的识别靠首行围栏**:`> ❝ ``` ` 开头的块按代码解析(围栏剥掉、胶囊等宽)。因此一条**普通**引用如果正文恰好以 ``` 或 `~~~` 开头(比如在 Markdown 文件预览里引用了源码),回读时也会被当成代码引用——显示上是等宽排版,文字不丢。
|
|
321
|
+
- 依赖 DSH 客户端的内部接口(`conversation.input.overlay` 槽位、`inputTriggers.registerSource`、`conversation.input.shell(id).insertReference`、
|
|
322
|
+
`reference-chip` 节点),DSH 升级有可能失效。
|
|
323
|
+
- **引用清单页依赖右侧栏的两个服务**(`sidebarRightTabs` 与 `sidebarRight`,实测 0.2.0-rc.2);宿主没有它们时页面不注册、`Alt+L` 给一条 toast
|
|
324
|
+
(「引用清单打不开:右侧栏服务不可用」)而不是报错。它的数据来自会话事件日志,**历史是分页的**:只列已加载的消息,更早的要按 `加载更早的消息`;
|
|
325
|
+
如果会话做过原生上下文压缩/裁剪,被裁掉的那些消息里的引用也不会出现在清单里。导出是浏览器下载(宿主没有给客户端插件写盘接口),
|
|
326
|
+
文件名形如 `quotes-<sessionId>.md`;只有被本插件看着选中过的引用那一行可点击跳转,其余行只是可读文本。
|
|
327
|
+
- 仅 web / 桌面 profile;选区必须落在会话正文里。
|
|
328
|
+
- 引用在输入框里显示为胶囊,编辑器中复制粘贴出去的是它的模型形态文本。
|
|
329
|
+
- transcript 胶囊是在 DSH 渲染结果之上做的覆盖层:它只隐藏原节点、不搬移 React 的节点,但若 DSH 改了消息的 DOM 结构
|
|
330
|
+
(`[data-conversation-content]`、纯文本 run 的形态),需要跟着改 `isQuoteRun` / `capsuleFor`。
|
|
331
|
+
- 收起状态的胶囊是**单行**:很长或分段的引用与评论会被省略号截断 —— 鼠标悬停会浮出完整内容,也可以点一下直接在原地展开。
|
|
332
|
+
- 更新**之前**发出的老消息不会被变成胶囊:它们的文本里没有 `❝` 标记,插件无从辨认(重新引用一次即可)。
|
|
333
|
+
|
|
334
|
+
## License
|
|
335
|
+
|
|
336
|
+
[MIT](LICENSE)
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
### English
|
|
341
|
+
|
|
342
|
+
**dsh-client-ui-quote** — select any sentence in a DSH conversation and quote it into the composer, Kimi-Code style.
|
|
343
|
+
The floating bar opens **under** the selection (above it only when there is no room below) and offers **Comment**,
|
|
344
|
+
**Add to conversation**, and — the plugin's own addition — **Side chat**: the quote is written into the composer of one
|
|
345
|
+
independent side thread per main session (dsh-better-sidebar's side chat), which inherits the main session's full context
|
|
346
|
+
and opens as a tab in DSH's native right sidebar, drawn in DeepSeek's whale blue. Nothing is sent — the question that
|
|
347
|
+
thread answers stays yours to write, and clicking again appends the next quote to the same thread's draft. The quoted
|
|
348
|
+
text itself is editable in the comment box (trim the selection, correct a revision's wording), and a selection from the
|
|
349
|
+
file preview or the terminal travels to the model as a fenced code block. The quote — and the comment, kept together in the same
|
|
350
|
+
chip the way Kimi keeps both halves in one composer node — becomes DSH's native inline `reference-chip`, is serialized
|
|
351
|
+
back into a `> ❝ …` / `> ❞ …` blockquote when you send, and is then drawn as a single rounded **capsule** in the
|
|
352
|
+
transcript (`quote │ comment`, hover for the full text, click to expand, copy button), so a quote never floods the message with `>` lines —
|
|
353
|
+
the model still receives the full text. The same hover card opens over the composer chip, whose label has to be clipped,
|
|
354
|
+
and it closes with the chip as soon as the message is sent. Each chip resolves its own quote at hover time — React
|
|
355
|
+
reuses a chip node for the next quote, so a card can never show the previous one's text.
|
|
356
|
+
Install with `dsh plugin --profile desktop add link:<clone-path>` (DSH >= 0.2.0-rc.2, web/desktop client).
|
|
357
|
+
Test with `npm test`. MIT licensed.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Bundle layer for dsh-client-ui-quote.
|
|
2
|
+
#
|
|
3
|
+
# Used when this package is selected as a profile bundle
|
|
4
|
+
# (`dsh plugin --profile <name> add link:<path-to-this-package>`): the row below
|
|
5
|
+
# inserts the plugin's Loader entry. The same row may instead be appended to the
|
|
6
|
+
# profile's own cordis.patch.yml, which is what the manual install path does.
|
|
7
|
+
- insert:
|
|
8
|
+
- id: ui-quote
|
|
9
|
+
name: dsh-client-ui-quote
|
package/icon.svg
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="quote selection">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="badge" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#74ACFF"/>
|
|
5
|
+
<stop offset="1" stop-color="#2F6BFF"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="pick" x1="0" y1="0" x2="1" y2="0">
|
|
8
|
+
<stop offset="0" stop-color="#FFE18F"/>
|
|
9
|
+
<stop offset="1" stop-color="#FFC53D"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<rect width="1024" height="1024" rx="232" fill="url(#badge)"/>
|
|
13
|
+
<rect x="352" y="440" width="512" height="136" rx="68" fill="url(#pick)" opacity=".95"/>
|
|
14
|
+
<rect x="240" y="272" width="88" height="480" rx="44" fill="#ffffff" opacity=".96"/>
|
|
15
|
+
<rect x="392" y="272" width="384" height="88" rx="44" fill="#ffffff" opacity=".92"/>
|
|
16
|
+
<rect x="392" y="464" width="320" height="88" rx="44" fill="#0F2F7A" opacity=".72"/>
|
|
17
|
+
<rect x="392" y="656" width="256" height="88" rx="44" fill="#ffffff" opacity=".74"/>
|
|
18
|
+
</svg>
|