dsh-panellist
Panellist manager: reorder the entries on the left rail, rename them, replace their icons, hide them, and fold several into a Group with a flyout menu, from a Settings tab.
Install
dsh plugin --profile web add dsh-panellist
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-panellist
[English](README.md) | 简体中文
DSH(DeepSeek Harness)客户端「面板列表」的管理插件。
面板列表(panellist)是会话界面左侧那一列由各个插件贡献的全局入口——插件、自动化任务、技能中心、记忆系统、SSH、MCP 连接器……它们来自不同的插件,顺序由各自声明,图标与名称也由各自决定。
本插件让用户接管这件事:哪些页签出现在轨道上、以什么顺序出现、叫什么名字、用什么图标,以及把若干页签收进一个分组。所有配置都在「设置」里的一项新页签内完成,改动即时生效。
状态:已实现。这套 README(中英各一份)就是完整的规范——不再有单独的设计文档:行为见 §2–§3,配置与接口见 §4,兼容契约见 §5,安装见 §6,实现方式见 §7。配置的权威形状是 [docs/schema/config.schema.json](docs/schema/config.schema.json)。
与最初设计的三处细化(都是实现期间发现的,已回填本文):分组的轨道 id 是dsh-panellist:<groupId>;未配置页签始终列在整条轨道里,但只有序列非空(即你做过结构性改动)时才写order;/panellist/api/reveal这个端点是为了实现「打开配置文件」而新增的。
截图
设置 → 面板列表 · 轨道上的分组菜单
 · 
