@sleep2agi/agent-network-dashboard 0.6.3-preview.26 → 0.6.3-preview.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/diagnostics/route-bundle-stats.json +56 -56
  4. package/.next/fallback-build-manifest.json +3 -3
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/server/app/_global-error.html +1 -1
  7. package/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +2 -2
  15. package/.next/server/app/_not-found.rsc +13 -13
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +8 -8
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/admin/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/admin.html +2 -2
  24. package/.next/server/app/admin.rsc +15 -15
  25. package/.next/server/app/admin.segments/_full.segment.rsc +15 -15
  26. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  27. package/.next/server/app/admin.segments/_index.segment.rsc +8 -8
  28. package/.next/server/app/admin.segments/_tree.segment.rsc +2 -2
  29. package/.next/server/app/admin.segments/admin/__PAGE__.segment.rsc +4 -4
  30. package/.next/server/app/admin.segments/admin.segment.rsc +3 -3
  31. package/.next/server/app/index.html +2 -2
  32. package/.next/server/app/index.rsc +15 -15
  33. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  34. package/.next/server/app/index.segments/_full.segment.rsc +15 -15
  35. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  36. package/.next/server/app/index.segments/_index.segment.rsc +8 -8
  37. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  38. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  39. package/.next/server/app/login.html +2 -2
  40. package/.next/server/app/login.rsc +15 -15
  41. package/.next/server/app/login.segments/_full.segment.rsc +15 -15
  42. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  43. package/.next/server/app/login.segments/_index.segment.rsc +8 -8
  44. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  45. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  46. package/.next/server/app/login.segments/login.segment.rsc +3 -3
  47. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/logs.html +2 -2
  49. package/.next/server/app/logs.rsc +15 -15
  50. package/.next/server/app/logs.segments/_full.segment.rsc +15 -15
  51. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  52. package/.next/server/app/logs.segments/_index.segment.rsc +8 -8
  53. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  54. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  55. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  56. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/messages.html +2 -2
  58. package/.next/server/app/messages.rsc +15 -15
  59. package/.next/server/app/messages.segments/_full.segment.rsc +15 -15
  60. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  61. package/.next/server/app/messages.segments/_index.segment.rsc +8 -8
  62. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  63. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  64. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  65. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  66. package/.next/server/app/node.html +2 -2
  67. package/.next/server/app/node.rsc +15 -15
  68. package/.next/server/app/node.segments/_full.segment.rsc +15 -15
  69. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  70. package/.next/server/app/node.segments/_index.segment.rsc +8 -8
  71. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  72. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  73. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  74. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/nodes.html +2 -2
  76. package/.next/server/app/nodes.rsc +15 -15
  77. package/.next/server/app/nodes.segments/_full.segment.rsc +15 -15
  78. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  79. package/.next/server/app/nodes.segments/_index.segment.rsc +8 -8
  80. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  81. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  82. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  83. package/.next/server/app/page_client-reference-manifest.js +1 -1
  84. package/.next/server/app/providers/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/providers.html +2 -2
  86. package/.next/server/app/providers.rsc +15 -15
  87. package/.next/server/app/providers.segments/_full.segment.rsc +15 -15
  88. package/.next/server/app/providers.segments/_head.segment.rsc +4 -4
  89. package/.next/server/app/providers.segments/_index.segment.rsc +8 -8
  90. package/.next/server/app/providers.segments/_tree.segment.rsc +2 -2
  91. package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +4 -4
  92. package/.next/server/app/providers.segments/providers.segment.rsc +3 -3
  93. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  94. package/.next/server/app/server-logs.html +2 -2
  95. package/.next/server/app/server-logs.rsc +15 -15
  96. package/.next/server/app/server-logs.segments/_full.segment.rsc +15 -15
  97. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  98. package/.next/server/app/server-logs.segments/_index.segment.rsc +8 -8
  99. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  100. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  101. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  102. package/.next/server/app/servers/page_client-reference-manifest.js +1 -1
  103. package/.next/server/app/servers.html +2 -2
  104. package/.next/server/app/servers.rsc +15 -15
  105. package/.next/server/app/servers.segments/_full.segment.rsc +15 -15
  106. package/.next/server/app/servers.segments/_head.segment.rsc +4 -4
  107. package/.next/server/app/servers.segments/_index.segment.rsc +8 -8
  108. package/.next/server/app/servers.segments/_tree.segment.rsc +2 -2
  109. package/.next/server/app/servers.segments/servers/__PAGE__.segment.rsc +4 -4
  110. package/.next/server/app/servers.segments/servers.segment.rsc +3 -3
  111. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  112. package/.next/server/app/settings/networks.html +2 -2
  113. package/.next/server/app/settings/networks.rsc +15 -15
  114. package/.next/server/app/settings/networks.segments/_full.segment.rsc +15 -15
  115. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  116. package/.next/server/app/settings/networks.segments/_index.segment.rsc +8 -8
  117. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  118. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  119. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  120. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  121. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  122. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  123. package/.next/server/app/settings/tokens.html +2 -2
  124. package/.next/server/app/settings/tokens.rsc +15 -15
  125. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +15 -15
  126. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  127. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +8 -8
  128. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  129. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  130. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  131. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  132. package/.next/server/app/settings.html +2 -2
  133. package/.next/server/app/settings.rsc +15 -15
  134. package/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  135. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  136. package/.next/server/app/settings.segments/_index.segment.rsc +8 -8
  137. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  138. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  139. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  140. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  141. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  142. package/.next/server/app/tasks.html +2 -2
  143. package/.next/server/app/tasks.rsc +15 -15
  144. package/.next/server/app/tasks.segments/_full.segment.rsc +15 -15
  145. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  146. package/.next/server/app/tasks.segments/_index.segment.rsc +8 -8
  147. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  148. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  149. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  150. package/.next/server/chunks/ssr/[root-of-the-server]__01ba829._.js +1 -1
  151. package/.next/server/chunks/ssr/[root-of-the-server]__01ba829._.js.map +1 -1
  152. package/.next/server/chunks/ssr/[root-of-the-server]__03jniap._.js +1 -1
  153. package/.next/server/chunks/ssr/[root-of-the-server]__03jniap._.js.map +1 -1
  154. package/.next/server/chunks/ssr/[root-of-the-server]__091su60._.js +2 -2
  155. package/.next/server/chunks/ssr/[root-of-the-server]__091su60._.js.map +1 -1
  156. package/.next/server/chunks/ssr/dash-loop-wt_0rh8p9o._.js +2 -2
  157. package/.next/server/chunks/ssr/dash-loop-wt_0rh8p9o._.js.map +1 -1
  158. package/.next/server/chunks/ssr/dash-loop-wt_app_0~.1~ew._.js +1 -1
  159. package/.next/server/chunks/ssr/dash-loop-wt_app_0~.1~ew._.js.map +1 -1
  160. package/.next/server/chunks/ssr/dash-loop-wt_app_13k3-ai._.js +1 -1
  161. package/.next/server/chunks/ssr/dash-loop-wt_app_13k3-ai._.js.map +1 -1
  162. package/.next/server/chunks/ssr/dash-loop-wt_app_components_0cym4cd._.js +1 -1
  163. package/.next/server/chunks/ssr/dash-loop-wt_app_components_0cym4cd._.js.map +1 -1
  164. package/.next/server/chunks/ssr/dash-loop-wt_app_nodes_page_tsx_0ma3oqt._.js +1 -1
  165. package/.next/server/chunks/ssr/dash-loop-wt_app_nodes_page_tsx_0ma3oqt._.js.map +1 -1
  166. package/.next/server/middleware-build-manifest.js +3 -3
  167. package/.next/server/pages/404.html +2 -2
  168. package/.next/server/pages/500.html +1 -1
  169. package/.next/server/server-reference-manifest.js +1 -1
  170. package/.next/server/server-reference-manifest.json +1 -1
  171. package/.next/static/chunks/003mjjqx3v6yk.js +1 -0
  172. package/.next/static/chunks/{0.wghorif1wb7.js → 06.n2y2k~yrt~.js} +1 -1
  173. package/.next/static/chunks/{0r7w27h~adl.3.js → 0an42rwo7varw.js} +2 -2
  174. package/.next/static/chunks/{0~iyv4rh~-isz.js → 0cezceovnbf62.js} +1 -1
  175. package/.next/static/chunks/{13f074b29_3mi.js → 0cikm6bvsx0mf.js} +1 -1
  176. package/.next/static/chunks/0lxgv945mayng.css +1 -0
  177. package/.next/static/chunks/{100b9zpd7fl1_.js → 0t6h_sz7ghqia.js} +2 -2
  178. package/.next/static/chunks/{0.n6i7_v_qiyj.js → 0z2gn4t9od9qr.js} +1 -1
  179. package/.next/static/chunks/{0qgybbsb9e6q4.js → 178ne2q2eh~.y.js} +1 -1
  180. package/.next/trace +2 -2
  181. package/.next/trace-build +1 -1
  182. package/app/components/AgentCard.tsx +9 -1
  183. package/app/components/CommandCenter.tsx +12 -1
  184. package/app/components/MobileNav.tsx +4 -1
  185. package/app/components/NodeSettingsPanel.tsx +27 -0
  186. package/app/components/TaskChatPanel.tsx +10 -1
  187. package/app/lib/chat-mute.ts +58 -0
  188. package/app/lib/chat-unread.ts +14 -5
  189. package/app/nodes/page.tsx +11 -2
  190. package/package.json +1 -1
  191. package/.next/static/chunks/01ehs~t9be8tg.css +0 -1
  192. package/.next/static/chunks/11.tobnbsiypu.js +0 -1
  193. /package/.next/static/{IBftMMAovgdSJ2wqVhkUA → 6ay82HXjtazDvlzvX_UGl}/_buildManifest.js +0 -0
  194. /package/.next/static/{IBftMMAovgdSJ2wqVhkUA → 6ay82HXjtazDvlzvX_UGl}/_clientMiddlewareManifest.js +0 -0
  195. /package/.next/static/{IBftMMAovgdSJ2wqVhkUA → 6ay82HXjtazDvlzvX_UGl}/_ssgManifest.js +0 -0
