hemppp/dsh-panel-dock-plugin ↗★ 0
dsh-panel-dock-plugin
Photoshop-style floating dockable panels for the DeepSeek Harness Web UI: draggable, resizable, collapsible, hideable panels with persisted layout. 适合需要自定义 DSH 界面布局、新增独立浮动面板以提升操作效率的用户。
インストール
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:hemppp/dsh-panel-dock-pluginドキュメント
README 全文を読む ↗dsh-panel-dock-plugin
Photoshop 式浮动面板坞(floating panel dock)for the DeepSeek Harness Web UI.
把界面上「可以安全新增」的区域做成一套独立面板系统:面板可拖动、可缩放、可折叠、 可显示/隐藏、布局持久化、一键恢复默认。侧栏的「面板坞」图标会打开一个同名控制页, 在那里能集中开关每个面板。面板颜色走 DSH 官方主题 token (唯一例外是投影,见 能做到 / 做不到),明暗主题一致。
诚实范围声明(先读这一段)
本插件不能把 DSH 已经发布的
sidebar/main/rightbar区域「拆下来」塞进浮动面板。 那些 slot 是single且replaceRisk: shadows-shipped-ui:第三方插件占用它们等于把整个界面顶掉。 因此本插件实现的是一套真实可用的、与原界面并存的浮动面板系统(新增面板), 不是「原区域重挂载」。详见下面 能做到 / 做不到。
1. 安装(未发布 npm)
方式 A:从 GitHub 仓库安装
dsh plugin add 是 pnpm 的包装(在 /profiles/desktop 下执行 pnpm add),
它 --help 列出的可用形式包含 :/ 与 ``:
dsh plugin --profile desktop add github:hemppp/dsh-panel-dock-plugin
# 或
dsh plugin --profile desktop add https://github.com/hemppp/dsh-panel-dock-plugin
仓库里已内置构建产物 lib/index.js 与 lib/client.js(.gitignore 刻意没有忽略 lib/),
所以安装端不需要装 devDependencies、也不需要自己构建。
方式 B:本地路径安装(开发用)
cd dsh-panel-dock-plugin
pnpm install
pnpm run bundle # 改完源码后重新构建
# 界面 Plugins → Add plugin,或:
dsh plugin --profile desktop add --save-exact @0.2.0
本仓库从未发布到 npm。GitHub 上提供源码 + 已构建的
lib/,Release 里附带npm pack产出的.tgz。仓库自身不做任何自动安装,也不改动 DSH 宿主或任何 profile —— 安装是你手动执行的一步;桌面 profile 由 Electron 独占管理。
2. 面板系统能力
| 能力 | 状态 | 实现位置 |
|---|---|---|
| 浮动面板层 | ✅ 已实现 | shell.overlay 入口 panel-dock(src/client/index.tsx) |
| 拖动 | ✅ 已实现 | 标题栏 pointer 事件 + setPointerCapture,拖动中按视口 clamp |
| 调整大小 | ✅ 已实现 | 右下角 resize 手柄,最小 240×140 |
| 折叠 / 展开 | ✅ 已实现 | 标题栏折叠按钮(双击标题栏亦可) |
| 关闭 / 隐藏 | ✅ 已实现 | 标题栏 ✕;底部 chips 或 sidebar.panellist 图标重新打开 |
| 全部显示 / 全部隐藏 | ✅ 已实现 | 坞右下角工具条的「全显 / 全隐」 |
| 恢复默认布局 | ✅ 已实现 | 工具条「复位」:清 localStorage 并回到默认位置 |
| 布局持久化 | ✅ 已实现 | localStorage key dsh-panel-dock-plugin:layout:v1 |
| 明暗主题一致 | ✅ 已实现(有一处例外) | 除投影外全部颜色走 --dsw-alias-* / --dsw-specific-* token |
| 面板开关图标 | ✅ 已实现 | sidebar.panellist 入口 id panel-dock-toggles;点击打开下面那个控制页 |
| 图标打开的控制页 | ✅ 已实现 | main 入口同 key panel-dock-toggles(selectPanel 要求两者成对) |
| 视口变化自适应 | ✅ 已实现 | resize 监听把所有面板 clamp 回可见范围 |
| 已发布区块拖动(v0.2.0) | ⚠️ 已实现(离线验证,尚未在真实浏览器验证) | src/client/blocks.tsx + 控制页开关,见第 3 节 |
| 区块缩放 / 平铺重排 / 拖出窗口 | ❌ 不做 | design.md §7,见第 3 节末 |
把已发布 sidebar/main 搬进浮动面板 | ❌ 不能做 | 平台限制,见下节 |
内置面板(3 个)
- 会话与工作区(
overview)— 通过 slot 标准 props 读取真实数据: 工作区数量、会话总数、每个工作区的会话数。不请求任何远端接口。 - 插件与环境(
environment)— 显示插件 id / 版本 / 三个入口 id / 当前可见面板数, 并能探测 Node half 的健康路由GET /dsh-panel-dock-plugin/health。 - 便签(
notes)— 自动保存到localStorage(keydsh-panel-dock-plugin:notes)。
3. 区块拖动(v0.2.0,已实现但尚未在真实浏览器验证)
v0.2.0 新增的能力:把 DSH 已经发布的四块区域 —— 左栏 sidebarCol、中区 centerCol、
右栏 rightbarCol、输入框 composer —— 变成可拖动区块。拖走后原位留空、其他区域不重排;
拖回原位(≤24px)精确复原;原有 UI 细节不变。
它与第 2 节那套「新增浮动面板」是两回事。面板是新增的 UI;区块拖动不新增任何 slot 入口,
只修改已经渲染出来的元素的内联样式(position / z-index / left / top / overflow /
width / box-sizing,以及 composer 原位父级的 min-height),
不插入、不移动、不删除任何 React 拥有的节点 —— 所以官方界面不会被替换、也不会被重挂载。
这是「不碰 single + shadows-shipped-ui 槽位」这条约束下能做到的另一种形态。
怎么用
- 点侧栏「面板坞」图标打开控制页(
sidebar.panellist→main,keypanel-dock-toggles)。 - 「开始拖动编辑」打开编辑模式(默认关,持久化在
localStoragekeydsh-panel-dock-plugin:blocks:v1)。开启后每个可拖动区块的上边缘出现一个把手。 - 拖把手移动区块;拖回原位 24px 以内会完全清除本插件写过的属性,即逐字复原。 编辑模式开着也不会「顺手把原位重写一遍」——只有真正带偏移的区块才有内联几何, 偏移为 0 的区块(刚吸附归位、刚全部复位、以及从未拖过的)一律不写任何属性。
- 「全部复位」或每行的单独复位只删本插件写过的属性(绝不
removeAttribute('style'), 也绝不清空 / 覆盖别人的内联样式)。 - 控制页显示
n/4 可拖动;锚点缺失或结构不满足不变式的区块会显示原因,把手禁用。
关键设计(为什么这样做)
- 三列用
position: relative+left/top,不用absolute/fixed:relative 不脱离文档流, 所以 Grid 不会重排、原位天然留空。left/top对 relative 是位移而不是坐标,所以列的写入集合是position/z-index/overflow/left/top(右栏那一条position只在计算值仍为static时才写,且永远不写overflow),复原就是删掉这几个属性,最多 5 个。 - composer 用
position: fixed,并给它的 in-flow 父级写min-height保留原位空间 (中区带[data-conversation-scroll]{overflow-y:auto},absolute 包含块会随data-phase漂移)。 三条由此而来的、真实使用中会碰到的边界(不是待办,是设计取舍): ①fixed的包含块是视口,所以持久化的偏移是视口坐标 —— 窗口缩放后位置按原偏移维持,不会按比例缩放; ②top有下限--dsh-windows-titlebar-height(无该变量时 40px),防止卡片落进 Windows 顶部 「拖动整个原生窗口」的条带;③空会话的hero阶段输入框本来就是居中的,那时它的「原位」就是居中位,min-height仍然按原高预留空间,所以浮起后不会比不浮起更挤。 - 一律禁止
transform/filter/backdrop-filter/perspective/contain/will-change参与浮动:它们会建立包含块,把区块内部的fixed后代改成锚定该区块,并被它的overflow:hidden裁掉。这是硬纪律,由门禁静态核对 ——blocks.tsx里交给setOwnedStyle/removeOwnedStyle的所有属性名会被逐个检查。 - 浮起区块
z-index: 10。宿主自己的数值是把手 11、列头 15、overlay 20、composer seat 7; 12–19 是宿主保留段,区块不许进入(门禁静态校验)。 - 把手渲染在既有的
shell.overlay入口内(该层z-index:20、pointer-events:none、>*{pointer-events:auto}),不新增 slot 入口;把手显式写-webkit-app-region: no-drag—— Windows 下.frame:before覆盖顶部 40px(--dsh-windows-titlebar-height), 落进去会拖动整个原生窗口。 - 降级:任一锚点缺失、或三列锚点不变式不成立时,该区块标记为不可用 —— 不写任何样式、
把手禁用、控制页显示原因。降级路径不抛异常、不破坏界面。另外浮起左栏或中区之前会做一次性
运行时审计:先对列内既有的
absolute/fixed后代取 rect 快照(此时我们还没写任何属性), 写完再比一次,任一后代位移超过 0.5px 就把该区块降级、清掉刚写的样式并在控制页给出原因。 这是为了兑现「原有 UI 细节不变」:给一列加position会让它成为这些后代的包含块,从而改变它们的锚点。 MutationObserver观察document.body(childList+subtree+attributes),脏标记 + rAF 合并;宿主重建 DOM、或只是折叠一列(写内联宽度)都会触发重解析与重放,不改结构;dispose时disconnect()。窗口resize与编辑模式切换会作废缓存的自然几何。 手势进行中观察到的重审请求会被推迟(记进一个待办集合),等手势结束(提交或取消)再按 当时的新树重审 —— 否则宿主恰好在拖动中改动列内 DOM 时,下一次pointermove会在指针按着的时候 把区块退回原位;推迟的只是「重审」,指针位置的实时写入不受影响。
已知边界(不是缺陷,是不去动的宿主行为)
- 左栏里两个宿主的
position: fixed折叠按钮不会跟着走。dsh-client-ui-sidebar的折叠态/展开态按钮是position: fixed; left: 12px(z-index: 30) 与position: fixed; left: 48px。fixed的包含块是视口,所以把左栏拖到别处时,这两个按钮 仍留在窗口左上角 —— 它们会停在原地,而不是跟着左栏位移。我们没有修:它们的类名是 CSS Module 的哈希名,按本项目「不依赖哈希类名、不改宿主 DOM」的纪律,修不得。 实际影响是「左栏浮起时,栏内的折叠按钮看起来还留在左上角」——知道这一点即可,不影响拖动本身。 - 区块可以被拖到视口外,而且把手会跟着一起出去。 我们只对 composer 的
top做标题栏下限钳制, 不限制其余方向(这是设计取舍:按 §A8#17,区块落在拖动请求的位置本身就是验收条件, 不做「整块必须留在视口内」的二次钳制)。找回办法:控制页的「全部复位」(或按行复位)会把该区块 连同把手一起送回原位;也可以重新打开编辑模式后从控制页复位。持久化的偏移同样可以通过 清除localStorage['dsh-panel-dock-plugin:blocks:v1']归零。 - 左栏锚点是「位置 + 三条校验」,不是语义钩子 —— 这是一条未消除的残差。
出厂布局里没有可用的语义钩子:
data-slot在真实layout-client.js里零命中,DocumentTitle兄弟节点不渲染 DOM,所以「frame 的第一个元素子节点就是左栏」目前只能按位置取 (src/client/blocks.tsx:443-455:注释 +directChild(frame.firstElementChild)那三行内联判断)。 代码因此补了三条佐证:必须是直接子节点、不得是宿主控件(overlay 层 / leading 座)、不得像宿主的 列宽把手(cursor: col-resize或 ≤20px 的绝对定位条,blocks.tsx:433-441)。任一不成立就降级, 不会把几何写到宿主节点上。但「将来宿主在左栏之前再插一个元素」这种变化,如果新元素同时满足 三条校验,仍会被当成左栏——本轮只记录,不消除(没有稳定的选择器可依赖,写死类名/data-*又会 违反「不依赖哈希类名」的纪律)。
已验证 / 尚未验证 / 结构上不可能
- 已离线验证:
pnpm run gates28 条静态门禁(其中 10 条是 v0.2.0 新增的区块门禁, 10 条全部做过变异测试:把被测缺陷真的种回代码,确认对应门禁由 PASS 翻成 FAIL,再还原; 第二轮独立复验又指出 #27/#28 各有一个盲点,task-12 已把断言收紧到「声明了box-sizing就必须真的写它」与「剪枝函数体内必须按被指名的元素判断」,并补了 9 组回注(9/9 全杀, 且每条都命中指定的那条门禁或检查);第三轮复验指出其中一个盲点没有被真正关掉——保留isConnected与失效调用的提前return(if (true) return)仍能骗过纯文本断言,task-14 据此再加一条结构断言「剪枝函数体内不得出现return」(该形态由存活变 killed,回注组 C10), 并把同形的if (true) continue明确记为文本门禁的已知盲区(它保留全部 token,由 jsdom 的 N-4 行为检查杀掉,回注组 C11)——静态门禁只断言 token 与结构,行为面始终由client-render.mjs与真实浏览器固定;终审又指出 #28 那条关于noteDomMutations(records)的断言是纯文本子串, 把整条调用注释掉仍能通过(F-04),task-16 把它改成「去注释 + 行首锚定」的真实调用行断言 (回注组 C12:注释掉调用即 FAIL)。这条断言仍然是文本判据(F-3X):它封的是一个 token, 既不是行为覆盖,也会误杀将来某个合法的提前return写法——不要把「门禁变强」读成 「#28 已覆盖该函数的行为」。同一类盲区的通式:只要断言读的是源码文本, 「保留 token、却删掉或注释掉真正产生副作用的调用/分支」就能骗过它 (if (true) continue、if (false) …、掏空函数体但留着调用点、注释掉调用), 能杀它们的只有行为层(client-render.mjs的 N-3/N-4 与 task-16 新增的 F-02/F-03 组)与真浏览器;task-12/task-14/task-16 的回注合计 17 组(17/17 全杀); 分组与结果见docs/acceptance-v0.2.0.md);node scripts/test/client-render.mjs168 条、node scripts/test/client-runtime.mjs50 条检查全绿。jsdom 里用合成 DOM 覆盖锚点解析、写入集合、「右栏不写 overflow」、24px 吸附归零、复位只删自有属性、 持久化往返、四类锚点缺失降级、MutationObserver重建后重放、dispose后 style 快照与加载前逐字相等, 以及几何纪律(relative 写纯位移、fixed 写绝对坐标、box-sizing转换、请求 rect == 渲染 rect ±0.5px)、 A7 包含块审计的降级、宿主重建后的不累积、指针捕获丢失(lostpointercapture)时结束手势 但保留实时偏移,以及 task-16 新增的三组行为检查:拖动期间列内出现新的绝对定位后代 不得中途重审(区块停在指针下),手势结束后按新树重审并降级;一个永不投递的 observer 下同步守卫仍然拒绝;放弃手势(lostpointercapture)后自渲染监听全部释放、而拖动期的 把手自渲染仍然生效。 离线阶段抓到并修掉两个只在真实 CSSOM 语义下才暴露的坑:style.setProperty只认连字符属性名 (zIndex/boxSizing/minHeight/transition*走驼峰会被静默忽略)、transform-style的初始值flat曾被误判成包含块制造者(会让 composer 永远不可拖)。 - 尚未在真实浏览器验证:本产物尚未在真实 DSH 桌面会话里加载过。桌面 Electron 渲染进程
不开放
--remote-debugging-port,所以没有在真实AppFrame上手动拖过一次。 真实 Chromium(非 jsdom)取证已有五轮:第一轮用真实layout-client.js的 AppFrame CSS 搭合成夹具、在真实 Chrome 里驱动真实输入, 量出 v0.2.0 首版的几何是错的(三列把自身原点加了两次、composer 的width在position:fixed之后才量而冻成收缩宽度、naturalBlockBox对fixed元素清top会读到静态位置), 本文件的几何规则就是照那批测量改写的;第二轮(task-9,独立复验)跑在改完几何规则之后的 产物上,判 passed 14 / failed 3 —— 几何错位全部不复现,但抓到 composer 在box-sizing转换后少 2px(宿主自带的height:96px被 border-box 重新解释)、列内后出现的绝对定位 后代不被审计、以及审计拒绝无法随 DOM 变更撤销三条。task-10 已按那三条修掉并补了 jsdom 回归;第三轮(task-11,同一独立复验者)跑在修完之后的产物上,三条全部判为已修 (composer 高 98 且四角一致、seat 707/98、先拖 composer 与先拖中列落点相同且往返逐字一致、 观察者/拖动前/拖动中三条路径都会重审、删除后代后拒绝自行撤销),另留三条 LOW 级残留: 程序化写偏移(不经手势)复用陈旧的 A7 快照、#27 整条删掉box-sizing写入仍能通过、 剪枝函数被掏空仍能通过。task-12 已补齐这三条并各自补了回归; 第四轮(task-13,独立复验)跑在补齐之后的产物上:程序化偏移的同步重审在「不等待观察者」 「先 settle」「惰性 observer」三条等价路径上都成立(prog-1434/34),#27 那条也确认由存活变 killed; 但同轮抓到两处记录不实——if (true) return形态当时仍能骗过 #28(F-3V), 以及「写出任何样式之前就已拒绝」的说法不成立(F-3U)。 第五轮(task-15,同一独立复验者)跑在 task-14 的措辞与门禁更正之后:证实 task-14 的改动是 纯注释(lib/client.js逐字不变、重建夹具与第四轮字节相同、无断言被放宽),M28c(提前return)由存活变 killed(27/28,命中新文案)、M28c2仍 killed、control 28/28, 并实测确认残留盲区if (true) continue仍 28/28 存活、由 jsdom 的 2 条 N-4 行为检查杀掉;prog-08127/127、prog-1434/34、prog-1331/31、client-runtime50/50、jsdom 161/161、.verify/client.mjs39/39、色板判别 12/12、verify_plugin.py11/11、bundle 幂等复现冻结值。 task-16 之后的产物尚未再上真 Chromium:本轮动了行为(F-02 拖动期监听成对释放、F-03 手势期间推迟重审) 与门禁 #28(F-04 改成锚定真正的调用行),这一环由下一轮独立复验(task-17)完成,本文件不预先认领。 两条 jsdom 证明不了、只能靠真机确认的点: ①-webkit-app-region不在 jsdom 的 cssstyle 白名单里 —— 只能通过el.style.WebkitAppRegion(大写 W)读回,getAttribute('style')里永远不出现,所以「把手真的不会被当成窗口拖动条」 必须有真 Chromium 的 computed style 证据;②CSSOM 会重新序列化别人预置的style属性(补尾分隔符), 逐字比较必须先归一化尾部;。未在实机确认的其它项:列内既有absolute/fixed后代 在包含块变化后的实际 rect(代码有一次性运行时审计与降级,已由 jsdom 合成样本覆盖, 但缺真实面板样本);宿主那两枚position: fixed折叠按钮的实际视觉偏移(见上文「已知边界」);[data-conversation-composer-overlay]的写入者不明; Grid 自动放置与display:contents的行为是规范推导(用 relative 已规避)。 本轮(task-10)新增的三条「只在合成样本下验证过」的项:A7 审计拒绝的撤销时机(DOM 变更即 重新审计、被指名的元素离开该列或离开文档即撤销;jsdom 用可变 rect 桩覆盖「插入 positioned 后代 → 拒绝」「删除后代 → 恢复」「整列replaceChild→ 恢复」三条路径,真实面板下的触发频率与手感没量过), task-12 又补了第四条路径:不经手势的程序化偏移写入先在当帧对当前 DOM 重审(有缓存、列在本地且 无手势时才做),jsdom 里连同「完全不等待观察者投递」的孪生用例一起断言;同一函数里真实面板下 一次拖动的触发频率与「拒绝晚一帧」的可感知程度都没量过(后者已在代码注释与门禁里写成 「拒绝由下一帧的 pass 写出」,不再有「同一任务内完成」的说法)。拒绝的时机只用可断言的说法: A7 审计必然要先把几何写出去才量得到后代是否改锚,所以拒绝不是「写出任何样式之前就已成立」, 可断言的是「写、比较、收回都在同一趟pass()内完成(不依赖观察者投递),且净残留为零」—— 真实 Chromium 复验数到的瞬时写入(不等观察者 22 次 / 先 settle 11 次)在回调送达时已全部为null; N-2 的顺序无关性(先拖 composer 再拖中列,与先拖中列再拖 composer,落点断言为同一绝对 rect; 真实 AppFrame 下 seat 的 sticky 盒是否与合成桩一致没量过);describeElement()写进用户可见 拒绝文案的名字(#id、前两个 class、最多 3 个data-*、3 级祖先路径)在真实面板下长什么样没看过。 两条设计注记,本轮刻意不改:composer 浮起后z-index:10低于宿主自己的 overlay(20), 所以宿主弹层仍会盖住它 —— 升到 12–19 是 design.md 划给宿主的禁区,改层序需要重新设计, 此处只记录建议;另有一次审计读数为z-index: 10px,复读与复现均未再现(写值恒为'10'), 作为未复现观察记录。 task-16 新增的三组行为(F-02 手势监听成对释放、F-03 手势期间推迟重审、F-04 的调用行断言) 同样只在 jsdom 合成样本下验证过:真实面板里「拖动期间宿主真的改了列内 DOM」的触发频率、 以及拖动期自渲染在真实事件频率下的渲染开销都没量过 —— 这三条要等下一轮真 Chromium 复验。 三条记录项(记录,不在本产物内修): ①(F-3Z)七条验收命令里的verify_plugin.py由 DSH 技能dsh-plugin-studio提供 (本机D:\dsh-home\skills\dsh-plugin-studio\scripts\verify_plugin.py),换个DSH_HOME或没装该技能的机器上 跑不到 —— 它是环境依赖门,不是本产物的能力,别把它读成产品保证; ②(F-07)dispose()只disconnect()观察者与摘除窗口监听,不清state.statuses、模块级lastWritten/positionedSeen/measure缓存与listeners集合;出厂路径是模块级单例 (blocks.tsx末尾createRuntime()单例 +index.tsx只挂测试接缝),所以真实运行下不构成 多实例串扰,仅测试接缝里反复建/销 runtime 会看到残留; ③(F-3X)见上文「已离线验证」里关于#28文本判据的一段。 - 结构上不可能:拖出窗口 / 跨显示器 —— frame 是
overflow:hidden,窗口外的位置拿不到。
v0.2.0 明确不做(对应 design.md §7)
区块缩放、真正的平铺与重排、接管官方栏宽拖拽、多显示器 / 拖出窗口、 区块 z-order 手动置顶、磁吸合并与 tab 分组、跨设备同步布局。
4. 能做到 / 做不到
做到了
- 一套独立于宿主原区域的浮动面板系统,真实注册在
shell.overlay(slot 目录原文:"Frame-wide floating layer, above every column and outside their scroll containers.")。 - 浮层本身是 click-through 的(
pointer-events: none),面板自身pointer-events: auto, 所以不会挡住底下的界面。 - 拖动 / 缩放 / 折叠 / 显隐 / 持久化 / 复位全部是真实逻辑,含视口 clamp 边界处理。
- 面板内容读取宿主真实状态(
useWorkspaces快照),不是占位文字。
做不到(平台限制,不假装实现)
- 无法把
sidebar/main/rightbar的既有内容整体搬进浮动面板。sidebar/rightbar在 slot catalog 中标记为single+replaceRisk: shadows-shipped-ui, 一旦第三方插件注册进去就会替换掉官方实现,等于让整个界面消失;main是keyed, 只有已被占用的 key(conversation、plugins)才会替换占用者。 本插件因此刻意没有注册root、sidebar、sidebar.workspaces、sidebar.settings等任何single槽位,也没有抢main的conversation/plugins键 —— 它只在main上新增了 自己的 key(panel-dock-toggles)。这个新增是必需的:sidebar.panellist的图标 id 必须寻址到 一个同名的mainkey,否则点图标时ctx.layout.selectPanel(id)会抛错并保留当前面板。pnpm run gates会静态校验这两点(不注册 single 槽位 + key 成对且不抢占用者)。 - 因此「像 Photoshop 那样把已有面板拆下来重新排布」在当前 DSH 插件 API 下不可实现。
可以实现的上限是:新增可浮动面板 + 用
sidebar.panellist提供开关图标。 - 面板之间没有 docking/吸附与拖拽合并(截图里的 Photoshop 停靠行为)。当前是自由浮动 + 整体复位;拖拽重排、磁吸停靠属于未实现范围。
- 未做:面板 z-order 手动置顶、多显示器坐标、面板分组为 tab、跨设备同步布局。
- 投影(box-shadow)颜色是固定的半透明黑,不随主题走。 原因:DSH 的 alias 清单里
没有 shadow / elevation token(
client/Theme.listTokens只有 bg、border、brand、label、 state、sidebar-fill 六类),而明暗两套主题都需要投影,所以三处 elevation 投影 (SHADOW:面板常态0 6px 20px rgba(0,0,0,0.16)、hover0 12px 32px rgba(0,0,0,0.22)、 工具条0 6px 18px rgba(0,0,0,0.18))是本产物中唯一非 token 驱动的颜色。 这不影响可读性(暗色下投影只变弱,不会让文字/边框失效),但确实不算「完全主题化」。 该例外被pnpm run gates显式枚举校验:除这三条投影和var(, )的回退值 之外,产物里不允许再出现任何颜色字面量。
5. 三个入口(为什么是这三个)
| Slot | kind / scope | 为什么用它 |
|---|---|---|
shell.overlay | list / root | 帧级浮层,位于所有列之上且在它们的滚动容器之外 —— 浮动面板的唯一正确落点。replaceRisk: none,用新 id 追加即可,不会顶掉已有的 10 个 occupant。 |
main | keyed / root | 开关图标必须有一个页面。 sidebar.panellist 的 id 寻址 main keyed slot 的同名 key;ctx.layout.selectPanel(id) 遇到缺失的 key 会抛错并保留当前选中态。官方 ui-schedule({name:'main',key:PANEL_ID} + {name:'sidebar.panellist',id:PANEL_ID})就是这么成对注册的。本插件只新增 key panel-dock-toggles,不碰 conversation / plugins。 |
sidebar.panellist | list / root | 全局面板图标栏,按 list id 寻址。用来放面板开关图标,符合该 slot 的设计意图("Global panel icons")。replaceRisk: none。 |
注册都包在 ctx.effect(...) 里,卸载插件时三个入口一起 dispose。
shell.overlay的条目不会收到ctx:service 在apply闭包里,组件只拿到标准 props。 本插件因此把面板状态放在 client 模块作用域的 store(同一个 client 模块注册三个入口, 所以是同一个实例),组件通过useSyncExternalStore订阅。
浮层的层叠与边界(静态可定论,不需要实机验证):
shell.overlay由官方@deepseek-ai/dsh-client-ui-layout渲染成AppFrame内的{overlays},其规则为position:absolute; inset:0; z-index:20; pointer-events:none,并且overlayLayer > * { pointer-events:auto }。z-index 20 不是「很大的数」,它只在 该层自己的 stacking context 里比较;它之所以盖住三列,是因为这个层位于 frame 的 stacking context 内、并且是 frame 的倒数第二个子元素(三列都没有 z-index)。 本插件在根节点写 inlinepointer-events:none、在面板上写auto—— inline 样式覆盖上面 那条>*规则,这正是「浮层 click-through、面板本身可点」成立的机制。 另外 frame 是overflow:hidden(Windows 下还带padding-top: var(--dsh-windows-titlebar-height)),所以面板在结构上不可能跑到窗口之外: 本插件的clampPanel按视口夹取与之一致,同时也是「多显示器 / 跨窗口面板」做不到的根因。