dsh-conversation-jump
English | 中文
A rail of four circular buttons for the DSH Web conversation: to top, previous user message, next user message, to bottom. It floats above the composer at the right edge of the conversation, and replaces the product's single built-in "to bottom" button.
What makes it different
Sibling plugins mostly draw a minimap, a table of contents, or inline anchors. This one does none of that. It steps through the conversation one user message at a time — a click lands exactly on the previous or next thing you asked — and pages older history in automatically when the target sits outside the loaded window.
Navigation targets are only user and steering entries. Tool calls, context injections, compaction markers and other process nodes are skipped, so "previous" means "my previous question", not "the previous DOM block".
Install
# From npm
dsh plugin --profile web add dsh-conversation-jump
# From GitHub, when you want the source
dsh plugin --profile web add github:uigdwunm/dsh-conversation-jump
# From the prebuilt release tarball (nothing to build locally)
dsh plugin --profile web add https://github.com/uigdwunm/dsh-conversation-jump/releases/download/v0.1.5/dsh-conversation-jump-0.1.5.tgz
Restart dsh web afterwards.
dsh plugin is a thin forwarder to pnpm in the profile directory, so a plain registry name, a github: spec and a remote tarball URL all work. This package declares no prepare script, so no allowBuilds entry is needed for the GitHub or tarball route.
Local development
npm install
npm run build
# install into the web profile; restart dsh web afterwards
dsh plugin --profile web add file:/absolute/path/dsh-conversation-jump
A rebuild alone does not reach the running page. The profile keeps its own copy under ~/.dsh/profiles/<profile>/node_modules/, and npm run build replaces lib/client.js by rename — which breaks the hard link the install created. Re-run the dsh plugin … add file:… command after each build (or copy the built file into that package) and then reload the page.
Capabilities
| Capability | Behaviour |
|---|
| To top | Scrolls to the current top, then keeps paging older history in until the real top is reached |
| Previous | Jumps to the previous user message; if it lies beyond the loaded window, it goes to the top and pages older history in first |
| Next | Jumps to the next user message |
| To bottom | Goes straight to the newest position |
| Message anchor | The target message settles about 1/12 down from the top of the viewport |
| When it appears | Hidden by default; appears when the average upward scroll speed exceeds 200 px/s, hides again as soon as you scroll down |
| Auto-hide | Disappears 3 s after appearing; clicking any button restarts that countdown |
| Position | Aligned to the right edge of the conversation and 16 px above the composer, positioned synchronously before paint so it does not flash |
| State | Page-memory only — nothing persisted |
Button labels are Chinese (回到顶部 / 上一个 / 下一个 / 回到底部); each button carries the same text as both its title and aria-label, and the rail is a role="toolbar" labelled 会话导航.
Implementation notes
- Targeting is attribute-based, not hash-based. The scrollport is
[data-conversation-scroll], user messages are [data-chat-anchor-key] filtered by data-chat-flow-kind, and the composer seat is [data-composer-seat]. The built-in "to bottom" button is hidden by matching the stable _toBottomSlot class suffix rather than a CSS-module hash, so a DSH upgrade that rehashes class names does not break it.
- Paging drives the product's own control. It locates the "load older" button inside
[data-chat-flow] — the first button that is not inside a message row and precedes the first row — clicks it, then polls the anchor count until it grows. Attempts are bounded, so a host that never grows the list cannot spin forever.
- Only the
slots seat is injected. cordis-plugin-timer is host-only and absent in the web client, so the plugin declares inject: ['slots'] and uses native setTimeout / setInterval, tracking every handle so disposal cancels them.
- The stylesheet heals itself. It lives in a
<style data-dsh-conversation-nav> node owned by the fiber, but every 100 ms sample checks the view and recreates the node when it is missing. A fiber re-apply that removes the styles (for example the dev-mode HMR driver rebuilding this plugin) therefore cannot leave the rail unstyled in the page's top-left corner with the product's own "to bottom" button reappearing while the rail is still mounted — it recovers on the next sample instead of needing a page refresh.
- Everything is cleaned up on unload: the style element, every pending timer, and the scroll listener.
Boundaries
- This is a Web UI plugin only. The host half (
index.mjs) is an empty apply — no host services, events, tools or storage.
- It keeps page-memory state only: no
localStorage, IndexedDB, cookies, files or backend database.
- If the host never renders a "load older" button, paging cannot proceed and the jump stops there rather than looping.
Development
npm run build # esbuild bundles src/client.ts into lib/client.js
npm run typecheck # tsc --noEmit
Publishing
npm login # requires an npm account
npm publish # prepublishOnly runs the build first
The repository field must point back at this GitHub repo. awesome-dsh-plugin's npm probe only claims a package when the package exists and its repository points at the same repo — otherwise there is no download-count sorting and no npm install entry.
Getting listed
- GitHub
dsh-plugin topic — already set; plugin markets sync from it.
- awesome-dsh-plugin — open a PR adding
data/plugins/uigdwunm__dsh-conversation-jump.yml (category ui) to awesome-dsh-plugin/awesome-dsh-plugin. That repo's READMEs are generated from the data files — do not edit them by hand.
Naming
The npm name dsh-conversation-nav is taken by another author (not this repo), so this package is published as dsh-conversation-jump. Repo name and package name match.
Structure
| File | Purpose |
|---|
package.json | DSH bundle/client declaration, exports, metadata and build scripts |
cordis.patch.yml | Mounts the plugin into the web profile composition |
index.mjs | Host half (browser-only plugin, empty apply) |
src/client.ts | Browser-side navigation, paging, positioning and visibility policy |
build.mjs | esbuild browser build and ModuleLoader wrapper |
lib/client.js | Build output (shipped with the package) |
License
MIT — see LICENSE.
中文
DSH Web 对话区的一组圆形导航按钮:回到顶部、上一条用户消息、下一条用户消息、回到底部。它悬浮在输入框上方、对话内容右缘,并替换产品自带的单个“回到底部”按钮。
与同类插件的区别
同类插件大多在做缩略图(minimap)、目录(TOC)或行内锚点。本插件都不做,而是按「用户消息」这个语义单位逐条前后跳转——一次点击精确落到上一条或下一条你提过的问题,并在目标超出已加载窗口时自动翻页加载更早历史。
导航目标只包含 user 与 steering 两类条目,会跳过工具调用、上下文注入、压缩标记和其他过程节点。所以“上一条”指的是“我上一个问题”,而不是“上一个 DOM 块”。
安装
# 从 npm 安装
dsh plugin --profile web add dsh-conversation-jump
# 从 GitHub 安装(需要源码时)
dsh plugin --profile web add github:uigdwunm/dsh-conversation-jump
# 从预构建 Release 包安装(无需本地构建)
dsh plugin --profile web add https://github.com/uigdwunm/dsh-conversation-jump/releases/download/v0.1.5/dsh-conversation-jump-0.1.5.tgz
安装后重启 dsh web 生效。
dsh plugin 只是把参数转发给 profile 目录里的 pnpm,因此「registry 包名」「github: 规格」「远程 tarball URL」三种写法都可用。本包没有声明 prepare 脚本,所以走 GitHub 或 tarball 路线都不需要额外配置 allowBuilds。
本地开发安装
npm install
npm run build
# 安装到 web profile,重启 dsh web 后生效
dsh plugin --profile web add file:/绝对路径/dsh-conversation-jump
只重新构建不会让运行中的页面生效。 profile 在 ~/.dsh/profiles/<profile>/node_modules/ 下保存自己的一份副本,而 npm run build 是用「改名替换」的方式写 lib/client.js,会切断安装时建立的硬链接。因此每次构建后都要重新执行一次上面的 dsh plugin … add file:…(或把构建产物复制进 profile 里的那个包),再刷新页面。
能力
| 能力 | 行为 |
|---|
| 回到顶部 | 先滚到当前顶部,再持续翻页加载更早历史,直到真正的顶部 |
| 上一个 | 跳到上一条用户消息;若它在已加载窗口之外,会先到顶部并自动加载更早历史 |
| 下一个 | 跳到下一条用户消息 |
| 回到底部 | 直接到最新位置 |
| 消息锚点 | 目标消息停在视口上方约 1/12 处 |
| 显示策略 | 默认隐藏;往上平均速度超过 200 px/s 时显示,往下滚动立即隐藏 |
| 自动隐藏 | 出现后 3 秒无点击自动消失;点击任一按钮重新计时 |
| 定位 | 对齐对话内容右缘、输入框上方 16 px;绘制前同步定位以避免闪动 |
| 状态 | 仅页面内存状态,不做持久化 |
实现说明
- 定位基于数据属性而非哈希类名。 滚动容器是
[data-conversation-scroll],用户消息是 [data-chat-anchor-key] 并按 data-chat-flow-kind 过滤,输入区是 [data-composer-seat]。产品自带的“回到底部”按钮通过匹配稳定的 _toBottomSlot 类名后缀隐藏,而不是写死 CSS-module 哈希,因此 DSH 升级重新生成类名不会失效。
- 翻页走产品自身的控件。 在
[data-chat-flow] 中定位“加载更早”按钮(第一个不在消息行内、且位于首行之前的按钮)并点击它,然后轮询锚点数量直到增长;尝试次数有上限,宿主始终不增长列表时也不会空转。
- 只注入
slots 一个席位。 cordis-plugin-timer 是纯 Host 服务,在 Web 端不存在,因此插件声明 inject: ['slots'] 并使用原生 setTimeout / setInterval,同时记录每个句柄以便卸载时取消。
- 样式会自愈。 样式挂在 fiber 拥有的
<style data-dsh-conversation-nav> 节点上,但每 100ms 采样都会检查它是否还在,缺失就重新创建。因此当 fiber 被重新 apply 而移除样式时(例如 dev 模式下 HMR 重建本插件),不会出现“导航条仍挂着却变成页面左上角的无样式按钮、产品自带『回到底部』同时回来”的状态——下一个采样周期就恢复,不需要刷新页面。
- 卸载时全部清理:样式元素、所有待执行定时器、滚动监听。
边界
- 这是纯 Web UI 插件。Host 半边(
index.mjs)是空的 apply——不需要 Host 服务、事件、工具或存储。
- 只维护页面内存状态:不使用
localStorage、IndexedDB、Cookie、文件或后端数据库。
- 若宿主始终没有渲染“加载更早”按钮,翻页无法进行,跳转会就此停止,不会死循环。
开发
npm run build # 用 esbuild 把 src/client.ts 打包到 lib/client.js
npm run typecheck # tsc --noEmit
发布
npm login # 需要 npm 账号
npm publish # prepublishOnly 会先执行构建
repository 字段必须指回本 GitHub 仓库。awesome-dsh-plugin 的 npm 探测只在「npm 包存在 且 其 repository 指向同一仓库」时才认领,否则拿不到下载量排序与 npm 安装入口。
收录渠道
- GitHub
dsh-plugin topic:已添加,各类插件市场会据此同步收录。
- awesome-dsh-plugin:向 awesome-dsh-plugin/awesome-dsh-plugin 提 PR,新增
data/plugins/uigdwunm__dsh-conversation-jump.yml 一个文件(分类 ui)。该仓库的 README 由数据文件生成,不要手工编辑。
命名说明
npm 上 dsh-conversation-nav 这一名称已被其他作者占用(非本仓库),因此本包发布名为 dsh-conversation-jump。仓库名与包名一致。
结构
| 文件 | 作用 |
|---|
package.json | DSH bundle/client 声明、exports、元数据与构建脚本 |
cordis.patch.yml | 把插件挂进 web profile 组合 |
index.mjs | Host 半边(浏览器 UI 插件,空 apply) |
src/client.ts | 浏览器端导航、分页、定位与显示策略 |
build.mjs | esbuild 浏览器构建与 ModuleLoader 包装 |
lib/client.js | 构建产物(发布时包含) |
许可证
MIT,见 LICENSE。