@swarmclawai/swarmclaw 0.5.3 → 0.6.2

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 (224) hide show
  1. package/README.md +53 -9
  2. package/bin/server-cmd.js +1 -0
  3. package/bin/swarmclaw.js +76 -16
  4. package/next.config.ts +11 -1
  5. package/package.json +5 -2
  6. package/scripts/postinstall.mjs +18 -0
  7. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  8. package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
  9. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  10. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  11. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  12. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  13. package/src/app/api/chatrooms/route.ts +50 -0
  14. package/src/app/api/connectors/[id]/route.ts +1 -0
  15. package/src/app/api/connectors/route.ts +2 -1
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/files/open/route.ts +43 -0
  18. package/src/app/api/knowledge/[id]/route.ts +13 -2
  19. package/src/app/api/knowledge/route.ts +8 -1
  20. package/src/app/api/memory/route.ts +8 -0
  21. package/src/app/api/notifications/route.ts +4 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +53 -1
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  29. package/src/app/api/sessions/[id]/route.ts +4 -0
  30. package/src/app/api/sessions/route.ts +3 -3
  31. package/src/app/api/settings/route.ts +9 -0
  32. package/src/app/api/setup/check-provider/route.ts +3 -16
  33. package/src/app/api/skills/[id]/route.ts +6 -0
  34. package/src/app/api/skills/route.ts +6 -0
  35. package/src/app/api/tasks/[id]/route.ts +12 -0
  36. package/src/app/api/tasks/bulk/route.ts +100 -0
  37. package/src/app/api/tasks/metrics/route.ts +101 -0
  38. package/src/app/api/tasks/route.ts +18 -2
  39. package/src/app/api/tts/route.ts +3 -2
  40. package/src/app/api/tts/stream/route.ts +3 -2
  41. package/src/app/api/uploads/[filename]/route.ts +19 -34
  42. package/src/app/api/uploads/route.ts +94 -0
  43. package/src/app/api/webhooks/[id]/route.ts +15 -1
  44. package/src/app/globals.css +63 -15
  45. package/src/app/page.tsx +142 -13
  46. package/src/cli/index.js +40 -1
  47. package/src/cli/index.test.js +30 -0
  48. package/src/cli/spec.js +42 -0
  49. package/src/components/agents/agent-avatar.tsx +57 -10
  50. package/src/components/agents/agent-card.tsx +50 -17
  51. package/src/components/agents/agent-chat-list.tsx +148 -12
  52. package/src/components/agents/agent-list.tsx +50 -19
  53. package/src/components/agents/agent-sheet.tsx +120 -65
  54. package/src/components/agents/inspector-panel.tsx +81 -6
  55. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  56. package/src/components/agents/personality-builder.tsx +42 -14
  57. package/src/components/agents/soul-library-picker.tsx +89 -0
  58. package/src/components/auth/access-key-gate.tsx +10 -3
  59. package/src/components/auth/setup-wizard.tsx +2 -2
  60. package/src/components/auth/user-picker.tsx +31 -3
  61. package/src/components/canvas/canvas-panel.tsx +96 -0
  62. package/src/components/chat/activity-moment.tsx +173 -0
  63. package/src/components/chat/chat-area.tsx +46 -22
  64. package/src/components/chat/chat-header.tsx +457 -286
  65. package/src/components/chat/chat-preview-panel.tsx +1 -2
  66. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  67. package/src/components/chat/delegation-banner.tsx +371 -0
  68. package/src/components/chat/file-path-chip.tsx +146 -0
  69. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  70. package/src/components/chat/markdown-utils.ts +9 -0
  71. package/src/components/chat/message-bubble.tsx +356 -315
  72. package/src/components/chat/message-list.tsx +230 -8
  73. package/src/components/chat/streaming-bubble.tsx +104 -47
  74. package/src/components/chat/suggestions-bar.tsx +1 -1
  75. package/src/components/chat/thinking-indicator.tsx +72 -10
  76. package/src/components/chat/tool-call-bubble.tsx +111 -73
  77. package/src/components/chat/tool-request-banner.tsx +31 -7
  78. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  79. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  80. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  81. package/src/components/chatrooms/chatroom-list.tsx +130 -0
  82. package/src/components/chatrooms/chatroom-message.tsx +432 -0
  83. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  84. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  85. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  86. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  87. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  88. package/src/components/connectors/connector-list.tsx +168 -90
  89. package/src/components/connectors/connector-sheet.tsx +95 -56
  90. package/src/components/home/home-view.tsx +501 -0
  91. package/src/components/input/chat-input.tsx +107 -43
  92. package/src/components/knowledge/knowledge-list.tsx +31 -1
  93. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  94. package/src/components/layout/app-layout.tsx +194 -97
  95. package/src/components/layout/update-banner.tsx +2 -2
  96. package/src/components/logs/log-list.tsx +2 -2
  97. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  98. package/src/components/memory/memory-agent-list.tsx +143 -0
  99. package/src/components/memory/memory-browser.tsx +205 -0
  100. package/src/components/memory/memory-card.tsx +34 -7
  101. package/src/components/memory/memory-detail.tsx +359 -120
  102. package/src/components/memory/memory-sheet.tsx +157 -23
  103. package/src/components/plugins/plugin-list.tsx +1 -1
  104. package/src/components/plugins/plugin-sheet.tsx +1 -1
  105. package/src/components/projects/project-detail.tsx +509 -0
  106. package/src/components/projects/project-list.tsx +195 -59
  107. package/src/components/providers/provider-list.tsx +2 -2
  108. package/src/components/providers/provider-sheet.tsx +3 -3
  109. package/src/components/schedules/schedule-card.tsx +1 -1
  110. package/src/components/schedules/schedule-list.tsx +1 -1
  111. package/src/components/schedules/schedule-sheet.tsx +259 -126
  112. package/src/components/secrets/secret-sheet.tsx +47 -24
  113. package/src/components/secrets/secrets-list.tsx +18 -8
  114. package/src/components/sessions/new-session-sheet.tsx +33 -65
  115. package/src/components/sessions/session-card.tsx +45 -14
  116. package/src/components/sessions/session-list.tsx +35 -18
  117. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  118. package/src/components/shared/agent-picker-list.tsx +90 -0
  119. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  120. package/src/components/shared/attachment-chip.tsx +165 -0
  121. package/src/components/shared/avatar.tsx +10 -1
  122. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  123. package/src/components/shared/check-icon.tsx +12 -0
  124. package/src/components/shared/confirm-dialog.tsx +1 -1
  125. package/src/components/shared/connector-platform-icon.tsx +51 -4
  126. package/src/components/shared/empty-state.tsx +32 -0
  127. package/src/components/shared/file-preview.tsx +34 -0
  128. package/src/components/shared/form-styles.ts +2 -0
  129. package/src/components/shared/icon-button.tsx +16 -2
  130. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  131. package/src/components/shared/notification-center.tsx +44 -6
  132. package/src/components/shared/profile-sheet.tsx +115 -0
  133. package/src/components/shared/reply-quote.tsx +26 -0
  134. package/src/components/shared/search-dialog.tsx +31 -15
  135. package/src/components/shared/section-label.tsx +12 -0
  136. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  137. package/src/components/shared/settings/section-embedding.tsx +48 -13
  138. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  139. package/src/components/shared/settings/section-providers.tsx +1 -1
  140. package/src/components/shared/settings/section-secrets.tsx +1 -1
  141. package/src/components/shared/settings/section-storage.tsx +206 -0
  142. package/src/components/shared/settings/section-theme.tsx +95 -0
  143. package/src/components/shared/settings/section-user-preferences.tsx +57 -0
  144. package/src/components/shared/settings/section-voice.tsx +42 -21
  145. package/src/components/shared/settings/section-web-search.tsx +30 -6
  146. package/src/components/shared/settings/settings-page.tsx +182 -27
  147. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  148. package/src/components/shared/settings/storage-browser.tsx +259 -0
  149. package/src/components/shared/sheet-footer.tsx +33 -0
  150. package/src/components/skills/skill-list.tsx +61 -30
  151. package/src/components/skills/skill-sheet.tsx +81 -2
  152. package/src/components/tasks/task-board.tsx +448 -26
  153. package/src/components/tasks/task-card.tsx +59 -9
  154. package/src/components/tasks/task-column.tsx +62 -3
  155. package/src/components/tasks/task-list.tsx +12 -4
  156. package/src/components/tasks/task-sheet.tsx +416 -74
  157. package/src/components/ui/hover-card.tsx +52 -0
  158. package/src/components/usage/metrics-dashboard.tsx +90 -6
  159. package/src/components/usage/usage-list.tsx +1 -1
  160. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  161. package/src/hooks/use-continuous-speech.ts +10 -4
  162. package/src/hooks/use-view-router.ts +69 -19
  163. package/src/hooks/use-voice-conversation.ts +53 -10
  164. package/src/hooks/use-ws.ts +4 -2
  165. package/src/instrumentation.ts +15 -1
  166. package/src/lib/chat.ts +2 -0
  167. package/src/lib/memory.ts +3 -0
  168. package/src/lib/providers/anthropic.ts +13 -7
  169. package/src/lib/providers/index.ts +1 -0
  170. package/src/lib/providers/openai.ts +13 -7
  171. package/src/lib/server/chat-execution.ts +75 -15
  172. package/src/lib/server/chatroom-helpers.ts +146 -0
  173. package/src/lib/server/connectors/manager.ts +229 -7
  174. package/src/lib/server/context-manager.ts +225 -13
  175. package/src/lib/server/create-notification.ts +14 -2
  176. package/src/lib/server/daemon-state.ts +157 -10
  177. package/src/lib/server/execution-log.ts +1 -0
  178. package/src/lib/server/heartbeat-service.ts +48 -6
  179. package/src/lib/server/heartbeat-wake.ts +110 -0
  180. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  181. package/src/lib/server/main-agent-loop.ts +1 -1
  182. package/src/lib/server/memory-consolidation.ts +105 -0
  183. package/src/lib/server/memory-db.ts +183 -10
  184. package/src/lib/server/mime.ts +51 -0
  185. package/src/lib/server/openclaw-gateway.ts +9 -1
  186. package/src/lib/server/orchestrator-lg.ts +2 -0
  187. package/src/lib/server/orchestrator.ts +5 -2
  188. package/src/lib/server/playwright-proxy.mjs +2 -3
  189. package/src/lib/server/prompt-runtime-context.ts +53 -0
  190. package/src/lib/server/provider-health.ts +125 -0
  191. package/src/lib/server/queue.ts +56 -10
  192. package/src/lib/server/scheduler.ts +8 -0
  193. package/src/lib/server/session-run-manager.ts +4 -0
  194. package/src/lib/server/session-tools/canvas.ts +67 -0
  195. package/src/lib/server/session-tools/chatroom.ts +136 -0
  196. package/src/lib/server/session-tools/connector.ts +83 -9
  197. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  198. package/src/lib/server/session-tools/crud.ts +21 -0
  199. package/src/lib/server/session-tools/delegate.ts +68 -4
  200. package/src/lib/server/session-tools/git.ts +71 -0
  201. package/src/lib/server/session-tools/http.ts +57 -0
  202. package/src/lib/server/session-tools/index.ts +10 -0
  203. package/src/lib/server/session-tools/memory.ts +7 -1
  204. package/src/lib/server/session-tools/search-providers.ts +16 -8
  205. package/src/lib/server/session-tools/subagent.ts +106 -0
  206. package/src/lib/server/session-tools/web.ts +115 -4
  207. package/src/lib/server/storage.ts +53 -29
  208. package/src/lib/server/stream-agent-chat.ts +185 -57
  209. package/src/lib/server/system-events.ts +49 -0
  210. package/src/lib/server/task-mention.ts +41 -0
  211. package/src/lib/server/ws-hub.ts +11 -0
  212. package/src/lib/sessions.ts +10 -0
  213. package/src/lib/soul-library.ts +103 -0
  214. package/src/lib/soul-suggestions.ts +109 -0
  215. package/src/lib/task-dedupe.ts +26 -0
  216. package/src/lib/tasks.ts +4 -1
  217. package/src/lib/tool-definitions.ts +2 -0
  218. package/src/lib/tts.ts +2 -2
  219. package/src/lib/view-routes.ts +36 -1
  220. package/src/lib/ws-client.ts +14 -4
  221. package/src/stores/use-app-store.ts +41 -3
  222. package/src/stores/use-chat-store.ts +113 -5
  223. package/src/stores/use-chatroom-store.ts +276 -0
  224. package/src/types/index.ts +88 -4
