slackhive 0.1.40 → 0.1.42

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 (534) hide show
  1. package/cli/dist/index.js +15786 -52
  2. package/package.json +9 -3
  3. package/.dockerignore +0 -14
  4. package/.env.example +0 -44
  5. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -65
  6. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  7. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -38
  8. package/.github/PULL_REQUEST_TEMPLATE.md +0 -27
  9. package/.github/dependabot.yml +0 -20
  10. package/.github/workflows/audit.yml +0 -149
  11. package/.github/workflows/ci.yml +0 -135
  12. package/CHANGELOG.md +0 -52
  13. package/CODE_OF_CONDUCT.md +0 -37
  14. package/CONTRIBUTING.md +0 -204
  15. package/SECURITY.md +0 -47
  16. package/apps/runner/Dockerfile +0 -33
  17. package/apps/runner/dist/__tests__/channel-restrictions.test.d.ts +0 -8
  18. package/apps/runner/dist/__tests__/channel-restrictions.test.js +0 -63
  19. package/apps/runner/dist/__tests__/channel-restrictions.test.js.map +0 -1
  20. package/apps/runner/dist/__tests__/claude-handler-resolve.test.d.ts +0 -20
  21. package/apps/runner/dist/__tests__/claude-handler-resolve.test.js +0 -178
  22. package/apps/runner/dist/__tests__/claude-handler-resolve.test.js.map +0 -1
  23. package/apps/runner/dist/__tests__/compile-claude-md.test.d.ts +0 -13
  24. package/apps/runner/dist/__tests__/compile-claude-md.test.js +0 -144
  25. package/apps/runner/dist/__tests__/compile-claude-md.test.js.map +0 -1
  26. package/apps/runner/dist/__tests__/memory-sync.test.d.ts +0 -11
  27. package/apps/runner/dist/__tests__/memory-sync.test.js +0 -56
  28. package/apps/runner/dist/__tests__/memory-sync.test.js.map +0 -1
  29. package/apps/runner/dist/__tests__/slack-file-support.test.d.ts +0 -9
  30. package/apps/runner/dist/__tests__/slack-file-support.test.js +0 -271
  31. package/apps/runner/dist/__tests__/slack-file-support.test.js.map +0 -1
  32. package/apps/runner/dist/__tests__/slack-formatting.test.d.ts +0 -12
  33. package/apps/runner/dist/__tests__/slack-formatting.test.js +0 -400
  34. package/apps/runner/dist/__tests__/slack-formatting.test.js.map +0 -1
  35. package/apps/runner/dist/__tests__/thread-context.test.d.ts +0 -12
  36. package/apps/runner/dist/__tests__/thread-context.test.js +0 -182
  37. package/apps/runner/dist/__tests__/thread-context.test.js.map +0 -1
  38. package/apps/runner/dist/agent-runner.d.ts +0 -118
  39. package/apps/runner/dist/agent-runner.js +0 -352
  40. package/apps/runner/dist/agent-runner.js.map +0 -1
  41. package/apps/runner/dist/claude-handler.d.ts +0 -122
  42. package/apps/runner/dist/claude-handler.js +0 -402
  43. package/apps/runner/dist/claude-handler.js.map +0 -1
  44. package/apps/runner/dist/compile-claude-md.d.ts +0 -59
  45. package/apps/runner/dist/compile-claude-md.js +0 -291
  46. package/apps/runner/dist/compile-claude-md.js.map +0 -1
  47. package/apps/runner/dist/correction-handler.d.ts +0 -46
  48. package/apps/runner/dist/correction-handler.js +0 -162
  49. package/apps/runner/dist/correction-handler.js.map +0 -1
  50. package/apps/runner/dist/correction-manager.d.ts +0 -53
  51. package/apps/runner/dist/correction-manager.js +0 -241
  52. package/apps/runner/dist/correction-manager.js.map +0 -1
  53. package/apps/runner/dist/db.d.ts +0 -193
  54. package/apps/runner/dist/db.js +0 -492
  55. package/apps/runner/dist/db.js.map +0 -1
  56. package/apps/runner/dist/index.d.ts +0 -9
  57. package/apps/runner/dist/index.js +0 -43
  58. package/apps/runner/dist/index.js.map +0 -1
  59. package/apps/runner/dist/job-scheduler.d.ts +0 -57
  60. package/apps/runner/dist/job-scheduler.js +0 -150
  61. package/apps/runner/dist/job-scheduler.js.map +0 -1
  62. package/apps/runner/dist/logger.d.ts +0 -32
  63. package/apps/runner/dist/logger.js +0 -52
  64. package/apps/runner/dist/logger.js.map +0 -1
  65. package/apps/runner/dist/mcp-process-manager.d.ts +0 -38
  66. package/apps/runner/dist/mcp-process-manager.js +0 -189
  67. package/apps/runner/dist/mcp-process-manager.js.map +0 -1
  68. package/apps/runner/dist/memory-mcp.d.ts +0 -14
  69. package/apps/runner/dist/memory-mcp.js +0 -88
  70. package/apps/runner/dist/memory-mcp.js.map +0 -1
  71. package/apps/runner/dist/memory-watcher.d.ts +0 -78
  72. package/apps/runner/dist/memory-watcher.js +0 -220
  73. package/apps/runner/dist/memory-watcher.js.map +0 -1
  74. package/apps/runner/dist/slack-handler.d.ts +0 -120
  75. package/apps/runner/dist/slack-handler.js +0 -843
  76. package/apps/runner/dist/slack-handler.js.map +0 -1
  77. package/apps/runner/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  78. package/apps/runner/package.json +0 -42
  79. package/apps/runner/src/__tests__/channel-restrictions.test.ts +0 -75
  80. package/apps/runner/src/__tests__/claude-handler-resolve.test.ts +0 -160
  81. package/apps/runner/src/__tests__/compile-claude-md.test.ts +0 -139
  82. package/apps/runner/src/__tests__/memory-sync.test.ts +0 -59
  83. package/apps/runner/src/__tests__/slack-file-support.test.ts +0 -376
  84. package/apps/runner/src/__tests__/slack-formatting.test.ts +0 -495
  85. package/apps/runner/src/__tests__/thread-context.test.ts +0 -215
  86. package/apps/runner/src/agent-runner.ts +0 -397
  87. package/apps/runner/src/claude-handler.ts +0 -475
  88. package/apps/runner/src/compile-claude-md.ts +0 -283
  89. package/apps/runner/src/correction-handler.ts +0 -191
  90. package/apps/runner/src/correction-manager.ts +0 -285
  91. package/apps/runner/src/db.ts +0 -604
  92. package/apps/runner/src/index.ts +0 -46
  93. package/apps/runner/src/job-scheduler.ts +0 -165
  94. package/apps/runner/src/logger.ts +0 -49
  95. package/apps/runner/src/mcp-process-manager.ts +0 -195
  96. package/apps/runner/src/memory-mcp.ts +0 -85
  97. package/apps/runner/src/memory-watcher.ts +0 -215
  98. package/apps/runner/src/slack-handler.ts +0 -929
  99. package/apps/runner/tsconfig.json +0 -17
  100. package/apps/runner/vitest.config.mts +0 -17
  101. package/apps/web/.eslintrc.json +0 -3
  102. package/apps/web/.next/app-build-manifest.json +0 -323
  103. package/apps/web/.next/app-path-routes-manifest.json +0 -46
  104. package/apps/web/.next/build-manifest.json +0 -33
  105. package/apps/web/.next/cache/.previewinfo +0 -1
  106. package/apps/web/.next/cache/.rscinfo +0 -1
  107. package/apps/web/.next/cache/webpack/client-production/0.pack +0 -0
  108. package/apps/web/.next/cache/webpack/client-production/1.pack +0 -0
  109. package/apps/web/.next/cache/webpack/client-production/2.pack +0 -0
  110. package/apps/web/.next/cache/webpack/client-production/3.pack +0 -0
  111. package/apps/web/.next/cache/webpack/client-production/4.pack +0 -0
  112. package/apps/web/.next/cache/webpack/client-production/index.pack +0 -0
  113. package/apps/web/.next/cache/webpack/client-production/index.pack.old +0 -0
  114. package/apps/web/.next/cache/webpack/edge-server-production/0.pack +0 -0
  115. package/apps/web/.next/cache/webpack/edge-server-production/1.pack +0 -0
  116. package/apps/web/.next/cache/webpack/edge-server-production/index.pack +0 -0
  117. package/apps/web/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  118. package/apps/web/.next/cache/webpack/server-production/0.pack +0 -0
  119. package/apps/web/.next/cache/webpack/server-production/1.pack +0 -0
  120. package/apps/web/.next/cache/webpack/server-production/2.pack +0 -0
  121. package/apps/web/.next/cache/webpack/server-production/index.pack +0 -0
  122. package/apps/web/.next/cache/webpack/server-production/index.pack.old +0 -0
  123. package/apps/web/.next/diagnostics/build-diagnostics.json +0 -6
  124. package/apps/web/.next/diagnostics/framework.json +0 -1
  125. package/apps/web/.next/package.json +0 -1
  126. package/apps/web/.next/react-loadable-manifest.json +0 -1
  127. package/apps/web/.next/server/app/_not-found/page.js +0 -2
  128. package/apps/web/.next/server/app/_not-found/page.js.nft.json +0 -1
  129. package/apps/web/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
  130. package/apps/web/.next/server/app/agents/[slug]/page.js +0 -4
  131. package/apps/web/.next/server/app/agents/[slug]/page.js.nft.json +0 -1
  132. package/apps/web/.next/server/app/agents/[slug]/page_client-reference-manifest.js +0 -1
  133. package/apps/web/.next/server/app/agents/new/page.js +0 -2
  134. package/apps/web/.next/server/app/agents/new/page.js.nft.json +0 -1
  135. package/apps/web/.next/server/app/agents/new/page_client-reference-manifest.js +0 -1
  136. package/apps/web/.next/server/app/api/agents/[id]/access/route.js +0 -1
  137. package/apps/web/.next/server/app/api/agents/[id]/access/route.js.nft.json +0 -1
  138. package/apps/web/.next/server/app/api/agents/[id]/access/route_client-reference-manifest.js +0 -1
  139. package/apps/web/.next/server/app/api/agents/[id]/claude-md/route.js +0 -6
  140. package/apps/web/.next/server/app/api/agents/[id]/claude-md/route.js.nft.json +0 -1
  141. package/apps/web/.next/server/app/api/agents/[id]/claude-md/route_client-reference-manifest.js +0 -1
  142. package/apps/web/.next/server/app/api/agents/[id]/logs/route.js +0 -3
  143. package/apps/web/.next/server/app/api/agents/[id]/logs/route.js.nft.json +0 -1
  144. package/apps/web/.next/server/app/api/agents/[id]/logs/route_client-reference-manifest.js +0 -1
  145. package/apps/web/.next/server/app/api/agents/[id]/manifest/route.js +0 -1
  146. package/apps/web/.next/server/app/api/agents/[id]/manifest/route.js.nft.json +0 -1
  147. package/apps/web/.next/server/app/api/agents/[id]/manifest/route_client-reference-manifest.js +0 -1
  148. package/apps/web/.next/server/app/api/agents/[id]/mcps/route.js +0 -1
  149. package/apps/web/.next/server/app/api/agents/[id]/mcps/route.js.nft.json +0 -1
  150. package/apps/web/.next/server/app/api/agents/[id]/mcps/route_client-reference-manifest.js +0 -1
  151. package/apps/web/.next/server/app/api/agents/[id]/memories/[memId]/route.js +0 -1
  152. package/apps/web/.next/server/app/api/agents/[id]/memories/[memId]/route.js.nft.json +0 -1
  153. package/apps/web/.next/server/app/api/agents/[id]/memories/[memId]/route_client-reference-manifest.js +0 -1
  154. package/apps/web/.next/server/app/api/agents/[id]/memories/route.js +0 -1
  155. package/apps/web/.next/server/app/api/agents/[id]/memories/route.js.nft.json +0 -1
  156. package/apps/web/.next/server/app/api/agents/[id]/memories/route_client-reference-manifest.js +0 -1
  157. package/apps/web/.next/server/app/api/agents/[id]/permissions/route.js +0 -1
  158. package/apps/web/.next/server/app/api/agents/[id]/permissions/route.js.nft.json +0 -1
  159. package/apps/web/.next/server/app/api/agents/[id]/permissions/route_client-reference-manifest.js +0 -1
  160. package/apps/web/.next/server/app/api/agents/[id]/reload/route.js +0 -1
  161. package/apps/web/.next/server/app/api/agents/[id]/reload/route.js.nft.json +0 -1
  162. package/apps/web/.next/server/app/api/agents/[id]/reload/route_client-reference-manifest.js +0 -1
  163. package/apps/web/.next/server/app/api/agents/[id]/restrictions/route.js +0 -1
  164. package/apps/web/.next/server/app/api/agents/[id]/restrictions/route.js.nft.json +0 -1
  165. package/apps/web/.next/server/app/api/agents/[id]/restrictions/route_client-reference-manifest.js +0 -1
  166. package/apps/web/.next/server/app/api/agents/[id]/route.js +0 -33
  167. package/apps/web/.next/server/app/api/agents/[id]/route.js.nft.json +0 -1
  168. package/apps/web/.next/server/app/api/agents/[id]/route_client-reference-manifest.js +0 -1
  169. package/apps/web/.next/server/app/api/agents/[id]/skills/[skillId]/route.js +0 -1
  170. package/apps/web/.next/server/app/api/agents/[id]/skills/[skillId]/route.js.nft.json +0 -1
  171. package/apps/web/.next/server/app/api/agents/[id]/skills/[skillId]/route_client-reference-manifest.js +0 -1
  172. package/apps/web/.next/server/app/api/agents/[id]/skills/route.js +0 -1
  173. package/apps/web/.next/server/app/api/agents/[id]/skills/route.js.nft.json +0 -1
  174. package/apps/web/.next/server/app/api/agents/[id]/skills/route_client-reference-manifest.js +0 -1
  175. package/apps/web/.next/server/app/api/agents/[id]/slack-info/route.js +0 -1
  176. package/apps/web/.next/server/app/api/agents/[id]/slack-info/route.js.nft.json +0 -1
  177. package/apps/web/.next/server/app/api/agents/[id]/slack-info/route_client-reference-manifest.js +0 -1
  178. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/restore/route.js +0 -1
  179. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/restore/route.js.nft.json +0 -1
  180. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/restore/route_client-reference-manifest.js +0 -1
  181. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/route.js +0 -1
  182. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/route.js.nft.json +0 -1
  183. package/apps/web/.next/server/app/api/agents/[id]/snapshots/[sid]/route_client-reference-manifest.js +0 -1
  184. package/apps/web/.next/server/app/api/agents/[id]/snapshots/route.js +0 -1
  185. package/apps/web/.next/server/app/api/agents/[id]/snapshots/route.js.nft.json +0 -1
  186. package/apps/web/.next/server/app/api/agents/[id]/snapshots/route_client-reference-manifest.js +0 -1
  187. package/apps/web/.next/server/app/api/agents/[id]/start/route.js +0 -1
  188. package/apps/web/.next/server/app/api/agents/[id]/start/route.js.nft.json +0 -1
  189. package/apps/web/.next/server/app/api/agents/[id]/start/route_client-reference-manifest.js +0 -1
  190. package/apps/web/.next/server/app/api/agents/[id]/stop/route.js +0 -1
  191. package/apps/web/.next/server/app/api/agents/[id]/stop/route.js.nft.json +0 -1
  192. package/apps/web/.next/server/app/api/agents/[id]/stop/route_client-reference-manifest.js +0 -1
  193. package/apps/web/.next/server/app/api/agents/route.js +0 -91
  194. package/apps/web/.next/server/app/api/agents/route.js.nft.json +0 -1
  195. package/apps/web/.next/server/app/api/agents/route_client-reference-manifest.js +0 -1
  196. package/apps/web/.next/server/app/api/auth/login/route.js +0 -1
  197. package/apps/web/.next/server/app/api/auth/login/route.js.nft.json +0 -1
  198. package/apps/web/.next/server/app/api/auth/login/route_client-reference-manifest.js +0 -1
  199. package/apps/web/.next/server/app/api/auth/logout/route.js +0 -1
  200. package/apps/web/.next/server/app/api/auth/logout/route.js.nft.json +0 -1
  201. package/apps/web/.next/server/app/api/auth/logout/route_client-reference-manifest.js +0 -1
  202. package/apps/web/.next/server/app/api/auth/me/route.js +0 -1
  203. package/apps/web/.next/server/app/api/auth/me/route.js.nft.json +0 -1
  204. package/apps/web/.next/server/app/api/auth/me/route_client-reference-manifest.js +0 -1
  205. package/apps/web/.next/server/app/api/auth/users/[id]/route.js +0 -1
  206. package/apps/web/.next/server/app/api/auth/users/[id]/route.js.nft.json +0 -1
  207. package/apps/web/.next/server/app/api/auth/users/[id]/route_client-reference-manifest.js +0 -1
  208. package/apps/web/.next/server/app/api/auth/users/route.js +0 -1
  209. package/apps/web/.next/server/app/api/auth/users/route.js.nft.json +0 -1
  210. package/apps/web/.next/server/app/api/auth/users/route_client-reference-manifest.js +0 -1
  211. package/apps/web/.next/server/app/api/env-vars/[key]/route.js +0 -1
  212. package/apps/web/.next/server/app/api/env-vars/[key]/route.js.nft.json +0 -1
  213. package/apps/web/.next/server/app/api/env-vars/[key]/route_client-reference-manifest.js +0 -1
  214. package/apps/web/.next/server/app/api/env-vars/route.js +0 -1
  215. package/apps/web/.next/server/app/api/env-vars/route.js.nft.json +0 -1
  216. package/apps/web/.next/server/app/api/env-vars/route_client-reference-manifest.js +0 -1
  217. package/apps/web/.next/server/app/api/jobs/[id]/route.js +0 -1
  218. package/apps/web/.next/server/app/api/jobs/[id]/route.js.nft.json +0 -1
  219. package/apps/web/.next/server/app/api/jobs/[id]/route_client-reference-manifest.js +0 -1
  220. package/apps/web/.next/server/app/api/jobs/[id]/runs/route.js +0 -1
  221. package/apps/web/.next/server/app/api/jobs/[id]/runs/route.js.nft.json +0 -1
  222. package/apps/web/.next/server/app/api/jobs/[id]/runs/route_client-reference-manifest.js +0 -1
  223. package/apps/web/.next/server/app/api/jobs/route.js +0 -1
  224. package/apps/web/.next/server/app/api/jobs/route.js.nft.json +0 -1
  225. package/apps/web/.next/server/app/api/jobs/route_client-reference-manifest.js +0 -1
  226. package/apps/web/.next/server/app/api/mcps/[id]/route.js +0 -1
  227. package/apps/web/.next/server/app/api/mcps/[id]/route.js.nft.json +0 -1
  228. package/apps/web/.next/server/app/api/mcps/[id]/route_client-reference-manifest.js +0 -1
  229. package/apps/web/.next/server/app/api/mcps/[id]/test/route.js +0 -1
  230. package/apps/web/.next/server/app/api/mcps/[id]/test/route.js.nft.json +0 -1
  231. package/apps/web/.next/server/app/api/mcps/[id]/test/route_client-reference-manifest.js +0 -1
  232. package/apps/web/.next/server/app/api/mcps/route.js +0 -1
  233. package/apps/web/.next/server/app/api/mcps/route.js.nft.json +0 -1
  234. package/apps/web/.next/server/app/api/mcps/route_client-reference-manifest.js +0 -1
  235. package/apps/web/.next/server/app/api/settings/route.js +0 -1
  236. package/apps/web/.next/server/app/api/settings/route.js.nft.json +0 -1
  237. package/apps/web/.next/server/app/api/settings/route_client-reference-manifest.js +0 -1
  238. package/apps/web/.next/server/app/icon.svg/route.js +0 -1
  239. package/apps/web/.next/server/app/icon.svg/route.js.nft.json +0 -1
  240. package/apps/web/.next/server/app/jobs/page.js +0 -2
  241. package/apps/web/.next/server/app/jobs/page.js.nft.json +0 -1
  242. package/apps/web/.next/server/app/jobs/page_client-reference-manifest.js +0 -1
  243. package/apps/web/.next/server/app/login/page.js +0 -2
  244. package/apps/web/.next/server/app/login/page.js.nft.json +0 -1
  245. package/apps/web/.next/server/app/login/page_client-reference-manifest.js +0 -1
  246. package/apps/web/.next/server/app/page.js +0 -2
  247. package/apps/web/.next/server/app/page.js.nft.json +0 -1
  248. package/apps/web/.next/server/app/page_client-reference-manifest.js +0 -1
  249. package/apps/web/.next/server/app/settings/env-vars/page.js +0 -2
  250. package/apps/web/.next/server/app/settings/env-vars/page.js.nft.json +0 -1
  251. package/apps/web/.next/server/app/settings/env-vars/page_client-reference-manifest.js +0 -1
  252. package/apps/web/.next/server/app/settings/mcps/page.js +0 -2
  253. package/apps/web/.next/server/app/settings/mcps/page.js.nft.json +0 -1
  254. package/apps/web/.next/server/app/settings/mcps/page_client-reference-manifest.js +0 -1
  255. package/apps/web/.next/server/app/settings/page.js +0 -2
  256. package/apps/web/.next/server/app/settings/page.js.nft.json +0 -1
  257. package/apps/web/.next/server/app/settings/page_client-reference-manifest.js +0 -1
  258. package/apps/web/.next/server/app-paths-manifest.json +0 -46
  259. package/apps/web/.next/server/chunks/1157.js +0 -9
  260. package/apps/web/.next/server/chunks/2287.js +0 -1
  261. package/apps/web/.next/server/chunks/3444.js +0 -1
  262. package/apps/web/.next/server/chunks/383.js +0 -6
  263. package/apps/web/.next/server/chunks/4012.js +0 -58
  264. package/apps/web/.next/server/chunks/6791.js +0 -1
  265. package/apps/web/.next/server/chunks/7171.js +0 -1
  266. package/apps/web/.next/server/chunks/8819.js +0 -22
  267. package/apps/web/.next/server/edge-runtime-webpack.js +0 -2
  268. package/apps/web/.next/server/edge-runtime-webpack.js.map +0 -1
  269. package/apps/web/.next/server/interception-route-rewrite-manifest.js +0 -1
  270. package/apps/web/.next/server/middleware-build-manifest.js +0 -1
  271. package/apps/web/.next/server/middleware-manifest.json +0 -32
  272. package/apps/web/.next/server/middleware-react-loadable-manifest.js +0 -1
  273. package/apps/web/.next/server/next-font-manifest.js +0 -1
  274. package/apps/web/.next/server/next-font-manifest.json +0 -1
  275. package/apps/web/.next/server/pages/_app.js +0 -1
  276. package/apps/web/.next/server/pages/_app.js.nft.json +0 -1
  277. package/apps/web/.next/server/pages/_document.js +0 -1
  278. package/apps/web/.next/server/pages/_document.js.nft.json +0 -1
  279. package/apps/web/.next/server/pages/_error.js +0 -19
  280. package/apps/web/.next/server/pages/_error.js.nft.json +0 -1
  281. package/apps/web/.next/server/pages-manifest.json +0 -5
  282. package/apps/web/.next/server/server-reference-manifest.js +0 -1
  283. package/apps/web/.next/server/server-reference-manifest.json +0 -1
  284. package/apps/web/.next/server/src/middleware.js +0 -14
  285. package/apps/web/.next/server/src/middleware.js.map +0 -1
  286. package/apps/web/.next/server/webpack-runtime.js +0 -1
  287. package/apps/web/.next/static/chunks/18-90b700ea37b686a2.js +0 -1
  288. package/apps/web/.next/static/chunks/87c73c54-24122e7b92478d00.js +0 -1
  289. package/apps/web/.next/static/chunks/9664-af80478aa73ba424.js +0 -1
  290. package/apps/web/.next/static/chunks/app/_not-found/page-b9cee17ed89ca24a.js +0 -1
  291. package/apps/web/.next/static/chunks/app/agents/[slug]/page-18369fc3fe1a9a7b.js +0 -1
  292. package/apps/web/.next/static/chunks/app/agents/new/page-bf11cf8901c7e2cd.js +0 -1
  293. package/apps/web/.next/static/chunks/app/api/agents/[id]/access/route-07f0f73ac9839899.js +0 -1
  294. package/apps/web/.next/static/chunks/app/api/agents/[id]/claude-md/route-07f0f73ac9839899.js +0 -1
  295. package/apps/web/.next/static/chunks/app/api/agents/[id]/logs/route-07f0f73ac9839899.js +0 -1
  296. package/apps/web/.next/static/chunks/app/api/agents/[id]/manifest/route-07f0f73ac9839899.js +0 -1
  297. package/apps/web/.next/static/chunks/app/api/agents/[id]/mcps/route-07f0f73ac9839899.js +0 -1
  298. package/apps/web/.next/static/chunks/app/api/agents/[id]/memories/[memId]/route-07f0f73ac9839899.js +0 -1
  299. package/apps/web/.next/static/chunks/app/api/agents/[id]/memories/route-07f0f73ac9839899.js +0 -1
  300. package/apps/web/.next/static/chunks/app/api/agents/[id]/permissions/route-07f0f73ac9839899.js +0 -1
  301. package/apps/web/.next/static/chunks/app/api/agents/[id]/reload/route-07f0f73ac9839899.js +0 -1
  302. package/apps/web/.next/static/chunks/app/api/agents/[id]/restrictions/route-07f0f73ac9839899.js +0 -1
  303. package/apps/web/.next/static/chunks/app/api/agents/[id]/route-07f0f73ac9839899.js +0 -1
  304. package/apps/web/.next/static/chunks/app/api/agents/[id]/skills/[skillId]/route-07f0f73ac9839899.js +0 -1
  305. package/apps/web/.next/static/chunks/app/api/agents/[id]/skills/route-07f0f73ac9839899.js +0 -1
  306. package/apps/web/.next/static/chunks/app/api/agents/[id]/slack-info/route-07f0f73ac9839899.js +0 -1
  307. package/apps/web/.next/static/chunks/app/api/agents/[id]/snapshots/[sid]/restore/route-07f0f73ac9839899.js +0 -1
  308. package/apps/web/.next/static/chunks/app/api/agents/[id]/snapshots/[sid]/route-07f0f73ac9839899.js +0 -1
  309. package/apps/web/.next/static/chunks/app/api/agents/[id]/snapshots/route-07f0f73ac9839899.js +0 -1
  310. package/apps/web/.next/static/chunks/app/api/agents/[id]/start/route-07f0f73ac9839899.js +0 -1
  311. package/apps/web/.next/static/chunks/app/api/agents/[id]/stop/route-07f0f73ac9839899.js +0 -1
  312. package/apps/web/.next/static/chunks/app/api/agents/route-07f0f73ac9839899.js +0 -1
  313. package/apps/web/.next/static/chunks/app/api/auth/login/route-07f0f73ac9839899.js +0 -1
  314. package/apps/web/.next/static/chunks/app/api/auth/logout/route-07f0f73ac9839899.js +0 -1
  315. package/apps/web/.next/static/chunks/app/api/auth/me/route-07f0f73ac9839899.js +0 -1
  316. package/apps/web/.next/static/chunks/app/api/auth/users/[id]/route-07f0f73ac9839899.js +0 -1
  317. package/apps/web/.next/static/chunks/app/api/auth/users/route-07f0f73ac9839899.js +0 -1
  318. package/apps/web/.next/static/chunks/app/api/env-vars/[key]/route-07f0f73ac9839899.js +0 -1
  319. package/apps/web/.next/static/chunks/app/api/env-vars/route-07f0f73ac9839899.js +0 -1
  320. package/apps/web/.next/static/chunks/app/api/jobs/[id]/route-07f0f73ac9839899.js +0 -1
  321. package/apps/web/.next/static/chunks/app/api/jobs/[id]/runs/route-07f0f73ac9839899.js +0 -1
  322. package/apps/web/.next/static/chunks/app/api/jobs/route-07f0f73ac9839899.js +0 -1
  323. package/apps/web/.next/static/chunks/app/api/mcps/[id]/route-07f0f73ac9839899.js +0 -1
  324. package/apps/web/.next/static/chunks/app/api/mcps/[id]/test/route-07f0f73ac9839899.js +0 -1
  325. package/apps/web/.next/static/chunks/app/api/mcps/route-07f0f73ac9839899.js +0 -1
  326. package/apps/web/.next/static/chunks/app/api/settings/route-07f0f73ac9839899.js +0 -1
  327. package/apps/web/.next/static/chunks/app/jobs/page-f5aa89a47c50efd8.js +0 -1
  328. package/apps/web/.next/static/chunks/app/layout-2079f4964aa7314e.js +0 -1
  329. package/apps/web/.next/static/chunks/app/login/layout-07f0f73ac9839899.js +0 -1
  330. package/apps/web/.next/static/chunks/app/login/page-aa259283dc38e8f9.js +0 -1
  331. package/apps/web/.next/static/chunks/app/page-e83437b608104dff.js +0 -1
  332. package/apps/web/.next/static/chunks/app/settings/env-vars/page-06479dbdfb78b76b.js +0 -1
  333. package/apps/web/.next/static/chunks/app/settings/mcps/page-75650686ed6490c7.js +0 -1
  334. package/apps/web/.next/static/chunks/app/settings/page-e1e62fc41ff6cddd.js +0 -1
  335. package/apps/web/.next/static/chunks/framework-811407f832a33072.js +0 -1
  336. package/apps/web/.next/static/chunks/main-3f1cddbdd67b1546.js +0 -1
  337. package/apps/web/.next/static/chunks/main-app-cebd8a6a5ccbf72d.js +0 -1
  338. package/apps/web/.next/static/chunks/pages/_app-50fa07b56b2d29ac.js +0 -1
  339. package/apps/web/.next/static/chunks/pages/_error-fed8688bdd23f211.js +0 -1
  340. package/apps/web/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
  341. package/apps/web/.next/static/chunks/webpack-6c05566dba553c97.js +0 -1
  342. package/apps/web/.next/static/css/15371687405525e2.css +0 -5
  343. package/apps/web/.next/static/ikfNbLhuw7jntn35bz0lk/_buildManifest.js +0 -1
  344. package/apps/web/.next/static/ikfNbLhuw7jntn35bz0lk/_ssgManifest.js +0 -1
  345. package/apps/web/.next/trace +0 -5
  346. package/apps/web/.next/types/app/agents/[slug]/page.ts +0 -84
  347. package/apps/web/.next/types/app/agents/new/page.ts +0 -84
  348. package/apps/web/.next/types/app/api/agents/[id]/access/route.ts +0 -347
  349. package/apps/web/.next/types/app/api/agents/[id]/claude-md/route.ts +0 -347
  350. package/apps/web/.next/types/app/api/agents/[id]/logs/route.ts +0 -347
  351. package/apps/web/.next/types/app/api/agents/[id]/manifest/route.ts +0 -347
  352. package/apps/web/.next/types/app/api/agents/[id]/mcps/route.ts +0 -347
  353. package/apps/web/.next/types/app/api/agents/[id]/memories/[memId]/route.ts +0 -347
  354. package/apps/web/.next/types/app/api/agents/[id]/memories/route.ts +0 -347
  355. package/apps/web/.next/types/app/api/agents/[id]/permissions/route.ts +0 -347
  356. package/apps/web/.next/types/app/api/agents/[id]/reload/route.ts +0 -347
  357. package/apps/web/.next/types/app/api/agents/[id]/restrictions/route.ts +0 -347
  358. package/apps/web/.next/types/app/api/agents/[id]/route.ts +0 -347
  359. package/apps/web/.next/types/app/api/agents/[id]/skills/[skillId]/route.ts +0 -347
  360. package/apps/web/.next/types/app/api/agents/[id]/skills/route.ts +0 -347
  361. package/apps/web/.next/types/app/api/agents/[id]/slack-info/route.ts +0 -347
  362. package/apps/web/.next/types/app/api/agents/[id]/snapshots/[sid]/restore/route.ts +0 -347
  363. package/apps/web/.next/types/app/api/agents/[id]/snapshots/[sid]/route.ts +0 -347
  364. package/apps/web/.next/types/app/api/agents/[id]/snapshots/route.ts +0 -347
  365. package/apps/web/.next/types/app/api/agents/[id]/start/route.ts +0 -347
  366. package/apps/web/.next/types/app/api/agents/[id]/stop/route.ts +0 -347
  367. package/apps/web/.next/types/app/api/agents/route.ts +0 -347
  368. package/apps/web/.next/types/app/api/auth/login/route.ts +0 -347
  369. package/apps/web/.next/types/app/api/auth/logout/route.ts +0 -347
  370. package/apps/web/.next/types/app/api/auth/me/route.ts +0 -347
  371. package/apps/web/.next/types/app/api/auth/users/[id]/route.ts +0 -347
  372. package/apps/web/.next/types/app/api/auth/users/route.ts +0 -347
  373. package/apps/web/.next/types/app/api/env-vars/[key]/route.ts +0 -347
  374. package/apps/web/.next/types/app/api/env-vars/route.ts +0 -347
  375. package/apps/web/.next/types/app/api/jobs/[id]/route.ts +0 -347
  376. package/apps/web/.next/types/app/api/jobs/[id]/runs/route.ts +0 -347
  377. package/apps/web/.next/types/app/api/jobs/route.ts +0 -347
  378. package/apps/web/.next/types/app/api/mcps/[id]/route.ts +0 -347
  379. package/apps/web/.next/types/app/api/mcps/[id]/test/route.ts +0 -347
  380. package/apps/web/.next/types/app/api/mcps/route.ts +0 -347
  381. package/apps/web/.next/types/app/api/settings/route.ts +0 -347
  382. package/apps/web/.next/types/app/jobs/page.ts +0 -84
  383. package/apps/web/.next/types/app/login/layout.ts +0 -84
  384. package/apps/web/.next/types/app/login/page.ts +0 -84
  385. package/apps/web/.next/types/app/page.ts +0 -84
  386. package/apps/web/.next/types/app/settings/env-vars/page.ts +0 -84
  387. package/apps/web/.next/types/app/settings/mcps/page.ts +0 -84
  388. package/apps/web/.next/types/app/settings/page.ts +0 -84
  389. package/apps/web/.next/types/cache-life.d.ts +0 -141
  390. package/apps/web/.next/types/package.json +0 -1
  391. package/apps/web/.next/types/routes.d.ts +0 -114
  392. package/apps/web/.next/types/validator.ts +0 -448
  393. package/apps/web/Dockerfile +0 -37
  394. package/apps/web/next-env.d.ts +0 -6
  395. package/apps/web/next.config.js +0 -6
  396. package/apps/web/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  397. package/apps/web/package.json +0 -48
  398. package/apps/web/postcss.config.js +0 -3
  399. package/apps/web/public/logo.svg +0 -17
  400. package/apps/web/src/app/agents/[slug]/page.tsx +0 -2235
  401. package/apps/web/src/app/agents/new/page.tsx +0 -1161
  402. package/apps/web/src/app/api/agents/[id]/access/route.ts +0 -76
  403. package/apps/web/src/app/api/agents/[id]/claude-md/route.ts +0 -111
  404. package/apps/web/src/app/api/agents/[id]/logs/route.ts +0 -84
  405. package/apps/web/src/app/api/agents/[id]/manifest/route.ts +0 -32
  406. package/apps/web/src/app/api/agents/[id]/mcps/route.ts +0 -73
  407. package/apps/web/src/app/api/agents/[id]/memories/[memId]/route.ts +0 -31
  408. package/apps/web/src/app/api/agents/[id]/memories/route.ts +0 -56
  409. package/apps/web/src/app/api/agents/[id]/permissions/route.ts +0 -74
  410. package/apps/web/src/app/api/agents/[id]/reload/route.ts +0 -33
  411. package/apps/web/src/app/api/agents/[id]/restrictions/route.ts +0 -85
  412. package/apps/web/src/app/api/agents/[id]/route.ts +0 -81
  413. package/apps/web/src/app/api/agents/[id]/skills/[skillId]/route.ts +0 -52
  414. package/apps/web/src/app/api/agents/[id]/skills/route.ts +0 -80
  415. package/apps/web/src/app/api/agents/[id]/slack-info/route.ts +0 -38
  416. package/apps/web/src/app/api/agents/[id]/snapshots/[sid]/restore/route.ts +0 -61
  417. package/apps/web/src/app/api/agents/[id]/snapshots/[sid]/route.ts +0 -53
  418. package/apps/web/src/app/api/agents/[id]/snapshots/route.ts +0 -84
  419. package/apps/web/src/app/api/agents/[id]/start/route.ts +0 -35
  420. package/apps/web/src/app/api/agents/[id]/stop/route.ts +0 -35
  421. package/apps/web/src/app/api/agents/route.ts +0 -99
  422. package/apps/web/src/app/api/auth/login/route.ts +0 -39
  423. package/apps/web/src/app/api/auth/logout/route.ts +0 -21
  424. package/apps/web/src/app/api/auth/me/route.ts +0 -24
  425. package/apps/web/src/app/api/auth/users/[id]/route.ts +0 -48
  426. package/apps/web/src/app/api/auth/users/route.ts +0 -63
  427. package/apps/web/src/app/api/env-vars/[key]/route.ts +0 -66
  428. package/apps/web/src/app/api/env-vars/route.ts +0 -59
  429. package/apps/web/src/app/api/jobs/[id]/route.ts +0 -51
  430. package/apps/web/src/app/api/jobs/[id]/runs/route.ts +0 -24
  431. package/apps/web/src/app/api/jobs/route.ts +0 -42
  432. package/apps/web/src/app/api/mcps/[id]/route.ts +0 -60
  433. package/apps/web/src/app/api/mcps/[id]/test/route.ts +0 -195
  434. package/apps/web/src/app/api/mcps/route.ts +0 -72
  435. package/apps/web/src/app/api/settings/route.ts +0 -42
  436. package/apps/web/src/app/globals.css +0 -124
  437. package/apps/web/src/app/icon.svg +0 -17
  438. package/apps/web/src/app/jobs/page.tsx +0 -543
  439. package/apps/web/src/app/layout-shell.tsx +0 -89
  440. package/apps/web/src/app/layout.tsx +0 -18
  441. package/apps/web/src/app/login/layout.tsx +0 -9
  442. package/apps/web/src/app/login/page.tsx +0 -150
  443. package/apps/web/src/app/page.tsx +0 -573
  444. package/apps/web/src/app/settings/env-vars/page.tsx +0 -216
  445. package/apps/web/src/app/settings/mcps/page.tsx +0 -763
  446. package/apps/web/src/app/settings/page.tsx +0 -528
  447. package/apps/web/src/app/sidebar.tsx +0 -345
  448. package/apps/web/src/lib/__tests__/api-guard.test.ts +0 -189
  449. package/apps/web/src/lib/__tests__/auth.test.ts +0 -262
  450. package/apps/web/src/lib/__tests__/boss-registry.test.ts +0 -323
  451. package/apps/web/src/lib/__tests__/compile.test.ts +0 -161
  452. package/apps/web/src/lib/__tests__/db-agent-hierarchy.test.ts +0 -136
  453. package/apps/web/src/lib/__tests__/db-env-vars.test.ts +0 -216
  454. package/apps/web/src/lib/__tests__/db-restrictions.test.ts +0 -117
  455. package/apps/web/src/lib/__tests__/db.integration.test.ts +0 -271
  456. package/apps/web/src/lib/__tests__/diff.test.ts +0 -102
  457. package/apps/web/src/lib/__tests__/mcp-mask.test.ts +0 -274
  458. package/apps/web/src/lib/__tests__/skill-templates.test.ts +0 -237
  459. package/apps/web/src/lib/__tests__/slack-manifest.test.ts +0 -105
  460. package/apps/web/src/lib/api-guard.ts +0 -68
  461. package/apps/web/src/lib/auth-context.tsx +0 -71
  462. package/apps/web/src/lib/auth.ts +0 -128
  463. package/apps/web/src/lib/boss-registry.ts +0 -90
  464. package/apps/web/src/lib/compile.ts +0 -51
  465. package/apps/web/src/lib/db.ts +0 -1196
  466. package/apps/web/src/lib/diff.ts +0 -43
  467. package/apps/web/src/lib/mcp-mask.ts +0 -91
  468. package/apps/web/src/lib/portal.tsx +0 -23
  469. package/apps/web/src/lib/skill-templates.ts +0 -148
  470. package/apps/web/src/lib/slack-manifest.ts +0 -85
  471. package/apps/web/src/middleware.ts +0 -68
  472. package/apps/web/tailwind.config.js +0 -6
  473. package/apps/web/tsconfig.json +0 -23
  474. package/apps/web/vitest.config.mts +0 -21
  475. package/cli/.claude/settings.local.json +0 -6
  476. package/cli/node_modules/.package-lock.json +0 -427
  477. package/cli/node_modules/commander/LICENSE +0 -22
  478. package/cli/node_modules/commander/Readme.md +0 -1157
  479. package/cli/node_modules/commander/esm.mjs +0 -16
  480. package/cli/node_modules/commander/index.js +0 -24
  481. package/cli/node_modules/commander/lib/argument.js +0 -149
  482. package/cli/node_modules/commander/lib/command.js +0 -2509
  483. package/cli/node_modules/commander/lib/error.js +0 -39
  484. package/cli/node_modules/commander/lib/help.js +0 -520
  485. package/cli/node_modules/commander/lib/option.js +0 -330
  486. package/cli/node_modules/commander/lib/suggestSimilar.js +0 -101
  487. package/cli/node_modules/commander/package-support.json +0 -16
  488. package/cli/node_modules/commander/package.json +0 -84
  489. package/cli/node_modules/commander/typings/esm.d.mts +0 -3
  490. package/cli/node_modules/commander/typings/index.d.ts +0 -969
  491. package/cli/package-lock.json +0 -449
  492. package/cli/package.json +0 -44
  493. package/cli/src/commands/init.ts +0 -514
  494. package/cli/src/commands/manage.ts +0 -115
  495. package/cli/src/index.ts +0 -63
  496. package/cli/tsconfig.json +0 -14
  497. package/docker-compose.yml +0 -122
  498. package/docs/agents/boss-agents.mdx +0 -108
  499. package/docs/agents/creating-agents.mdx +0 -132
  500. package/docs/agents/memory.mdx +0 -113
  501. package/docs/agents/tools.mdx +0 -103
  502. package/docs/configuration/env-vars.mdx +0 -166
  503. package/docs/configuration/mcp-servers.mdx +0 -203
  504. package/docs/configuration/slack-app.mdx +0 -175
  505. package/docs/docs.json +0 -79
  506. package/docs/favicon.svg +0 -17
  507. package/docs/features/history.mdx +0 -60
  508. package/docs/features/import-export.mdx +0 -77
  509. package/docs/features/logs.mdx +0 -131
  510. package/docs/features/multi-workspace.mdx +0 -90
  511. package/docs/features/scheduled-jobs.mdx +0 -231
  512. package/docs/features/users.mdx +0 -92
  513. package/docs/introduction.mdx +0 -160
  514. package/docs/logo/dark.svg +0 -17
  515. package/docs/logo/light.svg +0 -17
  516. package/docs/logo/wide-dark.svg +0 -12
  517. package/docs/logo/wide-light.svg +0 -12
  518. package/docs/quickstart.mdx +0 -270
  519. package/docs/self-hosting/docker.mdx +0 -151
  520. package/docs/self-hosting/production.mdx +0 -176
  521. package/packages/shared/dist/index.d.ts +0 -8
  522. package/packages/shared/dist/index.d.ts.map +0 -1
  523. package/packages/shared/dist/index.js +0 -24
  524. package/packages/shared/dist/index.js.map +0 -1
  525. package/packages/shared/dist/types.d.ts +0 -584
  526. package/packages/shared/dist/types.d.ts.map +0 -1
  527. package/packages/shared/dist/types.js +0 -39
  528. package/packages/shared/dist/types.js.map +0 -1
  529. package/packages/shared/package.json +0 -15
  530. package/packages/shared/src/db/schema.sql +0 -354
  531. package/packages/shared/src/index.ts +0 -8
  532. package/packages/shared/src/types.ts +0 -683
  533. package/packages/shared/tsconfig.json +0 -17
  534. package/scripts/dev.sh +0 -45