@@ -5,6 +5,7 @@ import { useChatUnread } from '../lib/chat-unread';
5
5
  import { useHealth } from '../lib/hooks';
6
6
  import { useNetworkId } from '../lib/network-context';
7
7
  import { useHasDraft } from '../lib/chat-drafts';
8
+ import { isMuted, useMuteVersion } from '../lib/chat-mute';
8
9
  import { TaskChatPanel } from './TaskChatPanel';
9
10
  import { t } from '../lib/ui-lang';
10
11
 
@@ -24,6 +25,8 @@ interface CommandCenterProps {
24
25
  */
25
26
  export function CommandCenter({ tabs, activeTab, onOpenTab, onCloseTab, onSetActive, onClose }: CommandCenterProps) {
26
27
  const { unreadCount } = useChatUnread();
28
+ const muteVersion = useMuteVersion();
29
+ void muteVersion;
27
30
  // R33: tab dot now shows PRESENCE (green online / gray offline, same SSE
28
31
  // definition as everywhere) — the active tab is already marked by the
29
32
  // border+text color, so the old active-state dot was redundant.
@@ -62,10 +65,18 @@ export function CommandCenter({ tabs, activeTab, onOpenTab, onCloseTab, onSetAct
62
65
  >
63
66
  <div className={`w-2 h-2 rounded-full ${isOnline(alias) ? 'bg-green-400' : 'bg-gray-600'}`} />
64
67
  <span className="max-w-[80px] truncate">{alias}</span>
68
+ {isMuted(alias) && (
69
+ <svg aria-label="已开启免打扰" role="img" className="h-3 w-3 shrink-0 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
70
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9.88 4.12A6 6 0 0118 9v3.6l1.5 2.4H12M6.3 6.3A5.98 5.98 0 006 9v3.6L4.5 15H13M10 19a2 2 0 004 0M3 3l18 18" />
71
+ </svg>
72
+ )}
65
73
  {/* R8: WeChat unread badge on BACKGROUND tabs (the active
66
74
  tab is being read — no badge). */}
67
75
  {activeTab !== alias && <TabDraftMark alias={alias} />}
68
- {activeTab !== alias && unreadCount(alias) > 0 && (
76
+ {activeTab !== alias && unreadCount(alias) > 0 && isMuted(alias) && (
77
+ <span className="h-2 w-2 shrink-0 rounded-full bg-red-500/80" />
78
+ )}
79
+ {activeTab !== alias && unreadCount(alias) > 0 && !isMuted(alias) && (
69
80
  <span className="min-w-[15px] h-[15px] px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-[15px] text-center">
70
81
  {unreadCount(alias) > 99 ? '99+' : unreadCount(alias)}
71
82
  </span>
@@ -28,7 +28,7 @@ export function MobileNav() {
28
28
  const pathname = usePathname();
29
29
  // R8: WeChat-style total unread on the Agents tab (a glance answers
30
30
  // "有没有人找我" from anywhere in the app).
31
- const { totalUnread } = useChatUnread();
31
+ const { totalUnread, hasMutedUnread } = useChatUnread();
32
32
  // Match Sidebar.tsx isActive — /settings must be exact-match because
33
33
  // Settings subpages (/settings/tokens, /settings/networks) have their
34
34
  // own headers; startsWith would keep the bottom-nav highlighted there
@@ -60,6 +60,9 @@ export function MobileNav() {
60
60
  <svg className="h-5 w-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.6}>
61
61
  <path strokeLinecap="round" strokeLinejoin="round" d={item.icon} />
62
62
  </svg>
63
+ {item.href === '/nodes' && totalUnread === 0 && hasMutedUnread && (
64
+ <span aria-label="有未读(已静音)" className="absolute -top-0.5 right-1 h-2 w-2 rounded-full bg-red-500/80" />
65
+ )}
63
66
  {item.href === '/nodes' && totalUnread > 0 && (
64
67
  <span className="absolute -top-1 -right-2.5 min-w-[15px] h-[15px] px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-[15px] text-center">
65
68
  {badgeLabel(totalUnread)}
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
4
4
  import { Session } from './types';
5
5
  import { AliasAvatar } from './AliasAvatar';
6
6
  import { setAvatarUrl, useAvatarUrl } from '../lib/avatars';
7
+ import { toggleMute, useMuted } from '../lib/chat-mute';
7
8
  import { useLifecycleCaps } from './NodeLifecycleMenu';
8
9
 
9
10
  /**
@@ -148,6 +149,30 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
148
149
  return <h3 className="text-[11px] font-medium uppercase tracking-wide text-gray-500 mb-2">{children}</h3>;
149
150
  }
150
151
 
152
+ /* R34 (微信消息免打扰): per-conversation mute switch. Muted = badge shows
153
+ a dot instead of the count and the global total excludes it. */
154
+ function MuteSection({ alias }: { alias: string }) {
155
+ const muted = useMuted(alias);
156
+ return (
157
+ <section>
158
+ <SectionTitle>聊天</SectionTitle>
159
+ <button
160
+ type="button"
161
+ role="switch"
162
+ aria-checked={muted}
163
+ onClick={() => toggleMute(alias)}
164
+ className="flex w-full items-center justify-between rounded-lg border border-[#26262b] bg-[#161618] px-3 py-2.5"
165
+ >
166
+ <span className="text-[12px] text-gray-300">消息免打扰</span>
167
+ <span className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${muted ? 'bg-cyan-600' : 'bg-[#2a2a30]'}`}>
168
+ <span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${muted ? 'translate-x-4.5' : 'translate-x-0.5'}`} style={{ transform: muted ? 'translateX(18px)' : 'translateX(2px)' }} />
169
+ </span>
170
+ </button>
171
+ <p className="mt-1.5 text-[10px] text-gray-600">静音后未读只显红点、不计入全局数字(存本浏览器)</p>
172
+ </section>
173
+ );
174
+ }
175
+
151
176
  /* R24 (Vincent 亲点: 头像换血) — per-node custom avatar. Stored browser-side
152
177
  (localStorage via app/lib/avatars) until the hub grows an RFC-024
153
178
  whitelist entry for avatar_url; the designed default set rides
@@ -639,6 +664,8 @@ export function NodeSettingsPanel({ session: s, onClose }: { session: Session; o
639
664
 
640
665
  <AvatarSection alias={s.alias} />
641
666
 
667
+ <MuteSection alias={s.alias} />
668
+
642
669
  {/* B. Runtime + 模型 (model editable) */}
643
670
  <section>
644
671
  <SectionTitle>Runtime · 模型</SectionTitle>
@@ -321,6 +321,7 @@ import remarkGfm from 'remark-gfm';
321
321
  import { useSSE } from '../lib/useSSE';
322
322
  import { useNetworkId } from '../lib/network-context';
323
323
  import { useHealth } from '../lib/hooks';
324
+ import { useMuted } from '../lib/chat-mute';
324
325
  import { markChatRead } from '../lib/chat-unread';
325
326
  import { readDraft, writeDraft } from '../lib/chat-drafts';
326
327
  import { pinyinMatch, usePinyinReady } from '../lib/pinyin-match';
@@ -464,6 +465,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
464
465
  // (#214 "one definition"); useHealth is SWR-deduped with the page-level
465
466
  // poll, so this adds no extra requests while Overview is mounted.
466
467
  const { health } = useHealth();
468
+ const chatMuted = useMuted(alias);
467
469
  const sseOnline = (() => {
468
470
  const m = health?.sse_sessions;
469
471
  if (!m) return null;
@@ -1444,7 +1446,14 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes, active }
1444
1446
  <div className="flex items-center gap-3">
1445
1447
  <AliasAvatar alias={alias} size={32} />
1446
1448
  <div>
1447
- <div className="text-sm font-semibold text-[var(--fg)]">{alias}</div>
1449
+ <div className="flex items-center gap-1.5 text-sm font-semibold text-[var(--fg)]">
1450
+ <span>{alias}</span>
1451
+ {chatMuted && (
1452
+ <svg aria-label={t('已开启免打扰', 'Muted')} role="img" className="h-3.5 w-3.5 text-[var(--fg-dim)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
1453
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9.88 4.12A6 6 0 0118 9v3.6l1.5 2.4H12M6.3 6.3A5.98 5.98 0 006 9v3.6L4.5 15H13M10 19a2 2 0 004 0M3 3l18 18" />
1454
+ </svg>
1455
+ )}
1456
+ </div>
1448
1457
  <div className="flex items-center gap-1.5 text-[10px] text-[var(--fg-muted)]">
1449
1458
  {sseOnline !== null && (
1450
1459
  <span aria-hidden className={`inline-block h-1.5 w-1.5 rounded-full ${sseOnline ? 'bg-green-400' : 'bg-gray-600'}`} />
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { useSyncExternalStore } from 'react';
4
+
5
+ /* Loop R34 (微信消息免打扰, 克制版): per-conversation mute. Muted
6
+ * conversations keep their red badge but show a DOT instead of the count,
7
+ * and their unreads are excluded from the global total (WeChat semantics).
8
+ * Same storage/reactivity pattern as chat-drafts. */
9
+
10
+ const STORE_KEY = 'anet_chat_mute_v1';
11
+ const MUTE_EVENT = 'anet-chat-mute-updated';
12
+
13
+ function readSet(): Set<string> {
14
+ if (typeof window === 'undefined') return new Set();
15
+ try {
16
+ const parsed = JSON.parse(window.localStorage.getItem(STORE_KEY) || '[]');
17
+ return new Set(Array.isArray(parsed) ? parsed : []);
18
+ } catch {
19
+ return new Set();
20
+ }
21
+ }
22
+
23
+ export function isMuted(alias?: string | null): boolean {
24
+ if (!alias) return false;
25
+ return readSet().has(alias);
26
+ }
27
+
28
+ export function toggleMute(alias: string) {
29
+ if (typeof window === 'undefined' || !alias) return;
30
+ try {
31
+ const s = readSet();
32
+ if (s.has(alias)) s.delete(alias);
33
+ else s.add(alias);
34
+ window.localStorage.setItem(STORE_KEY, JSON.stringify([...s]));
35
+ window.dispatchEvent(new Event(MUTE_EVENT));
36
+ } catch {}
37
+ }
38
+
39
+ let version = 0;
40
+ function subscribe(callback: () => void) {
41
+ const bump = () => { version++; callback(); };
42
+ window.addEventListener(MUTE_EVENT, bump);
43
+ window.addEventListener('storage', bump);
44
+ return () => {
45
+ window.removeEventListener(MUTE_EVENT, bump);
46
+ window.removeEventListener('storage', bump);
47
+ };
48
+ }
49
+
50
+ export function useMuted(alias?: string | null): boolean {
51
+ return useSyncExternalStore(subscribe, () => (alias ? isMuted(alias) : false), () => false);
52
+ }
53
+
54
+ /** Monotonic version for callers that use plain isMuted() inside render
55
+ * loops (map callbacks can't call hooks). */
56
+ export function useMuteVersion(): number {
57
+ return useSyncExternalStore(subscribe, () => version, () => 0);
58
+ }
@@ -3,6 +3,7 @@
3
3
  import { useCallback, useEffect, useMemo, useState } from 'react';
4
4
  import useSWR from 'swr';
5
5
  import { useNetworkId } from './network-context';
6
+ import { isMuted, useMuteVersion } from './chat-mute';
6
7
 
7
8
  const STORAGE_PREFIX = 'anet_chat_read_v1:';
8
9
  const UNREAD_EVENT = 'anet-chat-read-updated';
@@ -181,17 +182,25 @@ export function useChatUnread() {
181
182
  return unreadMap.lastActivity.get(alias) || 0;
182
183
  }, [unreadMap]);
183
184
 
184
- const totalUnread = useMemo(() => {
185
+ // R34: muted conversations are excluded from the global count (WeChat
186
+ // semantics); hasMutedUnread lets entry badges fall back to a dot.
187
+ const muteVersion = useMuteVersion();
188
+ const { totalUnread, hasMutedUnread } = useMemo(() => {
189
+ void muteVersion;
185
190
  let t = 0;
186
- for (const n of unreadMap.counts.values()) t += n;
187
- return t;
188
- }, [unreadMap]);
191
+ let mutedAny = false;
192
+ for (const [alias, n] of unreadMap.counts) {
193
+ if (isMuted(alias)) { if (n > 0) mutedAny = true; continue; }
194
+ t += n;
195
+ }
196
+ return { totalUnread: t, hasMutedUnread: mutedAny };
197
+ }, [unreadMap, muteVersion]);
189
198
 
190
199
  const clearUnread = useCallback((alias: string) => {
191
200
  markChatRead(alias, networkId);
192
201
  }, [networkId]);
193
202
 
194
- return { unreadMap, hasUnread, unreadCount, lastActivityAt, totalUnread, clearUnread };
203
+ return { unreadMap, hasUnread, unreadCount, lastActivityAt, totalUnread, hasMutedUnread, clearUnread };
195
204
  }
196
205
 
197
206
  /** WeChat badge label: 1-99 as-is, above → "99+"; 0/negative → '' (hide). */
@@ -10,6 +10,7 @@ import { NodeLifecycleMenu } from '../components/NodeLifecycleMenu';
10
10
  import { EmptyState, NodesEmptyState } from '../components/EmptyState';
11
11
  import { AliasAvatar } from '../components/AliasAvatar';
12
12
  import { useHasDraft } from '../lib/chat-drafts';
13
+ import { isMuted, useMuteVersion } from '../lib/chat-mute';
13
14
  import { pinyinMatch, usePinyinReady } from '../lib/pinyin-match';
14
15
  import { useCollapsibleSearch } from '../components/CollapsibleSearch';
15
16
  import type { Session } from '../components/types';
@@ -56,6 +57,8 @@ export default function NodesPage() {
56
57
  const [filterStatus, setFilterStatus] = useState('');
57
58
  const [search, setSearch] = useState('');
58
59
  const pinyinReady = usePinyinReady();
60
+ const muteVersion = useMuteVersion();
61
+ void muteVersion; // re-render on mute toggles (isMuted used in map callbacks)
59
62
  void pinyinReady; // R25: re-filter once the lazy pinyin dict lands
60
63
  const [viewMode, setViewMode] = useState<ViewMode>('list');
61
64
  const [chatAlias, setChatAlias] = useState<string | null>(null);
@@ -231,7 +234,10 @@ export default function NodesPage() {
231
234
  <div className="relative shrink-0">
232
235
  <span className="relative inline-flex shrink-0">
233
236
  <AliasAvatar alias={s.alias} size={36} />
234
- {unreadCount(s.alias) > 0 && (
237
+ {unreadCount(s.alias) > 0 && isMuted(s.alias) && (
238
+ <span aria-label={t('有未读(已静音)', 'Unread (muted)')} className="absolute -top-0.5 -right-1 h-2.5 w-2.5 rounded-full bg-red-500/80 ring-2 ring-[#161618]" />
239
+ )}
240
+ {unreadCount(s.alias) > 0 && !isMuted(s.alias) && (
235
241
  <span aria-label={t(`${unreadCount(s.alias)} 条未读`, `${unreadCount(s.alias)} unread`)} className="absolute -top-1 -right-1.5 min-w-[16px] h-4 px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-4 text-center ring-2 ring-[#161618]">
236
242
  {unreadCount(s.alias) > 99 ? '99+' : unreadCount(s.alias)}
237
243
  </span>
@@ -321,7 +327,10 @@ export default function NodesPage() {
321
327
  <div className="relative shrink-0">
322
328
  <span className="relative inline-flex shrink-0">
323
329
  <AliasAvatar alias={s.alias} size={20} />
324
- {unreadCount(s.alias) > 0 && (
330
+ {unreadCount(s.alias) > 0 && isMuted(s.alias) && (
331
+ <span aria-label={t('有未读(已静音)', 'Unread (muted)')} className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-red-500/80" />
332
+ )}
333
+ {unreadCount(s.alias) > 0 && !isMuted(s.alias) && (
325
334
  <span aria-label={t(`${unreadCount(s.alias)} 条未读`, `${unreadCount(s.alias)} unread`)} className="absolute -top-1.5 -right-2 min-w-[15px] h-[15px] px-1 rounded-full bg-red-500 text-white text-[9px] font-semibold leading-[15px] text-center">
326
335
  {unreadCount(s.alias) > 99 ? '99+' : unreadCount(s.alias)}
327
336
  </span>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/agent-network-dashboard",
3
- "version": "0.6.3-preview.26",
3
+ "version": "0.6.3-preview.27",
4
4
  "description": "Agent Network Dashboard — Web UI for managing AI Agent networks",
5
5
  "main": "apps/desktop/electron/main.cjs",
6
6
  "scripts": {