dsh-client-ui-quote 0.12.3-lab.36 → 0.12.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/CHANGELOG.md CHANGED
@@ -2,11 +2,15 @@
2
2
 
3
3
  ## Unreleased (lab)
4
4
 
5
+ _(下一轮)_
6
+
7
+ ## 0.12.3 — 2026-10-26
8
+
5
9
  ### Added
6
10
 
7
- - 侧边对话默认内置隐藏线程,不再需要安装 dsh-better-sidebar:宿主半注册 `POST /ui-quote/api/sidechat.start`(同源围栏),从在线父会话复制种子,用宿主 `agents.create` 建 `origin: "subagent"` 子会话,并在宿主内置 `subagentchat` 页签打开;descriptor 与会话标题使用与 better-sidebar 共享的 `Side: ` 约定,装不装它都不冲突。宿主半不可用(旧宿主 / 父会话不在线 / 创建被拒)时自动退回 `sessions.fork` + 本插件原生 `quote-sidechat` 页签;`sideChatBackend: "better"` 才优先走 better-sidebar。
11
+ - 侧边对话默认内置隐藏线程,不再需要安装 dsh-better-sidebar:宿主半注册 `POST /ui-quote/api/sidechat.start`(同源围栏),从在线父会话取最近几轮原话做成一条隐性上下文(不复制日志),用宿主 `agents.create` 建 `origin: "subagent"` 子会话,并在宿主内置 `subagentchat` 页签打开;descriptor 与会话标题使用与 better-sidebar 共享的 `Side: ` 约定,装不装它都不冲突。宿主半不可用(旧宿主 / 父会话不在线 / 创建被拒)时自动退回 `sessions.fork` + 本插件原生 `quote-sidechat` 页签;`sideChatBackend: "better"` 才优先走 better-sidebar。
8
12
  - 侧边线程记忆与清单:新增 `dsh-client-ui-quote:v1:sidechat-target` 记录隐藏线程的父会话;读取线程时用 subagent 地址 `{parentSessionId, childSessionId, mode:"continuable"}`;线程列表同时扫描会话列表里的 `Side:` 子会话,所以 better-sidebar(或旧版本)创建的线程也能在本插件里打开、带回和导出。
9
- - 离线断言 **662 组**(check 12 + capsule 449 + panel 159 + host 42);新增原生页签的地址解析、标题、资源 retain/release、页签注册与卸载断言,以及宿主半的种子复制 / 继承切点 / 回合收口 / 悬挂工具调用截断 / `agents.create` 参数 / 探针与围栏断言。
13
+ - 离线断言 **667 组**(check 12 + capsule 451 + panel 160 + host 44);新增原生页签的地址解析、标题、资源 retain/release、页签注册与卸载断言,以及宿主半的隐性上下文合成(只收原话、8000 字上限、超长截断)/ 继承切点与 descriptor 顺序 / 收口回合 / `agents.create` 参数 / 探针与围栏断言。
10
14
 
11
15
  ### Fixed
12
16
 
@@ -18,7 +22,7 @@
18
22
  - 侧聊打开时不再把主会话的历史铺在界面上:种子从「复制父会话整条日志」改成**一条隐性上下文**——先是 session/end-seed{inherited:true} 切点,再是一条 source.kind = session-context 的 user/message(只收用户与助手的原话、最新在后、上限 8000 字,跳过工具事件与注入上下文),最后是 descriptor。模型照旧拿到上下文,侧聊里只折叠成一行「上下文」;同时不再为复制整条日志付一遍 token。
19
23
  - 侧聊草稿里不再写 `Side conversation boundary.` 那段元文案:「只作背景、别继续执行」的指令挪进隐性上下文,输入框里只剩这条引用本身。
20
24
  - 隐藏线程的侧聊页一打开就「历史加载失败:subagent descriptor is unavailable」:种子把 `subagent/descriptor` 放在继承前缀里、又用整段种子的长度当 `inheritedEventCount`,宿主的 `api-session-controller` 要求 descriptor 的 `seq` 不小于继承切点,于是整条子会话的历史都读不出来。现在种子先落一条 `session/end-seed { inherited: true }` 标出切点,回合收口、工作目录与 descriptor 全部排在切点之后;嵌套侧聊的父 descriptor 留在继承前缀里,我们自己的那条排在后面(投影按最后一次生效)。
21
- - 修复前造出的坏线程会被反复打开:`sessions.retain` 对它们**照样成功**(只有历史 RPC 拒绝),所以「试开一次」判断不出来。现在客户端给新建的线程盖形状版本戳(`dsh-client-ui-quote:v1:sidechat-target` 里的 `v: 2`),只复用自己盖过章的线程;better-sidebar 的、旧版本的、扫描发现的线程一律丢弃重建——一次误杀换永久自愈。
25
+ - 修复前造出的坏线程会被反复打开:`sessions.retain` 对它们**照样成功**(只有历史 RPC 拒绝),所以「试开一次」判断不出来。现在客户端给新建的线程盖形状版本戳(`dsh-client-ui-quote:v1:sidechat-target` 里的当前版本,现在是 `v: 4`),只复用自己盖过章的线程;better-sidebar 的、旧版本的、扫描发现的线程一律丢弃重建——一次误杀换永久自愈。
22
26
  - 正文以 ``` 或 `~~~` 开头的普通引用在回读时丢了首行:识别代码引用只看首行围栏,却没有要求块尾有配对的收尾围栏,于是引用一段 Markdown 源码时那行被当围栏剥掉(只有一行的引用会整条消失,胶囊、悬停卡片与引用清单都少字)。现在围栏必须成对才算传输层——写出去的代码引用收尾围栏始终成对,旧消息解析不变;不成对的孤围栏按正文还原。
23
27
 
24
28
  ## 0.12.2 — 2026-10-22
package/README.md CHANGED
@@ -96,7 +96,7 @@ __dshQuote.probe() // 宿主服务可见性(inputTriggers
96
96
 
97
97
  ### 方式一:npm 包(推荐)
98
98
 
99
- 已发布到 npm:`dsh-client-ui-quote`(0.10.0 起上架;稳定版当前 **0.12.2**)。`lab` 分支始终领先稳定包一个补丁:日常改动先在 lab 上发 `-lab.N` 预发布,验收后再提升;两边的差异看 `CHANGELOG.md` 的 `Unreleased` 段。
99
+ 已发布到 npm:`dsh-client-ui-quote`(0.10.0 起上架;稳定版当前 **0.12.3**)。`lab` 分支始终领先稳定包一个补丁:日常改动先在 lab 上发 `-lab.N` 预发布,验收后再提升;两边的差异看 `CHANGELOG.md` 的 `Unreleased` 段。
100
100
 
101
101
  ```bash
102
102
  dsh plugin --profile desktop add dsh-client-ui-quote
@@ -110,7 +110,7 @@ npmmirror 与 npmjs 一般 10 分钟内同步,DSH 配置了镜像源即可国
110
110
 
111
111
  1. 把包装进 profile:`cd <DSH_HOME>/profiles/<profile> && pnpm add dsh-client-ui-quote`;
112
112
  2. 编辑 `<DSH_HOME>/profiles/<profile>/package.json`:
