@sleep2agi/agent-network-dashboard 0.4.5-preview.2 → 0.4.5-preview.21

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 (238) 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 +46 -46
  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 +12 -12
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +7 -7
  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 +14 -14
  25. package/.next/server/app/admin.segments/_full.segment.rsc +14 -14
  26. package/.next/server/app/admin.segments/_head.segment.rsc +4 -4
  27. package/.next/server/app/admin.segments/_index.segment.rsc +7 -7
  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 +14 -14
  33. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  34. package/.next/server/app/index.segments/_full.segment.rsc +14 -14
  35. package/.next/server/app/index.segments/_head.segment.rsc +4 -4
  36. package/.next/server/app/index.segments/_index.segment.rsc +7 -7
  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 +14 -14
  41. package/.next/server/app/login.segments/_full.segment.rsc +14 -14
  42. package/.next/server/app/login.segments/_head.segment.rsc +4 -4
  43. package/.next/server/app/login.segments/_index.segment.rsc +7 -7
  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.js.nft.json +1 -1
  48. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  49. package/.next/server/app/logs.html +2 -2
  50. package/.next/server/app/logs.rsc +14 -14
  51. package/.next/server/app/logs.segments/_full.segment.rsc +14 -14
  52. package/.next/server/app/logs.segments/_head.segment.rsc +4 -4
  53. package/.next/server/app/logs.segments/_index.segment.rsc +7 -7
  54. package/.next/server/app/logs.segments/_tree.segment.rsc +2 -2
  55. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +4 -4
  56. package/.next/server/app/logs.segments/logs.segment.rsc +3 -3
  57. package/.next/server/app/messages/page.js.nft.json +1 -1
  58. package/.next/server/app/messages/page_client-reference-manifest.js +1 -1
  59. package/.next/server/app/messages.html +2 -2
  60. package/.next/server/app/messages.rsc +14 -14
  61. package/.next/server/app/messages.segments/_full.segment.rsc +14 -14
  62. package/.next/server/app/messages.segments/_head.segment.rsc +4 -4
  63. package/.next/server/app/messages.segments/_index.segment.rsc +7 -7
  64. package/.next/server/app/messages.segments/_tree.segment.rsc +2 -2
  65. package/.next/server/app/messages.segments/messages/__PAGE__.segment.rsc +4 -4
  66. package/.next/server/app/messages.segments/messages.segment.rsc +3 -3
  67. package/.next/server/app/node/page.js.nft.json +1 -1
  68. package/.next/server/app/node/page_client-reference-manifest.js +1 -1
  69. package/.next/server/app/node.html +2 -2
  70. package/.next/server/app/node.rsc +14 -14
  71. package/.next/server/app/node.segments/_full.segment.rsc +14 -14
  72. package/.next/server/app/node.segments/_head.segment.rsc +4 -4
  73. package/.next/server/app/node.segments/_index.segment.rsc +7 -7
  74. package/.next/server/app/node.segments/_tree.segment.rsc +2 -2
  75. package/.next/server/app/node.segments/node/__PAGE__.segment.rsc +4 -4
  76. package/.next/server/app/node.segments/node.segment.rsc +3 -3
  77. package/.next/server/app/nodes/page.js.nft.json +1 -1
  78. package/.next/server/app/nodes/page_client-reference-manifest.js +1 -1
  79. package/.next/server/app/nodes.html +2 -2
  80. package/.next/server/app/nodes.rsc +14 -14
  81. package/.next/server/app/nodes.segments/_full.segment.rsc +14 -14
  82. package/.next/server/app/nodes.segments/_head.segment.rsc +4 -4
  83. package/.next/server/app/nodes.segments/_index.segment.rsc +7 -7
  84. package/.next/server/app/nodes.segments/_tree.segment.rsc +2 -2
  85. package/.next/server/app/nodes.segments/nodes/__PAGE__.segment.rsc +4 -4
  86. package/.next/server/app/nodes.segments/nodes.segment.rsc +3 -3
  87. package/.next/server/app/page.js.nft.json +1 -1
  88. package/.next/server/app/page_client-reference-manifest.js +1 -1
  89. package/.next/server/app/server-logs/page_client-reference-manifest.js +1 -1
  90. package/.next/server/app/server-logs.html +2 -2
  91. package/.next/server/app/server-logs.rsc +14 -14
  92. package/.next/server/app/server-logs.segments/_full.segment.rsc +14 -14
  93. package/.next/server/app/server-logs.segments/_head.segment.rsc +4 -4
  94. package/.next/server/app/server-logs.segments/_index.segment.rsc +7 -7
  95. package/.next/server/app/server-logs.segments/_tree.segment.rsc +2 -2
  96. package/.next/server/app/server-logs.segments/server-logs/__PAGE__.segment.rsc +4 -4
  97. package/.next/server/app/server-logs.segments/server-logs.segment.rsc +3 -3
  98. package/.next/server/app/settings/networks/page.js.nft.json +1 -1
  99. package/.next/server/app/settings/networks/page_client-reference-manifest.js +1 -1
  100. package/.next/server/app/settings/networks.html +2 -2
  101. package/.next/server/app/settings/networks.rsc +14 -14
  102. package/.next/server/app/settings/networks.segments/_full.segment.rsc +14 -14
  103. package/.next/server/app/settings/networks.segments/_head.segment.rsc +4 -4
  104. package/.next/server/app/settings/networks.segments/_index.segment.rsc +7 -7
  105. package/.next/server/app/settings/networks.segments/_tree.segment.rsc +2 -2
  106. package/.next/server/app/settings/networks.segments/settings/networks/__PAGE__.segment.rsc +4 -4
  107. package/.next/server/app/settings/networks.segments/settings/networks.segment.rsc +3 -3
  108. package/.next/server/app/settings/networks.segments/settings.segment.rsc +3 -3
  109. package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  110. package/.next/server/app/settings/tokens/page_client-reference-manifest.js +1 -1
  111. package/.next/server/app/settings/tokens.html +2 -2
  112. package/.next/server/app/settings/tokens.rsc +14 -14
  113. package/.next/server/app/settings/tokens.segments/_full.segment.rsc +14 -14
  114. package/.next/server/app/settings/tokens.segments/_head.segment.rsc +4 -4
  115. package/.next/server/app/settings/tokens.segments/_index.segment.rsc +7 -7
  116. package/.next/server/app/settings/tokens.segments/_tree.segment.rsc +2 -2
  117. package/.next/server/app/settings/tokens.segments/settings/tokens/__PAGE__.segment.rsc +4 -4
  118. package/.next/server/app/settings/tokens.segments/settings/tokens.segment.rsc +3 -3
  119. package/.next/server/app/settings/tokens.segments/settings.segment.rsc +3 -3
  120. package/.next/server/app/settings.html +2 -2
  121. package/.next/server/app/settings.rsc +14 -14
  122. package/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  123. package/.next/server/app/settings.segments/_head.segment.rsc +4 -4
  124. package/.next/server/app/settings.segments/_index.segment.rsc +7 -7
  125. package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  126. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +4 -4
  127. package/.next/server/app/settings.segments/settings.segment.rsc +3 -3
  128. package/.next/server/app/tasks/[id]/page_client-reference-manifest.js +1 -1
  129. package/.next/server/app/tasks/page.js.nft.json +1 -1
  130. package/.next/server/app/tasks/page_client-reference-manifest.js +1 -1
  131. package/.next/server/app/tasks.html +2 -2
  132. package/.next/server/app/tasks.rsc +14 -14
  133. package/.next/server/app/tasks.segments/_full.segment.rsc +14 -14
  134. package/.next/server/app/tasks.segments/_head.segment.rsc +4 -4
  135. package/.next/server/app/tasks.segments/_index.segment.rsc +7 -7
  136. package/.next/server/app/tasks.segments/_tree.segment.rsc +2 -2
  137. package/.next/server/app/tasks.segments/tasks/__PAGE__.segment.rsc +4 -4
  138. package/.next/server/app/tasks.segments/tasks.segment.rsc +3 -3
  139. package/.next/server/chunks/ssr/[root-of-the-server]__02ee4km._.js +3 -0
  140. package/.next/server/chunks/ssr/[root-of-the-server]__02ee4km._.js.map +1 -0
  141. package/.next/server/chunks/ssr/[root-of-the-server]__0jjqrcf._.js +3 -0
  142. package/.next/server/chunks/ssr/[root-of-the-server]__0jjqrcf._.js.map +1 -0
  143. package/.next/server/chunks/ssr/[root-of-the-server]__0o8yj3c._.js +3 -0
  144. package/.next/server/chunks/ssr/[root-of-the-server]__0o8yj3c._.js.map +1 -0
  145. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js +1 -1
  146. package/.next/server/chunks/ssr/[root-of-the-server]__0xm3h9x._.js.map +1 -1
  147. package/.next/server/chunks/ssr/_0nne7mz._.js +3 -0
  148. package/.next/server/chunks/ssr/_0nne7mz._.js.map +1 -0
  149. package/.next/server/chunks/ssr/app_0.zbtn5._.js +9 -0
  150. package/.next/server/chunks/ssr/app_0.zbtn5._.js.map +1 -0
  151. package/.next/server/chunks/ssr/app_04n78pr._.js +3 -0
  152. package/.next/server/chunks/ssr/app_04n78pr._.js.map +1 -0
  153. package/.next/server/chunks/ssr/app_0_agk0r._.js +3 -0
  154. package/.next/server/chunks/ssr/app_0_agk0r._.js.map +1 -0
  155. package/.next/server/chunks/ssr/app_0dk5wao._.js +1 -1
  156. package/.next/server/chunks/ssr/app_0dk5wao._.js.map +1 -1
  157. package/.next/server/chunks/ssr/app_components_08ys5y3._.js +2 -2
  158. package/.next/server/chunks/ssr/app_components_08ys5y3._.js.map +1 -1
  159. package/.next/server/chunks/ssr/app_components_0lhp_k.._.js +3 -0
  160. package/.next/server/chunks/ssr/app_components_0lhp_k.._.js.map +1 -0
  161. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js +1 -1
  162. package/.next/server/chunks/ssr/app_login_page_tsx_0z71ad3._.js.map +1 -1
  163. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js +1 -1
  164. package/.next/server/chunks/ssr/app_server-logs_page_tsx_0_kpp1p._.js.map +1 -1
  165. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js +1 -1
  166. package/.next/server/chunks/ssr/app_tasks_page_tsx_0z-d6mp._.js.map +1 -1
  167. package/.next/server/middleware-build-manifest.js +3 -3
  168. package/.next/server/pages/404.html +2 -2
  169. package/.next/server/pages/500.html +1 -1
  170. package/.next/server/server-reference-manifest.js +1 -1
  171. package/.next/server/server-reference-manifest.json +1 -1
  172. package/.next/static/chunks/00-ce-5_uu76b.js +1 -0
  173. package/.next/static/chunks/0943n0t.9c-o2.js +1 -0
  174. package/.next/static/chunks/{07okhs5561y5y.js → 0c1bp~g7.8ru..js} +1 -1
  175. package/.next/static/chunks/0eajktwdxy7qt.js +1 -0
  176. package/.next/static/chunks/0hqhvq6f83m38.js +1 -0
  177. package/.next/static/chunks/0irp6cdaafs7m.css +2 -0
  178. package/.next/static/chunks/0l3gf0ickq445.js +1 -0
  179. package/.next/static/chunks/0pf4_9.-8s34p.js +1 -0
  180. package/.next/static/chunks/0q4rltx2ngh5m.js +1 -0
  181. package/.next/static/chunks/0qnpc7rbxn12u.js +1 -0
  182. package/.next/static/chunks/10-ka4k4ni9u2.js +1 -0
  183. package/.next/static/chunks/15nn5pyy3l5zo.js +7 -0
  184. package/.next/static/chunks/169v~dd1tdxdm.js +7 -0
  185. package/.next/static/chunks/17uwnuk~0zoo3.js +1 -0
  186. package/.next/trace +2 -2
  187. package/.next/trace-build +1 -1
  188. package/app/components/AgentCard.tsx +6 -2
  189. package/app/components/AliasAvatar.tsx +51 -0
  190. package/app/components/CommandPalette.tsx +53 -20
  191. package/app/components/HelpOverlay.tsx +50 -10
  192. package/app/components/Sidebar.tsx +40 -35
  193. package/app/components/TaskChatPanel.tsx +8 -10
  194. package/app/components/TaskDrawer.tsx +62 -23
  195. package/app/components/TopoGraph.tsx +16 -3
  196. package/app/globals.css +129 -0
  197. package/app/login/page.tsx +37 -7
  198. package/app/logs/page.tsx +28 -4
  199. package/app/messages/page.tsx +79 -48
  200. package/app/nodes/page.tsx +21 -23
  201. package/app/page.tsx +66 -13
  202. package/app/server-logs/page.tsx +37 -3
  203. package/app/settings/networks/page.tsx +37 -13
  204. package/app/settings/page.tsx +33 -8
  205. package/app/tasks/page.tsx +95 -35
  206. package/package.json +1 -1
  207. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js +0 -3
  208. package/.next/server/chunks/ssr/[root-of-the-server]__0_jnflc._.js.map +0 -1
  209. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js +0 -3
  210. package/.next/server/chunks/ssr/[root-of-the-server]__0nrckjd._.js.map +0 -1
  211. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js +0 -3
  212. package/.next/server/chunks/ssr/[root-of-the-server]__12mpoig._.js.map +0 -1
  213. package/.next/server/chunks/ssr/_0d-fuox._.js +0 -3
  214. package/.next/server/chunks/ssr/_0d-fuox._.js.map +0 -1
  215. package/.next/server/chunks/ssr/app_0.0wpa~._.js +0 -3
  216. package/.next/server/chunks/ssr/app_0.0wpa~._.js.map +0 -1
  217. package/.next/server/chunks/ssr/app_0et5yua._.js +0 -3
  218. package/.next/server/chunks/ssr/app_0et5yua._.js.map +0 -1
  219. package/.next/server/chunks/ssr/app_0ff~kjj._.js +0 -9
  220. package/.next/server/chunks/ssr/app_0ff~kjj._.js.map +0 -1
  221. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js +0 -3
  222. package/.next/server/chunks/ssr/app_page_tsx_0es_sk2._.js.map +0 -1
  223. package/.next/static/chunks/051gslfjka1kj.js +0 -1
  224. package/.next/static/chunks/06t_vzei8s9_h.js +0 -1
  225. package/.next/static/chunks/0b1ymo-btjfha.js +0 -1
  226. package/.next/static/chunks/0e7c40p~wq665.js +0 -1
  227. package/.next/static/chunks/0g7m7zk.v7g6h.js +0 -1
  228. package/.next/static/chunks/0khpyxpdrp~ty.js +0 -1
  229. package/.next/static/chunks/0lo_g.q1fm-hr.js +0 -7
  230. package/.next/static/chunks/0ogeals5ufhho.js +0 -1
  231. package/.next/static/chunks/0qgc758gz3on4.js +0 -7
  232. package/.next/static/chunks/0xxk2ms76kkuz.css +0 -2
  233. package/.next/static/chunks/1031v6nw33ud3.js +0 -1
  234. package/.next/static/chunks/10njj7kn9ylkf.js +0 -1
  235. package/.next/static/chunks/13.dst7liziwo.js +0 -1
  236. /package/.next/static/{dvjBKNMRovte6RFP_k8Lq → I8oYGCFqEglZXHP7xG8Cd}/_buildManifest.js +0 -0
  237. /package/.next/static/{dvjBKNMRovte6RFP_k8Lq → I8oYGCFqEglZXHP7xG8Cd}/_clientMiddlewareManifest.js +0 -0
  238. /package/.next/static/{dvjBKNMRovte6RFP_k8Lq → I8oYGCFqEglZXHP7xG8Cd}/_ssgManifest.js +0 -0
