dsh-plugin-rightbar
给 DeepSeek Harness 网页端加一个「聊天右边那一列」:别的插件往这一列里挂标签页,而不是各自
position:fixed 抢位置、比 z-index。
> English: a shared right sidebar for DeepSeek Harness. Other plugins contribute tabs into the
> rightbar.tab slot instead of each pinning its own fixed panel. Web client only. See
> The contract for the integration surface.
- 适用范围:只支持 Web 客户端(
dsh.client.platform = web)。宿主半端是个空apply,整个右栏都在浏览器里。 - 版本:
0.1.0· 许可证 MIT · 插件 idrightbar· 标签页槽位名rightbar.tab
它解决什么问题
Harness 没有「聊天旁边的一列」这个座位:
details是 single 型槽位,而且已经被工具详情面板占着 —— 谁要占就得顶掉别人;shell.overlay只是一层没有布局协调的浮层 —— 能浮起来,但不管谁压着谁。
于是每个想贴边的插件都自己写 position:fixed; right:0:互相遮挡(谁 z-index 大谁赢)、各自去压
#root 的宽度(谁最后渲染谁生效)、留宽 / 收起 / 记住宽度各写一遍。
本插件往 shell.overlay 注册一个条目,用它的 children 声明一个新的 list 型槽位 rightbar.tab
(跟宿主 AppFrame 声明 sidebar / details 是同一个机制)。想贴边的插件往这个槽位注册标签页即可。
安装
前置:一个能跑 Web 客户端的 DeepSeek Harness(--profile web)。
方式 A:直接从 GitHub 装(推荐)
cd /path/to/deepseek-harness # 需 node ≥ 22 在 PATH 上
node --import tsx/esm apps/cli/src/bin.ts plugin --profile web add github:lo2589/dsh-plugin-rightbar
(装了全局 dsh 命令的话,等价于 dsh plugin --profile web add github:lo2589/dsh-plugin-rightbar。
第一次装会 clone 整个仓库,一两分钟起步,属正常。)
方式 B:装本地目录(改代码时用)
cd /path/to/deepseek-harness
node --import tsx/esm apps/cli/src/bin.ts plugin --profile web add link:/path/to/dsh-plugin-rightbar
装完必须重启一次 dsh。 bundle 层是启动时组装的,不重启这个插件不会挂上去。
装完应该看到什么
页面右侧多出一列(默认展开,宽度 420px,可拖左边缘改宽;点右上角 › 收成 32px 竖条)。
⚠️ 如果右侧什么都没有,那是正常的:本插件自己不提供任何标签页,没有标签注册时整列不渲染
(同时把让出的宽度归零)。想先确认它装好了,打开自带的两个演示标签:
localStorage.setItem('rightbar.demo', 'true') // 然后在浏览器里刷新页面
刷新后右侧会出现「演示 A」「演示 B」两个标签 —— 说明槽位声明、注册、渲染这条链是通的。
卸载
cd /path/to/deepseek-harness
node --import tsx/esm apps/cli/src/bin.ts plugin --profile web remove dsh-plugin-rightbar
然后重启一次。已经接入的插件不会崩:rightbar.tab 不再被声明,它们的 slots.inject 永远不触发。
给你自己的插件接入
最小可用例子(id 唯一,label 显示在标签上,order 决定标签顺序):
// 你的插件 client 半端 apply(ctx) 里
var slots = ctx.get('slots')
slots.inject('rightbar.tab', function () {
return slots.register(
{ name: 'rightbar.tab', id: 'my-plugin', label: '我的面板', order: 20 },
MyPanel,
)
})
为什么这么写:
slots.inject会等声明出现,所以加载顺序无所谓:rightbar 晚一点挂上,你的注册会自动补进去;
rightbar 卸载时自动撤销,重回来自动恢复(你不用重载)。- rightbar 对你始终是可选的。没装它,
rightbar.tab永远不被声明,这个 inject 永远不触发 ——
保留你自己浮动的兜底,别把 rightbar 写成硬依赖。 - 面板内只管填满给定的盒子(宽高由 rightbar 给)。三条硬性要求:
1. 不要position:fixed;
2. 不要改#root的宽度(宽度只有 rightbar 写,写成` 上的--rightbar-w`);
3. 不要自己造 z-index 去压别人。 - 同一页面只有一个 rightbar:第二次声明
rightbar.tab会被槽位注册表拒掉。
从别处切到自己的标签
比如工具栏按钮、URL 参数、宿主里某个动作之后,想直接激活你的标签:
window.dispatchEvent(new CustomEvent('rightbar:activate', { detail: { id: 'my-plugin' } }))
它只是窗口事件,不是 import —— 两个插件之间没有代码依赖,rightbar 没装时没人监听,无害。
自带的 skill(给 agent 看的)
装了这个插件,你的 agent 会话 skill 目录里会多一条 rightbar-tab —— 内容是"怎么给 rightbar 挂 tab":
槽位名、注册调用、三条硬性要求、把 rightbar 当可选座位的写法、rightbar:activate 事件,以及下面那个让位开关。
它由 cordis.patch.yml 的第二行 rightbar-skill 提供(src/skill.js,依赖为零:只调
ctx.skills.register(),读不到正文就只 warn 不启用)。所以以后让 agent 给你写新面板时,它不用读这个
仓库也知道该往哪挂。
让还没迁过来的面板让位(默认关)
别的插件的老面板如果是自己 position:fixed; right:0,会和右栏重叠。本插件能按下面这套处理,但
默认一个都不碰:
localStorage['rightbar.adopt'] |
行为 |
|---|---|
不设 / off(默认) |
什么都不做 |
declared |
只处理自己加了 data-rightbar-adopt="1" 的面板 |
all |
按样式猜:同时满足 fixed、right≈0、宽度 < 窗口 80%、高度 ≥ 120px 的 body 直接子元素 |
localStorage.setItem('rightbar.adopt', 'declared') // 然后刷新页面
⚠️ 为什么默认关:
- 只能靠样式猜,误伤面很大(模态、遮罩、toast、浮动工具栏都可能是 fixed + 贴右);
- 收编动作只有一种 —— 把它的
right改成var(--rightbar-w),绝不搬 DOM。React 17+ 的合成事件挂在
root 容器上,节点跨容器移动会让别人的事件与状态错乱;只改偏移量最坏是位置偏一点,不会让别人的插件崩; - 默认开启等于告诉生态"不用改代码,rightbar 会捡",契约就没人迁了。
被收编的面板会打上 data-rightbar-adopted="1";卸载或关掉时原样还回去(原来的 inline right
优先保留)。它只是兼容性照顾,不是契约 —— 正式做法仍是注册一个 tab。
契约(也给别人看的接口)
| 项 | 值 |
|---|---|
| 槽位名 | rightbar.tab(kind: 'list'、scope: 'root',由本插件声明) |
| 注册签名 | slots.register({ name: 'rightbar.tab', id, label, order }, Component) |
| 让出的宽度 | ` 上的 CSS 变量--rightbar-w(只有本插件写;无标签时为0px`) |
| 激活事件 | window.dispatchEvent(new CustomEvent('rightbar:activate', { detail: { id } })) |
| 渲染方式 | 一次只渲染当前那一个标签(renderSlot('rightbar.tab', {}, { only: id })) |
| 层级 | z-index: 600(高于遗留浮动面板 500–530,低于全屏模态 9997+) |
| 尺寸 | 默认宽 420 · 最小 280 · 最大窗口宽 70% · 收起时 32 |
| 浏览器偏好 | localStorage:rightbar.open / rightbar.active / rightbar.width |
| 演示标签开关 | localStorage:rightbar.demo |
| 让位开关 | localStorage:rightbar.adopt(off 默认 / declared / all) |
| 让位标记 | 面板上加 data-rightbar-adopt;被收编后变成 data-rightbar-adopted="1" |
测试
单测跑的是真的 SlotCore 和真的 React,所以要有一个 harness 仓库;而且必须在 harness 目录里跑
(tsx 在宿主那棵树里,从插件目录跑会 ERR_MODULE_NOT_FOUND):
cd /path/to/deepseek-harness
DSH_REPO=$PWD node --import tsx/esm /path/to/dsh-plugin-rightbar/tests/slot-core.mjs
DSH_REPO 不写的话默认取插件的同级目录 deepseek-harness。
覆盖到的 7 项:rightbar 不存在时别人的注册会等(不抛错)、声明本身、先等着的一方自动落进来、
第二次声明被拒、只渲染当前标签、卸载时标签一起撤、重新挂载时注册者自动回来。
⚠️ 这个单测抓不到层级遮挡和 Safari 裁剪这两类问题,它们必须在真页面上看(见下面报错记录)。
另外两个测试不需要 harness,直接从插件目录跑(纯 node):
node tests/skill-register.mjs # 自带的 rightbar-tab skill:字段合法性、正文口径、正文读不到时只 warn
node tests/adopt.mjs # 让位开关:默认关、只认声明、不碰自己的面板、卸载还原、all 模式的保守判据
tests/skill-registry.mjs 是集成检查:把 src/skill.js 挂到真的 SkillRegistry 上,
确认目录里有它、正文取得到、卸载后消失。它也需要 harness,跑法跟 slot-core 一样(换文件名即可):
cd /path/to/deepseek-harness
DSH_REPO=$PWD node --import tsx/esm /path/to/dsh-plugin-rightbar/tests/skill-registry.mjs
这条是必需的,不是锦上添花:单元测试里的 ctx.skills.register 是假的,抓不到"字段缺失"——
source 漏过一次,结果是 skill 能列出、加载时抛 TypeError(目录里看得见却用不了)。
常见问题
装完右侧空白 —— 正常,见上文「装完应该看到什么」:没有标签注册时整列不渲染。开演示标签或接入一个 tab。
右侧面板被别的插件盖住 —— 本插件层级是 600。如果对方压得更高,把对方迁进 rightbar.tab
(这是本插件存在的意义),或者给它提 issue。
Safari 里右侧一片空白(Chrome 正常) —— 宿主 .frame 是 overflow:hidden,#root 让窄后面板整个
落在 .frame 盒子外,Safari 照样按祖先裁剪。0.1.0 起面板与收起竖条用 ReactDOM.createPortal 挂到
document.body,已修;请确认真装上的是这个版本之后的。
宽度 / 展开状态记不住 —— 偏好存在 localStorage,隐私模式下读写会抛错,此时静默退回默认值(展开、420px)。
想默认收起 —— localStorage.setItem('rightbar.open', 'false')。
报错记录
每条:现象 → 根因 → 证据 → 修复。验证一律在真实 dsh 页面上截图 + 读 DOM,Chromium 和 Safari 各一遍;
只靠 SlotCore 单测抓不到下面任何一条。
2026-10-07 · 面板被另一个插件的浮动面板盖住
- 现象:右侧一列只露出左边约 120px,其余被同层的「媒体展示台」面板压住。
- 根因:rightbar 与其他插件的浮动面板同在宿主 overlay 层,按 z-index 比高低。rightbar 写成
z-index:40,那个浮动面板是500。 - 证据:
document.elementFromPoint在面板中心命中对方的.sc-head/.sc-title,不在 rightbar 内。 - 修复:z-index 提到 600(高过 500–530 的浮动面板,低于 9997+ 的全屏模态);对方插件后来也改成
软接入rightbar.tab,不再自己浮着。
2026-10-07 · Safari 里右侧只有空白
- 现象:Chrome 正常;Safari 26.4 里
#root已让出宽度,右侧却一片空白。 - 根因:overlay 层在宿主
.frame(overflow:hidden)里面,#root让窄后面板(position:fixed)
整个落在.frame的盒子之外。Chrome 让 fixed 元素逃出祖先裁剪,Safari 照样按.frame裁掉。 - 证据:Safari 里面板在 DOM 中(x=1018, w=515,
visibility:visible,opacity:1, z=600),
elementFromPoint命中.rb-tabs,屏幕却是空的;临时把.frame的overflow改成visible,
面板立刻出现,改回即消失。Playwright 的无头 WebKit 复现不了,必须在真实 Safari 里查
(Safari 设置 → 开发者 → 允许 Apple 事件中的 JavaScript,然后osascript … do JavaScript)。 - 修复:面板与收起竖条用
ReactDOM.createPortal挂到document.body,不碰宿主.frame。
观察到但不属于 rightbar
- 2026-10-07:页面加载时另一个插件自己的 HTTP 路由(
GET /plugins//split)返回 HTTP 400,
共 11 次;来源不是 rightbar。记在这里只是为了下次不必重新排查。
License
MIT — 见 LICENSE。