113
- - `dependencies` 里应有 `"dsh-client-ui-quote": "^0.12.2"`(安装器会写);
113
+ - `dependencies` 里应有 `"dsh-client-ui-quote": "^0.12.3"`(安装器会写);
114
114
  - `dsh.profile.bundles` 追加 `"dsh-client-ui-quote"`;
115
115
  3. 重启 DSH(profile 打开 `patchReload` 时也会自动重载)。
116
116
 
@@ -139,7 +139,7 @@ link 的优点是改完刷新即生效;**日常运行请用方式一**,不
139
139
 
140
140
  本机切换这三档(这台开发机专用的辅助脚本,不在仓库里):`node .dsh-quote-work/profile-mode.mjs link|lab|stable|status`。换完在窗口里 `Ctrl+Shift+R` 生效;只有宿主半有改动时才需要重启桌面端。
141
141
 
142
- 两条线的断言入口相同:`npm test`(662 项:check 12 + capsule 449 + panel 159 + host 42);`.github/workflows/test.yml` 在每次 push / PR 上用 Node 18/20/22 各跑一遍,`publish.yml` 里 `prepublishOnly` 再跑一次才发。
142
+ 两条线的断言入口相同:`npm test`(667 项:check 12 + capsule 451 + panel 160 + host 44);`.github/workflows/test.yml` 在每次 push / PR 上用 Node 18/20/22 各跑一遍,`publish.yml` 里 `prepublishOnly` 再跑一次才发。
143
143
 
144
144
  **推送通道**:`git push` 优先,但本机 `github.com:443` **时通时不通**(2026-10-19 实测:短时可用、随后连续超时,每次失败等约 21 秒)。日常用工作区根的 `promote.ps1`:先试 git,失败自动转 REST 兜底(`.dsh-quote-work/publish-release.mjs`、`.dsh-quote-work/push-branch.mjs`,`-Channel rest` 可跳过探测),并核对远端 SHA。**别混用两条通道**:REST 重建的提交时区归一到 `+0000`,内容相同但 SHA 不同,同一分支混推会分叉(`git status` 看着干净,远端却多一条平行提交);恢复就先 `git fetch`、确认 `git diff <本地> <远端>` 为空,再把本地线 reset 到远端。
145
145
 
@@ -254,7 +254,7 @@ DSH 的输入框是 Lexical,没有 Kimi 的 `quote` 节点,但有一个通
254
254
 
255
255
  第三项是本插件自己的扩展(Kimi 的浮层只有两项),融合了三家 agent 同类组件的优点:**Kimi Code 的 `/btw` 侧边聊天**——一键在侧边栏开一条继承主会话上下文的线程;**Qoder 的「在侧边任务中提问」**——同一会话进行中的创建请求自动并入第一次,失败只弹一条提示,绝不大张旗鼓;**WorkBuddy 的划选引用**——引用是线程里持久的内容,而不是一次性的草稿附件。与前两家不同的是**问什么由你来写**:引用只落进那条线程的输入框,不会被当成第一条消息替你发出去。样式上第三项用 DeepSeek 的鲸鱼蓝(`#4d6bfe`)画图标与悬停色,白底浅色主题是主基调,深色主题同一色相。
256
256
 
257
- - **内置隐藏线程(默认)**:本插件的宿主半注册 `POST /ui-quote/api/sidechat.start`(同源围栏 `connection.requestRejection`,探针返回可用性)。正式调用时从在线父会话的事件日志复制种子(开着的回合补 `turn/end{interrupted}` 收口;有悬挂工具调用就截到回合前),用宿主 `agents.create` 建一个 **`origin: "subagent"`** 的子会话;descriptor 与会话标题都用 `Side: <引用首行>`。会话列表隐藏它,页签用宿主内置的 `subagentchat` 页打开(`dsh-resource://subagentchat/session/<child>?parent=<sessionId>&mode=continuable`),引用写进它的草稿。草稿第一行是可见的 `Side conversation boundary.` 说明,**同一段草稿里只写一次**(发送后下一段草稿会再带一次,作为提醒),避免模型把继承历史当成新任务。
257
+ - **内置隐藏线程(默认)**:本插件的宿主半注册 `POST /ui-quote/api/sidechat.start`(同源围栏 `connection.requestRejection`,探针返回可用性)。正式调用时从在线父会话取**最近几轮用户与助手的原话**(跳过工具事件与注入上下文,上限 8000 字),做成一条 `source.kind = "session-context"` 的 `user/message`,包在一个已收口的小回合里(有回合才不是 `blank`,否则宿主渲染欢迎页),再用宿主 `agents.create` 建一个 **`origin: "subagent"`** 的子会话;descriptor 与会话标题都用 `Side: <引用首行>`。**不复制父会话日志**,所以侧聊里看不到主会话那一长串历史。会话列表隐藏它,页签用宿主内置的 `subagentchat` 页打开(`dsh-resource://subagentchat/session/<child>?parent=<sessionId>&mode=continuable`),引用以**原生芯片**写进它的草稿(宿主没有芯片面时退回块引用),不写任何可见的边界文案——「只作背景、别继续执行」的指令在隐性上下文里。
258
258
  - **父会话在线时可继续**:隐藏线程是 `continuable` 子会话,父会话在线时内置页自己的输入框、流式输出与工具调用都能用;父会话关闭后该页按宿主规则只读,重新打开父会话即可继续。这个限制来自宿主的内置 `subagentchat` 页,不是本插件的额外依赖。
259
259
  - **共享 `Side:` 约定**:与 dsh-better-sidebar 的侧聊线程使用同一套持久约定(`origin: "subagent"` + `Side: ` 标题)。装不装 better-sidebar 都不冲突:它的自动打开会忽略这类标题;它的侧聊列表也会列出我们的线程,我们的引用清单也会扫到它的 `Side:` 线程。路由、存储键与 descriptor provider(`ui-quote`)全部独立。
260
260
  - **回退链**:宿主半不可用(旧宿主、`agents.create` 失败、父会话不在线)→ `sessions.fork` + 本插件 `quote-sidechat` 页签(线程以普通 fork 出现在会话列表)→ better-sidebar(仅显式 `sideChatBackend: "better"` 或最后兜底)→ 无。所有后端都只写草稿、不发送。
