🌐 中文 · English
# dsh-unarchived-watch
> *「会话攒到第 11 个,侧边栏底部会替你亮一盏黄灯。」*

[](LICENSE)

**未归档会话积压的仪表灯:越线就亮 —— 它不归档、不删除、不落盘;唯一会写的,是你在设置页设的那个阈值。**
[它解决什么问题](#它解决什么问题) · [效果示例](#效果示例) · [快速开始](#快速开始) · [触发方式](#触发方式) · [它和同类有什么不同](#它和同类有什么不同) · [安全边界](#安全边界) · [文件结构](#文件结构) · [验证与测试](#验证与测试)
---
## 它解决什么问题
DSH 里的归档是个**手动动作**,而"该归档了"这件事没有任何人提醒你。
于是它总是这样发生:你连着几周只顾开新会话,直到某天打开侧边栏,发现要滚很久才能找到上周那条 —— 然后花半小时翻捡、归档、后悔没早点整理。
这个插件把"该清理了"变成一个**被动触发的信号**:未归档的普通会话一旦超过你设的阈值(默认 10),侧边栏底部就会亮起一个黄色警告三角,角标上是当前数量;设置页的阈值配置行里也带着同一个数。
**它只负责告诉你,不替你动手。** 归档动作仍然在你熟悉的侧边栏行内菜单里;插件不接管任何状态。
## 效果示例
下面是本机实例(DSH Desktop,2026-10-02 11:16)的**真实回放**,不是编的样例:
```text
会话列表 125 行
其中普通会话 63 个 (排除 48 个子代理子会话 + 14 个空白新建会话)
其中已归档 34 个
→ 未归档普通会话 29 个
→ 阈值 10
→ 侧边栏 亮灯,角标 29
```
同一场排查里我前后读了两次,归档集从 11 涨到 38(这段时间有 27 个会话被归档),计数随之从 **54 掉到 29** —— 它跟着你的清理实时变化,不需要刷新。
数字会跟着你的清理走:写这份文档的十几分钟里,同一个实例的归档集从 11 涨到 59,计数从 54 一路掉到 9(这时灯就灭了)。所以下面这句是**那个时刻**的真实读数,你自己跑 [scripts/replay-count.mjs](scripts/replay-count.mjs) 得到的一定是新的。
悬停图标会显示一句话,屏幕阅读器读到的是同一句:
```text
未归档会话 29 个,已超过阈值 10 个
```
## 快速开始
两条路,任选一条:
```sh
# 1) npm 包
dsh plugin --profile web add dsh-unarchived-watch
# 2) 直接指向 GitHub 仓库(不需要构建授权:lib/ 随仓库提交)
dsh plugin --profile web add github:d0ublecl1ck/dsh-unarchived-watch
```
装完刷新窗口(Cmd+R)。装完第一句话可以直接对 Agent 说:
```text
现在有多少未归档会话?超阈值的时候把阈值调成 20。
```
阈值也能手写进 profile 的 `cordis.patch.yml`(注意 **row id 就是设置命名空间**):
```yaml
- id: unarchived-watch
name: dsh-unarchived-watch
config:
threshold: 20
```
## 触发方式
- "我有多少会话没归档?"
- "侧边栏底部那个黄三角是什么?"
- "未归档有点多了,提醒我一下"
- "阈值改成 20"
- "把未归档会话提醒关掉"(把阈值调到很大,或禁用 row)
## 它会交付什么
| 位置 | 内容 | 什么时候出现 |
|---|---|---|
| 侧边栏底部 | 黄色警告三角 + 数量角标 | 未归档普通会话数 **严格大于** 阈值 |
| 悬停 / 键盘聚焦 | 文本气泡:`未归档会话 {count} 个,已超过阈值 {threshold} 个` | 图标可见时 |
| Settings → 通用 | 「未归档会话提醒」配置行:输入框 + 当前数量 | Host 服务了本插件配置时 |
## 它和同类有什么不同
DSH 生态里已经有几个**归档管理器**(做得都很好)。这个插件的定位刻意不同:
| 维度 | 归档管理器(如 dsh-archive-manager / dsh-archived-conversations / dsh-session-sweeper) | dsh-unarchived-watch |
|---|---|---|
| 回答的问题 | "已归档的在哪里、怎么恢复/删除" | "什么时候该归档了" |
| 入口 | 设置页整页列表 / 侧边栏列表 | 侧边栏底部一盏灯 |
| 触发时机 | 你想起来去看 | 越线自动出现 |
| 写操作 | 会改归档状态(部分会删除会话) | **零写操作**,全程只读快照 |
| 状态归属 | 自己持有一份列表/索引 | 不持有任何状态,计数现场算 |
如果你要的是"管理归档",请用那三个;如果你要的是"别让我攒到失控",用这个。
## 安全边界
- **不写会话或归档状态**:没有 `archiveSession` / `unarchiveSession` / 删除调用;归档仍走官方侧边栏的行内动作。插件唯一的写入是你自己改的阈值 —— 它经 Host settings 存进 profile 的 `cordis.patch.yml`,那是配置,不是会话状态。
- **不联网**:客户端只读 shell 已经发布的快照(`useSessions` / `useWorkspaces`),不发起任何网络请求。
- **不落盘**:插件自己不建文件、不写 `localStorage`、不注册持久化 domain。
- **不会静默误算**:徽标与设置页配置行共用 `src/count.ts` 一份实现,口径(只数普通会话)由单测钉死。
- **什么时候停手**:Host 没有服务本插件的配置命名空间时,配置行不注册(徽标照常,用默认阈值)——不会渲染一个改不动的控件。
## 文件结构
```text
src/index.ts host 半边:只导出 name / Config / apply(threshold 是 volatile 字段)
src/count.ts 纯计数口径(无框架依赖,单测直接打它)
src/client/index.ts 浏览器半边:注册徽标 / 配置行,注册文案与样式
src/client/WarningBadge.tsx 侧边栏底部的图标 + 数量角标 + 官方 Tooltip
src/client/ThresholdRow.tsx 设置页通用栏的阈值输入行
src/client/threshold.ts 把 config form 投影成可订阅的阈值
src/client/styles.ts 注入的 uw-* 样式(一次注入,fiber 卸载时移除)
scripts/build.mjs tsc + esbuild + __ModuleLoader__ 包装
scripts/check-release.mjs 发布前离线体检(清单 / 契约 / externals / 产物新鲜度)
test/ 22 个 node:test 用例(口径、模块契约、SSR 真渲染)
cordis.patch.yml bundle 层:插入 row id 为 unarchived-watch 的那一行
```
## 验证与测试
一条命令跑完全部门禁:
```sh
npm run verify # typecheck + build + 22 个单测 + check-release
```
- **单测 22/22**:`test/count.test.mjs`(口径与边界)、`test/client-contract.test.mjs`(加载真实 `lib/client.js` 验模块 id 与两次注册、且 `settings.section` 不再注册)、`test/client-render.test.mjs`(`react-dom/server` 真渲染徽标与配置行)。
- **发布前体检**:`npm run check-release` 会拦住"改了 `src/` 忘了 `npm run build`"、`export default` 折掉 `inject`、浏览器产物 require 了 platform seed 之外的模块、以及 tarball 会漏掉 `cordis.patch.yml` 这四类发布事故。
- **真实数据回放**:上面的数字是用本机实例跑出来的。[scripts/replay-count.mjs](scripts/replay-count.mjs) 就是当时用的脚本 —— 把实例的会话列表与 `storages/workspace.json` 喂给它,它在任何一台 DSH 实例上都能算出同样的口径(命令见 `CHANGELOG.md` 的 0.2.0 节)。
- **已知边界**:单测覆盖到"组件渲染出什么"为止;slot 在浏览器里的最终挂载与视觉效果需要刷新页面确认(页面里看不到图标时,先看 `document.head` 里有没有 `