@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 +767 -33
- package/lib/index.js +27 -8
- package/lib/running.js +26 -5
- package/lib/typert.host.js +19 -10
- package/lib/unread.js +66 -13
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
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
|
|
881
|
-
* them: they are not part of the browsing the user does
|
|
882
|
-
* `sessionVisible` hides them outright), so
|
|
883
|
-
*
|
|
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")
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3817
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4621
|
+
hintLine("notificationPermissionHint"),
|
|
3926
4622
|
React.createElement("div", { style: style.hint }, t("appBadgeFootnote"))
|
|
3927
4623
|
),
|
|
3928
|
-
|
|
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
|
-
|
|
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();
|