@polderlabs/bizar 6.3.0 → 7.0.1

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 (264) hide show
  1. package/.claude/hooks/sessionend-recall.mjs +3 -3
  2. package/.claude/hooks/worker-suggest.mjs +110 -0
  3. package/.claude/settings.json +9 -0
  4. package/README.md +125 -47
  5. package/bizar-dash/dist/assets/{EnvVarsSection-B58aiJiE.js → EnvVarsSection-weVnLz3Z.js} +3 -3
  6. package/bizar-dash/dist/assets/EnvVarsSection-weVnLz3Z.js.map +1 -0
  7. package/bizar-dash/dist/assets/MobileChat-BRFjer8p.js +1 -0
  8. package/bizar-dash/dist/assets/{MobileChat-BJrqwVDd.js.map → MobileChat-BRFjer8p.js.map} +1 -1
  9. package/bizar-dash/dist/assets/MobileSettings-Dw5jyJWi.js +1 -0
  10. package/bizar-dash/dist/assets/MobileSettings-Dw5jyJWi.js.map +1 -0
  11. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js → Toast-WTxc3m2g.js} +1 -1
  12. package/bizar-dash/dist/assets/{Toast-Cpl6-E63.js.map → Toast-WTxc3m2g.js.map} +1 -1
  13. package/bizar-dash/dist/assets/flow-BpU0-gK2.js +1 -0
  14. package/bizar-dash/dist/assets/flow-BpU0-gK2.js.map +1 -0
  15. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js +1 -0
  16. package/bizar-dash/dist/assets/fuzzy-l0sNRNKZ.js.map +1 -0
  17. package/bizar-dash/dist/assets/{icons-Do5N2jmX.js → icons-B0AKnYI3.js} +98 -103
  18. package/bizar-dash/dist/assets/icons-B0AKnYI3.js.map +1 -0
  19. package/bizar-dash/dist/assets/main-BE4Uh9im.css +1 -0
  20. package/bizar-dash/dist/assets/main-DqC58FEZ.js +18 -0
  21. package/bizar-dash/dist/assets/main-DqC58FEZ.js.map +1 -0
  22. package/bizar-dash/dist/assets/markdown-w2bDDkAH.js +1 -0
  23. package/bizar-dash/dist/assets/markdown-w2bDDkAH.js.map +1 -0
  24. package/bizar-dash/dist/assets/mobile-DRZsK3ic.js +1 -0
  25. package/bizar-dash/dist/assets/{mobile-DYCHcUpq.js.map → mobile-DRZsK3ic.js.map} +1 -1
  26. package/bizar-dash/dist/assets/mobile-layout-BWLtwtfQ.css +1 -0
  27. package/bizar-dash/dist/assets/mobile-layout-Ckru3HKJ.js +2 -0
  28. package/bizar-dash/dist/assets/mobile-layout-Ckru3HKJ.js.map +1 -0
  29. package/bizar-dash/dist/assets/react-vendor-BnuhLJ6X.css +1 -0
  30. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js +46 -0
  31. package/bizar-dash/dist/assets/react-vendor-D3pra6w-.js.map +1 -0
  32. package/bizar-dash/dist/assets/useSlashCommands-BAxVOU_a.js +2 -0
  33. package/bizar-dash/dist/assets/useSlashCommands-BAxVOU_a.js.map +1 -0
  34. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js +45 -0
  35. package/bizar-dash/dist/assets/vendor-Di7Cp-rw.js.map +1 -0
  36. package/bizar-dash/dist/index.html +13 -11
  37. package/bizar-dash/dist/mobile.html +8 -5
  38. package/bizar-dash/src/server/api.mjs +11 -0
  39. package/bizar-dash/src/server/memory-consolidator.mjs +225 -0
  40. package/bizar-dash/src/server/memory-distillation.mjs +296 -0
  41. package/bizar-dash/src/server/routes/distill.mjs +129 -0
  42. package/bizar-dash/src/server/routes/goal-planner.mjs +236 -0
  43. package/bizar-dash/src/web/App.tsx +4 -0
  44. package/bizar-dash/src/web/components/Tag.tsx +18 -5
  45. package/bizar-dash/src/web/components/Topbar.tsx +3 -0
  46. package/bizar-dash/src/web/components/agents/CommunicationLog.tsx +126 -0
  47. package/bizar-dash/src/web/components/agents/DependencyGraph.tsx +163 -0
  48. package/bizar-dash/src/web/components/agents/QualityGates.tsx +123 -0
  49. package/bizar-dash/src/web/components/agents/RealTimeEventLog.tsx +127 -0
  50. package/bizar-dash/src/web/components/agents/RoutingDecisions.test.tsx +148 -0
  51. package/bizar-dash/src/web/components/agents/RoutingDecisions.tsx +160 -0
  52. package/bizar-dash/src/web/components/goals/GoalInput.tsx +86 -0
  53. package/bizar-dash/src/web/components/goals/PlanVisualization.tsx +111 -0
  54. package/bizar-dash/src/web/lib/goapPlanner.ts +614 -0
  55. package/bizar-dash/src/web/lib/types.ts +25 -0
  56. package/bizar-dash/src/web/mobile/MobileApp.tsx +33 -2
  57. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +2 -2
  58. package/bizar-dash/src/web/mobile/MobileEval.tsx +4 -4
  59. package/bizar-dash/src/web/mobile/MobileMemory.tsx +2 -2
  60. package/bizar-dash/src/web/mobile/MobileSettings.tsx +3 -3
  61. package/bizar-dash/src/web/mobile/MobileTasks.tsx +2 -2
  62. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +3 -3
  63. package/bizar-dash/src/web/styles/agents-redesign.css +311 -0
  64. package/bizar-dash/src/web/styles/main.css +251 -0
  65. package/bizar-dash/src/web/styles/mobile.css +41 -0
  66. package/bizar-dash/src/web/styles/tasks-redesign.css +253 -0
  67. package/bizar-dash/src/web/ui/controls/Button.tsx +77 -0
  68. package/bizar-dash/src/web/ui/controls/Checkbox.tsx +95 -0
  69. package/bizar-dash/src/web/ui/controls/IconButton.tsx +66 -0
  70. package/bizar-dash/src/web/ui/controls/Kbd.tsx +24 -0
  71. package/bizar-dash/src/web/ui/controls/NumberInput.tsx +77 -0
  72. package/bizar-dash/src/web/ui/controls/RadioGroup.tsx +109 -0
  73. package/bizar-dash/src/web/ui/controls/SearchInput.tsx +91 -0
  74. package/bizar-dash/src/web/ui/controls/Select.tsx +92 -0
  75. package/bizar-dash/src/web/ui/controls/Slider.tsx +70 -0
  76. package/bizar-dash/src/web/ui/controls/TextInput.tsx +67 -0
  77. package/bizar-dash/src/web/ui/controls/Textarea.tsx +56 -0
  78. package/bizar-dash/src/web/ui/controls/Toggle.tsx +79 -0
  79. package/bizar-dash/src/web/ui/controls/controls.css +476 -0
  80. package/bizar-dash/src/web/ui/controls/index.ts +35 -0
  81. package/bizar-dash/src/web/ui/data/BarChart.tsx +60 -0
  82. package/bizar-dash/src/web/ui/data/DataTable.tsx +216 -0
  83. package/bizar-dash/src/web/ui/data/EmptyState.tsx +60 -0
  84. package/bizar-dash/src/web/ui/data/ErrorState.tsx +56 -0
  85. package/bizar-dash/src/web/ui/data/KeyValueList.tsx +106 -0
  86. package/bizar-dash/src/web/ui/data/LoadingState.tsx +65 -0
  87. package/bizar-dash/src/web/ui/data/Sparkline.tsx +87 -0
  88. package/bizar-dash/src/web/ui/data/StatTile.tsx +87 -0
  89. package/bizar-dash/src/web/ui/data/data.css +535 -0
  90. package/bizar-dash/src/web/ui/data/index.ts +31 -0
  91. package/bizar-dash/src/web/ui/feedback/Badge.tsx +49 -0
  92. package/bizar-dash/src/web/ui/feedback/Dialog.tsx +209 -0
  93. package/bizar-dash/src/web/ui/feedback/ProgressBar.tsx +69 -0
  94. package/bizar-dash/src/web/ui/feedback/StatusDot.tsx +50 -0
  95. package/bizar-dash/src/web/ui/feedback/Toast.tsx +200 -0
  96. package/bizar-dash/src/web/ui/feedback/Tooltip.tsx +126 -0
  97. package/bizar-dash/src/web/ui/feedback/feedback.css +314 -0
  98. package/bizar-dash/src/web/ui/feedback/index.ts +43 -0
  99. package/bizar-dash/src/web/ui/index.ts +56 -0
  100. package/bizar-dash/src/web/ui/layout/AppShell.tsx +51 -0
  101. package/bizar-dash/src/web/ui/layout/Breadcrumbs.tsx +97 -0
  102. package/bizar-dash/src/web/ui/layout/Panel.tsx +73 -0
  103. package/bizar-dash/src/web/ui/layout/PanelHeader.tsx +39 -0
  104. package/bizar-dash/src/web/ui/layout/Sidebar.tsx +131 -0
  105. package/bizar-dash/src/web/ui/layout/Tabs.tsx +133 -0
  106. package/bizar-dash/src/web/ui/layout/Topbar.tsx +42 -0
  107. package/bizar-dash/src/web/ui/layout/ViewHeader.tsx +48 -0
  108. package/bizar-dash/src/web/ui/layout/index.ts +34 -0
  109. package/bizar-dash/src/web/ui/layout/layout.css +464 -0
  110. package/bizar-dash/src/web/ui/navigation/CommandPalette.tsx +199 -0
  111. package/bizar-dash/src/web/ui/navigation/NavGroup.tsx +95 -0
  112. package/bizar-dash/src/web/ui/navigation/NavLink.tsx +93 -0
  113. package/bizar-dash/src/web/ui/navigation/index.ts +17 -0
  114. package/bizar-dash/src/web/ui/navigation/navigation.css +343 -0
  115. package/bizar-dash/src/web/ui/primitives/Box.tsx +91 -0
  116. package/bizar-dash/src/web/ui/primitives/Grid.tsx +49 -0
  117. package/bizar-dash/src/web/ui/primitives/Inline.tsx +55 -0
  118. package/bizar-dash/src/web/ui/primitives/Separator.tsx +46 -0
  119. package/bizar-dash/src/web/ui/primitives/Stack.tsx +54 -0
  120. package/bizar-dash/src/web/ui/primitives/VisuallyHidden.tsx +26 -0
  121. package/bizar-dash/src/web/ui/primitives/index.ts +23 -0
  122. package/bizar-dash/src/web/ui/primitives/primitives.css +183 -0
  123. package/bizar-dash/src/web/ui/styles/globals.css +86 -0
  124. package/bizar-dash/src/web/ui/styles/reset.css +125 -0
  125. package/bizar-dash/src/web/ui/styles/tokens.css +208 -0
  126. package/bizar-dash/src/web/ui/theme/ThemeProvider.tsx +142 -0
  127. package/bizar-dash/src/web/ui/theme/tokens.ts +131 -0
  128. package/bizar-dash/src/web/ui/theme/useTheme.ts +22 -0
  129. package/bizar-dash/src/web/ui/utils/cx.ts +42 -0
  130. package/bizar-dash/src/web/views/Agents.tsx +516 -268
  131. package/bizar-dash/src/web/views/GoalPlanner.tsx +158 -0
  132. package/bizar-dash/src/web/views/Overview.tsx +985 -448
  133. package/bizar-dash/src/web/views/Tasks.tsx +271 -273
  134. package/bizar-dash/tests/communication-log.test.tsx +67 -0
  135. package/bizar-dash/tests/dep-graph.test.tsx +40 -0
  136. package/bizar-dash/tests/event-log.test.tsx +61 -0
  137. package/bizar-dash/tests/goal-input.test.tsx +72 -0
  138. package/bizar-dash/tests/goap-planner.test.ts +68 -0
  139. package/bizar-dash/tests/quality-gates.test.tsx +59 -0
  140. package/bizar-dash/tests/setup.ts +31 -1
  141. package/bizar-dash/tests/ui/controls/Button.test.tsx +50 -0
  142. package/bizar-dash/tests/ui/controls/Checkbox.test.tsx +44 -0
  143. package/bizar-dash/tests/ui/controls/IconButton.test.tsx +37 -0
  144. package/bizar-dash/tests/ui/controls/Kbd.test.tsx +39 -0
  145. package/bizar-dash/tests/ui/controls/NumberInput.test.tsx +56 -0
  146. package/bizar-dash/tests/ui/controls/RadioGroup.test.tsx +58 -0
  147. package/bizar-dash/tests/ui/controls/SearchInput.test.tsx +52 -0
  148. package/bizar-dash/tests/ui/controls/Select.test.tsx +66 -0
  149. package/bizar-dash/tests/ui/controls/Slider.test.tsx +41 -0
  150. package/bizar-dash/tests/ui/controls/TextInput.test.tsx +44 -0
  151. package/bizar-dash/tests/ui/controls/Textarea.test.tsx +56 -0
  152. package/bizar-dash/tests/ui/controls/Toggle.test.tsx +39 -0
  153. package/bizar-dash/tests/ui/data/BarChart.test.tsx +55 -0
  154. package/bizar-dash/tests/ui/data/DataTable.test.tsx +112 -0
  155. package/bizar-dash/tests/ui/data/EmptyState.test.tsx +57 -0
  156. package/bizar-dash/tests/ui/data/ErrorState.test.tsx +47 -0
  157. package/bizar-dash/tests/ui/data/KeyValueList.test.tsx +59 -0
  158. package/bizar-dash/tests/ui/data/LoadingState.test.tsx +37 -0
  159. package/bizar-dash/tests/ui/data/Sparkline.test.tsx +56 -0
  160. package/bizar-dash/tests/ui/data/StatTile.test.tsx +69 -0
  161. package/bizar-dash/tests/ui/feedback/Badge.test.tsx +41 -0
  162. package/bizar-dash/tests/ui/feedback/Dialog.test.tsx +99 -0
  163. package/bizar-dash/tests/ui/feedback/ProgressBar.test.tsx +66 -0
  164. package/bizar-dash/tests/ui/feedback/StatusDot.test.tsx +41 -0
  165. package/bizar-dash/tests/ui/feedback/Toast.test.tsx +124 -0
  166. package/bizar-dash/tests/ui/feedback/Tooltip.test.tsx +102 -0
  167. package/bizar-dash/tests/ui/layout/AppShell.test.tsx +50 -0
  168. package/bizar-dash/tests/ui/layout/Breadcrumbs.test.tsx +85 -0
  169. package/bizar-dash/tests/ui/layout/Panel.test.tsx +52 -0
  170. package/bizar-dash/tests/ui/layout/PanelHeader.test.tsx +32 -0
  171. package/bizar-dash/tests/ui/layout/Sidebar.test.tsx +95 -0
  172. package/bizar-dash/tests/ui/layout/Tabs.test.tsx +87 -0
  173. package/bizar-dash/tests/ui/layout/Topbar.test.tsx +41 -0
  174. package/bizar-dash/tests/ui/layout/ViewHeader.test.tsx +47 -0
  175. package/bizar-dash/tests/ui/navigation/CommandPalette.test.tsx +105 -0
  176. package/bizar-dash/tests/ui/navigation/NavGroup.test.tsx +55 -0
  177. package/bizar-dash/tests/ui/navigation/NavLink.test.tsx +61 -0
  178. package/bizar-dash/tests/ui/primitives/Box.test.tsx +49 -0
  179. package/bizar-dash/tests/ui/primitives/Grid.test.tsx +37 -0
  180. package/bizar-dash/tests/ui/primitives/Inline.test.tsx +33 -0
  181. package/bizar-dash/tests/ui/primitives/Separator.test.tsx +34 -0
  182. package/bizar-dash/tests/ui/primitives/Stack.test.tsx +45 -0
  183. package/bizar-dash/tests/ui/primitives/VisuallyHidden.test.tsx +27 -0
  184. package/bizar-dash/tests/views/Agents.test.tsx +359 -0
  185. package/bizar-dash/tests/views/Overview.test.tsx +413 -0
  186. package/bizar-dash/tests/views/Tasks.test.tsx +250 -0
  187. package/bizar-dash/vitest.config.ts +3 -1
  188. package/cli/__tests__/cost-gate.test.mjs +301 -0
  189. package/cli/__tests__/feature-list-bridge.test.mjs +371 -0
  190. package/cli/bin.mjs +38 -0
  191. package/cli/commands/claim.mjs +282 -0
  192. package/cli/commands/cost.mjs +279 -0
  193. package/cli/cost-gate.mjs +501 -0
  194. package/cli/feature-list-bridge.mjs +400 -0
  195. package/cli/memory-constants.mjs +17 -0
  196. package/cli/provision-claude.mjs +94 -0
  197. package/cli/worker-dispatcher.mjs +259 -0
  198. package/cli/worker-dispatcher.test.mjs +204 -0
  199. package/config/trigger-patterns.json +186 -0
  200. package/package.json +2 -2
  201. package/packages/sdk/package.json +1 -1
  202. package/packages/sdk/src/agent-registry.ts +304 -0
  203. package/packages/sdk/src/consensus/byzantine.ts +587 -0
  204. package/packages/sdk/src/consensus/index.ts +147 -0
  205. package/packages/sdk/src/consensus/queen.ts +173 -0
  206. package/packages/sdk/src/consensus/types.ts +199 -0
  207. package/packages/sdk/src/federation/audit.ts +156 -0
  208. package/packages/sdk/src/federation/budget.ts +269 -0
  209. package/packages/sdk/src/federation/envelope.ts +144 -0
  210. package/packages/sdk/src/federation/hmac.ts +160 -0
  211. package/packages/sdk/src/federation/index.ts +443 -0
  212. package/packages/sdk/src/federation/pii.ts +241 -0
  213. package/packages/sdk/src/federation/policy.ts +112 -0
  214. package/packages/sdk/src/federation/trust.ts +146 -0
  215. package/packages/sdk/src/index.ts +67 -0
  216. package/packages/sdk/src/mcp/server.ts +351 -1
  217. package/packages/sdk/src/router/codemod-intent.ts +125 -0
  218. package/packages/sdk/src/router/index.ts +247 -0
  219. package/packages/sdk/src/router/memory-distillation-shim.mjs +18 -0
  220. package/packages/sdk/src/router/memory-distillation.ts +249 -0
  221. package/packages/sdk/src/router/model-router.ts +235 -0
  222. package/packages/sdk/src/router/q-learning-router.ts +304 -0
  223. package/packages/sdk/src/swarm-topology.ts +279 -0
  224. package/packages/sdk/tests/agent-registry.test.ts +285 -0
  225. package/packages/sdk/tests/consensus.test.ts +376 -0
  226. package/packages/sdk/tests/federation/audit.test.ts +164 -0
  227. package/packages/sdk/tests/federation/budget.test.ts +189 -0
  228. package/packages/sdk/tests/federation/envelope.test.ts +120 -0
  229. package/packages/sdk/tests/federation/hmac.test.ts +194 -0
  230. package/packages/sdk/tests/federation/orchestrator.test.ts +176 -0
  231. package/packages/sdk/tests/federation/pii.test.ts +106 -0
  232. package/packages/sdk/tests/federation/policy.test.ts +140 -0
  233. package/packages/sdk/tests/federation/trust.test.ts +124 -0
  234. package/packages/sdk/tests/mcp-tools.test.ts +204 -0
  235. package/packages/sdk/tests/memory-distillation.test.mjs +186 -0
  236. package/packages/sdk/tests/model-router.test.mjs +108 -0
  237. package/packages/sdk/tests/q-learning-router.test.mjs +100 -0
  238. package/packages/sdk/tests/router-orchestrator.test.mjs +172 -0
  239. package/packages/sdk/tests/router.test.mjs +91 -0
  240. package/packages/sdk/tests/sdk.test.mjs +26 -2
  241. package/packages/sdk/tests/swarm-topology.test.ts +258 -0
  242. package/scripts/bh-full-e2e.mjs +11 -15
  243. package/bizar-dash/dist/assets/EnvVarsSection-B58aiJiE.js.map +0 -1
  244. package/bizar-dash/dist/assets/MobileChat-BJrqwVDd.js +0 -1
  245. package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js +0 -1
  246. package/bizar-dash/dist/assets/MobileSettings-CEQNJNLJ.js.map +0 -1
  247. package/bizar-dash/dist/assets/icons-Do5N2jmX.js.map +0 -1
  248. package/bizar-dash/dist/assets/main-IvfQAOfy.js +0 -18
  249. package/bizar-dash/dist/assets/main-IvfQAOfy.js.map +0 -1
  250. package/bizar-dash/dist/assets/main-ietCEg_R.css +0 -1
  251. package/bizar-dash/dist/assets/markdown-tOLaD6nm.js +0 -1
  252. package/bizar-dash/dist/assets/markdown-tOLaD6nm.js.map +0 -1
  253. package/bizar-dash/dist/assets/mobile-DYCHcUpq.js +0 -1
  254. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js +0 -2
  255. package/bizar-dash/dist/assets/mobile-layout-CBHjpwsb.js.map +0 -1
  256. package/bizar-dash/dist/assets/mobile-layout-CJnZNLy3.css +0 -1
  257. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js +0 -40
  258. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +0 -1
  259. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js +0 -2
  260. package/bizar-dash/dist/assets/useSlashCommands-Bd7_FA6U.js.map +0 -1
  261. package/bizar-dash/dist/assets/vendor-C843201K.js +0 -29
  262. package/bizar-dash/dist/assets/vendor-C843201K.js.map +0 -1
  263. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  264. package/packages/sdk/package-lock.json +0 -32