@@ -326,7 +326,7 @@ transcript 侧的 `.dshq-cap` 胶囊:一颗胶囊同时装引用与评论,`
326
326
  1. **语义而不是机制**:引用与评论是同一颗节点(对齐 Kimi 的 `quote` 节点 `{text, comment}`),一颗芯片 → 一个块 → 一颗胶囊 → 一张卡片 → 一次复制;机制(reference chip、DOM 增强)谁都能抄,这条链子不好抄。
327
327
  2. **引用是带坐标的活对象**:`跳回原文` 在自己的会话面板里找行、按宿主的方式对齐到 24px,行被虚拟列表藏起来或没挂载都能找回来;坐标以「人可读来源头 + base64 尾巴」随消息走——前者让模型知道出处,后者让插件跨刷新回读。base64 确实占一点 prompt,这是用「不依赖本地表」换来的取舍。
328
328
  3. **引用有下落**:清单页(搜索 / 跳转 / 导出 Markdown)+ 侧边线程回流(结论写回主会话草稿 / 导出线程),这两件事在同类里仍是少数。
329
- 4. **工程化**:662 项离线断言(含真跑 `sessions.retain` / `setDraft` / `agents.create` 参数 / 下载链路)、每个宿主契约都在 README 里写明出处、每个版本都有可回读的 release。同类里做到同等断言规模的仍是少数。
329
+ 4. **工程化**:667 项离线断言(含真跑 `sessions.retain` / `setDraft` / `agents.create` 参数 / 下载链路)、每个宿主契约都在 README 里写明出处、每个版本都有可回读的 release。同类里做到同等断言规模的仍是少数。
330
330
 
331
331
  反过来也要说清楚:**本插件不做的**——不做翻译/解释/改写/飞书投递类选区动作;不做图片/截图引用(**明确取舍**:图片引用要的是另一套胶囊/卡片/附件语义,是第二个产品的量级,塞进这里只会把「文本引用带坐标」这条主线冲淡;`dsh-selection-toolbar` 与 Kimi 桌面端已经做了,真要做建议独立成插件);不做完整 Topic 系统;不做常显大块引用。它只把「引用」这一件事做穿——但「做穿」包括发送后的管理、来源闭环和审阅,不只是浮层。
332
332
 
@@ -363,9 +363,9 @@ locale/zh.json 插件页卡片文案(中文)
363
363
  locale/en.json 插件页卡片文案(英文)
364
364
  test/check.mjs 离线自检(清单 + 组件契约)
365
365
  test/dom.mjs 给离线自检用的最小 DOM / MutationObserver 垫片
366
- test/capsule.mjs 离线自检(发送后胶囊 + 悬停卡片 + 跳回原文 + 键位 + 改这条引用 + 代码引用 + 文件路径打开 + 聊天代码块划选 + 选区保结构 + 回合级引用 + 逐条回应双语 + 草稿引用编辑/持久化 + 聚合条移除/无障碍 + 行范围标记 + 浮层不随座位失效,443 项断言)
367
- test/panel.mjs 离线自检(引用清单:解析 / 搜索 / Markdown / 读取 / 导出 / 页面注册 + 文件路径打开 + 侧边线程回流 + 审阅工作台(状态 / 标签 / 批量)+ JSON 导出 + 本地索引,153 项断言)
368
- test/host.mjs 离线自检(宿主半:标签 / 隐性上下文种子(只有三件事、只带用户与助手的原话、8000 字上限、跳过注入上下文)/ 空日志兜底 / 继承切点与 descriptor 顺序 / agents.create 参数 / 探针 / 围栏 / 路由注册与卸载,42 项断言)
366
+ test/capsule.mjs 离线自检(发送后胶囊 + 悬停卡片 + 跳回原文 + 键位 + 改这条引用 + 代码引用 + 文件路径打开 + 聊天代码块划选 + 选区保结构 + 回合级引用 + 逐条回应双语 + 草稿引用编辑/持久化 + 聚合条移除/无障碍 + 行范围标记 + 浮层不随座位失效,451 项断言)
367
+ test/panel.mjs 离线自检(引用清单:解析 / 搜索 / Markdown / 读取 / 导出 / 页面注册 + 文件路径打开 + 侧边线程回流 + 审阅工作台(状态 / 标签 / 批量)+ JSON 导出 + 本地索引,160 项断言)
368
+ test/host.mjs 离线自检(宿主半:标签 / 隐性上下文种子(切点 + 一个收口回合包着上下文消息 + descriptor;只带用户与助手的原话、8000 字上限、跳过注入上下文)/ 空日志兜底 / 继承切点与 descriptor 顺序 / agents.create 参数 / 探针 / 围栏 / 路由注册与卸载,43 项断言)
369
369
  test/capsule.html 真浏览器里跑同一套胶囊断言(51 项,可选)
370
370
  ```
371
371
 
@@ -416,7 +416,7 @@ React 重渲染不会出现第二颗、引用变了就地重建、消息被删
416
416
  修订不改写引用原文本身、清空引用正文时确认按钮禁用、改后的正文随新芯片发出;
417
417
  **浮层不跟着座位生死**(`conversation.input.overlay` 没挂载也能选中出栏、挂上两个会话座位既不叠加也不丢监听、
418
418
  座位不在时用输入框自己的 `actions` 落芯片、无处可插时给 toast 而不是静默返回、effect 卸载/重装后监听恰好一份);
419
- (共 443 项,含文件路径与逐条回应的线上形态、`parseCodeSource`/`fileAddressFor` 与「卡片/清单点击打开 session 文件并带上起始行」、**表格单元格 / 表格行 / 段落 / 列表项 / `<br>` 边界的引用高亮**、**输入框已有文字时引用置顶并将原文另起一行**、聊天代码块划选的语言探测与跨界降级、选区的链接/行内代码保真与纯文本回退、回合级引用的取文与坐标、英文逐条指令与「Quote N」徽章、草稿芯片索引的跨刷新恢复与聚合条编辑/移除、回复徽章的建立/重建/卸载与悬停卡片复用、原生 `quote-sidechat` 页签的注册 / 地址 / 标题 / retain-release、内置 subagentchat 的降级与复用,以及评论框两个输入区的自适应高度——引用正文那格曾经在被挂进栏之前就量高,读到 0 后被写成 `height:0`,首行因此被裁掉)。
419
+ (共 451 项,含文件路径与逐条回应的线上形态、`parseCodeSource`/`fileAddressFor` 与「卡片/清单点击打开 session 文件并带上起始行」、**表格单元格 / 表格行 / 段落 / 列表项 / `<br>` 边界的引用高亮**、**输入框已有文字时引用置顶并将原文另起一行**、聊天代码块划选的语言探测与跨界降级、选区的链接/行内代码保真与纯文本回退、回合级引用的取文与坐标、英文逐条指令与「Quote N」徽章、草稿芯片索引的跨刷新恢复与聚合条编辑/移除、回复徽章的建立/重建/卸载与悬停卡片复用、原生 `quote-sidechat` 页签的注册 / 地址 / 标题 / retain-release、内置 subagentchat 的降级与复用,以及评论框两个输入区的自适应高度——引用正文那格曾经在被挂进栏之前就量高,读到 0 后被写成 `height:0`,首行因此被裁掉)。
420
420
  `test/capsule.html` 是同一套断言的真浏览器版(51 项),用 `file://` 打开即可(页面会把结果写进标题与 `#probe-out`)。这一版修好了它**缺失的 `<body>` 标签**——脚本在 `<head>` 里执行时 `document.body` 为 null,这个页面以前在真浏览器里是直接坏的;并补上真 `Range` 的链接/行内代码断言(真浏览器的 `cloneContents()` 返回的是 DocumentFragment,不是元素——序列化器只认元素的话,链接在真浏览器里会退化成纯文本;离线桩现在也用真片段,不再放过这一类),另外补了 `CSS.highlights` 选区保持与关闭清理。
421
421
 
422
422
  `test/panel.mjs` 单独跑引用清单页与侧边线程回流(159 项):只认**持久**的 `user/message`(`transient`、助手消息、`data: null` 都跳过)、
@@ -437,13 +437,13 @@ Markdown 的标题/编号/块引用/评论行/时间戳与空列表兜底、超
437
437
 
