dsh-group-status 0.5.0 → 0.5.2

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 (74) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.en.md +138 -133
  3. package/README.md +140 -133
  4. package/docs/facet-protocol.md +274 -0
  5. package/docs/media/glyph-completed.svg +3 -0
  6. package/docs/media/glyph-pending.svg +3 -0
  7. package/docs/media/glyph-running.svg +6 -0
  8. package/docs/media/screenshot-folders.png +0 -0
  9. package/docs/media/screenshot-hover.png +0 -0
  10. package/docs/upgrading.md +241 -0
  11. package/lib/client.js +1197 -1159
  12. package/lib/client.js.map +1 -1
  13. package/lib/index.js +31 -4
  14. package/lib/types/build-stamp.d.ts +4 -0
  15. package/lib/types/build-stamp.d.ts.map +1 -0
  16. package/lib/types/build-stamp.js +23 -0
  17. package/lib/types/build-stamp.js.map +1 -0
  18. package/lib/types/client/BadgeSettingsCard.js +3 -3
  19. package/lib/types/client/BadgeSettingsCard.js.map +1 -1
  20. package/lib/types/client/group-status.d.ts +5 -0
  21. package/lib/types/client/group-status.d.ts.map +1 -1
  22. package/lib/types/client/group-status.js +25 -0
  23. package/lib/types/client/group-status.js.map +1 -1
  24. package/lib/types/client/locales.d.ts +4 -0
  25. package/lib/types/client/locales.d.ts.map +1 -1
  26. package/lib/types/client/locales.js +4 -0
  27. package/lib/types/client/locales.js.map +1 -1
  28. package/lib/types/client/navigation.d.ts +23 -8
  29. package/lib/types/client/navigation.d.ts.map +1 -1
  30. package/lib/types/client/navigation.js +20 -3
  31. package/lib/types/client/navigation.js.map +1 -1
  32. package/lib/types/client/rows/EmptySessions.d.ts +4 -0
  33. package/lib/types/client/rows/EmptySessions.d.ts.map +1 -0
  34. package/lib/types/client/rows/EmptySessions.js +10 -0
  35. package/lib/types/client/rows/EmptySessions.js.map +1 -0
  36. package/lib/types/client/rows/ListView.d.ts +24 -0
  37. package/lib/types/client/rows/ListView.d.ts.map +1 -0
  38. package/lib/types/client/rows/ListView.js +81 -0
  39. package/lib/types/client/rows/ListView.js.map +1 -0
  40. package/lib/types/client/rows/SessionTree.d.ts +4 -0
  41. package/lib/types/client/rows/SessionTree.d.ts.map +1 -0
  42. package/lib/types/client/rows/SessionTree.js +381 -0
  43. package/lib/types/client/rows/SessionTree.js.map +1 -0
  44. package/lib/types/client/rows/ViewOptionsMenu.d.ts +14 -0
  45. package/lib/types/client/rows/ViewOptionsMenu.d.ts.map +1 -0
  46. package/lib/types/client/rows/ViewOptionsMenu.js +50 -0
  47. package/lib/types/client/rows/ViewOptionsMenu.js.map +1 -0
  48. package/lib/types/client/rows/WorkspaceBrowser.d.ts +0 -5
  49. package/lib/types/client/rows/WorkspaceBrowser.d.ts.map +1 -1
  50. package/lib/types/client/rows/WorkspaceBrowser.js +7 -557
  51. package/lib/types/client/rows/WorkspaceBrowser.js.map +1 -1
  52. package/lib/types/client/rows/browser-shared.d.ts +105 -0
  53. package/lib/types/client/rows/browser-shared.d.ts.map +1 -0
  54. package/lib/types/client/rows/browser-shared.js +93 -0
  55. package/lib/types/client/rows/browser-shared.js.map +1 -0
  56. package/lib/types/client/session-actions/derived.d.ts.map +1 -1
  57. package/lib/types/client/session-actions/derived.js +13 -2
  58. package/lib/types/client/session-actions/derived.js.map +1 -1
  59. package/lib/types/client/session-watermarks.d.ts +6 -1
  60. package/lib/types/client/session-watermarks.d.ts.map +1 -1
  61. package/lib/types/client/session-watermarks.js +31 -11
  62. package/lib/types/client/session-watermarks.js.map +1 -1
  63. package/lib/types/client/toast/register.d.ts.map +1 -1
  64. package/lib/types/client/toast/register.js +102 -132
  65. package/lib/types/client/toast/register.js.map +1 -1
  66. package/lib/types/host-contract.d.ts +15 -1
  67. package/lib/types/host-contract.d.ts.map +1 -1
  68. package/lib/types/host-contract.js +47 -4
  69. package/lib/types/host-contract.js.map +1 -1
  70. package/lib/types/index.d.ts +6 -3
  71. package/lib/types/index.d.ts.map +1 -1
  72. package/lib/types/index.js +10 -4
  73. package/lib/types/index.js.map +1 -1
  74. package/package.json +17 -3