package/src/app/page.tsx CHANGED
@@ -12,6 +12,130 @@ import { SetupWizard } from '@/components/auth/setup-wizard'
12
12
  import { AppLayout } from '@/components/layout/app-layout'
13
13
  import { useViewRouter } from '@/hooks/use-view-router'
14
14
 
15
+ function FullScreenLoader() {
16
+ return (
17
+ <div className="h-full flex flex-col items-center justify-center bg-bg overflow-hidden select-none">
18
+ {/* Animated orbital ring */}
19
+ <div className="relative w-[120px] h-[120px] mb-8">
20
+ {/* Outer glow pulse */}
21
+ <div
22
+ className="absolute inset-[-20px] rounded-full"
23
+ style={{
24
+ background: 'radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%)',
25
+ animation: 'sc-glow 2.5s ease-in-out infinite',
26
+ }}
27
+ />
28
+
29
+ {/* Orbital ring */}
30
+ <div
31
+ className="absolute inset-0 rounded-full border border-white/[0.06]"
32
+ style={{ animation: 'sc-ring 3s linear infinite' }}
33
+ />
34
+
35
+ {/* Orbiting dots */}
36
+ {[0, 1, 2, 3, 4, 5].map((i) => (
37
+ <div
38
+ key={i}
39
+ className="absolute inset-0"
40
+ style={{
41
+ animation: `sc-orbit 2.4s cubic-bezier(0.4, 0, 0.2, 1) infinite`,
42
+ animationDelay: `${i * -0.4}s`,
43
+ }}
44
+ >
45
+ <div
46
+ className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
47
+ style={{
48
+ width: i === 0 ? 8 : 6,
49
+ height: i === 0 ? 8 : 6,
50
+ background: i === 0 ? '#818CF8' : `rgba(129, 140, 248, ${0.7 - i * 0.1})`,
51
+ boxShadow: i === 0 ? '0 0 12px rgba(99,102,241,0.5)' : 'none',
52
+ }}
53
+ />
54
+ </div>
55
+ ))}
56
+
57
+ {/* Center logo mark */}
58
+ <div className="absolute inset-0 flex items-center justify-center">
59
+ <div
60
+ className="relative"
61
+ style={{ animation: 'sc-breathe 2.5s ease-in-out infinite' }}
62
+ >
63
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none">
64
+ {/* Hexagonal claw mark */}
65
+ <path
66
+ d="M18 4L30 11V25L18 32L6 25V11L18 4Z"
67
+ stroke="rgba(129, 140, 248, 0.3)"
68
+ strokeWidth="1"
69
+ fill="none"
70
+ />
71
+ <path
72
+ d="M18 9L25 13V23L18 27L11 23V13L18 9Z"
73
+ stroke="rgba(129, 140, 248, 0.5)"
74
+ strokeWidth="1.5"
75
+ fill="rgba(99, 102, 241, 0.06)"
76
+ />
77
+ {/* Claw lines */}
78
+ <path d="M14 15L18 20L22 15" stroke="#818CF8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
79
+ <path d="M12 13L18 20L24 13" stroke="rgba(129, 140, 248, 0.3)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
80
+ </svg>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ {/* Brand text */}
86
+ <div
87
+ className="text-[15px] font-display font-700 tracking-[0.15em] uppercase"
88
+ style={{
89
+ background: 'linear-gradient(135deg, rgba(255,255,255,0.6), rgba(129, 140, 248, 0.8))',
90
+ WebkitBackgroundClip: 'text',
91
+ WebkitTextFillColor: 'transparent',
92
+ animation: 'sc-text-fade 2s ease-in-out infinite alternate',
93
+ }}
94
+ >
95
+ SwarmClaw
96
+ </div>
97
+
98
+ {/* Loading bar */}
99
+ <div className="mt-4 w-[100px] h-[2px] rounded-full bg-white/[0.06] overflow-hidden">
100
+ <div
101
+ className="h-full rounded-full bg-accent-bright/60"
102
+ style={{ animation: 'sc-progress 1.5s ease-in-out infinite' }}
103
+ />
104
+ </div>
105
+
106
+ {/* Loading animation keyframes */}
107
+ <style>{`
108
+ @keyframes sc-orbit {
109
+ from { transform: rotate(0deg); }
110
+ to { transform: rotate(360deg); }
111
+ }
112
+ @keyframes sc-ring {
113
+ from { transform: rotate(0deg) scale(1); }
114
+ 50% { transform: rotate(180deg) scale(1.02); }
115
+ to { transform: rotate(360deg) scale(1); }
116
+ }
117
+ @keyframes sc-breathe {
118
+ 0%, 100% { transform: scale(1); opacity: 0.9; }
119
+ 50% { transform: scale(1.06); opacity: 1; }
120
+ }
121
+ @keyframes sc-glow {
122
+ 0%, 100% { opacity: 0.5; transform: scale(0.9); }
123
+ 50% { opacity: 1; transform: scale(1.1); }
124
+ }
125
+ @keyframes sc-text-fade {
126
+ 0% { opacity: 0.6; }
127
+ 100% { opacity: 1; }
128
+ }
129
+ @keyframes sc-progress {
130
+ 0% { width: 0; margin-left: 0; }
131
+ 50% { width: 70%; margin-left: 15%; }
132
+ 100% { width: 0; margin-left: 100%; }
133
+ }
134
+ `}</style>
135
+ </div>
136
+ )
137
+ }
138
+
15
139
  export default function Home() {
16
140
  const currentUser = useAppStore((s) => s.currentUser)
17
141
  const setUser = useAppStore((s) => s.setUser)
@@ -83,26 +207,31 @@ export default function Home() {
83
207
 
84
208
  useWs('sessions', loadSessions, 5000)
85
209
 
86
- // Auto-select default agent's thread on load
210
+ // Auto-select agent's thread on load — resolves a persisted agentId into a session,
211
+ // or falls back to defaultAgentId from settings, then first agent.
212
+ const [agentReady, setAgentReady] = useState(false)
87
213
  useEffect(() => {
88
214
  if (!authenticated || !currentUser) return
89
- const state = useAppStore.getState()
90
- // Only auto-select if no agent is selected yet
91
- if (state.currentAgentId) return
92
-
93
- // Load agents and select 'default' agent
94
215
  let cancelled = false
95
216
  ;(async () => {
96
217
  try {
218
+ const state = useAppStore.getState()
97
219
  await state.loadAgents()
98
220
  if (cancelled) return
99
- const agents = useAppStore.getState().agents
100
- // Try 'default' agent first, then fall back to first agent
101
- const defaultAgent = agents['default'] || Object.values(agents)[0]
102
- if (defaultAgent) {
103
- await useAppStore.getState().setCurrentAgent(defaultAgent.id)
221
+
222
+ const { agents, currentAgentId, appSettings } = useAppStore.getState()
223
+ // Priority: persisted agent > settings default > first agent
224
+ const targetId = (currentAgentId && agents[currentAgentId])
225
+ ? currentAgentId
226
+ : (appSettings.defaultAgentId && agents[appSettings.defaultAgentId])
227
+ ? appSettings.defaultAgentId
228
+ : Object.values(agents)[0]?.id || null
229
+
230
+ if (targetId) {
231
+ await useAppStore.getState().setCurrentAgent(targetId)
104
232
  }
105
233
  } catch { /* ignore */ }
234
+ if (!cancelled) setAgentReady(true)
106
235
  })()
107
236
  return () => { cancelled = true }
108
237
  }, [authenticated, currentUser])
@@ -148,10 +277,10 @@ export default function Home() {
148
277
 
149
278
  useViewRouter()
150
279
 
151
- if (!hydrated || !authChecked) return null
280
+ if (!hydrated || !authChecked) return <FullScreenLoader />
152
281
  if (!authenticated) return <AccessKeyGate onAuthenticated={() => setAuthenticated(true)} />
153
282
  if (!currentUser) return <UserPicker />
154
- if (setupDone === null) return null
283
+ if (setupDone === null || !agentReady) return <FullScreenLoader />
155
284
  if (!setupDone) return <SetupWizard onComplete={() => setSetupDone(true)} />
156
285
  return <AppLayout />
157
286
  }
package/src/cli/index.js CHANGED
@@ -56,6 +56,37 @@ const COMMAND_GROUPS = [
56
56
  cmd('install', 'POST', '/clawhub/install', 'Install a skill from ClawHub', { expectsJsonBody: true }),
57
57
  ],
58
58
  },
59
+ {
60
+ name: 'chatrooms',
61
+ description: 'Manage multi-agent chatrooms',
62
+ commands: [
63
+ cmd('list', 'GET', '/chatrooms', 'List chatrooms'),
64
+ cmd('get', 'GET', '/chatrooms/:id', 'Get chatroom by id'),
65
+ cmd('create', 'POST', '/chatrooms', 'Create a chatroom', { expectsJsonBody: true }),
66
+ cmd('update', 'PUT', '/chatrooms/:id', 'Update a chatroom', { expectsJsonBody: true }),
67
+ cmd('delete', 'DELETE', '/chatrooms/:id', 'Delete a chatroom'),
68
+ cmd('chat', 'POST', '/chatrooms/:id/chat', 'Post a message to a chatroom and stream agent replies', {
69
+ expectsJsonBody: true,
70
+ responseType: 'sse',
71
+ }),
72
+ cmd('add-member', 'POST', '/chatrooms/:id/members', 'Add an agent to a chatroom (use --data \'{"agentId":"..."}\')', { expectsJsonBody: true }),
73
+ cmd('remove-member', 'DELETE', '/chatrooms/:id/members', 'Remove an agent from a chatroom (use --data \'{"agentId":"..."}\')', { expectsJsonBody: true }),
74
+ cmd('react', 'POST', '/chatrooms/:id/reactions', 'Toggle a reaction on a chatroom message', {
75
+ expectsJsonBody: true,
76
+ }),
77
+ cmd('pin', 'POST', '/chatrooms/:id/pins', 'Toggle pin on a chatroom message', {
78
+ expectsJsonBody: true,
79
+ }),
80
+ ],
81
+ },
82
+ {
83
+ name: 'canvas',
84
+ description: 'Read/update per-session canvas content',
85
+ commands: [
86
+ cmd('get', 'GET', '/canvas/:sessionId', 'Get current canvas content for a session'),
87
+ cmd('set', 'POST', '/canvas/:sessionId', 'Set/clear canvas content for a session', { expectsJsonBody: true }),
88
+ ],
89
+ },
59
90
  {
60
91
  name: 'connectors',
61
92
  description: 'Manage chat connectors',
@@ -131,6 +162,7 @@ const COMMAND_GROUPS = [
131
162
  description: 'Serve and manage local files',
132
163
  commands: [
133
164
  cmd('serve', 'GET', '/files/serve', 'Serve a local file (use --query path=/abs/path)'),
165
+ cmd('open', 'POST', '/files/open', 'Open a local file path via the host default app/browser', { expectsJsonBody: true }),
134
166
  ],
135
167
  },
136
168
  {
@@ -358,6 +390,8 @@ const COMMAND_GROUPS = [
358
390
  }),
359
391
  cmd('messages', 'GET', '/sessions/:id/messages', 'Get session messages'),
360
392
  cmd('messages-update', 'PUT', '/sessions/:id/messages', 'Update session message metadata (e.g. bookmark)', { expectsJsonBody: true }),
393
+ cmd('messages-send', 'POST', '/sessions/:id/messages', 'Append a user/system message to a session', { expectsJsonBody: true }),
394
+ cmd('messages-delete', 'DELETE', '/sessions/:id/messages', 'Delete a message from a session', { expectsJsonBody: true }),
361
395
  cmd('fork', 'POST', '/sessions/:id/fork', 'Fork session from a specific message index', { expectsJsonBody: true }),
362
396
  cmd('edit-resend', 'POST', '/sessions/:id/edit-resend', 'Edit and resend from a specific message index', { expectsJsonBody: true }),
363
397
  cmd('main-loop', 'GET', '/sessions/:id/main-loop', 'Get main mission loop state'),
@@ -427,10 +461,12 @@ const COMMAND_GROUPS = [
427
461
  cmd('list', 'GET', '/tasks', 'List tasks'),
428
462
  cmd('get', 'GET', '/tasks/:id', 'Get task'),
429
463
  cmd('create', 'POST', '/tasks', 'Create task', { expectsJsonBody: true }),
464
+ cmd('bulk', 'POST', '/tasks/bulk', 'Bulk update tasks (status/agent/project)', { expectsJsonBody: true }),
430
465
  cmd('update', 'PUT', '/tasks/:id', 'Update task', { expectsJsonBody: true }),
431
466
  cmd('delete', 'DELETE', '/tasks/:id', 'Delete task'),
432
467
  cmd('purge', 'DELETE', '/tasks', 'Bulk delete tasks', { expectsJsonBody: true }),
433
468
  cmd('approve', 'POST', '/tasks/:id/approve', 'Approve or reject a pending tool execution', { expectsJsonBody: true }),
469
+ cmd('metrics', 'GET', '/tasks/metrics', 'Get task board metrics (supports --query range=24h|7d|30d)'),
434
470
  ],
435
471
  },
436
472
  {
@@ -461,9 +497,12 @@ const COMMAND_GROUPS = [
461
497
  },
462
498
  {
463
499
  name: 'uploads',
464
- description: 'Fetch uploaded artifacts',
500
+ description: 'Manage uploaded artifacts',
465
501
  commands: [
502
+ cmd('list', 'GET', '/uploads', 'List uploaded artifacts'),
466
503
  cmd('get', 'GET', '/uploads/:filename', 'Download uploaded artifact', { responseType: 'binary' }),
504
+ cmd('delete', 'DELETE', '/uploads/:filename', 'Delete uploaded artifact by filename'),
505
+ cmd('delete-many', 'DELETE', '/uploads', 'Delete uploads by filter/body (filenames, olderThanDays, category, or all)', { expectsJsonBody: true }),
467
506
  ],
468
507
  },
469
508
  {
@@ -81,6 +81,36 @@ test('CLI command map covers all API route method/path pairs', () => {
81
81
  assert.deepEqual(missing, [])
82
82
  })
83
83
 
84
+ test('Binary CLI router reaches every mapped API command pair', async () => {
85
+ const { shouldUseLegacyTsCli, TS_CLI_ACTIONS } = await import('../../bin/swarmclaw.js')
86
+
87
+ for (const command of COMMANDS) {
88
+ if (command.virtual) continue
89
+
90
+ const pathArgs = extractPathParams(command.route).map((name, index) => `${name}-${index + 1}`)
91
+ const routedToLegacyTs = shouldUseLegacyTsCli([command.group, command.action, ...pathArgs])
92
+
93
+ if (routedToLegacyTs) {
94
+ assert.ok(
95
+ TS_CLI_ACTIONS[command.group]?.has(command.action),
96
+ `legacy TS router should only claim known actions (${command.group} ${command.action})`,
97
+ )
98
+ }
99
+ }
100
+
101
+ // Spot-check known API commands that are map-only today.
102
+ assert.equal(shouldUseLegacyTsCli(['chatrooms', 'list']), false)
103
+ assert.equal(shouldUseLegacyTsCli(['tasks', 'approve', 'task-1']), false)
104
+
105
+ // Help paths should route to mapped CLI for full command discoverability.
106
+ assert.equal(shouldUseLegacyTsCli([]), false)
107
+ assert.equal(shouldUseLegacyTsCli(['--help']), false)
108
+ assert.equal(shouldUseLegacyTsCli(['tasks', '--help']), false)
109
+
110
+ // And a legacy command that should remain on the richer TS path.
111
+ assert.equal(shouldUseLegacyTsCli(['tasks', 'create']), true)
112
+ })
113
+
84
114
  test('parseArgv parses group/action/options', () => {
85
115
  const parsed = parseArgv([
86
116
  'runs',
package/src/cli/spec.js CHANGED
@@ -25,6 +25,28 @@ const COMMAND_GROUPS = {
25
25
  login: { description: 'Validate an access key', method: 'POST', path: '/auth' },
26
26
  },
27
27
  },
28
+ chatrooms: {
29
+ description: 'Manage multi-agent chatrooms',
30
+ commands: {
31
+ list: { description: 'List chatrooms', method: 'GET', path: '/chatrooms' },
32
+ get: { description: 'Get chatroom by id', method: 'GET', path: '/chatrooms/:id', params: ['id'] },
33
+ create: { description: 'Create a chatroom', method: 'POST', path: '/chatrooms' },
34
+ update: { description: 'Update a chatroom', method: 'PUT', path: '/chatrooms/:id', params: ['id'] },
35
+ delete: { description: 'Delete a chatroom', method: 'DELETE', path: '/chatrooms/:id', params: ['id'] },
36
+ chat: { description: 'Post chatroom message and stream agent replies', method: 'POST', path: '/chatrooms/:id/chat', params: ['id'] },
37
+ 'add-member': { description: 'Add an agent to a chatroom', method: 'POST', path: '/chatrooms/:id/members', params: ['id'] },
38
+ 'remove-member': { description: 'Remove an agent from a chatroom', method: 'DELETE', path: '/chatrooms/:id/members', params: ['id'] },
39
+ react: { description: 'Toggle reaction on a chatroom message', method: 'POST', path: '/chatrooms/:id/reactions', params: ['id'] },
40
+ pin: { description: 'Toggle pin on a chatroom message', method: 'POST', path: '/chatrooms/:id/pins', params: ['id'] },
41
+ },
42
+ },
43
+ canvas: {
44
+ description: 'Session canvas content',
45
+ commands: {
46
+ get: { description: 'Get current canvas content for a session', method: 'GET', path: '/canvas/:sessionId', params: ['sessionId'] },
47
+ set: { description: 'Set/clear canvas content for a session', method: 'POST', path: '/canvas/:sessionId', params: ['sessionId'] },
48
+ },
49
+ },
28
50
  connectors: {
29
51
  description: 'Manage chat connectors',
30
52
  commands: {
@@ -105,6 +127,22 @@ const COMMAND_GROUPS = {
105
127
  },
106
128
  },
107
129
  },
130
+ uploads: {
131
+ description: 'Manage uploaded artifacts',
132
+ commands: {
133
+ list: { description: 'List uploaded artifacts', method: 'GET', path: '/uploads' },
134
+ get: { description: 'Download uploaded artifact by filename', method: 'GET', path: '/uploads/:filename', params: ['filename'], binary: true },
135
+ delete: { description: 'Delete uploaded artifact by filename', method: 'DELETE', path: '/uploads/:filename', params: ['filename'] },
136
+ 'delete-many': { description: 'Delete uploads by filter/body (filenames, olderThanDays, category, or all)', method: 'DELETE', path: '/uploads' },
137
+ },
138
+ },
139
+ files: {
140
+ description: 'Serve/open local files',
141
+ commands: {
142
+ serve: { description: 'Serve a local file (supports --query path=/some/file)', method: 'GET', path: '/files/serve' },
143
+ open: { description: 'Open a local file path via host default app/browser', method: 'POST', path: '/files/open' },
144
+ },
145
+ },
108
146
  logs: {
109
147
  description: 'Application logs',
110
148
  commands: {
@@ -247,6 +285,8 @@ const COMMAND_GROUPS = {
247
285
  'heartbeat-disable-all': { description: 'Disable all session heartbeats and cancel queued heartbeat runs', method: 'POST', path: '/sessions/heartbeat' },
248
286
  messages: { description: 'Get session message history', method: 'GET', path: '/sessions/:id/messages', params: ['id'] },
249
287
  'messages-update': { description: 'Update session message metadata (e.g. bookmark)', method: 'PUT', path: '/sessions/:id/messages', params: ['id'] },
288
+ 'messages-send': { description: 'Append a user/system message to a session', method: 'POST', path: '/sessions/:id/messages', params: ['id'] },
289
+ 'messages-delete': { description: 'Delete a message from a session', method: 'DELETE', path: '/sessions/:id/messages', params: ['id'] },
250
290
  fork: { description: 'Fork session from a specific message index', method: 'POST', path: '/sessions/:id/fork', params: ['id'] },
251
291
  'edit-resend': { description: 'Edit and resend from a specific message index', method: 'POST', path: '/sessions/:id/edit-resend', params: ['id'] },
252
292
  'main-loop': { description: 'Get main mission loop state for a session', method: 'GET', path: '/sessions/:id/main-loop', params: ['id'] },
@@ -303,10 +343,12 @@ const COMMAND_GROUPS = {
303
343
  list: { description: 'List tasks', method: 'GET', path: '/tasks' },
304
344
  get: { description: 'Get task by id', method: 'GET', path: '/tasks/:id', params: ['id'] },
305
345
  create: { description: 'Create task', method: 'POST', path: '/tasks' },
346
+ bulk: { description: 'Bulk update tasks (status/agent/project)', method: 'POST', path: '/tasks/bulk' },
306
347
  update: { description: 'Update task', method: 'PUT', path: '/tasks/:id', params: ['id'] },
307
348
  delete: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
308
349
  archive: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
309
350
  approve: { description: 'Approve or reject a pending tool execution', method: 'POST', path: '/tasks/:id/approve', params: ['id'] },
351
+ metrics: { description: 'Get task board metrics (supports --query range=24h|7d|30d)', method: 'GET', path: '/tasks/metrics' },
310
352
  },
311
353
  },
312
354
  webhooks: {
@@ -3,26 +3,66 @@
3
3
  import { useMemo } from 'react'
4
4
  import multiavatar from '@multiavatar/multiavatar'
5
5
 
6
+ /** Strip scripts/event handlers from SVG to prevent XSS */
7
+ function sanitizeSvg(svg: string): string {
8
+ return svg
9
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
10
+ .replace(/\bon\w+\s*=\s*"[^"]*"/gi, '')
11
+ .replace(/\bon\w+\s*=\s*'[^']*'/gi, '')
12
+ }
13
+
6
14
  interface Props {
7
15
  seed?: string | null
8
16
  name: string
9
17
  size?: number
10
18
  className?: string
19
+ status?: 'idle' | 'busy' | 'online'
20
+ heartbeatPulse?: boolean
11
21
  }
12
22
 
13
- export function AgentAvatar({ seed, name, size = 32, className = '' }: Props) {
23
+ const STATUS_COLORS: Record<string, string> = {
24
+ busy: 'bg-amber-400',
25
+ online: 'bg-emerald-400',
26
+ }
27
+
28
+ const HEART_PATH = 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'
29
+
30
+ export function AgentAvatar({ seed, name, size = 32, className = '', status, heartbeatPulse }: Props) {
14
31
  const svgHtml = useMemo(() => {
15
32
  if (!seed) return null
16
- return multiavatar(seed)
33
+ return sanitizeSvg(multiavatar(seed))
17
34
  }, [seed])
18
35
 
36
+ const dotSize = Math.max(6, Math.round(size * 0.28))
37
+ const dot = status && status !== 'idle' ? (
38
+ <span
39
+ className={`absolute -bottom-0.5 -right-0.5 rounded-full ${STATUS_COLORS[status]} ring-2 ring-[#0f0f1a]`}
40
+ style={{ width: dotSize, height: dotSize }}
41
+ title={status === 'busy' ? 'Busy' : 'Online'}
42
+ />
43
+ ) : null
44
+
45
+ const heartEl = heartbeatPulse ? (
46
+ <svg
47
+ className="absolute left-1/2 -translate-x-1/2 pointer-events-none"
48
+ style={{ top: -Math.max(10, size * 0.35), width: 10, height: 10, animation: 'heartbeat-float 1.5s ease forwards' }}
49
+ viewBox="0 0 24 24"
50
+ fill="#22c55e"
51
+ >
52
+ <path d={HEART_PATH} />
53
+ </svg>
54
+ ) : null
55
+
19
56
  if (svgHtml) {
20
57
  return (
21
- <div
22
- className={`shrink-0 rounded-full overflow-hidden ${className}`}
23
- style={{ width: size, height: size }}
24
- dangerouslySetInnerHTML={{ __html: svgHtml }}
25
- />
58
+ <div className={`relative shrink-0 ${className}`} style={{ width: size, height: size }}>
59
+ <div
60
+ className="rounded-full overflow-hidden w-full h-full"
61
+ dangerouslySetInnerHTML={{ __html: svgHtml }}
62
+ />
63
+ {heartEl}
64
+ {dot}
65
+ </div>
26
66
  )
27
67
  }
28
68
 
@@ -36,10 +76,17 @@ export function AgentAvatar({ seed, name, size = 32, className = '' }: Props) {
36
76
 
37
77
  return (
38
78
  <div
39
- className={`shrink-0 rounded-full flex items-center justify-center bg-accent-soft text-accent-bright font-600 ${className}`}
40
- style={{ width: size, height: size, fontSize: size * 0.38 }}
79
+ className={`relative shrink-0 ${className}`}
80
+ style={{ width: size, height: size }}
41
81
  >
42
- {initials || '?'}
82
+ <div
83
+ className="rounded-full flex items-center justify-center bg-accent-soft text-accent-bright font-600 w-full h-full"
84
+ style={{ fontSize: size * 0.38 }}
85
+ >
86
+ {initials || '?'}
87
+ </div>
88
+ {heartEl}
89
+ {dot}
43
90
  </div>
44
91
  )
45
92
  }
@@ -4,6 +4,7 @@ import { useState } from 'react'
4
4
  import type { Agent } from '@/types'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
7
+ import { useWs } from '@/hooks/use-ws'
7
8
  import { api } from '@/lib/api-client'
8
9
  import { createAgent, deleteAgent } from '@/lib/agents'
9
10
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
@@ -17,16 +18,18 @@ import {
17
18
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
18
19
  import { useApprovalStore } from '@/stores/use-approval-store'
19
20
  import { AgentAvatar } from './agent-avatar'
21
+ import { toast } from 'sonner'
20
22
 
21
23
  interface Props {
22
24
  agent: Agent
23
25
  isDefault?: boolean
24
26
  isRunning?: boolean
27
+ isOnline?: boolean
25
28
  isSelected?: boolean
26
29
  onSetDefault?: (id: string) => void
27
30
  }
28
31
 
29
- export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefault }: Props) {
32
+ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, onSetDefault }: Props) {
30
33
  const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
31
34
  const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
32
35
  const loadSessions = useAppStore((s) => s.loadSessions)
@@ -34,12 +37,18 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
34
37
  const setCurrentSession = useAppStore((s) => s.setCurrentSession)
35
38
  const setActiveView = useAppStore((s) => s.setActiveView)
36
39
  const setMessages = useChatStore((s) => s.setMessages)
40
+ const togglePinAgent = useAppStore((s) => s.togglePinAgent)
37
41
  const [running, setRunning] = useState(false)
38
42
  const [dialogOpen, setDialogOpen] = useState(false)
39
43
  const [taskInput, setTaskInput] = useState('')
40
44
  const [confirmDelete, setConfirmDelete] = useState(false)
41
45
  const approvals = useApprovalStore((s) => s.approvals)
42
46
  const pendingApprovalCount = Object.values(approvals).filter((a) => a.agentId === agent.id).length
47
+ const [heartbeatPulse, setHeartbeatPulse] = useState(false)
48
+ useWs(`heartbeat:agent:${agent.id}`, () => {
49
+ setHeartbeatPulse(true)
50
+ setTimeout(() => setHeartbeatPulse(false), 1500)
51
+ })
43
52
 
44
53
  const handleClick = () => {
45
54
  setEditingAgentId(agent.id)
@@ -74,11 +83,13 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
74
83
  const { id: _id, createdAt: _ca, updatedAt: _ua, ...rest } = agent
75
84
  await createAgent({ ...rest, name: agent.name + ' (Copy)' })
76
85
  await loadAgents()
86
+ toast.success('Agent duplicated')
77
87
  }
78
88
 
79
89
  const handleDelete = async () => {
80
90
  await deleteAgent(agent.id)
81
91
  await loadAgents()
92
+ toast.success('Agent moved to trash')
82
93
  setConfirmDelete(false)
83
94
  }
84
95
 
@@ -93,6 +104,21 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
93
104
  : 'bg-transparent border border-transparent hover:bg-white/[0.05] hover:border-white/[0.08]'}`}
94
105
  >
95
106
  {isSelected && <div className="card-select-indicator" />}
107
+ {/* Pin/star button */}
108
+ <button
109
+ onClick={(e) => {
110
+ e.stopPropagation()
111
+ togglePinAgent(agent.id)
112
+ toast.success(agent.pinned ? 'Agent unpinned' : 'Agent pinned')
113
+ }}
114
+ aria-label={agent.pinned ? 'Unpin agent' : 'Pin agent'}
115
+ className={`absolute top-3 right-10 p-1 rounded-[6px] transition-all bg-transparent border-none cursor-pointer hover:bg-white/[0.06]
116
+ ${agent.pinned ? 'opacity-100 text-amber-400' : 'opacity-0 group-hover:opacity-60 hover:!opacity-100 text-text-3'}`}
117
+ >
118
+ <svg width="12" height="12" viewBox="0 0 24 24" fill={agent.pinned ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
119
+ <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
120
+ </svg>
121
+ </button>
96
122
  {/* Three-dot dropdown */}
97
123
  <DropdownMenu>
98
124
  <DropdownMenuTrigger asChild>
@@ -111,9 +137,12 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
111
137
  </DropdownMenuTrigger>
112
138
  <DropdownMenuContent align="end" className="min-w-[140px]">
113
139
  <DropdownMenuItem onClick={handleClick}>Edit</DropdownMenuItem>
140
+ <DropdownMenuItem onClick={() => { togglePinAgent(agent.id); toast.success(agent.pinned ? 'Agent unpinned' : 'Agent pinned') }}>
141
+ {agent.pinned ? 'Unpin' : 'Pin'}
142
+ </DropdownMenuItem>
114
143
  <DropdownMenuItem onClick={handleDuplicate}>Duplicate</DropdownMenuItem>
115
144
  {!isDefault && onSetDefault && (
116
- <DropdownMenuItem onClick={() => onSetDefault(agent.id)}>Set Default</DropdownMenuItem>
145
+ <DropdownMenuItem onClick={() => { onSetDefault(agent.id); toast.success(`${agent.name} set as default`) }}>Set Default</DropdownMenuItem>
117
146
  )}
118
147
  <DropdownMenuSeparator />
119
148
  <DropdownMenuItem
@@ -126,14 +155,17 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
126
155
  </DropdownMenu>
127
156
 
128
157
  <div className="flex items-center gap-2.5">
129
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={28} />
130
- {isRunning && (
131
- <span className="shrink-0 w-2 h-2 rounded-full bg-emerald-400" style={{ animation: 'pulse 2s ease infinite' }} title="Running" />
132
- )}
158
+ <AgentAvatar
159
+ seed={agent.avatarSeed}
160
+ name={agent.name}
161
+ size={28}
162
+ status={isRunning ? 'busy' : isOnline ? 'online' : undefined}
163
+ heartbeatPulse={heartbeatPulse}
164
+ />
133
165
  <span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{agent.name}</span>
134
166
  {pendingApprovalCount > 0 && (
135
- <span className="shrink-0 inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-red-500 text-white text-[10px] font-700">
136
- {pendingApprovalCount}
167
+ <span className="shrink-0 text-[9px] font-600 uppercase tracking-wider px-2 py-0.5 rounded-[6px] text-amber-400 bg-amber-400/[0.08] border border-amber-400/15">
168
+ {pendingApprovalCount} {pendingApprovalCount === 1 ? 'approval' : 'approvals'}
137
169
  </span>
138
170
  )}
139
171
  {isDefault && (
@@ -146,15 +178,16 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
146
178
  onClick={handleRunClick}
147
179
  disabled={running}
148
180
  className="shrink-0 text-[10px] font-600 uppercase tracking-wider px-2.5 py-1 rounded-[6px] cursor-pointer
149
- transition-all border-none bg-[#6366F1]/20 text-[#818CF8] hover:bg-[#6366F1]/30 disabled:opacity-40"
181
+ transition-all border-none bg-accent-bright/20 text-accent-bright hover:bg-accent-bright/30 disabled:opacity-40"
150
182
  style={{ fontFamily: 'inherit' }}
151
183
  >
152
184
  {running ? '...' : 'Run'}
153
185
  </button>
154
186
  )}
155
187
  {agent.isOrchestrator && (
156
- <span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-amber-400/80 bg-amber-400/[0.08] px-2 py-0.5 rounded-[6px]">
157
- orch
188
+ <span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-amber-400/80 bg-amber-400/[0.08] px-2 py-0.5 rounded-[6px] flex items-center gap-1">
189
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M16 3h5v5"/><path d="M21 3l-7 7"/><path d="M8 21H3v-5"/><path d="M3 21l7-7"/></svg>
190
+ delegates
158
191
  </span>
159
192
  )}
160
193
  </div>
@@ -168,19 +201,19 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
168
201
  )}
169
202
  </div>
170
203
  <div className="flex items-center gap-3 mt-1.5 text-[11px] text-text-3/50">
171
- {(agent as any).lastUsedAt ? (
204
+ {agent.lastUsedAt ? (
172
205
  <span>Last used: {(() => {
173
- const days = Math.floor((Date.now() - (agent as any).lastUsedAt) / 86400000)
206
+ const days = Math.floor((Date.now() - agent.lastUsedAt) / 86400000)
174
207
  return days === 0 ? 'today' : `${days}d ago`
175
208
  })()}</span>
176
- ) : (agent as any).updatedAt ? (
209
+ ) : agent.updatedAt ? (
177
210
  <span>Updated: {(() => {
178
211
  const days = Math.floor((Date.now() - agent.updatedAt) / 86400000)
179
212
  return days === 0 ? 'today' : `${days}d ago`
180
213
  })()}</span>
181
214
  ) : null}
182
- {(agent as any).totalCost != null && (agent as any).totalCost > 0 && (
183
- <span>Cost: ${((agent as any).totalCost as number).toFixed(2)}</span>
215
+ {agent.totalCost != null && agent.totalCost > 0 && (
216
+ <span>Cost: ${agent.totalCost.toFixed(2)}</span>
184
217
  )}
185
218
  </div>
186
219
  </div>
@@ -214,7 +247,7 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
214
247
  <button
215
248
  onClick={handleConfirmRun}
216
249
  disabled={!taskInput.trim()}
217
- className="px-4 py-2 rounded-[10px] border-none bg-[#6366F1] text-white text-[13px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
250
+ className="px-4 py-2 rounded-[10px] border-none bg-accent-bright text-white text-[13px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
218
251
  style={{ fontFamily: 'inherit' }}
219
252
  >
220
253
  Run