438
438
  `test/host.mjs` 不需要浏览器:它直接 import 宿主半,用桩 `agents` / `agentPresets` / `sessionTitle` / `webServer` 驱动。断言包括:`Side: ` 标签的前缀与截断;种子固定三件事(`session/end-seed{inherited:true}` → 一条 `source.kind="session-context"` 的 `user/message` → `subagent/descriptor`)、`inheritedEventCount` 恒为 0、上下文字段只收用户与助手的原话(最新在后、8000 字上限、跳过 `runtime-context` 与工具事件)、空日志也有可读文案;descriptor 排在切点之后且 seq 连续;`agents.create` 收到 `origin: "subagent"` / `parentSession` / `isSeeded` / `inheritedEventCount` / 父会话当前模型路由 / preset setup / 清空继承 inbox / 固定标题;探针、409、503、403、405、404 与 `apply` 的注册/卸载全部有断言。
439
439
 
440
- 合计 **662 组**(check 12 + capsule 449 + panel 159 + host 42),`npm test` 全绿。
440
+ 合计 **667 组**(check 12 + capsule 451 + panel 160 + host 44),`npm test` 全绿。
441
441
 
442
442
  ## 已知限制
443
443
 
444
- - **内置隐藏线程依赖宿主内部契约**(app.asar 0.2.1-rc.3 实测):宿主半 `lib/index.js` 用 `ctx.inject(["webServer"])` 注册 `POST /ui-quote/api/sidechat.start`,在宿主服务就绪后从父会话 `snapshotEvents()` 复制种子,再用 `ctx.get("agents").create({ meta: { parentSession, isSeeded: true, origin: "subagent", delegationDepth, agentPreset }, seed, inheritedEventCount, agentOptions, setup })` 建子会话;descriptor 为 `{version: 3, mode: "continuable", provider: "ui-quote", label}`,标题 `Side: <引用首行>`。`webServer` / `connection.requestRejection` / `agents` 缺失时探针失败,客户端退回 fork;`agents.create` 失败只影响这一条线程。
444
+ - **内置隐藏线程依赖宿主内部契约**(app.asar 0.2.1-rc.3 实测):宿主半 `lib/index.js` 用 `ctx.inject(["webServer"])` 注册 `POST /ui-quote/api/sidechat.start`,在宿主服务就绪后从父会话 `snapshotEvents()` 取原话合成一条隐性上下文(不复制日志),再用 `ctx.get("agents").create({ meta: { parentSession, isSeeded: true, origin: "subagent", delegationDepth, agentPreset }, seed, inheritedEventCount, agentOptions, setup })` 建子会话;descriptor 为 `{version: 3, mode: "continuable", provider: "ui-quote", label}`,标题 `Side: <引用首行>`。`webServer` / `connection.requestRejection` / `agents` 缺失时探针失败,客户端退回 fork;`agents.create` 失败只影响这一条线程。
445
445
  **种子不复制父会话日志,只带一条隐性上下文**:`seed` 固定七个事件——`session/end-seed { inherited: true }`(切点在下标 0)、一个**已收口的小回合**(`turn/start` → `step/start` → 上下文消息 → `step/end` → `turn/end{done}`)、以及 `subagent/descriptor`。那个回合不是摆设:宿主把「一个 turn 都没有」的会话算成 `blank`,`blank` 会话渲染的是欢迎页(探索未至之境)而不是聊天页——没有输入框,引用草稿无处可落,0.12.3-lab.29 就踩了这个坑;有回合才会进正常聊天态。那条消息里是主会话最近若干轮的**纯文本**(用户/助手、上限 8000 字,工具调用与注入上下文都跳过),消息里带一句「只作背景、别继续执行」的指令——所以模型仍有上下文,而侧聊界面只把它折叠成一行「上下文」,不会把整段主会话铺出来,也不再为复制整条日志付一遍 token。宿主读子会话身份时走 `subagent` 投影(最后一次 descriptor 生效),并在 `api-session-controller` 里要求 `identity.seq >= inheritedEventCount`,否则整个子会话的历史都打不开(`subagent/catalog-diagnostic` · "subagent descriptor is unavailable")——descriptor 必须排在切点之后,0.12.3-lab.26 就是栽在这一步。
446
- **旧线程靠形状版本戳识别,不靠 `sessions.retain`**:retain 对这类坏线程**照样成功**(只有历史 RPC 会拒),所以客户端给新建的线程在 `dsh-client-ui-quote:v1:sidechat-target` 里盖 `v: 2`,只复用自己盖过章的线程;better-sidebar 的、旧版本的、清单里扫描发现的线程一律丢弃重建——点一次就自愈,代价是最多重建一条线程。隐藏线程是 `continuable` 子会话,**父会话在线时可继续**;父会话关闭后宿主内置 `subagentchat` 页按自己的规则只读,重新打开父会话即可继续。宿主升级改了 `agents.create` / 种子格式时,线程创建会失败并自动退回 `sessions.fork`,不会白屏或报错。
446
+ **旧线程靠形状版本戳识别,不靠 `sessions.retain`**:retain 对这类坏线程**照样成功**(只有历史 RPC 会拒),所以客户端给新建的线程在 `dsh-client-ui-quote:v1:sidechat-target` 里盖当前形状版本(现在是 `v: 4`:收口回合 + 隐性上下文),只复用自己盖过章的线程;better-sidebar 的、旧版本的、清单里扫描发现的线程一律丢弃重建——点一次就自愈,代价是最多重建一条线程。隐藏线程是 `continuable` 子会话,**父会话在线时可继续**;父会话关闭后宿主内置 `subagentchat` 页按自己的规则只读,重新打开父会话即可继续。宿主升级改了 `agents.create` / 种子格式时,线程创建会失败并自动退回 `sessions.fork`,不会白屏或报错。
447
447
  想优先用 dsh-better-sidebar(实测 0.28.0)的侧聊 UI,把 `sideChatBackend` 设成 `"better"`;它仍走自己的 `POST /sidebar/api/sidechat.start`,父会话未运行时会收到 409。两边共享 `Side: ` 标题约定,装不装都不会互相抢自动打开的焦点。
448
448
  - **「改这条引用」是追加,不是原地编辑**:DSH 的 transcript 只追加(侦察确认全树没有 `editMessage` / `updateMessage` / `rewriteMessage` / 回收 API,只有**尚未进入会话**的队列条目可改)。所以修订发出一条新引用(评论、引用正文都可改),旧胶囊只挂 `已修订` 标记——标记存在**本机** `localStorage`(`dsh-client-ui-quote:v1:quote-revised`,按 **`ref + 引用原文`** 归一键、上限 60 条),同一句话从两个不同位置引用时互不影响;没有 ref 的旧引用仍按原文键读写。它不会跨标签页、跨设备同步,也不进入模型上下文;模型看到的是那条改后的新引用。正文改过后键仍是旧原文:`已修订` 挂在**旧**胶囊上(它的卡片里显示改后的原文与评论),改后的新胶囊不挂标。