@@ -22,6 +22,9 @@ interface Command {
22
22
  group: 'Recents' | 'Agents' | 'Tasks' | 'Actions' | 'Navigate';
23
23
  icon: React.ReactNode;
24
24
  perform: (router: ReturnType<typeof useRouter>) => void;
25
+ /** Matched char indices (set by fuzzy filter for highlight render). */
26
+ _titleHi?: number[];
27
+ _hintHi?: number[];
25
28
  }
26
29
 
27
30
  interface SessionRow { alias?: string; status?: string; agent?: string }
@@ -46,19 +49,21 @@ function pushRecent(id: string) {
46
49
  } catch {}
47
50
  }
48
51
 
49
- /** Fuzzy match: returns a score (higher = better) if every char in `needle`
50
- * appears in `hay` in order (consecutive bonus), else -1. Case-insensitive.
51
- * Round 18 replaces literal `String.includes` so `stg` matches `Settings`. */
52
- function fuzzyScore(hay: string, needle: string): number {
53
- if (!needle) return 0;
52
+ /** Fuzzy match: returns score + matched char positions in `hay`. -1 score
53
+ * means no match. Round 18 introduced fuzzy; round 20 returns indices so
54
+ * the UI can highlight matched chars (Linear / Raycast style). */
55
+ interface FuzzyResult { score: number; indices: number[]; }
56
+ function fuzzyMatch(hay: string, needle: string): FuzzyResult {
57
+ if (!needle) return { score: 0, indices: [] };
54
58
  const H = hay.toLowerCase();
55
59
  const N = needle.toLowerCase();
56
60
  let hi = 0, ni = 0, score = 0, streak = 0;
61
+ const indices: number[] = [];
57
62
  while (hi < H.length && ni < N.length) {
58
63
  if (H[hi] === N[ni]) {
59
- score += 2 + streak; // streak bonus rewards contiguous matches
64
+ score += 2 + streak;
60
65
  streak++;
61
- // Word-start bonus
66
+ indices.push(hi);
62
67
  if (hi === 0 || /[\s·:_/-]/.test(H[hi - 1])) score += 4;
63
68
  ni++;
64
69
  } else {
@@ -66,9 +71,25 @@ function fuzzyScore(hay: string, needle: string): number {
66
71
  }
67
72
  hi++;
68
73
  }
69
- if (ni < N.length) return -1; // not all needle chars matched
70
- // Shorter strings rank slightly higher (preferring exact hits)
71
- return score - Math.max(0, H.length - N.length) * 0.05;
74
+ if (ni < N.length) return { score: -1, indices: [] };
75
+ return { score: score - Math.max(0, H.length - N.length) * 0.05, indices };
76
+ }
77
+
78
+ /** Renders `text` with the chars at `indices` wrapped in <mark>. Handles
79
+ * unicode (Chinese aliases, emoji) by iterating code points via Array.from. */
80
+ function Highlight({ text, indices }: { text: string; indices: number[] }) {
81
+ if (!indices.length) return <>{text}</>;
82
+ const set = new Set(indices);
83
+ const chars = Array.from(text);
84
+ return (
85
+ <>
86
+ {chars.map((ch, i) =>
87
+ set.has(i)
88
+ ? <mark key={i} className="anet-cmdk-mark">{ch}</mark>
89
+ : <span key={i}>{ch}</span>
90
+ )}
91
+ </>
92
+ );
72
93
  }
73
94
 
74
95
  function NavIcon({ d }: { d: string }) {
@@ -237,21 +258,27 @@ export function CommandPalette() {
237
258
  }
238
259
  // Fuzzy match — `stg` matches `Settings`, `taska` matches `Task all-time`.
239
260
  // Score against title (weight 1.0), hint (0.6), id (0.3). Drop unmatched.
261
+ // Round 20: keep matched indices for title + hint so we can highlight.
240
262
  const scored = pool
241
263
  .map(c => {
242
- const sTitle = fuzzyScore(c.title, query);
243
- const sHint = c.hint ? fuzzyScore(c.hint, query) : -1;
244
- const sId = fuzzyScore(c.id, query);
264
+ const mt = fuzzyMatch(c.title, query);
265
+ const mh = c.hint ? fuzzyMatch(c.hint, query) : { score: -1, indices: [] };
266
+ const mi = fuzzyMatch(c.id, query);
245
267
  const best = Math.max(
246
- sTitle >= 0 ? sTitle * 1.0 : -Infinity,
247
- sHint >= 0 ? sHint * 0.6 : -Infinity,
248
- sId >= 0 ? sId * 0.3 : -Infinity,
268
+ mt.score >= 0 ? mt.score * 1.0 : -Infinity,
269
+ mh.score >= 0 ? mh.score * 0.6 : -Infinity,
270
+ mi.score >= 0 ? mi.score * 0.3 : -Infinity,
249
271
  );
250
- return { c, score: best };
272
+ return {
273
+ c,
274
+ score: best,
275
+ _titleHi: mt.score >= 0 ? mt.indices : [],
276
+ _hintHi: mh.score >= 0 ? mh.indices : [],
277
+ };
251
278
  })
252
279
  .filter(x => x.score > -Infinity);
253
280
  scored.sort((a, b) => b.score - a.score);
254
- return scored.map(x => x.c);
281
+ return scored.map(x => ({ ...x.c, _titleHi: x._titleHi, _hintHi: x._hintHi }));
255
282
  }, [query, recentIds, dynamic]);
256
283
 
257
284
  // Clamp selected when results shrink
@@ -332,8 +359,14 @@ export function CommandPalette() {
332
359
  >
333
360
  <span className={isActive ? 'text-cyan-400' : 'text-gray-500'}>{c.icon}</span>
334
361
  <span className="flex-1 min-w-0">
335
- <span className="block truncate">{c.title}</span>
336
- {c.hint && <span className="block text-[11px] text-gray-600 truncate">{c.hint}</span>}
362
+ <span className="block truncate">
363
+ <Highlight text={c.title} indices={c._titleHi || []} />
364
+ </span>
365
+ {c.hint && (
366
+ <span className="block text-[11px] text-gray-600 truncate">
367
+ <Highlight text={c.hint} indices={c._hintHi || []} />
368
+ </span>
369
+ )}
337
370
  </span>
338
371
  {isActive && <span className="text-[10px] text-gray-500">↵</span>}
339
372
  </button>
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState } from 'react';
3
+ import { useEffect, useState, useRef } from 'react';
4
+ import { useRouter } from 'next/navigation';
4
5
 
5
6
  /** Help overlay — press `?` to see keyboard shortcuts.
6
7
  * Restrained: no animation noise, single column, all global keys in one
@@ -23,29 +24,52 @@ const SHORTCUTS: { group: string; items: Shortcut[] }[] = [
23
24
  ],
24
25
  },
25
26
  {
26
- group: 'Command palette',
27
+ group: 'Navigate (vim-style)',
27
28
  items: [
28
- { keys: ['', ''], label: 'Move selection' },
29
- { keys: [''], label: 'Activate selected command' },
30
- { keys: ['type'], label: 'Filter searches commands, agents, tasks' },
29
+ { keys: ['g', 'o'], label: 'Go to Overview' },
30
+ { keys: ['g', 't'], label: 'Go to Tasks' },
31
+ { keys: ['g', 'n'], label: 'Go to Nodes' },
32
+ { keys: ['g', 'm'], label: 'Go to Messages' },
33
+ { keys: ['g', 'w'], label: 'Go to Networks' },
34
+ { keys: ['g', 'a'], label: 'Go to Admin' },
35
+ { keys: ['g', 'l'], label: 'Go to Audit Log' },
36
+ { keys: ['g', 's'], label: 'Go to Settings' },
31
37
  ],
32
38
  },
33
39
  {
34
- group: 'Pages',
40
+ group: 'Command palette',
35
41
  items: [
36
- { keys: ['click', 'sidebar logo'], label: 'Back to Overview' },
37
- { keys: ['Cmd+K', 'Go to agent X'], label: 'Jump straight to a node' },
42
+ { keys: ['', ''], label: 'Move selection' },
43
+ { keys: [''], label: 'Activate selected command' },
44
+ { keys: ['type'], label: 'Filter — searches commands, agents, tasks' },
38
45
  ],
39
46
  },
40
47
  ];
41
48
 
49
+ const GO_TARGETS: Record<string, string> = {
50
+ o: '/',
51
+ t: '/tasks',
52
+ n: '/nodes',
53
+ m: '/messages',
54
+ w: '/settings/networks',
55
+ a: '/admin',
56
+ l: '/logs',
57
+ s: '/settings',
58
+ };
59
+
42
60
  export function HelpOverlay() {
43
61
  const [open, setOpen] = useState(false);
62
+ const router = useRouter();
63
+ /** Timestamp when `g` was last pressed. The next single letter within
64
+ * 1500ms is interpreted as the target page. vim-style `g o`, `g t`, etc. */
65
+ const gAtRef = useRef<number>(0);
44
66
 
45
67
  useEffect(() => {
46
68
  const onKey = (e: KeyboardEvent) => {
69
+ const tag = document.activeElement?.tagName;
70
+ const inInput = tag === 'INPUT' || tag === 'TEXTAREA' || (document.activeElement as HTMLElement | null)?.isContentEditable;
47
71
  // `?` toggles — Shift+/ on US layout. Only when no input is focused.
48
- if (e.key === '?' && document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
72
+ if (e.key === '?' && !inInput) {
49
73
  e.preventDefault();
50
74
  setOpen(prev => !prev);
51
75
  return;
@@ -53,10 +77,26 @@ export function HelpOverlay() {
53
77
  if (e.key === 'Escape' && open) {
54
78
  setOpen(false);
55
79
  }
80
+ // Vim-style `g + letter` route shortcuts (round 35). Skip when modifier
81
+ // keys are held (g is a perfectly fine letter when ⌘ is involved) and
82
+ // when typing into a field.
83
+ if (inInput || e.metaKey || e.ctrlKey || e.altKey) return;
84
+ if (e.key === 'g') {
85
+ gAtRef.current = Date.now();
86
+ return;
87
+ }
88
+ if (Date.now() - gAtRef.current < 1500) {
89
+ const target = GO_TARGETS[e.key.toLowerCase()];
90
+ if (target) {
91
+ e.preventDefault();
92
+ gAtRef.current = 0;
93
+ router.push(target);
94
+ }
95
+ }
56
96
  };
57
97
  window.addEventListener('keydown', onKey);
58
98
  return () => window.removeEventListener('keydown', onKey);
59
- }, [open]);
99
+ }, [open, router]);
60
100
 
61
101
  if (!open) return null;
62
102
 
@@ -51,11 +51,12 @@ export function Sidebar() {
51
51
  href={item.href}
52
52
  prefetch={false}
53
53
  onClick={() => setMobileOpen(false)}
54
+ title={collapsed ? item.label : undefined}
54
55
  className={`relative flex items-center gap-3 px-3 py-3 rounded-lg text-sm transition-colors active:bg-[#1a1a3a] ${
55
56
  isActive(item.href)
56
57
  ? 'bg-cyan-500/10 text-cyan-300 border border-cyan-500/20 anet-nav-active'
57
58
  : 'text-gray-400 hover:text-gray-200 hover:bg-[#1a1a2a]'
58
- } ${collapsed ? 'justify-center' : ''}`}
59
+ } ${collapsed ? 'justify-center px-0' : ''}`}
59
60
  >
60
61
  <svg className="w-5 h-5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
61
62
  <path strokeLinecap="round" strokeLinejoin="round" d={item.icon} />
@@ -134,45 +135,49 @@ export function Sidebar() {
134
135
  {nav}
135
136
  </div>
136
137
 
137
- {/* Sign out + collapse */}
138
+ {/* Sign out + collapse — round 27: collapsed-state gets icon-only
139
+ variants so users still have Sign out / Quick search access at
140
+ 56px width, plus title= tooltips. */}
138
141
  <div className="absolute bottom-0 left-0 right-0 border-t border-[#2a2a4a] bg-[#0d0d1a]">
139
- {!collapsed && (
140
- <button
141
- onClick={async () => {
142
- await fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
143
- sessionStorage.removeItem('anet_v3_auth');
144
- window.location.assign('/login');
145
- }}
146
- className="w-full flex items-center gap-3 px-5 py-3 text-xs text-gray-500 hover:text-red-400 hover:bg-red-500/5 transition-colors"
147
- >
148
- <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
149
- <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
142
+ <button
143
+ onClick={() => {
144
+ window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, ctrlKey: true }));
145
+ }}
146
+ title={collapsed ? 'Quick search (⌘K)' : undefined}
147
+ className={`w-full flex items-center text-[11px] text-gray-600 hover:text-gray-400 hover:bg-[#1a1a2a] transition-colors ${
148
+ collapsed ? 'justify-center px-0 py-2.5' : 'justify-between gap-2 px-5 py-2'
149
+ }`}
150
+ aria-label="Open command palette"
151
+ >
152
+ <span className={`flex items-center ${collapsed ? '' : 'gap-2'}`}>
153
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
154
+ <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
150
155
  </svg>
151
- Sign out
152
- </button>
153
- )}
154
- {!collapsed && (
155
- <button
156
- onClick={() => {
157
- // Dispatch a synthetic keypress so the global Cmd+K listener fires
158
- window.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true, ctrlKey: true }));
159
- }}
160
- className="w-full flex items-center justify-between gap-2 px-5 py-2 text-[11px] text-gray-600 hover:text-gray-400 hover:bg-[#1a1a2a] transition-colors"
161
- aria-label="Open command palette"
162
- >
163
- <span className="flex items-center gap-2">
164
- <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
165
- <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
166
- </svg>
167
- Quick search
168
- </span>
169
- <kbd className="text-[10px] border border-current rounded px-1 py-0.5 opacity-60 font-mono">⌘K</kbd>
170
- </button>
171
- )}
172
- <div className={`flex items-center gap-2 px-3 py-2 ${collapsed ? 'justify-center' : 'justify-between'}`}>
156
+ {!collapsed && 'Quick search'}
157
+ </span>
158
+ {!collapsed && <kbd className="text-[10px] border border-current rounded px-1 py-0.5 opacity-60 font-mono">⌘K</kbd>}
159
+ </button>
160
+ <button
161
+ onClick={async () => {
162
+ await fetch('/api/auth/logout', { method: 'POST' }).catch(() => {});
163
+ sessionStorage.removeItem('anet_v3_auth');
164
+ window.location.assign('/login');
165
+ }}
166
+ title={collapsed ? 'Sign out' : undefined}
167
+ className={`w-full flex items-center text-xs text-gray-500 hover:text-red-400 hover:bg-red-500/5 transition-colors ${
168
+ collapsed ? 'justify-center px-0 py-2.5' : 'gap-3 px-5 py-3'
169
+ }`}
170
+ >
171
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
172
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
173
+ </svg>
174
+ {!collapsed && 'Sign out'}
175
+ </button>
176
+ <div className={`flex items-center gap-2 ${collapsed ? 'flex-col px-0 py-2' : 'px-3 py-2 justify-between'}`}>
173
177
  <ThemeSwitcher compact={collapsed} />
174
178
  <button
175
179
  onClick={() => setCollapsed(!collapsed)}
180
+ title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
176
181
  className="hidden lg:flex p-1.5 rounded text-gray-600 hover:text-gray-400 transition-colors"
177
182
  aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
178
183
  >
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useRef, useCallback } from 'react';
4
4
  import { timeAgo } from './utils';
5
+ import { AliasAvatar } from './AliasAvatar';
5
6
 
6
7
  interface ChatTask {
7
8
  task_id: string;
@@ -386,7 +387,10 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
386
387
  {m.result && (
387
388
  <div className="flex justify-start">
388
389
  <div className="max-w-[85%] bg-green-500/8 border border-green-500/15 rounded-2xl rounded-bl-md px-4 py-2.5 shadow-sm">
389
- <div className="text-[10px] text-green-400 font-medium mb-1.5">{m.to_name}</div>
390
+ <div className="flex items-center gap-1.5 mb-1.5">
391
+ {m.to_name && <AliasAvatar alias={m.to_name} size={14} />}
392
+ <span className="text-[10px] text-gray-200 font-medium">{m.to_name}</span>
393
+ </div>
390
394
  <div className="text-[13px] text-gray-200">
391
395
  <MarkdownContent text={m.result} />
392
396
  </div>
@@ -422,7 +426,7 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
422
426
  {filteredMentions.map(node => (
423
427
  <button key={node} onClick={() => insertMention(node)}
424
428
  className="w-full flex items-center gap-2 px-3 py-2 text-xs text-left hover:bg-cyan-500/10 transition-colors">
425
- <span className="w-2 h-2 rounded-full bg-cyan-400 shrink-0" />
429
+ <AliasAvatar alias={node} size={16} />
426
430
  <span className="text-gray-300">{node}</span>
427
431
  </button>
428
432
  ))}
@@ -471,14 +475,12 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
471
475
 
472
476
  return (
473
477
  <>
474
- <div className="fixed inset-0 bg-black/30 z-40 lg:hidden" onClick={onClose} />
478
+ <div className="fixed inset-0 bg-black/30 z-40 lg:hidden anet-fade-in" onClick={onClose} />
475
479
  <div className="fixed top-0 right-0 h-full w-full lg:w-[500px] bg-[#0a0a1a] border-l border-[#2a2a4a] z-50 flex flex-col shadow-2xl shadow-black/60 animate-slide-in">
476
480
  {/* Header */}
477
481
  <div className="flex items-center justify-between px-4 py-3 border-b border-[#2a2a4a] bg-[#0d0d1a]">
478
482
  <div className="flex items-center gap-3">
479
- <div className="w-8 h-8 rounded-full bg-gradient-to-br from-cyan-500/30 to-blue-500/30 flex items-center justify-center text-xs font-bold text-cyan-300">
480
- {alias.slice(0, 2)}
481
- </div>
483
+ <AliasAvatar alias={alias} size={32} />
482
484
  <div>
483
485
  <div className="text-sm font-semibold text-white">{alias}</div>
484
486
  <div className="text-[10px] text-gray-500">{pollingIds.size > 0 ? 'Processing...' : 'Ready'}</div>
@@ -492,10 +494,6 @@ export function TaskChatPanel({ alias, onClose, inline, availableNodes }: TaskCh
492
494
  </div>
493
495
  {chatContent}
494
496
  </div>
495
- <style jsx global>{`
496
- @keyframes slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
497
- .animate-slide-in { animation: slide-in 0.2s ease-out; }
498
- `}</style>
499
497
  </>
500
498
  );
501
499
  }
@@ -58,12 +58,20 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
58
58
  })();
59
59
  }, [taskId]);
60
60
 
61
+ /** Timeline steps — same 4 hops every task goes through. `time` is set
62
+ * when the hop has happened; missing time means pending. Round 36 adds
63
+ * `key` for the "current step" highlight + the `done` flag for layout. */
61
64
  const timeline = task ? [
62
- { label: 'Created', time: task.created_at, color: 'bg-gray-400' },
63
- { label: 'Delivered', time: task.delivered_at, color: 'bg-blue-400' },
64
- { label: 'Started', time: task.started_at, color: 'bg-green-400' },
65
- { label: 'Completed', time: task.completed_at, color: 'bg-purple-400' },
66
- ] : [];
65
+ { key: 'created', label: 'Created', time: task.created_at, color: 'bg-gray-400' },
66
+ { key: 'delivered', label: 'Delivered', time: task.delivered_at, color: 'bg-blue-400' },
67
+ { key: 'started', label: 'Started', time: task.started_at, color: 'bg-green-400' },
68
+ { key: 'completed', label: 'Completed', time: task.completed_at, color: 'bg-purple-400'},
69
+ ].map(s => ({ ...s, done: !!s.time })) : [];
70
+
71
+ /** Index of the highest-completed step. The step *after* this index is
72
+ * the "current" one (in-progress). When all are done, currentIdx = -1. */
73
+ const currentStepIdx = timeline.findIndex(s => !s.done);
74
+ const isActive = task && task.status !== 'completed' && task.status !== 'failed' && task.status !== 'expired' && task.status !== 'cancelled';
67
75
 
68
76
  const duration = task?.started_at && task?.completed_at
69
77
  ? Math.round((new Date(task.completed_at).getTime() - new Date(task.started_at).getTime()) / 1000)
@@ -71,7 +79,7 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
71
79
 
72
80
  return (
73
81
  <>
74
- <div className="fixed inset-0 bg-black/30 z-40" onClick={onClose} />
82
+ <div className="fixed inset-0 bg-black/30 z-40 anet-fade-in" onClick={onClose} />
75
83
  <div className="fixed top-0 right-0 h-full w-full lg:w-[500px] bg-[#0a0a1a] border-l border-[#2a2a4a] z-50 flex flex-col shadow-2xl shadow-black/60 overflow-y-auto animate-slide-in">
76
84
  {/* Header */}
77
85
  <div className="flex items-center justify-between px-5 py-4 border-b border-[#2a2a4a] bg-[#0d0d1a] sticky top-0">
@@ -112,22 +120,57 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
112
120
  <span className="text-cyan-400 font-medium">{task.to_name || '--'}</span>
113
121
  </div>
114
122
 
115
- {/* Timeline */}
123
+ {/* Timeline — round 36 polish: relative timestamps, current-step
124
+ pulse if task is still in flight, full ISO in title=. */}
116
125
  <div className="bg-[#111128] border border-[#2a2a4a] rounded-xl p-4">
117
- <div className="text-xs text-gray-500 uppercase mb-3">Timeline</div>
126
+ <div className="flex items-center justify-between mb-3">
127
+ <div className="text-xs text-gray-500 uppercase tracking-wide">Timeline</div>
128
+ {duration !== null && (
129
+ <div className="text-[10px] text-gray-600">
130
+ {duration < 60 ? `${duration}s` : `${Math.floor(duration/60)}m ${duration%60}s`} runtime
131
+ </div>
132
+ )}
133
+ </div>
118
134
  <div className="space-y-3">
119
- {timeline.map((step, i) => (
120
- <div key={step.label} className="flex items-center gap-3">
121
- <div className="flex flex-col items-center">
122
- <div className={`w-3 h-3 rounded-full ${step.time ? step.color : 'bg-gray-700'}`} />
123
- {i < timeline.length - 1 && <div className={`w-0.5 h-4 mt-1 ${step.time && timeline[i+1]?.time ? 'bg-gray-600' : 'bg-gray-800'}`} />}
135
+ {timeline.map((step, i) => {
136
+ const isCurrent = isActive && i === currentStepIdx;
137
+ const isNextDone = !!timeline[i+1]?.done;
138
+ return (
139
+ <div key={step.key} className="flex items-start gap-3">
140
+ <div className="flex flex-col items-center pt-0.5">
141
+ <span
142
+ className={`relative w-3 h-3 rounded-full shrink-0 ${
143
+ step.done ? step.color : 'bg-gray-700'
144
+ }`}
145
+ >
146
+ {isCurrent && (
147
+ <span
148
+ aria-hidden
149
+ className="absolute -inset-1 rounded-full border border-current opacity-60 anet-current-step-pulse"
150
+ style={{ borderColor: 'currentColor' }}
151
+ />
152
+ )}
153
+ </span>
154
+ {i < timeline.length - 1 && (
155
+ <div className={`w-0.5 h-5 mt-1 ${
156
+ step.done && isNextDone ? 'bg-gray-500' : step.done ? 'bg-gradient-to-b from-gray-500 to-gray-800' : 'bg-gray-800'
157
+ }`} />
158
+ )}
159
+ </div>
160
+ <div className="flex-1 min-w-0">
161
+ <div className={`text-xs font-medium flex items-center gap-2 ${
162
+ step.done ? 'text-gray-200' : isCurrent ? 'text-cyan-300' : 'text-gray-600'
163
+ }`}>
164
+ <span>{step.label}</span>
165
+ {isCurrent && <span className="text-[9px] uppercase tracking-wide text-cyan-400">in&nbsp;progress</span>}
166
+ </div>
167
+ <div className="text-[10px] text-gray-500" title={step.time || undefined}>
168
+ {step.time ? timeAgo(step.time) : isCurrent ? '—' : 'Pending'}
169
+ </div>
170
+ </div>
124
171
  </div>
125
- <div className="flex-1">
126
- <div className={`text-xs font-medium ${step.time ? 'text-gray-300' : 'text-gray-600'}`}>{step.label}</div>
127
- <div className="text-[10px] text-gray-500">{step.time || 'Pending'}</div>
128
- </div>
129
- </div>
130
- ))}
172
+ );
173
+ })}
131
174
  </div>
132
175
  </div>
133
176
 
@@ -172,10 +215,6 @@ export function TaskDrawer({ taskId, onClose }: TaskDrawerProps) {
172
215
  </div>
173
216
  )}
174
217
  </div>
175
- <style jsx global>{`
176
- @keyframes slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
177
- .animate-slide-in { animation: slide-in 0.2s ease-out; }
178
- `}</style>
179
218
  </>
180
219
  );
181
220
  }
@@ -33,10 +33,14 @@ const cy = 330;
33
33
  const onlineRadius = 220;
34
34
  const offlineRadius = 315;
35
35
 
36
- function polarPoint(index: number, total: number, radius: number) {
36
+ /** Polar coordinate for a node on a ring. `rotateBy` lets the caller offset
37
+ * the whole ring so two stacked rings don't align radially (round 25: the
38
+ * offline ring gets a half-step rotation so its nodes sit in the gaps
39
+ * between online ones). */
40
+ function polarPoint(index: number, total: number, radius: number, rotateBy = 0) {
37
41
  const spread = total <= 2 ? Math.PI : Math.PI * 1.78;
38
42
  const start = -Math.PI / 2 - spread / 2;
39
- const angle = total <= 1 ? -Math.PI / 2 : start + (spread * index) / (total - 1);
43
+ const angle = total <= 1 ? -Math.PI / 2 : start + (spread * index) / (total - 1) + rotateBy;
40
44
  return {
41
45
  x: cx + radius * Math.cos(angle),
42
46
  y: cy + radius * Math.sin(angle),
@@ -248,8 +252,17 @@ export function TopoGraph({ sessions, sseSessions }: TopoGraphProps) {
248
252
  positions[s.alias] = polarPoint(index, Math.max(online.length, 1), onlineRadius);
249
253
  });
250
254
 
255
+ // Offset the offline ring radially by half the online step so offline
256
+ // bubbles sit in the angular gaps between online bubbles instead of
257
+ // stacking directly behind them. Also push the outer ring further when
258
+ // there are many offline nodes so labels don't crowd the legend.
259
+ const spread = online.length <= 2 ? Math.PI : Math.PI * 1.78;
260
+ const onlineStep = online.length > 1 ? spread / (online.length - 1) : 0;
261
+ const offlineRotation = online.length > 0 ? onlineStep / 2 : 0;
262
+ const offlineR = offlineRadius + Math.max(0, offline.length - 4) * 6;
263
+
251
264
  offline.forEach((s, index) => {
252
- positions[s.alias] = polarPoint(index, Math.max(offline.length, 1), offlineRadius);
265
+ positions[s.alias] = polarPoint(index, Math.max(offline.length, 1), offlineR, offlineRotation);
253
266
  });
254
267
 
255
268
  const links = buildFlowLinks(messages, positions);