@swarmclawai/swarmclaw 0.2.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 (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,491 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useMemo } from 'react'
4
+ import type { Session } from '@/types'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import { useChatStore } from '@/stores/use-chat-store'
7
+ import { IconButton } from '@/components/shared/icon-button'
8
+ import { UsageBadge } from '@/components/shared/usage-badge'
9
+ import { ChatToolToggles } from './chat-tool-toggles'
10
+ import { api } from '@/lib/api-client'
11
+ import {
12
+ ConnectorPlatformIcon,
13
+ CONNECTOR_PLATFORM_META,
14
+ getSessionConnector,
15
+ } from '@/components/shared/connector-platform-icon'
16
+
17
+ function shortPath(p: string): string {
18
+ return (p || '').replace(/^\/Users\/\w+/, '~')
19
+ }
20
+
21
+ const PROVIDER_LABELS: Record<string, string> = {
22
+ 'claude-cli': 'CLI',
23
+ openai: 'OpenAI',
24
+ ollama: 'Ollama',
25
+ anthropic: 'Anthropic',
26
+ }
27
+
28
+ interface Props {
29
+ session: Session
30
+ streaming: boolean
31
+ onStop: () => void
32
+ onMenuToggle: () => void
33
+ onBack?: () => void
34
+ mobile?: boolean
35
+ browserActive?: boolean
36
+ onStopBrowser?: () => void
37
+ }
38
+
39
+ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser }: Props) {
40
+ const ttsEnabled = useChatStore((s) => s.ttsEnabled)
41
+ const toggleTts = useChatStore((s) => s.toggleTts)
42
+ const debugOpen = useChatStore((s) => s.debugOpen)
43
+ const setDebugOpen = useChatStore((s) => s.setDebugOpen)
44
+ const lastUsage = useChatStore((s) => s.lastUsage)
45
+ const agents = useAppStore((s) => s.agents)
46
+ const tasks = useAppStore((s) => s.tasks)
47
+ const setActiveView = useAppStore((s) => s.setActiveView)
48
+ const setMemoryAgentFilter = useAppStore((s) => s.setMemoryAgentFilter)
49
+ const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
50
+ const appSettings = useAppStore((s) => s.appSettings)
51
+ const loadSessions = useAppStore((s) => s.loadSessions)
52
+ const connectors = useAppStore((s) => s.connectors)
53
+ const loadConnectors = useAppStore((s) => s.loadConnectors)
54
+ const providerLabel = PROVIDER_LABELS[session.provider] || session.provider
55
+ const agent = session.agentId ? agents[session.agentId] : null
56
+ const connector = getSessionConnector(session, connectors)
57
+ const connectorMeta = connector ? CONNECTOR_PLATFORM_META[connector.platform] : null
58
+ const modelName = session.model || agent?.model || ''
59
+ const [copied, setCopied] = useState(false)
60
+ const [heartbeatSaving, setHeartbeatSaving] = useState(false)
61
+ const [mainLoopSaving, setMainLoopSaving] = useState(false)
62
+ const [mainLoopError, setMainLoopError] = useState('')
63
+ const [mainLoopNotice, setMainLoopNotice] = useState('')
64
+
65
+ // Find linked task for this session
66
+ const linkedTask = useMemo(() => {
67
+ return Object.values(tasks).find((t) => t.sessionId === session.id)
68
+ }, [tasks, session.id])
69
+
70
+ const resumeHandle = useMemo(() => {
71
+ const fromSessionClaude = session.claudeSessionId
72
+ ? { label: 'Claude', id: session.claudeSessionId, command: `claude --resume ${session.claudeSessionId}` }
73
+ : null
74
+ const fromSessionCodex = session.codexThreadId
75
+ ? { label: 'Codex', id: session.codexThreadId, command: `codex exec resume ${session.codexThreadId}` }
76
+ : null
77
+ const fromSessionOpenCode = session.opencodeSessionId
78
+ ? { label: 'OpenCode', id: session.opencodeSessionId, command: `opencode run \"<task>\" --session ${session.opencodeSessionId}` }
79
+ : null
80
+ const fromDelegateClaude = session.delegateResumeIds?.claudeCode
81
+ ? { label: 'Claude', id: session.delegateResumeIds.claudeCode, command: `claude --resume ${session.delegateResumeIds.claudeCode}` }
82
+ : null
83
+ const fromDelegateCodex = session.delegateResumeIds?.codex
84
+ ? { label: 'Codex', id: session.delegateResumeIds.codex, command: `codex exec resume ${session.delegateResumeIds.codex}` }
85
+ : null
86
+ const fromDelegateOpenCode = session.delegateResumeIds?.opencode
87
+ ? { label: 'OpenCode', id: session.delegateResumeIds.opencode, command: `opencode run \"<task>\" --session ${session.delegateResumeIds.opencode}` }
88
+ : null
89
+ return fromSessionClaude
90
+ || fromSessionCodex
91
+ || fromSessionOpenCode
92
+ || fromDelegateClaude
93
+ || fromDelegateCodex
94
+ || fromDelegateOpenCode
95
+ || null
96
+ }, [session.claudeSessionId, session.codexThreadId, session.opencodeSessionId, session.delegateResumeIds])
97
+
98
+ const handleCopySessionId = () => {
99
+ if (!resumeHandle) return
100
+ navigator.clipboard.writeText(resumeHandle.command)
101
+ setCopied(true)
102
+ setTimeout(() => setCopied(false), 2000)
103
+ }
104
+
105
+ const heartbeatEnabled = session.heartbeatEnabled !== false
106
+ const heartbeatSupported = (session.tools?.length ?? 0) > 0
107
+ const loopIsOngoing = appSettings.loopMode === 'ongoing'
108
+ const heartbeatIntervalRaw = session.heartbeatIntervalSec ?? appSettings.heartbeatIntervalSec ?? 120
109
+ const heartbeatIntervalSec = Number.isFinite(Number(heartbeatIntervalRaw)) ? Math.max(0, Math.trunc(Number(heartbeatIntervalRaw))) : 120
110
+ const isMainSession = session.name === '__main__'
111
+ const missionState = session.mainLoopState || {}
112
+ const missionPaused = missionState.paused === true
113
+ const missionMode = missionState.autonomyMode === 'assist' ? 'assist' : 'autonomous'
114
+ const missionStatus = missionState.status || 'idle'
115
+ const missionMomentum = typeof missionState.momentumScore === 'number' ? missionState.momentumScore : null
116
+ const missionEventsCount = missionState.pendingEvents?.length || 0
117
+
118
+ const handleToggleHeartbeat = async () => {
119
+ if (!heartbeatSupported || heartbeatSaving) return
120
+ setHeartbeatSaving(true)
121
+ try {
122
+ await api('PUT', `/sessions/${session.id}`, { heartbeatEnabled: !heartbeatEnabled })
123
+ await loadSessions()
124
+ } finally {
125
+ setHeartbeatSaving(false)
126
+ }
127
+ }
128
+
129
+ const handleCycleHeartbeatInterval = async () => {
130
+ if (!heartbeatSupported || heartbeatSaving) return
131
+ const presets = [30, 60, 120, 300, 600]
132
+ const current = heartbeatIntervalSec
133
+ const idx = presets.indexOf(current)
134
+ const next = idx === -1 ? 120 : presets[(idx + 1) % presets.length]
135
+ setHeartbeatSaving(true)
136
+ try {
137
+ await api('PUT', `/sessions/${session.id}`, { heartbeatIntervalSec: next, heartbeatEnabled: true })
138
+ await loadSessions()
139
+ } finally {
140
+ setHeartbeatSaving(false)
141
+ }
142
+ }
143
+
144
+ const postMainLoopAction = async (action: string, extra?: Record<string, unknown>) => {
145
+ if (!isMainSession || mainLoopSaving) return
146
+ setMainLoopSaving(true)
147
+ try {
148
+ const result = await api<{ runId?: string; deduped?: boolean }>('POST', `/sessions/${session.id}/main-loop`, {
149
+ action,
150
+ ...(extra || {}),
151
+ })
152
+ setMainLoopError('')
153
+ if (action === 'nudge') {
154
+ setMainLoopNotice(result?.deduped ? 'Nudge already queued.' : 'Nudge queued.')
155
+ } else if (action === 'set_mode') {
156
+ setMainLoopNotice(`Mode set to ${extra?.mode === 'assist' ? 'Assist' : 'Auto'}.`)
157
+ } else {
158
+ setMainLoopNotice('')
159
+ }
160
+ await loadSessions()
161
+ } catch (err: unknown) {
162
+ const message = err instanceof Error ? err.message : 'Failed to update mission controls.'
163
+ setMainLoopError(message)
164
+ } finally {
165
+ setMainLoopSaving(false)
166
+ }
167
+ }
168
+
169
+ const handleToggleMissionPause = () => {
170
+ void postMainLoopAction(missionPaused ? 'resume' : 'pause')
171
+ }
172
+
173
+ const handleToggleMissionMode = () => {
174
+ const nextMode = missionMode === 'autonomous' ? 'assist' : 'autonomous'
175
+ void postMainLoopAction('set_mode', { mode: nextMode })
176
+ }
177
+
178
+ const handleNudgeMission = () => {
179
+ void postMainLoopAction('nudge')
180
+ }
181
+
182
+ const handleSetMissionGoal = () => {
183
+ if (!isMainSession) return
184
+ const seededGoal = typeof missionState.goal === 'string' ? missionState.goal : ''
185
+ const raw = window.prompt('Set mission goal', seededGoal)
186
+ const goal = (raw || '').trim()
187
+ if (!goal) return
188
+ void postMainLoopAction('set_goal', { goal })
189
+ }
190
+
191
+ const handleClearMissionEvents = () => {
192
+ if (!isMainSession || missionEventsCount <= 0) return
193
+ void postMainLoopAction('clear_events')
194
+ }
195
+
196
+ useEffect(() => {
197
+ if (session.name.startsWith('connector:')) {
198
+ void loadConnectors()
199
+ }
200
+ }, [session.name, loadConnectors])
201
+
202
+ useEffect(() => {
203
+ setMainLoopError('')
204
+ setMainLoopNotice('')
205
+ }, [session.id])
206
+
207
+ useEffect(() => {
208
+ if (!mainLoopNotice) return
209
+ const timer = setTimeout(() => setMainLoopNotice(''), 2500)
210
+ return () => clearTimeout(timer)
211
+ }, [mainLoopNotice])
212
+
213
+ return (
214
+ <header className="relative z-20 flex flex-col border-b border-white/[0.04] bg-bg/80 backdrop-blur-md shrink-0"
215
+ style={mobile ? { paddingTop: 'max(12px, env(safe-area-inset-top))' } : undefined}>
216
+ <div className="flex items-center gap-3 px-5 py-3 min-h-[56px]">
217
+ {onBack && (
218
+ <IconButton onClick={onBack} aria-label="Go back">
219
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
220
+ <polyline points="15 18 9 12 15 6" />
221
+ </svg>
222
+ </IconButton>
223
+ )}
224
+ <div className="flex-1 min-w-0">
225
+ <div className="flex items-center gap-2.5">
226
+ <span className="font-display text-[16px] font-600 block truncate tracking-[-0.02em]">{
227
+ session.name === '__main__' ? 'Main Chat'
228
+ : session.name.startsWith('agent-thread:') ? (agent?.name || session.name)
229
+ : session.name
230
+ }</span>
231
+ {connector && connectorMeta && (
232
+ <span
233
+ className="shrink-0 inline-flex items-center gap-1.5 px-2 py-0.5 rounded-[7px] border text-[10px] font-700 uppercase tracking-wider"
234
+ style={{
235
+ color: connectorMeta.color,
236
+ backgroundColor: `${connectorMeta.color}1A`,
237
+ borderColor: `${connectorMeta.color}33`,
238
+ }}
239
+ title={`${connector.name} connector`}
240
+ >
241
+ <ConnectorPlatformIcon platform={connector.platform} size={11} />
242
+ {connectorMeta.label}
243
+ </span>
244
+ )}
245
+ {session.provider && session.provider !== 'claude-cli' && (
246
+ <span className="shrink-0 px-2.5 py-0.5 rounded-[7px] bg-accent-soft text-accent-bright text-[10px] font-700 uppercase tracking-wider">
247
+ {providerLabel}
248
+ </span>
249
+ )}
250
+ {agent?.isOrchestrator && (
251
+ <span className="shrink-0 px-2.5 py-0.5 rounded-[7px] bg-[#F59E0B]/10 text-[#F59E0B] text-[10px] font-700 uppercase tracking-wider">
252
+ Orchestrator
253
+ </span>
254
+ )}
255
+ {session.tools?.length ? (
256
+ <span className="shrink-0 px-2.5 py-0.5 rounded-[7px] bg-emerald-500/10 text-emerald-400 text-[10px] font-700 uppercase tracking-wider">
257
+ Tools
258
+ </span>
259
+ ) : null}
260
+ {streaming && (
261
+ <span className="shrink-0 w-2 h-2 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
262
+ )}
263
+ </div>
264
+ <div className="flex items-center gap-2 mt-0.5">
265
+ <span className="text-[11px] text-text-3/60 font-mono block truncate">{shortPath(session.cwd)}</span>
266
+ {modelName && (
267
+ <>
268
+ <span className="text-[11px] text-text-3/60">·</span>
269
+ <span className="text-[11px] text-text-3/50 font-mono truncate shrink-0">{modelName}</span>
270
+ </>
271
+ )}
272
+ {lastUsage && !streaming && (
273
+ <>
274
+ <span className="text-[11px] text-text-3/60">·</span>
275
+ <UsageBadge {...lastUsage} />
276
+ </>
277
+ )}
278
+ </div>
279
+ </div>
280
+ <div className="flex gap-1.5">
281
+ {streaming && (
282
+ <IconButton onClick={onStop} variant="danger" aria-label="Stop generation">
283
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
284
+ <rect x="6" y="6" width="12" height="12" rx="2" />
285
+ </svg>
286
+ </IconButton>
287
+ )}
288
+ <IconButton onClick={() => setDebugOpen(!debugOpen)} active={debugOpen} aria-label="Toggle debug panel">
289
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
290
+ <path d="M12 20V10" />
291
+ <path d="M18 20V4" />
292
+ <path d="M6 20v-4" />
293
+ </svg>
294
+ </IconButton>
295
+ <IconButton onClick={toggleTts} active={ttsEnabled} aria-label="Toggle text-to-speech">
296
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
297
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
298
+ <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
299
+ </svg>
300
+ </IconButton>
301
+ <IconButton onClick={(e) => { e.stopPropagation(); onMenuToggle() }} aria-label="Session menu">
302
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
303
+ <circle cx="12" cy="6" r="1" />
304
+ <circle cx="12" cy="12" r="1" />
305
+ <circle cx="12" cy="18" r="1" />
306
+ </svg>
307
+ </IconButton>
308
+ </div>
309
+ </div>
310
+
311
+ {/* Sub-bar: tools toggle + agent memories + task link + CLI session ID + browser */}
312
+ {(agent || linkedTask || resumeHandle || browserActive || session.tools?.length || isMainSession) && (
313
+ <div className="flex items-center gap-3 px-5 pb-2.5 -mt-1">
314
+ {(((agent?.tools?.length ?? 0) > 0) || ((session.tools?.length ?? 0) > 0)) && (
315
+ <ChatToolToggles session={session} />
316
+ )}
317
+ {heartbeatSupported && (
318
+ <>
319
+ <button
320
+ onClick={handleToggleHeartbeat}
321
+ disabled={heartbeatSaving}
322
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] transition-colors cursor-pointer border-none
323
+ ${heartbeatEnabled ? 'bg-emerald-500/10 hover:bg-emerald-500/15 text-emerald-400' : 'bg-white/[0.04] hover:bg-white/[0.07] text-text-3'}`}
324
+ title={loopIsOngoing ? 'Toggle heartbeat for this session' : 'Global loop mode is bounded; heartbeats are paused'}
325
+ >
326
+ <span className={`w-1.5 h-1.5 rounded-full ${heartbeatEnabled ? 'bg-emerald-400' : 'bg-text-3/40'}`} />
327
+ <span className="text-[11px] font-600">
328
+ HB {heartbeatEnabled ? 'On' : 'Off'}
329
+ </span>
330
+ {!loopIsOngoing && (
331
+ <span className="text-[10px] text-text-3/50">(bounded)</span>
332
+ )}
333
+ </button>
334
+ <button
335
+ onClick={handleCycleHeartbeatInterval}
336
+ disabled={heartbeatSaving}
337
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] text-text-3 transition-colors cursor-pointer border-none"
338
+ title="Cycle heartbeat interval for this session"
339
+ >
340
+ <span className="text-[11px] font-600">{heartbeatIntervalSec}s</span>
341
+ </button>
342
+ </>
343
+ )}
344
+ {isMainSession && (
345
+ <>
346
+ <button
347
+ onClick={handleToggleMissionPause}
348
+ disabled={mainLoopSaving}
349
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] transition-colors cursor-pointer border-none
350
+ ${missionPaused ? 'bg-amber-500/12 hover:bg-amber-500/20 text-amber-300' : 'bg-emerald-500/10 hover:bg-emerald-500/15 text-emerald-400'}`}
351
+ title={missionPaused ? 'Resume autonomous mission loop' : 'Pause autonomous mission loop'}
352
+ >
353
+ <span className={`w-1.5 h-1.5 rounded-full ${missionPaused ? 'bg-amber-300' : 'bg-emerald-400'}`} />
354
+ <span className="text-[11px] font-600">
355
+ Mission {missionPaused ? 'Paused' : 'Live'}
356
+ </span>
357
+ </button>
358
+ <button
359
+ onClick={handleToggleMissionMode}
360
+ disabled={mainLoopSaving}
361
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] transition-colors cursor-pointer border-none
362
+ ${missionMode === 'autonomous'
363
+ ? 'bg-indigo-500/15 hover:bg-indigo-500/25 text-indigo-200'
364
+ : 'bg-white/[0.04] hover:bg-white/[0.07] text-text-3'
365
+ }`}
366
+ title="Toggle mission autonomy mode"
367
+ >
368
+ <span className="text-[11px] font-600">
369
+ Mode {missionMode === 'autonomous' ? 'Auto' : 'Assist'}
370
+ </span>
371
+ </button>
372
+ <button
373
+ onClick={handleNudgeMission}
374
+ disabled={mainLoopSaving || missionPaused}
375
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-[#3B82F6]/10 hover:bg-[#3B82F6]/18 text-[#60A5FA] transition-colors cursor-pointer border-none disabled:opacity-60"
376
+ title="Run one immediate main-loop mission tick"
377
+ >
378
+ <span className="text-[11px] font-600">Nudge</span>
379
+ </button>
380
+ <button
381
+ onClick={handleSetMissionGoal}
382
+ disabled={mainLoopSaving}
383
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-fuchsia-500/10 hover:bg-fuchsia-500/18 text-fuchsia-300 transition-colors cursor-pointer border-none"
384
+ title="Set an explicit mission goal"
385
+ >
386
+ <span className="text-[11px] font-600">Set Goal</span>
387
+ </button>
388
+ {missionEventsCount > 0 && (
389
+ <button
390
+ onClick={handleClearMissionEvents}
391
+ disabled={mainLoopSaving}
392
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] text-text-3 transition-colors cursor-pointer border-none"
393
+ title="Clear pending mission events"
394
+ >
395
+ <span className="text-[11px] font-600">Events {missionEventsCount}</span>
396
+ </button>
397
+ )}
398
+ <span className="text-[10px] text-text-3/50 uppercase tracking-wider">
399
+ {`State ${missionStatus}${missionMomentum !== null ? ` · ${missionMomentum}` : ''}`}
400
+ </span>
401
+ {mainLoopError && (
402
+ <span className="text-[10px] text-red-300/90 truncate max-w-[280px]" title={mainLoopError}>
403
+ {mainLoopError}
404
+ </span>
405
+ )}
406
+ {mainLoopNotice && (
407
+ <span className="text-[10px] text-emerald-300/90 truncate max-w-[220px]" title={mainLoopNotice}>
408
+ {mainLoopNotice}
409
+ </span>
410
+ )}
411
+ </>
412
+ )}
413
+ {agent && session.tools?.includes('memory') && (
414
+ <button
415
+ onClick={() => {
416
+ setMemoryAgentFilter(session.agentId!)
417
+ setActiveView('memory')
418
+ setSidebarOpen(true)
419
+ }}
420
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-accent-soft/50 hover:bg-accent-soft transition-colors cursor-pointer"
421
+ >
422
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-accent-bright/60">
423
+ <ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" /><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
424
+ </svg>
425
+ <span className="text-[11px] font-600 text-accent-bright/60">
426
+ {agent.name} Memories
427
+ </span>
428
+ </button>
429
+ )}
430
+ {linkedTask && (
431
+ <button
432
+ onClick={() => setActiveView('tasks')}
433
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-[#F59E0B]/10 hover:bg-[#F59E0B]/15 transition-colors cursor-pointer"
434
+ >
435
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" strokeWidth="2.5" strokeLinecap="round">
436
+ <path d="M9 11l3 3L22 4" />
437
+ <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
438
+ </svg>
439
+ <span className="text-[11px] font-600 text-[#F59E0B] truncate max-w-[200px]">
440
+ Task: {linkedTask.title}
441
+ </span>
442
+ </button>
443
+ )}
444
+ {resumeHandle && (
445
+ <button
446
+ onClick={handleCopySessionId}
447
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] transition-colors cursor-pointer group"
448
+ title="Copy resume handle/command to clipboard"
449
+ >
450
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/50">
451
+ <path d="M4 17l6 0l0 -6" />
452
+ <path d="M20 7l-6 0l0 6" />
453
+ <path d="M4 17l10 -10" />
454
+ </svg>
455
+ <span className="text-[11px] font-mono text-text-3/50 group-hover:text-text-3/70 truncate max-w-[220px]">
456
+ {copied ? 'Copied!' : `${resumeHandle.label}: ${resumeHandle.id}`}
457
+ </span>
458
+ {!copied && (
459
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/60 shrink-0">
460
+ <rect x="9" y="9" width="13" height="13" rx="2" />
461
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
462
+ </svg>
463
+ )}
464
+ </button>
465
+ )}
466
+ {browserActive && (
467
+ <button
468
+ onClick={onStopBrowser}
469
+ className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-[#3B82F6]/10 hover:bg-[#F43F5E]/15 transition-colors cursor-pointer group"
470
+ title="Stop browser session"
471
+ >
472
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-[#3B82F6] group-hover:text-[#F43F5E]">
473
+ <rect x="3" y="3" width="18" height="14" rx="2" />
474
+ <path d="M3 9h18" />
475
+ <circle cx="7" cy="6" r="0.5" fill="currentColor" />
476
+ <circle cx="10" cy="6" r="0.5" fill="currentColor" />
477
+ </svg>
478
+ <span className="text-[11px] font-600 text-[#3B82F6] group-hover:text-[#F43F5E]">
479
+ Browser Active
480
+ </span>
481
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-text-3/60 group-hover:text-[#F43F5E] shrink-0">
482
+ <line x1="18" y1="6" x2="6" y2="18" />
483
+ <line x1="6" y1="6" x2="18" y2="18" />
484
+ </svg>
485
+ </button>
486
+ )}
487
+ </div>
488
+ )}
489
+ </header>
490
+ )
491
+ }
@@ -0,0 +1,161 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useEffect } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { api } from '@/lib/api-client'
6
+ import type { Session } from '@/types'
7
+
8
+ const TOOL_GROUPS: { label: string; tools: Record<string, string> }[] = [
9
+ {
10
+ label: 'Tools',
11
+ tools: {
12
+ shell: 'Shell',
13
+ files: 'Files',
14
+ edit_file: 'Edit File',
15
+ process: 'Process',
16
+ web_search: 'Web Search',
17
+ web_fetch: 'Web Fetch',
18
+ browser: 'Browser',
19
+ memory: 'Memory',
20
+ },
21
+ },
22
+ {
23
+ label: 'Delegation',
24
+ tools: {
25
+ claude_code: 'Claude Code',
26
+ codex_cli: 'Codex CLI',
27
+ opencode_cli: 'OpenCode CLI',
28
+ },
29
+ },
30
+ {
31
+ label: 'Platform',
32
+ tools: {
33
+ orchestrator: 'Orchestrator',
34
+ manage_agents: 'Agents',
35
+ manage_tasks: 'Tasks',
36
+ manage_schedules: 'Schedules',
37
+ manage_skills: 'Skills',
38
+ manage_documents: 'Documents',
39
+ manage_webhooks: 'Webhooks',
40
+ manage_connectors: 'Connectors',
41
+ manage_sessions: 'Sessions',
42
+ manage_secrets: 'Secrets',
43
+ },
44
+ },
45
+ ]
46
+
47
+ // Flat lookup for display names
48
+ const ALL_TOOLS: Record<string, string> = {}
49
+ for (const g of TOOL_GROUPS) Object.assign(ALL_TOOLS, g.tools)
50
+
51
+ const TOOL_HINTS: Record<string, string> = {
52
+ orchestrator: 'Can delegate tasks to other agents',
53
+ }
54
+
55
+ interface Props {
56
+ session: Session
57
+ }
58
+
59
+ export function ChatToolToggles({ session }: Props) {
60
+ const [open, setOpen] = useState(false)
61
+ const ref = useRef<HTMLDivElement>(null)
62
+ const loadSessions = useAppStore((s) => s.loadSessions)
63
+ const agents = useAppStore((s) => s.agents)
64
+ const skills = useAppStore((s) => s.skills)
65
+
66
+ const agent = session.agentId ? agents[session.agentId] : null
67
+ const sessionTools: string[] = session.tools || []
68
+
69
+ // Agent's skill IDs
70
+ const agentSkillIds: string[] = agent?.skillIds || []
71
+
72
+ useEffect(() => {
73
+ if (!open) return
74
+ const handler = (e: MouseEvent) => {
75
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
76
+ }
77
+ document.addEventListener('mousedown', handler)
78
+ return () => document.removeEventListener('mousedown', handler)
79
+ }, [open])
80
+
81
+ const toggleTool = async (toolId: string) => {
82
+ const updated = sessionTools.includes(toolId)
83
+ ? sessionTools.filter((t) => t !== toolId)
84
+ : [...sessionTools, toolId]
85
+ await api('PUT', `/sessions/${session.id}`, { tools: updated })
86
+ loadSessions()
87
+ }
88
+
89
+ const enabledCount = sessionTools.length
90
+ const totalCount = Object.keys(ALL_TOOLS).length
91
+
92
+ return (
93
+ <div className="relative" ref={ref}>
94
+ <button
95
+ onClick={() => setOpen(!open)}
96
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] transition-colors cursor-pointer border-none
97
+ ${open ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.04] text-text-3 hover:bg-white/[0.07]'}`}
98
+ >
99
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
100
+ <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
101
+ </svg>
102
+ <span className="text-[11px] font-600">
103
+ {enabledCount}/{totalCount}
104
+ </span>
105
+ </button>
106
+
107
+ {open && (
108
+ <div className="absolute top-full left-0 mt-1.5 w-[260px] max-h-[420px] overflow-y-auto rounded-[12px] border border-white/[0.08] shadow-xl z-[120] overflow-hidden"
109
+ style={{ animation: 'fade-in 0.15s ease', backgroundColor: '#171a2b' }}>
110
+
111
+ {TOOL_GROUPS.map((group, gi) => (
112
+ <div key={group.label} className={`px-3 pb-1 ${gi === 0 ? 'pt-3' : 'pt-1 border-t border-white/[0.04]'}`}>
113
+ <p className="text-[10px] font-600 text-text-3/60 uppercase tracking-wider mb-2">{group.label}</p>
114
+ {Object.entries(group.tools).map(([toolId, label]) => {
115
+ const enabled = sessionTools.includes(toolId)
116
+ return (
117
+ <label key={toolId} className="flex items-center gap-2.5 py-1.5 cursor-pointer">
118
+ <div
119
+ onClick={() => toggleTool(toolId)}
120
+ className={`w-8 h-[18px] rounded-full transition-all duration-200 relative cursor-pointer shrink-0
121
+ ${enabled ? 'bg-[#6366F1]' : 'bg-white/[0.12]'}`}
122
+ >
123
+ <div className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white transition-all duration-200
124
+ ${enabled ? 'left-[16px]' : 'left-[2px]'}`} />
125
+ </div>
126
+ <span className={`text-[12px] ${enabled ? 'text-text-2' : 'text-text-3/70'}`}>
127
+ {label}
128
+ {TOOL_HINTS[toolId] && (
129
+ <span className="ml-2 text-[10px] text-text-3/70 font-400">{TOOL_HINTS[toolId]}</span>
130
+ )}
131
+ </span>
132
+ </label>
133
+ )
134
+ })}
135
+ </div>
136
+ ))}
137
+
138
+ {agentSkillIds.length > 0 && (
139
+ <div className="px-3 pb-2 pt-1 border-t border-white/[0.04]">
140
+ <p className="text-[10px] font-600 text-text-3/60 uppercase tracking-wider mb-2">Skills</p>
141
+ {agentSkillIds.map((skillId) => {
142
+ const skill = skills[skillId]
143
+ if (!skill) return null
144
+ return (
145
+ <div key={skillId} className="flex items-center gap-2.5 py-1.5">
146
+ <span className="w-1.5 h-1.5 rounded-full bg-accent-bright/40 shrink-0" />
147
+ <span className="text-[12px] text-text-2 truncate">{skill.name}</span>
148
+ </div>
149
+ )
150
+ })}
151
+ </div>
152
+ )}
153
+
154
+ <div className="px-3 py-2 border-t border-white/[0.04] bg-white/[0.02]">
155
+ <p className="text-[10px] text-text-3/70">Changes apply to the next message</p>
156
+ </div>
157
+ </div>
158
+ )}
159
+ </div>
160
+ )
161
+ }