449
449
  - **代码引用的识别靠首行围栏**:块首是 `> ❝ ``` `、块尾有配对的收尾围栏时,这块按代码解析(围栏剥掉、胶囊等宽)。因此一条**普通**引用如果正文恰好以 ``` 或 `~~~` 开头(比如在 Markdown 文件预览里引用了源码),只要它没有配对的收尾围栏,就仍然按正文读回——首行不会被当成围栏吃掉,引用与清单里都是原文。写成代码引用时收尾围栏始终成对,所以正文自带 ``` 的代码引用改用 `~~~` 也不会与这条规则打架。
@@ -481,7 +481,7 @@ Markdown 的标题/编号/块引用/评论行/时间戳与空列表兜底、超
481
481
  - **Markdown 预览的行号**:要精修就得读源文件,而客户端必须声明 `remote` 才能拿到它;声明错会让 client bundle 在不匹配的宿主上**静默失效**(`dsh-quote-annotate` 上游就是这么死的)。所以先不声明——文件仍然打开,只是落在顶部而不是行上。
482
482
  - **表格/列表/LaTeX 的结构保真**:链接与行内代码已做;表格要还原成 Markdown 表格,列表要还原成缩进,成本明显更高,留到有人真的需要时再做。
483
483
  - **图片/截图引用**:明确不做(理由见「本插件不做的」)。真要做请独立成插件:图片引用要的是另一套胶囊/卡片/附件语义。
484
- - **真机验证**:这一轮的宿主契约全部来自 `app.asar` 的一手文档,功能路径有 662 项离线断言;隐藏线程的 `agents.create` / 种子格式已在**隔离的 DSH web 实例**(临时 `DSH_HOME`,端口 8791,CLI 0.2.1-rc.3)里确认:宿主半通过 `ctx.inject(["webServer"])` 晚到注册,`POST /ui-quote/api/sidechat.start` 的探针返回 `{"ok":true,"value":{"available":true,"version":1}}`,说明 `agents` 服务与同源围栏都就绪。本轮又在**运行中的桌面端**(link 模式,重启后加载 `lib/index.js`)核对过一次:`/ui-quote/api/*` 下任意路径都先被围栏拦成 `403 {"code":"forbidden"}`,而同样方法打一个不存在的顶层路径是 `404`,说明前缀路由确实挂上了、并且先过围栏。原生侧聊页签的 `SessionProvider` / `conversation.content` 嵌入契约还需要在真实窗口里重开一次侧边对话确认(离线桩只覆盖注册、地址、标题与 retain/release,不渲染宿主组件)。真浏览器(1600×2000 视口)同一套断言本轮复核 **51/51**。欢迎页的交互点击被安全闸门拦下,因此下面五条仍需在真实窗口逐项点:聚合条编辑、回合尾的两个按钮、打开文件与行范围标记、另开侧边对话、JSON 导出。
484
+ - **真机验证**:这一轮的宿主契约全部来自 `app.asar` 的一手文档,功能路径有 667 项离线断言;隐藏线程的 `agents.create` / 种子格式已在**隔离的 DSH web 实例**(临时 `DSH_HOME`,端口 8791,CLI 0.2.1-rc.3)里确认:宿主半通过 `ctx.inject(["webServer"])` 晚到注册,`POST /ui-quote/api/sidechat.start` 的探针返回 `{"ok":true,"value":{"available":true,"version":1}}`,说明 `agents` 服务与同源围栏都就绪。本轮又在**运行中的桌面端**(link 模式,重启后加载 `lib/index.js`)核对过一次:`/ui-quote/api/*` 下任意路径都先被围栏拦成 `403 {"code":"forbidden"}`,而同样方法打一个不存在的顶层路径是 `404`,说明前缀路由确实挂上了、并且先过围栏。原生侧聊页签的 `SessionProvider` / `conversation.content` 嵌入契约还需要在真实窗口里重开一次侧边对话确认(离线桩只覆盖注册、地址、标题与 retain/release,不渲染宿主组件)。真浏览器(1600×2000 视口)同一套断言本轮复核 **51/51**。欢迎页的交互点击被安全闸门拦下,因此下面四条仍需在真实窗口逐项点:聚合条编辑、回合尾的两个按钮、打开文件与行范围标记、JSON 导出。
485
485
  - **市场收录与首屏**:README 已有截图、徽章、「30 秒上手」与 24 秒静态轮播 GIF;还差把插件提交到 dsh.market / dsh-plugin.org / dsh.pub / awesome-deepseek-harness —— 这些需要外部账号,是人工动作。
486
486
 
487
487
  ## 变更记录
package/lib/client.js CHANGED
@@ -2630,7 +2630,9 @@ box-shadow:0 1px 3px rgba(0,0,0,.25)}
2630
2630
  quote: info.quote,
2631
2631
  comment: info.comment,
2632
2632
  code: info.code === true,
2633
- fence: typeof info.path === "string" ? info.path : "",
2633
+ // The info string is the file when there is one, otherwise the
2634
+ // chat code block's language — the same slot the wire uses.
2635
+ fence: typeof info.path === "string" && info.path !== "" ? info.path : typeof info.lang === "string" ? info.lang : "",
2634
2636
  time: "",
2635
2637
  ref: info.ref
2636
2638
  })
@@ -6071,7 +6073,12 @@ box-shadow:0 1px 3px rgba(0,0,0,.25)}
6071
6073
  /** In-flight thread creations per session (a second click folds into the first). */
6072
6074
  const sideChatPending = new Map();
6073
6075
 
6074
- /** Hidden threads already checked this page: thread id → the host can open it. */
6076
+ /**
6077
+ * Hidden threads already checked this page: thread id → the host can open
6078
+ * it. Deliberately never invalidated: a thread that breaks *after* the
6079
+ * first check still reads as healthy until the next reload, because
6080
+ * probing on every click costs an RPC and can flap on a slow parent.
6081
+ */
6075
6082
  const hiddenThreadHealth = new Map();
6076
6083
 
6077
6084
  /** One stored "session → side thread" map, best effort (a denied store is fine). */
@@ -6545,15 +6552,32 @@ box-shadow:0 1px 3px rgba(0,0,0,.25)}
6545
6552
  const code = state.code;
6546
6553
  const path = state.path;
6547
6554
  const lang = state.lang;
6548
- // The chip's own metadata: the file a preview/diff quote came from and
6549
- // the reader's origin. Without it the side thread's chip would render
6550
- // as a bare quote with no provenance.
6555
+ // The chip's own metadata: the file a preview/diff quote came from,
6556
+ // the reader's origin, and the wire coordinate (same shape `commit`
6557
+ // writes) so the side thread's chip still says where the quote came
6558
+ // from once it is sent there.
6559
+ const saved = state.source === null || state.source === undefined ? null : saveSource(plainOf(text), sessionId, state.source);
6551
6560
  const meta = {
6552
6561
  path,
6553
6562
  lang,
6554
6563
  origin: state.origin,
6555
- ...(state.ref === undefined || state.ref === null ? {} : { ref: state.ref })
6564
+ ...(saved === null
6565
+ ? {}
6566
+ : {
6567
+ ref: {
6568
+ v: 1,
6569
+ s: saved.sessionId,
6570
+ a: saved.attr,
6571
+ k: saved.key,
6572
+ t: saved.turn,
6573
+ n: saved.n,
6574
+ ...ROLE_CODES.has(saved.role) ? { r: saved.role } : {}
6575
+ }
6576
+ })
6556
6577
  };