@@ -0,0 +1,274 @@
1
+ # 分组状态 facet 协议 v1
2
+
3
+ 折叠分组的徽标由本插件画。任何插件想让自己的状态出现在那只徽标上,就按本文注册一个
4
+ facet。**本插件不需要为你的插件改任何代码**。
5
+
6
+ 这份文档是契约。改它要升版本号,并同时通知已接入方。
7
+
8
+ ## 你要实现什么
9
+
10
+ 一个对象,六个成员,三个必填:
11
+
12
+ ```ts
13
+ import { createElement } from 'react'
14
+
15
+ const groupStatus = require('dsh-group-status/client')
16
+
17
+ const unregister = groupStatus?.registerGroupStatusFacet?.({
18
+ protocol: groupStatus.GROUP_STATUS_FACET_PROTOCOL_VERSION, // 必填,1
19
+ id: 'my-plugin.review', // 必填,'<包名>.<状态名>'
20
+ subscribe: store.subscribe, // 必填,外部存储订阅面
21
+ getSnapshot: store.getSnapshot, // 必填,读当前快照
22
+ fold: (snapshot, sessionIds, now) => ({ // 必填,折成这一组的事实
23
+ count: snapshot?.size ?? 0,
24
+ tone: 'warn',
25
+ pulse: false,
26
+ data: snapshot,
27
+ }),
28
+ merge: (a, b) => ({ ... }), // 可选,多层文件夹怎么合并
29
+ label: (fold, { now }) => `${fold.count} 在等评审`, // 可选,进 aria-label 与明细
30
+ renderIcon: (fold) => createElement('svg', { /* ... */ }), // 可选,图标
31
+ order: 20, // 可选,图标排列顺序
32
+ })
33
+
34
+ ctx.effect(() => () => unregister?.(), 'my-plugin: group status facet')
35
+ ```
36
+
37
+ 只交必填三项也能跑:徽标会显示「N 需要关注」加一个按色档取色的通用点。**不会**把你的
38
+ 内部 id 念给用户。
39
+
40
+ ## 为什么是这个形状
41
+
42
+ 三处设计取舍都有具体原因,改之前请先读完这一节。
43
+
44
+ ### 为什么是 `subscribe` / `getSnapshot`,不是 React hook
45
+
46
+ 徽标是 React 组件。上一版让每家导出一个 `useXxxSummary()`,本插件在同一个组件里
47
+ 逐个调。这条路不成立:
48
+
49
+ - **同一组件里 hook 调用序列必须逐次渲染恒定**,而 facet 是运行时注册 / 注销的。注册面
50
+ 一变,hook 数量就变,React 直接抛 "Rendered more hooks than during the previous
51
+ render"。
52
+ - 本插件因此必须在编译期知道"有几个来源、各叫什么",正是要消掉的耦合。
53
+
54
+ `subscribe` / `getSnapshot` 是同一能力的非 React 形态(宿主 `useSyncExternalStore`
55
+ 的外部存储面)。本插件只订阅一次、只读一个版本号,背后挂多少个来源都与 hook 数量无关。
56
+
57
+ ### 为什么 `fold` 要返回 `data`,而不是直接返回可渲染文本
58
+
59
+ 折叠的父文件夹要覆盖整棵子树,而子树里每一层都已各自 `fold` 过一次,所以本插件必须把
60
+ 同名的折果**再合并**一次。文本合并不了:「剩 3分钟」和「剩 5分钟」拼起来不是答案。
61
+ 结构化 `data` 能合并,合并规则由你提供(`merge`),文案在渲染时刻由你的 `label` 现算。
62
+
63
+ 本插件从头到尾**不解释 `data`**。它是你的私有形状。
64
+
65
+ ### 为什么图标和文案归你
66
+
67
+ 「这个状态长什么样、超时该静止还是该闪」是状态所有者的知识。上一版本插件按状态名反推,
68
+ 结果与你的判断相反:你把 `overdue` 画成静止(「静止即不再期待」),徽标却在同一件事上
69
+ 脉冲,同一个事实在同一屏里出现两种说法。
70
+
71
+ `renderIcon` 返回**已含配色**的 React 元素。不提供时本插件按 `tone` 退回一个通用点。
72
+
73
+ ## 成员逐条
74
+
75
+ | 成员 | 必填 | 语义 |
76
+ |---|---|---|
77
+ | `protocol` | 是 | 必须是 `GROUP_STATUS_FACET_PROTOCOL_VERSION` 的原值。对不上会被拒绝登记并在 console 点名。 |
78
+ | `id` | 是 | 全局唯一,建议 `'<包名>.<状态名>'`。它同时是 React key 和同 id 冲突的判据。 |
79
+ | `subscribe(listener)` | 是 | 快照可能变化时调用 `listener`;必须支持多个订阅者;返回退订函数。 |
80
+ | `getSnapshot()` | 是 | 当前快照。还没拿到数据时返回 `undefined`,本插件按"此刻无贡献"处理。 |
81
+ | `fold(snapshot, sessionIds, now)` | 是 | 纯函数。同样的三个入参必须给出等价结果:它在每次渲染里被调用,不该有副作用。 |
82
+ | `merge(a, b)` | 否 | 合并同名 facet 在两层的折果。不提供时用缺省规则(见下)。 |
83
+ | `label(fold, { now })` | 否 | 进徽标 `aria-label` / `title` 与悬停明细的那句话。返回空串按"我没有文案"处理。 |
84
+ | `renderIcon(fold, { now })` | 否 | React 元素,自带配色。 |
85
+ | ~~`settingKey`~~ | — | **已废弃**:v1 仍接受(不报错),但本插件**不再读取**。显隐见下一节。 |
86
+ | `order` | 否 | 图标排列顺序,升序。**缺省排在所有声明过 `order` 的来源之后**,彼此按注册先后。 |
87
+
88
+ ## 显隐归谁:登记即显示,关掉就是不注册
89
+
90
+ **外部来源的开关归来源自己,不归本插件。** 想让用户能关掉你的状态,就在**你自己的**配置
91
+ 卡片里加一项(两家本来就都有自己的卡片),关掉时**不注册 facet** —— 只有你知道"关掉它"
92
+ 意味着什么,也只有你的卡片知道该把它叫什么。
93
+
94
+ 这条是从反面学来的。本插件曾有两个字段
95
+ `showScheduledInBadge` / `showWaitingReviewInBadge`,名字、图标形状("时钟"、"沙漏")和
96
+ 来源名(dsh-later / dsh-gitea-dispatch)全都写死在**消费方**的 `Config`、设置卡片与文案里:
97
+
98
+ - 与「本插件不认识任何具体来源」直接矛盾 —— 协议把方向翻正了,配置面却没跟着翻;
99
+ - 而且**已经是错的**:dsh-gitea-dispatch 自其 #60 起让徽标跟随自己的 `waitingIconStyle`,
100
+ 三档里两档不是沙漏,卡片却仍在承诺"沙漏";
101
+ - 还在承诺一件可能不存在的事:对方没装时,那句"关闭后不再画"是空的。
102
+
103
+ 两个字段已删除。这是一次**破坏性配置变更**:profile 里残留的旧布尔不再被读取,等同回到
104
+ 默认"显示"(默认全开,最坏情况是某个来源重新出现,不会留下坏数据)。回归用例见
105
+ `tests/badge-settings.client.spec.ts`(旧值一律忽略)与
106
+ `tests/use-registered-facets.client.spec.tsx`(废弃的 `settingKey` 不再筛掉任何来源)。
107
+
108
+ > 曾考虑的另一种收口:本插件保留一个与来源无关的 `disabledFacets: string[]`,设置卡片
109
+ > 遍历登记面渲染,协议再加一个 `settingName` 给来源自述标题。它保住"一站式面板",但
110
+ > 机制仍长在消费方身上 —— 只是把来源名从文案换成了 id。既然原则是消费方不该有任何
111
+ > 生产方的代码,连"替生产方存开关"这件事也不该有,所以选了现在这条。
112
+
113
+ ### `FacetFold`
114
+
115
+ ```ts
116
+ {
117
+ count: number // 进徽标数字的条数;<= 0 时该来源整块跳过
118
+ tone: 'info' | 'warn' | 'error' // 语义色档(本插件据此取色令牌)
119
+ pulse: boolean // 你这一条要不要脉冲(#22:作用在你自己的图标上)
120
+ data?: unknown // 你的私有形状,本插件从不读
121
+ }
122
+ ```
123
+
124
+ `count` 是你对徽标数字的贡献。你的 `label` 应当与它口径一致(徽标显示 2,明细写 3,
125
+ 用户会认为界面在撒谎)。
126
+
127
+ **`pulse` 只作用于你交出的那一枚图标**(#22),不作用于整只徽标:同组里别家声明 `pulse:
128
+ true` 不会让你的图标跟着呼吸,你声明 `pulse: false` 也不会因为邻居开闪而失效。计数数字与
129
+ 宿主会话状态点**任何时候都不闪**——它们不是你的状态,就不该继承你的紧急程度。
130
+
131
+ ### 缺省 `merge` 与它不做什么
132
+
133
+ 不声明 `merge` 时:`count` 相加、`tone` 取更紧急的一档、`pulse` 取或,并且**丢弃
134
+ `data`**(本插件不知道如何合并你的私有形状)。需要 `data` 跨层生效(PR 号并集、最早
135
+ 截止时刻之类)就必须自己实现 `merge`。
136
+
137
+ ## 同 id 冲突:由来源决胜,与注册顺序无关
138
+
139
+ 宿主不保证插件 apply 的先后。所以:
140
+
141
+ | 已登记 | 新来的 | 结果 |
142
+ |---|---|---|
143
+ | 你的(`provider`,缺省) | 代管者(`legacy`) | 代管被拒,不覆盖你 |
144
+ | 代管者(`legacy`) | 你的 | 你覆盖它 |
145
+ | 同来源重复登记 | 同来源 | 直接覆盖(热替换不必先注销) |
146
+
147
+ `origin` 字段你**不用填**,缺省即 `provider`。只有替别人代管某个状态的注册方才显式写 `'legacy'` —— 本插件目前不代管任何来源(见下面的迁移史)。
148
+
149
+ ## 生命周期与降级
150
+
151
+ - **返回的注销函数**挂到 `ctx.effect` 的清理上。宿主卸载你的插件时它会跑。
152
+ - **本插件未安装 / 未启用**:`require('dsh-group-status/client')` 抛错。必须 `try/catch`
153
+ 或 `?.` 兜住,视为"没有徽标可登记",不要因此影响你自己的功能。
154
+ - **本插件后加载**:模块表行在你 `require` 时才 materialize,登记面是**进程级共享表**
155
+ (见下节),所以你的登记不会丢。
156
+ - **你的 store 迟到**:这是你内部的事。把"store 刚创建出来"也当作一次 `listener()`
157
+ 通知,否则徽标要等到下一次轮询才出现,而且没有任何报错线索。
158
+ - **快照读失败 / `fold` 抛错**:本插件跳过这一个来源,不拖累徽标其余部分。
159
+
160
+ ## 登记面的生命周期:换代不丢注册
161
+
162
+ 宿主的热替换(HMR、`reload(id, rev)`)**只换代被替换的那个模块**:`dsh-client-modules`
163
+ 的 `invalidate()` 只删自己那一行的 factory 与 loadCache,**不级联到"谁 require 过它"**,
164
+ 依赖方的 `apply` 不会重跑。
165
+
166
+ 所以登记面不能是模块级单例。本插件把它挂在
167
+ `globalThis.__dshGroupStatusFacetRegistry__` 上:本插件被换成新实例时拿回的是**同一张表**,
168
+ 提供方那一次性登记跨换代存活。提供方被换代时按同 id 覆盖登记(见上面的来源决胜),
169
+ 不会重复计数。
170
+
171
+ 顺带说一句现场诊断:怀疑"注册了却没显示"时,直接在浏览器控制台读
172
+ `globalThis.__dshGroupStatusFacetRegistry__.list().map(e => e.facet.id)` —— 表里**有**这个
173
+ id 而徽标**没有**,问题就在消费侧(渲染/设置过滤);表里**没有**,问题就在提供侧
174
+ (`require` 落空、`protocol` 对不上、被同 id 顶掉)。这条二分是免费的,而"少一只徽标、
175
+ 无任何报错"以前只能靠刷新页面碰运气。
176
+
177
+ ## 版本规则
178
+
179
+ - `protocol` 对不上 → **拒绝登记**并在 console 点名(不静默忽略:「我注册了为什么没
180
+ 显示」必须能查)。
181
+ - 加**可选**成员、加新的 `tone` 取值:不升版本。
182
+ - 改必填成员、改任何成员的语义:升版本,并在本文变更记录里写明迁移步骤。
183
+
184
+ ## 迁移史:`legacy/` 已删除
185
+
186
+ 本插件一度为两家代管(`origin: 'legacy'` 的过渡适配器)。两家已各自注册同名 facet,
187
+ 代管目录与 `package.json` 里对应的两条 `dsh.client.external` 声明随后删除:
188
+
189
+ | facet id | 归属 | 接入位置 |
190
+ |---|---|---|
191
+ | `dsh-gitea-dispatch.waiting-review` | dsh-gitea-dispatch | `client/group-status-facet.js`(其 Issues #47 / PR #51) |
192
+ | `dsh-later.schedule` | dsh-later | `src/client/group-status-facet.ts`(其 Issues #36 / PR #40) |
193
+
194
+ 保留 `FacetOrigin` / `FACET_ORIGIN_RANK` 不是为已经删掉的代管留兼容:它是**任何一次迁移**
195
+ 的安全网。下次再需要临时代管某个来源时,注册方与所有者仍不必协商发布顺序。
196
+
197
+ ### 提供方必读:`require` 只能是注入的那一个
198
+
199
+ 宿主给闭包工厂注入 `require`(`window.__ModuleLoader__.load({ factory: (require) => ... })`),
200
+ 但**没有全局 `require`**(实测 `typeof globalThis.require === 'undefined'`)。用全局写法不会
201
+ 报错,只会拿到 `undefined`,于是整条跨插件集成「看起来装着、其实一直没工作」。必须用工厂
202
+ 注入的那个,并且 `try` / `?.` 兜住对端缺席。
203
+
204
+ ### 提供方必读:`protocol` 要比对,不要回声
205
+
206
+ 版本常量填**你自己实现的那个**,并在登记前比对对端声明的版本;不一致就留一条可见日志并
207
+ 放弃登记。把对端的数字原样抄进 `protocol` 看似更聪明(「对方说几就是几」),实际会关掉
208
+ 版本闸门的唯一作用:将来对端升到 v2 且改了某个既有成员的语义时,你这份 v1 形状的 facet
209
+ 会声称自己是 v2 并被接受,徽标从此按错的语义渲染,而且没有任何一处会报错。沉默的等价
210
+ 正是这份文档反复要消灭的那类失败。
211
+
212
+ ## 宿主给了什么、我们数什么
213
+
214
+ 这一节回答一个常见问题:**「官方 DSH 是不是还有别的状态?」** 答案是"有,但不是状态"。
215
+
216
+ ### 我们数的:宿主的三个状态事实
217
+
218
+ 宿主的 `SessionStatus` 只有三个正交事实(`@deepseek-ai/dsh-client-ui-session`):
219
+
220
+ | 事实 | 徽标 | 卡片开关 |
221
+ |---|---|---|
222
+ | `running` | 进行中(含子智能体)| 「显示『进行中』转圈」|
223
+ | `pendingInteraction`(kind 开放集合)| 待审批 | 「显示『待审批』黄点」|
224
+ | `completionUnread` | 已完成 | 「显示『已完成』绿点」|
225
+
226
+ 徽标那三档与这三个事实**一一对应**,不多不少。宿主自己的 workspace 投影把它们映成
227
+ `running` / `pendingInteraction` / `completed` 三个字段,而本仓库的 `src/client/tree.ts` 是那份
228
+ 投影的 **fork**(见 `docs/upgrading.md`)—— 所以我们数的就是宿主数的那三个。
229
+
230
+ ### 我们数不到的:宿主的接入点
231
+
232
+ 宿主还把「定时任务 / 活跃任务」做成了**一等公民的接入点**:
233
+
234
+ - Session 行首格 `sidebar.session.row.leading`(`kind: 'list'`,**多占位**):官方
235
+ `dsh-client-ui-schedule` 占着它,"标出**空闲**行里那个有活跃任务的会话"(行首格只在会话空闲时
236
+ 挂载,免得和状态点并排);
237
+ - Session hover 卡小节 `sidebar.session.row.hover`(同样 `kind: 'list'`);
238
+ - 官方的「任务」目录页(`dsh-client-ui-schedule` = "Host task management page and Session
239
+ reminder catalog")。
240
+
241
+ **但那不是状态**:宿主的状态快照里没有「这个会话有任务」这一项,官方也没给插件一个公开的
242
+ 「某会话有活跃任务」查询面(`dsh-schedule/client` 只导出类型,目录在 UI 包内部)。席位按设计
243
+ 只收 `sessionId`,**占位者自己读自己那份数据**。
244
+
245
+ 所以:**徽标上的「定时任务」只能由占位插件通过 facet 交上来**(现在是 dsh-later 的
246
+ `dsh-later.schedule`)。消费方不画那个时钟、也不替它写形状 —— 它画的是占位者交来的元素,
247
+ 文案是占位者给的 `label`。
248
+
249
+ ### 一个已知的歧义:两枚同形的时钟
250
+
251
+ 官方行首格那个时钟与徽标里 dsh-later 那个时钟**形状一样**(都是 `IconClockOutlineRegular`),
252
+ 但口径**互补**:官方目录标的是系统 / 工具创建的活跃任务,dsh-later 标的是**用户经它创建**的提醒
253
+ (两套调度器共用同一份持久化日志,靠「fold → append dispatch」的原子判断去重,不会双触发)。
254
+ 一个会话可能只有其中一个。
255
+
256
+ 消歧的责任在**占位者与官方之间**(形状或措辞),**不在本插件**:本插件既不画那个图标、
257
+ 也不写它的名字,它只把占位者交来的东西原样摆进徽标格。
258
+
259
+ ## 已知取舍
260
+
261
+ - **本插件不提供"关掉某个来源"的开关**,这是有意的(见上一节「显隐归谁」)。历史上它
262
+ 提供过:两个布尔开关,实测成本 **14 个改动点、5 个文件**(`src/index.ts` 2 处、
263
+ `badge-settings.ts` 4 处、`badge-field.ts` 3 处、`BadgeSettingsCard.tsx` 1 处、
264
+ `locales.ts` 4 条字符串),而根因是 `Config` 在服务端静态声明、facet 在客户端运行时
265
+ 注册,两边永远错开一个时序。当时把这笔账记成"已知设计债",计划的收口是换成一个泛化
266
+ 字典 + 卡片遍历登记面,并以"第三个来源出现之前不划算"为由搁置。
267
+ **那个方向是错的**:它只是把来源名从文案换成 id,机制仍然长在消费方身上。现在两个
268
+ 字段连同整条读取路径一起删除,成本归零,来源要开关请开在自己的卡片里。
269
+ - **宿主会话状态在明细里只有 warning / ongoing / done 三档点**(这三档是宿主的语义,本
270
+ 插件无权增减)。**外部来源不适用这条限制**:`renderIcon` 交出的图标在徽标与悬停明细两
271
+ 处都会渲染,所以 `tone: 'error'` 由你自己的图标色档表达,不靠文案自救(#24)。没交
272
+ `renderIcon` 的来源在明细里退回 warning 点。
273
+ - **倒计时精度到分钟**。徽标只在你的 store 通知时重渲染,秒级读数会停在旧值上撒谎。
274
+ 要秒级就需要本插件为文案装一个定时器,那会破掉"零额外轮询、零额外计时器"的形态。
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" role="img">
2
+ <circle cx="5" cy="5" r="3" fill="#22C55E"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" role="img">
2
+ <circle cx="5" cy="5" r="3" fill="#F59E0B"/>
3
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" role="img">
2
+ <g transform="rotate(90 12 12)">
3
+ <circle cx="12" cy="12" r="9.5" fill="none" stroke="#9AA0A5" stroke-opacity="0.25" stroke-width="2" stroke-linecap="round"/>
4
+ <circle cx="12" cy="12" r="9.5" fill="none" stroke="#9AA0A5" stroke-width="2" stroke-linecap="round" stroke-dasharray="12 150"/>
5
+ </g>
6
+ </svg>
Binary file
@@ -0,0 +1,241 @@
1
+ # 上游升级手册(rebase runbook)
2
+
3
+ 本仓库是 `@deepseek-ai/dsh-client-ui-workspace` 的 **fork 覆盖包**:同一个侧栏槽位再注册一份,并把官方那一行 disable。
4
+ 上游一换代,本包就得手工 rebase 一次。这份手册写清楚基线从哪来、本地改了哪几处、下一代怎么合、怎么验证。
5
+
6
+ > 目标读者:维护者。使用与安装请看 [README](../README.md)。
7
+
8
+ ## 基线从哪来
9
+
10
+ - **基线版本**:`@deepseek-ai/dsh-client-ui-workspace 0.2.1-rc.2`(DSH `dsh-v0.2.1-rc.2` 的发布产物与类型契约)。
11
+ - **来源**:`src/` 与 `tests/` 原从上游 `0.2.0-rc.2` 源码树拷入。`0.2.1-rc.2` 一代的发布包只带 `lib/`,本次同步以官方 `.d.ts`、`lib/client.js`、官方 README 为对照基线,并按本地 `src/` 结构逐项移植。
12
+ - **不要再 diff `lib/`**:`lib/client.js` 是 tsdown 产物(`window.__ModuleLoader__.load({ id: 'dsh-group-status', factory })` 闭包工厂),不是可 diff 的官方复制品;npm 上发布的上游包同样只带构建产物。**rebase 对象永远是 `src/`。**
13
+ - **构建链**:`build/tools/` 是上游 tsdown 客户端预设拉平后的自包含副本(自解析 manifest、扁平导入、不扫 monorepo 路径);`tsconfig.json` 是独立客户端配置,去掉了 monorepo base 与 project references。
14
+ - **宿主契约有守卫**:`src/client/tree.ts` 是宿主 workspace 投影的 fork,其中 `pendingInteraction.kind` 与两个状态事实(`completionUnread` / `pendingInteraction`)是**会静默失效**的契约点 —— 宿主改了它们的形状,我们照样编译通过、测试照样全绿,只是徽标从此少画一类状态。`tests/host-pending-kinds.node.test.mjs` 直接读**锁定版本**宿主的实现来比对,宿主一改就红;人看的版本是 `pnpm check-upstream`(见 rebase 步骤第 0 步)。
15
+ - **产物要提交**:`lib/` 随仓库提交,`dsh plugin --profile web add "github:dsh-plugin-collection/dsh-group-status"` 直接吃它。**改完 `src/` 必须 `pnpm build` 并一并提交 `lib/`**,否则从 GitHub 安装的人拿不到新行为。
16
+ - **产物不是逐字节可复现的**:`lib/client.js` 里 CSS Modules 的类名哈希(如 `.eCE9Ta_…`)与 `//#region` 注释路径都跟**构建时的绝对路径**有关。同一份 `src/` 在不同 checkout(比如 issue worktree)里构建,`lib/` 会有几十行「只有哈希和路径不同」的 diff——那是噪声,不是功能差异。所以:**谁改 `src/` 谁重建并提交**,review 时不要把这种 diff 当行为变更。
17
+ - **`tsc` 的增量缓存会静默漏产物**(2026-10-16 踩过):`tsconfig.json` 开了 `incremental` + `composite`,缓存写在 `lib/tsconfig.tsbuildinfo`(已被 `.gitignore` 忽略,不入库)。但**本地那份会陈旧**:`tsc` 认为源文件没变就跳过输出,产物于是停在旧版本,而它**退出码仍是 0、一行错误都不报**。这次是 `lib/types/index.js`(`tsdown.config.ts` 的打包入口)比 `src/index.ts` 少 6 个配置项,`lib/index.js` 里的 `Config` 只剩 1 个字段,**设置界面因此只画得出 1 个开关**。彻底修复要**全量**重建,只删 `lib/types` 不够:
18
+
19
+ ```bash
20
+ rm -rf lib/types lib/tsconfig.tsbuildinfo && pnpm build
21
+ ```
22
+
23
+ `tests/artifacts.node.test.mjs` 已锁住这条(产物存在性 + `Config` 字段对齐),改完 `src/` 跑 `pnpm test` 就能拦住。
24
+
25
+ ## 本地改动清单(rebase 时逐条重新施加)
26
+
27
+ | # | 文件 / 符号 | 改了什么 |
28
+ |---|---|---|
29
+ | 1 | `src/client/tree.ts` — `GroupNode.liveStatus`(**可选字段**)+ `GroupLiveStatus` 类型 re-export | 只留类型面:`liveStatus?` 供渲染层读取,`GroupLiveStatus` 从 `group-status.ts` 转出给 `Rows.tsx` 用。**计算不在此文件**(见 #21、#22) |
30
+ | 2 | `src/client/rows/GroupBadge.tsx`(画)+ `src/client/badge-labels.ts`(说) | **本仓库新增文件**(上游没有):前者在折叠且有活时画徽标(点 + 图标 + 数字,`role="img"` + `aria-label`),后者是纯函数文案面(`groupLiveLabel` / `hoverStatusLines` / `waitingReviewLabel` / `facetPulseClass`)。文案拆开的唯一理由:GroupBadge 引入宿主图标集,其 `require` 图只在 DSH 应用内解析(见 `vitest.config.ts` 排除理由 (b)),留在原地等于跨插件状态永远测不到。`Rows.tsx` 只剩 import 与两个调用点 |
31
+ | 3 | `src/client/rows/Rows.tsx` — `WorkspaceHoverContent` 的 `status` prop | 折叠时在 hover 卡里追加聚合明细行 |
32
+ | 4 | `src/client/rows/Rows.module.css` — `.groupBadge*` / `.groupLive*` | 徽标与数字的样式 |
33
+ | 5 | `src/client/rows/WorkspaceBrowser.tsx` — `require('dsh-later/client')` / `require('dsh-gitea-dispatch/client')` | 可选消费跨插件摘要 hook,容忍未安装(`try/catch` 后保持 `undefined`)。gitea-dispatch 那份还一并取它的分组聚合 `summarizeWaitingGroup()`:取到就委托它折叠组内等待,取不到(旧版提供方)才用 `group-status.ts` 的等价实现 |
34
+ | 6 | `src/client/cross-plugin-summary.ts` | **本仓库新增文件**(上游没有):provider 侧摘要骨架,导出 `createSummaryStore` / `createSummaryRegistry` / `defaultCanPoll` / `entryFor` / `mapEquals` / `normalizeSessionId` / `summarizeGroup` |
35
+ | 7 | `src/client/locales.ts` | 复用上游文案键;新增 `status.waitingReview`、等待评审的三档状态 `status.waitingReviewOverdue` / `status.waitingReviewImminent` / `status.waitingReviewLeft`、`status.scheduled` 与设置卡片的 `settings.*` 文案(中 / 英,两侧键集由 `satisfies Record<WorkspaceKey, string>` 强制对齐) |
36
+ | 8 | `package.json` | `dsh.client.external: ["dsh-later", "dsh-gitea-dispatch"]`;`icon` 字段;`test` / `test:node` / `test:spec` / `test:watch` 脚本与测试用 devDependencies;`@deepseek-ai/schemastery` 依赖(宿主侧 `Config` 用) |
37
+ | 9 | `vitest.config.ts`、`tests/support/`、`tests/group-status.client.spec.ts` | **本仓库新增**:让上游 spec 在独立仓库可跑(见下节),加一份属于本包的折叠聚合 spec |
38
+ | 10 | `tests/cross-plugin-summary.node.test.mjs` | **本仓库新增**:模板模块的 12 条 `node --test` 用例(import 编译产物 `lib/types/client/cross-plugin-summary.js`,所以 `test:node` 先跑 `types`) |
39
+ | 11 | `src/index.ts` — `name` / `Config` / `apply()` | **本仓库新增**:上游宿主侧是空 `apply()`;这里加 `Config`(schemastery `.volatile()`,5 个字段:3 个按状态显隐 + 悬停明细 + 数字样式)供宿主投影成设置页。这是宿主侧唯一的面,不承载行为。⚠️ **只允许出现本插件自己的语义**:曾有 `showScheduledInBadge` / `showWaitingReviewInBadge` 两个替别人家写死的字段,已删除(外部来源的显隐归来源自己,见 `docs/facet-protocol.md`「显隐归谁」) |
40
+ | 12 | `src/client/badge-settings.ts` | **本仓库新增文件**(上游没有):设置面快照 → 标准 props 源的适配、派生与引用稳定记忆 |
41
+ | 13 | `src/client/index.ts` — `ConfigFormsLike` / `createBadgeSettingsSource()` / `ctx.inject(['configForms'], …)` / `GlobalStandardProps.useBadgeSettings` | 挂一个**恒在**的 `useBadgeSettings` 标准 props 源,再非阻塞地等 `configForms` 就绪并推入快照。寻址键是**条目 id**(`dsh-group-status`),不是 settings namespace;同一块里再用 `forms.whileServed([ENTRY_ID], …)` 把下面的配置卡片注册进 `plugins.bundle.config` |
42
+ | 14 | `src/client/rows/Rows.tsx` — `ProjectRowItem.settings` / `groupLiveBadge(group, t, settings)` | 按设置决定各来源是否画点/图标、是否计入数字、是否参与「显示徽标」判断 |
43
+ | 15 | `src/client/rows/WorkspaceBrowser.tsx` — `SessionTreeProps` 增 `useBadgeSettings`、`ProjectRowItem` 增 `settings` | 把设置 hook 从浏览器根透传到折叠行(完整 `ResolvedBadgeSettings` 对象,不再是单个布尔) |
44
+ | 16 | `tests/badge-settings.client.spec.ts` | **本仓库新增**:解析与源契约用例(显式 false 才隐藏、引用稳定、无关变更不通知、设置面收回回落默认) |
45
+ | 17 | `src/client/badge-field.ts` | **本仓库新增**:卡片字段的官方规格(`SettingsFieldSpec`:`format` 缺失值读成默认、`parse` 只收 `'true'`/`'false'`,其余草稿判非法挡住保存)。纯逻辑、无 React 依赖,单独可测。⚠️ `ALL_BADGE_FIELDS` 的**顺序就是徽标画点顺序**(单一事实来源),新增档位请按 `rows/GroupBadge.tsx` 的顺序插入 |
46
+ | 18 | `src/client/BadgeSettingsCard.tsx` + `BadgeSettingsCard.module.css` | **本仓库新增**:插件详情页(插件 → dsh-group-status)的配置卡片。**为什么必需**:宿主只把 `Config` 投影成 `configForms` 表音,页面要插件自己注册卡片才画得出来——没有它 README「设置项」描述的 UI 不存在。外观与交互**不自造**:框架用官方 `SettingsForm`、草稿与保存用官方 `SettingsFormModel`、开关用官方 `Switch`(与「设置 → 内置插件 → 子智能体」同一套);CSS 只写 section/heading 与开关行,取值逐条照抄 `ui-settings-subagent` 的 `SubagentCard.module.css` / `SubagentModelSelectionFields.module.css` |
47
+ | 19 | `src/client/plugin-manager-slot.d.ts` | **本仓库新增**:`plugins.bundle.config` 的本地 `SlotMap` 声明(按包名寻址)。手写而非 `import type` 插件管理器:该包不进 devDependencies;若哪天进了,删掉本文件 |
48
+ | 20 | `tests/badge-settings-card.client.spec.tsx` | **本仓库新增**:字段规格用真实现测;卡片接线用契约等价桩(官方 `SettingsForm` / `SettingsFormModel` / `Switch`)测——那个包的 bundle 依赖宿主应用预载的整棵依赖闭包,独立仓库跑不起来(见 `vitest.config.ts` 头部) |
49
+ | 21 | `src/client/group-status.ts` | **本仓库新增文件**(上游没有):徽标数字的全部计算——`GroupLiveStatus` / `EMPTY_LIVE_STATUS` / `mergeLiveStatus` / `summarizeGroupStatus`,加上多层聚合三件套 `directStatusesForGroups` → `aggregateGroupStatuses` → `withAggregatedStatus`,与渲染契约类型 `StatusedGroupNode`(`liveStatus` 必填)。纯函数、无 React 依赖,所以整条管线可脱离渲染测 |
50
+ | 22 | `src/client/rows/WorkspaceBrowser.tsx` — 聚合管线三个 memo | `childrenByParentForAggregation`(父→子树)→ `directStatusesForGroups`(每组直接成员)→ `withAggregatedStatus`(上卷成子树合计),渲染行类型为 `StatusedGroupNode`。**两条铁律**:① 展开的分组同样必须被测量——「不画徽标」是 `groupLiveBadge` 的渲染决定,把 `expanded` 短路成 `EMPTY_LIVE_STATUS` 会让每个折叠父级按展开后代的规模少算(曾以此形态发布);② 上游 `deriveGroups` 已不再算 `liveStatus`,若上游重新加回,本行与 #23 一起撤 |
51
+ | 23 | `src/client/tree.ts` — `deriveGroups()` 契约变更 | 三处:不再接收 `waitingReviewSummary` / `scheduleSummary` 入参;不再给 `liveStatus` 赋值(留空由 #22 填);**`sessions` 恒为全量成员**,折叠时不再置空。第三条最危险——渲染层必须自己按 `expanded` 决定画不画会话行与溢出按钮,漏一处就是「折叠的工作区里会话还在列」 |
52
+ | 24 | `src/client/rows/WorkspaceBrowser.tsx` — `AnimatedRows` 的 `resetKey` | 上游用独立的 `watermarkMotionKey` 区分“水位线变化”和“视图替换”;本包同步该机制,折叠与展开都能保留淡出 |
53
+ | 25 | `src/client/index.ts`、`navigation.ts`、`shortcuts.ts`、`Rows.tsx`、`contract/toast.ts`、`toast/` | 同步 `0.2.1-rc.2` 上游能力:Workspace access keyed hook、Session ID 菜单、草稿与 Agent preset、Fork 迁移提示、会话水位线、共享 `workspace.toast` 架构、HoverCard group 与 Tooltip portal;徽标聚合、facet 协议和设置卡片仍按本仓库原有规则运行 |
54
+
55
+ `cordis.patch.yml` 只有两条:`insert` 本包、`ui-workspace` `disabled: true`。升级不动它。
56
+
57
+ ## rebase 步骤
58
+
59
+ 0. **先看契约有没有漂移**:`pnpm check-upstream --latest` —— 它比对我们认的待处理 kind
60
+ 集合与宿主实现是否一致,以及宿主是否还带着 `completionUnread` / `pendingInteraction`
61
+ 这两个状态事实。**这是唯一能自动发现"宿主换了代"的地方**:这两个契约点一旦变了,我们的
62
+ fork 会**静默**地再也不画某一类状态(不报错、不红测试),只有这道检查会说话。
63
+ > 老版本这里有个 `check-upstream` 脚本,比的是"vendored 上游源码目录"与 npm 最新版的
64
+ > 哈希。它**从来没有工作过**:本地那份 pristine 副本在 `src/vendor/`(早就不存在了,
65
+ > vendored 的上游源码直接摊在 `src/` 下),而上游发布包里**没有 `src/`**(只有 `lib/`),
66
+ > 于是脚本一跑就 `ENOENT: scandir 'src/vendor/src'`。文档却还让人照着它 rebase —— 一条
67
+ > 指着不存在目录的命令比没有更糟:它让人以为"没红就是没漂移"。现在它比的是**契约点**,
68
+ > 离线可跑、且同一份实现也进了 `npm test`(`tests/host-pending-kinds.node.test.mjs`)。
69
+ 1. **取新基线源码**:优先从新一代 DSH 源码树取 `@deepseek-ai/dsh-client-ui-workspace` 的 `src/` 与 `tests/`。发布包不含 `src/` 时,用 `lib/types/**/*.d.ts`、`lib/client.js` 与包内 README 对照移植;不要只看类型就认定行为一致。
70
+ 2. **先看上游动了什么**:把新基线的 `src/` 与本地 `src/` 逐个 diff,重点看第 1–5、7 条改动落点周围有没有结构变化,以及槽位名 / 组件签名是否变了(见下节)。
71
+ 3. **覆盖再施加**:用新基线覆盖 `src/`、`tests/`,然后按上表逐条把本地改动重新施加(不要直接 `git merge` 整棵源码树之外的东西)。
72
+ 4. **补跨插件接入**:第 5、6、10 条属于本仓库新增能力,覆盖后要确认两个 hook 的接入点还在、`src/client/cross-plugin-summary.ts` 未被删、模板用例仍全绿。
73
+ 5. **类型检查**:`npx tsc --noEmit -p tsconfig.json` 必须干净(上游严格选项不变)。
74
+ 6. **构建**:`pnpm build`(= `pnpm types` + `tsdown`)→ 产出 `lib/index.js`、`lib/client.js`、`lib/types/`。
75
+ 7. **跑测试**:`pnpm test` 必须全绿;看到跳过的 spec 数变了要回头核对下节。
76
+ 8. **重装重启**:
77
+ ```sh
78
+ dsh plugin --profile web remove dsh-group-status
79
+ dsh plugin --profile web add "link:$PWD" # 或 github: 装法
80
+ # 重启 dsh web
81
+ ```
82
+ 9. **验证**(见下节),然后提交:commit 信息写清新基线版本,`lib/` 一起提交;tag 带基线,如 `v0.2.1-rc.2-fork1`。
83
+
84
+ ## 测试基线
85
+
86
+ `pnpm test` = `test:node`(`npm run types && node --test`,50 用例)→ `test:spec`(`vitest run`,jsdom,14 个文件 / 191 用例)。README 里的这两个数字由 `tests/readme-facts.node.test.mjs` 对齐实跑值看守。下表列出与 `navigation.ts`、聚合与设置相关的文件(部分动态生成的循环用例不计入静态表):
87
+
88
+ | 文件 | 来源 | 覆盖 |
89
+ |---|---|---|
90
+ | `tests/artifacts.node.test.mjs` | **本仓库** | 构建产物存在性、`Config` 字段对齐、DSH `0.2.1-rc.2` 依赖基线(4 用例,`node --test`)|
91
+ | `tests/host-pending-kinds.node.test.mjs` | **本仓库** | **宿主契约守卫**:我们认的待处理 kind 必须与锁定版宿主实现一致;宿主认的状态事实(`completionUnread` / `pendingInteraction`)不能消失;外加一条"守卫自己不是摆设"的反例(3 用例,`node --test`)|
92
+ | `tests/cross-plugin-summary.node.test.mjs` | **本仓库** | provider 侧模板:store 订阅、迟到登记自愈、分组折叠、id 归一、帧等值(12 用例,`node --test`)|
93
+ | `tests/facets.node.test.mjs` | **本仓库** | facet 协议:校验、来源决胜、注销、排序、重入合并、缺省合并与非法色档护栏(11 用例,`node --test`)|
94
+ | `tests/tree.client.spec.ts` | 上游 | 分组派生、排序、会话行与搜索 |
95
+ | `tests/browser-styles.client.spec.ts` | 上游 | 侧栏样式契约(直接读 CSS 文件) |
96
+ | `tests/animated-rows.client.spec.tsx` | 上游 | 行动画 |
97
+ | `tests/session-watermarks.client.spec.ts` | **本仓库** | `0.2.1-rc.2` 会话水位线:空白行、基线、活动行抬升、自动上限、搜索揭示与 stored marks 身份稳定 |
98
+ | `tests/badge-settings.client.spec.ts` | **本仓库** | 设置源、默认值回落与引用稳定 |
99
+ | `tests/badge-labels.client.spec.ts` | **本仓库** | 徽标文案、明细顺序、来源图标与脉冲规则 |
100
+ | `tests/badge-settings-card.client.spec.tsx` | **本仓库** | 设置字段规格与插件详情页卡片接线 |
101
+ | `tests/navigation-draft-blank.client.spec.ts` | **本仓库** | Issues #43 前瞻适配:空白复用的 preset 感知(无 preset 请求跳过列 preset 的空白、命名 preset 命中同值或无 preset 空白、全部不匹配时新建),以及 `startSession` 草稿意图判定(`whenDraftExists` 单独出现也初始化草稿、`prompt`/`clearPreviousDraft: true` 照旧、`clearPreviousDraft: false` 与仅 `agentPreset` 不碰草稿)。纯对象 stub 驱动真实 `UiWorkspaceService`,运行期只依赖 `cordis`(8 用例)|
102
+ | `tests/group-status.client.spec.ts` | **本仓库** | 折叠聚合:计数、待处理类互斥、子智能体、空白/归档跳过、定时任务与等待评审聚合、未安装时降级;**多层聚合**(展开的中间层必须计入折叠祖先、任意深度、父子环不死循环);聚合路径全折叠 / 全展开两侧的 perf 护栏。注意 `groupOf()` 走的是真实管线,**不要**再在测试里手工赋 `liveStatus`——那样测试只能证明副本自己 |
103
+
104
+ **9 个上游 spec 被显式跳过**(`vitest.config.ts` 顶部的 `MONOREPO_ONLY_SPECS`),因为它们在独立仓库里无解:
105
+
106
+ - `rows` / `workspace-browser` / `workspace-picker`:`@deepseek-ai/dsh-client-test-runtime` 深导入 `dsh-client-ui-renderer`、`dsh-api-session-controller` 的**源码**(`…/src/client/bind.ts`、`scoped-slots.tsx`、`scope.ts`)。这些包不发布 `src/`(`0.2.1-alpha` 也没有),产物里也没有导出 `bindSnapshotSelector` / `createSlotRenderer`。
107
+ - `apply` / `shortcuts` / `workspaces-service` / `session-actions` / `host-home-staleness` / `rename-assembly`:从已发布的 `…/client` 产物里取**运行期值**(`SlotRegistry`、`LocaleRuntime`、`LayoutController`、各种 `*Error`)。这些产物是闭包工厂,`require` 图假定宿主已预载外部依赖——例如 `dsh-client-ui-primitives` 自己**一个依赖都不声明**,却 require 了 shiki / katex / micromark 全家桶。要在本地跑就得把 DSH 应用整棵依赖树装成插件 devDependencies。
108
+
109
+ > 底线:不要给这两类写假实现让它们变绿——那测的是假实现。上游哪天开始发布 `src/` 或宿主无关的测试运行时,删掉对应条目即可。
110
+
111
+ `vitest.config.ts` 还负责四件让上游 spec 保持原样的接线:把 `@deepseek-ai/dsh-client-ui-workspace/client` 指到本仓库 `src/client/index.ts`;把 `@deepseek-ai/dsh-client-locale/src/locales/{en,zh}.ts` 指到 `tests/support/common-locale.ts`(该包只发布类型,没有运行期字典);把 `../../shortcuts/src/client/registry.ts` 指到 `@deepseek-ai/dsh-client-shortcuts/client`;给测试模块真实的 `file:` URL(有一个 spec 用 `new URL(..., import.meta.url)` 读文件)。`tests/support/setup.ts` 每个用例前清 `localStorage`——viewing store 会从 `dsh.workspace.view.v5` 水合,不清就会串上一条用例的排序状态。
112
+
113
+ 依赖说明:`vitest` / `jsdom` / `@testing-library/react` 是测试运行的直接依赖;`zustand` 与 `immer` 是给 `@deepseek-ai/dsh-client-store` 补的——该包**没有声明**这两个依赖,它假定宿主提供(版本对齐宿主:`zustand@5.0.10`、`immer@10.2.0`)。
114
+
115
+ ## 核对清单:上游换代时最容易踩的两类东西
116
+
117
+ **槽位名是否改名**(本包注册 / 消费的):
118
+
119
+ - `sidebar.workspaces`
120
+ - `sidebar.workspaces.session.menu.item`
121
+ - `sidebar.workspaces.session.row.action`
122
+ - `sidebar.session.row.leading` / `sidebar.session.row.hover`
123
+ - `conversation.hero.workspace`
124
+ - `shell.overlay`
125
+ - `workspace.toast`(`shell.overlay` 的 row-toast 注册项声明)
126
+ - `workspace.toast.content`(全局共享 Factory)
127
+ - `plugins.bundle.config`
128
+
129
+ **组件签名是否变化**(本包改过的两处所在):
130
+
131
+ - `ProjectRowItem`:`group, containsCurrentDescendant, onToggle, onCreate, actions, drag, home, newShortcut, t`
132
+ - `WorkspaceHoverContent`:`label, cwd, createdAt, status, t`
133
+
134
+ **跨插件接口是否搬家**:
135
+
136
+ - **facet 协议是本包对外的契约**([`docs/facet-protocol.md`](facet-protocol.md))。改它的任何必填成员或语义都要升 `GROUP_STATUS_FACET_PROTOCOL_VERSION` 并在文档变更记录里写迁移;provider 侧只认 `require('dsh-group-status/client')` 的运行时符号,类型抄结构声明,不 import 本包。
137
+ - 聚合与渲染**按协议工作,不按来源分支**:新状态源接入不应改动 `group-status.ts` / `badge-labels.ts` / `rows/GroupBadge.tsx`。上游换代若要求这三处重新施加,检查点是没有把来源名写回去。
138
+ - 本插件**不代管任何来源**:不要为了「方便」重新引入 `legacy/` 式适配器,两家现有接入方各自注册(见 [`facet-protocol.md`](facet-protocol.md) 的迁移史一节)。
139
+ - provider 侧模板的导出面(`createSummaryStore` / `createSummaryRegistry` / `summarizeGroup` / `mapEquals` / `entryFor` / `normalizeSessionId` / `defaultCanPoll`)——它被别的插件**复制**走,改名会静默影响它们的调用点;设计说明与「复制、不要 import」的实测依据写在 [`src/client/cross-plugin-summary.ts`](../src/client/cross-plugin-summary.ts) 文件头
140
+
141
+ ## 兼容声明为什么长这样(12 条 peer 不是元数据)
142
+
143
+ README 只给结论(声明 `>=0.2.1-rc.2 <0.3.0-0`,另有 12 条 peer),机制记在这里。
144
+
145
+ peer 里那 12 条**不是元数据**:≤0.2.0 的内核不读 `engines.dsh`,只按 `@deepseek-ai/dsh*` peer 的范围判兼容。不逐条声明时,0.2.0 会把本包判成「兼容」并装进去 —— 而那一代没有 `@deepseek-ai/dsh-api-remote-access`,客户端 `inject` 的 `remoteAccess` 永远拿不到,fiber 永久 pending,`uiWorkspace` 不再被提供,`ui-sidebar` / `ui-conversation` / `ui-chat` 等 12 条一起不激活,**整页 "Failed to load plugins",用户失去整个侧栏**。声明之后,0.2.0-rc.2 实测是内核直接 `skipping profile bundle`,宿主不受损;而 0.2.1 起 `engines.dsh` 命中即返回、peer 不参与判定,窗口内代次不受影响。
146
+
147
+ **这条链上有两个静默点**,所以换代时两处都要人看着:
148
+
149
+ - 漏写一条 peer 不会立刻报错,只会在**旧内核**上炸,而旧内核根本不在 CI 矩阵里;
150
+ - `engines.dsh` 与 peer 下界必须同一条线(`tests/artifacts.node.test.mjs` 锁住),只改一边等于声明面失效。
151
+
152
+ **预发布版匹配不上 caret 范围,这是 semver 规则而不是破口**(Issues #43 §3,2026-11-02 复核):12 条
153
+ peer 的 `^0.2.1-rc.2` 按预发布规则**不匹配** `0.2.2-alpha.x`(预发布只对同 `[major,minor,patch]` 元组的
154
+ 范围可见),但 `0.2.2` **正式版**落在 `>=0.2.1-rc.2 <0.3.0` 里,会自动纳入。所以 alpha 线提前验证时才需要
155
+ 临时把范围写成 `^0.2.1-rc.2 || ^0.2.2-alpha.2`,**这类临时改动不入仓**:`tests/artifacts.node.test.mjs`
156
+ 锁的就是 peer == `^` + `engines.dsh` 下界这一条不变式,commit 半个 `||` 范围会让声明面与守卫互相说谎,
157
+ 而"支持 0.2.2-alpha.x"在实测通过之前也不该被写进任何兼容声明(未判定不是绿)。
158
+
159
+ ## 上游代次跟踪(2026-10-27 实测)
160
+
161
+ 本包声明 `engines.dsh: >=0.2.1-rc.2 <0.3.0-0`,peer 侧另有 12 条 `@deepseek-ai/dsh-*`(`^0.2.1-rc.2`,
162
+ 用于让不读 `engines.dsh` 的旧内核拒绝装载)。
163
+
164
+ **已验代次**:`0.2.1-rc.2`(= 当时 npm `latest`,也是本包声明的下界)、`0.2.1-rc.3`、`0.2.1-rc.4`。
165
+ 三代的门禁(本运行时 + 0.2.1+ 语义双判)、冷启动装配、`dsh.client.inject` 12 个包可解析、
166
+ `tsc`(**对着该代次真实的 `.d.ts`**,不是对着基线猜)、`vitest 183` + `node 41` 全部通过。
167
+ rc.2 上另做了真 GUI 功能验收:分组行 / 会话行 / 视图菜单 / 扁平列表 / 搜索命中与无匹配 /
168
+ 折叠徽标(「1 进行中 · 1 已完成」两枚点并存)/ 悬停明细两行。证据(JSON 明细 + real-gui 截图)
169
+ 在 workspace 的 `devkit/results/dsh-group-status-cross-kernel-2026-10-27.json` 与
170
+ `devkit/results/screens/`,不在本仓库内。
171
+
172
+ **契约面没变**:rc.4 的 `dsh-client-ui-workspace/lib/client.js` 相比 rc.2 有 15 行差异
173
+ (231 078 → 231 234 字节),但 `visiblePendingKind` 认的 kind 仍是 `approval` / `plan-review` /
174
+ `question`,`completionUnread` 与 `pendingInteraction` 两个事实也都还在 ——
175
+ 所以 `src/host-contract.ts` 那道守卫在 rc.4 上是绿的,**不需要跟着改**。
176
+
177
+ **但行为面漂了,漂的正是本仓库 fork 的那块**。rc.4 在 `UiWorkspaceService` 上改了三处,
178
+ `src/client/navigation.ts` 是它的 fork,此前仍是 rc.2 写法(2026-10-27 登记;2026-11-02 按 Issues #43
179
+ 对 `0.2.2-alpha.2` 复核后更新下表):
180
+
181
+ | rc.4 的改动 | `navigation.ts` 现状(2026-11-02) |
182
+ |---|---|
183
+ | 删掉 `connectBlank` 里的 `if (reuse === undefined) return this.sessions.create({ workspaceId })` 早退 | 未跟:`connectWorkspace` 恒传 `{}`,该早退不可达,无行为差 |
184
+ | 空白会话复用改为 **preset 感知**(比较 `summary.projectionValues?.agentPreset`) | **已对齐**(Issues #43):`createInWorkspace` 挑选复用候选时跳过列出其它 preset 的空白;无 preset 的空白对任何请求仍可复用。`0.2.2-alpha.2` 的定稿措辞与此一致 |
185
+ | 草稿初始化 `clearPreviousDraft: boolean` → `whenDraftExists: 'replace' \| 'keep'`(旧字段降级为别名;`0.2.2-alpha.2` 定稿为三态 `replace \| append \| keep`) | **已对齐**(Issues #43):`StartSessionOptions` 前瞻声明三态并原样透传给 `requestDraftInitialization`(旧宿主忽略、新宿主生效);`initializeDraft` 判定同时识别 `prompt` / `clearPreviousDraft: true` / `whenDraftExists`,两字段语义分叉后导航层不再漏判 |
186
+
187
+ 这件事**不会**自动修好:`cordis.patch.yml` 把官方 `ui-workspace` 行 `disabled: true`,`uiWorkspace`
188
+ 由本插件自己提供 —— 所以在 rc.4 上跑的时候,**rc.4 的这三处改进根本到不了用户**,用户拿到的是 fork 里
189
+ 那份 rc.2 行为。**能跑 ≠ 行为一致**,这是"全绿"之外必须单独记一笔的差别。
190
+
191
+ **本仓库的约定**:容器服务(`navigation.ts`)以**基线代次**为准,不逐 rc 跟。此前的理由:这块没有任何本地
192
+ 用例覆盖(`tests/workspaces-service.client.spec.ts` 在 `MONOREPO_ONLY_SPECS` 里,从未在 CI 跑过),
193
+ 改 inflight 复用与草稿保留语义无法证明没坏,而收益只是三处细节 UX。该前提在 Issues #43 后**已经不成立**:
194
+ 新增的 `tests/navigation-draft-blank.client.spec.ts` 用纯对象 stub 驱动真实 `UiWorkspaceService`
195
+ (运行期只依赖 `cordis`,本仓库可跑),preset 感知复用与草稿意图判定两侧共 8 用例看守。此后逐 rc 跟改
196
+ 按这份形状补用例即可;但**前瞻适配不等于基线迁移**,升基线仍按下面的触发清单与迁移步骤走。
197
+
198
+ **什么时候必须动**(任一触发即升基线,别拖):
199
+
200
+ 1. npm 的 `latest` 移到基线之后的代次 —— 用户会自动装上,漂移立刻变成用户可见行为;
201
+ 2. `devkit/matrix.mjs` 的 `gate` 或 `boot` 在某个代次变红;
202
+ 3. `tests/host-pending-kinds.node.test.mjs` 变红(kind 集合或两个事实名漂移);
203
+ 4. `tsc` 对着新代次的 `.d.ts` 变红。
204
+
205
+ **升基线的清单**(漏一项 CI 就红,2026-10-27 实测踩过):
206
+
207
+ - 26 个 `@deepseek-ai/dsh-*` devDependencies 一起改(`tests/artifacts.node.test.mjs` 逐个断言);
208
+ - `peerDependencies` 里那 12 条的范围必须等于新的 `engines.dsh` 下界(同文件另一条用例锁住);
209
+ - `tests/artifacts.node.test.mjs` 里那条**硬编码**的 `'0.2.1-rc.2'`;
210
+ - 本文件「基线从哪来」「本地改动清单」两节里的版本号;
211
+ - 重新 `pnpm install` 并重建 `lib/`(产物随源码提交)。
212
+
213
+ ## 验证
214
+
215
+ 1. 找一个有进行中 / 待审批会话的分组 → 收起 → 文件夹名后出现徽标;
216
+ 2. 悬停文件夹行 → 明细行出现,文案与徽标 `aria-label` 一致;
217
+ 3. 展开分组 → 徽标消失,子行自带状态;
218
+ 4. 组内有定时任务 / 等待评审时,徽标各多一枚图标,且明细与 `aria-label` 带上 `#PR` 与剩余时长;等待窗口到期后 `aria-label` 换成该来源的超窗文案(当前是「已超窗,仍在等待」),**徽标不闪动**(`pulse: false` 由来源声明,与 dsh-gitea-dispatch 那条会话行的静止图标同一口径)。⚠️ **别在这里核对形状与颜色**:图标由来源自己的 `renderIcon` 交出,形状还跟随该插件自己的风格档位(dsh-gitea-dispatch 的 `waitingIconStyle` 三档),本插件无权决定,写死成"时钟/沙漏"的清单会指着一个不存在的形状验收;
219
+ 5. 临时卸掉 dsh-later / dsh-gitea-dispatch → 图标消失但**不报错**、不空转;本插件的设置卡片上**不应出现**任何提供方名字或它的图标名(回归用例:`tests/badge-settings-card.client.spec.tsx`);
220
+ 6. 提供方不导出 `summarizeWaitingGroup()`(旧版)时,徽标走本仓库的等价折叠,数字/色档/文案与新版一致(`tests/group-status.client.spec.ts` 锁住);
221
+ 7. **设置项**:侧栏 **插件** → dsh-group-status(bundle 详情页,描述与「包含的组件」之间)→ 卡片分 3 个 section(折叠徽标 **3** 个开关,顺序与徽标画点一致:待审批 → 进行中 → 已完成 / 悬停明细 1 个开关 / 数字显示 1 个数字)→ 关掉某个开关 → 保存 → 对应档位的点从徽标与悬停明细中消失、数字相应减少;再打开 → 回来。**外部来源在这一页没有开关**(显隐归来源自己:不注册就是不显示)。改完**无需刷新**;刷新页面后设置仍在(宿主把它写进 profile patch 的 `dsh-group-status` 条目 `config`)。表单外观应与「插件 → 官方 → 子智能体」同款:section 标题 13px/600、开关行 label 左 + switch 右、说明 12px 次级灰度、保存按钮在内容列左缘(官方 `SettingsForm` 的定位);
222
+ 8. **设置面缺失时**(把 `Config` 临时去掉再构建)→ 插件照常加载,徽标按默认「显示已完成」;console 无 `cannot get property … without inject` 一类报错;
223
+ 9. 浏览器 console 无本插件错误;
224
+ 10. 中 / 英界面语言切换后文案跟随。
225
+ 11. 会话行的 `...` 菜单末尾有「复制 Session ID」;点击后菜单关闭,成功与失败各出现一次全局提示,且不会打开会话或加载历史。
226
+ 12. 组内第 6 条之后的会话进入水位线规则:运行中、子智能体运行中、等待用户、未读完成、当前打开或搜索揭示的会话自动抬高水位线;折叠父组或列表卸载时水位线回落,不写入持久化 store。
227
+ 13. 无权限 Peer 不显示 fork 菜单项;有权限但源会话需要迁移时,菜单提示提供「打开原会话」,只导航、不自动重试 fork。快捷键失败通过共享 `workspace.toast` 显示,不再由 `WorkspaceBrowser` 自己挂旧 Toast。
228
+
229
+ ## 回滚
230
+
231
+ ```sh
232
+ dsh plugin --profile web remove dsh-group-status
233
+ # 两条补丁一起撤,官方行自动回来;重启 dsh web 生效
234
+ ```
235
+
236
+ ## 已知限制
237
+
238
+ - 本包 vendored 上游源码,不是公共 API 消费方:上游重构时 rebase 成本可能不止「重新施加 25 条」。
239
+ - 设置卡片依赖宿主的 `configForms.whileServed`(0.1.7 settings rewrite 一代,`0.2.1-rc.2` 仍在)。更早一代拿不到它时只跳过插件页卡片,折叠徽标读取设置面与其余功能不受影响。
240
+ - 聚合计数与展开态共用同一份派生,超大分组(数百会话)每次状态变更多一次全量 traverse,与展开渲染同量级。
241
+ - 定时任务按「组内有」计 1,不按任务条数累计,也不占 hover 明细行。