1. 为什么需要它
DSH 的面板列表是只读投影:外壳枚举 sidebar.panellist 槽位里的注册项,按每一项注册时自报的顺序排序渲染。官方没有提供任何改写他人注册的 API:
- 顺序只读——插件只能决定自己那一项的
order; - 没有「分组」概念——一行就是一个页签;
- 图标由贡献者提供的组件决定,名称由贡献者的
label决定; - 「设置」导航页签的图标只能从内核内置的一份清单里挑。
因此本插件必须同时用两个杠杆,并且把 DOM 介入压到最小:
1. 注册表改写——在运行时改写注册项上的顺序、名称与图标组件(注册项对象未被冻结,且由槽位注册表原样交回,所以事后写入对下一次投影可见);本插件的改动不触碰任何插件的页面本身。
2. 最小 DOM/CSS 重投影——只有一件事无法靠注册表完成:把某一行从轨道上「拿掉」。分组行本身也是注册项(由本插件注册),因此它的行框、高亮、提示与收起态行为依旧由外壳负责;弹出菜单是覆盖层,不改动原生行。
2. 行为规范(已确认)
2.1 管理对象
- 面板列表里的所有注册项一视同仁:内核自带项(插件、自动化任务、技能中心、记忆系统、SSH、MCP 连接器)与第三方插件注册的项都可被管理。
- 本插件自己的设置页签不在面板列表内,因此不参与管理。
- 真实页签 id 在运行时发现,不硬编码;本文档示例里的 id 仅是示意。
- 轨道自带的「更多」行也会列出,虽然它由外壳渲染、并非 Panellist 贡献者(§2.12)。
2.2 序列模型
轨道序列是一串有序项,每一项要么是单个页签,要么是一个分组:
- 每个页签在整条序列中恰好出现一次(序列是对页签集合的划分,不重复);
- 分组自身占据序列里的一个位置,并含有一个有序的页签子集;
- 只允许一层嵌套(分组内不能再放分组);
- 空分组非法;只含 1 项的允许,但管理面板会提示。
设计讨论里确认过的写法按此模型解释如下(G{…} = 一个分组):
写法 · 解释
a c b d e · [a][c][b][d][e]
a b c · [a][b][c]
a 下拉(b c d e) · [a][G{b,c,d,e}]
a 下拉(b d e c) · [a][G{b,d,e,c}]
a 下拉(b e c)d · [a][G{b,e,c}][d]
下拉(a b c)d e · [G{a,b,c}][d][e]
2.3 显示 / 隐藏
- 只有两态:显示 / 隐藏(没有「禁用」第三态)。
- 隐藏 = 不再出现在轨道上,但页签本身依然存在、依然可用(可从管理面板选中、可被其它插件或快捷键激活),它所属插件的功能不受影响。
- 例外:被隐藏的页签若正处于激活态,它那一行临时显示——避免出现「人还在这个页面,轨道上却没有入口」。
- 分组内的成员也支持隐藏,含义换成「不出现在分组菜单里」:成员的轨道行本来就被分组抑制掉了,所以对成员而言隐藏只作用于下拉菜单。成员仍留在组内——计数、排序、组头高亮都照旧看见它。
- 已缺失的成员自动不出现在分组菜单里(注册项没了,菜单里留一行禁用的「已缺失」只是占地方,用户也无从操作);它仍留在配置里、仍列在设置页的「已缺失的面板」里,等你清理。
2.4 名称覆盖
- 纯显示层覆盖,只影响三处:轨道行的文字、收起状态的提示文字、无障碍名称。
- 不改动 DSH 其它位置(设置、命令面板等)中的同名显示,也不改变贡献者声明的原始名称。
- 首版为单字符串,对所有语言生效。
2.5 图标覆盖
三种来源,可随时切回原图标:
1. 保留原图标(默认);
2. 内置图标集——本插件自带的 24 个内联矢量图形(零依赖,离线可用)。它们按轨道上邻居们所用的规范绘制,即 Icon*OutlineRegular 那一档:viewBox="0 0 16 16"、fill="none"、stroke="currentColor"、线宽 1px、几何落在 16 网格的半像素坐标上并大致收在 2…14 之内、圆头圆角、aria-hidden、无 id 与外部引用。其中三个直接采用外壳自己那几颗默认图标的美术数据,因此选上去和它要替代的项看起来完全一样:puzzle 是插件风车(IconPluginPinwheelOutline)、clock 是时钟(IconClockOutline)、more 是省略号(IconEllipsisOutline)。这套图形在 16px 下与上述官方美术并排比对,并逐个量过着墨包围盒;
3. 用户上传图标——粘贴 SVG 源码;
- 白名单净化:移除
script、foreignObject、事件属性、外部引用(href/xlink:href指向外部的用例); - 单个 SVG 上限 64 KB;
- 内联渲染(不是
<img>),因此能随主题变色。
设置导航那一行有它自己专用的图形(一个「面板列表」,线宽取那一列所用的 Icon*OutlineMedium 1.3)——见 §3。
2.6 分组与组头
- 组头有独立的图标与名称,默认预填「组内第一项」的图标与名称(常用名如「更多」),用户可改。没设名称时,组头文字在渲染时按「第一个活着的成员」解析——配置里不烘焙字面量。
- 组头整行可点,点击即展开分组菜单;整行点击一律用于展开菜单,不会跳转页面。菜单里只列该分组的成员:标题行里原本还有一个指向分组自己那一页的「概览」入口,已按用户反馈删除。那一页仍保留为分组
main的兜底页(这样程序化选中分组 id 时不会报错),但不再有任何自己的入口。 - 组内任一项处于激活态时,组头高亮,用户不展开菜单也能看出当前页面在哪个组里。
- 拖拽新建分组时,分组插到「最靠上的那个成员」原来的位置,即分组「长在」它成员的原地。这条规则由模型保证(
createGroup以「最靠上成员上方那一行」的身份为锚点),因此与具体手势无关。
2.7 分组菜单
- 点击展开(悬停不展开,避免误触)。
- 只列用户能操作的成员:隐藏的(§2.3)与已缺失的都不出现;组内成员全部隐藏时,菜单写明这一点,而不是给一张空卡片。
- 键盘可达:
Enter/Space展开、Esc关闭、上下方向键在组内移动。 - 侧栏收起成 56 px 轨道时,菜单一律向右弹出(flyout)。
- 组内项数超过可视高度时菜单可滚动;首版不做组内搜索框。
2.8 未配置项与孤儿
- 新装的插件注册了一个你还没动过的页签 → 它直接出现在序列里(在已配置项之后,按它自身的顺序),不需要先「纳入」它。
- 某个页签的贡献者被卸载、而配置里还留着它 → 配置保留,管理面板标注「已缺失」,并提供「清理孤儿」按钮。
- 在你做结构性改动之前(拖动、建组/解散分组),本插件不写任何顺序:轨道保持贡献者自己的顺序。改名、换图标、隐藏只覆盖那一项、不动顺序——这正是「不碰你没让我碰的东西」的落点。
2.9 生效方式
配置改动即时生效:不需要重启 DSH,不需要重载任何插件,也不需要重新加载页面。这对每一次改动都成立,包括那些「什么都不用写」的改动:改写环节会比较一份「轨道渲染所依赖的一切」(顺序、名称、图标、显示/隐藏)的摘要,只要它变了就重新投影——所以第二次换图标、第二次改名,和第一次一样立刻上轨道。
2.10 接管范围
管理面板里从第一次打开起就列出整条轨道:不需要先把某个页签「纳入」才能碰它,也不存在一份「还没接管」的清单。(中途曾有过「未配置清单 + 逐项接管 / 恢复原生」的设计,已按用户反馈删除——它们只是让人多点一步;后来那条「还没接管顺序」的说明横幅与「以当前轨道顺序作为起点」按钮也一并删掉了:列表本身就是轨道,这道门槛不可见。)
真正仍然成立的「最小干预」性质落在写入上,而不是界面上:
- 配置里的
sequence是部分权威:只记你实际改过的东西。 sequence为空时不写任何order(新装即如此,纯 no-op),未配置项的名字/图标/隐藏一概不碰;它们保持贡献者自己的顺序。- 你第一次做结构性改动(拖动、合并分组)时,当前顺序会被写成显式序列;此后未配置项仍按各自的原生顺序列在已配置块之后(§2.8)。
实现见 src/shared/model.ts:rows 永远是整条轨道(未配置的行 order 为 null,即「不改」),而 reordering 决定是否真的写 order。
2.11 每页只允许一个挂载
外壳可能在同一页里把客户端 bundle 应用两次(文件变了会重新求值,HMR 也会再应用一次)。两个控制器同时操作同一份账本会互相打架:后来者会把前一个装上去的图标包装当成贡献者自己的组件,双方还都往同一批注册项上写。因此 apply 在构建自己的挂载之前先退役上一个(src/client/mount.ts),并且本插件装上去的每个包装都是可识别的——遇到还挂着包装的注册项会先拆开再接管(§7.2)。
2.12 外壳自带的「更多」行
轨道最后一行「更多」不是 Panellist 的注册项:它由 DSH 自己渲染(来自 sidebar.panellist.actions 槽位),点开的侧边菜单被 portal 到 document.body。所以它不是 sidebar.panellist 的条目——没有 order、没有名称 thunk、没有图标组件可以让本插件改写(§7.2)。但它同样是那个 nav 里的普通 flex 子节点,因此仍然出现在序列里:
- 位置。 该行(
shell:more)在序列里占一个位置,可以拖拽或上移/下移。改写环节按 DOM 顺序写轨道,所以它在序列里的下标就说明了它前面有多少行;注入的规则给「更多」锚点order: 1、给它所在位置及其后的每一行order: 2,用 flex 把它精确停在那里。不改动任何其他行选择器;当它本来就在最后时,一条规则也不写。 - 隐藏。 隐藏它就等于去掉面板菜单的入口,所以只在你不用那个菜单时才隐藏。
按设计它不能被改名、换图标或并入分组:名称、图标与行为都属于外壳,而且它没有可打开的页面,既不能作为分组的开头,也不能被折进分组——它的外观编辑会直接说明这一点,而不是给出那些控件。还有一个注意点:移动的是它画出来的位置,键盘顺序并不跟随,Tab 仍按 DOM 位置最后到达它。(其他行不同:它们的顺序写进注册对象本身,外壳按新顺序渲染,键盘顺序也随之改变。)
旧版本靠探测处理,不比较版本号。 探测 5(§5)如实报告轨道里到底有什么,只有两种结果表示「没有这一行」:面板列表里除了行以外什么都没有,以及列表里那个非行的子节点不再匹配。这两种情况下该行不出现在序列里,于是不支持的版本上不会留下一条死条目。如果这个配置里已经写过它——存过位置,或点过隐藏——该行仍会列出,并带一个「当前版本无此行」标记,因为直接丢掉等于把它交给孤儿清理。仅仅「轨道没挂载」(侧边栏收起)属于未知而不是「没有」,未知时保留该行。
3. 设置页签
在「设置」对话框左侧新增一项:
- 名称:面板列表;位置:排在设置导航靠后;
- 图标:自己专用的图形——一个「面板列表」,线宽取那一列统一的
Icon*OutlineMedium(1.3),以currentColor蒙版画在外壳原本的兜底齿轮位置。设置导航的契约里没有图标字段,所以这一行按可见文本定位并打标记;探测失败则保
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::4,最后更新 2026-10-26