@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/client/client.js CHANGED
@@ -15,6 +15,7 @@ window.__ModuleLoader__.load({
15
15
  none: "使用平台默认图标",
16
16
  upload: "上传图标",
17
17
  uploadHint: "支持 SVG、PNG、ICO,最大 10 MB。SVG 会自动转成 PNG,自动适配安卓/iOS/平板的主屏图标。上传后立即生效,刷新页面也会保留。",
18
+ uploadHintSummary: "支持 SVG/PNG/ICO,≤10 MB,上传即生效。",
18
19
  svgFail: "该 SVG 无法转换,请改用 PNG。",
19
20
  reset: "恢复默认",
20
21
  resetHint: "清除自定义图标并回到平台默认图标。",
@@ -28,12 +29,15 @@ window.__ModuleLoader__.load({
28
29
  successReset: "已恢复默认图标",
29
30
  pwa: "同时替换 PWA 安装图标",
30
31
  pwaHint: "安装为应用(添加到主屏幕)后,桌面上的图标也换成这张;已安装的应用可能需要重新安装才会更新。",
32
+ pwaHintSummary: "装成应用后桌面图标也换成这张,可能要重装。",
31
33
  successPwa: "PWA 图标设置已更新",
32
34
  logo: "同时替换页面 Logo 图标",
33
35
  logoHint: "把网页左上角品牌 logo 里的鲸鱼图标换成这张(不影响 deepseek HARNESS 文字)。",
36
+ logoHintSummary: "把左上角品牌 logo 里的鲸鱼换成这张。",
34
37
  successLogo: "页面 Logo 已更新",
35
38
  fail: "操作失败",
36
39
  noFile: "请先选择图标文件",
40
+ hintMoreLabel: "查看完整说明",
37
41
  badgeTest: "未读红点",
38
42
  badgeSourceLabel: "数据来源:",
39
43
  badgeSourceReal: "真实未读",
@@ -41,6 +45,7 @@ window.__ModuleLoader__.load({
41
45
  badgeTestUnit: "0 = 不显示",
42
46
  badgeSizeLabel: "红点大小:",
43
47
  badgeSizeHint: "单独出现时按这三档决定大小;红黄两颗数字同时出现时(未读 + 进行中),它们会一起缩到能塞下的最大尺寸 —— 这时三档画出来是一样的,因为画布就这么大。",
48
+ badgeSizeHintSummary: "单独出现按三档;红黄同框时一起缩到能容纳。",
44
49
  badgeSizeSmall: "小",
45
50
  badgeSizeMedium: "中",
46
51
  badgeSizeLarge: "大",
@@ -53,6 +58,9 @@ window.__ModuleLoader__.load({
53
58
  reasonAbortedUser: "我自己停止",
54
59
  reasonAbortedOther: "其他中断",
55
60
  reasonPending: "有人在等我(审批/提问/计划)",
61
+ unreadIncludeSubagentsLabel: "子代理引起的结束也算未读",
62
+ unreadIncludeSubagentsHint: "后台子代理跑完时,运行时会把它所属的会话**再叫醒跑一轮**;这一项决定那一轮结束算不算未读。关掉它:由子代理唤醒的那一轮结束不再亮红点,你自己发起的一轮照旧提醒(子代理自己没有会话行,一直都不算)。打开(默认)= 和以前完全一样。判定依据是官方日志里的那条「子代理结算」消息(`subagent-settled` / `agent-message`),所以一个回合里第一条消息是谁发的就决定了这一轮的归属;**你中途在同一条会话里插话**不会把已经记下的归属改写。",
63
+ unreadIncludeSubagentsHintSummary: "子代理唤醒的那一轮结束算不算未读。默认算。",
56
64
  unreadPanelShort: "待处理",
57
65
  unreadPanelCount: "{n} 个待处理",
58
66
  unreadPanelNone: "暂无待处理",
@@ -76,10 +84,13 @@ window.__ModuleLoader__.load({
76
84
  ageYears: "{n}年",
77
85
  workspaceDotLabel: "在工作区和会话行上显示红点(实验)",
78
86
  workspaceDotHint: "在侧栏工作区那一行的文件夹图标上点一个小红点(收起的工作区里有多少未读,就看它),有未读的会话在其标题右边也跟一个小红点。会话行上的点只在那一行真的显示着时才画:折叠起来的会话由工作区那一行的点和「待处理」清单代表。这一项是贴着页面结构做的,DSH 升级后可能失效;失效时只会变成\"不显示\",不会影响红点数字、清单、跳转这些功能,随时可以关掉它。",
87
+ workspaceDotHintSummary: "工作区文件夹和会话行上加小红点(实验,可能失效)。",
79
88
  revealLabel: "点清单里的会话时,在侧栏定位到它(实验)",
80
89
  revealHint: "点「待处理」或「进行中」清单里的一条时,除了打开那个会话,还会在侧栏把它所在的工作区展开、滚到那一行、并闪一下。这是贴着页面结构做的:官方只把\"展开 + 滚动\"留给了它自己的搜索框,没有给插件留接口,所以这里只能模拟点击官方的工作区行和「显示更多」。DSH 升级后可能失效,失效的表现只是\"侧栏不动\",打开会话完全不受影响。**展开状态是官方自己记住的**(存在浏览器里),所以它不会自己收回去——不想要就手动收起那个工作区,或者关掉这个开关。",
90
+ revealHintSummary: "点清单时在侧栏展开并闪一下(实验,可能失效)。",
81
91
  appBadgeLabel: "在系统应用图标上显示红点",
82
92
  appBadgeHint: "安装为应用后,把同一个数字也放到系统图标上:Windows 是任务栏图标上的角标,macOS 是 Dock 角标,iOS 是主屏图标上的数字。样子由系统决定——Windows 上 Chrome 画深色圆+白字,Edge 走 Windows 系统徽章通道(是否显示数字由 Edge/Windows 决定);插件会周期重设数字,但只在应用窗口开着时更新。Safari 的程序坞网页应用 / 主屏应用还要先允许通知(见下面的「通知权限」),否则角标不会出现。",
93
+ appBadgeHintSummary: "把同一个数字也放到系统应用图标上。",
83
94
  appBadgeFootnote: "角标只在应用窗口开着时由本插件更新;数字超过 99 时由系统显示为 99+。",
84
95
  appBadgeProbeInstalled: "当前环境:已安装为应用,本机可以设置角标。是否真正显示由系统决定。",
85
96
  appBadgeProbeTab: "当前环境:浏览器标签页(不是安装的应用)——安装为应用后才会显示角标。",
@@ -92,15 +103,25 @@ window.__ModuleLoader__.load({
92
103
  notificationPermissionUnsupported: "本机浏览器没有通知能力",
93
104
  notificationPermissionButton: "允许通知",
94
105
  notificationPermissionHint: "Chrome / Edge 的角标不需要这一步;Safari 不一样——macOS 的「添加到程序坞」网页应用和 iOS 的主屏应用,只有在你允许通知之后才会画出角标(Apple 把角标权限绑在通知权限上)。这里只申请权限,本插件目前不会给你发通知。",
95
- badgeTestHint: "真实未读 = 有会话发生了上面勾选的情况、而且你还没看过它(打开该会话即视为已读)。你正开着的会话也算——窗口可能被最小化、页面可能切到后台,这里无从判断你在不在看,所以不做这个区分。只有子代理不计。手动模式:自己填数字试看效果。刷新页面后回到真实未读。",
106
+ notificationPermissionHintSummary: "只有 Safari 系(iOS/macOS)需要先允许通知。",
107
+ badgeTestHint: "真实未读 = 有会话发生了上面勾选的情况、而且你还没看过它(打开该会话即视为已读)。你正开着的会话也算——窗口可能被最小化、页面可能切到后台,这里无从判断你在不在看,所以不做这个区分。子代理是否计入,由上方「子代理引起的结束也算未读」开关决定。手动模式:自己填数字试看效果。刷新页面后回到真实未读。",
108
+ badgeTestHintSummary: "真实未读 = 勾选情况发生且你没看过;子代理看上方的开关。",
96
109
  clearDelayLabel: "进入会话后多久算已读:",
97
110
  clearDelayUnit: "秒",
98
111
  clearDelayHint: "填 0 = 进去就算已读(红点立即消失)。大于 0 时,你要在那个会话里连续待满这么多秒它才算看过:中途切走会清零重数,刷新页面也重新数。**计时只认\"你进入会话时已经存在的结束\"**——你人已经在里面的时候又跑完一轮,那一轮不算数:它会一直亮着,等你切走再进来才会清。上限 600 秒。**切到「插件/设置」页、或把标签页切到后台,都只是暂停计时,不会因此清掉红点**(它们不算离开);真正算离开的只有一件事:**去别的会话**。已读记在宿主($DSH_HOME/custom-favicon/unread-seen.json):任何一台设备上看过,所有设备都不再提醒;浏览器那份只是缓存,清站点数据不会把已读丢回去。",
112
+ clearDelayHintSummary: "0 = 进去即已读;>0 要连续待满,上限 600 秒。",
99
113
  /** 进行中黄点(0.18.0):官方 running 的第二次投影,与红点互不牵连。 */
100
114
  runningMarkSection: "进行中黄点",
101
115
  runningMarkLabel: "显示进行中标记(标题数字 / 文件夹与会话行 / 左上角 / 标签页)",
102
116
  runningMarkCount: "{n} 个进行中",
103
- runningMarkHint: "黄点就是官方的「进行中」:会话有回合正在跑、而且没有停下来等你(停下等你的时候,官方那一行显示琥珀色的「等待审批 / 等待回答」,黄点也就不亮)。它和未读红点完全独立、各画各的:侧栏「工作区」标题后面是黄色数字——此刻全局有几个会话在跑(和红数字上下排:右上红、右下黄);工作区文件夹图标上是黄点,有未读时红点在左、黄点在右;会话行上同样是红点在左、黄点在右;左上角 Logo 与标签页图标右下角也是同一个数字,红数字仍占右上角。事实全部来自官方(会话列表里每个会话的 `running`)——插件不落盘、不推断,所以刷新页面立刻是对的,进程重启也不会留下一个永远亮着的黄点。关掉这个开关,红点完全不受影响。"
117
+ runningMarkHint: "黄点就是官方的「进行中」:会话有回合正在跑、而且没有停下来等你(停下等你的时候,官方那一行显示琥珀色的「等待审批 / 等待回答」,黄点也就不亮)。它和未读红点完全独立、各画各的:侧栏「工作区」标题后面是黄色数字——此刻全局有几个会话在跑(和红数字上下排:右上红、右下黄);工作区文件夹图标上是黄点,有未读时红点在左、黄点在右;会话行上同样是红点在左、黄点在右;左上角 Logo 与标签页图标右下角也是同一个数字,红数字仍占右上角。事实全部来自官方(会话列表里每个会话的 `running`)——插件不落盘、不推断,所以刷新页面立刻是对的,进程重启也不会留下一个永远亮着的黄点。关掉这个开关,红点完全不受影响。",
118
+ runningMarkHintSummary: "黄点就是官方的进行中,和红点各自独立、互不影响。",
119
+ runningIncludeSubagentsLabel: "把子代理的「进行中」算到它所属的会话上",
120
+ runningIncludeSubagentsHint: "子代理(子会话)在侧栏里没有自己的那一行,所以它正在跑的时候,它上面那个真正属于你的会话看上去是空闲的。打开这个开关:有子代理在跑,就把最顶层那个不是子代理的祖先会话记为进行中——黄点/黄数字反映的是「有活正在干」,而不只是「你亲手开的会话在跑」。同一个祖先下好几个子代理同时跑,只算一次(数字始终是会话数,不是子代理数);子代理自己那一行照旧不画。祖先会话正在等你(官方那行是琥珀色)时不计。关掉就完全回到旧行为:所有子代理一律不计。",
121
+ runningIncludeSubagentsHintSummary: "子代理在跑就把最顶层祖先会话记为进行中。",
122
+ headerRingLabel: "在会话头部的子代理计数上叠一个黄圈",
123
+ headerRingHint: "聊天区顶部那颗「N 个子智能体」的计数,右上角会叠一个小黄圈:只在**有直接子代理正在运行**时出现,里面的数字就是它们此刻的个数——不含更深一层的孙代理,同一个直接子代理只算一次,超过 99 显示 `99+`。它只是个数字:**不能点**,也不拦截任何点击,更不会改动画面的排版;会话头部那颗计数本身的文字和点击照旧,官方按钮的朗读里已经含运行数,所以它对着屏幕阅读器是隐藏的。关掉这个开关,其他地方的红黄点、左上角数字、标签页图标与系统角标一个都不受影响。子代理自己的会话页不显示官方那行,这个黄圈自然也不会出现。",
124
+ headerRingHintSummary: "只在有直接子代理在跑时,会话头部计数右上角叠一个黄圈。"
104
125
  };
105
126
  const en = {
106
127
  nav: "Favicon",
@@ -108,6 +129,7 @@ window.__ModuleLoader__.load({
108
129
  none: "Using the platform default icon",
109
130
  upload: "Upload",
110
131
  uploadHint: "SVG, PNG or ICO, up to 10 MB. SVG is auto-rasterized to PNG, and sizes are derived to fit Android/iOS/tablet home-screen icons. Applies instantly and persists across reloads.",
132
+ uploadHintSummary: "SVG, PNG or ICO up to 10 MB; applies at once and persists.",
111
133
  svgFail: "This SVG could not be rasterized. Use a PNG instead.",
112
134
  reset: "Reset",
113
135
  resetHint: "Clear the custom icon and use the platform default.",
@@ -121,12 +143,15 @@ window.__ModuleLoader__.load({
121
143
  successReset: "Default icon restored",
122
144
  pwa: "Also replace the PWA app icon",
123
145
  pwaHint: "The icon used after installing the site as an app (add to home screen). Already-installed apps may need a reinstall to pick it up.",
146
+ pwaHintSummary: "The icon after installing as an app; may need a reinstall.",
124
147
  successPwa: "PWA icon setting updated",
125
148
  logo: "Also replace the page logo mark",
126
149
  logoHint: "Replaces the whale mark beside the brand name in the top-left (the deepseek HARNESS text stays).",
150
+ logoHintSummary: "Swaps the whale mark inside the top-left brand logo.",
127
151
  successLogo: "Page logo updated",
128
152
  fail: "Operation failed",
129
153
  noFile: "Choose a file first",
154
+ hintMoreLabel: "Show full description",
130
155
  badgeTest: "Unread badge",
131
156
  badgeSourceLabel: "Number source:",
132
157
  badgeSourceReal: "Real unread",
@@ -134,6 +159,7 @@ window.__ModuleLoader__.load({
134
159
  badgeTestUnit: "0 = hidden",
135
160
  badgeSizeLabel: "Badge size:",
136
161
  badgeSizeHint: "A number shown on its own uses this size; when the red and the yellow number share the icon (unread + running) they shrink together to the largest pair that fits — the three sizes then draw the same, because the canvas is the constraint.",
162
+ badgeSizeHintSummary: "Used when alone; both numbers shrink together to fit.",
137
163
  badgeSizeSmall: "Small",
138
164
  badgeSizeMedium: "Medium",
139
165
  badgeSizeLarge: "Large",
@@ -146,6 +172,9 @@ window.__ModuleLoader__.load({
146
172
  reasonAbortedUser: "I stopped it",
147
173
  reasonAbortedOther: "Other cancel",
148
174
  reasonPending: "Someone is waiting",
175
+ unreadIncludeSubagentsLabel: "Count sub-agent activity as unread",
176
+ unreadIncludeSubagentsHint: "When a background sub-agent finishes, the runtime **wakes its parent session for one more turn**; this decides whether that turn's ending lights the red dot. Off: endings caused by a sub-agent stop lighting it, while a turn you started still notifies you (a sub-agent has no session row of its own and never counted). On (default) = exactly the old behaviour. The judgement comes from the official settlement notice in the log (`subagent-settled` / `agent-message`), so whichever message opened a turn owns that turn; typing into the same session halfway through does not rewrite an attribution already recorded.",
177
+ unreadIncludeSubagentsHintSummary: "Whether a sub-agent-woken turn counts as unread; on by default.",
149
178
  unreadPanelShort: "Pending",
150
179
  unreadPanelCount: "{n} pending",
151
180
  unreadPanelNone: "Nothing pending",
@@ -169,10 +198,13 @@ window.__ModuleLoader__.load({
169
198
  ageYears: "{n}y",
170
199
  workspaceDotLabel: "Dots on workspace and session rows (experimental)",
171
200
  workspaceDotHint: "Adds a small red dot to the folder icon of each workspace row (a collapsed workspace shows how many of its sessions are unread there), and one beside the title of every unread session row. A session dot is only drawn while that row is really rendered: folded sessions are represented by their workspace row's dot and the pending panel. This one reads the page structure, so a DSH upgrade may break it; when it does it simply stops showing, never affecting the counts, the list, or navigation. Turn it off any time.",
201
+ workspaceDotHintSummary: "Small dots on workspace folders and session rows (experimental).",
172
202
  revealLabel: "Locate the session in the sidebar when a list row is clicked (experimental)",
173
203
  revealHint: "Clicking a row in the \"needing you\" or \"running\" list opens that session AND, in the sidebar, expands its workspace, scrolls to the row and flashes it once. This reads the page structure: the official sidebar keeps expand-and-scroll for its own search box and exposes no interface for it, so this drives the shipped workspace row and \"show more\" button the way a click would. A DSH upgrade may break it, and the only symptom is a sidebar that does not move — opening the session is never affected. **The expanded state is remembered by the official sidebar itself** (in the browser), so it will not collapse again on its own: collapse that workspace by hand, or turn this off.",
204
+ revealHintSummary: "Expands and flashes the session's sidebar row (experimental).",
174
205
  appBadgeLabel: "Badge the system app icon",
175
206
  appBadgeHint: "Once installed as an app, the same number also goes to the system icon: a taskbar badge on Windows, a Dock badge on macOS, a number on the iOS home-screen icon. The system decides how it looks — on Windows, Chrome draws a dark circle with white text, while Edge goes through the Windows badge channel (whether it shows the number is up to Edge/Windows). The plugin re-asserts the number periodically, but only while the app window is open. Safari's Dock and home-screen web apps additionally need notifications allowed first (see the permission row below), or the badge never appears.",
207
+ appBadgeHintSummary: "Puts the same number onto the system app icon.",
176
208
  appBadgeFootnote: "The badge is only updated by this plugin while the app window is open; above 99 the system shows 99+.",
177
209
  appBadgeProbeInstalled: "This environment: installed as an app, so this device can set a badge. Whether it actually appears is up to the system.",
178
210
  appBadgeProbeTab: "This environment: a browser tab (not an installed app) — install as an app to get a badge.",
@@ -185,15 +217,25 @@ window.__ModuleLoader__.load({
185
217
  notificationPermissionUnsupported: "this browser has no Notification API",
186
218
  notificationPermissionButton: "Allow notifications",
187
219
  notificationPermissionHint: "A badge needs no notification permission on Chrome / Edge, but Safari is different: a macOS web app added to the Dock (or an iOS home-screen web app) only draws its badge once you allow notifications — Apple ties the two together. This only asks for the permission; the plugin does not send notifications.",
188
- badgeTestHint: "Real unread = a session ended for one of the checked reasons and you have not looked at it yet (opening a session marks it read). The session you are viewing counts too — the window may be minimised or the page in the background, so this half cannot tell whether you are looking, and does not pretend to. Only sub-agents never count. Manual = type a number to preview. Resets to Real on reload.",
220
+ notificationPermissionHintSummary: "Only Safari (iOS/macOS) needs notifications allowed first.",
221
+ badgeTestHint: "Real unread = a session ended for one of the checked reasons and you have not looked at it yet (opening a session marks it read). The session you are viewing counts too — the window may be minimised or the page in the background, so this half cannot tell whether you are looking, and does not pretend to. Whether sub-agents count is decided by the \"Count sub-agent activity as unread\" switch above. Manual = type a number to preview. Resets to Real on reload.",
222
+ badgeTestHintSummary: "Real unread = a checked ending you have not seen; sub-agents follow the switch above.",
189
223
  clearDelayLabel: "Mark read after staying:",
190
224
  clearDelayUnit: "seconds",
191
225
  clearDelayHint: "0 = read as soon as you enter (the dot clears at once). Above 0 you must stay in that session for this many seconds before it counts as read: leaving it resets the clock, and so does a reload. The clock only ever covers **the ending that was already there when you entered** — a turn that finishes while you are sitting in the session does not count: it stays red until you leave and come back. Capped at 600 seconds. A settings or plugins page, or a tab in the background, only **pauses** the clock — neither clears a dot (neither counts as leaving); the one thing that does count as leaving is **opening another session**. Read state lives on the Host ($DSH_HOME/custom-favicon/unread-seen.json): read on any device means read on every device, and the browser's own copy is only a cache — clearing site data no longer throws it away.",
226
+ clearDelayHintSummary: "0 = read at once; above 0, stay that long (max 600s).",
192
227
  /** Running mark (0.18.0): a second projection of the official `running`. */
193
228
  runningMarkSection: "Running marks",
194
229
  runningMarkLabel: "Show running marks (header number / folder and session rows / top-left / tab)",
195
230
  runningMarkCount: "{n} running",
196
- runningMarkHint: "The yellow mark IS the official \"running\": the session has a turn in flight and is not parked waiting for you (while it waits, the official row shows the amber \"waiting for approval / waiting for answer\" and the yellow goes dark). It is completely independent of the unread red dot, and each draws its own mark: after the sidebar's \"工作区\" label sits the yellow NUMBER of sessions running right now (stacked against the red number — red top-right, yellow bottom-right); the workspace folder icon gets a yellow dot with the red dot to its left; a session row is red left, yellow right; and the top-left logo plus the bottom-right of the tab icon carry the same number, with the red number keeping its top-right corner. Every fact comes from the official host (each session's `running` in the session list); the plugin stores nothing and infers nothing, so a reload is immediately right and a process restart never leaves a yellow mark stuck on. Turning this off does not affect the red dot at all."
231
+ runningMarkHint: "The yellow mark IS the official \"running\": the session has a turn in flight and is not parked waiting for you (while it waits, the official row shows the amber \"waiting for approval / waiting for answer\" and the yellow goes dark). It is completely independent of the unread red dot, and each draws its own mark: after the sidebar's \"工作区\" label sits the yellow NUMBER of sessions running right now (stacked against the red number — red top-right, yellow bottom-right); the workspace folder icon gets a yellow dot with the red dot to its left; a session row is red left, yellow right; and the top-left logo plus the bottom-right of the tab icon carry the same number, with the red number keeping its top-right corner. Every fact comes from the official host (each session's `running` in the session list); the plugin stores nothing and infers nothing, so a reload is immediately right and a process restart never leaves a yellow mark stuck on. Turning this off does not affect the red dot at all.",
232
+ runningMarkHintSummary: "The yellow mark is the official running state, independent of red dots.",
233
+ runningIncludeSubagentsLabel: "Count sub-agent activity under the session that owns it",
234
+ runningIncludeSubagentsHint: "A sub-agent (child session) has no row of its own in the sidebar, so while it works the real session above it looks idle. Turn this on: when a sub-agent is running, the topmost non-sub-agent ancestor session is credited as running — the yellow mark and number then reflect \"work is in flight\", not merely \"a session you opened is running\". Several sub-agents under one ancestor count once (the number stays a session count, never a sub-agent count), and the sub-agent row itself is still never drawn. An ancestor that is itself parked waiting for you (the official row is amber) is not counted. Turn it off and the old behaviour returns exactly: every sub-agent is ignored.",
235
+ runningIncludeSubagentsHintSummary: "Credits the topmost ancestor session while a sub-agent runs.",
236
+ headerRingLabel: "Draw a yellow ring on the header's sub-agent count",
237
+ headerRingHint: "The \"N sub-agents\" count at the top of the chat area gets a small yellow ring on its top-right corner: it appears only while **direct sub-agents are running**, and the number inside is how many of them are active right now — it never counts deeper grandchildren, and one direct sub-agent counts once; above 99 it reads `99+`. It is a number only: **it is not clickable**, it swallows no pointer events, and it never changes the layout; the header count keeps its own text and its own click, and because the official button's label already announces the running count the ring is hidden from screen readers. Turning this switch off leaves every other red/yellow mark, the top-left number, the tab icon and the system badge untouched. A sub-agent's own conversation renders none of that header row, so no ring appears there either.",
238
+ headerRingHintSummary: "A yellow ring on the header count while direct sub-agents run."
197
239
  };
198
240
  //#endregion
199
241
 
@@ -313,7 +355,9 @@ window.__ModuleLoader__.load({
313
355
  const UNREAD_APP_BADGE_FALLBACK = true;
314
356
  /** Mirror of the host's default (see lib/unread.js) — locating a clicked row is on. */
315
357
  const UNREAD_REVEAL_FALLBACK = true;
316
- let unreadConfig = { reasons: { ...UNREAD_FALLBACK_REASONS }, pending: true, workspaceDot: true, appBadge: UNREAD_APP_BADGE_FALLBACK, clearDelaySec: 0, revealOnOpen: UNREAD_REVEAL_FALLBACK };
358
+ /** Mirror of the host's default (see lib/unread.js) — sub-agent-caused endings count as unread. */
359
+ const UNREAD_INCLUDE_SUBAGENTS_FALLBACK = true;
360
+ let unreadConfig = { reasons: { ...UNREAD_FALLBACK_REASONS }, pending: true, workspaceDot: true, appBadge: UNREAD_APP_BADGE_FALLBACK, clearDelaySec: 0, revealOnOpen: UNREAD_REVEAL_FALLBACK, unreadIncludeSubagents: UNREAD_INCLUDE_SUBAGENTS_FALLBACK };
317
361
  const unreadConfigListeners = new Set();
318
362
  /** Mirror of the host's normalizer: any input becomes an integer 0..cap seconds. */
319
363
  function normalizeClearDelay(input) {
@@ -336,7 +380,12 @@ window.__ModuleLoader__.load({
336
380
  workspaceDot: typeof source.workspaceDot === "boolean" ? source.workspaceDot : true,
337
381
  appBadge: typeof source.appBadge === "boolean" ? source.appBadge : UNREAD_APP_BADGE_FALLBACK,
338
382
  clearDelaySec: normalizeClearDelay(source.clearDelaySec),
339
- revealOnOpen: typeof source.revealOnOpen === "boolean" ? source.revealOnOpen : UNREAD_REVEAL_FALLBACK
383
+ revealOnOpen: typeof source.revealOnOpen === "boolean" ? source.revealOnOpen : UNREAD_REVEAL_FALLBACK,
384
+ // Missing or dirty falls back to ON (= today's behaviour): an old host that
385
+ // never heard of this field must not make the badge quieter. Spelled as a
386
+ // literal like `pending` / `workspaceDot` above, so a partial slice of this
387
+ // normalizer (the tests take exactly this function) needs no extra constant.
388
+ unreadIncludeSubagents: typeof source.unreadIncludeSubagents === "boolean" ? source.unreadIncludeSubagents : true
340
389
  };
341
390
  }
342
391
  function emitUnreadConfig(input) {
@@ -365,11 +414,35 @@ window.__ModuleLoader__.load({
365
414
  const RUNNING_YELLOW = "#f5c518";
366
415
  /** Mirror of the host default (lib/running.js). */
367
416
  const RUNNING_MARK_FALLBACK = true;
368
- let runningConfig = { enabled: RUNNING_MARK_FALLBACK };
417
+ /**
418
+ * Mirror of the host's second running default (lib/running.js): rolling a
419
+ * running sub-agent up to the ancestor session that owns it is ON. The two
420
+ * questions are independent — whether to draw the marks at all is not the same
421
+ * as whether work you cannot see should light the session it belongs to.
422
+ */
423
+ const RUNNING_INCLUDE_SUBAGENTS_FALLBACK = true;
424
+ /**
425
+ * Mirror of the host's third running default (lib/running.js): the yellow ring
426
+ * on the conversation header's sub-agent count is ON. A third, independent
427
+ * question — the two above decide whether marks are drawn at all and whether a
428
+ * sub-agent's work is credited to its ancestor; this one only decides whether
429
+ * that one header count also carries a running number. It feeds no other count.
430
+ */
431
+ const RUNNING_HEADER_RING_FALLBACK = true;
432
+ let runningConfig = { enabled: RUNNING_MARK_FALLBACK, includeSubagents: RUNNING_INCLUDE_SUBAGENTS_FALLBACK, headerRing: RUNNING_HEADER_RING_FALLBACK };
369
433
  const runningConfigListeners = new Set();
370
434
  function normalizeRunningConfig(input) {
371
435
  const source = input !== null && typeof input === "object" ? input : {};
372
- return { enabled: typeof source.enabled === "boolean" ? source.enabled : RUNNING_MARK_FALLBACK };
436
+ // `typeof` (not a bare read) keeps this normalizer usable when a regression
437
+ // test lifts it out of the module: an absent mirror is simply the default.
438
+ const enabledFallback = typeof RUNNING_MARK_FALLBACK === "boolean" ? RUNNING_MARK_FALLBACK : true;
439
+ const includeFallback = typeof RUNNING_INCLUDE_SUBAGENTS_FALLBACK === "boolean" ? RUNNING_INCLUDE_SUBAGENTS_FALLBACK : true;
440
+ const headerRingFallback = typeof RUNNING_HEADER_RING_FALLBACK === "boolean" ? RUNNING_HEADER_RING_FALLBACK : true;
441
+ return {
442
+ enabled: typeof source.enabled === "boolean" ? source.enabled : enabledFallback,
443
+ includeSubagents: typeof source.includeSubagents === "boolean" ? source.includeSubagents : includeFallback,
444
+ headerRing: typeof source.headerRing === "boolean" ? source.headerRing : headerRingFallback
445
+ };
373
446
  }
374
447
  function emitRunningConfig(input) {
375
448
  runningConfig = normalizeRunningConfig(input);
@@ -787,7 +860,17 @@ window.__ModuleLoader__.load({
787
860
  /**
788
861
  * How many sessions deserve the badge right now: endings whose reason is
789
862
  * enabled and that this browser has not seen since, plus sessions waiting
790
- * for you (when that source is enabled). Only sub-agent sessions never count.
863
+ * for you (when that source is enabled). Sub-agent sessions never count on
864
+ * their own; whether an ending caused by a sub-agent's settlement counts is
865
+ * decided by the `unreadIncludeSubagents` rule, not by this function.
866
+ *
867
+ * A PARENT session woken by a sub-agent settling is a second kind of
868
+ * sub-agent-caused unread: the child has no row of its own, but its
869
+ * settlement notice makes the parent run a turn, and that turn's ending used
870
+ * to light the parent's dot. The fold marks those endings
871
+ * (`subagentCaused`, see lib/unread.js) instead of hiding them, and this
872
+ * function drops them only while the rule says so — the fact stays in the
873
+ * projection either way, so flipping the switch never re-folds a log.
791
874
  *
792
875
  * The session you are LOOKING AT counts too. It used to be excluded, on the
793
876
  * theory that "you have it open, so you have seen it" — but that premise is
@@ -805,6 +888,9 @@ window.__ModuleLoader__.load({
805
888
  */
806
889
  function collectUnread(list, pending, config, seen, lit) {
807
890
  const reasons = config !== null && typeof config === "object" && config.reasons !== null && typeof config.reasons === "object" ? config.reasons : {};
891
+ // Absent, dirty or an old host's answer → ON (= everything this function
892
+ // did before the switch existed).
893
+ const includeSubagents = !(config !== null && typeof config === "object" && config.unreadIncludeSubagents === false);
808
894
  // The watermark is a parameter, not module state: this stays a pure
809
895
  // decision function that a test can drive with any marks it likes.
810
896
  const marks = seen !== null && typeof seen === "object" ? seen : {};
@@ -823,6 +909,12 @@ window.__ModuleLoader__.load({
823
909
  const values = entry.projectionValues;
824
910
  const value = values !== null && typeof values === "object" ? values[LAST_TURN_END_KEY] : undefined;
825
911
  if (value === null || typeof value !== "object") continue;
912
+ // The switch is OFF: an ending the fold attributed to a sub-agent
913
+ // settling is not news for the person sitting here. This is NOT the
914
+ // reason-toggle exception below — that one protects an already lit dot
915
+ // from a vocabulary change, while this one is the user explicitly
916
+ // saying they do not want this class of news at all.
917
+ if (!includeSubagents && value.subagentCaused === true) continue;
826
918
  const endAt = typeof value.endAt === "number" ? value.endAt : 0;
827
919
  const seenAt = marks[id];
828
920
  if (endAt <= 0 || typeof seenAt !== "number" || endAt <= seenAt) continue;
@@ -877,12 +969,20 @@ window.__ModuleLoader__.load({
877
969
  * so it is NOT running here either. That is why a session with the
878
970
  * amber dot never also gets a yellow one.
879
971
  *
880
- * Sub-agent sessions are skipped for the same reason `collectUnread` skips
881
- * them: they are not part of the browsing the user does (ui-workspace's
882
- * `sessionVisible` hides them outright), so counting them would leave the
883
- * mark lit by work nobody can open.
972
+ * Sub-agent sessions get no ids of their own for the same reason
973
+ * `collectUnread` skips them: they are not part of the browsing the user does
974
+ * (ui-workspace's `sessionVisible` hides them outright), so a mark on their row
975
+ * would point at work nobody can open. With `includeSubagents` on (the
976
+ * default), a running sub-agent still credits the topmost non-sub-agent
977
+ * ancestor session that owns it, walking `parentSessionId` upwards: that is the
978
+ * session the user actually opened, and without the roll-up it looks idle while
979
+ * its sub-agents work. Several sub-agents under one ancestor collapse into that
980
+ * single id, so the number stays a count of sessions. The ancestor's own
981
+ * `pendingInteraction` still wins, switch or no switch: a session parked
982
+ * waiting for you is never also "running".
884
983
  * @param list - the session list snapshot (may be absent).
885
984
  * @param status - the official session-status map (may be absent).
985
+ * @param includeSubagents - optional override for the switch; absent means "ask the live config", whose documented default is ON.
886
986
  * @returns the running session ids, in list order.
887
987
  */
888
988
  function runningSessionIds(list, status) {
@@ -893,17 +993,68 @@ window.__ModuleLoader__.load({
893
993
  const lookup = status !== null && status !== undefined && typeof status.get === "function"
894
994
  ? (id) => status.get(id)
895
995
  : () => undefined;
996
+ // The roll-up switch. A third argument wins (the regression tests drive it
997
+ // that way); otherwise the live config answers, and a copy lifted out of the
998
+ // module with no config in scope is the documented default: ON.
999
+ const includeSubagents = arguments.length > 2
1000
+ ? arguments[2] !== false
1001
+ : !(typeof runningConfig === "object" && runningConfig !== null && runningConfig.includeSubagents === false);
1002
+ /** Is this row parked waiting for the user (the amber state)? */
1003
+ const waitingForYou = (id) => {
1004
+ const row = lookup(id);
1005
+ return row !== null && typeof row === "object" && row.pendingInteraction !== null && row.pendingInteraction !== undefined;
1006
+ };
1007
+ /** The official running fact for a row, sidebar precedence, minus the wait state. */
1008
+ const isRunning = (id) => {
1009
+ if (waitingForYou(id)) return false;
1010
+ const row = lookup(id);
1011
+ const known = row !== null && typeof row === "object";
1012
+ const entry = byId[id];
1013
+ const running = known && typeof row.running === "boolean"
1014
+ ? row.running
1015
+ : entry !== null && typeof entry === "object" ? entry.running : undefined;
1016
+ return running === true;
1017
+ };
1018
+ /**
1019
+ * Up the `parentSessionId` chain to the topmost ancestor that is not itself a
1020
+ * sub-agent. A broken, cyclic or unknown chain yields "" (unattributable):
1021
+ * guessing an owner would light a session that has nothing to do with the
1022
+ * sub-agent.
1023
+ */
1024
+ const ownerOf = (id) => {
1025
+ let current = id;
1026
+ const walked = new Set([id]);
1027
+ for (let hops = 0; hops < 64; hops++) {
1028
+ const entry = byId[current];
1029
+ const parent = entry !== null && typeof entry === "object" && typeof entry.parentSessionId === "string" ? entry.parentSessionId : "";
1030
+ if (parent === "" || walked.has(parent)) return "";
1031
+ walked.add(parent);
1032
+ const next = byId[parent];
1033
+ if (next !== null && typeof next === "object" && next.origin === "subagent") {
1034
+ current = parent;
1035
+ continue;
1036
+ }
1037
+ if (next === null || typeof next !== "object") return "";
1038
+ return parent;
1039
+ }
1040
+ return "";
1041
+ };
1042
+ /** Ancestors are deduplicated: two sub-agents under one parent are one session. */
1043
+ const push = (id) => { if (!out.includes(id)) out.push(id); };
896
1044
  for (const id of order) {
897
1045
  if (typeof id !== "string" || id === "") continue;
898
1046
  const entry = byId[id];
899
1047
  if (entry === null || typeof entry !== "object") continue;
900
- if (entry.origin === "subagent") continue;
901
- const row = lookup(id);
902
- const known = row !== null && typeof row === "object";
903
- if (known && row.pendingInteraction !== null && row.pendingInteraction !== undefined) continue;
904
- const running = known && typeof row.running === "boolean" ? row.running : entry.running;
905
- if (running !== true) continue;
906
- out.push(id);
1048
+ if (entry.origin === "subagent") {
1049
+ if (!includeSubagents || !isRunning(id)) continue;
1050
+ const owner = ownerOf(id);
1051
+ // Only the ancestor's own wait state counts: the sub-agent has no row, so
1052
+ // it can never be the session the user is being asked about.
1053
+ if (owner !== "" && !waitingForYou(owner)) push(owner);
1054
+ continue;
1055
+ }
1056
+ if (!isRunning(id)) continue;
1057
+ push(id);
907
1058
  }
908
1059
  return out;
909
1060
  }
@@ -2666,6 +2817,274 @@ window.__ModuleLoader__.load({
2666
2817
  }
2667
2818
  //#endregion
2668
2819
 
2820
+ //#region header ring
2821
+ /**
2822
+ * One yellow ring on the conversation header's sub-agent count.
2823
+ *
2824
+ * The official "N sub-agents" row is a `button[aria-haspopup="tree"]` inside
2825
+ * the `conversation.session.header.actions` slot, and its count is the LAST
2826
+ * `<span>` child of that button. The ring is stacked on that span's top-right
2827
+ * corner — the same corner logic as the workspace folder's yellow dot — and it
2828
+ * is display only: not clickable, `pointer-events:none`, `aria-hidden` because
2829
+ * the official button's own label already announces the running count.
2830
+ *
2831
+ * The number is the official `subagentCatalog` projection of the session on
2832
+ * screen, counting only DIRECT children that are running. It never joins any
2833
+ * other count this plugin draws (sidebar dots, the top-left number, the tab
2834
+ * icon, the OS badge), so this switch can never move one of them.
2835
+ *
2836
+ * Class names are build hashes and are never used; the anchor is found by the
2837
+ * official `data-slot` plus structure/role only. Like the sidebar marks this
2838
+ * half is injected into shipped DOM, reconciled instead of redrawn, quiet when
2839
+ * it cannot find the anchor, and self-disabling after repeated failures.
2840
+ */
2841
+ const HEAD_RING_ATTR = "data-icon-custom-headring";
2842
+ /** Marks the count element whose inline `position` we changed, for restore. */
2843
+ const HEAD_RING_HOST_ATTR = "data-icon-custom-headring-host";
2844
+ /** Breaker ceiling: a changed header structure must not warn forever. */
2845
+ const HEAD_RING_MAX_FAILURES = 20;
2846
+ /** The number for the session on screen; -1 means "no answer, draw nothing". */
2847
+ let headerRingValue = -1;
2848
+ let headerRingFailures = 0;
2849
+ let headerRingGivenUp = false;
2850
+ let headerRingHost = null;
2851
+ let headerRingHostPosition = "";
2852
+ let headerRingObserved = null;
2853
+ let headerRingObserver = null;
2854
+ /** The session service lookup, filled in by `apply` (may stay null). */
2855
+ let headerRingSessions = null;
2856
+ /** Throttle for the "catalog projection not loaded yet" refresh ask. */
2857
+ let headerRingRefreshSession = "";
2858
+ let headerRingRefreshAt = 0;
2859
+
2860
+ /**
2861
+ * How many DIRECT children of `sessionId` are running, from the official
2862
+ * `subagentCatalog` projection.
2863
+ *
2864
+ * Grandchildren are not part of a parent's catalog, so they are excluded by
2865
+ * construction. `status` (the official session-status map) wins over the list
2866
+ * row exactly the way the shipped sidebar reads it. An unloaded projection is
2867
+ * 0, never a guess.
2868
+ * @param list - the session list snapshot (may be absent).
2869
+ * @param status - the official session-status map (may be absent).
2870
+ * @param sessionId - the session whose header is on screen.
2871
+ * @returns the count of running direct children.
2872
+ */
2873
+ function headerRingCount(list, status, sessionId) {
2874
+ if (typeof sessionId !== "string" || sessionId === "") return 0;
2875
+ if (list === null || typeof list !== "object") return 0;
2876
+ const projections = list.projectionsBySession;
2877
+ if (projections === null || typeof projections !== "object") return 0;
2878
+ const snapshot = projections[sessionId];
2879
+ const values = snapshot !== null && typeof snapshot === "object" ? snapshot.values : undefined;
2880
+ const catalog = values !== null && typeof values === "object" ? values.subagentCatalog : undefined;
2881
+ if (!Array.isArray(catalog)) return 0;
2882
+ const byId = list.byId !== null && typeof list.byId === "object" ? list.byId : {};
2883
+ const lookup = status !== null && status !== undefined && typeof status.get === "function" ? (id) => status.get(id) : () => undefined;
2884
+ let count = 0;
2885
+ for (const entry of catalog) {
2886
+ if (entry === null || typeof entry !== "object" || typeof entry.id !== "string" || entry.id === "") continue;
2887
+ const row = lookup(entry.id);
2888
+ const known = row !== null && typeof row === "object" && typeof row.running === "boolean";
2889
+ const fallback = byId[entry.id];
2890
+ const running = known ? row.running : fallback !== null && typeof fallback === "object" ? fallback.running : undefined;
2891
+ if (running === true) count++;
2892
+ }
2893
+ return count;
2894
+ }
2895
+
2896
+ /**
2897
+ * Whether that session's catalog projection is loaded at all. `undefined`
2898
+ * means "not asked yet" — the one case worth nudging the host about (see
2899
+ * `requestHeaderRingRefresh`); an empty array is a real answer.
2900
+ */
2901
+ function headerRingCatalogLoaded(list, sessionId) {
2902
+ if (list === null || typeof list !== "object" || typeof sessionId !== "string" || sessionId === "") return false;
2903
+ const projections = list.projectionsBySession;
2904
+ const snapshot = projections !== null && typeof projections === "object" ? projections[sessionId] : undefined;
2905
+ const values = snapshot !== null && typeof snapshot === "object" ? snapshot.values : undefined;
2906
+ return values !== null && typeof values === "object" && Array.isArray(values.subagentCatalog);
2907
+ }
2908
+
2909
+ /**
2910
+ * Ask the host to fold that session's projections, at most once per 10 s. The
2911
+ * service lookup is optional (`apply` fills it in): without it the next render
2912
+ * simply tries again, which is the documented fallback.
2913
+ */
2914
+ function requestHeaderRingRefresh(sessionId) {
2915
+ const now = Date.now();
2916
+ if (sessionId === headerRingRefreshSession && now - headerRingRefreshAt < 10000) return;
2917
+ headerRingRefreshSession = sessionId;
2918
+ headerRingRefreshAt = now;
2919
+ try {
2920
+ const service = typeof headerRingSessions === "function" ? headerRingSessions() : null;
2921
+ const refresh = service !== null && service !== undefined ? service.refreshProjections : undefined;
2922
+ if (typeof refresh !== "function") return;
2923
+ const result = refresh.call(service, sessionId);
2924
+ if (result !== null && typeof result === "object" && typeof result.catch === "function") result.catch(() => {});
2925
+ } catch { /* best effort: the next render is the fallback */ }
2926
+ }
2927
+
2928
+ /** The rendered action button for the session on screen, or null. */
2929
+ function headerRingButton() {
2930
+ try {
2931
+ const slots = document.querySelectorAll('[data-slot="conversation.session.header.actions"]');
2932
+ if (slots === null || slots === undefined) return null;
2933
+ for (const slot of slots) {
2934
+ // The right sidebar keeps an embedded conversation MOUNTED while
2935
+ // collapsed (hidden by CSS and `inert`), so its header actions must
2936
+ // never be the anchor for the visible conversation's ring. Same guard
2937
+ // as `conversationOnScreen` uses for the same duplicate.
2938
+ if (typeof slot.closest === "function" && (slot.closest("[data-sidebar-right-panel]") !== null || slot.closest("[inert]") !== null)) continue;
2939
+ if (typeof slot.querySelectorAll !== "function") continue;
2940
+ for (const button of slot.querySelectorAll("button")) {
2941
+ if (button.getAttribute("aria-haspopup") === "tree") return button;
2942
+ }
2943
+ }
2944
+ return null;
2945
+ } catch { return null; }
2946
+ }
2947
+
2948
+ /** The count element: the LAST `<span>` direct child of that button. */
2949
+ function headerRingCountElement(button) {
2950
+ let found = null;
2951
+ if (button === null || button === undefined || button.children === undefined) return null;
2952
+ for (const child of button.children) {
2953
+ if (child === null || child === undefined || child.tagName !== "SPAN") continue;
2954
+ if (child.getAttribute(HEAD_RING_ATTR) !== null) continue;
2955
+ found = child;
2956
+ }
2957
+ return found;
2958
+ }
2959
+
2960
+ /**
2961
+ * Whether the decoration must stay off this conversation. A sub-agent's own
2962
+ * session renders no header row at all, so it must render no ring either.
2963
+ */
2964
+ function headerRingSuppressed(list, sessionId) {
2965
+ if (typeof sessionId !== "string" || sessionId === "") return true;
2966
+ if (list === null || typeof list !== "object") return true;
2967
+ const byId = list.byId !== null && typeof list.byId === "object" ? list.byId : {};
2968
+ const entry = byId[sessionId];
2969
+ // No row means the page cannot prove this is not a sub-agent's own
2970
+ // conversation: suppress rather than draw on a page we cannot read.
2971
+ if (entry === null || typeof entry !== "object") return true;
2972
+ return entry.origin === "subagent";
2973
+ }
2974
+
2975
+ /**
2976
+ * The effective number to draw. `-1` (no answer) and `0` both mean "draw
2977
+ * nothing"; the switch is the only other input. Pure, so the switch and the
2978
+ * zero case are testable without a DOM.
2979
+ */
2980
+ function headerRingVisibleCount(config, count) {
2981
+ const enabled = !(config !== null && typeof config === "object" && config.headerRing === false);
2982
+ if (!enabled) return -1;
2983
+ const value = Number(count);
2984
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
2985
+ }
2986
+
2987
+ /** Remove the ring and put the count element's own `position` back. */
2988
+ function clearHeaderRing() {
2989
+ try { document.querySelectorAll("[" + HEAD_RING_ATTR + "]").forEach((node) => node.remove()); } catch {}
2990
+ if (headerRingHost !== null) {
2991
+ try { headerRingHost.style.position = headerRingHostPosition; } catch {}
2992
+ try { headerRingHost.removeAttribute(HEAD_RING_HOST_ATTR); } catch {}
2993
+ }
2994
+ headerRingHost = null;
2995
+ headerRingHostPosition = "";
2996
+ if (headerRingObserver !== null) {
2997
+ try { headerRingObserver.disconnect(); } catch {}
2998
+ headerRingObserver = null;
2999
+ }
3000
+ headerRingObserved = null;
3001
+ }
3002
+
3003
+ /** Watch the header slot, so a re-render that dropped the ring gets it back. */
3004
+ function observeHeaderRingAnchor(button) {
3005
+ if (typeof window === "undefined" || window === null || typeof window.MutationObserver !== "function") return;
3006
+ const slot = button !== null && typeof button.closest === "function" ? button.closest('[data-slot="conversation.session.header.actions"]') : null;
3007
+ if (slot === null || slot === undefined || slot === headerRingObserved) return;
3008
+ try {
3009
+ if (headerRingObserver !== null) headerRingObserver.disconnect();
3010
+ headerRingObserver = new window.MutationObserver(() => { repaintHeaderRing(); });
3011
+ headerRingObserver.observe(slot, { childList: true, subtree: true, characterData: true });
3012
+ headerRingObserved = slot;
3013
+ } catch {
3014
+ headerRingObserver = null;
3015
+ headerRingObserved = null;
3016
+ }
3017
+ }
3018
+
3019
+ /** Re-run the current value; used by the observer, which has no arguments. */
3020
+ function repaintHeaderRing() {
3021
+ paintHeaderRing(runningConfig, headerRingValue);
3022
+ }
3023
+
3024
+ /** Warn once, then give up for this page — the sidebar marks' exact posture. */
3025
+ function noteHeaderRingFailure(why) {
3026
+ headerRingFailures++;
3027
+ if (headerRingFailures === 1) {
3028
+ try { console.warn("dsh-icon-custom: 会话头部黄圈定位失败,已降级(不影响其他功能):", why); } catch {}
3029
+ return;
3030
+ }
3031
+ if (headerRingFailures < HEAD_RING_MAX_FAILURES || headerRingGivenUp) return;
3032
+ headerRingGivenUp = true;
3033
+ try { console.warn("dsh-icon-custom: 会话头部黄圈连续失败,本次会话内已自动关闭。"); } catch {}
3034
+ clearHeaderRing();
3035
+ }
3036
+
3037
+ /**
3038
+ * Reconcile the ring with the current number. Idempotent (writes only what
3039
+ * differs) and quiet when the anchor is not on this page (a sub-agent's own
3040
+ * conversation, the landing view, a boot).
3041
+ * @param config - the running config, for the `headerRing` switch.
3042
+ * @param count - the ring's number, from `headerRingCount`; -1 hides it.
3043
+ */
3044
+ function paintHeaderRing(config, count) {
3045
+ if (headerRingGivenUp) return;
3046
+ const value = headerRingVisibleCount(config, count);
3047
+ if (value <= 0) { clearHeaderRing(); return; }
3048
+ const button = headerRingButton();
3049
+ // No anchor is not a failure: child sessions render no header row, and the
3050
+ // page may simply be booting. Leave whatever is there alone and wait.
3051
+ if (button === null) { clearHeaderRing(); return; }
3052
+ const anchor = headerRingCountElement(button);
3053
+ if (anchor === null) { noteHeaderRingFailure("找不到计数元素"); return; }
3054
+ try {
3055
+ if (headerRingHost !== anchor) {
3056
+ clearHeaderRing();
3057
+ headerRingHost = anchor;
3058
+ headerRingHostPosition = anchor.style.position;
3059
+ anchor.setAttribute(HEAD_RING_HOST_ATTR, "1");
3060
+ if (window.getComputedStyle(anchor).position === "static") anchor.style.position = "relative";
3061
+ }
3062
+ observeHeaderRingAnchor(button);
3063
+ let node = anchor.querySelector("[" + HEAD_RING_ATTR + "]");
3064
+ if (node === null) {
3065
+ node = document.createElement("span");
3066
+ node.setAttribute(HEAD_RING_ATTR, "1");
3067
+ // Not a control and never announced: the official button's own label
3068
+ // already speaks the running count.
3069
+ node.setAttribute("aria-hidden", "true");
3070
+ // Looks like the sidebar workspace's red count: a rounded-square pill (never a
3071
+ // full circle), with a dark number on the running yellow.
3072
+ node.style.cssText = "position:absolute;pointer-events:none;top:-6px;right:-5px;display:inline-flex;"
3073
+ + "align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 4px;box-sizing:border-box;"
3074
+ + "border-radius:6px;background:" + RUNNING_YELLOW + ";color:#1f2328;font:inherit;font-size:11px;"
3075
+ + "font-weight:600;line-height:16px;font-variant-numeric:tabular-nums;z-index:3;"
3076
+ + "box-shadow:0 0 0 1.5px var(--dsw-alias-bg-layer-1,#fff)";
3077
+ anchor.appendChild(node);
3078
+ }
3079
+ const text = badgeLabel(value);
3080
+ if (node.textContent !== text) node.textContent = text;
3081
+ headerRingFailures = 0;
3082
+ } catch (error) {
3083
+ noteHeaderRingFailure(String((error && error.message) || error));
3084
+ }
3085
+ }
3086
+ //#endregion
3087
+
2669
3088
  //#region components
2670
3089
  /**
2671
3090
  * Headless occupant of the frame-wide `shell.overlay` slot. It reads DSH's
@@ -2886,6 +3305,21 @@ window.__ModuleLoader__.load({
2886
3305
  const runningRowSignature = runningItemsSignature(runningRows);
2887
3306
  const runningCount = runningIds.length;
2888
3307
  React.useEffect(() => { emitRunningState(runningCount, runningRows); }, [runningRowSignature]);
3308
+ // The conversation header's yellow ring: the session on screen, and ONLY
3309
+ // its direct children from the official `subagentCatalog`. A separate
3310
+ // number for a separate decoration — it never joins `runningIds`, so no
3311
+ // sidebar, tab or OS-badge count can move because of it.
3312
+ const ringSessionId = viewed.ids.length === 1 ? viewed.ids[0] : "";
3313
+ const ringSuppressed = headerRingSuppressed(list, ringSessionId);
3314
+ const ringCount = ringSuppressed ? -1 : headerRingCount(list, status, ringSessionId);
3315
+ const ringSignature = ringSessionId + "\u0000" + ringCount;
3316
+ React.useEffect(() => {
3317
+ headerRingValue = ringCount;
3318
+ // Not loaded yet is not "zero running": ask the host once, then let the
3319
+ // next render answer. Without a session service this is a quiet no-op.
3320
+ if (!ringSuppressed && !headerRingCatalogLoaded(list, ringSessionId)) requestHeaderRingRefresh(ringSessionId);
3321
+ paintHeaderRing(runningConfig, headerRingValue);
3322
+ }, [ringSignature, running.enabled, running.headerRing]);
2889
3323
  // The session rows, for naming the running ones the way the sidebar shows
2890
3324
  // them (`displayTitle`, the same fallback the red dots use).
2891
3325
  React.useEffect(() => {
@@ -3463,6 +3897,229 @@ window.__ModuleLoader__.load({
3463
3897
  }, children);
3464
3898
  }
3465
3899
 
3900
+ //#region hint disclosure
3901
+ /**
3902
+ * Every long hint used to be printed in full on the settings page, which made
3903
+ * one screen a wall of grey small print. Now each one keeps a one-line
3904
+ * summary on the page and moves the full text behind a "?" button.
3905
+ *
3906
+ * The bubble's material is borrowed, its behaviour is ours:
3907
+ * - the official `MenuSurface` primitive as the bubble container, when this
3908
+ * build exposes it to plugins;
3909
+ * - a self-drawn bubble carrying the same inline styling otherwise.
3910
+ * Opening and closing (click the "?", click it again, click away, Escape) is
3911
+ * the plugin's own code on BOTH paths, and placement stays in the pure
3912
+ * `hintBubblePlacement` below — `MenuSurface` only paints the surface, it
3913
+ * does not position or dismiss anything.
3914
+ *
3915
+ * Why not the official `Tooltip` (which is what this used to use): its
3916
+ * `label` is `string | (() => string)`
3917
+ * (`@deepseek-ai/dsh-client-ui-primitives/lib/types/Tooltip.d.ts:20`), so it
3918
+ * can only print plain text and can never render the emphasised phrases the
3919
+ * requirement asks for. `MenuSurface` takes arbitrary children
3920
+ * (`ComponentPropsWithoutRef<'div'>`), so the same self-managed disclosure
3921
+ * can render real React nodes — hence `**bold**` becomes a real `<strong>`.
3922
+ * See docs/adr/0012-hint-bubble-menusurface.md.
3923
+ *
3924
+ * The client half is loaded through the ModuleLoader's `require` with no build
3925
+ * step, so that package can be absent or not yet materialized — a `require`
3926
+ * miss must degrade to the fallback, never to a blank section.
3927
+ * Note: this package is resolved by the host at require time and is therefore
3928
+ * deliberately left out of `dsh.client.inject` and `peerDependencies`
3929
+ * (matching the official packages), so no injection gate can block mount.
3930
+ */
3931
+ const HINT_BUBBLE_WIDTH = 320;
3932
+ const HINT_BUBBLE_MARGIN = 8;
3933
+ const HINT_BUBBLE_GAP = 6;
3934
+ const HINT_ROW_STYLE = { display: "flex", alignItems: "flex-start", gap: "6px", marginTop: "4px" };
3935
+ const HINT_SUMMARY_STYLE = { fontSize: 11, color: "var(--dsw-alias-label-tertiary)", lineHeight: "16px" };
3936
+ const HINT_BUTTON_STYLE = { flex: "none", width: "16px", height: "16px", padding: 0, borderRadius: "50%", border: "1px solid var(--dsw-alias-border-l2)", background: "transparent", color: "var(--dsw-alias-label-tertiary)", fontSize: 11, lineHeight: "14px", textAlign: "center", cursor: "pointer" };
3937
+ /** The emphasised run: a real `<strong>`, so `**` never reaches the screen. */
3938
+ const HINT_STRONG_STYLE = { fontWeight: 600, color: "var(--dsw-alias-label-primary)" };
3939
+ /** The fallback bubble carries its own material; the official surface paints its own. */
3940
+ const HINT_BUBBLE_STYLE = { position: "fixed", zIndex: 70, padding: "8px 10px", borderRadius: "10px", border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-1)", boxShadow: "0 10px 30px rgba(0,0,0,.18)", color: "var(--dsw-alias-label-primary)", fontSize: 12, lineHeight: "18px", whiteSpace: "pre-wrap", maxHeight: "40vh", overflowY: "auto" };
3941
+ /** Placement-only style for the official surface: no fill/border/radius of ours. */
3942
+ const HINT_BUBBLE_PLACEMENT_STYLE = { position: "fixed", zIndex: 70, padding: "8px 10px", color: "var(--dsw-alias-label-primary)", fontSize: 12, lineHeight: "18px", whiteSpace: "pre-wrap", maxHeight: "40vh", overflowY: "auto" };
3943
+
3944
+ /**
3945
+ * Resolve the official MenuSurface lazily: requiring it at factory time can
3946
+ * run before the primitive is materialized, and a miss throws (the loader's
3947
+ * module table has no fallback entry). Success is memoized; a miss is retried
3948
+ * on the next render, so a plugin that boots before the primitive still picks
3949
+ * it up instead of being stuck on the fallback forever.
3950
+ * @returns the MenuSurface component, or null when this build does not expose it.
3951
+ */
3952
+ let officialMenuSurface = null;
3953
+ function officialHintSurface() {
3954
+ if (officialMenuSurface !== null) return officialMenuSurface;
3955
+ try {
3956
+ const primitives = require("@deepseek-ai/dsh-client-ui-primitives");
3957
+ // "Present" is the whole judgement, not "is a function": the shipped
3958
+ // `MenuSurface` is a `forwardRef` object (`lib/index.js` builds it with
3959
+ // `forwardRef(...)`, and its type is `ForwardRefExoticComponent`), so
3960
+ // `typeof` is "object". The old `typeof === "function"` test therefore
3961
+ // never matched in production and left the official branch dead. A build
3962
+ // that exposes it as a plain function still works.
3963
+ const candidate = primitives !== null && typeof primitives === "object" ? primitives.MenuSurface : undefined;
3964
+ if (candidate !== null && candidate !== undefined
3965
+ && (typeof candidate === "function" || typeof candidate === "object")) {
3966
+ officialMenuSurface = candidate;
3967
+ }
3968
+ } catch (error) {
3969
+ officialMenuSurface = null;
3970
+ }
3971
+ return officialMenuSurface;
3972
+ }
3973
+
3974
+ /**
3975
+ * Turn one hint string into React children, converting every BALANCED
3976
+ * `**pair**` into a real `<strong>`.
3977
+ *
3978
+ * Only a closed pair is markup: an unpaired `**` (or a lone marker) is kept
3979
+ * verbatim, because dropping the characters would silently lose text — worse
3980
+ * than showing the marker. The marker itself never reaches the screen for a
3981
+ * balanced pair: what lands in the DOM is `<strong>pair</strong>`.
3982
+ * @param text - the locale string, possibly carrying `**` runs.
3983
+ * @returns an array of strings and `<strong>` elements, never empty.
3984
+ */
3985
+ function hintTextNodes(text) {
3986
+ const source = typeof text === "string" ? text : "";
3987
+ const nodes = [];
3988
+ const bold = /\*\*([\s\S]+?)\*\*/g;
3989
+ let last = 0;
3990
+ let match;
3991
+ while ((match = bold.exec(source)) !== null) {
3992
+ if (match.index > last) nodes.push(source.slice(last, match.index));
3993
+ nodes.push(React.createElement("strong", { key: "hint-bold-" + nodes.length, style: HINT_STRONG_STYLE }, match[1]));
3994
+ last = match.index + match[0].length;
3995
+ }
3996
+ if (last < source.length) nodes.push(source.slice(last));
3997
+ if (nodes.length === 0) nodes.push(source);
3998
+ return nodes;
3999
+ }
4000
+
4001
+ /**
4002
+ * Where the fallback bubble goes: just below the button's anchor rect, clamped
4003
+ * into the viewport. Pure, so the placement is testable without a DOM.
4004
+ * @param anchor - the button's client rect, or null.
4005
+ * @param viewportWidth - window width.
4006
+ * @param viewportHeight - window height.
4007
+ * @returns `{ left, top, width }` in viewport pixels.
4008
+ */
4009
+ function hintBubblePlacement(anchor, viewportWidth, viewportHeight) {
4010
+ const width = Math.min(HINT_BUBBLE_WIDTH, Math.max(120, viewportWidth - HINT_BUBBLE_MARGIN * 2));
4011
+ const anchorLeft = anchor !== null && typeof anchor === "object" && typeof anchor.left === "number" ? anchor.left : HINT_BUBBLE_MARGIN;
4012
+ const anchorBottom = anchor !== null && typeof anchor === "object" && typeof anchor.bottom === "number" ? anchor.bottom : HINT_BUBBLE_MARGIN;
4013
+ const left = Math.max(HINT_BUBBLE_MARGIN, Math.min(anchorLeft, viewportWidth - width - HINT_BUBBLE_MARGIN));
4014
+ const top = Math.max(HINT_BUBBLE_MARGIN, Math.min(anchorBottom + HINT_BUBBLE_GAP, viewportHeight - HINT_BUBBLE_MARGIN));
4015
+ return { left: Math.round(left), top: Math.round(top), width: Math.round(width) };
4016
+ }
4017
+
4018
+ /**
4019
+ * The bubble node for one disclosure: the official `MenuSurface` material when
4020
+ * `surface` is given, the self-drawn inline bubble otherwise. Pure, so both
4021
+ * paths are exercised without a DOM; closed renders nothing.
4022
+ *
4023
+ * `MenuSurface` is handed placement only (position/left/top/width/scroll) and
4024
+ * keeps its own fill, radius, border and blur; the fallback div carries the
4025
+ * whole look itself. `compact` asks the official surface for the smaller
4026
+ * radius a small bubble wants.
4027
+ * @param open - whether the bubble is showing.
4028
+ * @param surface - the official MenuSurface component, or null for the fallback.
4029
+ * @param label - accessible name for the dialog.
4030
+ * @param place - `{ left, top, width }` from `hintBubblePlacement`.
4031
+ * @param text - the full hint text, `**` markers included.
4032
+ * @returns the bubble element, or null while closed.
4033
+ */
4034
+ function hintBubbleNode(open, surface, label, place, text) {
4035
+ if (!open) return null;
4036
+ const placement = { left: place.left + "px", top: place.top + "px", width: place.width + "px" };
4037
+ const content = hintTextNodes(text);
4038
+ if (surface === null) {
4039
+ return React.createElement("div", {
4040
+ "data-icon-custom-hint": "1",
4041
+ role: "dialog",
4042
+ "aria-label": label,
4043
+ style: { ...HINT_BUBBLE_STYLE, ...placement }
4044
+ }, ...content);
4045
+ }
4046
+ return React.createElement(surface, {
4047
+ "data-icon-custom-hint": "1",
4048
+ role: "dialog",
4049
+ "aria-label": label,
4050
+ compact: true,
4051
+ style: { ...HINT_BUBBLE_PLACEMENT_STYLE, ...placement }
4052
+ }, ...content);
4053
+ }
4054
+
4055
+ /**
4056
+ * The button plus its bubble, with the open/close state managed here — the same
4057
+ * code on both paths, because the official surface only supplies material.
4058
+ * Opened by the "?", closed by the same button, an outside pointerdown, or
4059
+ * Escape. The button is the only way to open the text (hovering does not).
4060
+ */
4061
+ function HintDisclosureBubble(props) {
4062
+ const [rect, setRect] = React.useState(null);
4063
+ const open = rect !== null;
4064
+ React.useEffect(() => {
4065
+ if (!open) return undefined;
4066
+ const onKey = (event) => { if (event.key === "Escape") setRect(null); };
4067
+ const onDown = (event) => {
4068
+ const node = event.target;
4069
+ if (node === null || typeof node.closest !== "function") return;
4070
+ // Both surfaces carry this marker (MenuSurface forwards div props), so
4071
+ // a click inside the bubble never counts as "clicked away".
4072
+ if (node.closest('[data-icon-custom-hint="1"]') !== null) return;
4073
+ if (node.closest('[data-icon-custom-hint-button="1"]') !== null) return;
4074
+ setRect(null);
4075
+ };
4076
+ window.addEventListener("keydown", onKey);
4077
+ document.addEventListener("mousedown", onDown, true);
4078
+ return () => {
4079
+ window.removeEventListener("keydown", onKey);
4080
+ document.removeEventListener("mousedown", onDown, true);
4081
+ };
4082
+ }, [open]);
4083
+ const toggle = (event) => {
4084
+ if (open) {
4085
+ setRect(null);
4086
+ return;
4087
+ }
4088
+ const node = event.currentTarget;
4089
+ setRect(node !== null && typeof node.getBoundingClientRect === "function" ? node.getBoundingClientRect() : null);
4090
+ };
4091
+ const place = hintBubblePlacement(rect, window.innerWidth, window.innerHeight);
4092
+ const button = React.createElement("button", {
4093
+ type: "button",
4094
+ "aria-label": props.label,
4095
+ title: props.label,
4096
+ "data-icon-custom-hint-button": "1",
4097
+ onClick: toggle,
4098
+ style: HINT_BUTTON_STYLE
4099
+ }, "?");
4100
+ return React.createElement(React.Fragment, null,
4101
+ button,
4102
+ hintBubbleNode(open, props.surface, props.label, place, props.text)
4103
+ );
4104
+ }
4105
+
4106
+ /**
4107
+ * One hint row: the short summary (plain text, never folded) stays on the page,
4108
+ * the full text sits behind the "?" button. The official primitive is resolved
4109
+ * once per row and passed down; a missing package simply means `surface === null`.
4110
+ */
4111
+ function HintDisclosure(props) {
4112
+ return React.createElement("div", { style: HINT_ROW_STYLE },
4113
+ React.createElement("span", { style: HINT_SUMMARY_STYLE }, props.summary),
4114
+ React.createElement(HintDisclosureBubble, {
4115
+ text: props.text,
4116
+ label: props.label,
4117
+ surface: officialHintSurface()
4118
+ })
4119
+ );
4120
+ }
4121
+ //#endregion
4122
+
3466
4123
  function FaviconSection(props) {
3467
4124
  const t = props.t;
3468
4125
  const rpc = props.rpc;
@@ -3579,7 +4236,7 @@ window.__ModuleLoader__.load({
3579
4236
  // arrives — so a field missing HERE is a field that silently reverts to its
3580
4237
  // default on the next page load, however well the local toggle worked.
3581
4238
  rpc.call("/api", "iconCustom/setUnreadRule", {
3582
- args: { request: { reasons: next.reasons, pending: next.pending, workspaceDot: next.workspaceDot, appBadge: next.appBadge, clearDelaySec: next.clearDelaySec, revealOnOpen: next.revealOnOpen } }
4239
+ args: { request: { reasons: next.reasons, pending: next.pending, workspaceDot: next.workspaceDot, appBadge: next.appBadge, clearDelaySec: next.clearDelaySec, revealOnOpen: next.revealOnOpen, unreadIncludeSubagents: next.unreadIncludeSubagents } }
3583
4240
  }).catch(() => {});
3584
4241
  }, [rpc]);
3585
4242
  // Toggle one unread reason. Applies locally at once; the host keeps the
@@ -3599,6 +4256,12 @@ window.__ModuleLoader__.load({
3599
4256
  const onRevealToggle = React.useCallback((checked) => {
3600
4257
  saveUnreadRule({ revealOnOpen: checked });
3601
4258
  }, [saveUnreadRule]);
4259
+ // Whether a background sub-agent settling (which makes its parent run one
4260
+ // more turn) is worth telling the person about. Same one save path, so the
4261
+ // field cannot be dropped here and silently revert to ON after a reload.
4262
+ const onIncludeSubagentsToggle = React.useCallback((checked) => {
4263
+ saveUnreadRule({ unreadIncludeSubagents: checked });
4264
+ }, [saveUnreadRule]);
3602
4265
  // The system-icon projection of the same number. Saved through the same
3603
4266
  // one save path, so this field can never be dropped by a caller.
3604
4267
  const onAppBadgeToggle = React.useCallback((checked) => {
@@ -3610,13 +4273,25 @@ window.__ModuleLoader__.load({
3610
4273
  const saveRunningRule = React.useCallback((patch) => {
3611
4274
  const next = normalizeRunningConfig({ ...runningConfig, ...patch });
3612
4275
  emitRunningConfig(next);
4276
+ // Both switches travel together, always: the host record is one object, and
4277
+ // a caller that remembered only one field would otherwise silently reset
4278
+ // the other one on the next load.
3613
4279
  rpc.call("/api", "iconCustom/setRunningRule", {
3614
- args: { request: { enabled: next.enabled } }
4280
+ args: { request: { enabled: next.enabled, includeSubagents: next.includeSubagents, headerRing: next.headerRing } }
3615
4281
  }).catch(() => {});
3616
4282
  }, [rpc]);
3617
4283
  const onRunningMarkToggle = React.useCallback((checked) => {
3618
4284
  saveRunningRule({ enabled: checked });
3619
4285
  }, [saveRunningRule]);
4286
+ const onRunningIncludeSubagentsToggle = React.useCallback((checked) => {
4287
+ saveRunningRule({ includeSubagents: checked });
4288
+ }, [saveRunningRule]);
4289
+ // Third switch of the same feature: the yellow ring on the conversation
4290
+ // header's sub-agent count. Independent by construction — nothing on the
4291
+ // sidebar, the tab icon or the OS badge reads it.
4292
+ const onHeaderRingToggle = React.useCallback((checked) => {
4293
+ saveRunningRule({ headerRing: checked });
4294
+ }, [saveRunningRule]);
3620
4295
  // Seconds you must stay in a session before it counts as read; 0 = as soon
3621
4296
  // as you enter. Clamped by the shared normalizer, so a stray keystroke can
3622
4297
  // never persist an absurd delay.
@@ -3783,6 +4458,15 @@ window.__ModuleLoader__.load({
3783
4458
  hint: { fontSize: 11, color: "var(--dsw-alias-label-tertiary)", marginTop: "4px", lineHeight: "16px" }
3784
4459
  };
3785
4460
 
4461
+ // One disclosure row per long hint: the summary is on the page, the full
4462
+ // text (and nothing else) sits behind the "?" button.
4463
+ const hintLine = (hintKey) => React.createElement(HintDisclosure, {
4464
+ key: hintKey,
4465
+ summary: t(hintKey + "Summary"),
4466
+ text: t(hintKey),
4467
+ label: t("hintMoreLabel")
4468
+ });
4469
+
3786
4470
  return React.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "4px" } },
3787
4471
  React.createElement("div", { style: style.row },
3788
4472
  React.createElement("div", { style: style.preview },
@@ -3808,13 +4492,13 @@ window.__ModuleLoader__.load({
3808
4492
  React.createElement("input", { type: "checkbox", checked: pwaEnabled, disabled: busy || !active, onChange: onTogglePwa, style: { cursor: busy ? "default" : "pointer" } }),
3809
4493
  React.createElement("span", null, t("pwa"))
3810
4494
  ),
3811
- React.createElement("div", { style: style.hint }, t("pwaHint")),
4495
+ hintLine("pwaHint"),
3812
4496
  React.createElement("label", { style: { display: "flex", alignItems: "center", gap: "8px", padding: "10px 0 0", fontSize: 12, color: "var(--dsw-alias-label-primary)", cursor: busy ? "default" : "pointer" } },
3813
4497
  React.createElement("input", { type: "checkbox", checked: logoEnabled, disabled: busy || !active, onChange: onToggleLogo, style: { cursor: busy ? "default" : "pointer" } }),
3814
4498
  React.createElement("span", null, t("logo"))
3815
4499
  ),
3816
- React.createElement("div", { style: style.hint }, t("logoHint")),
3817
- React.createElement("div", { style: style.hint }, t("uploadHint")),
4500
+ hintLine("logoHint"),
4501
+ hintLine("uploadHint"),
3818
4502
  React.createElement("div", { style: style.hint }, t("resetHint")),
3819
4503
  React.createElement("div", { style: { borderTop: "1px solid var(--dsw-alias-border-l2)", marginTop: "12px", paddingTop: "12px" } },
3820
4504
  React.createElement("div", { style: style.label }, t("badgeTest")),
@@ -3851,7 +4535,7 @@ window.__ModuleLoader__.load({
3851
4535
  style: { ...(badgeSizeValue === key ? style.btnPrimary : style.btn), padding: "4px 10px" }
3852
4536
  }, t(key === "sm" ? "badgeSizeSmall" : key === "md" ? "badgeSizeMedium" : "badgeSizeLarge")))
3853
4537
  ),
3854
- React.createElement("div", { style: style.hint }, t("badgeSizeHint")),
4538
+ hintLine("badgeSizeHint"),
3855
4539
  React.createElement("div", { style: { marginTop: "12px" } },
3856
4540
  React.createElement("div", { style: style.desc }, t("unreadReasonsLabel")),
3857
4541
  React.createElement("div", { style: { display: "flex", flexWrap: "wrap", alignItems: "center" } },
@@ -3859,6 +4543,18 @@ window.__ModuleLoader__.load({
3859
4543
  renderReasonCheckbox("pending", "reasonPending")
3860
4544
  )
3861
4545
  ),
4546
+ React.createElement("div", { style: { marginTop: "10px" } },
4547
+ React.createElement("label", { style: { display: "inline-flex", alignItems: "center", gap: "6px", fontSize: 12, color: "var(--dsw-alias-label-primary)", cursor: "pointer" } },
4548
+ React.createElement("input", {
4549
+ type: "checkbox",
4550
+ checked: unread.unreadIncludeSubagents !== false,
4551
+ onChange: (event) => onIncludeSubagentsToggle(event.target.checked === true),
4552
+ "aria-label": t("unreadIncludeSubagentsLabel")
4553
+ }),
4554
+ React.createElement("span", null, t("unreadIncludeSubagentsLabel"))
4555
+ ),
4556
+ hintLine("unreadIncludeSubagentsHint")
4557
+ ),
3862
4558
  React.createElement("div", { style: { marginTop: "12px" } },
3863
4559
  React.createElement("div", { style: style.desc }, t("clearDelayLabel")),
3864
4560
  React.createElement("label", { style: { display: "inline-flex", alignItems: "center", gap: "6px", marginTop: "6px", fontSize: 12, color: "var(--dsw-alias-label-primary)" } },
@@ -3873,7 +4569,7 @@ window.__ModuleLoader__.load({
3873
4569
  }),
3874
4570
  React.createElement("span", null, t("clearDelayUnit"))
3875
4571
  ),
3876
- React.createElement("div", { style: style.hint }, t("clearDelayHint"))
4572
+ hintLine("clearDelayHint")
3877
4573
  ),
3878
4574
  React.createElement("div", { style: { marginTop: "10px" } },
3879
4575
  React.createElement("label", { style: { display: "inline-flex", alignItems: "center", gap: "6px", fontSize: 12, color: "var(--dsw-alias-label-primary)", cursor: "pointer" } },
@@ -3884,7 +4580,7 @@ window.__ModuleLoader__.load({
3884
4580
  }),
3885
4581
  React.createElement("span", null, t("workspaceDotLabel"))
3886
4582
  ),
3887
- React.createElement("div", { style: style.hint }, t("workspaceDotHint"))
4583
+ hintLine("workspaceDotHint")
3888
4584
  ),
3889
4585
  React.createElement("div", { style: { marginTop: "10px" } },
3890
4586
  React.createElement("label", { style: { display: "inline-flex", alignItems: "center", gap: "6px", fontSize: 12, color: "var(--dsw-alias-label-primary)", cursor: "pointer" } },
@@ -3895,7 +4591,7 @@ window.__ModuleLoader__.load({
3895
4591
  }),
3896
4592
  React.createElement("span", null, t("revealLabel"))
3897
4593
  ),
3898
- React.createElement("div", { style: style.hint }, t("revealHint"))
4594
+ hintLine("revealHint")
3899
4595
  ),
3900
4596
  React.createElement("div", { style: { marginTop: "10px" } },
3901
4597
  React.createElement("label", { style: { display: "inline-flex", alignItems: "center", gap: "6px", fontSize: 12, color: "var(--dsw-alias-label-primary)", cursor: "pointer" } },
@@ -3906,7 +4602,7 @@ window.__ModuleLoader__.load({
3906
4602
  }),
3907
4603
  React.createElement("span", null, t("appBadgeLabel"))
3908
4604
  ),
3909
- React.createElement("div", { style: style.hint }, t("appBadgeHint")),
4605
+ hintLine("appBadgeHint"),
3910
4606
  // What THIS device is — never a claim about the platform.
3911
4607
  React.createElement("div", { style: style.hint }, t(appBadgeCapabilityKey(appBadgeProbe))),
3912
4608
  // The WebKit precondition as a live local fact, plus the one
@@ -3922,10 +4618,10 @@ window.__ModuleLoader__.load({
3922
4618
  }, t("notificationPermissionButton"))
3923
4619
  : null
3924
4620
  ),
3925
- React.createElement("div", { style: style.hint }, t("notificationPermissionHint")),
4621
+ hintLine("notificationPermissionHint"),
3926
4622
  React.createElement("div", { style: style.hint }, t("appBadgeFootnote"))
3927
4623
  ),
3928
- React.createElement("div", { style: style.hint }, t("badgeTestHint"))
4624
+ hintLine("badgeTestHint")
3929
4625
  ),
3930
4626
  // A second, independent feature in its own block: same settings page,
3931
4627
  // same save discipline, different fact.
@@ -3940,7 +4636,34 @@ window.__ModuleLoader__.load({
3940
4636
  }),
3941
4637
  React.createElement("span", null, t("runningMarkLabel"))
3942
4638
  ),
3943
- React.createElement("div", { style: style.hint }, t("runningMarkHint"))
4639
+ hintLine("runningMarkHint"),
4640
+ // The second switch of the same feature: still no host fact, only a
4641
+ // counting rule. Nested under the first one because it only matters
4642
+ // while the marks themselves are on.
4643
+ React.createElement("div", { style: { marginTop: "6px" } },
4644
+ React.createElement("label", { style: { display: "inline-flex", alignItems: "center", gap: "6px", fontSize: 12, color: "var(--dsw-alias-label-primary)", cursor: "pointer" } },
4645
+ React.createElement("input", {
4646
+ type: "checkbox",
4647
+ checked: running.includeSubagents !== false,
4648
+ onChange: (event) => onRunningIncludeSubagentsToggle(event.target.checked === true)
4649
+ }),
4650
+ React.createElement("span", null, t("runningIncludeSubagentsLabel"))
4651
+ ),
4652
+ hintLine("runningIncludeSubagentsHint")
4653
+ ),
4654
+ // The third switch of the same feature: one decoration on the
4655
+ // conversation header, independent of every count above.
4656
+ React.createElement("div", { style: { marginTop: "6px" } },
4657
+ React.createElement("label", { style: { display: "inline-flex", alignItems: "center", gap: "6px", fontSize: 12, color: "var(--dsw-alias-label-primary)", cursor: "pointer" } },
4658
+ React.createElement("input", {
4659
+ type: "checkbox",
4660
+ checked: running.headerRing !== false,
4661
+ onChange: (event) => onHeaderRingToggle(event.target.checked === true)
4662
+ }),
4663
+ React.createElement("span", null, t("headerRingLabel"))
4664
+ ),
4665
+ hintLine("headerRingHint")
4666
+ )
3944
4667
  )
3945
4668
  ),
3946
4669
  notice ? React.createElement("div", { style: style.notice }, notice) : null,
@@ -3973,6 +4696,13 @@ window.__ModuleLoader__.load({
3973
4696
  return typeof key === "string" && key !== "" ? key : undefined;
3974
4697
  } catch { return undefined; }
3975
4698
  };
4699
+ // The sessions service, looked up lazily for the same reason as the binding
4700
+ // above: it is only needed to nudge the host when a session's
4701
+ // `subagentCatalog` projection has not been folded yet, and a build without
4702
+ // it simply waits for the next render (see `requestHeaderRingRefresh`).
4703
+ headerRingSessions = () => {
4704
+ try { return ctx.get("sessions"); } catch { return null; }
4705
+ };
3976
4706
  // Pull the host state once, so the brand mark and the badge are correct
3977
4707
  // before the settings section is ever opened. The unread rule rides its
3978
4708
  // own endpoint: the icon RPCs keep their pre-badge response shape.
@@ -4168,6 +4898,9 @@ window.__ModuleLoader__.load({
4168
4898
  // Same region, same watchdog: the section-header badge is
4169
4899
  // re-injected whenever the shipped UI re-renders over it.
4170
4900
  paintHeaderBadge();
4901
+ // And the conversation header's ring, which lives outside this
4902
+ // container: any churn nearby is a fair moment to reconcile it.
4903
+ paintHeaderRing(runningConfig, headerRingValue);
4171
4904
  }, 200);
4172
4905
  };
4173
4906
  const attach = () => {
@@ -4201,6 +4934,7 @@ window.__ModuleLoader__.load({
4201
4934
  offRunning();
4202
4935
  clearWorkspaceDots();
4203
4936
  clearHeaderBadge();
4937
+ clearHeaderRing();
4204
4938
  // The one highlight this plugin ever puts on a SHIPPED row: it must not
4205
4939
  // outlive the plugin, even if the unload lands inside its flash window.
4206
4940
  clearRevealFlash();