@@ -1,2235 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * @fileoverview Agent detail page — tabbed control panel.
5
- *
6
- * Tabs: Overview · Skills · MCPs · Permissions · Memory · Logs
7
- *
8
- * Route: /agents/[slug]
9
- * @module web/app/agents/[slug]
10
- */
11
-
12
- import React, { useEffect, useState, useRef, use } from 'react';
13
- import { Brain, Camera, Clock, History, Upload, Download } from 'lucide-react';
14
- import Link from 'next/link';
15
- import { useRouter } from 'next/navigation';
16
- import type { Agent, Skill, McpServer, Memory, Permission, Restriction, AgentSnapshot } from '@slackhive/shared';
17
- import { Portal } from '@/lib/portal';
18
- import { useAuth } from '@/lib/auth-context';
19
- import { lineDiff, type DiffLine } from '@/lib/diff';
20
-
21
- type Tab = 'overview' | 'skills' | 'claude-md' | 'mcps' | 'permissions' | 'memory' | 'logs' | 'history';
22
-
23
- interface AgentExportPayload {
24
- version: number;
25
- exportedAt?: string;
26
- claudeMd: string;
27
- skills: { category: string; filename: string; content: string; sortOrder: number }[];
28
- }
29
-
30
- const TABS: { id: Tab; label: string }[] = [
31
- { id: 'overview', label: 'Overview' },
32
- { id: 'skills', label: 'Skills' },
33
- { id: 'claude-md', label: 'System Prompt' },
34
- { id: 'mcps', label: 'MCPs' },
35
- { id: 'permissions', label: 'Tools' },
36
- { id: 'memory', label: 'Memory' },
37
- { id: 'logs', label: 'Logs' },
38
- { id: 'history', label: 'History' },
39
- ];
40
-
41
- const STATUS_COLOR = { running: '#16a34a', stopped: 'var(--border-2)', error: '#ef4444' } as const;
42
-
43
- // ─── Page ─────────────────────────────────────────────────────────────────────
44
-
45
- /**
46
- * Agent detail page — loads the agent by slug then renders the tabbed UI.
47
- *
48
- * @param {{ params: Promise<{ slug: string }> }} props
49
- */
50
- export default function AgentPage({ params }: { params: Promise<{ slug: string }> }) {
51
- const { slug } = use(params);
52
- const { role, canManageUsers } = useAuth();
53
- const [agent, setAgent] = useState<Agent | null>(null);
54
- const [allAgents, setAllAgents] = useState<Agent[]>([]);
55
- const [canEdit, setCanEdit] = useState(false);
56
- const [tab, setTab] = useState<Tab>('overview');
57
- const [loading, setLoading] = useState(true);
58
- const [actionMsg, setActionMsg] = useState('');
59
- const [exporting, setExporting] = useState(false);
60
- const [importPreview, setImportPreview] = useState<AgentExportPayload | null>(null);
61
- const [importing, setImporting] = useState(false);
62
- const [importError, setImportError] = useState('');
63
- const fileInputRef = useRef<HTMLInputElement>(null);
64
-
65
- useEffect(() => {
66
- fetch('/api/agents')
67
- .then(r => r.json())
68
- .then((agents: Agent[]) => {
69
- setAllAgents(agents);
70
- const found = agents.find(a => a.slug === slug) ?? null;
71
- setAgent(found);
72
- if (found) {
73
- if (role === 'admin' || role === 'superadmin') {
74
- setCanEdit(true);
75
- } else if (role === 'editor' || role === 'viewer') {
76
- fetch(`/api/agents/${found.id}/access`)
77
- .then(r => r.json())
78
- .then(data => setCanEdit(role === 'editor' && (data.canWrite ?? false)));
79
- }
80
- }
81
- })
82
- .finally(() => setLoading(false));
83
- }, [slug, role]);
84
-
85
- const triggerAction = async (action: 'start' | 'stop' | 'reload') => {
86
- if (!agent) return;
87
- setActionMsg(action === 'start' ? 'Starting…' : action === 'stop' ? 'Stopping…' : 'Reloading…');
88
- await fetch(`/api/agents/${agent.id}/${action}`, { method: 'POST' });
89
- const r = await fetch(`/api/agents/${agent.id}`);
90
- setAgent(await r.json());
91
- setActionMsg('Done');
92
- setTimeout(() => setActionMsg(''), 2000);
93
- };
94
-
95
- const handleExport = async () => {
96
- if (!agent) return;
97
- setExporting(true);
98
- try {
99
- const [skillsRes, mdRes] = await Promise.all([
100
- fetch(`/api/agents/${agent.id}/skills`),
101
- fetch(`/api/agents/${agent.id}/claude-md`),
102
- ]);
103
- const skills: Skill[] = await skillsRes.json();
104
- const claudeMd = await mdRes.text();
105
- const payload: AgentExportPayload = {
106
- version: 1,
107
- exportedAt: new Date().toISOString(),
108
- claudeMd,
109
- skills: skills.map(s => ({ category: s.category, filename: s.filename, content: s.content, sortOrder: s.sortOrder })),
110
- };
111
- const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
112
- const url = URL.createObjectURL(blob);
113
- const a = document.createElement('a');
114
- a.href = url; a.download = `${agent.slug}-export.json`; a.click();
115
- URL.revokeObjectURL(url);
116
- } finally { setExporting(false); }
117
- };
118
-
119
- const handleImportFile = (e: React.ChangeEvent<HTMLInputElement>) => {
120
- const file = e.target.files?.[0];
121
- if (!file) return;
122
- e.target.value = '';
123
- setImportError('');
124
- const reader = new FileReader();
125
- reader.onload = (ev) => {
126
- try {
127
- const data = JSON.parse(ev.target?.result as string);
128
- if (typeof data.claudeMd !== 'string' || !Array.isArray(data.skills)) {
129
- setImportError('Invalid export file'); return;
130
- }
131
- setImportPreview(data);
132
- } catch { setImportError('Could not parse file'); }
133
- };
134
- reader.readAsText(file);
135
- };
136
-
137
- const applyImport = async () => {
138
- if (!agent || !importPreview) return;
139
- setImporting(true);
140
- try {
141
- await fetch(`/api/agents/${agent.id}/claude-md`, {
142
- method: 'PUT', headers: { 'Content-Type': 'text/plain' },
143
- body: importPreview.claudeMd,
144
- });
145
- await Promise.all(importPreview.skills.map(s =>
146
- fetch(`/api/agents/${agent.id}/skills?noSnapshot=1`, {
147
- method: 'POST', headers: { 'Content-Type': 'application/json' },
148
- body: JSON.stringify(s),
149
- })
150
- ));
151
- const updated = await fetch(`/api/agents/${agent.id}`).then(r => r.json());
152
- setAgent(updated);
153
- setImportPreview(null);
154
- } finally { setImporting(false); }
155
- };
156
-
157
- if (loading) return <PageLoader />;
158
- if (!agent) return <NotFound slug={slug} />;
159
-
160
- const statusColor = STATUS_COLOR[agent.status] ?? 'var(--border-2)';
161
-
162
- return (
163
- <div style={{ minHeight: '100vh' }} className="fade-up">
164
-
165
- {/* ── Top bar ──────────────────────────────────────────────────────── */}
166
- <div style={{
167
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
168
- padding: '28px 40px 0',
169
- borderBottom: '1px solid var(--border)',
170
- paddingBottom: 0,
171
- flexWrap: 'wrap', gap: 12,
172
- }}>
173
- <div>
174
- {/* Breadcrumb */}
175
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10, fontSize: 12, color: 'var(--muted)' }}>
176
- <Link href="/" style={{ color: 'var(--muted)', textDecoration: 'none' }}>Agents</Link>
177
- <span style={{ color: 'var(--subtle)' }}>/</span>
178
- <span style={{ color: 'var(--text)' }}>{agent.name}</span>
179
- </div>
180
-
181
- {/* Agent name + status */}
182
- <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
183
- <div style={{
184
- width: 36, height: 36, borderRadius: 10, flexShrink: 0,
185
- background: agent.isBoss
186
- ? 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)'
187
- : 'linear-gradient(135deg, #3b82f6 0%, #6366f1 100%)',
188
- display: 'flex', alignItems: 'center', justifyContent: 'center',
189
- fontSize: 14, fontWeight: 700, color: '#fff',
190
- }}>
191
- {agent.name.charAt(0)}
192
- </div>
193
- <div>
194
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
195
- <h1 style={{ margin: 0, fontSize: 18, fontWeight: 600, letterSpacing: '-0.02em', color: 'var(--text)' }}>
196
- {agent.name}
197
- </h1>
198
- {agent.isBoss && (
199
- <span style={{
200
- fontSize: 10, fontWeight: 600, letterSpacing: '0.06em',
201
- background: 'rgba(245,158,11,0.15)', color: '#f59e0b',
202
- padding: '2px 7px', borderRadius: 5,
203
- border: '1px solid rgba(245,158,11,0.25)',
204
- textTransform: 'uppercase',
205
- }}>Boss</span>
206
- )}
207
- <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
208
- <div
209
- className={agent.status === 'running' ? 'status-running' : ''}
210
- style={{ width: 7, height: 7, borderRadius: '50%', background: statusColor }}
211
- />
212
- <span style={{ fontSize: 12, color: statusColor, fontWeight: 500, textTransform: 'capitalize' }}>
213
- {agent.status}
214
- </span>
215
- </div>
216
- </div>
217
- <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11.5, color: 'var(--muted)', marginTop: 1 }}>
218
- @{agent.slug} · {agent.model.replace('claude-', '').split('-20')[0]}
219
- </div>
220
- </div>
221
- </div>
222
- </div>
223
-
224
- {/* Controls */}
225
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, paddingBottom: 16 }}>
226
- {actionMsg && <span style={{ fontSize: 12, color: 'var(--muted)' }}>{actionMsg}</span>}
227
- {importError && <span style={{ fontSize: 12, color: 'var(--danger)' }}>{importError}</span>}
228
-
229
- {/* Export / Import icon buttons */}
230
- <IconBtn title="Export config" onClick={handleExport} loading={exporting}>
231
- <Download size={15} />
232
- </IconBtn>
233
- {canEdit && (
234
- <IconBtn title="Import config" onClick={() => fileInputRef.current?.click()}>
235
- <Upload size={15} />
236
- </IconBtn>
237
- )}
238
- <input ref={fileInputRef} type="file" accept=".json" style={{ display: 'none' }} onChange={handleImportFile} />
239
-
240
- <div style={{ width: 1, height: 20, background: 'var(--border)', margin: '0 2px' }} />
241
-
242
- {canEdit && agent.status !== 'running' && (
243
- <Btn color="#22c55e" onClick={() => triggerAction('start')}>Start</Btn>
244
- )}
245
- {canEdit && agent.status === 'running' && (
246
- <Btn color="var(--border-2)" textColor="var(--muted)" onClick={() => triggerAction('reload')}>Reload</Btn>
247
- )}
248
- {canEdit && agent.status === 'running' && (
249
- <Btn color="#ef4444" onClick={() => triggerAction('stop')}>Stop</Btn>
250
- )}
251
- </div>
252
- </div>
253
-
254
- {/* Import confirmation modal */}
255
- {importPreview && (
256
- <Portal>
257
- <div style={{
258
- position: 'fixed', inset: 0, zIndex: 1000,
259
- background: 'rgba(0,0,0,0.45)', backdropFilter: 'blur(4px)',
260
- display: 'flex', alignItems: 'center', justifyContent: 'center',
261
- }} onClick={() => setImportPreview(null)}>
262
- <div style={{
263
- background: '#fff', borderRadius: 14, padding: '28px 32px',
264
- maxWidth: 480, width: '90%',
265
- boxShadow: '0 20px 60px rgba(0,0,0,0.15)',
266
- }} onClick={e => e.stopPropagation()}>
267
- <h3 style={{ margin: '0 0 16px', fontSize: 16, fontWeight: 700, color: 'var(--text)', letterSpacing: '-0.02em' }}>
268
- Import agent config
269
- </h3>
270
-
271
- {/* Danger warning — shown first */}
272
- <div style={{
273
- display: 'flex', gap: 10, padding: '12px 14px', marginBottom: 16,
274
- background: '#fff1f2', border: '1.5px solid #fecdd3', borderRadius: 8,
275
- }}>
276
- <span style={{ fontSize: 16, flexShrink: 0 }}>⚠️</span>
277
- <div>
278
- <div style={{ fontSize: 13, fontWeight: 600, color: '#be123c', marginBottom: 2 }}>
279
- This will overwrite current CLAUDE.md and skills
280
- </div>
281
- <div style={{ fontSize: 12, color: '#9f1239' }}>
282
- Existing CLAUDE.md will be replaced. Skills with matching category/filename will be overwritten. A snapshot is saved automatically before applying.
283
- </div>
284
- </div>
285
- </div>
286
-
287
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 20 }}>
288
- {importPreview.exportedAt && <InfoRow label="Exported at" value={new Date(importPreview.exportedAt).toLocaleString()} />}
289
- <InfoRow label="Skills" value={`${importPreview.skills.length} skill${importPreview.skills.length !== 1 ? 's' : ''} will be upserted`} />
290
- </div>
291
- <div style={{ display: 'flex', gap: 10 }}>
292
- <PrimaryBtn onClick={applyImport} loading={importing}>Apply Import</PrimaryBtn>
293
- <GhostBtn onClick={() => setImportPreview(null)}>Cancel</GhostBtn>
294
- </div>
295
- </div>
296
- </div>
297
- </Portal>
298
- )}
299
-
300
- {/* ── Tab bar ──────────────────────────────────────────────────────── */}
301
- <div style={{
302
- display: 'flex', gap: 0, padding: '0 36px',
303
- borderBottom: '1px solid var(--border)',
304
- background: 'var(--surface)',
305
- overflowX: 'auto', WebkitOverflowScrolling: 'touch',
306
- }}>
307
- {TABS.map(t => (
308
- <button
309
- key={t.id}
310
- onClick={() => setTab(t.id)}
311
- className={tab === t.id ? 'tab-active' : ''}
312
- style={{
313
- background: 'none', border: 'none', cursor: 'pointer',
314
- padding: '10px 14px', fontSize: 13,
315
- color: tab === t.id ? 'var(--text)' : 'var(--muted)',
316
- fontWeight: tab === t.id ? 500 : 400,
317
- transition: 'color 0.15s',
318
- fontFamily: 'var(--font-sans)',
319
- }}
320
- >
321
- {t.label}
322
- </button>
323
- ))}
324
- </div>
325
-
326
- {/* ── Tab content ──────────────────────────────────────────────────── */}
327
- <div style={{ padding: '28px 36px' }}>
328
- {tab === 'overview' && <OverviewTab agent={agent} onUpdate={setAgent} canEdit={canEdit} allAgents={allAgents} role={role} />}
329
- {tab === 'skills' && <SkillsTab agentId={agent.id} canEdit={canEdit} />}
330
- {tab === 'claude-md' && <ClaudeMdTab agentId={agent.id} canEdit={canEdit} />}
331
- {tab === 'mcps' && <McpsTab agentId={agent.id} canEdit={canEdit} />}
332
- {tab === 'permissions' && <PermissionsTab agentId={agent.id} canEdit={canEdit} />}
333
- {tab === 'memory' && <MemoryTab agentId={agent.id} canEdit={canEdit} />}
334
- {tab === 'logs' && <LogsTab agentId={agent.id} slug={agent.slug} />}
335
- {tab === 'history' && <HistoryTab agentId={agent.id} canEdit={canEdit} />}
336
- </div>
337
- </div>
338
- );
339
- }
340
-
341
- // ─── Overview ─────────────────────────────────────────────────────────────────
342
-
343
- function OverviewTab({ agent, onUpdate, canEdit, allAgents, role }: { agent: Agent; onUpdate: (a: Agent) => void; canEdit: boolean; allAgents: Agent[]; role: string | null }) {
344
- const [form, setForm] = useState({
345
- name: agent.name,
346
- description: agent.description ?? '',
347
- persona: agent.persona ?? '',
348
- model: agent.model,
349
- slackBotToken: agent.slackBotToken,
350
- slackAppToken: agent.slackAppToken,
351
- slackSigningSecret: agent.slackSigningSecret,
352
- isBoss: agent.isBoss,
353
- reportsTo: agent.reportsTo ?? [] as string[],
354
- });
355
- const [saving, setSaving] = useState(false);
356
- const [msg, setMsg] = useState('');
357
- const [manifest, setManifest] = useState('');
358
- const [showManifest, setShowManifest] = useState(false);
359
- const [deleting, setDeleting] = useState(false);
360
- const [slackInfo, setSlackInfo] = useState<{ displayName: string; handle: string; teamName: string } | null>(null);
361
- const router = useRouter();
362
-
363
- useEffect(() => {
364
- if (!agent.slackBotToken) return;
365
- fetch(`/api/agents/${agent.id}/slack-info`)
366
- .then(r => r.ok ? r.json() : null)
367
- .then(d => d && setSlackInfo(d))
368
- .catch(() => {});
369
- }, [agent.id, agent.slackBotToken]);
370
-
371
- // Channel restrictions state
372
- const [allowedChannels, setAllowedChannels] = useState('');
373
-
374
- useEffect(() => {
375
- fetch(`/api/agents/${agent.id}/restrictions`)
376
- .then(r => r.json())
377
- .then((d: Restriction) => setAllowedChannels((d.allowedChannels ?? []).join('\n')));
378
- }, [agent.id]);
379
-
380
- const save = async () => {
381
- setSaving(true);
382
- try {
383
- const [r] = await Promise.all([
384
- fetch(`/api/agents/${agent.id}`, {
385
- method: 'PATCH', headers: { 'Content-Type': 'application/json' },
386
- body: JSON.stringify(form),
387
- }),
388
- fetch(`/api/agents/${agent.id}/restrictions`, {
389
- method: 'PUT', headers: { 'Content-Type': 'application/json' },
390
- body: JSON.stringify({ allowedChannels: allowedChannels.split('\n').map(s => s.trim()).filter(Boolean) }),
391
- }),
392
- ]);
393
- const data = await r.json();
394
- if (r.ok) { onUpdate(data); setMsg('Saved'); } else setMsg(data.error ?? 'Error');
395
- } finally { setSaving(false); setTimeout(() => setMsg(''), 3000); }
396
- };
397
-
398
- const loadManifest = async () => {
399
- const r = await fetch(`/api/agents/${agent.id}/manifest`);
400
- setManifest(JSON.stringify(await r.json(), null, 2));
401
- setShowManifest(true);
402
- };
403
-
404
- const handleDelete = async () => {
405
- if (!confirm(`Permanently delete agent "${agent.name}"? This cannot be undone.`)) return;
406
- setDeleting(true);
407
- const r = await fetch(`/api/agents/${agent.id}`, { method: 'DELETE' });
408
- if (r.ok) {
409
- router.push('/');
410
- } else {
411
- const err = await r.json();
412
- setMsg(err.error ?? 'Delete failed');
413
- setDeleting(false);
414
- }
415
- };
416
-
417
- const isAdmin = role === 'admin' || role === 'superadmin';
418
-
419
- return (
420
- <div style={{ maxWidth: 640 }} className="fade-up">
421
- <Section title="Identity">
422
- <Grid2>
423
- <Field label="Name" value={form.name} onChange={v => setForm(f => ({ ...f, name: v }))} readOnly={!canEdit}
424
- hint="This is the internal agent name. To update the Slack bot display name, change it in your Slack App settings → App Home." />
425
- <Field label="Model" value={form.model} onChange={v => setForm(f => ({ ...f, model: v }))}
426
- hint="claude-opus-4-6 · claude-sonnet-4-6 · claude-haiku-4-5-20251001" readOnly={!canEdit} />
427
- </Grid2>
428
- <Field label="Description" value={form.description}
429
- onChange={v => setForm(f => ({ ...f, description: v }))}
430
- hint="Shown to the boss agent for delegation decisions." readOnly={!canEdit} />
431
- <TextArea label="Persona" value={form.persona}
432
- onChange={v => setForm(f => ({ ...f, persona: v }))}
433
- hint="Injected into CLAUDE.md — who is this agent?" rows={4} readOnly={!canEdit} />
434
- </Section>
435
-
436
- <Section title="Role & Hierarchy">
437
- {/* Boss toggle */}
438
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
439
- <div>
440
- <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text)', marginBottom: 2 }}>Boss Agent</div>
441
- <div style={{ fontSize: 12, color: 'var(--muted)' }}>Boss agents orchestrate other agents and delegate tasks</div>
442
- </div>
443
- <button
444
- disabled={!canEdit}
445
- onClick={() => setForm(f => ({ ...f, isBoss: !f.isBoss }))}
446
- style={{
447
- width: 44, height: 24, borderRadius: 12, border: 'none',
448
- background: form.isBoss ? '#d97706' : 'var(--border-2)',
449
- cursor: canEdit ? 'pointer' : 'default',
450
- position: 'relative', transition: 'background 0.2s', flexShrink: 0,
451
- }}
452
- >
453
- <div style={{
454
- position: 'absolute', top: 3, left: form.isBoss ? 23 : 3,
455
- width: 18, height: 18, borderRadius: '50%', background: '#fff',
456
- transition: 'left 0.2s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
457
- }} />
458
- </button>
459
- </div>
460
-
461
- {/* Reports To — only show for non-boss agents */}
462
- {!form.isBoss && (() => {
463
- const bosses = allAgents.filter(a => a.isBoss && a.id !== agent.id);
464
- if (bosses.length === 0) return (
465
- <div style={{ fontSize: 12, color: 'var(--subtle)', fontStyle: 'italic' }}>
466
- No boss agents available. Create a boss agent first.
467
- </div>
468
- );
469
- return (
470
- <div>
471
- <div style={{ fontSize: 12, fontWeight: 500, color: 'var(--text)', marginBottom: 6 }}>Reports To</div>
472
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
473
- {bosses.map(boss => {
474
- const checked = form.reportsTo.includes(boss.id);
475
- return (
476
- <label key={boss.id} style={{
477
- display: 'flex', alignItems: 'center', gap: 10,
478
- padding: '8px 12px', borderRadius: 8,
479
- border: `1px solid ${checked ? 'rgba(217,119,6,0.3)' : 'var(--border)'}`,
480
- background: checked ? 'rgba(217,119,6,0.04)' : 'var(--surface)',
481
- cursor: canEdit ? 'pointer' : 'default',
482
- transition: 'all 0.15s',
483
- }}>
484
- <input
485
- type="checkbox"
486
- checked={checked}
487
- disabled={!canEdit}
488
- onChange={() => setForm(f => ({
489
- ...f,
490
- reportsTo: checked
491
- ? f.reportsTo.filter(id => id !== boss.id)
492
- : [...f.reportsTo, boss.id],
493
- }))}
494
- style={{ accentColor: '#d97706', width: 14, height: 14 }}
495
- />
496
- <div style={{
497
- width: 24, height: 24, borderRadius: 6, background: '#171717',
498
- display: 'flex', alignItems: 'center', justifyContent: 'center',
499
- fontSize: 11, fontWeight: 600, color: '#fff', flexShrink: 0,
500
- }}>
501
- {boss.name.charAt(0).toUpperCase()}
502
- </div>
503
- <div style={{ minWidth: 0 }}>
504
- <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text)' }}>{boss.name}</div>
505
- <div style={{ fontSize: 11, color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>@{boss.slug}</div>
506
- </div>
507
- {checked && (
508
- <span style={{
509
- marginLeft: 'auto', fontSize: 10, fontWeight: 600,
510
- color: '#d97706', letterSpacing: '0.04em', textTransform: 'uppercase',
511
- }}>Reports to</span>
512
- )}
513
- </label>
514
- );
515
- })}
516
- </div>
517
- <div style={{ fontSize: 11.5, color: 'var(--subtle)', marginTop: 8 }}>
518
- An agent can report to multiple bosses.
519
- </div>
520
- </div>
521
- );
522
- })()}
523
- </Section>
524
-
525
- <Section title="Slack Credentials">
526
- <Field label="Bot Token" value={form.slackBotToken}
527
- onChange={v => setForm(f => ({ ...f, slackBotToken: v }))} type="password" readOnly={!canEdit}
528
- hint={<>api.slack.com/apps → your app → <strong>OAuth &amp; Permissions</strong> → Bot User OAuth Token</>} />
529
- <Field label="App-Level Token" value={form.slackAppToken}
530
- onChange={v => setForm(f => ({ ...f, slackAppToken: v }))} type="password" readOnly={!canEdit}
531
- hint={<>Basic Information → <strong>App-Level Tokens</strong> → Generate with scope <code style={{ fontFamily: 'var(--font-mono)', fontSize: 11 }}>connections:write</code></>} />
532
- <Field label="Signing Secret" value={form.slackSigningSecret}
533
- onChange={v => setForm(f => ({ ...f, slackSigningSecret: v }))} type="password" readOnly={!canEdit}
534
- hint="Basic Information → App Credentials → Signing Secret" />
535
- {slackInfo && (
536
- <div style={{
537
- background: '#f0fdf4', border: '1px solid #bbf7d0',
538
- borderRadius: 7, padding: '10px 14px', fontSize: 12,
539
- }}>
540
- <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
541
- <div style={{ width: 7, height: 7, borderRadius: '50%', background: '#16a34a', flexShrink: 0 }} />
542
- <span style={{ color: '#15803d', fontWeight: 600 }}>Connected to Slack</span>
543
- <span style={{ color: '#86efac', marginLeft: 'auto', fontSize: 11 }}>{slackInfo.teamName}</span>
544
- </div>
545
- <div style={{ display: 'grid', gridTemplateColumns: 'max-content 1fr', gap: '4px 16px' }}>
546
- <span style={{ color: '#6b7280' }}>Display name</span>
547
- <span style={{ color: '#166534', fontWeight: 500 }}>{slackInfo.displayName}</span>
548
- <span style={{ color: '#6b7280' }}>@handle</span>
549
- <span style={{ color: '#166534', fontFamily: 'var(--font-mono)' }}>@{slackInfo.handle}</span>
550
- {agent.slackBotUserId && <>
551
- <span style={{ color: '#6b7280' }}>Bot User ID</span>
552
- <span style={{ color: '#166534', fontFamily: 'var(--font-mono)' }}>{agent.slackBotUserId}</span>
553
- </>}
554
- </div>
555
- </div>
556
- )}
557
- {!slackInfo && agent.slackBotUserId && (
558
- <div style={{
559
- display: 'flex', alignItems: 'center', gap: 8,
560
- background: '#f0fdf4', border: '1px solid #bbf7d0',
561
- borderRadius: 7, padding: '8px 12px', fontSize: 12,
562
- }}>
563
- <div style={{ width: 7, height: 7, borderRadius: '50%', background: '#16a34a', flexShrink: 0 }} />
564
- <span style={{ color: '#15803d' }}>Connected ·</span>
565
- <span style={{ color: '#166534', fontFamily: 'var(--font-mono)' }}>Bot User ID: {agent.slackBotUserId}</span>
566
- </div>
567
- )}
568
- </Section>
569
-
570
- <Section title="Allowed Channels">
571
- <p style={{ margin: '0 0 10px', fontSize: 12.5, color: 'var(--muted)', lineHeight: 1.6 }}>
572
- Restrict this bot to specific Slack channels. Enter one Slack channel ID per line (e.g. <code style={{ fontFamily: 'var(--font-mono)', fontSize: 11 }}>C01234ABCDE</code>).
573
- If empty, the bot responds in all channels it's invited to.
574
- When invited to a non-allowed channel, it will post a notice and leave automatically.
575
- Bot-initiated messages from scheduled jobs are not affected.
576
- </p>
577
- <textarea
578
- value={allowedChannels}
579
- onChange={e => setAllowedChannels(e.target.value)}
580
- rows={4}
581
- readOnly={!canEdit}
582
- placeholder={'C01234ABCDE\nC09876ZYXWV'}
583
- style={{
584
- width: '100%', background: 'var(--surface)', border: '1px solid var(--border)',
585
- borderRadius: 8, padding: '10px 12px', color: 'var(--text)',
586
- fontFamily: 'var(--font-mono)', fontSize: 12, lineHeight: 1.7,
587
- outline: 'none', resize: 'vertical', boxSizing: 'border-box',
588
- }}
589
- onFocus={e => (e.currentTarget.style.borderColor = 'var(--accent)')}
590
- onBlur={e => (e.currentTarget.style.borderColor = 'var(--border)')}
591
- />
592
- </Section>
593
-
594
- <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
595
- {canEdit && <PrimaryBtn onClick={save} loading={saving}>Save Changes</PrimaryBtn>}
596
- <GhostBtn onClick={loadManifest}>View Slack Manifest</GhostBtn>
597
- {msg && <span style={{ fontSize: 12, color: '#16a34a' }}>{msg}</span>}
598
- </div>
599
-
600
- {showManifest && (
601
- <div style={{
602
- marginTop: 20, background: 'var(--surface-2)',
603
- border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden',
604
- }}>
605
- <div style={{
606
- display: 'flex', justifyContent: 'space-between', alignItems: 'center',
607
- padding: '10px 16px', borderBottom: '1px solid var(--border)',
608
- background: 'var(--surface-2)',
609
- }}>
610
- <span style={{ fontSize: 11.5, color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>
611
- slack-manifest.json
612
- </span>
613
- <button
614
- onClick={() => navigator.clipboard.writeText(manifest)}
615
- style={{ fontSize: 11.5, color: 'var(--accent)', background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)' }}
616
- >Copy</button>
617
- </div>
618
- <pre style={{
619
- margin: 0, padding: '16px', fontSize: 11.5, color: 'var(--accent)',
620
- fontFamily: 'var(--font-mono)', overflow: 'auto', maxHeight: 320,
621
- }}>{manifest}</pre>
622
- </div>
623
- )}
624
-
625
- {/* ── Danger Zone ── */}
626
- {isAdmin && (
627
- <div style={{
628
- marginTop: 40, borderTop: '1px solid #fecaca', paddingTop: 28,
629
- }}>
630
- <div style={{ fontSize: 11, fontWeight: 700, color: '#dc2626', letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: 16 }}>
631
- Danger Zone
632
- </div>
633
- <div style={{
634
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
635
- background: '#fff8f8', border: '1px solid #fecaca', borderRadius: 8, padding: '14px 18px',
636
- }}>
637
- <div>
638
- <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text)', marginBottom: 3 }}>Delete this agent</div>
639
- <div style={{ fontSize: 12, color: 'var(--muted)' }}>Permanently removes the agent, all its skills, memories, and history. This cannot be undone.</div>
640
- </div>
641
- <button
642
- onClick={handleDelete}
643
- disabled={deleting}
644
- style={{
645
- flexShrink: 0, marginLeft: 24,
646
- padding: '8px 18px', borderRadius: 7, border: '1px solid #dc2626',
647
- background: deleting ? '#fef2f2' : '#fff', color: '#dc2626',
648
- fontSize: 13, fontWeight: 600, cursor: deleting ? 'not-allowed' : 'pointer',
649
- fontFamily: 'var(--font-sans)', whiteSpace: 'nowrap',
650
- }}
651
- >{deleting ? 'Deleting…' : 'Delete Agent'}</button>
652
- </div>
653
- </div>
654
- )}
655
- </div>
656
- );
657
- }
658
-
659
- // ─── CLAUDE.md viewer ─────────────────────────────────────────────────────────
660
-
661
- function ClaudeMdTab({ agentId, canEdit }: { agentId: string; canEdit: boolean }) {
662
- const [content, setContent] = useState<string>('');
663
- const [draft, setDraft] = useState<string>('');
664
- const [editing, setEditing] = useState(false);
665
- const [loading, setLoading] = useState(true);
666
- const [saving, setSaving] = useState(false);
667
- const [msg, setMsg] = useState('');
668
-
669
- const load = () => {
670
- setLoading(true);
671
- fetch(`/api/agents/${agentId}/claude-md`)
672
- .then(r => r.text())
673
- .then(t => { setContent(t); setDraft(t); })
674
- .catch(() => setContent('Failed to load CLAUDE.md'))
675
- .finally(() => setLoading(false));
676
- };
677
-
678
- useEffect(() => { load(); }, [agentId]);
679
-
680
- const save = async () => {
681
- setSaving(true);
682
- try {
683
- const res = await fetch(`/api/agents/${agentId}/claude-md`, {
684
- method: 'PUT',
685
- headers: { 'Content-Type': 'text/plain' },
686
- body: draft,
687
- });
688
- if (!res.ok) throw new Error(await res.text());
689
- setContent(draft);
690
- setEditing(false);
691
- setMsg('Saved — agent will use this on next reload.');
692
- setTimeout(() => setMsg(''), 4000);
693
- } catch (e: any) {
694
- setMsg(`Error: ${e.message}`);
695
- } finally {
696
- setSaving(false);
697
- }
698
- };
699
-
700
- if (loading) return <p style={{ color: 'var(--muted)', fontSize: 14 }}>Loading...</p>;
701
-
702
- return (
703
- <div>
704
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
705
- <div>
706
- <h3 style={{ margin: 0, fontSize: 15, fontWeight: 600 }}>CLAUDE.md</h3>
707
- <p style={{ margin: '4px 0 0', fontSize: 13, color: 'var(--muted)' }}>
708
- {editing ? 'Editing raw system prompt — this overrides all individual skills.' : 'Compiled system prompt sent to Claude.'}
709
- </p>
710
- </div>
711
- <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
712
- {!editing && (
713
- <span style={{ fontSize: 12, color: 'var(--muted)', background: 'var(--surface-2)', padding: '4px 10px', borderRadius: 6, border: '1px solid var(--border)' }}>
714
- {(content.length / 1024).toFixed(1)} KB · {content.split('\n').length} lines
715
- </span>
716
- )}
717
- {editing ? (
718
- <>
719
- <button onClick={() => { setEditing(false); setDraft(content); }} style={{ padding: '6px 14px', borderRadius: 7, border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, cursor: 'pointer' }}>
720
- Cancel
721
- </button>
722
- <button onClick={save} disabled={saving} style={{ padding: '6px 16px', borderRadius: 7, border: 'none', background: 'var(--accent)', color: '#fff', fontSize: 13, fontWeight: 600, cursor: 'pointer', opacity: saving ? 0.7 : 1 }}>
723
- {saving ? 'Saving…' : 'Save'}
724
- </button>
725
- </>
726
- ) : (
727
- canEdit && <button onClick={() => setEditing(true)} style={{ padding: '6px 16px', borderRadius: 7, border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, cursor: 'pointer' }}>
728
- Edit
729
- </button>
730
- )}
731
- </div>
732
- </div>
733
-
734
- {msg && <p style={{ fontSize: 13, color: msg.startsWith('Error') ? 'var(--danger)' : 'var(--success)', marginBottom: 12 }}>{msg}</p>}
735
-
736
- {editing ? (
737
- <textarea
738
- value={draft}
739
- onChange={e => setDraft(e.target.value)}
740
- style={{
741
- width: '100%', height: '70vh', background: 'var(--surface)',
742
- border: '1px solid var(--accent)', borderRadius: 10,
743
- padding: '20px 24px', fontSize: 12.5, lineHeight: 1.7,
744
- color: 'var(--text)', fontFamily: 'var(--font-mono)',
745
- resize: 'vertical', outline: 'none', boxSizing: 'border-box',
746
- }}
747
- />
748
- ) : (
749
- <pre style={{
750
- background: 'var(--surface-2)', border: '1px solid var(--border)',
751
- borderRadius: 10, padding: '20px 24px', fontSize: 12.5, lineHeight: 1.7,
752
- overflowX: 'auto', overflowY: 'auto', maxHeight: '70vh', margin: 0,
753
- whiteSpace: 'pre-wrap', wordBreak: 'break-word',
754
- color: 'var(--text)', fontFamily: 'var(--font-mono)',
755
- }}>
756
- {content}
757
- </pre>
758
- )}
759
- </div>
760
- );
761
- }
762
-
763
- // ─── Skills ───────────────────────────────────────────────────────────────────
764
-
765
- function SkillsTab({ agentId, canEdit }: { agentId: string; canEdit: boolean }) {
766
- const [skills, setSkills] = useState<Skill[]>([]);
767
- const [selected, setSelected] = useState<Skill | null>(null);
768
- const [content, setContent] = useState('');
769
- const [saving, setSaving] = useState(false);
770
- const [msg, setMsg] = useState('');
771
- const [showNew, setShowNew] = useState(false);
772
- const [newSkill, setNewSkill] = useState({ category: '', filename: '', content: '' });
773
-
774
- const load = () =>
775
- fetch(`/api/agents/${agentId}/skills`).then(r => r.json()).then(setSkills);
776
-
777
- useEffect(() => { load(); }, [agentId]);
778
-
779
- const select = (s: Skill) => { setSelected(s); setContent(s.content); };
780
-
781
- const save = async () => {
782
- if (!selected) return;
783
- setSaving(true);
784
- await fetch(`/api/agents/${agentId}/skills`, {
785
- method: 'POST', headers: { 'Content-Type': 'application/json' },
786
- body: JSON.stringify({ category: selected.category, filename: selected.filename, content, sortOrder: selected.sortOrder }),
787
- });
788
- setSaving(false); setMsg('Saved'); setTimeout(() => setMsg(''), 2000); load();
789
- };
790
-
791
- const remove = async (s: Skill) => {
792
- if (!confirm(`Delete ${s.category}/${s.filename}?`)) return;
793
- await fetch(`/api/agents/${agentId}/skills/${s.id}`, { method: 'DELETE' });
794
- if (selected?.id === s.id) { setSelected(null); setContent(''); }
795
- load();
796
- };
797
-
798
- const create = async () => {
799
- if (!newSkill.category || !newSkill.filename) return;
800
- await fetch(`/api/agents/${agentId}/skills`, {
801
- method: 'POST', headers: { 'Content-Type': 'application/json' },
802
- body: JSON.stringify(newSkill),
803
- });
804
- setShowNew(false); setNewSkill({ category: '', filename: '', content: '' }); load();
805
- };
806
-
807
- const grouped = skills.reduce<Record<string, Skill[]>>((acc, s) => {
808
- (acc[s.category] ??= []).push(s); return acc;
809
- }, {});
810
-
811
- return (
812
- <div className="fade-up" style={{ display: 'flex', gap: 14, height: 580 }}>
813
- {/* File tree */}
814
- <div style={{
815
- width: 220, flexShrink: 0,
816
- background: 'var(--surface)', border: '1px solid var(--border)',
817
- borderRadius: 10, overflow: 'auto', display: 'flex', flexDirection: 'column',
818
- }}>
819
- <div style={{
820
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
821
- padding: '10px 12px', borderBottom: '1px solid var(--border)',
822
- }}>
823
- <span style={{ fontSize: 11, fontWeight: 600, color: 'var(--muted)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
824
- Files
825
- </span>
826
- {canEdit && <button onClick={() => setShowNew(true)} style={{
827
- background: 'none', border: 'none', cursor: 'pointer',
828
- fontSize: 12, color: 'var(--accent)', fontFamily: 'var(--font-sans)',
829
- }}>+ New</button>}
830
- </div>
831
- <div style={{ padding: '6px 6px', flex: 1, overflow: 'auto' }}>
832
- {Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b)).map(([cat, catSkills]) => (
833
- <div key={cat}>
834
- <div style={{
835
- fontSize: 10.5, color: 'var(--subtle)', padding: '6px 6px 2px',
836
- fontFamily: 'var(--font-mono)', letterSpacing: '0.02em',
837
- }}>{cat}/</div>
838
- {catSkills.map(s => (
839
- <div
840
- key={s.id}
841
- onClick={() => select(s)}
842
- className="skill-row"
843
- style={{
844
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
845
- padding: '5px 8px', borderRadius: 6, cursor: 'pointer',
846
- fontSize: 12, fontFamily: 'var(--font-mono)',
847
- background: selected?.id === s.id ? 'rgba(59,130,246,0.12)' : 'transparent',
848
- color: selected?.id === s.id ? 'var(--accent)' : 'var(--muted)',
849
- transition: 'background 0.12s, color 0.12s',
850
- }}
851
- onMouseEnter={e => {
852
- if (selected?.id !== s.id) (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.04)';
853
- const btn = (e.currentTarget as HTMLElement).querySelector('.delete-btn') as HTMLElement | null;
854
- if (btn) btn.style.opacity = '1';
855
- }}
856
- onMouseLeave={e => {
857
- if (selected?.id !== s.id) (e.currentTarget as HTMLElement).style.background = 'transparent';
858
- const btn = (e.currentTarget as HTMLElement).querySelector('.delete-btn') as HTMLElement | null;
859
- if (btn) btn.style.opacity = '0';
860
- }}
861
- >
862
- <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.filename}</span>
863
- {canEdit && <button
864
- onClick={e => { e.stopPropagation(); remove(s); }}
865
- className="delete-btn"
866
- style={{
867
- background: 'none', border: 'none', cursor: 'pointer',
868
- color: '#ef4444', fontSize: 14, opacity: 0, transition: 'opacity 0.12s',
869
- fontFamily: 'var(--font-sans)', lineHeight: 1, padding: '0 2px', flexShrink: 0,
870
- }}
871
- >×</button>}
872
- </div>
873
- ))}
874
- </div>
875
- ))}
876
- </div>
877
- </div>
878
-
879
- {/* Editor */}
880
- <div style={{
881
- flex: 1, background: 'var(--surface)', border: '1px solid var(--border)',
882
- borderRadius: 10, display: 'flex', flexDirection: 'column', overflow: 'hidden',
883
- }}>
884
- {selected ? (
885
- <>
886
- <div style={{
887
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
888
- padding: '10px 16px', borderBottom: '1px solid var(--border)',
889
- background: 'var(--surface-2)',
890
- }}>
891
- <span style={{ fontSize: 12, color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>
892
- {selected.category}/{selected.filename}
893
- </span>
894
- <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
895
- {msg && <span style={{ fontSize: 11.5, color: '#16a34a' }}>{msg}</span>}
896
- {canEdit && <button
897
- onClick={save} disabled={saving}
898
- style={{
899
- background: saving ? 'var(--border)' : 'var(--accent)',
900
- color: '#fff', border: 'none', borderRadius: 6,
901
- padding: '5px 14px', fontSize: 12, fontWeight: 500,
902
- cursor: saving ? 'not-allowed' : 'pointer',
903
- fontFamily: 'var(--font-sans)',
904
- }}
905
- >
906
- {saving ? 'Saving…' : 'Save'}
907
- </button>}
908
- </div>
909
- </div>
910
- <textarea
911
- value={content}
912
- onChange={e => setContent(e.target.value)}
913
- readOnly={!canEdit}
914
- style={{
915
- flex: 1, border: 'none', outline: 'none', resize: 'none',
916
- background: 'transparent', color: 'var(--text)',
917
- fontFamily: 'var(--font-mono)', fontSize: 12.5, lineHeight: 1.65,
918
- padding: '16px', caretColor: 'var(--accent)',
919
- }}
920
- spellCheck={false}
921
- />
922
- </>
923
- ) : (
924
- <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--subtle)', fontSize: 13 }}>
925
- Select a file to edit
926
- </div>
927
- )}
928
- </div>
929
-
930
- {/* New skill modal */}
931
- {showNew && (
932
- <Modal title="New Skill File" onClose={() => setShowNew(false)}>
933
- <Field label="Category" value={newSkill.category}
934
- onChange={v => setNewSkill(s => ({ ...s, category: v }))} hint="e.g. 00-core" />
935
- <Field label="Filename" value={newSkill.filename}
936
- onChange={v => setNewSkill(s => ({ ...s, filename: v }))} hint="e.g. identity.md" />
937
- <TextArea label="Content (optional)" value={newSkill.content}
938
- onChange={v => setNewSkill(s => ({ ...s, content: v }))} rows={4} />
939
- <div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
940
- <PrimaryBtn onClick={create}>Create</PrimaryBtn>
941
- <GhostBtn onClick={() => setShowNew(false)}>Cancel</GhostBtn>
942
- </div>
943
- </Modal>
944
- )}
945
- </div>
946
- );
947
- }
948
-
949
- // ─── MCPs ─────────────────────────────────────────────────────────────────────
950
-
951
- function McpsTab({ agentId, canEdit }: { agentId: string; canEdit: boolean }) {
952
- const [all, setAll] = useState<McpServer[]>([]);
953
- const [assigned, setAssigned] = useState<Set<string>>(new Set());
954
- const [saving, setSaving] = useState(false);
955
- const [msg, setMsg] = useState('');
956
-
957
- useEffect(() => {
958
- Promise.all([
959
- fetch('/api/mcps').then(r => r.json()),
960
- fetch(`/api/agents/${agentId}/mcps`).then(r => r.json()),
961
- ]).then(([a, b]: [McpServer[], McpServer[]]) => {
962
- setAll(a); setAssigned(new Set(b.map(m => m.id)));
963
- });
964
- }, [agentId]);
965
-
966
- const toggle = (id: string) =>
967
- setAssigned(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
968
-
969
- const save = async () => {
970
- setSaving(true);
971
- await fetch(`/api/agents/${agentId}/mcps`, {
972
- method: 'PUT', headers: { 'Content-Type': 'application/json' },
973
- body: JSON.stringify({ mcpIds: [...assigned] }),
974
- });
975
- setSaving(false); setMsg('Saved & reload triggered');
976
- setTimeout(() => setMsg(''), 3000);
977
- };
978
-
979
- return (
980
- <div style={{ maxWidth: 560 }} className="fade-up">
981
- <p style={{ margin: '0 0 16px', fontSize: 13, color: 'var(--muted)' }}>
982
- Select MCP servers from the platform catalog to enable for this agent.
983
- </p>
984
- <div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden', marginBottom: 16 }}>
985
- {all.length === 0 ? (
986
- <div style={{ padding: '24px', textAlign: 'center', color: 'var(--muted)', fontSize: 13 }}>
987
- No MCP servers yet.{' '}
988
- <Link href="/settings/mcps" style={{ color: 'var(--accent)', textDecoration: 'none' }}>Add some →</Link>
989
- </div>
990
- ) : all.map((mcp, i) => (
991
- <label
992
- key={mcp.id}
993
- style={{
994
- display: 'flex', alignItems: 'center', gap: 12,
995
- padding: '13px 16px', cursor: mcp.enabled ? 'pointer' : 'not-allowed',
996
- borderBottom: i < all.length - 1 ? '1px solid var(--border)' : 'none',
997
- background: 'transparent', transition: 'background 0.12s',
998
- opacity: mcp.enabled ? 1 : 0.45,
999
- }}
1000
- onMouseEnter={e => { if (mcp.enabled) (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.03)'; }}
1001
- onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = 'transparent'; }}
1002
- >
1003
- <input
1004
- type="checkbox"
1005
- checked={assigned.has(mcp.id)}
1006
- onChange={() => toggle(mcp.id)}
1007
- disabled={!mcp.enabled || !canEdit}
1008
- style={{ accentColor: 'var(--accent)', width: 14, height: 14, flexShrink: 0 }}
1009
- />
1010
- <div style={{ flex: 1, minWidth: 0 }}>
1011
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
1012
- <span style={{ fontSize: 13, fontWeight: 500, color: 'var(--text)' }}>{mcp.name}</span>
1013
- <span style={{
1014
- fontSize: 10.5, fontFamily: 'var(--font-mono)',
1015
- color: 'var(--muted)', background: 'var(--border)',
1016
- padding: '1px 6px', borderRadius: 4,
1017
- }}>{mcp.type}</span>
1018
- {!mcp.enabled && <span style={{ fontSize: 11, color: 'var(--subtle)' }}>disabled</span>}
1019
- </div>
1020
- {mcp.description && <p style={{ margin: 0, fontSize: 12, color: 'var(--muted)', marginTop: 1 }}>{mcp.description}</p>}
1021
- </div>
1022
- </label>
1023
- ))}
1024
- </div>
1025
- <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
1026
- {canEdit && <PrimaryBtn onClick={save} loading={saving}>Save Assignments</PrimaryBtn>}
1027
- {msg && <span style={{ fontSize: 12, color: '#16a34a' }}>{msg}</span>}
1028
- </div>
1029
- </div>
1030
- );
1031
- }
1032
-
1033
- // ─── Permissions ──────────────────────────────────────────────────────────────
1034
-
1035
- const QUICK_TOOLS = ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'WebFetch', 'WebSearch'];
1036
-
1037
- function PermissionsTab({ agentId, canEdit }: { agentId: string; canEdit: boolean }) {
1038
- const [allowed, setAllowed] = useState('');
1039
- const [denied, setDenied] = useState('');
1040
- const [saving, setSaving] = useState(false);
1041
- const [msg, setMsg] = useState('');
1042
-
1043
- useEffect(() => {
1044
- fetch(`/api/agents/${agentId}/permissions`).then(r => r.json()).then((p: Permission) => {
1045
- setAllowed((p.allowedTools ?? []).join('\n'));
1046
- setDenied((p.deniedTools ?? []).join('\n'));
1047
- });
1048
- }, [agentId]);
1049
-
1050
- const addTool = (tool: string, list: 'allowed' | 'denied') => {
1051
- const setter = list === 'allowed' ? setAllowed : setDenied;
1052
- const current = (list === 'allowed' ? allowed : denied).split('\n').map(s => s.trim()).filter(Boolean);
1053
- if (!current.includes(tool)) setter([...current, tool].join('\n'));
1054
- };
1055
-
1056
- const save = async () => {
1057
- setSaving(true);
1058
- await fetch(`/api/agents/${agentId}/permissions`, {
1059
- method: 'PUT', headers: { 'Content-Type': 'application/json' },
1060
- body: JSON.stringify({
1061
- allowedTools: allowed.split('\n').map(s => s.trim()).filter(Boolean),
1062
- deniedTools: denied.split('\n').map(s => s.trim()).filter(Boolean),
1063
- }),
1064
- });
1065
- setSaving(false); setMsg('Saved & reload triggered');
1066
- setTimeout(() => setMsg(''), 3000);
1067
- };
1068
-
1069
- return (
1070
- <div style={{ maxWidth: 660 }} className="fade-up">
1071
- {/* Quick add */}
1072
- <div style={{
1073
- background: 'var(--surface)', border: '1px solid var(--border)',
1074
- borderRadius: 10, padding: '12px 16px', marginBottom: 18,
1075
- }}>
1076
- <div style={{ fontSize: 11, color: 'var(--muted)', marginBottom: 8, fontWeight: 500, letterSpacing: '0.04em', textTransform: 'uppercase' }}>
1077
- Quick add built-in tools
1078
- </div>
1079
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
1080
- {QUICK_TOOLS.map(t => (
1081
- <button
1082
- key={t}
1083
- onClick={() => addTool(t, 'allowed')}
1084
- disabled={!canEdit}
1085
- style={{
1086
- background: 'var(--border)', border: '1px solid var(--border-2)',
1087
- color: 'var(--text)', padding: '3px 10px', borderRadius: 5,
1088
- fontSize: 11.5, fontFamily: 'var(--font-mono)', cursor: 'pointer',
1089
- transition: 'background 0.12s, border-color 0.12s',
1090
- }}
1091
- onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = 'var(--border-2)'; (e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)'; }}
1092
- onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = 'var(--border)'; (e.currentTarget as HTMLElement).style.borderColor = 'var(--border-2)'; }}
1093
- >{t}</button>
1094
- ))}
1095
- </div>
1096
- <p style={{ margin: '8px 0 0', fontSize: 11, color: 'var(--subtle)' }}>
1097
- MCP tools pattern: <code style={{ fontFamily: 'var(--font-mono)', color: 'var(--muted)' }}>mcp__serverName__toolName</code>
1098
- </p>
1099
- </div>
1100
-
1101
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, marginBottom: 16 }}>
1102
- <div>
1103
- <label style={{ display: 'block', fontSize: 12, fontWeight: 500, color: 'var(--muted)', marginBottom: 6 }}>
1104
- Allowed Tools <span style={{ color: 'var(--subtle)', fontWeight: 400 }}>· one per line</span>
1105
- </label>
1106
- <textarea
1107
- value={allowed} onChange={e => setAllowed(e.target.value)}
1108
- rows={12} readOnly={!canEdit}
1109
- style={{
1110
- width: '100%', background: 'var(--surface)', border: '1px solid var(--border)',
1111
- borderRadius: 8, padding: '10px 12px', color: 'var(--text)',
1112
- fontFamily: 'var(--font-mono)', fontSize: 12, lineHeight: 1.7,
1113
- outline: 'none', resize: 'vertical',
1114
- }}
1115
- onFocus={e => (e.currentTarget.style.borderColor = 'var(--accent)')}
1116
- onBlur={e => (e.currentTarget.style.borderColor = 'var(--border)')}
1117
- placeholder={'Read\nWrite\nmcp__redshift-mcp__query'}
1118
- />
1119
- </div>
1120
- <div>
1121
- <label style={{ display: 'block', fontSize: 12, fontWeight: 500, color: 'var(--muted)', marginBottom: 6 }}>
1122
- Denied Tools <span style={{ color: 'var(--subtle)', fontWeight: 400 }}>· overrides allowed</span>
1123
- </label>
1124
- <textarea
1125
- value={denied} onChange={e => setDenied(e.target.value)}
1126
- rows={12} readOnly={!canEdit}
1127
- style={{
1128
- width: '100%', background: 'var(--surface)', border: '1px solid var(--border)',
1129
- borderRadius: 8, padding: '10px 12px', color: 'var(--danger)',
1130
- fontFamily: 'var(--font-mono)', fontSize: 12, lineHeight: 1.7,
1131
- outline: 'none', resize: 'vertical',
1132
- }}
1133
- onFocus={e => (e.currentTarget.style.borderColor = '#ef4444')}
1134
- onBlur={e => (e.currentTarget.style.borderColor = 'var(--border)')}
1135
- placeholder={'Bash'}
1136
- />
1137
- </div>
1138
- </div>
1139
- <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
1140
- {canEdit && <PrimaryBtn onClick={save} loading={saving}>Save Permissions</PrimaryBtn>}
1141
- {msg && <span style={{ fontSize: 12, color: '#16a34a' }}>{msg}</span>}
1142
- </div>
1143
- </div>
1144
- );
1145
- }
1146
-
1147
- // ─── Memory ───────────────────────────────────────────────────────────────────
1148
-
1149
- const MEM_TYPE_STYLE: Record<string, { bg: string; color: string }> = {
1150
- user: { bg: '#f3f0ff', color: '#7c3aed' },
1151
- feedback: { bg: '#eff6ff', color: '#2563eb' },
1152
- project: { bg: '#fffbeb', color: '#b45309' },
1153
- reference: { bg: '#f0fdf4', color: '#15803d' },
1154
- };
1155
-
1156
- function MemoryTab({ agentId, canEdit }: { agentId: string; canEdit: boolean }) {
1157
- const [memories, setMemories] = useState<Memory[]>([]);
1158
- const [expanded, setExpanded] = useState<Set<string>>(new Set());
1159
-
1160
- const load = () => fetch(`/api/agents/${agentId}/memories`).then(r => r.json()).then(setMemories);
1161
- useEffect(() => { load(); }, [agentId]);
1162
-
1163
- const remove = async (id: string) => {
1164
- await fetch(`/api/agents/${agentId}/memories/${id}`, { method: 'DELETE' });
1165
- load();
1166
- };
1167
-
1168
- const toggle = (id: string) =>
1169
- setExpanded(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
1170
-
1171
- const grouped = memories.reduce<Record<string, Memory[]>>((acc, m) => {
1172
- (acc[m.type] ??= []).push(m); return acc;
1173
- }, {});
1174
-
1175
- if (memories.length === 0) {
1176
- return (
1177
- <div className="fade-up" style={{
1178
- display: 'flex', flexDirection: 'column', alignItems: 'center',
1179
- paddingTop: 80, color: 'var(--muted)',
1180
- }}>
1181
- <Brain size={36} style={{ marginBottom: 12, color: 'var(--border-2)' }} />
1182
- <p style={{ margin: '0 0 4px', fontSize: 15, fontWeight: 600, color: 'var(--text)', textAlign: 'center' }}>
1183
- No memories yet
1184
- </p>
1185
- <p style={{ fontSize: 13, color: 'var(--muted)', maxWidth: 300, margin: '0', textAlign: 'center' }}>
1186
- The agent will automatically accumulate memories as it interacts in Slack.
1187
- </p>
1188
- </div>
1189
- );
1190
- }
1191
-
1192
- return (
1193
- <div style={{ maxWidth: 720 }} className="fade-up">
1194
- <div style={{ fontSize: 13, color: 'var(--muted)', marginBottom: 18 }}>
1195
- {memories.length} memories across {Object.keys(grouped).length} categories
1196
- </div>
1197
- {(['feedback', 'user', 'project', 'reference'] as const).map(type => {
1198
- const items = grouped[type];
1199
- if (!items?.length) return null;
1200
- const style = MEM_TYPE_STYLE[type] ?? { bg: 'var(--border)', color: 'var(--muted)' };
1201
- return (
1202
- <div key={type} style={{ marginBottom: 20 }}>
1203
- <div style={{
1204
- display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8,
1205
- }}>
1206
- <span style={{
1207
- fontSize: 10.5, fontWeight: 600, letterSpacing: '0.06em',
1208
- textTransform: 'uppercase',
1209
- background: style.bg, color: style.color,
1210
- padding: '2px 8px', borderRadius: 5,
1211
- }}>{type}</span>
1212
- <span style={{ fontSize: 11.5, color: 'var(--subtle)' }}>{items.length}</span>
1213
- </div>
1214
- <div style={{ border: '1px solid var(--border)', borderRadius: 10, overflow: 'hidden' }}>
1215
- {items.map((m, i) => (
1216
- <div key={m.id} style={{ borderBottom: i < items.length - 1 ? '1px solid var(--border)' : 'none' }}>
1217
- <div style={{
1218
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
1219
- padding: '10px 14px', cursor: 'pointer',
1220
- }} onClick={() => toggle(m.id)}>
1221
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
1222
- <span style={{ fontSize: 11, color: 'var(--subtle)' }}>
1223
- {expanded.has(m.id) ? '▼' : '▶'}
1224
- </span>
1225
- <span style={{ fontSize: 13, color: 'var(--text)', fontWeight: 500, fontFamily: 'var(--font-mono)' }}>
1226
- {m.name}
1227
- </span>
1228
- </div>
1229
- <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
1230
- <span style={{ fontSize: 11, color: 'var(--subtle)' }}>
1231
- {new Date(m.updatedAt).toLocaleDateString()}
1232
- </span>
1233
- {canEdit && <button
1234
- onClick={e => { e.stopPropagation(); remove(m.id); }}
1235
- style={{
1236
- background: 'none', border: 'none', cursor: 'pointer',
1237
- color: '#ef4444', fontSize: 13, opacity: 0.5, transition: 'opacity 0.12s',
1238
- fontFamily: 'var(--font-sans)',
1239
- }}
1240
- onMouseEnter={e => (e.currentTarget.style.opacity = '1')}
1241
- onMouseLeave={e => (e.currentTarget.style.opacity = '0.5')}
1242
- >Delete</button>}
1243
- </div>
1244
- </div>
1245
- {expanded.has(m.id) && (
1246
- <pre style={{
1247
- margin: 0, padding: '12px 14px',
1248
- background: 'var(--surface-2)',
1249
- borderTop: '1px solid var(--border)',
1250
- fontFamily: 'var(--font-mono)', fontSize: 11.5,
1251
- color: 'var(--muted)', whiteSpace: 'pre-wrap', lineHeight: 1.6,
1252
- }}>{m.content}</pre>
1253
- )}
1254
- </div>
1255
- ))}
1256
- </div>
1257
- </div>
1258
- );
1259
- })}
1260
- </div>
1261
- );
1262
- }
1263
-
1264
- // ─── Logs ─────────────────────────────────────────────────────────────────────
1265
-
1266
- type LogLevel = 'all' | 'debug' | 'info' | 'warn' | 'error';
1267
-
1268
- interface ParsedLog {
1269
- raw: string;
1270
- level: LogLevel;
1271
- time: string;
1272
- message: string;
1273
- fields: Record<string, string>;
1274
- }
1275
-
1276
- function parseLine(raw: string): ParsedLog {
1277
- const stripped = raw.replace(/\x1b\[[0-9;]*m/g, '');
1278
- try {
1279
- const obj = JSON.parse(stripped);
1280
- const level: LogLevel =
1281
- obj.level === 'error' || obj.level === 50 ? 'error' :
1282
- obj.level === 'warn' || obj.level === 40 ? 'warn' :
1283
- obj.level === 'debug' || obj.level === 20 ? 'debug' : 'info';
1284
- const ts = obj.timestamp ? new Date(obj.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
1285
- const rawMsg = obj.message ?? obj.msg ?? '';
1286
- const msg = rawMsg.replace(/^(error|warn|info|debug|trace):\s*/i, '');
1287
- const skip = new Set(['level', 'message', 'msg', 'timestamp', 'agent', 'service']);
1288
- const fields: Record<string, string> = {};
1289
- for (const [k, v] of Object.entries(obj)) {
1290
- if (!skip.has(k)) fields[k] = typeof v === 'object' ? JSON.stringify(v) : String(v);
1291
- }
1292
- return { raw: stripped, level, time: ts, message: msg, fields };
1293
- } catch {
1294
- const lo = stripped.toLowerCase();
1295
- const level: LogLevel =
1296
- lo.includes('"level":"error"') || lo.includes('"level":50') || lo.includes('error:') ? 'error' :
1297
- lo.includes('"level":"warn"') || lo.includes('"level":40') || lo.includes('warn:') ? 'warn' :
1298
- lo.includes('"level":"debug"') || lo.includes('"level":20') || lo.includes('debug:') ? 'debug' : 'info';
1299
- const plainMsg = stripped.replace(/^(error|warn|info|debug|trace):\s*/i, '');
1300
- const tsMatch = stripped.match(/"timestamp":"([^"]+)"/);
1301
- const plainTime = tsMatch ? new Date(tsMatch[1]).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
1302
- return { raw: stripped, level, time: plainTime, message: plainMsg, fields: {} };
1303
- }
1304
- }
1305
-
1306
- const LOG_META: Record<LogLevel, { label: string; color: string; bg: string; border: string; rowBg: string }> = {
1307
- all: { label: 'ALL', color: '#6b7280', bg: '#f3f4f6', border: '#e5e7eb', rowBg: 'transparent' },
1308
- info: { label: 'INFO', color: '#1d4ed8', bg: '#eff6ff', border: '#bfdbfe', rowBg: 'transparent' },
1309
- debug: { label: 'DEBUG', color: '#9ca3af', bg: '#f9fafb', border: '#e5e7eb', rowBg: 'transparent' },
1310
- warn: { label: 'WARN', color: '#92400e', bg: '#fffbeb', border: '#fde68a', rowBg: '#fffdf0' },
1311
- error: { label: 'ERR', color: '#991b1b', bg: '#fef2f2', border: '#fecaca', rowBg: '#fff8f8' },
1312
- };
1313
-
1314
- function LogRow({ log }: { log: ParsedLog }) {
1315
- const [expanded, setExpanded] = useState(false);
1316
- const [hovered, setHovered] = useState(false);
1317
- const m = LOG_META[log.level];
1318
- const hasFields = Object.keys(log.fields).length > 0;
1319
- const msgColor = log.level === 'error' ? '#7f1d1d' : log.level === 'warn' ? '#78350f' : log.level === 'debug' ? '#9ca3af' : 'var(--text)';
1320
-
1321
- return (
1322
- <div
1323
- onClick={() => setExpanded(e => !e)}
1324
- onMouseEnter={() => setHovered(true)}
1325
- onMouseLeave={() => setHovered(false)}
1326
- style={{
1327
- cursor: 'pointer',
1328
- background: hovered ? 'var(--surface-2)' : (expanded ? 'var(--surface-2)' : m.rowBg),
1329
- borderLeft: `3px solid ${expanded ? m.border : 'transparent'}`,
1330
- borderBottom: '1px solid var(--border)',
1331
- transition: 'background 0.1s',
1332
- }}
1333
- >
1334
- {/* Compact single row */}
1335
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '5px 10px', minHeight: 28 }}>
1336
- <span style={{ color: 'var(--subtle)', flexShrink: 0, fontSize: 10.5, fontVariantNumeric: 'tabular-nums', minWidth: 68 }}>
1337
- {log.time}
1338
- </span>
1339
- <span style={{
1340
- flexShrink: 0, fontSize: 9.5, fontWeight: 700, letterSpacing: '0.06em',
1341
- padding: '1px 6px', borderRadius: 3, border: `1px solid ${m.border}`,
1342
- background: m.bg, color: m.color, minWidth: 34, textAlign: 'center',
1343
- }}>{m.label}</span>
1344
- <span style={{ flex: 1, color: msgColor, fontSize: 11.5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
1345
- {log.message}
1346
- </span>
1347
- {!expanded && hasFields && (
1348
- <span style={{ flexShrink: 0, display: 'flex', gap: 3 }}>
1349
- {Object.keys(log.fields).slice(0, 3).map(k => (
1350
- <span key={k} style={{ fontSize: 9.5, color: 'var(--muted)', background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 3, padding: '0 4px' }}>{k}</span>
1351
- ))}
1352
- {Object.keys(log.fields).length > 3 && <span style={{ fontSize: 9.5, color: 'var(--subtle)' }}>+{Object.keys(log.fields).length - 3}</span>}
1353
- </span>
1354
- )}
1355
- <span style={{ flexShrink: 0, color: 'var(--subtle)', fontSize: 9, transform: expanded ? 'rotate(90deg)' : 'none', transition: 'transform 0.15s' }}>▶</span>
1356
- </div>
1357
-
1358
- {/* Expanded detail */}
1359
- {expanded && (
1360
- <div style={{ padding: '8px 14px 12px 92px', borderTop: '1px solid var(--border)', background: 'var(--surface-2)' }}>
1361
- {log.message.includes('\n') && (
1362
- <pre style={{ margin: '0 0 10px', color: 'var(--text)', fontSize: 11.5, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{log.message}</pre>
1363
- )}
1364
- {hasFields && (
1365
- <div style={{ display: 'grid', gridTemplateColumns: 'max-content 1fr', gap: '4px 16px', marginBottom: 8 }}>
1366
- {Object.entries(log.fields).map(([k, v]) => (
1367
- <>
1368
- <span key={`k-${k}`} style={{ color: 'var(--accent)', fontSize: 11, fontWeight: 500 }}>{k}</span>
1369
- <span key={`v-${k}`} style={{ color: 'var(--muted)', fontSize: 11, wordBreak: 'break-all' }}>{v}</span>
1370
- </>
1371
- ))}
1372
- </div>
1373
- )}
1374
- {log.raw && (
1375
- <>
1376
- <div style={{ fontSize: 10, color: 'var(--subtle)', marginTop: 8, marginBottom: 4 }}>Raw</div>
1377
- <pre style={{
1378
- margin: 0, padding: '8px 10px', background: 'var(--surface-2)',
1379
- border: '1px solid var(--border)', borderRadius: 4,
1380
- fontSize: 10.5, color: 'var(--muted)', whiteSpace: 'pre-wrap', wordBreak: 'break-all', maxHeight: 180, overflow: 'auto',
1381
- }}>{log.raw}</pre>
1382
- </>
1383
- )}
1384
- </div>
1385
- )}
1386
- </div>
1387
- );
1388
- }
1389
-
1390
- function LogsTab({ agentId, slug }: { agentId: string; slug: string }) {
1391
- const [lines, setLines] = useState<ParsedLog[]>([]);
1392
- const [connected, setConnected] = useState(false);
1393
- const [levelFilter, setLevelFilter] = useState<LogLevel>('all');
1394
- const [search, setSearch] = useState('');
1395
- const [autoScroll, setAutoScroll] = useState(true);
1396
- const bottomRef = useRef<HTMLDivElement>(null);
1397
- const containerRef = useRef<HTMLDivElement>(null);
1398
-
1399
- useEffect(() => {
1400
- const es = new EventSource(`/api/agents/${agentId}/logs`);
1401
- setConnected(true);
1402
- es.onmessage = e => {
1403
- const raw = JSON.parse(e.data) as string;
1404
- setLines(prev => [...prev.slice(-1000), parseLine(raw)]);
1405
- };
1406
- es.onerror = () => setConnected(false);
1407
- return () => es.close();
1408
- }, [agentId]);
1409
-
1410
- useEffect(() => {
1411
- if (autoScroll) bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
1412
- }, [lines, autoScroll]);
1413
-
1414
- const LEVEL_ORDER: LogLevel[] = ['error', 'warn', 'info', 'debug'];
1415
-
1416
- const counts = lines.reduce<Record<LogLevel, number>>((acc, l) => {
1417
- acc[l.level] = (acc[l.level] ?? 0) + 1; return acc;
1418
- }, { all: lines.length, error: 0, warn: 0, info: 0, debug: 0 });
1419
-
1420
- const visibleLines = lines.filter(l => {
1421
- if (levelFilter !== 'all' && l.level !== levelFilter) return false;
1422
- if (search) {
1423
- const q = search.toLowerCase();
1424
- return l.message.toLowerCase().includes(q) ||
1425
- Object.values(l.fields).some(v => v.toLowerCase().includes(q));
1426
- }
1427
- return true;
1428
- });
1429
-
1430
- return (
1431
- <div className="fade-up">
1432
- {/* Toolbar */}
1433
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, flexWrap: 'wrap' }}>
1434
- <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
1435
- <div className={connected ? 'status-running' : ''}
1436
- style={{ width: 6, height: 6, borderRadius: '50%', background: connected ? '#16a34a' : 'var(--border-2)' }} />
1437
- <span style={{ fontSize: 11, color: 'var(--muted)' }}>{connected ? 'Live' : 'Disconnected'}</span>
1438
- </div>
1439
- <div style={{ width: 1, height: 14, background: 'var(--border)', margin: '0 2px' }} />
1440
- {/* Level filters with counts */}
1441
- {(['all', ...LEVEL_ORDER] as LogLevel[]).map(lvl => {
1442
- const m = LOG_META[lvl];
1443
- const active = levelFilter === lvl;
1444
- return (
1445
- <button key={lvl} onClick={() => setLevelFilter(lvl)} style={{
1446
- padding: '2px 8px', borderRadius: 4,
1447
- border: `1px solid ${active ? m.border : 'var(--border)'}`,
1448
- fontSize: 10.5, fontFamily: 'var(--font-sans)', cursor: 'pointer',
1449
- background: active ? m.bg : 'transparent',
1450
- color: active ? m.color : 'var(--muted)',
1451
- fontWeight: active ? 700 : 400,
1452
- display: 'flex', alignItems: 'center', gap: 4,
1453
- }}>
1454
- {m.label}
1455
- {counts[lvl] > 0 && <span style={{ fontSize: 9.5, opacity: 0.75 }}>{counts[lvl]}</span>}
1456
- </button>
1457
- );
1458
- })}
1459
- <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Filter logs…"
1460
- style={{
1461
- padding: '3px 10px', borderRadius: 4, border: '1px solid var(--border)',
1462
- fontSize: 11, fontFamily: 'var(--font-mono)', background: 'transparent',
1463
- color: 'var(--text)', outline: 'none', width: 180,
1464
- }} />
1465
- <button onClick={() => setLines([])} style={{
1466
- marginLeft: 'auto', background: 'none', border: 'none', cursor: 'pointer',
1467
- fontSize: 11, color: 'var(--subtle)', fontFamily: 'var(--font-sans)',
1468
- }}>Clear</button>
1469
- </div>
1470
-
1471
- {/* Log pane */}
1472
- <div ref={containerRef} onScroll={e => {
1473
- const el = e.currentTarget;
1474
- setAutoScroll(el.scrollTop + el.clientHeight >= el.scrollHeight - 40);
1475
- }} style={{
1476
- background: '#fff', border: '1px solid var(--border)', borderRadius: 8,
1477
- height: 520, overflow: 'auto', fontFamily: 'var(--font-mono)',
1478
- }}>
1479
- {visibleLines.length === 0 ? (
1480
- <div style={{ padding: '40px 20px', textAlign: 'center', color: 'var(--subtle)', fontSize: 12 }}>
1481
- {lines.length === 0 ? 'Waiting for log lines…' : 'No matching lines.'}
1482
- </div>
1483
- ) : (
1484
- visibleLines.map((log, i) => <LogRow key={i} log={log} />)
1485
- )}
1486
- <div ref={bottomRef} />
1487
- </div>
1488
-
1489
- <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 5, padding: '0 2px' }}>
1490
- <span style={{ fontSize: 10.5, color: 'var(--subtle)' }}>
1491
- {visibleLines.length}{visibleLines.length !== lines.length ? ` / ${lines.length}` : ''} line{visibleLines.length !== 1 ? 's' : ''}
1492
- </span>
1493
- {!autoScroll && (
1494
- <button onClick={() => { setAutoScroll(true); bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }}
1495
- style={{ fontSize: 10.5, color: 'var(--accent)', background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)' }}>
1496
- ↓ Jump to latest
1497
- </button>
1498
- )}
1499
- </div>
1500
- </div>
1501
- );
1502
- }
1503
-
1504
- // ─── Shared UI primitives ─────────────────────────────────────────────────────
1505
-
1506
- function Section({ title, children }: { title: string; children: React.ReactNode }) {
1507
- return (
1508
- <div style={{
1509
- marginBottom: 32, paddingBottom: 28,
1510
- borderBottom: '1px solid var(--border)',
1511
- }}>
1512
- <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--muted)', letterSpacing: '0.08em',
1513
- textTransform: 'uppercase', marginBottom: 16 }}>
1514
- {title}
1515
- </div>
1516
- <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>{children}</div>
1517
- </div>
1518
- );
1519
- }
1520
-
1521
- function Grid2({ children }: { children: React.ReactNode }) {
1522
- return <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>{children}</div>;
1523
- }
1524
-
1525
- function Field({ label, value, onChange, hint, type = 'text', readOnly }: {
1526
- label: string; value: string; onChange: (v: string) => void;
1527
- hint?: React.ReactNode; type?: string; readOnly?: boolean;
1528
- }) {
1529
- return (
1530
- <div>
1531
- <label style={{ display: 'block', fontSize: 12, fontWeight: 500, color: 'var(--muted)', marginBottom: 5 }}>
1532
- {label}
1533
- </label>
1534
- <input
1535
- type={type} value={value} onChange={e => onChange(e.target.value)} readOnly={readOnly}
1536
- style={{
1537
- width: '100%', background: 'var(--surface)', border: '1.5px solid var(--border)',
1538
- borderRadius: 'var(--radius)', padding: '10px 14px', color: 'var(--text)',
1539
- fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none',
1540
- transition: 'border-color 0.15s',
1541
- }}
1542
- onFocus={e => (e.currentTarget.style.borderColor = 'var(--accent)')}
1543
- onBlur={e => (e.currentTarget.style.borderColor = 'var(--border)')}
1544
- />
1545
- {hint && <p style={{ margin: '5px 0 0', fontSize: 12, color: 'var(--subtle)' }}>{hint}</p>}
1546
- </div>
1547
- );
1548
- }
1549
-
1550
- function TextArea({ label, value, onChange, hint, rows = 3, readOnly }: {
1551
- label: string; value: string; onChange: (v: string) => void;
1552
- hint?: string; rows?: number; readOnly?: boolean;
1553
- }) {
1554
- return (
1555
- <div>
1556
- <label style={{ display: 'block', fontSize: 12, fontWeight: 500, color: 'var(--muted)', marginBottom: 5 }}>
1557
- {label}
1558
- </label>
1559
- <textarea
1560
- value={value} onChange={e => onChange(e.target.value)} rows={rows} readOnly={readOnly}
1561
- style={{
1562
- width: '100%', background: 'var(--surface)', border: '1.5px solid var(--border)',
1563
- borderRadius: 'var(--radius)', padding: '10px 14px', color: 'var(--text)',
1564
- fontSize: 14, fontFamily: 'var(--font-sans)', outline: 'none', resize: 'vertical',
1565
- transition: 'border-color 0.15s',
1566
- }}
1567
- onFocus={e => (e.currentTarget.style.borderColor = 'var(--accent)')}
1568
- onBlur={e => (e.currentTarget.style.borderColor = 'var(--border)')}
1569
- />
1570
- {hint && <p style={{ margin: '5px 0 0', fontSize: 12, color: 'var(--subtle)' }}>{hint}</p>}
1571
- </div>
1572
- );
1573
- }
1574
-
1575
- function PrimaryBtn({ children, onClick, loading }: {
1576
- children: React.ReactNode; onClick?: () => void; loading?: boolean;
1577
- }) {
1578
- return (
1579
- <button onClick={onClick} disabled={loading} style={{
1580
- background: loading ? 'var(--border)' : 'var(--accent)',
1581
- color: '#fff', border: 'none', borderRadius: 'var(--radius)',
1582
- padding: '10px 22px', fontSize: 14, fontWeight: 600,
1583
- letterSpacing: '-0.01em',
1584
- cursor: loading ? 'not-allowed' : 'pointer',
1585
- fontFamily: 'var(--font-sans)',
1586
- boxShadow: loading ? 'none' : 'var(--shadow-sm)',
1587
- transition: 'opacity 0.15s, transform 0.15s, box-shadow 0.15s',
1588
- }}
1589
- onMouseEnter={e => { if (!loading) { (e.currentTarget as HTMLElement).style.opacity = '0.88'; (e.currentTarget as HTMLElement).style.transform = 'translateY(-1px)'; (e.currentTarget as HTMLElement).style.boxShadow = 'var(--shadow-hover)'; }}}
1590
- onMouseLeave={e => { (e.currentTarget as HTMLElement).style.opacity = '1'; (e.currentTarget as HTMLElement).style.transform = 'translateY(0)'; (e.currentTarget as HTMLElement).style.boxShadow = 'var(--shadow-sm)'; }}
1591
- >{loading ? 'Saving…' : children}</button>
1592
- );
1593
- }
1594
-
1595
- function GhostBtn({ children, onClick, loading }: { children: React.ReactNode; onClick?: () => void; loading?: boolean }) {
1596
- return (
1597
- <button onClick={onClick} disabled={loading} style={{
1598
- background: 'transparent', color: 'var(--muted)',
1599
- border: '1.5px solid var(--border-2)', borderRadius: 'var(--radius)',
1600
- padding: '10px 20px', fontSize: 14, fontWeight: 500, fontFamily: 'var(--font-sans)',
1601
- cursor: loading ? 'default' : 'pointer', opacity: loading ? 0.6 : 1,
1602
- transition: 'border-color 0.15s, color 0.15s',
1603
- }}
1604
- onMouseEnter={e => { if (!loading) { (e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)'; (e.currentTarget as HTMLElement).style.color = 'var(--text)'; }}}
1605
- onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--border-2)'; (e.currentTarget as HTMLElement).style.color = 'var(--muted)'; }}
1606
- >{loading ? '…' : children}</button>
1607
- );
1608
- }
1609
-
1610
- function InfoRow({ label, value }: { label: string; value: string }) {
1611
- return (
1612
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 13 }}>
1613
- <span style={{ color: 'var(--muted)' }}>{label}</span>
1614
- <span style={{ color: 'var(--text)', fontWeight: 500 }}>{value}</span>
1615
- </div>
1616
- );
1617
- }
1618
-
1619
- function IconBtn({ children, onClick, title, loading }: { children: React.ReactNode; onClick?: () => void; title?: string; loading?: boolean }) {
1620
- return (
1621
- <button
1622
- onClick={onClick}
1623
- title={title}
1624
- disabled={loading}
1625
- style={{
1626
- display: 'flex', alignItems: 'center', justifyContent: 'center',
1627
- width: 32, height: 32, borderRadius: 8, border: '1px solid var(--border)',
1628
- background: 'var(--surface)', color: 'var(--muted)',
1629
- cursor: loading ? 'default' : 'pointer', opacity: loading ? 0.5 : 1,
1630
- transition: 'all 0.15s',
1631
- }}
1632
- onMouseEnter={e => { if (!loading) { const el = e.currentTarget as HTMLElement; el.style.borderColor = 'var(--border-2)'; el.style.color = 'var(--text)'; el.style.background = 'var(--surface-2)'; }}}
1633
- onMouseLeave={e => { const el = e.currentTarget as HTMLElement; el.style.borderColor = 'var(--border)'; el.style.color = 'var(--muted)'; el.style.background = 'var(--surface)'; }}
1634
- >
1635
- {loading ? <span style={{ fontSize: 11 }}>…</span> : children}
1636
- </button>
1637
- );
1638
- }
1639
-
1640
- function Btn({ children, onClick, color, textColor }: {
1641
- children: React.ReactNode; onClick?: () => void;
1642
- color?: string; textColor?: string;
1643
- }) {
1644
- return (
1645
- <button onClick={onClick} style={{
1646
- background: color ?? 'var(--border)', color: textColor ?? '#fff',
1647
- border: 'none', borderRadius: 'var(--radius)', padding: '8px 18px',
1648
- fontSize: 13, fontWeight: 600, cursor: 'pointer',
1649
- fontFamily: 'var(--font-sans)', transition: 'opacity 0.15s, transform 0.15s',
1650
- }}
1651
- onMouseEnter={e => { (e.currentTarget as HTMLElement).style.opacity = '0.85'; (e.currentTarget as HTMLElement).style.transform = 'translateY(-1px)'; }}
1652
- onMouseLeave={e => { (e.currentTarget as HTMLElement).style.opacity = '1'; (e.currentTarget as HTMLElement).style.transform = 'translateY(0)'; }}
1653
- >{children}</button>
1654
- );
1655
- }
1656
-
1657
- function Modal({ title, children, onClose }: {
1658
- title: string; children: React.ReactNode; onClose: () => void;
1659
- }) {
1660
- return (
1661
- <Portal>
1662
- <div style={{
1663
- position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.6)',
1664
- display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999,
1665
- backdropFilter: 'blur(4px)',
1666
- }}>
1667
- <div style={{
1668
- background: 'var(--surface)', border: '1px solid var(--border)',
1669
- borderRadius: 'var(--radius-lg)', padding: '28px', width: 440,
1670
- boxShadow: 'var(--shadow-modal)',
1671
- display: 'flex', flexDirection: 'column', gap: 16,
1672
- maxHeight: '90vh', overflow: 'auto',
1673
- }}>
1674
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
1675
- <h3 style={{ margin: 0, fontSize: 15, fontWeight: 600, color: 'var(--text)' }}>{title}</h3>
1676
- <button onClick={onClose} style={{
1677
- background: 'none', border: 'none', cursor: 'pointer',
1678
- color: 'var(--muted)', fontSize: 18, lineHeight: 1, fontFamily: 'var(--font-sans)',
1679
- }}>×</button>
1680
- </div>
1681
- {children}
1682
- </div>
1683
- </div>
1684
- </Portal>
1685
- );
1686
- }
1687
-
1688
- function PageLoader() {
1689
- return (
1690
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', color: 'var(--muted)', fontSize: 13 }}>
1691
- Loading…
1692
- </div>
1693
- );
1694
- }
1695
-
1696
- function NotFound({ slug }: { slug: string }) {
1697
- return (
1698
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh', gap: 12 }}>
1699
- <p style={{ color: 'var(--muted)', fontSize: 13 }}>Agent not found: <code style={{ fontFamily: 'var(--font-mono)' }}>{slug}</code></p>
1700
- <Link href="/" style={{ color: 'var(--accent)', fontSize: 13, textDecoration: 'none' }}>← Back to dashboard</Link>
1701
- </div>
1702
- );
1703
- }
1704
-
1705
- // ─── History ──────────────────────────────────────────────────────────────────
1706
-
1707
- // ── Diff panel ───────────────────────────────────────────────────────────────
1708
-
1709
- function SkillDiff({ snapshot, current }: { snapshot: AgentSnapshot; current: AgentSnapshot | null }) {
1710
- const snapSkills = snapshot.skillsJson;
1711
- const currSkills = current ? current.skillsJson : null;
1712
-
1713
- // Build lookup maps
1714
- const snapMap = new Map(snapSkills.map(s => [`${s.category}/${s.filename}`, s.content]));
1715
- const currMap = currSkills ? new Map(currSkills.map(s => [`${s.category}/${s.filename}`, s.content])) : new Map<string, string>();
1716
-
1717
- const allKeys = new Set([...snapMap.keys(), ...currMap.keys()]);
1718
- const files: { key: string; status: 'added' | 'removed' | 'modified' | 'same'; diff?: DiffLine[] }[] = [];
1719
-
1720
- for (const key of allKeys) {
1721
- const snapContent = snapMap.get(key);
1722
- const currContent = currMap.get(key);
1723
- if (snapContent === undefined) {
1724
- files.push({ key, status: 'added' });
1725
- } else if (currContent === undefined) {
1726
- files.push({ key, status: 'removed' });
1727
- } else if (snapContent !== currContent) {
1728
- files.push({ key, status: 'modified', diff: lineDiff(snapContent, currContent) });
1729
- }
1730
- }
1731
-
1732
- if (files.length === 0) {
1733
- return <p style={{ fontSize: 13, color: 'var(--subtle)', margin: 0 }}>No skill changes.</p>;
1734
- }
1735
-
1736
- return (
1737
- <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
1738
- {files.map(f => (
1739
- <div key={f.key} style={{ border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden' }}>
1740
- <div style={{
1741
- padding: '7px 12px', background: 'var(--surface-2)',
1742
- borderBottom: '1px solid var(--border)',
1743
- display: 'flex', alignItems: 'center', gap: 8,
1744
- }}>
1745
- <span style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text)' }}>{f.key}</span>
1746
- <span style={{
1747
- fontSize: 10, padding: '1px 6px', borderRadius: 4, fontWeight: 600,
1748
- background: f.status === 'added' ? 'rgba(22,163,74,0.15)' : f.status === 'removed' ? 'rgba(239,68,68,0.15)' : 'rgba(234,179,8,0.15)',
1749
- color: f.status === 'added' ? '#16a34a' : f.status === 'removed' ? '#ef4444' : '#ca8a04',
1750
- }}>{f.status}</span>
1751
- </div>
1752
- {f.diff && (
1753
- <pre style={{
1754
- margin: 0, padding: '10px 0', fontSize: 11.5, fontFamily: 'var(--font-mono)',
1755
- lineHeight: 1.6, overflow: 'auto', maxHeight: 320,
1756
- }}>
1757
- {f.diff.map((line, i) => (
1758
- <div key={i} style={{
1759
- padding: '0 12px',
1760
- background: line.type === 'add' ? 'rgba(22,163,74,0.1)' : line.type === 'remove' ? 'rgba(239,68,68,0.1)' : 'transparent',
1761
- color: line.type === 'add' ? '#16a34a' : line.type === 'remove' ? '#ef4444' : 'var(--muted)',
1762
- }}>
1763
- {line.type === 'add' ? '+ ' : line.type === 'remove' ? '- ' : ' '}{line.line}
1764
- </div>
1765
- ))}
1766
- </pre>
1767
- )}
1768
- {f.status === 'added' && <p style={{ margin: '8px 12px', fontSize: 12, color: '#16a34a' }}>File added since this snapshot.</p>}
1769
- {f.status === 'removed' && <p style={{ margin: '8px 12px', fontSize: 12, color: '#ef4444' }}>File deleted since this snapshot.</p>}
1770
- </div>
1771
- ))}
1772
- </div>
1773
- );
1774
- }
1775
-
1776
- function PermsDiff({ snapshot, current }: { snapshot: AgentSnapshot; current: AgentSnapshot | null }) {
1777
- const currAllowed = new Set(current ? current.allowedTools : []);
1778
- const currDenied = new Set(current ? current.deniedTools : []);
1779
- const snapAllowed = new Set(snapshot.allowedTools);
1780
- const snapDenied = new Set(snapshot.deniedTools);
1781
-
1782
- const addedAllowed = [...currAllowed].filter(t => !snapAllowed.has(t));
1783
- const removedAllowed = [...snapAllowed].filter(t => !currAllowed.has(t));
1784
- const addedDenied = [...currDenied].filter(t => !snapDenied.has(t));
1785
- const removedDenied = [...snapDenied].filter(t => !currDenied.has(t));
1786
-
1787
- if (!addedAllowed.length && !removedAllowed.length && !addedDenied.length && !removedDenied.length) {
1788
- return <p style={{ fontSize: 13, color: 'var(--subtle)', margin: 0 }}>No permission changes.</p>;
1789
- }
1790
- const row = (label: string, items: string[], color: string) => items.length > 0 && (
1791
- <div key={label}>
1792
- <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--muted)', marginBottom: 4 }}>{label}</div>
1793
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
1794
- {items.map(t => (
1795
- <span key={t} style={{ fontSize: 11.5, fontFamily: 'var(--font-mono)', padding: '2px 8px', borderRadius: 4, background: `${color}22`, color }}>{t}</span>
1796
- ))}
1797
- </div>
1798
- </div>
1799
- );
1800
- return (
1801
- <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
1802
- {row('Allowed tools added', addedAllowed, '#16a34a')}
1803
- {row('Allowed tools removed', removedAllowed, '#ef4444')}
1804
- {row('Denied tools added', addedDenied, '#ef4444')}
1805
- {row('Denied tools removed', removedDenied, '#16a34a')}
1806
- </div>
1807
- );
1808
- }
1809
-
1810
- function McpsDiff({ snapshot, current, allMcps }: { snapshot: AgentSnapshot; current: AgentSnapshot | null; allMcps: McpServer[] }) {
1811
- const nameFor = (id: string) => allMcps.find(m => m.id === id)?.name ?? id;
1812
- const currIds = new Set(current ? current.mcpIds : []);
1813
- const snapIds = new Set(snapshot.mcpIds);
1814
- const added = [...currIds].filter(id => !snapIds.has(id));
1815
- const removed = [...snapIds].filter(id => !currIds.has(id));
1816
- if (!added.length && !removed.length) return <p style={{ fontSize: 13, color: 'var(--subtle)', margin: 0 }}>No MCP changes.</p>;
1817
- return (
1818
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
1819
- {added.length > 0 && <div>
1820
- <div style={{ fontSize: 11, fontWeight: 600, color: '#16a34a', marginBottom: 4 }}>Added</div>
1821
- {added.map(id => <div key={id} style={{ fontSize: 12.5, color: '#16a34a' }}>+ {nameFor(id)}</div>)}
1822
- </div>}
1823
- {removed.length > 0 && <div>
1824
- <div style={{ fontSize: 11, fontWeight: 600, color: '#ef4444', marginBottom: 4 }}>Removed</div>
1825
- {removed.map(id => <div key={id} style={{ fontSize: 12.5, color: '#ef4444' }}>- {nameFor(id)}</div>)}
1826
- </div>}
1827
- </div>
1828
- );
1829
- }
1830
-
1831
- // ── Trigger badge ─────────────────────────────────────────────────────────────
1832
-
1833
- const TRIGGER_COLORS: Record<string, { bg: string; color: string }> = {
1834
- skills: { bg: 'rgba(59,130,246,0.12)', color: '#3b82f6' },
1835
- permissions: { bg: 'rgba(234,179,8,0.12)', color: '#ca8a04' },
1836
- mcps: { bg: 'rgba(168,85,247,0.12)', color: '#a855f7' },
1837
- 'claude-md': { bg: 'rgba(236,72,153,0.12)', color: '#ec4899' },
1838
- manual: { bg: 'rgba(22,163,74,0.12)', color: '#16a34a' },
1839
- };
1840
-
1841
- function TriggerBadge({ trigger }: { trigger: string }) {
1842
- const c = TRIGGER_COLORS[trigger] ?? { bg: 'var(--surface-2)', color: 'var(--muted)' };
1843
- const label: Record<string, string> = {
1844
- skills: 'Skills', permissions: 'Tools', mcps: 'MCPs',
1845
- 'claude-md': 'System Prompt', manual: 'Manual', restrictions: 'Channels',
1846
- };
1847
- return (
1848
- <span style={{
1849
- fontSize: 10.5, fontWeight: 600, padding: '3px 8px', borderRadius: 6,
1850
- background: c.bg, color: c.color, letterSpacing: '0.03em',
1851
- }}>{label[trigger] ?? trigger}</span>
1852
- );
1853
- }
1854
-
1855
- // ── Main HistoryTab component ─────────────────────────────────────────────────
1856
-
1857
- function HistoryTab({ agentId, canEdit }: { agentId: string; canEdit: boolean }) {
1858
- const [snapshots, setSnapshots] = useState<AgentSnapshot[]>([]);
1859
- const [loading, setLoading] = useState(true);
1860
- const [selectedId, setSelectedId] = useState<string | null>(null);
1861
- const [fullSnapshot, setFullSnapshot] = useState<AgentSnapshot | null>(null);
1862
- const [compareId, setCompareId] = useState<string>('__current__');
1863
- const [compareSnapshot, setCompareSnapshot] = useState<AgentSnapshot | null>(null);
1864
- // Live current state — fetched once and used as the "Current state" comparison target
1865
- const [liveSnapshot, setLiveSnapshot] = useState<AgentSnapshot | null>(null);
1866
- const [allMcps, setAllMcps] = useState<McpServer[]>([]);
1867
- const [restoring, setRestoring] = useState(false);
1868
- const [loadingDetail, setLoadingDetail] = useState(false);
1869
- const [msg, setMsg] = useState('');
1870
-
1871
- // Load snapshot list + MCP catalog (fast path — no live state on mount)
1872
- useEffect(() => {
1873
- Promise.all([
1874
- fetch(`/api/agents/${agentId}/snapshots`).then(r => r.json()),
1875
- fetch('/api/mcps').then(r => r.json()),
1876
- ]).then(([snaps, mcps]) => {
1877
- setSnapshots(Array.isArray(snaps) ? snaps : []);
1878
- setAllMcps(mcps);
1879
- setLoading(false);
1880
- });
1881
- }, [agentId]);
1882
-
1883
- // Lazy-load live state only when user picks "Compare with current"
1884
- useEffect(() => {
1885
- if (compareId !== '__current__' || liveSnapshot) return;
1886
- Promise.all([
1887
- fetch(`/api/agents/${agentId}/skills`).then(r => r.json()),
1888
- fetch(`/api/agents/${agentId}/permissions`).then(r => r.json()),
1889
- fetch(`/api/agents/${agentId}/mcps`).then(r => r.json()),
1890
- fetch(`/api/agents/${agentId}/claude-md`).then(r => r.text()),
1891
- ]).then(([skills, perms, agentMcps, claudeMd]) => {
1892
- setLiveSnapshot({
1893
- id: '__current__',
1894
- agentId,
1895
- trigger: 'manual',
1896
- createdBy: 'current',
1897
- skillsJson: skills.map((s: Skill) => ({
1898
- category: s.category,
1899
- filename: s.filename,
1900
- content: s.content,
1901
- sort_order: s.sortOrder,
1902
- })),
1903
- allowedTools: perms?.allowedTools ?? [],
1904
- deniedTools: perms?.deniedTools ?? [],
1905
- mcpIds: (agentMcps as McpServer[]).map(m => m.id),
1906
- compiledMd: claudeMd ?? '',
1907
- allowedChannels: [],
1908
- createdAt: new Date(),
1909
- });
1910
- });
1911
- }, [agentId, compareId, liveSnapshot]);
1912
-
1913
- // Load full snapshot when selected
1914
- useEffect(() => {
1915
- if (!selectedId) { setFullSnapshot(null); return; }
1916
- setFullSnapshot(null);
1917
- setLoadingDetail(true);
1918
- fetch(`/api/agents/${agentId}/snapshots/${selectedId}`)
1919
- .then(r => r.json())
1920
- .then(snap => { setFullSnapshot(snap); setLoadingDetail(false); })
1921
- .catch(() => setLoadingDetail(false));
1922
- }, [agentId, selectedId]);
1923
-
1924
- // Load compare snapshot when compareId changes
1925
- useEffect(() => {
1926
- if (compareId === '__current__') { setCompareSnapshot(null); return; }
1927
- fetch(`/api/agents/${agentId}/snapshots/${compareId}`)
1928
- .then(r => r.json())
1929
- .then(setCompareSnapshot);
1930
- }, [agentId, compareId]);
1931
-
1932
- const handleCreateManual = async () => {
1933
- const label = window.prompt('Snapshot label (optional):') ?? '';
1934
- const r = await fetch(`/api/agents/${agentId}/snapshots`, {
1935
- method: 'POST', headers: { 'Content-Type': 'application/json' },
1936
- body: JSON.stringify({ label: label || null }),
1937
- });
1938
- if (r.ok) {
1939
- const snap = await r.json();
1940
- setSnapshots(prev => [snap, ...prev]);
1941
- setMsg('Snapshot created.');
1942
- }
1943
- };
1944
-
1945
- const handleDelete = async (id: string) => {
1946
- if (!window.confirm('Delete this snapshot?')) return;
1947
- await fetch(`/api/agents/${agentId}/snapshots/${id}`, { method: 'DELETE' });
1948
- setSnapshots(prev => prev.filter(s => s.id !== id));
1949
- if (selectedId === id) { setSelectedId(null); setFullSnapshot(null); }
1950
- setMsg('Snapshot deleted.');
1951
- };
1952
-
1953
- const handleRestore = async (snap: AgentSnapshot) => {
1954
- if (!window.confirm(`Restore to snapshot from ${new Date(snap.createdAt).toLocaleString()}?\n\nThis will replace current skills, permissions, and MCPs.`)) return;
1955
- setRestoring(true);
1956
- const r = await fetch(`/api/agents/${agentId}/snapshots/${snap.id}/restore`, { method: 'POST' });
1957
- setRestoring(false);
1958
- if (r.ok) {
1959
- setMsg('Restored. Agent is reloading.');
1960
- } else {
1961
- const err = await r.json();
1962
- setMsg(`Restore failed: ${err.error}`);
1963
- }
1964
- };
1965
-
1966
- const fmt = (d: Date | string) => {
1967
- const dt = new Date(d);
1968
- return `${dt.toLocaleDateString()} ${dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
1969
- };
1970
-
1971
- // Build comparison target: live state or a selected historical snapshot
1972
- const currentAsSnapshot: AgentSnapshot | null = compareId === '__current__' ? liveSnapshot : compareSnapshot;
1973
-
1974
- if (loading) return (
1975
- <div style={{ display: 'flex', gap: 20, minHeight: 500 }}>
1976
- <div style={{ width: 280, flexShrink: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
1977
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
1978
- <div style={{ width: 70, height: 13, borderRadius: 5, background: 'var(--surface-2)' }} />
1979
- <div style={{ width: 110, height: 30, borderRadius: 8, background: 'var(--surface-2)' }} />
1980
- </div>
1981
- {[1, 2, 3, 4].map(i => (
1982
- <div key={i} style={{
1983
- background: '#fff', borderRadius: 'var(--radius)', padding: '14px 16px',
1984
- boxShadow: 'var(--shadow-card)', opacity: 1 - (i - 1) * 0.2,
1985
- }}>
1986
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
1987
- <div style={{ width: 70, height: 18, borderRadius: 6, background: 'var(--surface-2)' }} />
1988
- <div style={{ width: 50, height: 11, borderRadius: 4, background: 'var(--surface-2)' }} />
1989
- </div>
1990
- <div style={{ width: '55%', height: 11, borderRadius: 4, background: 'var(--surface-2)' }} />
1991
- </div>
1992
- ))}
1993
- </div>
1994
- <div style={{ flex: 1, background: '#fff', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-card)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
1995
- <div style={{ fontSize: 13, color: 'var(--subtle)' }}>Loading history…</div>
1996
- </div>
1997
- </div>
1998
- );
1999
-
2000
- return (
2001
- <div style={{ display: 'flex', gap: 20, minHeight: 500, alignItems: 'flex-start' }}>
2002
-
2003
- {/* ── Left: snapshot list ────────────────────────────────────────────── */}
2004
- <div style={{ width: 280, flexShrink: 0 }}>
2005
- {/* Header */}
2006
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
2007
- <span style={{
2008
- fontSize: 11, fontWeight: 700, letterSpacing: '0.08em',
2009
- color: 'var(--subtle)', textTransform: 'uppercase',
2010
- }}>
2011
- {snapshots.length} snapshot{snapshots.length !== 1 ? 's' : ''}
2012
- </span>
2013
- {canEdit && (
2014
- <button onClick={handleCreateManual} style={{
2015
- background: 'var(--accent)', color: '#fff', border: 'none',
2016
- borderRadius: 'var(--radius-sm)', padding: '7px 13px',
2017
- fontSize: 12, fontWeight: 600, cursor: 'pointer',
2018
- fontFamily: 'var(--font-sans)', letterSpacing: '-0.01em',
2019
- boxShadow: 'var(--shadow-sm)', transition: 'opacity 0.15s',
2020
- }}
2021
- onMouseEnter={e => (e.currentTarget.style.opacity = '0.85')}
2022
- onMouseLeave={e => (e.currentTarget.style.opacity = '1')}
2023
- >+ Snapshot</button>
2024
- )}
2025
- </div>
2026
-
2027
- {msg && (
2028
- <div style={{
2029
- fontSize: 12, color: '#16a34a', background: '#f0fdf4',
2030
- border: '1px solid #bbf7d0', borderRadius: 8,
2031
- padding: '8px 12px', marginBottom: 10,
2032
- }}>{msg}</div>
2033
- )}
2034
-
2035
- {snapshots.length === 0 ? (
2036
- <div style={{
2037
- background: '#fff', borderRadius: 'var(--radius)',
2038
- boxShadow: 'var(--shadow-card)', padding: '28px 20px',
2039
- textAlign: 'center',
2040
- }}>
2041
- <Camera size={22} style={{ marginBottom: 10, color: 'var(--border-2)' }} />
2042
- <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', marginBottom: 6 }}>No snapshots yet</div>
2043
- <div style={{ fontSize: 12, color: 'var(--muted)', lineHeight: 1.6 }}>
2044
- Snapshots are saved automatically when you change skills, MCPs, or permissions.
2045
- </div>
2046
- </div>
2047
- ) : (
2048
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
2049
- {snapshots.map(snap => {
2050
- const isSelected = snap.id === selectedId;
2051
- return (
2052
- <div
2053
- key={snap.id}
2054
- onClick={() => { setSelectedId(isSelected ? null : snap.id); setCompareId('__current__'); setCompareSnapshot(null); }}
2055
- style={{
2056
- background: '#fff',
2057
- borderRadius: 'var(--radius)',
2058
- boxShadow: isSelected ? '0 0 0 2px var(--accent), var(--shadow-card)' : 'var(--shadow-card)',
2059
- padding: '13px 15px', cursor: 'pointer',
2060
- transition: 'box-shadow 0.15s',
2061
- }}
2062
- onMouseEnter={e => { if (!isSelected) (e.currentTarget as HTMLElement).style.boxShadow = 'var(--shadow-hover)'; }}
2063
- onMouseLeave={e => { if (!isSelected) (e.currentTarget as HTMLElement).style.boxShadow = 'var(--shadow-card)'; }}
2064
- >
2065
- {/* Top row: badge + author */}
2066
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 7 }}>
2067
- <TriggerBadge trigger={snap.trigger} />
2068
- <span style={{ fontSize: 11, color: 'var(--subtle)', fontFamily: 'var(--font-mono)' }}>{snap.createdBy}</span>
2069
- </div>
2070
-
2071
- {/* Timestamp */}
2072
- <div style={{ fontSize: 12.5, fontWeight: 500, color: 'var(--text)', marginBottom: snap.label ? 4 : 0 }}>
2073
- {fmt(snap.createdAt)}
2074
- </div>
2075
-
2076
- {/* Optional label */}
2077
- {snap.label && (
2078
- <div style={{
2079
- fontSize: 11.5, color: 'var(--muted)', fontStyle: 'italic',
2080
- marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
2081
- }}>{snap.label}</div>
2082
- )}
2083
-
2084
- {/* Actions — only when selected */}
2085
- {isSelected && canEdit && (
2086
- <div style={{ display: 'flex', gap: 7, marginTop: 11, paddingTop: 11, borderTop: '1px solid var(--border)' }} onClick={e => e.stopPropagation()}>
2087
- <button
2088
- onClick={() => handleRestore(snap)}
2089
- disabled={restoring}
2090
- style={{
2091
- flex: 1, fontSize: 12, padding: '6px 0', borderRadius: 6, cursor: restoring ? 'not-allowed' : 'pointer',
2092
- background: '#16a34a', color: '#fff', border: 'none',
2093
- fontFamily: 'var(--font-sans)', fontWeight: 600, transition: 'opacity 0.15s',
2094
- }}
2095
- onMouseEnter={e => { if (!restoring) (e.currentTarget.style.opacity = '0.85'); }}
2096
- onMouseLeave={e => (e.currentTarget.style.opacity = '1')}
2097
- >{restoring ? 'Restoring…' : 'Restore'}</button>
2098
- <button
2099
- onClick={() => handleDelete(snap.id)}
2100
- style={{
2101
- fontSize: 12, padding: '6px 12px', borderRadius: 6, cursor: 'pointer',
2102
- background: 'transparent', color: 'var(--red)',
2103
- border: '1.5px solid rgba(220,38,38,0.25)',
2104
- fontFamily: 'var(--font-sans)', fontWeight: 500, transition: 'all 0.15s',
2105
- }}
2106
- onMouseEnter={e => { (e.currentTarget.style.background = 'var(--red)'); (e.currentTarget.style.color = '#fff'); (e.currentTarget.style.borderColor = 'var(--red)'); }}
2107
- onMouseLeave={e => { (e.currentTarget.style.background = 'transparent'); (e.currentTarget.style.color = 'var(--red)'); (e.currentTarget.style.borderColor = 'rgba(220,38,38,0.25)'); }}
2108
- >Delete</button>
2109
- </div>
2110
- )}
2111
- </div>
2112
- );
2113
- })}
2114
- </div>
2115
- )}
2116
- </div>
2117
-
2118
- {/* ── Right: diff panel ─────────────────────────────────────────────── */}
2119
- {loadingDetail ? (
2120
- <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 16 }}>
2121
- {/* Compare bar skeleton */}
2122
- <div style={{ background: '#fff', borderRadius: 'var(--radius)', boxShadow: 'var(--shadow-card)', padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 12 }}>
2123
- <div style={{ width: 90, height: 14, borderRadius: 4, background: 'var(--surface-2)' }} />
2124
- <div style={{ flex: 1, height: 34, borderRadius: 8, background: 'var(--surface-2)' }} />
2125
- </div>
2126
- {/* Section skeletons */}
2127
- {[120, 80, 60, 200].map((h, i) => (
2128
- <div key={i} style={{ background: '#fff', borderRadius: 'var(--radius)', boxShadow: 'var(--shadow-card)', overflow: 'hidden', opacity: 1 - i * 0.15 }}>
2129
- <div style={{ padding: '12px 18px', borderBottom: '1px solid var(--border)' }}>
2130
- <div style={{ width: 70, height: 11, borderRadius: 4, background: 'var(--surface-2)' }} />
2131
- </div>
2132
- <div style={{ padding: '16px 18px' }}>
2133
- <div style={{ height: h, borderRadius: 6, background: 'var(--surface-2)' }} />
2134
- </div>
2135
- </div>
2136
- ))}
2137
- </div>
2138
- ) : fullSnapshot ? (
2139
- <div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 16 }}>
2140
-
2141
- {/* Compare bar */}
2142
- <div style={{
2143
- background: '#fff', borderRadius: 'var(--radius)',
2144
- boxShadow: 'var(--shadow-card)', padding: '14px 18px',
2145
- display: 'flex', alignItems: 'center', gap: 12,
2146
- }}>
2147
- <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--muted)', letterSpacing: '0.02em', whiteSpace: 'nowrap' }}>
2148
- Compare with
2149
- </span>
2150
- <select
2151
- value={compareId}
2152
- onChange={e => setCompareId(e.target.value)}
2153
- style={{
2154
- flex: 1, fontSize: 13, padding: '7px 12px', borderRadius: 8,
2155
- border: '1.5px solid var(--border)', background: 'var(--surface-2)',
2156
- color: 'var(--text)', fontFamily: 'var(--font-sans)', outline: 'none', cursor: 'pointer',
2157
- }}
2158
- onFocus={e => (e.currentTarget.style.borderColor = 'var(--accent)')}
2159
- onBlur={e => (e.currentTarget.style.borderColor = 'var(--border)')}
2160
- >
2161
- <option value="__current__">Current state</option>
2162
- {snapshots.filter(s => s.id !== selectedId).map(s => (
2163
- <option key={s.id} value={s.id}>{fmt(s.createdAt)} — {s.trigger}{s.label ? ` · ${s.label}` : ''}</option>
2164
- ))}
2165
- </select>
2166
- </div>
2167
-
2168
- {/* Diff sections — wait for compare target to load */}
2169
- {!currentAsSnapshot ? (
2170
- <div style={{
2171
- background: '#fff', borderRadius: 'var(--radius)', boxShadow: 'var(--shadow-card)',
2172
- padding: '24px 18px', textAlign: 'center', color: 'var(--subtle)', fontSize: 13,
2173
- }}>
2174
- Loading comparison…
2175
- </div>
2176
- ) : [
2177
- { title: 'Skills', content: <SkillDiff snapshot={fullSnapshot} current={currentAsSnapshot} /> },
2178
- { title: 'Tools', content: <PermsDiff snapshot={fullSnapshot} current={currentAsSnapshot} /> },
2179
- { title: 'MCPs', content: <McpsDiff snapshot={fullSnapshot} current={currentAsSnapshot} allMcps={allMcps} /> },
2180
- { title: 'System Prompt', content: (() => {
2181
- if (!fullSnapshot.compiledMd || !currentAsSnapshot.compiledMd)
2182
- return <p style={{ fontSize: 12.5, color: 'var(--subtle)', margin: 0 }}>Not available for this snapshot</p>;
2183
- const diff = lineDiff(fullSnapshot.compiledMd.trim(), currentAsSnapshot.compiledMd.trim());
2184
- const changed = diff.some(l => l.type !== 'same');
2185
- if (!changed) return <p style={{ fontSize: 12.5, color: 'var(--subtle)', margin: 0 }}>No changes</p>;
2186
- return (
2187
- <pre style={{
2188
- margin: 0, padding: '14px 16px', borderRadius: 8, fontSize: 12,
2189
- fontFamily: 'var(--font-mono)', background: 'var(--surface-2)',
2190
- border: '1px solid var(--border)', overflow: 'auto', maxHeight: 380,
2191
- color: 'var(--text)', lineHeight: 1.7,
2192
- }}>
2193
- {diff.map((l, i) => (
2194
- <div key={i} style={{
2195
- background: l.type === 'add' ? 'rgba(34,197,94,0.12)' : l.type === 'remove' ? 'rgba(239,68,68,0.10)' : 'transparent',
2196
- color: l.type === 'add' ? '#16a34a' : l.type === 'remove' ? '#dc2626' : 'inherit',
2197
- padding: '1px 6px', borderRadius: 3, marginBottom: 1,
2198
- }}>
2199
- {l.type === 'add' ? '+ ' : l.type === 'remove' ? '- ' : ' '}{l.line}
2200
- </div>
2201
- ))}
2202
- </pre>
2203
- );
2204
- })(),
2205
- },
2206
- ].map(({ title, content }) => (
2207
- <div key={title} style={{
2208
- background: '#fff', borderRadius: 'var(--radius)',
2209
- boxShadow: 'var(--shadow-card)', overflow: 'hidden',
2210
- }}>
2211
- <div style={{
2212
- padding: '12px 18px', borderBottom: '1px solid var(--border)',
2213
- fontSize: 11, fontWeight: 700, letterSpacing: '0.08em',
2214
- color: 'var(--muted)', textTransform: 'uppercase',
2215
- }}>{title}</div>
2216
- <div style={{ padding: '16px 18px' }}>{content}</div>
2217
- </div>
2218
- ))}
2219
- </div>
2220
- ) : (
2221
- <div style={{
2222
- flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
2223
- background: '#fff', borderRadius: 'var(--radius-lg)',
2224
- boxShadow: 'var(--shadow-card)', gap: 10, padding: 40,
2225
- }}>
2226
- <History size={32} style={{ color: 'var(--border-2)' }} />
2227
- <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>Select a snapshot</div>
2228
- <div style={{ fontSize: 13, color: 'var(--muted)', textAlign: 'center', maxWidth: 260, lineHeight: 1.6 }}>
2229
- Click any snapshot on the left to view what changed at that point in time.
2230
- </div>
2231
- </div>
2232
- )}
2233
- </div>
2234
- );
2235
- }