6578
+ // `mode: "new"` stays a supported internal capability even though the
6579
+ // floating bar no longer offers a second entry for it; the service face
6580
+ // is still tested with it.
6557
6581
  const next = mode === "new" ? "new" : "reuse";
6558
6582
  const quoteKey = sourceKeyOf(text);
6559
6583
  close();
@@ -6954,8 +6978,12 @@ border-top:1px solid color-mix(in srgb,currentColor 18%,transparent)}
6954
6978
  if (row === null || row === undefined) return "";
6955
6979
  const lines = [];
6956
6980
  if (row.code === true) {
6981
+ // The fence must not collide with the body: a quote that itself
6982
+ // contains ``` (written with ~~~ on the wire) would end the block
6983
+ // early. Pick the other one, exactly like the wire writer does.
6984
+ const fence = String(row.quote).split("\n").some((line) => /^\s*```/u.test(line)) ? "~~~" : "```";
6957
6985
  const info = typeof row.fence === "string" && row.fence !== "" ? " " + row.fence : "";
6958
- lines.push("```" + info, String(row.quote), "```");
6986
+ lines.push(fence + info, String(row.quote), fence);
6959
6987
  } else {
6960
6988
  for (const line of String(row.quote).split("\n")) lines.push("> " + line);
6961
6989
  }
@@ -6986,9 +7014,11 @@ border-top:1px solid color-mix(in srgb,currentColor 18%,transparent)}
6986
7014
  lines.push("## " + (index + 1) + ". " + firstLineOf(row.quote, 80) + (marks.length === 0 ? "" : " " + marks.join(" ")), "");
6987
7015
  // A code quote exports as a real fenced block, not a blockquote; a
6988
7016
  // preview quote's file rides in the info string, as it did on the wire.
7017
+ // The fence flips to ~~~ when the body itself contains ```.
6989
7018
  if (row.code === true) {
7019
+ const fence = String(row.quote).split("\n").some((line) => /^\s*```/u.test(line)) ? "~~~" : "```";
6990
7020
  const info = typeof row.fence === "string" && row.fence !== "" ? " " + row.fence : "";
6991
- lines.push("```" + info, String(row.quote), "```", "");
7021
+ lines.push(fence + info, String(row.quote), fence, "");
6992
7022
  } else {
6993
7023
  for (const line of String(row.quote).split("\n")) lines.push("> " + line);
6994
7024
  lines.push("");
@@ -7974,11 +8004,18 @@ border-top:1px solid color-mix(in srgb,currentColor 18%,transparent)}
7974
8004
  found.push(summary);
7975
8005
  }
7976
8006
  found.sort((left, right) => (Number(right.updatedAt) || 0) - (Number(left.updatedAt) || 0));
8007
+ // Threads we already bind to this session are ours: stamp the current
8008
+ // shape version so a scan never demotes one into "replace on next
8009
+ // click". A thread only the scan knows about keeps no stamp (it may be
8010
+ // better-sidebar's, whose shape this build cannot vouch for).
8011
+ const bound = sideThreadsOf(sessionId);
8012
+ const mine = new Set(Object.values(bound.quotes));
8013
+ if (typeof bound.default === "string" && bound.default !== "") mine.add(bound.default);
7977
8014
  for (const summary of found) {
7978
8015
  const id = summary.id;
7979
8016
  if (typeof id !== "string" || id === "") continue;
7980
8017
  if (ids.includes(id) === false) ids.push(id);
7981
- rememberThreadTarget(id, { kind: "hidden", parentId: sessionId });
8018
+ rememberThreadTarget(id, { kind: "hidden", parentId: sessionId, ...(mine.has(id) ? { v: HIDDEN_THREAD_VERSION } : {}) });
7982
8019
  }
7983
8020
  return ids;
7984
8021
  }
@@ -8543,31 +8580,22 @@ border-top:1px solid color-mix(in srgb,currentColor 18%,transparent)}
8543
8580
  nativeSideChatRequest,
8544
8581
  nativeSideChatReady: () => nativeSideChatReady,
8545
8582
  hiddenSideChatRequest,
8546
- probeBuiltinSideChat,
8547
- startBuiltinSideThread,
8548
8583
  builtinSideChatState: () => builtinSideChatState,
8549
8584
  setBuiltinSideChatState: (state) => {
8550
8585
  builtinSideChatState = state;
8551
8586
  builtinSideChatProbe = null;
8552
8587
  builtinSideChatProbeAt = 0;
8553
8588
  },
8554
- openBuiltinThreadTab,
8555
- openThreadTarget,
8556
- cleanSideThreadTitle,
8557
8589
  rememberThreadTarget,
8558
8590
  targetOfThread,
8559
8591
  mintedHiddenThread,
8560
8592
  HIDDEN_THREAD_VERSION,
8561
8593
  threadRetainTarget,
8562
8594
  readThreadTargets,
8563
- knownSideThreadIds,
8564
8595
  defaultThreadForSession,
8565
8596
  nativeThreadAddress,
8566
8597
  parseNativeThreadAddress,
8567
- builtinThreadAddress,
8568
- nativeThreadTitle,
8569
8598
  installNativeSideChatPage,
8570
- SideChatTab,
8571
8599
  failureSnapshot,
8572
8600
  openNativeThreadTab,
8573
8601
  openSideChat,
package/lib/index.js CHANGED
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * The plugin's UI lives in lib/client.js. This half contributes exactly one
5
5
  * capability: a hidden side thread. A side thread has to be a child Session
