Skip to main content
H

dsh-conversation

huangfuren/dsh-conversation

Conversation outline panel for the DSH web surface: user questions as root nodes (index, time, tool count and text volume per turn) with the Markdown headings of assistant replies hanging beneath them, plus level slider, search scope, bookmarks, copy and export as Markdown, section jump, keyboard navigation, a right-edge density rail and per-session reading-position memory. Read-only UI: it never touches prompts, tool results or provider traffic, and writes nothing outside its own localStorage keys.

Install

dsh plugin --profile web add github:huangfuren/dsh-conversation

README

dsh-conversation

中文 | English

Web 端「会话大纲」插件:在页面右上角贡献一个浮动面板,把当前会话组织成一棵大纲树 —— 用户提问是根节点(带序号、提问时间、该轮工具数与正文量),助手回复里的 Markdown 标题按其层级挂到最近的提问下。点击任意一行跳到正文并短暂高亮。纯 UI 插件,不接触任何提示词、消息、工具结果,也不向提供方发起请求。

Project status: 1.x。已在 dsh 0.1.5-rc.2 + web profile 上验证,并适配 dsh 0.2.x(>=0.2.0-rc.1 <1.0.0);更早的 dsh 版本与其它 profile 未认证 —— 插件遇到不认识的宿主 API 会降级为"面板不出现"而不是抛错,但那条路径不在测试矩阵内。

版本脉络:1.1.0 合并了原 dsh-outline 插件的全部能力并把包名从 dsh-client-hqst 改为 dsh-conversation;1.2.0 起补齐了大量导航能力(三档宽度 / 侧边刻度条 / 搜索范围 / 导出 / 节级跳转 / 快捷键 / 阅读位置记忆),其中侧边刻度条已于 1.3.1 停用,1.5.5 起其代码就地注释保留并逐段标注原因(无行为变更)。

效果

  • 常驻态(默认):浮动面板(dsh-conv-panel),标题「历史提问 · 大纲」。
    • 三档宽度:紧凑 320px → 加宽 480px → 全屏 720px 整高,标题栏按钮循环切换; 换档时按面板位置决定朝哪边生长 —— 贴左向右长、贴右向左长、拖到中间则两侧对称,居中时若某一侧 空间不够,差额全部转给另一侧;贴边时靠墙那条边始终不动,所以加宽不会顶出窗口、缩窄会退回原位置 (边距 ≤ 32px 视为贴边,含默认停靠的 16px);
    • 左右停靠:标题栏的停靠按钮在靠左 / 靠右之间切换,切换后回到默认位置;
    • 标题栏是拖拽把手(4px 阈值,拖动后位置持久化);
    • 工具行:展开/收起全部、只看收藏、复制大纲(Markdown 进剪贴板)、导出为 .md 文件、搜索框(命中片段高亮);
    • 搜索范围:点击搜索框左侧的「全部 / 问题 / 标题」按钮循环切换,只匹配提问或只匹配标题;
    • 层级滑块:0 = 仅显示提问,1~6 = 展开到对应标题层级,圆点 tooltip 显示各层级数量;
    • 背景不透明度:面板底部滑杆可在 40%~100% 之间调面板背景的不透明度(默认 100%,与旧版观感一致)。只降底色、不降文字;背景模糊由它反向派生 —— 越透明,背后的字越透得出来、模糊半径也越大(40% 时约 3.6px,100% 时为 0),这是设计使然而非渲染故障。点右侧百分比回到完全不透明,双击滑杆同样可复位。用于面板挡住正文时把内容透出来;
    • 列表行:折叠箭头 / Q1 序号徽标 / 文本 / 提问时间 / 该轮「N 工具」「N 字」统计 / 收藏星;当前阅读位置的行自动浅底高亮并滚入可视区;
    • 底部:回到顶部 / 回到底部。
  • 收起态:面板收起后,唯一的入口是会话标题栏上的「大纲」按钮(order: 100,位于原生按钮旁)。 1.3.1 起停用了原本的右侧刻度条(dsh-conv-rail)与右上角胶囊(dsh-conv-fab)两个入口 —— 它们与标题栏按钮重复,且刻度条会压住自带同类导航的插件。1.5.5 起这段代码已就地注释保留: 每段都写明失效原因(渲染点在哪个提交移除)、保留原因、恢复方式与停用日期, 在 client.js 里搜「停用保留」即可定位;既不删除、也不参与构建,将来要重启时去掉注释标记即可。
  • 跳转粒度:单击任意行 → 跳到该节点;双击任意行 → 跳到"本节末尾"(下一个同级或更高级节点,已是最后一节则滚到底部)。
  • 键盘:面板聚焦时 Alt+↓ / Alt+↑ 跳到下一个 / 上一个节点(只在面板内监听,不抢宿主按键)。
  • 阅读位置记忆:每个会话记录上次看到的大纲节点,切回来时面板直接落在那里。
  • 空态:会话里还没有提问/标题时显示「暂无大纲内容」。
  • 首屏占位:刷新后宿主会话/chat 数据尚未就绪时,先在面板位置显示占位「正在读取会话…」,会话绑定就绪后才挂载面板;若绑定先到、chat 快照稍后写入,面板可能先渲染一次空态,下一次快照更新即补上;宿主超过 2 秒仍未给出会话绑定就照常挂载并展示空态。
  • 标题栏按钮自适应(icon-only):右栏展开、标题栏变窄时,标题栏里的「大纲」按钮自动收成纯图标(与宿主原生按钮同机制:同一容器查询 @container (width<=540px)、同一容器 .wSkVaW_titleRow),按钮真实收窄且 aria-label / title 保留、可点击、可键盘聚焦;右栏收起后自动还原。旧版宿主无该容器 ⇒ 保持宽态(降级安全)。
  • 状态持久化:pinned / 拖拽位置 / 视图档位 / 面板背景不透明度 / 默认展开层级 / 每会话收藏 / 每会话阅读位置,全部走 localStorage(键前缀 dsh-conversation:);1.3.1 停用的刻度条开关字段自 1.5.5 起不再读取(写入逻辑已注释掉),旧 localStorage 里的该键不会被清除、只是被忽略,恢复那段代码即可继续复用旧值。

