dsh-hero-rightbar
qcsjjjjj/dsh-hero-rightbar
在没有对话的页面上补回右侧栏的展开入口,位置与外观对齐产品自带控件。
安装
dsh plugin --profile web add github:qcsjjjjj/dsh-hero-rightbarREADME
dsh-hero-rightbar
在没有对话的页面上,把右侧栏的开启按钮补回来。
dsh plugin --profile web add /绝对路径/dsh-hero-rightbar-0.1.0.tgz
# 客户端 bundle 由 dsh-client-hmr 监视:改写已安装的
# <profile>/node_modules/dsh-hero-rightbar/client/client.js
# 会在约 1 秒内原地热替换该插件,不需要重启 dsh,也不需要刷新页面。
1. 为什么原本没有那颗按钮
右侧栏唯一的入口,是挂在会话 header 角落席位上的那颗展开按钮:
-
@deepseek-ai/dsh-client-ui-sidebar-right把自己的按钮注册进conversation.session.header.corner; -
而这个席位所在的 header,在空会话阶段整段不渲染——
@deepseek-ai/dsh-client-ui-conversation的会话骨架里写着:const hideChrome = session.blank && conversationPhase(session, conversation) === "blank"hideChrome为真时,它渲染的是一个空的、aria-hidden的<header>, 里面一个子席位都没有,角落按钮自然也不存在。
所以在下面几种「还没有对话」的页面上,右栏都没有入口:
| 页面状态 | 右栏本身 | 入口按钮 |
|---|---|---|
| 点「新会话」后的空白会话(hero) | 已挂载、可用 | 不渲染 |
| 被「新会话」复用的空会话 | 已挂载、可用 | 不渲染 |
| 在另一个工作区新建的空会话 | 已挂载、可用 | 不渲染 |
| 完全没有选中会话的空页面 | 无(状态按会话 id 键控) | 不渲染 |
最后一种是因为右栏的状态按会话 id 键控(@deepseek-ai/dsh-client-ui-sidebar-right
自己的已知限制里写着「没有会话就没有停靠面」),没有会话就没有可展开的面板。
1.1 曾经修不好的一种情况(已修)
旧版本在渲染期间用 DOM 判断「产品是不是已经画了按钮」:
if (document.querySelector(BUILTIN_SELECTOR) !== null) return null;
React 是「先把整棵树 render 完,再 commit DOM」。从有对话的会话切到新的空会话
(例如新工作区里的新会话)时,组件重渲染的那一刻,旧会话 header 里那颗
[data-sidebar-right-expand] 还在真实 DOM 里,于是判定成「产品已经画了」→
整个组件返回 null;而这个判断不是响应式的,commit 之后不会重新求值,按钮就
永久消失了。
这正好解释了现象:页面从没画过 header 时(一开始完全没有会话)有效;一旦进过 有对话的会话、再新建空会话就失效。
2. 这个插件做了什么
往框架可叠加的浮层席位 shell.overlay(root 作用域、list 类型)加一颗按钮:
- 位置对齐产品自带那颗(
top: 11px; right: 12px,即 headerpadding: 10px 28px 0 20px加上角落席位margin-right: -16px后的落点); - 28px 圆形控件、
IconPanelLeftOutline16镜像图标、同样的 hover 底色; - 文案直接取产品
sidebarRight命名空间里的chrome.expand/chrome.expandAria, 取不到才用自己的兜底文案。
2.1 判定只有两条,且都由浏览器保持最新
组件只判断两个响应式条件:
- 会话界面被选中(
panelInfo.activePanelId === null,否则按钮会飘在设置等全局面板上); - 框架报告右栏收起——框架根上的
data-rightbar-collapsed,是框架按自己算出的列宽 写下的呈现信号本身。
「产品是不是已经画了自己的按钮」不在这里判断,交给一条 CSS:
body:has([data-sidebar-right-expand]) .dsh-hero-rightbar-trigger { display: none }
body:has([data-rightbar-fullscreen]) .dsh-hero-rightbar-trigger { display: none }
:has() 由浏览器在任何 DOM 变化后立刻重算,所以:
- 产品把按钮画出来时,这里瞬间隐藏 —— 不可能出现两颗按钮;
- 会话切换 commit 的中间态也不会让这里卡死(不再有「render 读到旧 DOM」这回事);
- 全屏时产品自己的面板铺满视口(浮层 z-index 更低),这里不画入口。
同时不再依赖会话列表的 blank 位:那是列表摘要投影,切会话时会滞后,列表还没填进来时
根本不存在。既然产品自己的控件已经由 CSS 仲裁,插件只需要在「右栏收起 + 产品没提供入口时」
把入口补上。
也不再读 ctx.sidebarRight.isExpanded() 作为收起判据:它的 binding 属于上一个刚挂载的
会话座位,跨会话切换时可能仍报告上一会话的展开状态,从而把入口藏起来。呈现事实以框架自己
写的属性为准。
点击行为与自带按钮一致——展开右栏;如果当前没有任何会话(右栏没有可键控的对象),就先走 一次「新会话」流程(和左侧「新会话」按钮同一个动作,创建一个空会话),等座位挂载后立刻展开。 展开出来的是产品自己的面板,所以文件、预览、第三方停靠的 tab(技能中心、任务看板、 工作流卡片、插件市场……)全部照旧可用。
3. 已知边界
- 没有工作区时会提示,不会静默失败:没有任何工作区时,「新会话」流程没有目标会话可创建, 按钮会浮出一条「先选择一个工作区」的提示(6 秒后自动消失)。
- 不改产品任何东西:不注入 header、不隐藏原按钮、不改会话骨架;只往一个官方留白的浮层 席位上添一件自己的东西,另加一条只作用于自身类名的 CSS 仲裁。
- 依赖三个稳定接缝:
shell.overlay席位、框架写在根节点上的data-rightbar-collapsed/data-rightbar-fullscreen、以及ctx.sidebarRight控制器(isExpanded/toggleExpanded)。 - 需要
:has():与产品自身前端同一基线(产品自己的 chat / conversation / trajectory 与 构建产物 CSS 都在用:has()和@container)。 - 展开后的收起走面板自己的控件(tab 条末端那颗),与自带按钮的行为一致。
4. 结构
dsh-hero-rightbar/
├── package.json # dsh.bundle.patch + dsh.client(platform: web, inject)
├── cordis.patch.yml # bundle patch:把插件行插进加载器树
├── lib/index.js # 宿主半边:无行为(逻辑全在浏览器侧)
├── client/client.js # 浏览器半边:shell.overlay 入口 + CSS 仲裁 + 点击流程
├── verify/ # 开发期验证:抽取真实 CSS、真浏览器断言、安装一致性
│ ├── extract-css.mjs
│ ├── arbitration.html
│ ├── run.mjs
│ ├── watch-graph.mjs
│ ├── prove-live-reload.mjs
│ └── check-install.mjs
└── README.md
无第三方运行期依赖(只用加载器暴露的 react 与 @deepseek-ai/dsh-client-ui-primitives)。
verify/ 只在开发期使用,已由 package.json 的 files 排除在发布产物之外。
5. 验证
node verify/extract-css.mjs # 从发布的 client.js 里切出真正下发的样式串
node verify/run.mjs # 本机 Chromium/Edge + puppeteer-core,断言仲裁行为
node verify/check-install.mjs # 源码 / 安装副本 / tgz / lock 完整性 / 在线下发产物 五方一致
node verify/prove-live-reload.mjs # 改写安装副本 → 等待 rebuilt 帧 → 校验新 rev 下发内容 → 还原
node verify/watch-graph.mjs 8 # 读在线客户模块图(SSE),确认插件在图中及其 rev
run.mjs 断言的是「产品控件在不在 DOM 里」这条仲裁,而不是某个组件状态:
| 断言 | 说明 |
|---|---|
| 无产品控件 → 入口可见 | 空会话 / 无会话页面 |
| 产品控件出现 → 入口同一任务内隐藏 | 不需要任何重渲染 |
| 产品控件消失 → 入口重新可见 | 会话切回空会话 |
| 全屏信号 → 入口隐藏 | 产品面板已铺满 |
| 旧实现模型在同一步骤里保持卡死 | 复现被修掉的那个 bug |
prove-live-reload.mjs 证明「装好不用重启」:它给安装副本追加一行注释、等
/plugins/events 上的 rebuilt 帧点名本插件、把该 rev 的下发内容取回来校验、再还原原始字节,
最后确认文件与自己开工前逐字节一致。
check-install.mjs 在最后打印的五方一致性结论形如:
OK - source, install, tarball, lockfile and the live server all agree on the fixed bundle