dsh-group-status 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.en.md +36 -18
  3. package/README.md +39 -19
  4. package/lib/client.js +659 -300
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +22 -9
  7. package/lib/types/client/BadgeSettingsCard.d.ts +24 -0
  8. package/lib/types/client/BadgeSettingsCard.d.ts.map +1 -0
  9. package/lib/types/client/BadgeSettingsCard.js +73 -0
  10. package/lib/types/client/BadgeSettingsCard.js.map +1 -0
  11. package/lib/types/client/badge-field.d.ts +44 -0
  12. package/lib/types/client/badge-field.d.ts.map +1 -0
  13. package/lib/types/client/badge-field.js +77 -0
  14. package/lib/types/client/badge-field.js.map +1 -0
  15. package/lib/types/client/badge-labels.d.ts +92 -0
  16. package/lib/types/client/badge-labels.d.ts.map +1 -0
  17. package/lib/types/client/badge-labels.js +150 -0
  18. package/lib/types/client/badge-labels.js.map +1 -0
  19. package/lib/types/client/badge-settings.d.ts +31 -3
  20. package/lib/types/client/badge-settings.d.ts.map +1 -1
  21. package/lib/types/client/badge-settings.js +50 -6
  22. package/lib/types/client/badge-settings.js.map +1 -1
  23. package/lib/types/client/facet-store.d.ts +46 -0
  24. package/lib/types/client/facet-store.d.ts.map +1 -0
  25. package/lib/types/client/facet-store.js +123 -0
  26. package/lib/types/client/facet-store.js.map +1 -0
  27. package/lib/types/client/facet-visibility.d.ts +17 -0
  28. package/lib/types/client/facet-visibility.d.ts.map +1 -0
  29. package/lib/types/client/facet-visibility.js +18 -0
  30. package/lib/types/client/facet-visibility.js.map +1 -0
  31. package/lib/types/client/facets.d.ts +265 -0
  32. package/lib/types/client/facets.d.ts.map +1 -0
  33. package/lib/types/client/facets.js +209 -0
  34. package/lib/types/client/facets.js.map +1 -0
  35. package/lib/types/client/group-status.d.ts +97 -45
  36. package/lib/types/client/group-status.d.ts.map +1 -1
  37. package/lib/types/client/group-status.js +148 -85
  38. package/lib/types/client/group-status.js.map +1 -1
  39. package/lib/types/client/index.d.ts +25 -0
  40. package/lib/types/client/index.d.ts.map +1 -1
  41. package/lib/types/client/index.js +59 -1
  42. package/lib/types/client/index.js.map +1 -1
  43. package/lib/types/client/legacy/bridge.d.ts +30 -0
  44. package/lib/types/client/legacy/bridge.d.ts.map +1 -0
  45. package/lib/types/client/legacy/bridge.js +58 -0
  46. package/lib/types/client/legacy/bridge.js.map +1 -0
  47. package/lib/types/client/legacy/register-legacy-facets.d.ts +14 -0
  48. package/lib/types/client/legacy/register-legacy-facets.d.ts.map +1 -0
  49. package/lib/types/client/legacy/register-legacy-facets.js +77 -0
  50. package/lib/types/client/legacy/register-legacy-facets.js.map +1 -0
  51. package/lib/types/client/legacy/schedule.d.ts +49 -0
  52. package/lib/types/client/legacy/schedule.d.ts.map +1 -0
  53. package/lib/types/client/legacy/schedule.js +73 -0
  54. package/lib/types/client/legacy/schedule.js.map +1 -0
  55. package/lib/types/client/legacy/store-source.d.ts +78 -0
  56. package/lib/types/client/legacy/store-source.d.ts.map +1 -0
  57. package/lib/types/client/legacy/store-source.js +82 -0
  58. package/lib/types/client/legacy/store-source.js.map +1 -0
  59. package/lib/types/client/legacy/waiting-review.d.ts +85 -0
  60. package/lib/types/client/legacy/waiting-review.d.ts.map +1 -0
  61. package/lib/types/client/legacy/waiting-review.js +179 -0
  62. package/lib/types/client/legacy/waiting-review.js.map +1 -0
  63. package/lib/types/client/locales.d.ts +46 -4
  64. package/lib/types/client/locales.d.ts.map +1 -1
  65. package/lib/types/client/locales.js +46 -4
  66. package/lib/types/client/locales.js.map +1 -1
  67. package/lib/types/client/rows/GroupBadge.d.ts +19 -25
  68. package/lib/types/client/rows/GroupBadge.d.ts.map +1 -1
  69. package/lib/types/client/rows/GroupBadge.js +45 -106
  70. package/lib/types/client/rows/GroupBadge.js.map +1 -1
  71. package/lib/types/client/rows/Rows.d.ts +7 -3
  72. package/lib/types/client/rows/Rows.d.ts.map +1 -1
  73. package/lib/types/client/rows/Rows.js +6 -6
  74. package/lib/types/client/rows/Rows.js.map +1 -1
  75. package/lib/types/client/rows/WorkspaceBrowser.d.ts.map +1 -1
  76. package/lib/types/client/rows/WorkspaceBrowser.js +21 -21
  77. package/lib/types/client/rows/WorkspaceBrowser.js.map +1 -1
  78. package/lib/types/client/tree.d.ts +2 -5
  79. package/lib/types/client/tree.d.ts.map +1 -1
  80. package/lib/types/client/tree.js +0 -5
  81. package/lib/types/client/tree.js.map +1 -1
  82. package/lib/types/index.d.ts +31 -1
  83. package/lib/types/index.d.ts.map +1 -1
  84. package/lib/types/index.js +13 -1
  85. package/lib/types/index.js.map +1 -1
  86. package/package.json +1 -5
package/CHANGELOG.md CHANGED
@@ -2,8 +2,50 @@
2
2
 
3
3
  本文件记录面向使用者的重要变更。细粒度提交历史见 git log。
4
4
 