6
- * seeded from the parent's log and marked `origin: "subagent"`, because that
7
- * is the only durable shape DSH's session list leaves out. The public session
8
- * RPCs accept neither a seed nor an origin, so the client cannot create one;
9
- * this route can, through the host's own AgentRegistry.
6
+ * marked `origin: "subagent"`, because that is the only durable shape DSH's
7
+ * session list leaves out. Its seed is NOT a copy of the parent's log: it is
8
+ * one synthesized context message (the parent's own words, capped) inside a
9
+ * closed turn, plus the subagent descriptor. The public session RPCs accept
10
+ * neither a seed nor an origin, so the client cannot create one; this route
11
+ * can, through the host's own AgentRegistry.
10
12
  *
11
13
  * Everything is guarded: a host without the web server, connection fence,
12
14
  * agents or session-title service registers no route (or answers 503), and
@@ -23,7 +25,7 @@ const START_METHOD = "sidechat.start";
23
25
  const THREAD_LABEL_PREFIX = "Side: ";
24
26
  /** Same cap better-sidebar uses for its own side-thread labels. */
25
27
  const LABEL_MAX_CHARS = 48;
26
- /** The subagent descriptor version this DSH reads (app.asar 0.2.1-rc.2). */
28
+ /** The subagent descriptor version this DSH reads (app.asar 0.2.1-rc.3). */
27
29
  const DESCRIPTOR_VERSION = 3;
28
30
  /** Producer identity in the descriptor; distinct from better-sidebar's. */
29
31
  const PROVIDER = "ui-quote";
@@ -141,6 +143,7 @@ function messageTextOf(event) {
141
143
  * @returns {string} one `用户:…` / `助手:…` line per message.
142
144
  */
143
145
  export function transcriptOf(events, maxChars = CONTEXT_MAX_CHARS) {
146
+ const budget = Number.isFinite(maxChars) && maxChars > 0 ? maxChars : CONTEXT_MAX_CHARS;
144
147
  const lines = [];
145
148
  let total = 0;
146
149
  for (let index = (Array.isArray(events) ? events : []).length - 1; index >= 0; index -= 1) {
@@ -150,7 +153,13 @@ export function transcriptOf(events, maxChars = CONTEXT_MAX_CHARS) {
150
153
  const text = messageTextOf(event).replace(/\s+/gu, " ").trim();
151
154
  if (text === "") continue;
152
155
  const line = (event.type === "user/message" ? "用户:" : "助手:") + text;
153
- if (total + line.length > maxChars) break;
156
+ if (total + line.length > budget) {
157
+ // One message longer than the whole budget is truncated, not dropped:
158
+ // a long paste as the parent's last turn is exactly the context the
159
+ // reader wants to ask about.
160
+ if (lines.length === 0) lines.push(line.slice(0, Math.max(0, budget - 1)) + "…");
161
+ break;
162
+ }
154
163
  lines.push(line);
155
164
  total += line.length + 1;
156
165
  }
@@ -296,7 +305,6 @@ export async function startSideChat(ctx, payload, handles = new Map()) {
296
305
  const routeEffort = selection?.reasoningEffort ?? parent.options?.reasoningEffort;
297
306
  const { agentPreset, setup } = await composeSetup(ctx, presetOf(parent.session.header, events));
298
307
  const { seed, inheritedEventCount } = buildThreadSeed(events, {
299
- cwd,
300
308
  label,
301
309
  agentProvider: routeProvider,
302
310
  agentModel: routeModel,
@@ -330,6 +338,11 @@ export async function startSideChat(ctx, payload, handles = new Map()) {
330
338
  } catch (error) {
331
339
  throw new SidechatError("sidechat-error", "thread creation failed: " + (error instanceof Error ? error.message : String(error)), 500);
332
340
  }
341
+ // One disposer per created child. A thread the user later replaces (a shape
342
+ // version bump, a fallback) keeps its disposer here until the plugin
343
+ // unloads: that holds the agent handle, not a session on disk. Releasing on
344
+ // replacement would need the client to say which thread it abandoned, and a
345
+ // wrong answer would kill a thread the user is still reading.
333
346
  if (handle !== null && handle !== undefined && typeof handle.dispose === "function") handles.set(childId, handle.dispose);
334
347
  try {
335
348
  handle?.agent?.inbox?.clear?.();
@@ -455,9 +468,13 @@ export async function handleSideChatRequest(ctx, req, res, handles) {
455
468
  export function apply(ctx) {
456
469
  if (ctx === null || ctx === undefined || typeof ctx.effect !== "function") return;
457
470
  const handles = new Map();
471
+ /** The route is installed at most once, even if a service is re-injected. */
472
+ let installed = false;
458
473
  const install = (hostCtx) => {
474
+ if (installed) return;
459
475
  const webServer = serviceOf(hostCtx, "webServer");
460
476
  if (webServer === null || webServer === undefined || typeof webServer.register !== "function") return;
477
+ installed = true;
461
478
  hostCtx.effect(() => {
462
479
  let disposeRoute = () => {};
463
480
  try {
package/locale/en.json CHANGED
@@ -2,6 +2,6 @@
2
2
  "sourceLost": "The source has changed",
3
3
  "meta": {
4
4
  "title": "Quote selection (dsh-client-ui-quote)",
5
- "description": "Select any sentence in the chat, a file preview or the terminal to quote it. Kimi Code is the starting point, but this version treats a quote as an object you can manage, review and carry back: the floating bar offers Comment, Add to conversation and Side chat, the first two insert a DSH reference chip into the composer, and the third writes the quote into the composer of one independent side thread per conversation in the right sidebar without sending it (a native plugin page by default; when the host lacks the required seams it falls back to the built-in subagentchat page, and setting sideChatBackend to better switches to dsh-better-sidebar's hidden thread). Alt+Q / Alt+C / Alt+B do the same three things, hovering a sent quote can jump back to its source, and the right sidebar's Quote list page searches every quote of the session, jumps back to any of them and exports them as Markdown (Alt+L), its footer takes a side thread's conclusion back into the main composer or exports it as a file, and a hover card action revises a sent quote (the old capsule is then marked as superseded). The quoted sentence keeps a highlight and ordinal in its source message, a 引用 ×N bar above the composer lists the draft's quotes and removes them one by one, and the sent ref line names the turn and role it came from in readable words. Diff and per-turn change panels are quotable too, and the quote list inserts a multi-selected set in one action."
5
+ "description": "Select any sentence in the chat, a file preview or the terminal to quote it. Kimi Code is the starting point, but this version treats a quote as an object you can manage, review and carry back: the floating bar offers Comment, Add to conversation and Side chat, the first two insert a DSH reference chip into the composer, and the third writes the quote into the composer of one independent side thread per conversation in the right sidebar without sending it (a hidden thread created by the plugin's host half by default, seeded with one implicit context message instead of the whole history; when the host half is unavailable it falls back to the plugin's own native page, and setting sideChatBackend to better switches to dsh-better-sidebar's hidden thread). Alt+Q / Alt+C / Alt+B do the same three things, hovering a sent quote can jump back to its source, and the right sidebar's Quote list page searches every quote of the session, jumps back to any of them and exports them as Markdown (Alt+L), its footer takes a side thread's conclusion back into the main composer or exports it as a file, and a hover card action revises a sent quote (the old capsule is then marked as superseded). The quoted sentence keeps a highlight and ordinal in its source message, a 引用 ×N bar above the composer lists the draft's quotes and removes them one by one, and the sent ref line names the turn and role it came from in readable words. Diff and per-turn change panels are quotable too, and the quote list inserts a multi-selected set in one action."
6
6
  }
7
7
  }
package/locale/zh.json CHANGED
@@ -2,6 +2,6 @@
2
2
  "sourceLost": "原文已变",
3
3
  "meta": {
4
4
  "title": "选中引用 (dsh-client-ui-quote)",
5
- "description": "在聊天、文件预览或终端里选中任意一句话即可引用并评论。Kimi Code 是最初的交互参照,但这一版把引用当成可管理、可审阅、可回流的对象:浮层提供「评论」「添加到对话」与「侧边对话」,前两者以 DSH 原生 reference-chip 插入输入框,第三项把引用写进右侧栏那条独立侧边线程的输入框(一条主会话一条线程,不替你发送;默认是插件自己的原生页签,宿主缺少所需接口时才退回内置 subagentchat 页,也可以在设置 sideChatBackend 为 better 后改用 dsh-better-sidebar 的隐藏线程)。Alt+Q / Alt+C / Alt+B 是同样的三个动作,悬停发送后的胶囊可「跳回原文」,右侧栏的「引用清单」页还能搜索本会话全部引用、跳回原文并导出 Markdown(Alt+L 打开),页面底部可把侧边线程的结论一键带回主会话输入框或导出成文件;悬停卡片里的「改这条引用」能把旧评论放回评论框改好再发(旧胶囊随即标「已修订」)。引用落进草稿后,原句在来源消息里留下高亮与序号,输入框上方出现「引用 ×N」聚合条(可逐条移除);发送块的坐标行前面还带一段可读的「引自第 N 轮 · 助手」。diff / 每轮改动面板也能划选为带路径行号的代码引用;引用清单可勾选多条,一次插入输入框;它也是审阅工作台——每条引用可标「待处理 / 已解决」、打 #标签、按状态或标签筛选,勾选后可批量标记解决或批量送回输入框。"
5
+ "description": "在聊天、文件预览或终端里选中任意一句话即可引用并评论。Kimi Code 是最初的交互参照,但这一版把引用当成可管理、可审阅、可回流的对象:浮层提供「评论」「添加到对话」与「侧边对话」,前两者以 DSH 原生 reference-chip 插入输入框,第三项把引用写进右侧栏那条独立侧边线程的输入框(一条主会话一条线程,不替你发送;默认是宿主半创建的内置隐藏线程(主会话最近几轮原话作为一条隐性上下文注入,不铺开历史),宿主半不可用时退回本插件的原生页签,也可以在设置 sideChatBackend 为 better 后改用 dsh-better-sidebar 的隐藏线程)。Alt+Q / Alt+C / Alt+B 是同样的三个动作,悬停发送后的胶囊可「跳回原文」,右侧栏的「引用清单」页还能搜索本会话全部引用、跳回原文并导出 Markdown(Alt+L 打开),页面底部可把侧边线程的结论一键带回主会话输入框或导出成文件;悬停卡片里的「改这条引用」能把旧评论放回评论框改好再发(旧胶囊随即标「已修订」)。引用落进草稿后,原句在来源消息里留下高亮与序号,输入框上方出现「引用 ×N」聚合条(可逐条移除);发送块的坐标行前面还带一段可读的「引自第 N 轮 · 助手」。diff / 每轮改动面板也能划选为带路径行号的代码引用;引用清单可勾选多条,一次插入输入框;它也是审阅工作台——每条引用可标「待处理 / 已解决」、打 #标签、按状态或标签筛选,勾选后可批量标记解决或批量送回输入框。"
6
6
  }
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-client-ui-quote",
3
- "version": "0.12.3-lab.36",
3
+ "version": "0.12.3",
4
4
  "description": "Turn any DSH selection into a reviewable quote — comment / add to chat / side chat, source highlights and jump-back, a searchable review list, side threads, revisions, and per-quote reply badges.",
5
5
  "icon": "icon.svg",
6
6
  "license": "MIT",
package/test/capsule.mjs CHANGED
@@ -1188,6 +1188,16 @@ try {
1188
1188
  list.ok === true && list.threads.some((entry) => entry.threadId === "session-hidden-scan"),
1189
1189
  JSON.stringify(list.threads.map((entry) => entry.threadId))
1190
1190
  );
1191
+ check(
1192
+ "the page's default thread is the remembered one",
1193
+ testing.defaultThreadForSession("session-hidden-main") === "session-hidden-1",
1194
+ JSON.stringify(testing.defaultThreadForSession("session-hidden-main"))
1195
+ );
1196
+ check(
1197
+ "a session with no stored thread has no default",
1198
+ testing.defaultThreadForSession("session-hidden-nothing") === null,
1199
+ JSON.stringify(testing.defaultThreadForSession("session-hidden-nothing"))
1200
+ );
1191
1201
  check(
1192
1202
  "a scanned thread remembers its parent",
1193
1203
  JSON.stringify(testing.targetOfThread("session-hidden-scan")) === JSON.stringify({ kind: "hidden", parentId: "session-hidden-main" }),
package/test/host.mjs CHANGED
@@ -2,9 +2,9 @@
2
2
  * Host-half harness: the one capability lib/index.js contributes.
3
3
  *
4
4
  * The client bundle cannot create a hidden side thread — the public session
5
- * RPCs take neither a seed nor an origin. The host half builds the seed from
6
- * the parent's log and asks the AgentRegistry for a child marked
7
- * `origin: "subagent"`. Everything here is offline: the registry, the preset
5
+ * RPCs take neither a seed nor an origin. The host half synthesizes one context
6
+ * message from the parent's own words, asks the AgentRegistry for a child
7
+ * marked `origin: "subagent"`, and marks the inherited cut before it. Everything here is offline: the registry, the preset
8
8
  * service, the title service and the HTTP request/response are stubs, so the
9
9
  * exact options the host would receive are asserted byte for byte.
10
10
  *
@@ -131,6 +131,39 @@ check(
131
131
  })(),
132
132
  "bounded"
133
133
  );
134
+ check(
135
+ "one message longer than the whole budget is truncated, not dropped",
136
+ (() => {
137
+ const huge = [
138
+ {
139
+ type: "user/message",
140
+ seq: 0,
141
+ time: 0,
142
+ data: { id: "m-long", role: "user", source: { kind: "user" }, content: [{ type: "text", text: "长".repeat(500) }] },
143
+ surfaceOp: "append"
144
+ }
145
+ ];
146
+ const text = transcriptOf(huge, 120);
147
+ return text.startsWith("用户:") && text.length <= 120 && text.endsWith("…");
148
+ })(),
149
+ "truncated"
150
+ );
151
+ check(
152
+ "a missing or nonsensical budget falls back to the default cap",
153
+ (() => {
154
+ const one = [
155
+ {
156
+ type: "user/message",
157
+ seq: 0,
158
+ time: 0,
159
+ data: { id: "m-one", role: "user", source: { kind: "user" }, content: [{ type: "text", text: "一句话" }] },
160
+ surfaceOp: "append"
161
+ }
162
+ ];
163
+ return transcriptOf(one, Number.NaN) === "用户:一句话" && transcriptOf(one, 0) === "用户:一句话" && transcriptOf(one, undefined) === "用户:一句话";
164
+ })(),
165
+ "guarded"
166
+ );
134
167
  check(
135
168
  "an empty parent still yields a usable context",
136
169
  (() => {
package/test/panel.mjs CHANGED
@@ -514,6 +514,12 @@ check(
514
514
  "``` src/app.ts#L12-L48\nSELECT 1;\n```",
515
515
  JSON.stringify(testing.quoteRowMarkdown({ quote: "SELECT 1;", comment: "", time: "", code: true, fence: "src/app.ts#L12-L48" }))
516
516
  );
517
+ check(
518
+ "a code quote that itself contains a fence copies with ~~~ instead",
519
+ testing.quoteRowMarkdown({ quote: "```\nSELECT 1;\n```", comment: "", time: "", code: true, fence: "" }) ===
520
+ "~~~\n```\nSELECT 1;\n```\n~~~",
521
+ JSON.stringify(testing.quoteRowMarkdown({ quote: "```\nSELECT 1;\n```", comment: "", time: "", code: true, fence: "" }))
522
+ );
517
523
  check(
518
524
  "a quote with no coordinates still copies as a plain blockquote",
519
525
  testing.quoteRowMarkdown({ quote: "只有原文", comment: "", time: "", code: false, fence: "" }) === "> 只有原文",