@@ -1,4 +1,13 @@
1
- // src/views/Agents.tsx — v3.1.0 agents with tags, categories, real-time status.
1
+ // src/views/Agents.tsx — Agent roster view (Wave 3 redesign).
2
+ //
3
+ // Wave 3 migrates the legacy card-grid agent roster to the Bizar design
4
+ // system: a split-pane layout with a sortable DataTable on the left and a
5
+ // detail Panel on the right that surfaces the selected agent's metadata,
6
+ // actions (Edit / Delete / Restart) and system prompt. Modals still use the
7
+ // legacy Modal component (the design-system Dialog will land in a later
8
+ // wave), and the public function signature, export name, API paths, and
9
+ // WS event subscriptions remain unchanged so callers and the server are
10
+ // not disturbed by this UI-only refactor.
2
11
  import { useEffect, useMemo, useState } from 'react';
3
12
  import {
4
13
  Bot,
@@ -8,29 +17,33 @@ import {
8
17
  Trash2,
9
18
  Play,
10
19
  Save,
11
- X,
12
20
  RotateCw,
13
- Activity,
14
- Tag as TagIcon,
15
- Folder,
16
- CheckCircle2,
17
- Clock,
18
- AlertTriangle,
19
- Circle,
20
- ChevronDown,
21
- ChevronRight,
21
+ CircleAlert,
22
22
  } from 'lucide-react';
23
23
  import { Button } from '../components/Button';
24
- import { Card, CardTitle } from '../components/Card';
25
- import { EmptyState } from '../components/EmptyState';
26
- import { Spinner } from '../components/Spinner';
27
- import { StatusBadge } from '../components/StatusBadge';
28
24
  import { useModal } from '../components/Modal';
29
25
  import { useToast } from '../components/Toast';
30
26
  import { api } from '../lib/api';
31
- import { cn, formatRelative, truncate } from '../lib/utils';
27
+ import { formatRelative, truncate } from '../lib/utils';
32
28
  import type { Agent, Settings, Snapshot } from '../lib/types';
33
29
 
30
+ // Design-system consumers — Wave 3 first real view to use them.
31
+ import { Stack, Inline, Box } from '../ui/primitives';
32
+ import {
33
+ DataTable,
34
+ KeyValueList,
35
+ EmptyState,
36
+ LoadingState,
37
+ type DataTableColumn,
38
+ type KeyValueItem,
39
+ } from '../ui/data';
40
+ import { Button as UIButton } from '../ui/controls';
41
+ import { Badge, StatusDot } from '../ui/feedback';
42
+ import { Panel } from '../ui/layout';
43
+ import { cx } from '../ui/utils/cx';
44
+
45
+ import '../styles/agents-redesign.css';
46
+
34
47
  type Props = {
35
48
  snapshot: Snapshot;
36
49
  settings: Settings;
@@ -49,36 +62,63 @@ const MODELS = [
49
62
  'openrouter/minimax/minimax-m2.7',
50
63
  ];
51
64
 
65
+ // Default agent swatch — the only place in the dashboard where a
66
+ // literal hex appears. HTML <input type="color"> requires CSS Color
67
+ // Module Level 3 hex format (no `oklch()`, no `var(--token)`). Keep
68
+ // this in sync with --default-agent-swatch in ui/styles/tokens.css;
69
+ // the token is what every CSS-side swatch reads, this is just the
70
+ // native color-input seed value.
71
+ const DEFAULT_AGENT_SWATCH_HEX = '#8b5cf6';
72
+
52
73
  const CATEGORIES = [
53
- { id: 'reasoning', label: 'Reasoning', color: 'var(--accent)' },
54
- { id: 'code', label: 'Code', color: 'var(--info)' },
55
- { id: 'design', label: 'Design', color: 'var(--success)' },
56
- { id: 'planning', label: 'Planning', color: 'var(--warning)' },
57
- { id: 'gitops', label: 'GitOps', color: 'var(--error)' },
58
- { id: 'analysis', label: 'Analysis', color: 'var(--text-dim)' },
74
+ { id: 'reasoning', label: 'Reasoning', variant: 'accent' as const },
75
+ { id: 'code', label: 'Code', variant: 'info' as const },
76
+ { id: 'design', label: 'Design', variant: 'success' as const },
77
+ { id: 'planning', label: 'Planning', variant: 'warning' as const },
78
+ { id: 'gitops', label: 'GitOps', variant: 'danger' as const },
79
+ { id: 'analysis', label: 'Analysis', variant: 'neutral' as const },
59
80
  ];
60
81
 
61
- function categoryColor(cat: string | undefined): string {
62
- return CATEGORIES.find((c) => c.id === cat)?.color || 'var(--text-dim)';
82
+ function categoryMeta(cat: string | undefined): { label: string; variant: 'neutral' | 'info' | 'success' | 'warning' | 'danger' | 'accent' } {
83
+ if (!cat) return { label: '—', variant: 'neutral' };
84
+ const found = CATEGORIES.find((c) => c.id === cat);
85
+ return found
86
+ ? { label: found.label, variant: found.variant }
87
+ : { label: cat, variant: 'neutral' };
63
88
  }
64
89
 
65
- function StatusDot({ status, isStuck }: { status?: string; isStuck?: boolean }) {
66
- const color =
67
- isStuck ? 'var(--error)'
68
- : status === 'working' ? 'var(--info)'
69
- : status === 'error' ? 'var(--error)'
70
- : 'var(--text-dim)';
71
- return <span className="agent-status-dot" style={{ background: color }} />;
90
+ type StatusKind = 'idle' | 'working' | 'error' | 'stuck';
91
+
92
+ function statusKind(agent: Agent): StatusKind {
93
+ if (agent.isStuck) return 'stuck';
94
+ const s = (agent.status || 'idle') as string;
95
+ if (s === 'working') return 'working';
96
+ if (s === 'error') return 'error';
97
+ return 'idle';
72
98
  }
73
99
 
74
- function StatusPill({ agent }: { agent: Agent }) {
75
- if (agent.isStuck) {
76
- return <StatusBadge kind="error" dot>stuck</StatusBadge>;
77
- }
78
- const status = agent.status || 'idle';
79
- if (status === 'working') return <StatusBadge kind="info" dot>working</StatusBadge>;
80
- if (status === 'error') return <StatusBadge kind="error" dot>error</StatusBadge>;
81
- return <StatusBadge kind="neutral" dot>idle</StatusBadge>;
100
+ function statusLabel(kind: StatusKind): string {
101
+ if (kind === 'stuck') return 'stuck';
102
+ return kind;
103
+ }
104
+
105
+ function statusDotVariant(kind: StatusKind): 'neutral' | 'info' | 'danger' {
106
+ if (kind === 'working') return 'info';
107
+ if (kind === 'error' || kind === 'stuck') return 'danger';
108
+ return 'neutral';
109
+ }
110
+
111
+ function lastSeenTs(agent: Agent): number | undefined {
112
+ // The Agent type exposes `lastSeen` (legacy) and a future `lastSeenAt`.
113
+ // Either is acceptable; missing fields render as "—".
114
+ const a = agent as unknown as { lastSeenAt?: number };
115
+ return a.lastSeenAt ?? agent.lastSeen;
116
+ }
117
+
118
+ function tagsToString(tags: string[] | undefined, max = 4): string {
119
+ if (!tags || tags.length === 0) return '—';
120
+ const shown = tags.slice(0, max).join(', ');
121
+ return tags.length > max ? `${shown} +${tags.length - max}` : shown;
82
122
  }
83
123
 
84
124
  export function Agents({ snapshot, refreshSnapshot }: Props) {
@@ -88,13 +128,22 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
88
128
  const [loading, setLoading] = useState(!snapshot.agents);
89
129
  const [categoryFilter, setCategoryFilter] = useState<string>('');
90
130
  const [search, setSearch] = useState('');
131
+ const [selectedName, setSelectedName] = useState<string | null>(null);
91
132
 
92
133
  useEffect(() => {
93
134
  setAgents(snapshot.agents || []);
94
135
  setLoading(!snapshot.agents);
95
136
  }, [snapshot.agents]);
96
137
 
97
- const reload = async () => {
138
+ // If the selected agent disappears from the list (deleted elsewhere, WS
139
+ // swap), clear the selection so the detail panel reverts to the placeholder.
140
+ useEffect(() => {
141
+ if (selectedName && !agents.some((a) => a.name === selectedName)) {
142
+ setSelectedName(null);
143
+ }
144
+ }, [agents, selectedName]);
145
+
146
+ const reload = async (): Promise<void> => {
98
147
  try {
99
148
  const d = await api.get<{ agents: Agent[] }>('/agents');
100
149
  setAgents(d.agents || []);
@@ -108,7 +157,11 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
108
157
  const sorted = useMemo(() => {
109
158
  let out = [...agents];
110
159
  if (categoryFilter) {
111
- out = out.filter((a) => categoryFilter === '__none__' ? !(a.category || '') : (a.category || '') === categoryFilter);
160
+ out = out.filter((a) =>
161
+ categoryFilter === '__none__'
162
+ ? !(a.category || '')
163
+ : (a.category || '') === categoryFilter,
164
+ );
112
165
  }
113
166
  if (search) {
114
167
  const q = search.toLowerCase();
@@ -122,13 +175,16 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
122
175
  return out.sort((a, b) => a.name.localeCompare(b.name));
123
176
  }, [agents, categoryFilter, search]);
124
177
 
125
- const allTags = useMemo(() => {
126
- const set = new Set<string>();
127
- for (const a of agents) for (const t of a.tags || []) set.add(t);
128
- return Array.from(set).sort();
129
- }, [agents]);
178
+ const hasLastSeen = sorted.some((a) => typeof lastSeenTs(a) === 'number');
130
179
 
131
- const onCreate = () => {
180
+ const selected = useMemo(
181
+ () => sorted.find((a) => a.name === selectedName) || null,
182
+ [sorted, selectedName],
183
+ );
184
+
185
+ // ─── Handlers (modal flows kept on the legacy Modal) ───────────────────
186
+
187
+ const onCreate = (): void => {
132
188
  let nameEl: HTMLInputElement | null = null;
133
189
  let descEl: HTMLInputElement | null = null;
134
190
  let modelEl: HTMLSelectElement | null = null;
@@ -181,7 +237,7 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
181
237
  </div>
182
238
  <div className="task-form-field" style={{ flex: '0 0 80px' }}>
183
239
  <label className="field-label" htmlFor="agent-new-color">Color</label>
184
- <input id="agent-new-color" ref={(el) => (colorEl = el)} className="input" type="color" defaultValue="#8b5cf6" />
240
+ <input id="agent-new-color" ref={(el) => (colorEl = el)} className="input" type="color" defaultValue={DEFAULT_AGENT_SWATCH_HEX} />
185
241
  </div>
186
242
  </div>
187
243
  <div className="task-form-row">
@@ -251,6 +307,7 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
251
307
  prompt: promptEl?.value || '',
252
308
  });
253
309
  setAgents((cur) => [...cur, created]);
310
+ setSelectedName(created.name);
254
311
  toast.success('Agent created.');
255
312
  modal.close();
256
313
  await refreshSnapshot();
@@ -266,7 +323,7 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
266
323
  });
267
324
  };
268
325
 
269
- const onEdit = async (a: Agent) => {
326
+ const onEdit = async (a: Agent): Promise<void> => {
270
327
  let descEl: HTMLInputElement | null = null;
271
328
  let modelEl: HTMLSelectElement | null = null;
272
329
  let modeEl: HTMLSelectElement | null = null;
@@ -313,7 +370,7 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
313
370
  </div>
314
371
  <div className="task-form-field" style={{ flex: '0 0 80px' }}>
315
372
  <label className="field-label" htmlFor="agent-edit-color">Color</label>
316
- <input id="agent-edit-color" ref={(el) => (colorEl = el)} className="input" type="color" defaultValue={full.color || '#8b5cf6'} />
373
+ <input id="agent-edit-color" ref={(el) => (colorEl = el)} className="input" type="color" defaultValue={full.color || DEFAULT_AGENT_SWATCH_HEX} />
317
374
  </div>
318
375
  </div>
319
376
  <div className="task-form-row">
@@ -345,7 +402,7 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
345
402
  <input type="checkbox" value={t} aria-label={t} defaultChecked={full.tools?.includes(t)} />
346
403
  <span>{t}</span>
347
404
  </label>
348
- ))}
405
+ ))}
349
406
  </div>
350
407
  </fieldset>
351
408
  <label className="field-label" htmlFor="agent-edit-prompt">System prompt</label>
@@ -404,7 +461,7 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
404
461
  }
405
462
  };
406
463
 
407
- const onDelete = async (a: Agent) => {
464
+ const onDelete = async (a: Agent): Promise<void> => {
408
465
  if (!confirm(`Delete agent "${a.name}"? This removes ${a.path}.`)) return;
409
466
  try {
410
467
  await api.del(`/agents/${encodeURIComponent(a.name)}`);
@@ -415,7 +472,7 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
415
472
  }
416
473
  };
417
474
 
418
- const onInvoke = async (a: Agent) => {
475
+ const onInvoke = (a: Agent): void => {
419
476
  let promptEl: HTMLTextAreaElement | null = null;
420
477
  modal.open({
421
478
  title: `Invoke ${a.name}`,
@@ -461,7 +518,7 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
461
518
  });
462
519
  };
463
520
 
464
- const onRestart = async (a: Agent) => {
521
+ const onRestart = async (a: Agent): Promise<void> => {
465
522
  try {
466
523
  const updated = await api.post<Agent>(`/agents/${encodeURIComponent(a.name)}/restart`);
467
524
  setAgents((cur) => cur.map((x) => (x.name === a.name ? updated : x)));
@@ -471,7 +528,7 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
471
528
  }
472
529
  };
473
530
 
474
- const onSetStatus = async (a: Agent, status: 'idle' | 'working' | 'error') => {
531
+ const onSetStatus = async (a: Agent, status: 'idle' | 'working' | 'error'): Promise<void> => {
475
532
  try {
476
533
  const updated = await api.post<Agent>(`/agents/${encodeURIComponent(a.name)}/status`, { status });
477
534
  setAgents((cur) => cur.map((x) => (x.name === a.name ? updated : x)));
@@ -480,237 +537,428 @@ export function Agents({ snapshot, refreshSnapshot }: Props) {
480
537
  }
481
538
  };
482
539
 
540
+ // ─── Table column definitions ──────────────────────────────────────────
541
+
542
+ type Col = DataTableColumn<Agent>;
543
+
544
+ const columns: Col[] = [
545
+ {
546
+ key: 'name',
547
+ header: 'Name',
548
+ sortable: true,
549
+ accessor: (a) => a.name,
550
+ render: (a) => (
551
+ <span className="agents-name-cell">
552
+ <span
553
+ className={cx(
554
+ 'agents-name-cell__swatch',
555
+ !a.color && 'agents-name-cell__swatch--empty',
556
+ )}
557
+ style={a.color ? { background: a.color } : undefined}
558
+ aria-hidden="true"
559
+ />
560
+ {a.name}
561
+ </span>
562
+ ),
563
+ },
564
+ {
565
+ key: 'status',
566
+ header: 'Status',
567
+ sortable: true,
568
+ accessor: (a) => statusKind(a),
569
+ render: (a) => {
570
+ const kind = statusKind(a);
571
+ return (
572
+ <span className="agents-status-cell">
573
+ <StatusDot
574
+ variant={statusDotVariant(kind)}
575
+ size="sm"
576
+ pulse={kind === 'stuck'}
577
+ label={statusLabel(kind)}
578
+ />
579
+ <span
580
+ className={cx(
581
+ 'agents-status-cell__label',
582
+ kind === 'stuck' && 'agents-status-cell__label--stuck',
583
+ )}
584
+ >
585
+ {statusLabel(kind)}
586
+ </span>
587
+ </span>
588
+ );
589
+ },
590
+ },
591
+ {
592
+ key: 'category',
593
+ header: 'Category',
594
+ sortable: true,
595
+ accessor: (a) => a.category || '',
596
+ render: (a) => {
597
+ const meta = categoryMeta(a.category);
598
+ if (!a.category) return <span className="muted">—</span>;
599
+ return (
600
+ <span className="agents-category-cell">
601
+ <Badge variant={meta.variant} size="sm">{meta.label}</Badge>
602
+ </span>
603
+ );
604
+ },
605
+ },
606
+ {
607
+ key: 'tags',
608
+ header: 'Tags',
609
+ accessor: (a) => (a.tags || []).join(','),
610
+ render: (a) => (
611
+ <span className="agents-tags-cell" title={(a.tags || []).join(', ')}>
612
+ {tagsToString(a.tags)}
613
+ </span>
614
+ ),
615
+ },
616
+ ];
617
+
618
+ if (hasLastSeen) {
619
+ columns.push({
620
+ key: 'lastSeen',
621
+ header: 'Last seen',
622
+ sortable: true,
623
+ accessor: (a) => lastSeenTs(a) || 0,
624
+ render: (a) => {
625
+ const ts = lastSeenTs(a);
626
+ return <span className="muted tabular-nums">{ts ? formatRelative(ts) : '—'}</span>;
627
+ },
628
+ });
629
+ }
630
+
631
+ // ─── Detail panel ─────────────────────────────────────────────────────
632
+
633
+ function renderDetailBody(): React.JSX.Element {
634
+ if (!selected) {
635
+ return (
636
+ <div className="agents-detail__placeholder">
637
+ <span className="agents-detail__placeholder-icon" aria-hidden="true">
638
+ <Bot size={28} />
639
+ </span>
640
+ <span className="agents-detail__placeholder-title">No agent selected</span>
641
+ <span>Pick a row from the table to see its metadata, actions, and prompt.</span>
642
+ </div>
643
+ );
644
+ }
645
+
646
+ const a = selected;
647
+ const kind = statusKind(a);
648
+ const catMeta = categoryMeta(a.category);
649
+ const canRestart = a.isStuck || a.status === 'working' || a.status === 'error';
650
+
651
+ const metaItems: KeyValueItem[] = [
652
+ {
653
+ key: 'status',
654
+ label: 'Status',
655
+ value: (
656
+ <span className="agents-status-cell">
657
+ <StatusDot
658
+ variant={statusDotVariant(kind)}
659
+ size="sm"
660
+ pulse={kind === 'stuck'}
661
+ label={statusLabel(kind)}
662
+ />
663
+ <span className="agents-status-cell__label">{statusLabel(kind)}</span>
664
+ </span>
665
+ ),
666
+ },
667
+ {
668
+ key: 'category',
669
+ label: 'Category',
670
+ value: a.category
671
+ ? <Badge variant={catMeta.variant} size="sm">{catMeta.label}</Badge>
672
+ : <span className="muted">—</span>,
673
+ },
674
+ {
675
+ key: 'tags',
676
+ label: 'Tags',
677
+ value: a.tags && a.tags.length > 0 ? (
678
+ <Inline gap={1} className="agents-detail__tags">
679
+ {a.tags.map((t) => (
680
+ <Badge key={t} variant="neutral" size="sm">{t}</Badge>
681
+ ))}
682
+ </Inline>
683
+ ) : <span className="muted">—</span>,
684
+ },
685
+ {
686
+ key: 'tools',
687
+ label: 'Tools',
688
+ value: a.tools && a.tools.length > 0 ? (
689
+ <Inline gap={1} className="agents-detail__tools">
690
+ {a.tools.map((t) => (
691
+ <Badge key={t} variant="info" size="sm">{t}</Badge>
692
+ ))}
693
+ </Inline>
694
+ ) : <span className="muted">—</span>,
695
+ },
696
+ {
697
+ key: 'model',
698
+ label: 'Model',
699
+ mono: true,
700
+ copyable: true,
701
+ value: a.model || '—',
702
+ },
703
+ {
704
+ key: 'mode',
705
+ label: 'Mode',
706
+ value: a.mode || 'subagent',
707
+ },
708
+ {
709
+ key: 'color',
710
+ label: 'Color',
711
+ value: a.color ? (
712
+ <Inline gap={2} align="center">
713
+ <span
714
+ className="agents-detail__name-swatch"
715
+ style={{ background: a.color }}
716
+ aria-hidden="true"
717
+ />
718
+ <code className="mono">{a.color}</code>
719
+ </Inline>
720
+ ) : <span className="muted">—</span>,
721
+ },
722
+ {
723
+ key: 'description',
724
+ label: 'Description',
725
+ value: a.description
726
+ ? <div className="agents-detail__description">{truncate(a.description, 600)}</div>
727
+ : <span className="muted">—</span>,
728
+ },
729
+ ];
730
+
731
+ if (typeof lastSeenTs(a) === 'number') {
732
+ metaItems.push({
733
+ key: 'lastSeen',
734
+ label: 'Last seen',
735
+ mono: true,
736
+ value: formatRelative(lastSeenTs(a) as number),
737
+ });
738
+ }
739
+
740
+ if (a.lastError?.message) {
741
+ metaItems.push({
742
+ key: 'lastError',
743
+ label: 'Last error',
744
+ value: (
745
+ <div className="agents-detail__error">{a.lastError.message}</div>
746
+ ),
747
+ });
748
+ }
749
+
750
+ return (
751
+ <Stack gap={4} className="agents-detail__meta">
752
+ <KeyValueList items={metaItems} orientation="horizontal" />
753
+
754
+ <Box className="agents-detail__prompt">
755
+ <span className="agents-detail__prompt-label">System prompt</span>
756
+ {a.prompt ? (
757
+ <pre className="agents-detail__prompt-body">{a.prompt}</pre>
758
+ ) : (
759
+ <span className="agents-detail__prompt-empty">No prompt configured.</span>
760
+ )}
761
+ </Box>
762
+
763
+ <Inline gap={2} wrap className="agents-detail__actions">
764
+ <UIButton
765
+ variant="secondary"
766
+ size="sm"
767
+ icon={<Pencil size={12} />}
768
+ onClick={() => onEdit(a)}
769
+ aria-label={`Edit ${a.name}`}
770
+ data-testid={`agents-detail-edit-${a.name}`}
771
+ >
772
+ Edit
773
+ </UIButton>
774
+ <UIButton
775
+ variant="secondary"
776
+ size="sm"
777
+ icon={<Play size={12} />}
778
+ onClick={() => onInvoke(a)}
779
+ aria-label={`Invoke ${a.name}`}
780
+ data-testid={`agents-detail-invoke-${a.name}`}
781
+ >
782
+ Invoke
783
+ </UIButton>
784
+ {canRestart && (
785
+ <UIButton
786
+ variant="ghost"
787
+ size="sm"
788
+ icon={<RotateCw size={12} />}
789
+ onClick={() => onRestart(a)}
790
+ aria-label={`Restart ${a.name}`}
791
+ data-testid={`agents-detail-restart-${a.name}`}
792
+ >
793
+ Restart
794
+ </UIButton>
795
+ )}
796
+ <UIButton
797
+ variant="danger"
798
+ size="sm"
799
+ icon={<Trash2 size={12} />}
800
+ onClick={() => onDelete(a)}
801
+ aria-label={`Delete ${a.name}`}
802
+ data-testid={`agents-detail-delete-${a.name}`}
803
+ >
804
+ Delete
805
+ </UIButton>
806
+ </Inline>
807
+
808
+ {a.isStuck && (
809
+ <Inline gap={2} align="center" className="agents-detail__error">
810
+ <CircleAlert size={12} />
811
+ <span>
812
+ Agent is stuck — restart to reset state.
813
+ </span>
814
+ </Inline>
815
+ )}
816
+ </Stack>
817
+ );
818
+ }
819
+
820
+ // ─── Render ────────────────────────────────────────────────────────────
821
+
822
+ const categoryOptions = [
823
+ { value: '', label: 'All categories' },
824
+ ...CATEGORIES.map((c) => ({ value: c.id, label: c.label })),
825
+ { value: '__none__', label: '(no category)' },
826
+ ];
827
+
483
828
  return (
484
- <div className="view view-agents">
485
- <header className="view-header">
486
- <div className="view-header-text">
487
- <h2 className="view-title">
488
- <Bot size={18} /> Agents ({sorted.length})
829
+ <div className="agents-redesign" data-testid="agents-view">
830
+ <header className="agents-redesign__header">
831
+ <div className="agents-redesign__header-text">
832
+ <h2 className="agents-redesign__title">
833
+ <Bot size={18} aria-hidden="true" />
834
+ <span>Agents</span>
835
+ <span className="agents-redesign__title-count" data-testid="agents-count">
836
+ ({sorted.length})
837
+ </span>
489
838
  </h2>
490
- <p className="view-subtitle">
491
- The Norse pantheon — click <kbd>Edit</kbd> to modify or <kbd>Invoke</kbd> to dispatch.
839
+ <p className="agents-redesign__subtitle">
840
+ The Norse pantheon — pick a row to inspect, <kbd>Edit</kbd> to modify, <kbd>Invoke</kbd> to dispatch.
492
841
  </p>
493
842
  </div>
494
- <div className="view-actions">
495
- <div className="search-input">
496
- <label htmlFor="agents-search" className="sr-only">Search agents</label>
843
+ <div className="agents-redesign__controls">
844
+ <span className="agents-redesign__control" style={{ minWidth: 180 }}>
845
+ <label htmlFor="agents-search" className="vh">Search agents</label>
497
846
  <input
498
847
  id="agents-search"
499
848
  className="input"
500
- type="text"
501
- placeholder="Search…"
849
+ type="search"
850
+ placeholder="Search name, description, tag…"
502
851
  value={search}
503
852
  onChange={(e) => setSearch(e.target.value)}
504
853
  aria-label="Search agents"
854
+ data-testid="agents-search"
505
855
  />
506
- </div>
507
- <label htmlFor="agents-category-filter" className="sr-only">Filter by category</label>
508
- <select
509
- id="agents-category-filter"
510
- className="select select-sm"
511
- value={categoryFilter}
512
- onChange={(e) => setCategoryFilter(e.target.value)}
856
+ </span>
857
+ <span className="agents-redesign__control" style={{ minWidth: 180 }}>
858
+ <label htmlFor="agents-category-filter" className="vh">Filter by category</label>
859
+ <select
860
+ id="agents-category-filter"
861
+ className="select"
862
+ value={categoryFilter}
863
+ onChange={(e) => setCategoryFilter(e.target.value)}
864
+ data-testid="agents-category-filter"
865
+ >
866
+ {categoryOptions.map((opt) => (
867
+ <option key={opt.value || '__all__'} value={opt.value}>{opt.label}</option>
868
+ ))}
869
+ </select>
870
+ </span>
871
+ <UIButton
872
+ variant="ghost"
873
+ size="sm"
874
+ icon={<RefreshCw size={12} />}
875
+ onClick={reload}
876
+ aria-label="Refresh"
877
+ data-testid="agents-refresh"
513
878
  >
514
- <option value="">All categories</option>
515
- {CATEGORIES.map((c) => (
516
- <option key={c.id} value={c.id}>{c.label}</option>
517
- ))}
518
- <option value="__none__">(no category)</option>
519
- </select>
520
- <Button variant="secondary" size="sm" onClick={reload}>
521
- <RefreshCw size={14} /> Refresh
522
- </Button>
523
- <Button variant="primary" size="sm" onClick={onCreate}>
524
- <Plus size={14} /> New agent
525
- </Button>
879
+ Refresh
880
+ </UIButton>
881
+ <UIButton
882
+ variant="primary"
883
+ size="sm"
884
+ icon={<Plus size={12} />}
885
+ onClick={onCreate}
886
+ aria-label="New agent"
887
+ data-testid="agents-new"
888
+ >
889
+ New agent
890
+ </UIButton>
526
891
  </div>
527
892
  </header>
528
893
 
529
- {allTags.length > 0 && (
530
- <div className="agent-tags-row">
531
- <TagIcon size={12} />
532
- {allTags.map((t) => (
533
- <span key={t} className="tag">{t}</span>
534
- ))}
535
- </div>
536
- )}
537
-
538
- {loading ? (
539
- <div className="view-loading"><Spinner size="lg" /></div>
540
- ) : sorted.length === 0 ? (
541
- <EmptyState
542
- icon={<Bot size={32} />}
543
- title="No agents found"
544
- message="Run bizar in the terminal to install Bizar."
545
- />
546
- ) : (
547
- <div className="agent-grid">
548
- {sorted.map((a) => (
549
- <AgentCard
550
- key={a.name}
551
- agent={a}
552
- onInvoke={() => onInvoke(a)}
553
- onEdit={() => onEdit(a)}
554
- onDelete={() => onDelete(a)}
555
- onRestart={() => onRestart(a)}
556
- onSetStatus={(s) => onSetStatus(a, s)}
894
+ <div className="agents-redesign__body">
895
+ <Box className="agents-roster">
896
+ {loading ? (
897
+ <LoadingState label="Loading agents…" rows={3} />
898
+ ) : sorted.length === 0 ? (
899
+ <EmptyState
900
+ icon={<Bot size={32} />}
901
+ title="No agents found"
902
+ description={
903
+ agents.length === 0
904
+ ? 'Run bizar in the terminal to install Bizar.'
905
+ : 'No agents match the current filters — try clearing the search or category.'
906
+ }
907
+ action={
908
+ agents.length === 0
909
+ ? undefined
910
+ : {
911
+ label: 'Clear filters',
912
+ onClick: () => {
913
+ setSearch('');
914
+ setCategoryFilter('');
915
+ },
916
+ }
917
+ }
557
918
  />
558
- ))}
559
- </div>
560
- )}
561
- </div>
562
- );
563
- }
564
-
565
- function AgentCard({
566
- agent,
567
- onInvoke,
568
- onEdit,
569
- onDelete,
570
- onRestart,
571
- onSetStatus,
572
- }: {
573
- agent: Agent;
574
- onInvoke: () => void;
575
- onEdit: () => void;
576
- onDelete: () => void;
577
- onRestart: () => void;
578
- onSetStatus: (s: 'idle' | 'working' | 'error') => void;
579
- }) {
580
- const [expanded, setExpanded] = useState(false);
581
- const catColor = categoryColor(agent.category);
582
- const isWorking = (agent.status === 'working' || !!agent.currentTaskId) && !agent.isStuck;
583
- return (
584
- <Card
585
- variant="elevated"
586
- interactive
587
- className={cn(
588
- 'agent-card',
589
- isWorking && 'is-working',
590
- agent.isStuck && 'is-stuck',
591
- )}
592
- >
593
- <div className="agent-card-head">
594
- <div className="agent-card-name">
595
- <StatusDot status={agent.status} isStuck={agent.isStuck} />
596
- {agent.name}
597
- </div>
598
- <div className="agent-card-badges">
599
- {agent.category && (
600
- <span
601
- className="agent-card-category"
602
- style={{ background: `color-mix(in srgb, ${catColor} 18%, transparent)`, color: catColor }}
603
- >
604
- {agent.category}
605
- </span>
606
- )}
607
- <StatusPill agent={agent} />
608
- </div>
609
- </div>
610
- <p className="agent-card-desc">{truncate(agent.description, 200)}</p>
611
- <div className="agent-card-meta">
612
- <span className="mono" title={agent.model || ''}>
613
- {agent.model || '—'}
614
- </span>
615
- <span className="tabular-nums muted">{formatRelative(agent.mtime)}</span>
616
- </div>
617
- {agent.tags && agent.tags.length > 0 && (
618
- <div className="agent-card-tags">
619
- {agent.tags.map((t) => (
620
- <span key={t} className="agent-card-tag">{t}</span>
621
- ))}
622
- </div>
623
- )}
624
- {(isWorking || agent.lastTask) && (
625
- <div className="agent-card-activity">
626
- {agent.currentTaskId && (
627
- <div className="agent-card-row">
628
- <Activity size={12} />
629
- <span className="muted">Working on</span>
630
- <code className="mono">{agent.currentTaskId}</code>
631
- </div>
632
- )}
633
- {agent.lastTask && !agent.currentTaskId && (
634
- <div className="agent-card-row">
635
- <CheckCircle2 size={12} />
636
- <span className="muted">Last</span>
637
- <code className="mono">{agent.lastTask.id}</code>
638
- <span className="muted tabular-nums">{formatRelative(agent.lastTask.finishedAt)}</span>
639
- </div>
640
- )}
641
- {agent.tasksTotal != null && agent.tasksTotal > 0 && (
642
- <div className="agent-card-row">
643
- <Circle size={12} />
644
- <span className="muted">Success rate</span>
645
- <span className="tabular-nums">{Math.round((agent.successRate || 0) * 100)}%</span>
646
- <span className="muted tabular-nums">({agent.tasksSucceeded}/{agent.tasksTotal})</span>
919
+ ) : (
920
+ <div className="agents-roster__table" data-testid="agents-table">
921
+ <DataTable<Agent>
922
+ columns={columns}
923
+ data={sorted}
924
+ rowKey={(a) => a.name}
925
+ compact
926
+ onRowClick={(a) => setSelectedName(a.name)}
927
+ />
647
928
  </div>
648
929
  )}
649
- </div>
650
- )}
651
- {agent.lastError && (
652
- <div className="agent-card-error">
653
- <AlertTriangle size={12} />
654
- <span className="muted">Last error: {agent.lastError.message}</span>
655
- </div>
656
- )}
657
- <div className="agent-card-actions">
658
- <Button variant="primary" size="sm" onClick={onInvoke}>
659
- <Play size={12} /> Invoke
660
- </Button>
661
- <Button variant="secondary" size="sm" onClick={onEdit}>
662
- <Pencil size={12} /> Edit
663
- </Button>
664
- {(agent.isStuck || agent.status === 'working' || agent.status === 'error') && (
665
- <Button variant="ghost" size="sm" onClick={onRestart} title="Reset agent status">
666
- <RotateCw size={12} /> Restart
667
- </Button>
668
- )}
669
- <Button variant="ghost" size="sm" onClick={onDelete}>
670
- <Trash2 size={12} />
671
- </Button>
672
- <button
673
- type="button"
674
- className="icon-btn"
675
- aria-label={expanded ? 'Collapse' : 'Expand'}
676
- onClick={() => setExpanded((v) => !v)}
677
- style={{ marginLeft: 'auto' }}
930
+ </Box>
931
+
932
+ <Panel
933
+ title={
934
+ selected ? (
935
+ <span className="agents-detail__name">
936
+ <span
937
+ className={cx(
938
+ 'agents-detail__name-swatch',
939
+ !selected.color && 'agents-detail__name-swatch--empty',
940
+ )}
941
+ style={selected.color ? { background: selected.color } : undefined}
942
+ aria-hidden="true"
943
+ />
944
+ <span>{selected.name}</span>
945
+ </span>
946
+ ) : (
947
+ 'Agent detail'
948
+ )
949
+ }
950
+ description={
951
+ selected
952
+ ? selected.path
953
+ : 'Select an agent from the roster to see its metadata.'
954
+ }
955
+ padding={4}
956
+ variant="outlined"
957
+ className="agents-detail"
678
958
  >
679
- {expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
680
- </button>
959
+ {renderDetailBody()}
960
+ </Panel>
681
961
  </div>
682
- {expanded && (
683
- <div className="agent-card-expanded">
684
- <div className="agent-card-status-actions">
685
- <span className="muted text-sm">Set status:</span>
686
- <Button
687
- variant={agent.status === 'idle' ? 'primary' : 'ghost'}
688
- size="sm"
689
- onClick={() => onSetStatus('idle')}
690
- >
691
- Idle
692
- </Button>
693
- <Button
694
- variant={agent.status === 'working' ? 'primary' : 'ghost'}
695
- size="sm"
696
- onClick={() => onSetStatus('working')}
697
- >
698
- Working
699
- </Button>
700
- <Button
701
- variant={agent.status === 'error' ? 'primary' : 'ghost'}
702
- size="sm"
703
- onClick={() => onSetStatus('error')}
704
- >
705
- Error
706
- </Button>
707
- </div>
708
- <div className="agent-card-meta">
709
- <Folder size={11} />
710
- <code className="mono agent-card-path">{agent.path}</code>
711
- </div>
712
- </div>
713
- )}
714
- </Card>
962
+ </div>
715
963
  );
716
- }
964
+ }