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 界面布局、新增独立浮动面板以提升操作效率的用户。

パッケージ
dsh-panel-dock-plugin
互換性
未検証
バージョン
0.2.0
ライセンス
MIT
最終更新
2026/10/03

インストール

$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 个)

  1. 会话与工作区(overview)— 通过 slot 标准 props 读取真实数据: 工作区数量、会话总数、每个工作区的会话数。不请求任何远端接口。
  2. 插件与环境(environment)— 显示插件 id / 版本 / 三个入口 id / 当前可见面板数, 并能探测 Node half 的健康路由 GET /dsh-panel-dock-plugin/health。
  3. 便签(notes)— 自动保存到 localStorage(key dsh-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 槽位」这条约束下能做到的另一种形态。

怎么用

  1. 点侧栏「面板坞」图标打开控制页(sidebar.panellist → main,key panel-dock-toggles)。
  2. 「开始拖动编辑」打开编辑模式(默认关,持久化在 localStorage key dsh-panel-dock-plugin:blocks:v1)。开启后每个可拖动区块的上边缘出现一个把手。
  3. 拖把手移动区块;拖回原位 24px 以内会完全清除本插件写过的属性,即逐字复原。 编辑模式开着也不会「顺手把原位重写一遍」——只有真正带偏移的区块才有内联几何, 偏移为 0 的区块(刚吸附归位、刚全部复位、以及从未拖过的)一律不写任何属性。
  4. 「全部复位」或每行的单独复位只删本插件写过的属性(绝不 removeAttribute('style'), 也绝不清空 / 覆盖别人的内联样式)。
  5. 控制页显示 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 gates 28 条静态门禁(其中 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.mjs 168 条、 node scripts/test/client-runtime.mjs 50 条检查全绿。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-14 34/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-08 127/127、prog-14 34/34、prog-13 31/31、client-runtime 50/50、jsdom 161/161、 .verify/client.mjs 39/39、色板判别 12/12、verify_plugin.py 11/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 必须寻址到 一个同名的 main key,否则点图标时 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)、hover 0 12px 32px rgba(0,0,0,0.22)、 工具条 0 6px 18px rgba(0,0,0,0.18))是本产物中唯一非 token 驱动的颜色。 这不影响可读性(暗色下投影只变弱,不会让文字/边框失效),但确实不算「完全主题化」。 该例外被 pnpm run gates 显式枚举校验:除这三条投影和 var(, ) 的回退值 之外,产物里不允许再出现任何颜色字面量。

5. 三个入口(为什么是这三个)

Slotkind / scope为什么用它
shell.overlaylist / root帧级浮层,位于所有列之上且在它们的滚动容器之外 —— 浮动面板的唯一正确落点。replaceRisk: none,用新 id 追加即可,不会顶掉已有的 10 个 occupant。
mainkeyed / 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.panellistlist / 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)。 本插件在根节点写 inline pointer-events:none、在面板上写 auto —— inline 样式覆盖上面 那条 >* 规则,这正是「浮层 click-through、面板本身可点」成立的机制。 另外 frame 是 overflow:hidden(Windows 下还带 padding-top: var(--dsh-windows-titlebar-height)),所以面板在结构上不可能跑到窗口之外: 本插件的 clampPanel 按视口夹取与之一致,同时也是「多显示器 / 跨窗口面板」做不到的根因。