Bundle
dsh-pin-color
DeepSeek Harness (DSH) Web GUI plugin: pin sidebar sessions to the top (within the session group or globally across the workspace) and give each session a tab color plus an emoji shown before its name. Dual-face (host + browser), persisted by the host, no DSH source changes.
- Source
- LuckVd
- License
- Apache-2.0
- Updated
- Updated 3 days ago
Readme
# 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`
迁移到新路径(旧文件保留不动)
## 安装
```sh
# 方式一(推荐):预构建 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 单测:
```sh
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.0Install
dsh plugin --profile web add github:LuckVd/dsh-pin-color
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-pin-color from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.