5
+ 文中 `Issues #NN` 指向本项目内部 Gitea 跟踪器上的条目(该跟踪器不对外,故不附链接)。
6
+
5
7
  ## 未发布
6
8
 
9
+ ## 0.3.0 — 2026-10-20
10
+
11
+ - **refactor** 删除过渡适配器 `src/client/legacy/`(5 个文件)、`package.json` 的两条 `dsh.client.external` 声明,以及只为它们存在的 5 个文案键(`status.waitingReview` / `...Overdue` / `...Imminent` / `...Left`、`status.scheduled`)。两家已各自注册同名 facet:dsh-gitea-dispatch 走其 PR #51(Issues #47),dsh-later 走其 PR #40(Issues #36)。
12
+ - **refactor** `visibleFacets` 从 `legacy/store-source.ts` 移到 `src/client/facet-visibility.ts`:它是徽标自己的可见性规则,不是某个来源的适配。移进 `legacy/` 只是当初为了让它能被测试,现在它本来就该在核心侧。
13
+ - **test** 核心用例不再引真实适配器,改用测试自己声明的合成来源。**这是一次所有权校正,不是减少覆盖**:用真实适配器测聚合,等于把「本插件不认识任何来源」这条断言悄悄写成「认识两个」。文案用例改为断言「来源交出的句子逐字呈现、无 label 时兜底且绝不读出来源 id」。删除 `tests/legacy-bridge.client.spec.ts`(被测对象已不存在);其"store 迟到"与"设置过滤"两条不变量分别由 `tests/facets.node.test.mjs`(登记面语义)与 `tests/badge-labels.client.spec.ts`(过滤后不残留)继续覆盖。
14
+ - **docs** `facet-protocol.md` 的过渡一节改写为迁移史,并补两条提供方必读:`require` 只能用工厂注入的那个(全局 `require` 缺席且静默失败);`protocol` 必须**比对**而不是回声对端版本(回声会关掉版本闸门,使 v1 形状的 facet 能声称 v2 并被接受,之后按错的语义静默渲染)。
15
+
16
+ ### 由 dsh-gitea-dispatch 提出的四个问题(#22 / #23 / #24 / #25)
17
+
18
+ - **fix** 脉冲作用在**整只**徽标上,同组里任何一家声明 `pulse: true` 就让别家本该静止的图标跟着呼吸(Issues #22)。此前只把**判据**交出去了(`fold.pulse`),**作用对象**仍留在徽标容器上,于是 `pulse: false` 是否生效取决于同组里恰好有没有别人开闪——「等待评审超时不闪」这条承诺(README 与 `badge-labels.ts` 注释都写着)会悄悄失效,而提供方无从发现:它交出了 `pulse: false`、自己的 CSS 里零 `animation`,失效完全发生在本仓库的容器 class 上。现在判据与作用对象一起下沉到**声明它的那个元素**(`facetPulseClass` 取代 `groupBadgeIsUrgent`),计数数字与宿主会话状态点任何时候都不再继承来源的紧急程度。`docs/facet-protocol.md` 同步改写 `pulse` 的语义。
19
+ - **fix** 悬停明细不渲染 provider 的图标,只画一个统一 `StateDot`,而写着这件事的注释理由是「紧急程度由文案与**图标色**表达」——明细里根本没有图标色,这个前提不存在(Issues #24)。结果是同一条状态、同一次悬停里,徽标上是红色沙漏、明细里是黄点:徽标与明细是**同一行的两个视图**却各说一套。宿主 `StateDot` 只有 warning / ongoing / done 三档、没有 error 档,所以色档只能由来源自己的图标表达;现在明细行与徽标用**同一个** `facetGlyph` 元素(`HoverStatusLine` 新增可选 `glyph`,provider 未交 `renderIcon` 时两处同款退回通用点)。`docs/facet-protocol.md` 的「已知取舍」改写成与实现一致。
20
+ - **refactor** 同一事实两份实现,四处(Issues #25):
21
+ - **点行顺序 ≠ 明细顺序**,而注释声称两者「完全一致」。徽标是 pending → active → **done** → facet,明细是 pending → active → **facet** → done。点行按明细对齐,注释与实现从此同一条来源。
22
+ - **状态优先级两份实现,结果不同**。徽标计数在 pending 分支里 `continue` 掉 `subagents += n.runningSubagentCount`(行内却给两个点),且未运行但已完成 + 有运行中子智能体时两处判断相反(徽标凭空多一枚绿点并计入数字)——`GroupBadge.tsx` 里「数字 = 进行中(**含子智能体**)」此前是空头承诺。计数判据改为与行内 `sessionStatuses()` 同一套规则,两处分歧各配一条回归测试。**这是行为变更**:折叠徽标的数字可能变大。
23
+ - **一次渲染两个时钟**。`groupLiveBadge` 与 `hoverStatusLines` 各走 `Date.now()` 默认参数,而聚合用一次读数——跨分钟边界时同一行的徽标文案与明细文案可能差 1 分钟,正是 `group-status.ts` 那条不变式想防的事。`ProjectRowItem` 现接收 `now`,由 `WorkspaceBrowser` 统一的读数下发到徽标与明细。
24
+ - `FACET_TONE_RANK` 仍是对外导出的名字但仓库内零使用(真正生效的是 `TONE_ORDER`)——**本次不动,且留作待决**。原先记的理由**经核实不成立**:「删除可能影响 dsh-gitea-dispatch 的产物断言」是错的,对方那条断言是**负向**的(其 `tests/group-status-facet.test.mjs:707`:`assert.equal(built.includes('… FACET_TONE_RANK'), false)`),它只要求「对方的内部件没被打进包」,本仓库删掉这个导出它照样通过。剩下的真实情况是:它从 `src/client/index.ts:157` 导出、第三方技术上能 import,但 `docs/facet-protocol.md` 与两份 README 都没提它,仓库内也没有任何实现读它。所以三选一:让 `defaultMergeFold` 真正用它、删掉这个导出、或把它写进契约文档承认是公共 API。判定过程见 Issues #25 的评论。
25
+ - **后续已收口,选第一项**:`defaultMergeFold` 改读对外的 `FACET_TONE_RANK`,私有的 `TONE_ORDER` 删除(它只有这一个使用者)。两份表的顺序逐项相同,所以对合法色档**零行为变更**(64 种组合逐一比对,0 处分歧)。非法档位靠 `FACET_TONE_RANK[tone] ?? -1` 保住旧性质——`facetRejection` 只校验 protocol / id / 三个方法,**不校验 `fold()` 返回的 `tone`**(那是跨插件边界、运行时才产出的东西),少了这个 `?? -1`,坏 provider 的非法档位会被传播而不是被另一侧的合法档压掉。护栏是 `tests/facets.node.test.mjs` 的两条 #25③ 用例,并已用变异测试验过其非空洞性:删掉 `?? -1` 后「非法色档」那条会红。
26
+ - **docs** 设置项标签与 hint(中 / 英)、字段注释、`Config` 注释、`README.md` 两处表格、`README.en.md` 两处表格(图标对照表 + 设置项表)、`docs/upgrading.md` 的升级验收清单里对「等待评审」图标的描述改回与屏幕一致:dsh-gitea-dispatch 自其 #56/#60 起交出自绘沙漏,不再是「列表笔」(Issues #23)。英文表格与升级验收清单是第一轮漏掉的,漏法与它自己要修的那条毛病同一种:zh 改了,而「照着核的人读的那份」没跟上。验收清单里写着一个已经不存在的形状,照它核的人只会得出「插件没生效」。`legacy/register-legacy-facets.tsx` 的 pen 保留为**兜底形状**并注明「仅 provider 缺席时现形,形状不一致是有意接受的」——provider 缺席时让形状与行内不一致,好过「等待评审彻底不进徽标」。
27
+ - **test** 新增 5 条用例:脉冲逐来源判定(含邻居开闪不影响 `pulse: false`)、明细取来源图标及其缺省兜底、点行与明细同序、同一 `now` 决定倒计时读数、#25② 的两处优先级分歧各一条 + 一条「无子智能体的已完成仍照常计入」的反向保护。
28
+
29
+ - **fix** 入库的构建产物比源码陈旧:`lib/types/index.js`(`tsdown` 的打包入口)比 `src/index.ts` 少 6 个配置项,导致发布出去的 `lib/index.js` 里 `Config` 只有 `showCompletedInBadge` 一个字段——**设置界面因此只画得出 1 个开关**,其余 6 项(进行中 / 待审批 / 定时任务 / 等待评审 / 悬停明细 / 数字阈值)在界面上无法配置。徽标行为不受影响(客户端对这些字段有「缺失即回落默认」的兜底),受影响的是可配置性。根因是 `tsc` 的 `incremental` 缓存(`lib/tsconfig.tsbuildinfo`)陈旧后**静默跳过输出且退出码为 0**,产物便一直停在旧版本。已全量重建 `lib/`,并新增 `tests/artifacts.node.test.mjs` 锁住「产物存在性 + `Config` 字段与源码对齐」,同时把全量重建的写法补进 `docs/upgrading.md`。
30
+ - **feat** 跨插件状态改为**协议**:本插件不再逐家适配,改由状态所有者注册 facet。新增 `src/client/facets.ts`(协议 v1 + 登记面)与 `docs/facet-protocol.md`(契约文档),client 入口导出 `registerGroupStatusFacet` / `GROUP_STATUS_FACET_PROTOCOL_VERSION`。加一个新状态源,本插件**零改动**。
31
+ - **refactor** 聚合与渲染去掉所有按来源写死的分支:`GroupLiveStatus` 不再有 `scheduled*` / `waitingReview*` 具名字段,改持 `facets: Map<id, {facet, fold}>`;徽标的数字取各来源自报的 `count` 之和,图标取来源自带的 `renderIcon`(缺则按 `tone` 退通用点),文案取 `label`(缺则「N 需要关注」,绝不把内部 id 念给用户),脉冲取 `pulse`。
32
+ - **refactor** 消费侧不再逐个调提供方 hook。facet 运行时可增删,而同一组件的 hook 序列必须恒定,逐个调会在注册面变化时直接抛 "Rendered more hooks than during the previous render"。改为订阅一个聚合外部存储 + 读版本号,与来源数量无关。
33
+ - **feat** 同 id 冲突由**来源**决胜而非注册顺序(宿主不保证 apply 先后):`provider` 恒胜过渡适配器 `legacy`,低一方的登记被拒并在 console 点名。
34
+ - **fix** 登记面的通知改为**合并补跑**而非递归:订阅者在回调里再登记会嵌套递归(两个互相触发的订阅者即无界),且中间那轮看到的登记面是改到一半的。
35
+ - **fix** 未声明 `order` 的来源排在所有声明过之后(彼此按注册先后)。原先缺省为 `0`,新接入的陌生来源会插到既有状态前面抢视觉位置。
36
+ - **refactor** 过渡适配器的可测部分拆到 `src/client/legacy/store-source.ts`(纯函数):与图标同文件的逻辑在本仓库根本 import 不动(`vitest.config.ts` 排除理由 (b)),"store 迟到"和"设置关掉哪个来源"这两处真会咬人的逻辑此前测不到。
37
+ - **test** 新增协议契约用例:`tests/facets.node.test.mjs`(校验、决胜、注销语义、排序、重入合并、缺省合并)与 `tests/legacy-bridge.client.spec.ts`(设置过滤、store 迟到与替换、提供方缺席、订阅接线);`tests/badge-labels.client.spec.ts` 改为经真实适配器驱动,锁住"陌生来源不写分支也能读出来"。
38
+ - **chore** 新增文案键 `status.needsAttention`(中 / 英)。
39
+
40
+ - **fix** 对 dsh-gitea-dispatch 的适配只做了一半:徽标只消费它发布的 `state`,其余字段全丢。提供方按 issue #16 发布的条目是 `{ prNumber, state, startedAt?, waitMaxSec?, deadline? }`,本插件的接收类型只声明 `{ prNumber, state }`,于是「在等哪个 PR」「还要等多久」「窗口缺失」三件事到不了界面。现在悬停明细与 `aria-label` 带上 PR 号与剩余时长(分钟粒度:徽标只在提供方 store 通知时重渲染,秒级读数会停在旧值上撒谎),窗口缺失时照实说「等待评审」,不编造倒计时。实测:注入一条 `PR #990002` 的等待登记后,折叠的 `dsh-plugins` 徽标由 `1 进行中 · 1 等待评审 · 1 定时任务` 变为 `1 进行中 · 1 等待评审(剩 2分钟) · #990002 · 1 定时任务`;把等待窗口改成已过期则变为 `…(已超时)…`。
41
+ - **fix** `waiting` 与 `overdue` 此前只有颜色不同,文案都是「等待评审」。这与本插件自己为定时任务与等待评审选用不同图标的理由(只靠颜色区分对色盲用户不可用)自相矛盾,也丢掉了提供方算出来的状态。现在两者各有一句文案:`等待评审` 与 `等待评审(已超时)`。
42
+ - **fix** 等待评审超时不再触发徽标脉冲。数据所有者把 `overdue` 的会话行图标画成静止(其 `lib/waiting-glyph.js` 写明「超时后停动画:静止即不再期待,比继续闪动更能表达已经结束」),且其 `overdue` 只代表 CI 的等待窗口到期、不代表评审失败。同一条等待在行内静止、在父级徽标闪动,就是同一事实的两种说法。红色不变。判据抽成纯函数 `groupBadgeIsUrgent()`,定时任务的 overdue/urgent 仍脉冲。(该判据已在上面的 #22 条里进一步下沉到单个元素,`groupBadgeIsUrgent` 由 `facetPulseClass` 取代。)
43
+ - **refactor** 分组折叠交给提供方:能用到 `summarizeWaitingGroup()` 就用它(id 口径、状态排序、截止时刻算法都归它所有),缺失时才退回本仓库的等价实现 `summarizeWaitingGroupLocally()`。此前本插件自己重写了一遍查表,且比提供方少两条兜底(`session-` / `webhook-` 前缀形态),只会静默 miss;现在与 `cross-plugin-summary.ts` 的 `entryFor()` 共用同一条四形态规则。
44
+ - **refactor** 徽标文案拆到 `src/client/badge-labels.ts`(纯函数,零运行时 import)。`rows/GroupBadge.tsx` 引入宿主图标集,其 `require` 图只在 DSH 应用内解析得到(`vitest.config.ts` 排除理由 (b)),文案留在原地等于这批跨插件状态永远测不到。新增 `tests/badge-labels.client.spec.ts` 11 例锁住文案与脉冲判据。
45
+ - **feat** 等待评审的事实随多层聚合正确上卷:PR 号取并集、去重、升序,倒计时取组内最早的未到期截止(一侧无窗口不会抹掉另一侧的)。多个会话等同一个 PR 时,明细只写一次 `#15`。
46
+
47
+ ## 0.2.2 — 2026-10-15
48
+
7
49
  - **feat** 多层聚合:父分组折叠时,徽标聚合所有后代分组的会话状态,不再只看直接成员。嵌套的 workspace 目录结构现在能正确反映整棵子树的活动。
8
50
  - **fix** 上面这条「多层聚合」交付时是坏的:折叠的父文件夹会**漏算处于展开状态的中间层文件夹**的会话。`directStatusesForGroups` 沿用了「展开即 `EMPTY_LIVE_STATUS`」的占位写法,而聚合器把这个值当输入消费,于是展开的中间层对祖先贡献 0。现在展开的分组同样被测量,「不画徽标」退回渲染层决定。实测:dsh-group-status 展开且 1 个运行中、dsh-gitea-dispatch 1、dsh-later 3 时,dsh-plugins 的折叠徽标由 `4` 修正为 `5`。
9
51
  - **fix** 折叠文件夹时完全没有淡出动画。`AnimatedRows` 的 `resetKey` 混入了 `sessionLimits`,而上游在折叠时会把该组的 limit 重置回默认值 → `resetKey` 变化 → 动画被整个跳过。属上游行为(vendor 基线 `d70ad82`),非本仓库引入。副作用:「展开其余 N 个会话」现在也会淡入而非瞬时呈现。
@@ -17,6 +59,17 @@
17
59
  - **fix** 设置项 `showCompletedInBadge` 在 GUI 里根本没有入口:宿主只把 `Config` 投影成客户端表单,插件页要插件自己往 `plugins.bundle.config` 注册卡片才画得出来。现在侧栏 **插件** → dsh-group-status 的 bundle 详情页有这张卡片,保存后写进 profile patch 并即时生效。外观与交互不自造——用官方 `SettingsForm` / `SettingsFormModel` / `Switch`,与「插件 → 官方 → 子智能体」同一套框架与取值。README 此前描述的路径是文档超前于实现,文案与升级手册已同步。
18
60
  - **test** 新增字段规格(真实现)与卡片接线(官方组件契约等价桩)用例。
19
61
 
62
+ ## 0.2.1 — 2026-10-15
63
+
64
+ 与 0.2.2 同一分钟内发布(npm `08:02:01Z` / `08:02:48Z`),**仓库里没有对应提交,也没有 tag**:
65
+ `git log -S'"version": "0.2.1"' -- package.json` 零命中。两个版本的 tarball 也不同(unpacked 1390616 vs
66
+ 1244786 字节),所以 0.2.1 的确切内容无法从仓库重建 —— 此处只如实记录这个缺口,不据猜测填写条目。
67
+
68
+ ## 0.2.0 — 2026-10-12
69
+
70
+ 首个正式发布。内容与下面的 `0.2.0-rc.2-fork1` 相同(unpacked 1269018 vs 1269025 字节,差值即版本号本身;
71
+ `files` 字段一致),特性清单见该小节。
72
+
20
73
  ## 0.2.0-rc.2-fork1
21
74
 
22
75
  - **feat** 折叠分组聚合徽标:收起的文件夹行显示进行中(转圈)、待审批(黄点)、已完成(绿点)的聚合状态,带数字与悬停明细。
package/README.en.md CHANGED
@@ -44,15 +44,16 @@ Fold any group that has live sessions; a badge appears after the folder name:
44
44
  | Spinner | Running sessions inside, incl. running subagents | ongoing (`StateDot` animation) |
45
45
  | Green dot | Completed-unread sessions inside | done (same as the row) |
46
46
  | Clock | Scheduled tasks inside (data from dsh-later) | purple · orange (≤5 min) · red (overdue) |
47
- | List-pen | Sessions waiting for a PR review (data from dsh-gitea-dispatch) | amber · red (overdue) |
47
+ | Hourglass | Sessions waiting for a PR review (data from dsh-gitea-dispatch) | amber · red (the CI wait window lapsed) |
48
48
 
49
49
  Rules:
50
50
 
51
51
  - **Completed reaches the badge**: `done` (completed-unread) now shows a green dot while folded, reusing the same `StateDot` state as the session row; a group with nothing but completed sessions shows a badge just like any other.
52
52
  - **The count** = approvals + running (**including running subagents**) + completed + sessions with scheduled tasks + sessions waiting for review. Whether the number renders at all is gated by `countThreshold` (default 2 — a lone item shows dots only).
53
- - **Hover details**: the folder row's hover card appends lines in the order `approval · plan review · answer · running · subagents · waiting review · scheduled tasks · completed`, e.g. `1 Waiting for answer · 1 Running`.
53
+ - **Hover details**: the folder row's hover card appends lines in the order `approval · plan review · answer · running · subagents · waiting review · scheduled tasks · completed`, e.g. `1 Waiting for answer · 1 Running`. The waiting-review line carries **which PR** and **how long is left**, e.g. `1 Waiting for review (3min left) · #2783`.
54
54
  - **Expanding defers to the rows**: the aggregate badge only renders while collapsed; expanded groups keep their per-row statuses with no duplication.
55
- - **Accessible**: the badge is `role="img"` with a localized `aria-label` (e.g. `1 Waiting for answer · 1 Running`) and the same text as its `title`.
55
+ - **Accessible**: the badge is `role="img"` with a localized `aria-label` (e.g. `1 Waiting for answer · 1 Running`) and the same text as its `title`. Waiting and overdue are **two different sentences** (`Waiting for review` / `Waiting for review (overdue)`), not one sentence in two colours.
56
+ - **Only scheduled reminders pulse.** A lapsed review wait does not animate, matching how dsh-gitea-dispatch draws its own session-row icon for that state: it stops the animation on purpose (its words: "stillness means no longer expecting"), and its `overdue` means only that CI stopped waiting, not that the review failed. A pulsing folder next to a still row icon would state the same fact twice. The red colour stays, same token as the publisher.
56
57
 
57
58
  ### Settings
58
59
 
@@ -64,7 +65,7 @@ Rules:
64
65
  | `showPendingInBadge` | `true` | Whether the folded badge shows the "pending" yellow dot (approval / plan review / waiting for answer) |
65
66
  | `showCompletedInBadge` | `true` | Whether the folded badge shows a green "completed" dot |
66
67
  | `showScheduledInBadge` | `true` | Whether the folded badge shows the "scheduled" clock icon (data from dsh-later) |
67
- | `showWaitingReviewInBadge` | `true` | Whether the folded badge shows the "waiting for review" list-pen icon (data from dsh-gitea-dispatch) |
68
+ | `showWaitingReviewInBadge` | `true` | Whether the folded badge shows the "waiting for review" hourglass icon (data from dsh-gitea-dispatch) |
68
69
  | `showHoverDetails` | `true` | Whether the hover card shows the aggregated status detail lines |
69
70
  | `countThreshold` | `2` | The count is shown on the badge only when the total is >= this value (default 2, i.e. omits the count when it equals 1 — the dots already convey "something is happening"; set to 1 to always show) |
70
71
 
@@ -81,32 +82,49 @@ Saving applies it live, with no refresh; the Host writes it into the profile pat
81
82
  - **Cross-plugin aggregation** (optional): scheduled tasks from dsh-later, waiting PR reviews from dsh-gitea-dispatch — works fine with neither installed.
82
83
  - **Usable with zero config**: sensible defaults out of the box; 7 settings let you fine-tune which sources appear in the folded badge, hover details, and count display.
83
84
  - **No stock files touched**: not a single file of the published `dsh-client-ui-workspace` is modified; all changes live in this repo's source, and the profile patch is two lines (add this package, disable the stock row).
84
- - **Localized**: follows the DSH UI language (中文 / English) by reusing the stock locale keys; only `status.waitingReview` and `status.scheduled` are new.
85
+ - **Localized**: follows the DSH UI language (中文 / English) by reusing the stock locale keys. This package adds `status.waitingReview`, `status.scheduled`, a fallback `status.needsAttention` for facets that ship no wording, and the three waiting-review states `status.waitingReviewOverdue` / `status.waitingReviewImminent` / `status.waitingReviewLeft`.
85
86
 
86
87
  ---
87
88
 
88
- ## Cross-plugin status (optional)
89
+ ## Cross-plugin status: the facet protocol
89
90
 
90
- ### Consumer: how this plugin reads others
91
+ A folded group badge can show state owned by **another plugin**. The direction is inverted: **the owner registers its facts in**, and this plugin's aggregation and render paths **know no specific source** (not one branch keyed on who is reporting). The full contract lives in [docs/facet-protocol.md](docs/facet-protocol.md).
91
92
 
92
- When sessions inside a group have scheduled tasks or wait on a PR review, the badge gains one icon each. This plugin does **not** poll anything itself — it asks the publishing plugin for a summary:
93
+ One exception remains open: the two current sources each have a dedicated setting, so adding a **third** one still touches 14 places in this plugin (cost and the way to close it are in the protocol doc's Known trade-offs). The transitional hosting is closed: `legacy/` and its two `dsh.client.external` entries are deleted, and both dsh-later and dsh-gitea-dispatch now register their own facets.
93
94
 
94
- | Publisher | Provides | Consumer entry point |
95
- |---|---|---|
96
- | [dsh-later](https://github.com/dsh-plugin-collection/dsh-later) | `useScheduleSummary()`: session id → `{ count, pausedCount, nextAt, state }` | `require('dsh-later/client')` |
97
- | dsh-gitea-dispatch | `useWaitingSummary()`: session id → `{ prNumber, state }` | `require('dsh-gitea-dispatch/client')` |
95
+ ```js
96
+ const gs = require('dsh-group-status/client')
97
+ const unregister = gs?.registerGroupStatusFacet?.({
98
+ protocol: gs.GROUP_STATUS_FACET_PROTOCOL_VERSION,
99
+ id: 'my-plugin.review', // '<package>.<state>'
100
+ subscribe: store.subscribe, // an external-store face, not a React hook
101
+ getSnapshot: store.getSnapshot,
102
+ fold: (snapshot, sessionIds, now) => ({ count, tone, pulse, data }),
103
+ merge, label, renderIcon, settingKey, order, // all optional
104
+ })
105
+ ctx.effect(() => () => unregister?.(), 'my-plugin: group status facet')
106
+ ```
98
107
 
99
- - When either plugin is **absent or disabled, the integration degrades silently**: no icon, everything else keeps working, no errors and no idle polling.
100
- - This repo declares both external modules in `package.json` under `dsh.client.external`.
108
+ - The five required members alone already render: `N Needs attention` plus a tone-coloured generic dot. Your internal id is **never** read out to the user.
109
+ - Icon and wording belong to the owner (`renderIcon` / `label`): what a state looks like, and whether a lapsed wait should sit still or pulse, is its knowledge. Previously this plugin inferred it from state names and ended up contradicting the owner, which draws `overdue` **statically** while the badge pulsed.
110
+ - A collapsed parent spans the whole subtree, so same-facet folds get merged again. That is why `fold` returns structured `data` rather than text: text cannot be merged. Without `merge` the default rule applies (counts add, the more urgent tone wins, `data` is dropped).
111
+ - When this plugin is absent or disabled `require` throws: catch it and carry on, your own features are unaffected.
112
+ - Same-id conflicts are settled **by origin, not by registration order** (the host does not order plugin `apply`): the owner always wins over a custodial (`legacy`) registrant.
101
113
 
102
- ### Publisher: exposing your own summary
114
+ ### Why `subscribe` / `getSnapshot` instead of a React hook
103
115
 
104
- A plugin that wants to publish its state for others copies the skeleton in [`src/client/cross-plugin-summary.ts`](src/client/cross-plugin-summary.ts): `createSummaryStore` (a subscribable store) + `createSummaryRegistry` (heals late-registered stores) + `summarizeGroup` / `mapEquals` / `entryFor` / `normalizeSessionId` (folding and id normalization).
116
+ The badge is a React component, and hook order must be constant across renders of one component. Facets register and unregister at runtime: calling each owner's hook would change the hook count the moment the registry moves, and React throws "Rendered more hooks than during the previous render". This plugin subscribes once and reads one version number, however many sources stand behind it.
105
117
 
106
- > ⚠️ **Copy it, do not import it.** Cross-plugin value imports do not resolve in this project — measured, the error is `Could not resolve "dsh-group-status"`: other plugins do not depend on this package, a client bundle is a self-contained closure factory, and the bundle-purity gate only rejects the `@deepseek-ai/*` prefix, so a cross-plugin import does not even get its dedicated diagnostic.
118
+ ### Who is connected today
119
+
120
+ | facet id | Owner |
121
+ |---|---|
122
+ | `dsh-gitea-dispatch.waiting-review` | registered by dsh-gitea-dispatch itself (its issue #47) |
123
+ | `dsh-later.schedule` | registered by dsh-later itself (its issue #36) |
107
124
 
108
- The working shape is: copy the template into the provider → the provider calls `registry.setStore(store)` and re-exports `registry.useSummary` from its client entry → the consumer then uses it through `require('<provider>/client')`. Every improvement to the template is locked by `tests/cross-plugin-summary.node.test.mjs`.
125
+ This plugin keeps no field types, tone mapping or wording for either. A third source follows the contract below and needs no change here, unless it also wants its own toggle.
109
126
 
127
+ > ⚠️ Cross-plugin **value imports** do not work in this project (measured: `Could not resolve "dsh-group-status"`: a client bundle is a self-contained closure factory). Owners therefore use only the runtime symbols from `require('dsh-group-status/client')` and never `import` this package's types; copying the structural declarations from the protocol doc is enough.
110
128
  ---
111
129
 
112
130
  ## Quick start
package/README.md CHANGED
@@ -44,15 +44,18 @@
44
44
  | 转圈 | 组内有进行中(含运行中的子智能体) | ongoing(`StateDot` 转圈动画)|
45
45
  | 绿点 | 组内有已完成未读的会话 | done(与行内一致)|
46
46
  | 时钟 | 组内有定时任务(数据来自 dsh-later) | 紫 · 橙(≤5 分钟)· 红(已到期)|
47
- | 列表笔 | 组内有等待评审的 PR(数据来自 dsh-gitea-dispatch) | 琥珀 · 红(已超时)|
47
+ | 沙漏 | 组内有等待评审的 PR(数据来自 dsh-gitea-dispatch) | 琥珀 · 红(CI 等待窗口已到期)|
48
48
 
49
49
  规则:
50
50
 
51
51
  - **已完成进徽标**:`done`(已完成未读)折叠时也显示绿点,和会话行的绿点同一个 `StateDot` 状态;一组只剩已完成时徽标照常出现。
52
- - **数字** = 待审批 + 进行中(**含运行中的子智能体**)+ 已完成 + 有定时任务的会话 + 等待评审的会话;数字是否出现受 `countThreshold` 控制(默认 2,即只剩 1 项时只画点、不显数字)。
53
- - **悬停明细**:文件夹行 hover 卡在路径、创建时间下面,按 `等待审批 · 计划待审 · 等待回答 · 进行中 · 子智能体 · 等待评审 · 定时任务 · 已完成` 的顺序追加明细行,例如 `1 等待回答 · 1 进行中`。
52
+ - **数字** = 待审批 + 进行中(**含运行中的子智能体**)+ 已完成 + 有定时任务的会话 + 等待评审的会话;数字是否出现受 `countThreshold` 控制(默认 2,即只剩 1 项时只画点、不显数字)。计数判据与**会话行自身的状态优先级同源**:等审批的会话照样计入它跑着的子智能体;有运行中子智能体但自己没在跑的会话不再额外计一次已完成——折叠前后看到的是同一套规则(Issues #25)。
53
+ - **悬停明细**:文件夹行 hover 卡在路径、创建时间下面,按 `等待审批 · 计划待审 · 等待回答 · 进行中 · 子智能体 · 等待评审 · 定时任务 · 已完成` 的顺序追加明细行,例如 `1 等待回答 · 1 进行中`。等待评审那一行带**在等的 PR 号**与**还剩多久**,例如 `1 等待评审(剩 3分钟) · #2783`。
54
54
  - **展开即让位**:聚合徽标只在折叠时出现;展开后子行自带状态,不重复显示。
55
- - **无障碍**:徽标是 `role="img"` + 本地化 `aria-label`(如 `1 等待回答 · 1 进行中`),悬停 `title` 同文案,读屏与鼠标都读得到。
55
+ - **无障碍**:徽标是 `role="img"` + 本地化 `aria-label`(如 `1 等待回答 · 1 进行中`),悬停 `title` 同文案,读屏与鼠标都读得到。等待评审的 `waiting` 与 `overdue` 在文案里就是两句话(`等待评审` / `等待评审(已超时)`),不只是颜色不同。
56
+ - **脉冲只给定时任务**:等待评审超时不闪动。数据所有者 dsh-gitea-dispatch 把自己那条会话行的超时图标画成**静止**(其 `lib/waiting-glyph.js`:「静止即不再期待」),且它的 `overdue` 只代表 CI 的等待窗口到期、不代表评审失败。同一条等待在父级徽标上闪、在行内静止,就是同一事实的两种说法。红色保留,与提供方同色。
57
+ - **脉冲逐来源生效**:每个来源的 `pulse` 只作用于它自己交出的那枚图标,不作用于整只徽标,也不作用于计数数字与宿主会话状态点。同一分组里「定时任务 overdue」与「等待评审」并存时,只有前者闪;等待评审是否静止不取决于邻居开没开闪(Issues #22)。
58
+ - **徽标与悬停明细同源**:外部来源的图标在徽标和 hover 明细里是**同一个元素**(provider 交出的、带色档的那一枚),色档不会在两处打架;没有 `renderIcon` 的来源在两处都退回通用点。宿主会话状态在明细里仍用三档 `StateDot`,因为那三档是宿主的语义(Issues #24)。
56
59
 
57
60
  ### 设置项
58
61
 
@@ -64,7 +67,7 @@
64
67
  | `showPendingInBadge` | `true` | 折叠徽标是否显示「待审批 / 待评审 / 待回答」黄点 |
65
68
  | `showCompletedInBadge` | `true` | 折叠徽标是否显示「已完成」绿点 |
66
69
  | `showScheduledInBadge` | `true` | 折叠徽标是否显示「定时任务」时钟图标(数据来自 dsh-later) |
67
- | `showWaitingReviewInBadge` | `true` | 折叠徽标是否显示「等待评审」列表笔图标(数据来自 dsh-gitea-dispatch) |
70
+ | `showWaitingReviewInBadge` | `true` | 折叠徽标是否显示「等待评审」沙漏图标(数据来自 dsh-gitea-dispatch) |
68
71
  | `showHoverDetails` | `true` | 悬停卡是否显示聚合状态明细行 |
69
72
  | `countThreshold` | `2` | 徽标数字显示阈值:数量 >= 此值时才显示数字(默认 2,即 count=1 时省略——点已表达"有东西在发生";设为 1 则始终显示) |
70
73
 
@@ -81,32 +84,49 @@
81
84
  - **跨插件聚合**(可选):dsh-later 的定时任务、dsh-gitea-dispatch 的等待评审,两个都没装也照常工作——见下。
82
85
  - **零配置可用**:默认开箱即用;7 个设置项可精细控制折叠徽标的来源显隐、悬停明细与数字样式。
83
86
  - **本体零改动**:官方 `dsh-client-ui-workspace` 包文件一个不动;改动全在本仓库源码里,profile 补丁只有两条(装本包、禁用官方行)。
84
- - **多语言**:跟随 DSH 界面语言(中文 / English),复用官方文案键,只新增了 `status.waitingReview` 与 `status.scheduled`。
87
+ - **多语言**:跟随 DSH 界面语言(中文 / English),复用官方文案键;本包新增 `status.waitingReview`、`status.scheduled`、陌生来源无文案时的兜底 `status.needsAttention`,以及等待评审的三档状态文案 `status.waitingReviewOverdue` / `status.waitingReviewImminent` / `status.waitingReviewLeft`。
85
88
 
86
89
  ---
87
90
 
88
- ## 跨插件状态(可选)
91
+ ## 跨插件状态:facet 协议
89
92
 
90
- ### 消费方:本插件怎么拿别人的状态
93
+ 折叠分组徽标可以显示**别的插件**的状态。方向是反的:**状态所有者把自己的事实注册进来**,本插件的聚合与渲染路径**不认识任何具体来源**(无一处按来源写分支)。完整契约见 [docs/facet-protocol.md](docs/facet-protocol.md)。
91
94
 
92
- 组内会话如果挂着定时任务或等待评审,徽标会各多一个图标。本插件**不自己轮询**,而是向数据源插件要一份摘要:
95
+ 一处尚未收口的例外:现有的两个来源各有一个专用设置项,加**第三个**独立开关仍需改本插件 14 个改动点(成本与收口方案见该文档的「已知取舍」)。过渡期代管已经收口:`legacy/` 目录连同两条 `dsh.client.external` 声明已删除,dsh-later 与 dsh-gitea-dispatch 各自注册自己的 facet。
93
96
 
94
- | 数据源 | 提供 | 消费方接入 |
95
- |---|---|---|
96
- | [dsh-later](https://github.com/dsh-plugin-collection/dsh-later) | `useScheduleSummary()`:会话 id → `{ count, pausedCount, nextAt, state }` | `require('dsh-later/client')` |
97
- | dsh-gitea-dispatch | `useWaitingSummary()`:会话 id → `{ prNumber, state }` | `require('dsh-gitea-dispatch/client')` |
97
+ ```js
98
+ const gs = require('dsh-group-status/client')
99
+ const unregister = gs?.registerGroupStatusFacet?.({
100
+ protocol: gs.GROUP_STATUS_FACET_PROTOCOL_VERSION,
101
+ id: 'my-plugin.review', // '<包名>.<状态名>'
102
+ subscribe: store.subscribe, // 外部存储面,不是 React hook
103
+ getSnapshot: store.getSnapshot,
104
+ fold: (snapshot, sessionIds, now) => ({ count, tone, pulse, data }),
105
+ merge, label, renderIcon, settingKey, order, // 全部可选
106
+ })
107
+ ctx.effect(() => () => unregister?.(), 'my-plugin: group status facet')
108
+ ```
98
109
 
99
- - 两个插件**未安装 / 未启用时静默降级**:图标不出现,其余徽标照常,不会报错也不会空转。
100
- - 本仓库 `package.json` 的 `dsh.client.external` 声明了这两个外部模块。
110
+ - 只交 `protocol` / `id` / `subscribe` / `getSnapshot` / `fold` 五项就能显示:「N 需要关注」加一个按色档取色的通用点。**不会**把内部 id 念给用户。
111
+ - 图标与文案归提供方(`renderIcon` / `label`):「这个状态长什么样、超时该静止还是该闪」是它的知识。上一版由本插件按状态名反推,结果与提供方自己的行内图标相反(提供方把 `overdue` 画成静止,徽标却在脉冲)。
112
+ - 折叠的父文件夹覆盖整棵子树,同名的折果要再合并一次,所以 `fold` 交结构化 `data` 而不是文本:文本合并不了。不写 `merge` 时用缺省规则(数量相加、色档取更紧急、`data` 丢弃)。
113
+ - 本插件未安装 / 未启用时 `require` 抛错:`try/catch` 兜住即可,不影响你其余功能。
114
+ - 同 id 冲突**由来源决胜,与注册顺序无关**(宿主不保证 apply 先后):状态所有者恒胜代管者(`legacy`)。
101
115
 
102
- ### 提供方:想暴露摘要的插件看这里
116
+ ### 为什么用 `subscribe` / `getSnapshot`,不用 React hook
103
117
 
104
- 别的插件要把自己的状态暴露给别人消费,可复用 [`src/client/cross-plugin-summary.ts`](src/client/cross-plugin-summary.ts) 这份骨架:`createSummaryStore`(可订阅 store)+ `createSummaryRegistry`(迟到的 store 自愈)+ `summarizeGroup` / `mapEquals` / `entryFor` / `normalizeSessionId`(折叠与 id 归一)。
118
+ 徽标是 React 组件,而 hook 调用序列必须逐次渲染恒定。facet 是运行时注册 / 注销的:逐个调提供方的 hook,注册面一变 hook 数量就变,React 直接抛 "Rendered more hooks than during the previous render"。本插件只订阅一次、只读一个版本号,背后挂多少个来源都与 hook 数量无关。
105
119
 
106
- > ⚠️ **复制,不要 import。** 跨插件值导入在本项目走不通,实测报错 `Could not resolve "dsh-group-status"`:别的插件不依赖本包,客户端 bundle 又是自包含的闭包工厂,而 bundle 纯净度闸门只拦 `@deepseek-ai/*` 前缀,连"跨插件值导入"的专用报错都不会给。
120
+ ### 现有接入方
121
+
122
+ | facet id | 归属 |
123
+ |---|---|
124
+ | `dsh-gitea-dispatch.waiting-review` | dsh-gitea-dispatch 自己注册(其 Issues #47) |
125
+ | `dsh-later.schedule` | dsh-later 自己注册(其 Issues #36) |
107
126
 
108
- 正确接法:把模板**复制**进 provider 插件 → provider 用 `registry.setStore(store)` 登记、从自己的 client 入口导出 `registry.useSummary` → consumer 再 `require('<provider>/client')` 用它的 hook。模板的每条改进都由 `tests/cross-plugin-summary.node.test.mjs` 锁住。
127
+ 本插件不再为这两家持有任何字段类型、色档映射或文案。要加第三个来源,照下面的契约注册即可,本仓库无需改动(除非还要一个独立开关)。
109
128
 
129
+ > ⚠️ 跨插件**值导入**在本项目走不通(实测 `Could not resolve "dsh-group-status"`:客户端 bundle 是自包含的闭包工厂),所以提供方只认 `require('dsh-group-status/client')` 拿到的运行时符号,不 `import` 本包的类型。协议形状见文档,抄一份结构声明即可。
110
130
  ---
111
131
 
112
132
  ## 快速开始