READMESource: main@4baa9ca0
dsh-pin-color
DeepSeek Harness (DSH) Web GUI 插件:给左侧会话加置顶、tab 颜色和 emoji。 Dual-face(host + browser)插件,不修改 DSH 源码,置顶/颜色/emoji 由 host 持久化,重启后保留。

功能
入口全部在会话行右侧的官方 "…" 菜单里(底部追加分隔线与两项,视觉与原菜单项一致):
可展开项带 ▸ 箭头,点开后像多级菜单一样在菜单项旁边展开选择面板,
官方主菜单保持打开(面板 z-index 在菜单之上),选完或点外部后一起收起:
- 🎨 会话颜色/表情:点开弹出会话颜色面板
- 色卡:10 色预设,选中的颜色生效在 会话 tab(对话区顶部标签页的文字+下划线) 以及侧边栏会话行(左侧色条 + 淡色底)
- 表情:24 个常用 emoji,选中后显示在会话名前面(侧边栏行与会话 tab 都是)
- 「无」清除颜色 / emoji,清除全部后条目自动移除
- 📌 置顶 / 取消置顶:未置顶的会话,点开弹层选择两个置顶级别;已置顶的会话
"…" 菜单直接变为一级「取消置顶」项(标注当前级别),点击一步取消,不用进二级菜单
- 置顶(本组顶部):把会话钉到它所在组/账户的会话列表最顶部
- 置顶(工作区全局顶部):把会话钉到整个工作区树的最顶部(所有组之前)
- 已置顶的会话不能再置顶、也不能切换置顶级别,只能取消:两种级别在 DOM 上的落位不同(组内列表 vs 整棵树),级别切换会把行挪出原本分组 (例如全局置顶后再本组置顶,行会跑到「未分组」区域),故已置顶即收起置顶入口
- 多会话置顶按置顶时间先后排列;行内会话名前缀 📌
- 持久化:host 半区把状态以 JSON 账本写到
$DSH_HOME/pin-color/state.json; 浏览器半区经同源 HTTP 路由读写,本地乐观更新 + 300ms 防抖落盘。 从旧名dsh-session-style升级时,首次启动自动把$DSH_HOME/session-style/state.json迁移到新路径(旧文件保留不动)
安装
# 方式一(推荐):预构建 tarball,免本地构建
dsh plugin --profile web add https://github.com/LuckVd/dsh-pin-color/releases/latest/download/dsh-pin-color.tgz
# 方式二:源码安装
git clone https://github.com/LuckVd/dsh-pin-color && cd dsh-pin-color
pnpm install && pnpm run build
dsh plugin --profile web add /path/to/dsh-pin-color
# 任一方式装完重启 dsh web 生效(浏览器强刷 Ctrl+Shift+R)
架构
| 面 | 实现 |
|---|---|
| Host | src/index.ts — StyleStore(原子写 JSON 账本)+ 两条 exact 路由 GET/PUT /dsh-pin-color/state(同源守卫,PUT 接受 {sessionId: 样式或 null};非 null 时整体替换该条目标样式,使"清除颜色/emoji"这类字段删除不会被旧字段合并回来;样式经 sanitizeStyle 校验,__proto__ 等键被拒绝) |
| Browser | src/client/index.ts — 纯 DOM 增强,零运行时依赖。MutationObserver 自愈扫描会话行([role="treeitem"]:not([aria-expanded]),排除搜索结果的 <button> 行)与对话 tab(button[role="tab"],按 displayTitle 匹配,兜底当前会话)。点击官方 "…" 按钮时(捕获阶段监听)记录目标会话,菜单 popup(div[role="menu"])挂载后把分隔线 + 两项克隆自官方菜单项模板追加进 popup;可展开项(带 ▸)点击后不关闭官方菜单,选择面板以多级菜单方式在菜单项右侧展开(视口放不下时翻到左侧)。官方菜单带 closeOnPointerLeave pointer-grace:指针离开其 React fiber 子树(触发按钮 + portal 列表)200ms 后自动收起——选择面板因此必须挂在 popup 的 DOM 内(而非 body,body 级浮层会被判为"菜单外"),并在面板与菜单之间的缝隙上放一块透明桥(.dss-popover-bridge,挂在 popup 内,覆盖滑行走廊):指针从菜单滑向面板的整个路径都落在 fiber 树内,主菜单保持打开。已置顶的会话则是一级「取消置顶」直接执行、随后收起主菜单。置顶按行自身定位(不依赖侧边栏容器):移动会话行的"可移动单元"(官方给每行套的 HoverCard 单子容器 span,或行本身),session 级钉到本组会话列表顶部、workspace 级钉到整棵树顶部,多会话按 pinAt 升序排列;取消置顶优先按记忆的原始位置恢复,否则按会话列表的顺序恢复到逻辑位置 |
冒烟测试 + host 单测:
pnpm install && pnpm build && pnpm test
tests/host.mjs— 直接加载lib/index.js:整体替换语义、null 删除、样式校验、__proto__防护、落盘/重载。tests/smoke.mjs— jsdom + 真实构建 bundle,fixture 镜像当前 DSH web 的会话树(HoverCard 包装 span + 组节点): 置顶按 pinAt 排序、菜单注入、"无"清除 emoji 且保留 pin/pinAt、置顶移动包装单元、取消置顶还原逻辑位置、 已置顶会话一级菜单直接取消(不下钻弹层、不能切换级别)、纯 pin 会话取消后条目删除。
已知限制
- 官方 "…" 菜单的 items 数组不可扩展(无公开 slot),插件在菜单打开后向 popup 追加克隆项;
依赖官方 popup 的 DOM 结构(
div[role="menu"]+button[role="menuitem"])与closeOnPointerLeave的 fiber 子树判定(选择面板借此随菜单保持打开), DSH 升级若改动该结构需要同步适配。 - 选择面板挂在官方 popup 内:菜单关闭(popup 卸载)时面板随之销毁,不会残留; 若菜单被 React 整节点重建(remount),已打开的面板会短暂消失,需要重新展开。
- 置顶采用 DOM 位移实现:官方列表在状态变化时会整体重渲染,插件在每次渲染后自愈恢复置顶位置, 极端情况下(会话频繁更新)可能出现轻微闪烁;每行 400ms 节流避免抖动循环。
- 会话身份按标题文本匹配(
displayTitle),同名会话取当前选中/最近更新的那个;个别同名场景可能错配。 - 颜色/emoji 作用于会话 tab 与侧边栏行;置顶只作用于侧边栏树,搜索结果列表保持官方顺序。
- 首次安装后需要重启
dsh web;浏览器半区在官方ui-workspace渲染出行之后才开始注入。
License
Apache-2.0
No comments yet. Be the first to write one.