@polderlabs/bizar 6.3.0 → 7.0.0

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