@cowwo/dsh-icon-custom 0.19.1 → 0.20.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.
package/lib/index.js CHANGED
@@ -371,7 +371,11 @@ export class FaviconCustomService extends TypertRemoteService {
371
371
  projectionCtx.sessionProjections.register({
372
372
  key: LAST_TURN_END_KEY,
373
373
  stateSchema: lastTurnEndStateSchema,
374
- stateVersion: 1,
374
+ // Bumped when the fold learned to attribute a turn to the sub-agent
375
+ // settlement that woke it (`user/message` could no longer be ignored).
376
+ // A cached row is keyed by this number, so an old one is re-folded
377
+ // instead of being read as "nobody caused this turn".
378
+ stateVersion: 2,
375
379
  init: () => initLastTurnEnd(),
376
380
  apply: applyLastTurnEnd,
377
381
  wire: { viewSchema: lastTurnEndViewSchema, view: lastTurnEndView }
@@ -517,7 +521,7 @@ export class FaviconCustomService extends TypertRemoteService {
517
521
  * icon RPCs (`getStatus` / `setIcon` / `setPwa` / `setLogo` / `resetIcon`)
518
522
  * keep their EXACT pre-badge response shape, so a problem in this feature
519
523
  * can never break the icon itself.
520
- * @returns `{ reasons, pending, workspaceDot, appBadge, clearDelaySec, hostNow }`.
524
+ * @returns `{ reasons, pending, workspaceDot, appBadge, clearDelaySec, revealOnOpen, unreadIncludeSubagents, hostNow }`.
521
525
  */
522
526
  async getUnreadRule() {
523
527
  return this._unreadRuleView()
@@ -527,7 +531,7 @@ export class FaviconCustomService extends TypertRemoteService {
527
531
  * typert endpoint: replace the unread-badge rule — which turn-end reasons
528
532
  * light the badge. The rule is applied on the client, so this takes effect
529
533
  * without re-folding any session log.
530
- * @param request - `{ reasons, pending?, workspaceDot?, appBadge?, clearDelaySec? }`; unknown or missing keys fall back to defaults.
534
+ * @param request - `{ reasons, pending?, workspaceDot?, appBadge?, clearDelaySec?, revealOnOpen?, unreadIncludeSubagents? }`; unknown or missing keys fall back to defaults.
531
535
  * @returns the stored rule.
532
536
  */
533
537
  async setUnreadRule(request) {
@@ -548,22 +552,35 @@ export class FaviconCustomService extends TypertRemoteService {
548
552
  * drag the other's storage or response shape along. There is deliberately no
549
553
  * `running` field here — who is running is the official host's answer and
550
554
  * travels in the ordinary session list.
551
- * @returns `{ enabled }`.
555
+ *
556
+ * Both switches are returned whole from the normalized copy — `enabled` (draw
557
+ * the marks at all), `includeSubagents` (credit a running sub-agent to the
558
+ * topmost non-sub-agent ancestor session) and `headerRing` (the yellow ring on
559
+ * the conversation header's sub-agent count) — so a caller can never drop one by
560
+ * copying only the field it happened to know about.
561
+ * @returns `{ enabled, includeSubagents, headerRing }`.
552
562
  */
553
563
  async getRunningRule() {
554
564
  return { ...this.running }
555
565
  }
556
566
 
557
567
  /**
558
- * typert endpoint: flip the running-mark switch.
559
- * @param request - `{ enabled: boolean }`; anything else falls back to the default.
560
- * @returns the stored switch.
568
+ * typert endpoint: flip a running-mark switch.
569
+ *
570
+ * A patch, not a replacement: `normalizeRunningConfig` fills whichever field is
571
+ * absent from the current defaults, so `{ includeSubagents: false }` alone never
572
+ * resets `enabled` and vice versa.
573
+ * @param request - `{ enabled?: boolean, includeSubagents?: boolean }`; anything else falls back to the defaults.
574
+ * @returns the stored switches.
561
575
  */
562
576
  async setRunningRule(request) {
563
577
  if (request === null || typeof request !== 'object') {
564
578
  throw new Error('缺少 enabled 参数')
565
579
  }
566
- this.running = normalizeRunningConfig(request)
580
+ // Merged onto the stored copy, not swapped in wholesale: the two switches live
581
+ // in one record, so a caller that only knows about one of them must not reset
582
+ // the other to its default.
583
+ this.running = normalizeRunningConfig({ ...this.running, ...request })
567
584
  await this._saveRunning().catch(() => {}) // switch file is advisory
568
585
  return { ...this.running }
569
586
  }
@@ -613,6 +630,8 @@ export class FaviconCustomService extends TypertRemoteService {
613
630
  workspaceDot: this.unread.workspaceDot,
614
631
  appBadge: this.unread.appBadge,
615
632
  clearDelaySec: this.unread.clearDelaySec,
633
+ revealOnOpen: this.unread.revealOnOpen,
634
+ unreadIncludeSubagents: this.unread.unreadIncludeSubagents,
616
635
  hostNow: Date.now(),
617
636
  seen: { ...this.seen }
618
637
  }
package/lib/running.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * 事件实时推到浏览器,客户端会话列表里本来就带着每个会话的 `running`。
7
7
  * 这个插件只把那件事**多投影几次**(黄点/黄数字/黄 favicon 角标)。
8
8
  *
9
- * 所以这个文件只有一个布尔开关,和一条纪律:
9
+ * 所以这个文件只有几个布尔开关,和一条纪律:
10
10
  * **不落盘"进行中",只落盘"要不要显示"。** 进程重启后冷会话按官方口径
11
11
  * 永远是 `running:false`,不会留下一个永远亮着的假黄点;而开关是偏好,
12
12
  * 应该跨设备、跨重启跟着你,所以它归宿主,和红点规则并列但互不牵连
@@ -17,16 +17,37 @@
17
17
  export const RUNNING_MARK_DEFAULT = true
18
18
 
19
19
  /**
20
- * 把任意输入夹成 `{ enabled }`。
20
+ * 默认:把子代理的「进行中」算到它所属的会话上。
21
+ *
22
+ * 和上面那个开关是**两个问题**:「要不要画黄点」与「子代理在跑,算不算它上面那个
23
+ * 会话在跑」。子代理自己没有会话行(官方侧栏根本不渲染),所以它跑得再欢,真正属于
24
+ * 用户的那个会话看上去也是空闲的;默认聚合上去,黄点才反映「有活正在干」。
25
+ * 聚合只影响计数归属:子代理自己的行照旧不画,等待你(琥珀色)的祖先也照旧不算。
26
+ */
27
+ export const RUNNING_INCLUDE_SUBAGENTS_DEFAULT = true
28
+
29
+ /**
30
+ * 默认:在会话头部那颗「N 个子智能体」计数上叠一个黄圈。
31
+ *
32
+ * 第三个问题,同样独立:前两个开关决定「黄点画不画」「子代理算不算到祖先」,
33
+ * 这一个只决定「会话头部那颗计数上要不要再叠一个运行数」。它不参与任何既有
34
+ * 计数(侧栏红黄点、左上角数字、favicon、系统角标),关掉它一个数字都不会动。
35
+ */
36
+ export const RUNNING_HEADER_RING_DEFAULT = true
37
+
38
+ /**
39
+ * 把任意输入夹成 `{ enabled, includeSubagents, headerRing }`。
21
40
  *
22
41
  * 只认严格的 `true`/`false`;缺项、脏值、null 一律回落默认——一个读不懂的
23
42
  * 开关值不该把一整套标记的可见性变成猜谜。
24
- * @param input - `{ enabled? }`,可以是脏的、缺项的、null
25
- * @returns `{ enabled }` —— 永远完整
43
+ * @param input - `{ enabled?, includeSubagents?, headerRing? }`,可以是脏的、缺项的、null
44
+ * @returns `{ enabled, includeSubagents, headerRing }` —— 永远完整
26
45
  */
27
46
  export function normalizeRunningConfig(input) {
28
47
  const source = input !== null && typeof input === 'object' ? input : {}
29
48
  return {
30
- enabled: typeof source.enabled === 'boolean' ? source.enabled : RUNNING_MARK_DEFAULT
49
+ enabled: typeof source.enabled === 'boolean' ? source.enabled : RUNNING_MARK_DEFAULT,
50
+ includeSubagents: typeof source.includeSubagents === 'boolean' ? source.includeSubagents : RUNNING_INCLUDE_SUBAGENTS_DEFAULT,
51
+ headerRing: typeof source.headerRing === 'boolean' ? source.headerRing : RUNNING_HEADER_RING_DEFAULT
31
52
  }
32
53
  }
@@ -59,6 +59,8 @@ const unreadRuleSchema = z.object({
59
59
  clearDelaySec: z.number().int().nonnegative().max(600),
60
60
  /** 实验项:点清单条目时在侧栏定位到该会话(展开工作区 + 滚动 + 闪一下;贴产品 DOM,可一键关闭)。 */
61
61
  revealOnOpen: z.boolean(),
62
+ /** 子代理结算唤醒父会话跑出来的那一轮结束,算不算未读。默认 true(= 现状)。 */
63
+ unreadIncludeSubagents: z.boolean().optional(),
62
64
  /**
63
65
  * 宿主此刻的 `Date.now()`。
64
66
  * 客户端用它把"看过"水位换算进宿主时钟域(`turn/end.time` 也是宿主时钟):
@@ -80,7 +82,8 @@ const setUnreadRuleRequestSchema = z.object({
80
82
  workspaceDot: z.boolean().optional(),
81
83
  appBadge: z.boolean().optional(),
82
84
  clearDelaySec: z.number().int().nonnegative().max(600).optional(),
83
- revealOnOpen: z.boolean().optional()
85
+ revealOnOpen: z.boolean().optional(),
86
+ unreadIncludeSubagents: z.boolean().optional()
84
87
  })
85
88
 
86
89
  /** 一次水位线同步:浏览器把自己那份交上来,宿主逐会话取 max 后回权威表。 */
@@ -96,15 +99,21 @@ const unreadSeenSchema = z.object({
96
99
  * 进行中黄点的开关。
97
100
  *
98
101
  * 这里**没有**"谁在跑"的字段,一条都没有:那是官方的 `SessionSummary.running`,
99
- * 由官方宿主实时推给浏览器,插件只读不存。宿主这一侧只有"要不要显示"这一个偏好,
100
- * 和红点规则并列、互不牵连(自己的端点对、自己的文件)。
102
+ * 由官方宿主实时推给浏览器,插件只读不存。宿主这一侧只有两个偏好——要不要显示、
103
+ * 子代理的进行中是否算到它所属的祖先会话上——和红点规则并列、互不牵连
104
+ * (自己的端点对、自己的文件)。
101
105
  */
102
106
  const runningRuleSchema = z.object({
103
- enabled: z.boolean()
107
+ enabled: z.boolean(),
108
+ includeSubagents: z.boolean().optional(),
109
+ /** 会话头部「N 个子智能体」计数上那颗黄圈(独立开关,不并入任何既有计数)。 */
110
+ headerRing: z.boolean().optional()
104
111
  })
105
112
 
106
113
  const setRunningRuleRequestSchema = z.object({
107
- enabled: z.boolean().optional()
114
+ enabled: z.boolean().optional(),
115
+ includeSubagents: z.boolean().optional(),
116
+ headerRing: z.boolean().optional()
108
117
  })
109
118
 
110
119
  const setIconResultSchema = statusSchema
@@ -379,7 +388,7 @@ export const TYPERT = {
379
388
  {
380
389
  kind: 'method',
381
390
  name: 'getUnreadRule',
382
- signature: 'async getUnreadRule(): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; revealOnOpen: boolean; hostNow: number; seen: Record<string, number> }>',
391
+ signature: 'async getUnreadRule(): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; revealOnOpen: boolean; unreadIncludeSubagents?: boolean; hostNow: number; seen: Record<string, number> }>',
383
392
  description: "Read the unread-badge rule, the Host clock and the seen watermark.",
384
393
  summary: "Read the unread-badge rule.",
385
394
  jsDoc: "/** Read the unread-badge rule. */",
@@ -388,7 +397,7 @@ export const TYPERT = {
388
397
  {
389
398
  kind: 'method',
390
399
  name: 'setUnreadRule',
391
- signature: 'async setUnreadRule(request: { reasons: Record<string, boolean>; pending?: boolean; workspaceDot?: boolean; appBadge?: boolean; clearDelaySec?: number; revealOnOpen?: boolean }): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; revealOnOpen: boolean; hostNow: number; seen: Record<string, number> }>',
400
+ signature: 'async setUnreadRule(request: { reasons: Record<string, boolean>; pending?: boolean; workspaceDot?: boolean; appBadge?: boolean; clearDelaySec?: number; revealOnOpen?: boolean; unreadIncludeSubagents?: boolean }): Promise<{ reasons: Record<string, boolean>; pending: boolean; workspaceDot: boolean; appBadge: boolean; clearDelaySec: number; revealOnOpen: boolean; unreadIncludeSubagents?: boolean; hostNow: number; seen: Record<string, number> }>',
392
401
  description: "Replace the unread-badge rule: which turn-end reasons light the red dot.",
393
402
  summary: "Set the unread-badge rule.",
394
403
  jsDoc: "/** Set the unread-badge rule. */",
@@ -406,8 +415,8 @@ export const TYPERT = {
406
415
  {
407
416
  kind: 'method',
408
417
  name: 'getRunningRule',
409
- signature: 'async getRunningRule(): Promise<{ enabled: boolean }>',
410
- description: "Read the running-mark switch (yellow mark for sessions that are running right now).",
418
+ signature: 'async getRunningRule(): Promise<{ enabled: boolean, includeSubagents?: boolean, headerRing?: boolean }>',
419
+ description: "Read the running-mark switches (show the yellow mark; roll a running sub-agent up to its ancestor session; draw the header count ring).",
411
420
  summary: "Read the running-mark switch.",
412
421
  jsDoc: "/** Read the running-mark switch. */",
413
422
  tags: []
@@ -415,7 +424,7 @@ export const TYPERT = {
415
424
  {
416
425
  kind: 'method',
417
426
  name: 'setRunningRule',
418
- signature: 'async setRunningRule(request: { enabled?: boolean }): Promise<{ enabled: boolean }>',
427
+ signature: 'async setRunningRule(request: { enabled?: boolean, includeSubagents?: boolean, headerRing?: boolean }): Promise<{ enabled: boolean, includeSubagents?: boolean, headerRing?: boolean }>',
419
428
  description: "Flip the running-mark switch. The running fact itself is official and is never stored here.",
420
429
  summary: "Set the running-mark switch.",
421
430
  jsDoc: "/** Set the running-mark switch. */",
package/lib/unread.js CHANGED
@@ -75,6 +75,31 @@ export const UNREAD_REVEAL_DEFAULT = true
75
75
  */
76
76
  export const UNREAD_APP_BADGE_DEFAULT = true
77
77
 
78
+ /**
79
+ * 默认:子代理引起的结束也算未读。
80
+ *
81
+ * 一个后台子代理结算时会**唤醒父会话跑一个回合**(官方把一条
82
+ * `source.kind === 'subagent-settled'` 的 user 消息投进父会话的日志),
83
+ * 父会话因此亮红点。这一项就是那个"我不关心子代理替我跑出来的结果"的开关:
84
+ * 关掉它,只屏蔽**由子代理唤醒**的那一轮结束,人自己发起的一轮照旧。
85
+ *
86
+ * 开关只改客户端的判定,折叠出来的**事实**里两样都在(见下面的
87
+ * `subagentCaused`),所以来回拨开关永远不需要重折日志。
88
+ */
89
+ export const UNREAD_INCLUDE_SUBAGENTS_DEFAULT = true
90
+
91
+ /**
92
+ * 一条 `user/message` 的 `source.kind` 里,哪些明确是"子代理在说话":
93
+ * * `subagent-settled` —— 运行时替子代理交代它结束了(唤醒父会话的那一条);
94
+ * * `agent-message` —— 子代理自己用 `send_message` 发过来的话。
95
+ * 人在输入框里说话是 `user`,所以这两条之外都不算。
96
+ */
97
+ export function isSubagentWakeSource(message) {
98
+ const source = message !== null && typeof message === 'object' && message.source !== null && typeof message.source === 'object' ? message.source : null
99
+ if (source === null) return false
100
+ return source.kind === 'subagent-settled' || source.kind === 'agent-message'
101
+ }
102
+
78
103
  /**
79
104
  * 默认:进入会话即视为已读(0 秒)。
80
105
  * 大于 0 时,你必须在那个会话里**连续**待满这么多秒它才算"看过";中途切走会
@@ -110,8 +135,8 @@ export function normalizeReasons(input) {
110
135
 
111
136
  /**
112
137
  * 把任意输入夹成完整设置。
113
- * @param input - `{ reasons?, pending?, workspaceDot?, appBadge?, clearDelaySec?, revealOnOpen? }`,可以是脏的、缺项的、null
114
- * @returns `{ reasons, pending, workspaceDot, appBadge, clearDelaySec, revealOnOpen }` —— 每一项都完整
138
+ * @param input - `{ reasons?, pending?, workspaceDot?, appBadge?, clearDelaySec?, revealOnOpen?, unreadIncludeSubagents? }`,可以是脏的、缺项的、null
139
+ * @returns `{ reasons, pending, workspaceDot, appBadge, clearDelaySec, revealOnOpen, unreadIncludeSubagents }` —— 每一项都完整
115
140
  */
116
141
  export function normalizeUnreadConfig(input) {
117
142
  const source = input !== null && typeof input === 'object' ? input : {}
@@ -121,7 +146,10 @@ export function normalizeUnreadConfig(input) {
121
146
  workspaceDot: typeof source.workspaceDot === 'boolean' ? source.workspaceDot : UNREAD_WORKSPACE_DOT_DEFAULT,
122
147
  appBadge: typeof source.appBadge === 'boolean' ? source.appBadge : UNREAD_APP_BADGE_DEFAULT,
123
148
  clearDelaySec: normalizeClearDelay(source.clearDelaySec),
124
- revealOnOpen: typeof source.revealOnOpen === 'boolean' ? source.revealOnOpen : UNREAD_REVEAL_DEFAULT
149
+ revealOnOpen: typeof source.revealOnOpen === 'boolean' ? source.revealOnOpen : UNREAD_REVEAL_DEFAULT,
150
+ // 缺项或非 boolean 一律回落 true(等于现状):一个没听懂这一项的旧客户端
151
+ // 不该因为宿主多了一个字段就少看见红点。
152
+ unreadIncludeSubagents: typeof source.unreadIncludeSubagents === 'boolean' ? source.unreadIncludeSubagents : UNREAD_INCLUDE_SUBAGENTS_DEFAULT
125
153
  }
126
154
  }
127
155
 
@@ -140,43 +168,68 @@ export function reasonKey(reason) {
140
168
 
141
169
  /** 折叠初始态:还没有任何回合结束过。 */
142
170
  export function initLastTurnEnd() {
143
- return { endAt: 0, reason: null }
171
+ return { endAt: 0, reason: null, subagentCaused: false, wakeSubagent: null }
144
172
  }
145
173
 
146
174
  /**
147
- * 纯转移:只有 `turn/end` 改动它,其余事件原样返回同一个引用
148
- * (契约要求 `Object.is` 不变以产生零下游工作)。
175
+ * 纯转移。
176
+ *
177
+ * 除了 `turn/end`,另有两类事件会改动它,因为"这一轮是谁叫醒的"必须先被记下来:
178
+ * * `user/message` —— 观察到的**第一条** user 消息就是这一轮的起因
179
+ * (`subagent-settled` / `agent-message` = 子代理,其余 = 人)。之后 steer
180
+ * 进来的消息不算:第一票定终身,人自己发起的一轮不会因为子代理在中途
181
+ * 插了句话就变成"子代理引起";
182
+ * * `turn/start` —— 清掉上一轮的起因,新的一轮重新投票。
183
+ * 与折叠单元无关的事件仍原样返回同一个引用(契约要求 `Object.is` 不变以产生
184
+ * 零下游工作)。
149
185
  * @param state - 覆盖此前全部事件的状态
150
186
  * @param event - 下一个已提交的会话事件
151
187
  * @returns 下一个状态(事件与本单元无关时返回同一个引用)
152
188
  */
153
189
  export function applyLastTurnEnd(state, event) {
154
- if (event === null || typeof event !== 'object' || event.type !== 'turn/end') return state
190
+ if (event === null || typeof event !== 'object') return state
191
+ const current = state !== null && typeof state === 'object' ? state : initLastTurnEnd()
192
+ const wake = typeof current.wakeSubagent === 'boolean' ? current.wakeSubagent : null
193
+ if (event.type === 'user/message') {
194
+ if (wake !== null) return state
195
+ return { ...current, wakeSubagent: isSubagentWakeSource(event.data) }
196
+ }
197
+ if (event.type === 'turn/start') {
198
+ if (wake === null) return state
199
+ return { ...current, wakeSubagent: null }
200
+ }
201
+ if (event.type !== 'turn/end') return state
155
202
  const data = event.data !== null && typeof event.data === 'object' ? event.data : {}
156
203
  const reason = reasonKey(data.reason)
157
204
  const endAt = typeof event.time === 'number' ? event.time : 0
158
- if (state.endAt === endAt && state.reason === reason) return state
159
- return { endAt, reason }
205
+ const subagentCaused = wake === true
206
+ if (current.endAt === endAt && current.reason === reason && (current.subagentCaused === true) === subagentCaused && wake === null) return state
207
+ return { endAt, reason, subagentCaused, wakeSubagent: null }
160
208
  }
161
209
 
162
210
  /** 状态 → 上线载荷。 */
163
211
  export function lastTurnEndView(state) {
164
212
  return {
165
213
  endAt: typeof state.endAt === 'number' ? state.endAt : 0,
166
- reason: typeof state.reason === 'string' && state.reason !== '' ? state.reason : null
214
+ reason: typeof state.reason === 'string' && state.reason !== '' ? state.reason : null,
215
+ // 这一轮是不是被"子代理结束"叫醒的。老检查点里没有这一项 → false(=现状)。
216
+ subagentCaused: state !== null && typeof state === 'object' && state.subagentCaused === true
167
217
  }
168
218
  }
169
219
 
170
- /** 持久化折叠状态(纯 JSON,供投影缓存检查点)。 */
220
+ /** 持久化折叠状态(纯 JSON,供投影缓存检查点)。新增的字段可选:老检查点照样能读。 */
171
221
  export const lastTurnEndStateSchema = z.object({
172
222
  endAt: z.number(),
173
- reason: z.string().nullable()
223
+ reason: z.string().nullable(),
224
+ subagentCaused: z.boolean().optional(),
225
+ wakeSubagent: z.boolean().nullable().optional()
174
226
  })
175
227
 
176
228
  /** 上线载荷。 */
177
229
  export const lastTurnEndViewSchema = z.object({
178
230
  endAt: z.number(),
179
- reason: z.string().nullable()
231
+ reason: z.string().nullable(),
232
+ subagentCaused: z.boolean()
180
233
  })
181
234
 
182
235
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cowwo/dsh-icon-custom",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "自定义浏览器标签页图标:在设置页上传 SVG/PNG/ICO,立即生效并持久保存,自动适配安卓/iOS/平板 PWA 图标,显示未读红点与进行中黄点(工作区数字/会话行/左上角/标签页,数字可点开清单、点条目定位到侧栏),并可把未读数显示为系统应用图标角标(Windows 任务栏 / macOS Dock,含通知权限申请)。· Custom browser tab favicon: upload SVG/PNG/ICO from the settings page, applies instantly and persists, auto-sizes PWA icons for Android/iOS/tablets, shows unread red dots and running yellow marks (workspace number / session row / top-left / tab icon, both counts open a list whose rows locate the session in the sidebar), and projects the unread count onto the system app-icon badge (Windows taskbar / macOS Dock, notifications permission included).",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "license": "MIT",
8
8
  "scripts": {
9
- "test": "node test/app-badge.mjs && node test/notification-permission.mjs && node test/unread-panel.mjs && node test/unread-clear.mjs && node test/unread-rows.mjs && node test/unread-seen.mjs && node test/unread-host-sync.mjs && node test/running-mark.mjs && node test/running-panel.mjs && node test/reveal.mjs"
9
+ "test": "node test/app-badge.mjs && node test/notification-permission.mjs && node test/unread-panel.mjs && node test/unread-clear.mjs && node test/unread-rows.mjs && node test/unread-seen.mjs && node test/unread-host-sync.mjs && node test/running-mark.mjs && node test/running-panel.mjs && node test/running-subagents.mjs && node test/header-ring.mjs && node test/unread-subagents.mjs && node test/reveal.mjs && node test/reveal-persist.mjs && node test/hint-disclosure.mjs"
10
10
  },
11
11
  "dependencies": {
12
12
  "jimp": "^1.6.1",