实现方式

半文件职责
Node 半index.js空 apply(),仅为让插件在宿主 cordis.yml / Loader 中可见;无任何宿主侧行为
客户端半client.js实际 UI,经 exports["./client"] + package.json 的 dsh.client 声明被 Web 宿主发现

客户端半行为:

  1. 通过 inject: ["slots", "sessions"] 取两个必需服务;只有 slots.register 本身缺失才打警告并禁用面板。apply() 同时把 sessions / uiSession 服务抓进插件私有的 host bridge。
  2. 在 conversation.session.header.actions 槽位注册 dsh-conversation(order: 100),把 store / host 合并在槽位 props 的最后 —— 宿主传空 props、或第三方 header 传另一套形状,都覆盖不掉它们;槽位声明晚到时有「注入 / 声明订阅 / 退避重试」三层通道(见「兼容性 / 注册通道三层兜底」)。
  3. 数据有两条路:slot props 的 useChat / useSession / useSessions(旧宿主 / 第三方 header 注入)优先;它们缺失时读 uiSession.current 的 hooks.chat(ChatSnapshot)与 hooks.session(hasMore / loadOlder)。两条路都经 conversationNodesOf 归一 + Array.isArray 兜底;都拿不到就是空态。sessions.binding(id) 仍用于旧回退与 loadOlder。
  4. buildOutlineItems 把快照摊平成大纲项:user/steering → level 0(同时统计该轮工具数与正文字数);assistant 的 text block 按 ATX 标题(^#{1,6}\s+,跳过围栏代码块内的 #)→ level 1~6;流式中的 partial 标题实时出现并带呼吸动画;tool-call / context 等非正文节点只计数、不进大纲。
  5. OutlineManager 持有层级、搜索(含范围)、收藏状态并与 localStorage 同步;折树 / 折叠恢复 / 可见行压平 / 定位揭示都是纯函数。
  6. 点击定位:先用宿主内部锚点属性 [data-chat-anchor-key] → [data-message-key] → [data-node-key],命不中再按 data-chat-flow-kind 顺序匹配 + 文本校验;始终找不到就提示「当前节点暂不可定位」,绝不静默错位滚动。
  7. 「本节末尾」跳转由 nextSectionItem 在先序扁平顺序里找下一个同级或更高级节点,没有则回退到滚到底部。
  8. 导出走内存里的 Blob + URL.createObjectURL(不请求宿主、不落盘到工作区),内容与「复制大纲」同源(outlineToMarkdown)。
  9. 样式全部使用 dsw-alias-* / dsw-shadow-* 主题令牌,无硬编码色值。

兼容性

  • 快照契约双支持:新结构 snapshot.nodes[] + snapshot.partial;旧结构 snapshot.chat.{order, nodes(Map)} + node.data.content。结构不符时返回空列表而不是抛错。
  • 槽位数据源加固:conversation.session.header.actions 的 owner props 可能为空(dsh 0.2.0-rc.2 即以 renderSlot(..., {}) 渲染),第三方 header(如 dsh-better-sidebar)接管 header 区时 props 形状也可能不同。面板优先用 slot props 注入的 hook,缺失时改读 uiSession.current 的 hooks.chat / hooks.session;两条路都拿不到就渲染空态,不会让槽位崩溃。
  • 宿主 API 降级:slots / sessions / uiSession 的形状、store 的 getSnapshot / subscribe 都逐层探测并吞掉异常;任一改名或缺失只让本面板降级为空态,不会拖垮同槽位的其它插件。
  • 一次性兼容性诊断:首次拿到可识别的快照(0 行也记录)时用一条 console.info 记录实际结构(legacy.nodes / nodes / chat.order …)与行数;结构无法识别时 console.warn 列出实际键名。首次渲染出大纲时探测 DOM 契约(data-conversation-scroll / data-chat-flow-kind / 锚点属性),缺失或改名时 console.warn 说明已降级为「按文本顺序匹配」。两类日志每次页面加载最多各一条,不改变任何容错行为。
  • 注册通道三层兜底:槽位声明可能晚于插件装载,首次 slots.register 会抛 slot "…" is not declared(0.1.5-rc.2 / 0.2.0-rc.1 / 0.2.1-rc.5 三版都会这样抛)。因此按宿主实际能力选通道:① slots.inject 存在 → 沿用宿主契约;② 否则 slots.subscribeDeclaration 存在 → 订阅声明事件后注册,槽位塌缩重建也能自动恢复;③ 都没有 → 立即尝试 + 700ms 退避重试(≤12 次)。inject 消失不再导致面板整块消失。重复注册由 declarationEpoch 把关,避免同 id 触发 already has an entry。
  • 样式能力探测:背景透明度依赖 CSS color-mix(Chromium 111+)。宿主不支持时该行不渲染、面板保持完全不透明,不会留下「拖了没反应」的死控件。
  • 文案双语:自带 zh/en 字典,按 navigator.language 回落;宿主若注入了可用的 t 则优先使用。

设计说明与来源

本插件的功能取舍参考了 DSH 生态内同类插件(会话大纲 / 提问导航 / minimap 类)公开的功能描述与交互形态 —— 同类能力(大纲树、右缘导航条、搜索、导出、阅读位置跟随等)属于通用交互设计,不受版权保护。代码为本仓库独立实现,未复制、未改编任何第三方源码,也不依赖任何第三方运行时包(唯一 peer 依赖是宿主提供的 react)。

依赖与许可

  • peerDependencies:react ^18.2.0(由宿主提供,无第三方运行时依赖)
  • license:MIT

兼容矩阵

组件支持基线
Node.js>= 22.19
DSH>= 0.1.5-rc.2 < 1.0.0(槽位名、slots.inject / register 契约在 0.1.5-rc.2、0.2.0-rc.1、0.2.1-rc.5 上核对一致;实机验证:0.2.1-rc.5 桌面端)
react^18.2.0(宿主提供)
界面web(dsh.client.platform)
渲染引擎Chromium 111+(仅「背景透明度」需要;0.2.1-rc.5 实测 Electron 44.7.0 / Chromium 152)

超出上表的组合未认证。

安装

优先用不可变 tag:

dsh plugin --profile web add github:huangfuren/dsh-conversation#v1.5.7

跟踪开发分支(需要尚未发布的最新提交时用这个):

dsh plugin --profile web add github:huangfuren/dsh-conversation

或指向本地 checkout:

dsh plugin --profile web add file:/absolute/path/to/dsh-conversation

package.json 已声明 dsh.bundle.patch(./cordis.patch.yml),由 dsh plugin add 自行维护组合。 若想手工插到 profile 的 cordis.patch.yml,等价写法是:

- insert:
    - id: conversation
      name: dsh-conversation

开发与测试

npm run check    # node --check 两个入口
npm test         # node --test
npm run verify   # check + test(已绑定 prepack,测试不过打不出包)

client.js 不是 ES 模块:dsh 客户端模块加载器按 window.__ModuleLoader__.load({ id, factory }) 加载它, factory 里的 require 只能解析宿主提供的外部模块(本插件只用到 react),相对路径依赖在真实加载器里 解析不了 —— 所以客户端保持自包含单文件(dsh-grafana 的 client.js 约 67 KB,同样如此)。 test/client.test.js 用最小加载器桩把它 load 进来,断言它导出的纯函数 internals; test/host-service.test.js 驱动 apply() 注册出来的槽位组件,锁定「宿主 slot props 为空时从 uiSession / sessions 服务取快照」以及服务缺失 / 恶意 props 的降级路径。 test/diagnostics.test.js 锁定 DOM 契约探测矩阵、快照形状矩阵,以及两类一次性日志「每次页面加载最多一条」的约定。

已知限制

  • 点击定位依赖宿主的 DOM 契约(data-chat-anchor-key / data-message-key / data-node-key 或 data-chat-flow-kind);若未来整体改属性,需在 locateByKey / collectUserRows 里追加新候选——插件会在首次渲染出大纲时探测一次,缺失时打一条 console.warn(见「兼容性 / 一次性兼容性诊断」)。
  • 服务路径跟随宿主「主视图」绑定的会话(uiSession.current):该槽位的 owner props 不带 sessionId,所以同页子会话(subagent)的 header 上,面板展示的仍是主会话大纲。若宿主将来在 props 里给 sessionId,会优先使用它。
  • 面板为 fixed 定位,与其它右上角悬浮插件共存时可能重叠,需各自错开 top。
  • 「N 工具 / N 字」统计口径是快照里该轮之后的 tool 节点数与 assistant 正文字符数,宿主若改变节点 kind 命名会退化为不显示(不会报错)。
  • 导出文件名固定为 dsh-conversation-outline.md,不做命名配置。

Related plugins