@desplega.ai/agent-swarm 1.20.0 → 1.51.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (561) hide show
  1. package/README.md +271 -169
  2. package/openapi.json +5015 -0
  3. package/package.json +40 -7
  4. package/plugin/commands/close-issue.md +7 -3
  5. package/plugin/commands/create-pr.md +18 -12
  6. package/plugin/commands/implement-issue.md +7 -3
  7. package/plugin/commands/respond-github.md +8 -4
  8. package/plugin/commands/review-pr.md +44 -10
  9. package/plugin/commands/start-leader.md +1 -3
  10. package/plugin/commands/start-worker.md +1 -3
  11. package/plugin/commands/work-on-task.md +22 -3
  12. package/plugin/pi-skills/close-issue/SKILL.md +90 -0
  13. package/plugin/pi-skills/create-pr/SKILL.md +99 -0
  14. package/plugin/pi-skills/implement-issue/SKILL.md +135 -0
  15. package/plugin/pi-skills/investigate-sentry-issue/SKILL.md +138 -0
  16. package/plugin/pi-skills/respond-github/SKILL.md +98 -0
  17. package/plugin/pi-skills/review-offered-task/SKILL.md +45 -0
  18. package/plugin/pi-skills/review-pr/SKILL.md +261 -0
  19. package/plugin/pi-skills/start-leader/SKILL.md +121 -0
  20. package/plugin/pi-skills/start-worker/SKILL.md +60 -0
  21. package/plugin/pi-skills/swarm-chat/SKILL.md +82 -0
  22. package/plugin/pi-skills/todos/SKILL.md +66 -0
  23. package/plugin/pi-skills/work-on-task/SKILL.md +65 -0
  24. package/plugin/skills/artifacts/examples/approval-flow.ts +34 -0
  25. package/plugin/skills/artifacts/examples/hono-dashboard.ts +31 -0
  26. package/plugin/skills/artifacts/examples/multi-artifact.ts +20 -0
  27. package/plugin/skills/artifacts/examples/static-report.sh +17 -0
  28. package/plugin/skills/artifacts/skill.md +71 -0
  29. package/src/agentmail/app.ts +65 -0
  30. package/src/agentmail/handlers.ts +262 -0
  31. package/src/agentmail/index.ts +9 -0
  32. package/src/agentmail/templates.ts +111 -0
  33. package/src/agentmail/types.ts +51 -0
  34. package/src/artifact-sdk/browser-sdk.ts +30 -0
  35. package/src/artifact-sdk/index.ts +2 -0
  36. package/src/artifact-sdk/localtunnel.d.ts +20 -0
  37. package/src/artifact-sdk/port.ts +12 -0
  38. package/src/artifact-sdk/server.ts +156 -0
  39. package/src/artifact-sdk/tunnel.ts +19 -0
  40. package/src/be/chunking.ts +193 -0
  41. package/src/be/db-queries/oauth.ts +90 -0
  42. package/src/be/db-queries/tracker.ts +182 -0
  43. package/src/be/db.ts +3327 -784
  44. package/src/be/embedding.ts +80 -0
  45. package/src/be/migrations/001_initial.sql +409 -0
  46. package/src/be/migrations/002_one_time_schedules.sql +59 -0
  47. package/src/be/migrations/003_workflows.sql +51 -0
  48. package/src/be/migrations/004_workflow_source.sql +81 -0
  49. package/src/be/migrations/005_epic_next_steps.sql +2 -0
  50. package/src/be/migrations/006_vcs_provider.sql +94 -0
  51. package/src/be/migrations/007_task_dir.sql +2 -0
  52. package/src/be/migrations/008_workflow_redesign.sql +85 -0
  53. package/src/be/migrations/009_tracker_integration.sql +144 -0
  54. package/src/be/migrations/010_step_diagnostics.sql +1 -0
  55. package/src/be/migrations/011_step_next_port.sql +1 -0
  56. package/src/be/migrations/012_trigger_schema.sql +1 -0
  57. package/src/be/migrations/013_task_output_schema.sql +2 -0
  58. package/src/be/migrations/014_prompt_templates.sql +33 -0
  59. package/src/be/migrations/015_workflow_workspace.sql +3 -0
  60. package/src/be/migrations/016_active_session_runner_session.sql +4 -0
  61. package/src/be/migrations/017_channel_activity_cursors.sql +6 -0
  62. package/src/be/migrations/018_fix_seed_double_version.sql +30 -0
  63. package/src/be/migrations/runner.ts +188 -0
  64. package/src/be/seed.ts +62 -0
  65. package/src/cli.tsx +231 -299
  66. package/src/commands/artifact.ts +241 -0
  67. package/src/commands/onboard/compose-generator.ts +169 -0
  68. package/src/commands/onboard/env-generator.ts +79 -0
  69. package/src/commands/onboard/manifest.ts +37 -0
  70. package/src/commands/onboard/presets.ts +85 -0
  71. package/src/commands/onboard/service-names.ts +47 -0
  72. package/src/commands/onboard/steps/core-credentials.tsx +111 -0
  73. package/src/commands/onboard/steps/custom-templates.tsx +168 -0
  74. package/src/commands/onboard/steps/generate.tsx +154 -0
  75. package/src/commands/onboard/steps/harness-credentials.tsx +195 -0
  76. package/src/commands/onboard/steps/harness.tsx +21 -0
  77. package/src/commands/onboard/steps/health-check.tsx +171 -0
  78. package/src/commands/onboard/steps/integration-github.tsx +105 -0
  79. package/src/commands/onboard/steps/integration-gitlab.tsx +79 -0
  80. package/src/commands/onboard/steps/integration-menu.tsx +58 -0
  81. package/src/commands/onboard/steps/integration-sentry.tsx +79 -0
  82. package/src/commands/onboard/steps/integration-slack.tsx +165 -0
  83. package/src/commands/onboard/steps/post-connect.tsx +145 -0
  84. package/src/commands/onboard/steps/post-dashboard.tsx +34 -0
  85. package/src/commands/onboard/steps/post-task.tsx +103 -0
  86. package/src/commands/onboard/steps/prereq-check.tsx +178 -0
  87. package/src/commands/onboard/steps/review.tsx +82 -0
  88. package/src/commands/onboard/steps/start.tsx +97 -0
  89. package/src/commands/onboard/templates.ts +34 -0
  90. package/src/commands/onboard/types.ts +259 -0
  91. package/src/commands/onboard.tsx +425 -0
  92. package/src/commands/runner.ts +1540 -630
  93. package/src/commands/setup.tsx +23 -38
  94. package/src/commands/shared/client-config.ts +41 -0
  95. package/src/commands/templates.ts +172 -0
  96. package/src/github/app.ts +8 -0
  97. package/src/github/handlers.ts +384 -151
  98. package/src/github/index.ts +1 -0
  99. package/src/github/mentions-aliases.test.ts +73 -0
  100. package/src/github/mentions.test.ts +3 -3
  101. package/src/github/mentions.ts +32 -6
  102. package/src/github/templates.ts +398 -0
  103. package/src/github/types.ts +1 -0
  104. package/src/gitlab/auth.ts +63 -0
  105. package/src/gitlab/handlers.ts +368 -0
  106. package/src/gitlab/index.ts +19 -0
  107. package/src/gitlab/reactions.ts +104 -0
  108. package/src/gitlab/templates.ts +140 -0
  109. package/src/gitlab/types.ts +130 -0
  110. package/src/heartbeat/heartbeat.ts +434 -0
  111. package/src/heartbeat/index.ts +1 -0
  112. package/src/heartbeat/templates.ts +30 -0
  113. package/src/hooks/hook.ts +555 -4
  114. package/src/hooks/tool-loop-detection.test.ts +158 -0
  115. package/src/hooks/tool-loop-detection.ts +167 -0
  116. package/src/http/active-sessions.ts +199 -0
  117. package/src/http/agents.ts +328 -0
  118. package/src/http/config.ts +191 -0
  119. package/src/http/core.ts +309 -0
  120. package/src/http/db-query.ts +91 -0
  121. package/src/http/ecosystem.ts +63 -0
  122. package/src/http/epics.ts +460 -0
  123. package/src/http/index.ts +216 -0
  124. package/src/http/mcp.ts +77 -0
  125. package/src/http/memory.ts +168 -0
  126. package/src/http/openapi.ts +109 -0
  127. package/src/http/poll.ts +299 -0
  128. package/src/http/prompt-templates.ts +412 -0
  129. package/src/http/repos.ts +195 -0
  130. package/src/http/route-def.ts +123 -0
  131. package/src/http/schedules.ts +426 -0
  132. package/src/http/session-data.ts +241 -0
  133. package/src/http/stats.ts +174 -0
  134. package/src/http/tasks.ts +468 -0
  135. package/src/http/trackers/index.ts +10 -0
  136. package/src/http/trackers/linear.ts +187 -0
  137. package/src/http/types.ts +12 -0
  138. package/src/http/utils.ts +87 -0
  139. package/src/http/webhooks.ts +432 -0
  140. package/src/http/workflows.ts +530 -0
  141. package/src/http.ts +1 -1890
  142. package/src/linear/README.md +65 -0
  143. package/src/linear/app.ts +48 -0
  144. package/src/linear/client.ts +18 -0
  145. package/src/linear/index.ts +1 -0
  146. package/src/linear/oauth.ts +35 -0
  147. package/src/linear/outbound.ts +212 -0
  148. package/src/linear/sync.ts +567 -0
  149. package/src/linear/templates.ts +47 -0
  150. package/src/linear/types.ts +7 -0
  151. package/src/linear/webhook.ts +104 -0
  152. package/src/oauth/README.md +66 -0
  153. package/src/oauth/index.ts +6 -0
  154. package/src/oauth/wrapper.ts +204 -0
  155. package/src/prompts/base-prompt.ts +150 -265
  156. package/src/prompts/defaults.ts +196 -0
  157. package/src/prompts/registry.ts +57 -0
  158. package/src/prompts/resolver.ts +296 -0
  159. package/src/prompts/session-templates.ts +604 -0
  160. package/src/providers/claude-adapter.ts +442 -0
  161. package/src/providers/index.ts +24 -0
  162. package/src/providers/pi-mono-adapter.ts +442 -0
  163. package/src/providers/pi-mono-extension.ts +624 -0
  164. package/src/providers/pi-mono-mcp-client.ts +124 -0
  165. package/src/providers/types.ts +75 -0
  166. package/src/scheduler/scheduler.test.ts +2 -0
  167. package/src/scheduler/scheduler.ts +231 -40
  168. package/src/server.ts +97 -6
  169. package/src/slack/HEURISTICS.md +105 -0
  170. package/src/slack/actions.ts +133 -0
  171. package/src/slack/app.ts +7 -0
  172. package/src/slack/assistant.ts +118 -0
  173. package/src/slack/blocks.ts +233 -0
  174. package/src/slack/channel-activity.ts +177 -0
  175. package/src/slack/commands.ts +31 -17
  176. package/src/slack/files.ts +1 -1
  177. package/src/slack/handlers.test.ts +114 -1
  178. package/src/slack/handlers.ts +230 -55
  179. package/src/slack/responses.ts +120 -67
  180. package/src/slack/router.ts +17 -99
  181. package/src/slack/templates.ts +55 -0
  182. package/src/slack/thread-buffer.ts +213 -0
  183. package/src/slack/watcher.ts +119 -4
  184. package/src/tests/agent-activity.test.ts +247 -0
  185. package/src/tests/agentmail-filters.test.ts +97 -0
  186. package/src/tests/artifact-sdk.test.ts +800 -0
  187. package/src/tests/base-prompt.test.ts +264 -0
  188. package/src/tests/build-pi-skills.test.ts +127 -0
  189. package/src/tests/channel-activity.test.ts +363 -0
  190. package/src/tests/claude-adapter.test.ts +126 -0
  191. package/src/tests/context-versioning.test.ts +425 -0
  192. package/src/tests/db-queries-oauth.test.ts +197 -0
  193. package/src/tests/db-queries-tracker.test.ts +230 -0
  194. package/src/tests/epics.test.ts +3 -3
  195. package/src/tests/error-tracker.test.ts +368 -0
  196. package/src/tests/fetch-resolved-env.test.ts +167 -0
  197. package/src/tests/generate-default-claude-md.test.ts +9 -1
  198. package/src/tests/generate-identity-templates.test.ts +124 -0
  199. package/src/tests/gitlab-auth.test.ts +109 -0
  200. package/src/tests/gitlab-handlers.test.ts +691 -0
  201. package/src/tests/gitlab-vcs-db.test.ts +177 -0
  202. package/src/tests/heartbeat.test.ts +364 -0
  203. package/src/tests/http-api-integration.test.ts +1698 -0
  204. package/src/tests/linear-outbound-sync.test.ts +200 -0
  205. package/src/tests/linear-webhook.test.ts +406 -0
  206. package/src/tests/match-route.test.ts +187 -0
  207. package/src/tests/memory.test.ts +737 -0
  208. package/src/tests/migration-runner-regressions.test.ts +86 -0
  209. package/src/tests/model-control.test.ts +338 -0
  210. package/src/tests/oauth-wrapper.test.ts +147 -0
  211. package/src/tests/onboard-compose.test.ts +138 -0
  212. package/src/tests/onboard-env.test.ts +174 -0
  213. package/src/tests/onboard-manifest.test.ts +137 -0
  214. package/src/tests/pi-mono-adapter.test.ts +234 -0
  215. package/src/tests/pool-session-logs.test.ts +199 -0
  216. package/src/tests/progress-dedup.test.ts +98 -0
  217. package/src/tests/prompt-template-github.test.ts +682 -0
  218. package/src/tests/prompt-template-remaining.test.ts +504 -0
  219. package/src/tests/prompt-template-resolver.test.ts +621 -0
  220. package/src/tests/prompt-template-session.test.ts +363 -0
  221. package/src/tests/prompt-templates-db.test.ts +616 -0
  222. package/src/tests/provider-adapter.test.ts +122 -0
  223. package/src/tests/provider-command-format.test.ts +98 -0
  224. package/src/tests/reload-config.test.ts +170 -0
  225. package/src/tests/runner-polling-api.test.ts +25 -20
  226. package/src/tests/scheduled-tasks.test.ts +104 -0
  227. package/src/tests/scheduler-backoff.test.ts +166 -0
  228. package/src/tests/self-improvement.test.ts +541 -0
  229. package/src/tests/session-attach.test.ts +536 -0
  230. package/src/tests/session-costs.test.ts +267 -1
  231. package/src/tests/slack-actions.test.ts +133 -0
  232. package/src/tests/slack-assistant.test.ts +136 -0
  233. package/src/tests/slack-blocks.test.ts +246 -0
  234. package/src/tests/slack-metadata-inheritance.test.ts +243 -0
  235. package/src/tests/slack-queue-offline.test.ts +174 -0
  236. package/src/tests/slack-router.test.ts +181 -0
  237. package/src/tests/slack-thread-buffer.test.ts +305 -0
  238. package/src/tests/slack-thread-followups.test.ts +298 -0
  239. package/src/tests/slack-watcher.test.ts +101 -0
  240. package/src/tests/structured-output.test.ts +307 -0
  241. package/src/tests/swarm-repos.test.ts +198 -0
  242. package/src/tests/task-cancellation.test.ts +6 -4
  243. package/src/tests/task-working-dir.test.ts +176 -0
  244. package/src/tests/template-fetch.test.ts +490 -0
  245. package/src/tests/tool-annotations.test.ts +371 -0
  246. package/src/tests/tracker-tools.test.ts +184 -0
  247. package/src/tests/update-profile-agentid.test.ts +248 -0
  248. package/src/tests/update-profile-api.test.ts +143 -3
  249. package/src/tests/update-profile-auth.test.ts +195 -0
  250. package/src/tests/validation-adapters.test.ts +86 -0
  251. package/src/tests/vcs-provider.test.ts +27 -0
  252. package/src/tests/workflow-agent-task.test.ts +196 -0
  253. package/src/tests/workflow-async-v2.test.ts +508 -0
  254. package/src/tests/workflow-convergence.test.ts +541 -0
  255. package/src/tests/workflow-definition-validation.test.ts +366 -0
  256. package/src/tests/workflow-engine-v2.test.ts +691 -0
  257. package/src/tests/workflow-executors.test.ts +736 -0
  258. package/src/tests/workflow-http-v2.test.ts +599 -0
  259. package/src/tests/workflow-integration-io.test.ts +902 -0
  260. package/src/tests/workflow-io-schemas.test.ts +624 -0
  261. package/src/tests/workflow-registry.test.ts +592 -0
  262. package/src/tests/workflow-retry-v2.test.ts +401 -0
  263. package/src/tests/workflow-retry-validation.test.ts +282 -0
  264. package/src/tests/workflow-schedule-trigger.test.ts +104 -0
  265. package/src/tests/workflow-template.test.ts +288 -0
  266. package/src/tests/workflow-trigger-schema.test.ts +359 -0
  267. package/src/tests/workflow-triggers-v2.test.ts +264 -0
  268. package/src/tests/workflow-versions.test.ts +208 -0
  269. package/src/tests/workflow-workspace.test.ts +272 -0
  270. package/src/tests/x402-client.test.ts +117 -0
  271. package/src/tests/x402-config.test.ts +182 -0
  272. package/src/tests/x402-spending-tracker.test.ts +185 -0
  273. package/src/tools/cancel-task.ts +2 -0
  274. package/src/tools/context-diff.ts +171 -0
  275. package/src/tools/context-history.ts +138 -0
  276. package/src/tools/create-channel.ts +1 -0
  277. package/src/tools/db-query.ts +78 -0
  278. package/src/tools/delete-channel.ts +132 -0
  279. package/src/tools/epics/assign-task-to-epic.ts +1 -0
  280. package/src/tools/epics/create-epic.ts +3 -2
  281. package/src/tools/epics/delete-epic.ts +2 -0
  282. package/src/tools/epics/get-epic-details.ts +2 -0
  283. package/src/tools/epics/list-epics.ts +2 -0
  284. package/src/tools/epics/unassign-task-from-epic.ts +1 -0
  285. package/src/tools/epics/update-epic.ts +7 -4
  286. package/src/tools/get-swarm.ts +2 -0
  287. package/src/tools/get-task-details.ts +2 -0
  288. package/src/tools/get-tasks.ts +27 -1
  289. package/src/tools/inject-learning.ts +106 -0
  290. package/src/tools/join-swarm.ts +17 -7
  291. package/src/tools/list-channels.ts +2 -0
  292. package/src/tools/list-services.ts +2 -0
  293. package/src/tools/memory-get.ts +56 -0
  294. package/src/tools/memory-search.ts +131 -0
  295. package/src/tools/my-agent-info.ts +2 -0
  296. package/src/tools/poll-task.ts +2 -20
  297. package/src/tools/post-message.ts +1 -0
  298. package/src/tools/prompt-templates/delete.ts +86 -0
  299. package/src/tools/prompt-templates/get.ts +89 -0
  300. package/src/tools/prompt-templates/index.ts +5 -0
  301. package/src/tools/prompt-templates/list.ts +95 -0
  302. package/src/tools/prompt-templates/preview.ts +84 -0
  303. package/src/tools/prompt-templates/set.ts +117 -0
  304. package/src/tools/read-messages.ts +2 -0
  305. package/src/tools/register-agentmail-inbox.ts +166 -0
  306. package/src/tools/register-service.ts +2 -0
  307. package/src/tools/schedules/create-schedule.ts +134 -24
  308. package/src/tools/schedules/delete-schedule.ts +2 -0
  309. package/src/tools/schedules/list-schedules.ts +20 -4
  310. package/src/tools/schedules/run-schedule-now.ts +1 -0
  311. package/src/tools/schedules/update-schedule.ts +49 -17
  312. package/src/tools/send-task.ts +132 -10
  313. package/src/tools/slack-download-file.ts +4 -2
  314. package/src/tools/slack-list-channels.ts +2 -0
  315. package/src/tools/slack-post.ts +2 -0
  316. package/src/tools/slack-read.ts +2 -0
  317. package/src/tools/slack-reply.ts +2 -0
  318. package/src/tools/slack-upload-file.ts +2 -0
  319. package/src/tools/store-progress.ts +205 -4
  320. package/src/tools/swarm-config/delete-config.ts +87 -0
  321. package/src/tools/swarm-config/get-config.ts +108 -0
  322. package/src/tools/swarm-config/index.ts +4 -0
  323. package/src/tools/swarm-config/list-config.ts +99 -0
  324. package/src/tools/swarm-config/set-config.ts +118 -0
  325. package/src/tools/task-action.ts +50 -5
  326. package/src/tools/task-dedup.ts +97 -0
  327. package/src/tools/templates.ts +53 -0
  328. package/src/tools/tool-config.ts +124 -0
  329. package/src/tools/tracker/index.ts +6 -0
  330. package/src/tools/tracker/tracker-link-epic.ts +64 -0
  331. package/src/tools/tracker/tracker-link-task.ts +64 -0
  332. package/src/tools/tracker/tracker-map-agent.ts +57 -0
  333. package/src/tools/tracker/tracker-status.ts +56 -0
  334. package/src/tools/tracker/tracker-sync-status.ts +42 -0
  335. package/src/tools/tracker/tracker-unlink.ts +41 -0
  336. package/src/tools/unregister-service.ts +2 -0
  337. package/src/tools/update-profile.ts +172 -17
  338. package/src/tools/update-service-status.ts +2 -0
  339. package/src/tools/utils.ts +10 -1
  340. package/src/tools/workflows/create-workflow.ts +129 -0
  341. package/src/tools/workflows/delete-workflow.ts +42 -0
  342. package/src/tools/workflows/get-workflow-run.ts +59 -0
  343. package/src/tools/workflows/get-workflow.ts +53 -0
  344. package/src/tools/workflows/index.ts +9 -0
  345. package/src/tools/workflows/list-workflow-runs.ts +48 -0
  346. package/src/tools/workflows/list-workflows.ts +42 -0
  347. package/src/tools/workflows/retry-workflow-run.ts +40 -0
  348. package/src/tools/workflows/trigger-workflow.ts +96 -0
  349. package/src/tools/workflows/update-workflow.ts +133 -0
  350. package/src/tracker/types.ts +51 -0
  351. package/src/types.ts +530 -14
  352. package/src/utils/credentials.test.ts +156 -0
  353. package/src/utils/credentials.ts +50 -0
  354. package/src/utils/error-tracker.ts +190 -0
  355. package/src/vcs/index.ts +15 -0
  356. package/src/vcs/types.ts +5 -0
  357. package/src/workflows/checkpoint.ts +121 -0
  358. package/src/workflows/cooldown.ts +28 -0
  359. package/src/workflows/definition.ts +235 -0
  360. package/src/workflows/engine.ts +580 -0
  361. package/src/workflows/event-bus.ts +29 -0
  362. package/src/workflows/executors/agent-task.ts +103 -0
  363. package/src/workflows/executors/base.ts +86 -0
  364. package/src/workflows/executors/code-match.ts +88 -0
  365. package/src/workflows/executors/index.ts +16 -0
  366. package/src/workflows/executors/notify.ts +93 -0
  367. package/src/workflows/executors/property-match.ts +104 -0
  368. package/src/workflows/executors/raw-llm.ts +83 -0
  369. package/src/workflows/executors/registry.ts +76 -0
  370. package/src/workflows/executors/script.ts +103 -0
  371. package/src/workflows/executors/validate.ts +215 -0
  372. package/src/workflows/executors/vcs.ts +58 -0
  373. package/src/workflows/index.ts +61 -0
  374. package/src/workflows/input.ts +46 -0
  375. package/src/workflows/json-schema-validator.ts +118 -0
  376. package/src/workflows/recovery.ts +139 -0
  377. package/src/workflows/resume.ts +229 -0
  378. package/src/workflows/retry-poller.ts +216 -0
  379. package/src/workflows/template.ts +74 -0
  380. package/src/workflows/templates.ts +86 -0
  381. package/src/workflows/triggers.ts +124 -0
  382. package/src/workflows/validation.ts +104 -0
  383. package/src/workflows/version.ts +44 -0
  384. package/src/x402/cli.ts +140 -0
  385. package/src/x402/client.ts +192 -0
  386. package/src/x402/config.ts +131 -0
  387. package/src/x402/index.ts +37 -0
  388. package/src/x402/openfort-signer.ts +83 -0
  389. package/src/x402/spending-tracker.ts +109 -0
  390. package/templates/official/coder/CLAUDE.md +49 -0
  391. package/templates/official/coder/IDENTITY.md +28 -0
  392. package/templates/official/coder/SOUL.md +43 -0
  393. package/templates/official/coder/TOOLS.md +40 -0
  394. package/templates/official/coder/config.json +23 -0
  395. package/templates/official/coder/start-up.sh +23 -0
  396. package/templates/official/content-reviewer/CLAUDE.md +68 -0
  397. package/templates/official/content-reviewer/IDENTITY.md +28 -0
  398. package/templates/official/content-reviewer/SOUL.md +44 -0
  399. package/templates/official/content-reviewer/TOOLS.md +37 -0
  400. package/templates/official/content-reviewer/config.json +23 -0
  401. package/templates/official/content-reviewer/start-up.sh +23 -0
  402. package/templates/official/content-strategist/CLAUDE.md +63 -0
  403. package/templates/official/content-strategist/IDENTITY.md +33 -0
  404. package/templates/official/content-strategist/SOUL.md +48 -0
  405. package/templates/official/content-strategist/TOOLS.md +47 -0
  406. package/templates/official/content-strategist/config.json +23 -0
  407. package/templates/official/content-strategist/start-up.sh +23 -0
  408. package/templates/official/content-writer/CLAUDE.md +72 -0
  409. package/templates/official/content-writer/IDENTITY.md +30 -0
  410. package/templates/official/content-writer/SOUL.md +46 -0
  411. package/templates/official/content-writer/TOOLS.md +44 -0
  412. package/templates/official/content-writer/config.json +23 -0
  413. package/templates/official/content-writer/start-up.sh +23 -0
  414. package/templates/official/forward-deployed-engineer/CLAUDE.md +54 -0
  415. package/templates/official/forward-deployed-engineer/IDENTITY.md +37 -0
  416. package/templates/official/forward-deployed-engineer/SOUL.md +55 -0
  417. package/templates/official/forward-deployed-engineer/config.json +21 -0
  418. package/templates/official/lead/CLAUDE.md +33 -0
  419. package/templates/official/lead/IDENTITY.md +36 -0
  420. package/templates/official/lead/SOUL.md +51 -0
  421. package/templates/official/lead/config.json +22 -0
  422. package/templates/official/researcher/CLAUDE.md +46 -0
  423. package/templates/official/researcher/IDENTITY.md +28 -0
  424. package/templates/official/researcher/SOUL.md +43 -0
  425. package/templates/official/researcher/config.json +21 -0
  426. package/templates/official/reviewer/CLAUDE.md +63 -0
  427. package/templates/official/reviewer/IDENTITY.md +28 -0
  428. package/templates/official/reviewer/SOUL.md +45 -0
  429. package/templates/official/reviewer/config.json +21 -0
  430. package/templates/official/tester/CLAUDE.md +53 -0
  431. package/templates/official/tester/IDENTITY.md +28 -0
  432. package/templates/official/tester/SOUL.md +55 -0
  433. package/templates/official/tester/config.json +21 -0
  434. package/templates/schema.ts +35 -0
  435. package/.claude/settings.local.json +0 -115
  436. package/.dockerignore +0 -61
  437. package/.editorconfig +0 -15
  438. package/.env.docker.example +0 -39
  439. package/.env.example +0 -40
  440. package/.github/workflows/ci.yml +0 -76
  441. package/.github/workflows/docker-and-deploy.yml +0 -117
  442. package/.wts-config.json +0 -4
  443. package/.wts-setup.ts +0 -102
  444. package/CLAUDE.md +0 -104
  445. package/CONTRIBUTING.md +0 -270
  446. package/DEPLOYMENT.md +0 -605
  447. package/Dockerfile +0 -57
  448. package/Dockerfile.worker +0 -157
  449. package/FAQ.md +0 -19
  450. package/MCP.md +0 -406
  451. package/UI.md +0 -40
  452. package/assets/agent-swarm-logo-orange.png +0 -0
  453. package/assets/agent-swarm-logo.png +0 -0
  454. package/assets/agent-swarm.mp4 +0 -0
  455. package/assets/agent-swarm.png +0 -0
  456. package/biome.json +0 -39
  457. package/deploy/DEPLOY.md +0 -60
  458. package/deploy/agent-swarm.service +0 -17
  459. package/deploy/docker-push.ts +0 -30
  460. package/deploy/install.ts +0 -85
  461. package/deploy/prod-db.ts +0 -42
  462. package/deploy/uninstall.ts +0 -12
  463. package/deploy/update.ts +0 -21
  464. package/docker-compose.example.yml +0 -159
  465. package/docker-entrypoint.sh +0 -352
  466. package/ecosystem.config.cjs +0 -66
  467. package/plugin/README.md +0 -1
  468. package/plugin/hooks/hooks.json +0 -71
  469. package/pyproject.toml +0 -9
  470. package/scripts/generate-mcp-docs.ts +0 -415
  471. package/slack-manifest.json +0 -71
  472. package/src/tests/get-inbox-message.test.ts +0 -145
  473. package/src/tools/get-inbox-message.ts +0 -89
  474. package/src/tools/inbox-delegate.ts +0 -113
  475. package/thoughts/shared/plans/2025-12-18-slack-integration.md +0 -1195
  476. package/thoughts/shared/plans/2025-12-19-agent-log-streaming.md +0 -732
  477. package/thoughts/shared/plans/2025-12-19-role-based-swarm-plugin.md +0 -361
  478. package/thoughts/shared/plans/2025-12-20-mobile-responsive-ui.md +0 -501
  479. package/thoughts/shared/plans/2025-12-20-startup-team-swarm.md +0 -560
  480. package/thoughts/shared/plans/2025-12-23-runner-level-polling.md +0 -934
  481. package/thoughts/shared/plans/2025-12-23-runner-session-logs.md +0 -1000
  482. package/thoughts/shared/plans/2025-12-23-worker-lead-spawn-triggers.md +0 -568
  483. package/thoughts/shared/plans/2026-01-09-inverse-teleport.md +0 -1516
  484. package/thoughts/shared/plans/2026-01-12-agent-rename-pm2-control.md +0 -1133
  485. package/thoughts/shared/plans/2026-01-12-github-app-integration.md +0 -380
  486. package/thoughts/shared/plans/2026-01-12-lead-inbox-model.md +0 -876
  487. package/thoughts/shared/plans/2026-01-12-ralph-wiggum-integration.md +0 -463
  488. package/thoughts/shared/plans/2026-01-13-agent-concurrency.md +0 -691
  489. package/thoughts/shared/plans/2026-01-13-github-assignment-handling.md +0 -690
  490. package/thoughts/shared/plans/2026-01-13-prevent-duplicate-trigger-processing.md +0 -1071
  491. package/thoughts/shared/plans/2026-01-14-fix-slack-thread-context.md +0 -507
  492. package/thoughts/shared/plans/2026-01-15-scheduled-tasks-implementation.md +0 -565
  493. package/thoughts/shared/plans/2026-01-15-usage-cost-tracking-ui.md +0 -1479
  494. package/thoughts/shared/plans/2026-01-16-epics-feature-implementation.md +0 -1230
  495. package/thoughts/shared/research/.gitkeep +0 -0
  496. package/thoughts/shared/research/2025-01-09-inverse-teleport-plan-review.md +0 -420
  497. package/thoughts/shared/research/2025-12-18-slack-integration.md +0 -442
  498. package/thoughts/shared/research/2025-12-19-agent-log-streaming.md +0 -339
  499. package/thoughts/shared/research/2025-12-19-agent-secrets-cli-research.md +0 -390
  500. package/thoughts/shared/research/2025-12-21-gemini-cli-integration.md +0 -376
  501. package/thoughts/shared/research/2025-12-22-runner-loop-architecture.md +0 -582
  502. package/thoughts/shared/research/2025-12-22-setup-experience-improvements.md +0 -264
  503. package/thoughts/shared/research/2026-01-13-lead-duplicate-trigger-processing.md +0 -223
  504. package/thoughts/shared/research/2026-01-14-lead-slack-thread-context.md +0 -277
  505. package/thoughts/shared/research/2026-01-15-ai-tracker-agent-swarm-integration.md +0 -376
  506. package/thoughts/shared/research/2026-01-15-auto-starting-processes-in-worker-containers.md +0 -787
  507. package/thoughts/shared/research/2026-01-15-scheduled-tasks.md +0 -390
  508. package/thoughts/shared/research/2026-01-16-epics-feature-research.md +0 -437
  509. package/thoughts/taras/plans/2026-01-22-agent-swarm-schemas.md +0 -98
  510. package/thoughts/taras/plans/2026-01-28-per-worker-claude-md.md +0 -617
  511. package/thoughts/taras/plans/2026-01-28-sentry-cli-integration.md +0 -214
  512. package/thoughts/taras/research/2026-01-22-vercel-cli-integration.md +0 -287
  513. package/thoughts/taras/research/2026-01-27-excessive-polling-issue.md +0 -311
  514. package/thoughts/taras/research/2026-01-28-per-worker-claude-md.md +0 -383
  515. package/thoughts/taras/research/2026-01-28-sentry-cli-integration.md +0 -240
  516. package/tsconfig.json +0 -37
  517. package/ui/CLAUDE.md +0 -49
  518. package/ui/bun.lock +0 -771
  519. package/ui/index.html +0 -22
  520. package/ui/package-lock.json +0 -5290
  521. package/ui/package.json +0 -33
  522. package/ui/pnpm-lock.yaml +0 -3341
  523. package/ui/postcss.config.js +0 -6
  524. package/ui/public/logo.png +0 -0
  525. package/ui/src/App.tsx +0 -63
  526. package/ui/src/components/ActivityFeed.tsx +0 -440
  527. package/ui/src/components/AgentDetailPanel.tsx +0 -733
  528. package/ui/src/components/AgentsPanel.tsx +0 -815
  529. package/ui/src/components/ChatPanel.tsx +0 -1920
  530. package/ui/src/components/ConfigModal.tsx +0 -253
  531. package/ui/src/components/Dashboard.tsx +0 -832
  532. package/ui/src/components/EditAgentProfileModal.tsx +0 -433
  533. package/ui/src/components/EpicDetailPage.tsx +0 -741
  534. package/ui/src/components/EpicsPanel.tsx +0 -566
  535. package/ui/src/components/Header.tsx +0 -160
  536. package/ui/src/components/JsonViewer.tsx +0 -171
  537. package/ui/src/components/ScheduledTaskDetailPanel.tsx +0 -517
  538. package/ui/src/components/ScheduledTasksPanel.tsx +0 -639
  539. package/ui/src/components/ServicesPanel.tsx +0 -622
  540. package/ui/src/components/SessionLogPanel.tsx +0 -1219
  541. package/ui/src/components/StatsBar.tsx +0 -321
  542. package/ui/src/components/StatusBadge.tsx +0 -168
  543. package/ui/src/components/TaskDetailPanel.tsx +0 -903
  544. package/ui/src/components/TasksPanel.tsx +0 -614
  545. package/ui/src/components/UsageCharts.tsx +0 -216
  546. package/ui/src/components/UsageTab.tsx +0 -394
  547. package/ui/src/hooks/queries.ts +0 -353
  548. package/ui/src/hooks/useAutoScroll.ts +0 -83
  549. package/ui/src/index.css +0 -257
  550. package/ui/src/lib/api.ts +0 -268
  551. package/ui/src/lib/config.ts +0 -35
  552. package/ui/src/lib/contentPreview.ts +0 -208
  553. package/ui/src/lib/theme.ts +0 -214
  554. package/ui/src/lib/utils.ts +0 -88
  555. package/ui/src/main.tsx +0 -28
  556. package/ui/src/types/api.ts +0 -323
  557. package/ui/src/vite-env.d.ts +0 -1
  558. package/ui/tailwind.config.js +0 -37
  559. package/ui/tsconfig.json +0 -31
  560. package/ui/vite.config.ts +0 -35
  561. /package/{thoughts/shared/plans → templates/community}/.gitkeep +0 -0
@@ -1,1920 +0,0 @@
1
- import Box from "@mui/joy/Box";
2
- import Card from "@mui/joy/Card";
3
- import Drawer from "@mui/joy/Drawer";
4
- import IconButton from "@mui/joy/IconButton";
5
- import Link from "@mui/joy/Link";
6
- import { useColorScheme } from "@mui/joy/styles";
7
- import Textarea from "@mui/joy/Textarea";
8
- import Tooltip from "@mui/joy/Tooltip";
9
- import Typography from "@mui/joy/Typography";
10
- import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
11
- import ReactMarkdown from "react-markdown";
12
- import remarkGfm from "remark-gfm";
13
- import { formatSmartTime } from "@/lib/utils";
14
- import {
15
- useAgents,
16
- useChannels,
17
- useInfiniteMessages,
18
- usePostMessage,
19
- useThreadMessages,
20
- } from "../hooks/queries";
21
- import type { Agent, ChannelMessage } from "../types/api";
22
-
23
- interface MentionInputProps {
24
- value: string;
25
- onChange: (value: string) => void;
26
- onSend: () => void;
27
- onMentionsChange?: (mentions: string[]) => void;
28
- placeholder: string;
29
- agents: Agent[];
30
- inputStyles: object;
31
- sendButtonStyles: object;
32
- sendLabel: string;
33
- disabled?: boolean;
34
- colors: Record<string, string>;
35
- isDark: boolean;
36
- }
37
-
38
- const MentionInput = React.memo(function MentionInput({
39
- value,
40
- onChange,
41
- onSend,
42
- onMentionsChange,
43
- placeholder,
44
- agents,
45
- inputStyles,
46
- sendButtonStyles,
47
- sendLabel,
48
- disabled,
49
- colors,
50
- isDark,
51
- }: MentionInputProps) {
52
- const [showMentionPopup, setShowMentionPopup] = useState(false);
53
- const [mentionQuery, setMentionQuery] = useState("");
54
- const [mentionStartPos, setMentionStartPos] = useState(0);
55
- const [selectedIndex, setSelectedIndex] = useState(0);
56
- const [mentions, setMentions] = useState<string[]>([]);
57
- const inputRef = useRef<HTMLTextAreaElement>(null);
58
- const popupRef = useRef<HTMLDivElement>(null);
59
-
60
- // Filter agents based on query
61
- const filteredAgents = useMemo(() => {
62
- if (!mentionQuery) return agents;
63
- const query = mentionQuery.toLowerCase();
64
- return agents.filter(
65
- (agent) =>
66
- agent.name.toLowerCase().includes(query) ||
67
- (agent.role && agent.role.toLowerCase().includes(query)),
68
- );
69
- }, [agents, mentionQuery]);
70
-
71
- // Reset selected index when filtered list changes
72
- useEffect(() => {
73
- setSelectedIndex(0);
74
- }, [filteredAgents.length]);
75
-
76
- // Notify parent of mention changes
77
- useEffect(() => {
78
- if (onMentionsChange) {
79
- onMentionsChange(mentions);
80
- }
81
- }, [mentions, onMentionsChange]);
82
-
83
- // Reset mentions when input is cleared
84
- useEffect(() => {
85
- if (!value) {
86
- setMentions([]);
87
- }
88
- }, [value]);
89
-
90
- const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
91
- const newValue = e.target.value;
92
- const cursorPos = e.target.selectionStart || 0;
93
- onChange(newValue);
94
-
95
- // Check if we should show mention popup
96
- // Find the last @ before cursor that isn't followed by a space
97
- const textBeforeCursor = newValue.slice(0, cursorPos);
98
- const lastAtIndex = textBeforeCursor.lastIndexOf("@");
99
-
100
- if (lastAtIndex >= 0) {
101
- const textAfterAt = textBeforeCursor.slice(lastAtIndex + 1);
102
- // Show popup if @ is at start or preceded by space, and no space after @
103
- const charBeforeAt = lastAtIndex > 0 ? newValue[lastAtIndex - 1] : " ";
104
- if ((charBeforeAt === " " || lastAtIndex === 0) && !textAfterAt.includes(" ")) {
105
- setShowMentionPopup(true);
106
- setMentionQuery(textAfterAt);
107
- setMentionStartPos(lastAtIndex);
108
- return;
109
- }
110
- }
111
-
112
- setShowMentionPopup(false);
113
- setMentionQuery("");
114
- };
115
-
116
- const handleSelectAgent = (agent: Agent) => {
117
- // Replace @query with @agentName
118
- const beforeMention = value.slice(0, mentionStartPos);
119
- const afterMention = value.slice(mentionStartPos + 1 + mentionQuery.length);
120
- const newValue = `${beforeMention}@${agent.name} ${afterMention}`;
121
-
122
- onChange(newValue);
123
- setMentions((prev) => [...new Set([...prev, agent.id])]);
124
- setShowMentionPopup(false);
125
- setMentionQuery("");
126
-
127
- // Focus input
128
- setTimeout(() => inputRef.current?.focus(), 0);
129
- };
130
-
131
- const handleKeyDown = (e: React.KeyboardEvent) => {
132
- if (showMentionPopup && filteredAgents.length > 0) {
133
- switch (e.key) {
134
- case "ArrowDown":
135
- e.preventDefault();
136
- setSelectedIndex((prev) => (prev + 1) % filteredAgents.length);
137
- break;
138
- case "ArrowUp":
139
- e.preventDefault();
140
- setSelectedIndex((prev) => (prev - 1 + filteredAgents.length) % filteredAgents.length);
141
- break;
142
- case "Enter":
143
- e.preventDefault();
144
- if (filteredAgents[selectedIndex]) {
145
- handleSelectAgent(filteredAgents[selectedIndex]);
146
- }
147
- break;
148
- case "Escape":
149
- e.preventDefault();
150
- setShowMentionPopup(false);
151
- break;
152
- case "Tab":
153
- e.preventDefault();
154
- if (filteredAgents[selectedIndex]) {
155
- handleSelectAgent(filteredAgents[selectedIndex]);
156
- }
157
- break;
158
- }
159
- } else if (e.key === "Enter" && !e.shiftKey) {
160
- e.preventDefault();
161
- onSend();
162
- }
163
- };
164
-
165
- // Close popup when clicking outside
166
- useEffect(() => {
167
- const handleClickOutside = (e: MouseEvent) => {
168
- if (popupRef.current && !popupRef.current.contains(e.target as Node)) {
169
- setShowMentionPopup(false);
170
- }
171
- };
172
- document.addEventListener("mousedown", handleClickOutside);
173
- return () => document.removeEventListener("mousedown", handleClickOutside);
174
- }, []);
175
-
176
- return (
177
- <Box sx={{ position: "relative", display: "flex", gap: 1.5, flex: 1, alignItems: "flex-end" }}>
178
- <Textarea
179
- slotProps={{
180
- textarea: {
181
- ref: inputRef,
182
- },
183
- }}
184
- placeholder={placeholder}
185
- value={value}
186
- onChange={handleInputChange}
187
- onKeyDown={handleKeyDown}
188
- minRows={1}
189
- maxRows={5}
190
- sx={{ ...inputStyles, flex: 1 }}
191
- />
192
-
193
- {/* Mention autocomplete popup */}
194
- {showMentionPopup && filteredAgents.length > 0 && (
195
- <Box
196
- ref={popupRef}
197
- sx={{
198
- position: "absolute",
199
- bottom: "100%",
200
- left: 0,
201
- right: 80,
202
- mb: 0.5,
203
- bgcolor: isDark ? "#1A130E" : "#FFFFFF",
204
- border: "1px solid",
205
- borderColor: colors.amberBorder,
206
- borderRadius: "8px",
207
- boxShadow: isDark ? "0 4px 20px rgba(0, 0, 0, 0.5)" : "0 4px 20px rgba(0, 0, 0, 0.15)",
208
- maxHeight: 200,
209
- overflow: "auto",
210
- zIndex: 1000,
211
- }}
212
- >
213
- {filteredAgents.map((agent, index) => (
214
- <Box
215
- key={agent.id}
216
- onClick={() => handleSelectAgent(agent)}
217
- sx={{
218
- px: 2,
219
- py: 1.5,
220
- cursor: "pointer",
221
- display: "flex",
222
- alignItems: "center",
223
- gap: 1.5,
224
- bgcolor: index === selectedIndex ? colors.selectedBg : "transparent",
225
- borderBottom: index < filteredAgents.length - 1 ? "1px solid" : "none",
226
- borderColor: "neutral.outlinedBorder",
227
- transition: "background-color 0.1s ease",
228
- "&:hover": {
229
- bgcolor: colors.hoverBg,
230
- },
231
- }}
232
- >
233
- {/* Status dot */}
234
- <Box
235
- sx={{
236
- width: 8,
237
- height: 8,
238
- borderRadius: "50%",
239
- bgcolor:
240
- agent.status === "busy"
241
- ? colors.amber
242
- : agent.status === "idle"
243
- ? colors.gold
244
- : colors.dormant || "#6B5344",
245
- boxShadow:
246
- agent.status === "busy"
247
- ? isDark
248
- ? "0 0 6px rgba(245, 166, 35, 0.4)"
249
- : "0 0 4px rgba(212, 136, 6, 0.3)"
250
- : "none",
251
- }}
252
- />
253
- <Box sx={{ flex: 1, minWidth: 0 }}>
254
- <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
255
- <Typography
256
- sx={{
257
- fontFamily: "'Space Grotesk', sans-serif",
258
- fontWeight: 600,
259
- fontSize: "0.85rem",
260
- color: agent.isLead ? colors.honey : colors.amber,
261
- whiteSpace: "nowrap",
262
- }}
263
- >
264
- {agent.name}
265
- </Typography>
266
- {agent.isLead && (
267
- <Typography
268
- sx={{
269
- fontFamily: "'JetBrains Mono', monospace",
270
- fontSize: "0.55rem",
271
- fontWeight: 700,
272
- color: colors.honey,
273
- bgcolor: isDark ? "rgba(255, 184, 77, 0.15)" : "rgba(184, 115, 0, 0.1)",
274
- px: 0.75,
275
- py: 0.2,
276
- borderRadius: "4px",
277
- border: "1px solid",
278
- borderColor: isDark ? "rgba(255, 184, 77, 0.3)" : "rgba(184, 115, 0, 0.25)",
279
- letterSpacing: "0.05em",
280
- }}
281
- >
282
- LEAD
283
- </Typography>
284
- )}
285
- </Box>
286
- {agent.role && (
287
- <Typography
288
- sx={{
289
- fontFamily: "'JetBrains Mono', monospace",
290
- fontSize: "0.7rem",
291
- color: "text.tertiary",
292
- }}
293
- >
294
- {agent.role}
295
- </Typography>
296
- )}
297
- </Box>
298
- </Box>
299
- ))}
300
- </Box>
301
- )}
302
-
303
- <Box component="button" onClick={onSend} disabled={disabled} sx={sendButtonStyles}>
304
- {sendLabel}
305
- </Box>
306
- </Box>
307
- );
308
- });
309
-
310
- // Helper to format date for dividers
311
- function formatDateDivider(dateStr: string): string {
312
- const date = new Date(dateStr);
313
- const now = new Date();
314
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
315
- const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
316
-
317
- if (messageDate.getTime() === today.getTime()) {
318
- return `Today (${date.toLocaleDateString(undefined, { month: "short", day: "numeric" })})`;
319
- }
320
-
321
- const yesterday = new Date(today);
322
- yesterday.setDate(yesterday.getDate() - 1);
323
- if (messageDate.getTime() === yesterday.getTime()) {
324
- return `Yesterday (${date.toLocaleDateString(undefined, { month: "short", day: "numeric" })})`;
325
- }
326
-
327
- return date.toLocaleDateString(undefined, {
328
- weekday: "long",
329
- month: "short",
330
- day: "numeric",
331
- year: messageDate.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
332
- });
333
- }
334
-
335
- // Get date key for grouping (YYYY-MM-DD)
336
- function getDateKey(dateStr: string): string {
337
- const date = new Date(dateStr);
338
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
339
- }
340
-
341
- interface DateDividerProps {
342
- date: string;
343
- isDark: boolean;
344
- colors: Record<string, string>;
345
- }
346
-
347
- function DateDivider({ date, isDark, colors }: DateDividerProps) {
348
- return (
349
- <Box
350
- sx={{
351
- display: "flex",
352
- alignItems: "center",
353
- gap: 2,
354
- px: 2,
355
- py: 1.5,
356
- my: 1,
357
- }}
358
- >
359
- <Box
360
- sx={{
361
- flex: 1,
362
- height: 1,
363
- bgcolor: isDark ? "rgba(212, 165, 116, 0.2)" : "rgba(139, 105, 20, 0.15)",
364
- }}
365
- />
366
- <Typography
367
- sx={{
368
- fontFamily: "'JetBrains Mono', monospace",
369
- fontSize: "0.7rem",
370
- fontWeight: 600,
371
- color: colors.gold,
372
- letterSpacing: "0.03em",
373
- whiteSpace: "nowrap",
374
- }}
375
- >
376
- {formatDateDivider(date)}
377
- </Typography>
378
- <Box
379
- sx={{
380
- flex: 1,
381
- height: 1,
382
- bgcolor: isDark ? "rgba(212, 165, 116, 0.2)" : "rgba(139, 105, 20, 0.15)",
383
- }}
384
- />
385
- </Box>
386
- );
387
- }
388
-
389
- interface MessageItemProps {
390
- message: ChannelMessage;
391
- isDark: boolean;
392
- colors: Record<string, string>;
393
- onOpenThread?: () => void;
394
- threadCount?: number;
395
- isThreadView?: boolean;
396
- onAgentClick?: (agentId: string) => void;
397
- onTaskClick?: (taskId: string) => void;
398
- isSelected?: boolean;
399
- agentsByName?: Map<string, string>; // name -> id mapping for @mentions
400
- isLeadAgent?: boolean; // Whether the message sender is the lead agent
401
- }
402
-
403
- function MessageItem({
404
- message,
405
- isDark,
406
- colors,
407
- onOpenThread,
408
- threadCount,
409
- isThreadView,
410
- onAgentClick,
411
- onTaskClick,
412
- isSelected,
413
- agentsByName,
414
- isLeadAgent,
415
- }: MessageItemProps) {
416
- const [copied, setCopied] = useState(false);
417
- const [showRaw, setShowRaw] = useState(false);
418
- const hasReplies = threadCount && threadCount > 0;
419
-
420
- const handleCopy = useCallback(() => {
421
- navigator.clipboard.writeText(message.content);
422
- setCopied(true);
423
- setTimeout(() => setCopied(false), 2000);
424
- }, [message.content]);
425
-
426
- const toggleRaw = useCallback(() => {
427
- setShowRaw((prev) => !prev);
428
- }, []);
429
-
430
- // Custom markdown components to handle @mentions
431
- const markdownComponents = useMemo(() => {
432
- // Helper to render text with @mentions
433
- const renderTextWithMentions = (text: string): React.ReactNode => {
434
- if (!agentsByName || agentsByName.size === 0) {
435
- return text;
436
- }
437
-
438
- // Build regex for agent names (longest first)
439
- const agentNames = Array.from(agentsByName.keys()).sort((a, b) => b.length - a.length);
440
- if (agentNames.length === 0) return text;
441
-
442
- const escapedNames = agentNames.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
443
- const mentionPattern = new RegExp(`@(${escapedNames.join("|")})(?=\\s|$|[.,!?;:])`, "g");
444
-
445
- const parts: React.ReactNode[] = [];
446
- let lastIndex = 0;
447
- let match;
448
- let keyCounter = 0;
449
-
450
- while ((match = mentionPattern.exec(text)) !== null) {
451
- if (match.index > lastIndex) {
452
- parts.push(text.slice(lastIndex, match.index));
453
- }
454
-
455
- const mentionName = match[1] ?? "";
456
- const agentId = agentsByName.get(mentionName);
457
-
458
- if (agentId && onAgentClick) {
459
- parts.push(
460
- <Link
461
- key={`mention-${keyCounter++}`}
462
- component="button"
463
- onClick={(e) => {
464
- e.stopPropagation();
465
- onAgentClick(agentId);
466
- }}
467
- sx={{
468
- fontFamily: "inherit",
469
- fontWeight: 600,
470
- fontSize: "inherit",
471
- color: colors.amber,
472
- textDecoration: "none",
473
- cursor: "pointer",
474
- bgcolor: isDark ? "rgba(245, 166, 35, 0.1)" : "rgba(212, 136, 6, 0.08)",
475
- px: 0.5,
476
- borderRadius: "4px",
477
- "&:hover": {
478
- textDecoration: "underline",
479
- color: colors.honey,
480
- },
481
- }}
482
- >
483
- @{mentionName}
484
- </Link>,
485
- );
486
- } else {
487
- parts.push(
488
- <Box
489
- key={`mention-${keyCounter++}`}
490
- component="span"
491
- sx={{
492
- fontWeight: 600,
493
- color: colors.gold,
494
- bgcolor: isDark ? "rgba(212, 165, 116, 0.1)" : "rgba(139, 105, 20, 0.08)",
495
- px: 0.5,
496
- borderRadius: "4px",
497
- }}
498
- >
499
- @{mentionName}
500
- </Box>,
501
- );
502
- }
503
-
504
- lastIndex = match.index + match[0].length;
505
- }
506
-
507
- if (lastIndex < text.length) {
508
- parts.push(text.slice(lastIndex));
509
- }
510
-
511
- return parts.length > 0 ? parts : text;
512
- };
513
-
514
- return {
515
- // Handle task: links for navigating to tasks
516
- a: ({ href, children }: { href?: string; children?: React.ReactNode }) => {
517
- if (href?.startsWith("task:")) {
518
- const taskId = href.slice(5); // Remove "task:" prefix
519
- if (onTaskClick) {
520
- return (
521
- <Link
522
- component="button"
523
- onClick={(e) => {
524
- e.stopPropagation();
525
- onTaskClick(taskId);
526
- }}
527
- sx={{
528
- fontFamily: "'JetBrains Mono', monospace",
529
- fontSize: "0.85em",
530
- color: colors.gold,
531
- textDecoration: "none",
532
- cursor: "pointer",
533
- bgcolor: isDark ? "rgba(212, 165, 116, 0.1)" : "rgba(139, 105, 20, 0.08)",
534
- px: 0.75,
535
- py: 0.25,
536
- borderRadius: "4px",
537
- "&:hover": {
538
- textDecoration: "underline",
539
- bgcolor: isDark ? "rgba(212, 165, 116, 0.15)" : "rgba(139, 105, 20, 0.12)",
540
- },
541
- }}
542
- >
543
- {children}
544
- </Link>
545
- );
546
- }
547
- // Non-clickable task reference
548
- return (
549
- <Box
550
- component="span"
551
- sx={{
552
- fontFamily: "'JetBrains Mono', monospace",
553
- fontSize: "0.85em",
554
- color: colors.gold,
555
- bgcolor: isDark ? "rgba(212, 165, 116, 0.1)" : "rgba(139, 105, 20, 0.08)",
556
- px: 0.75,
557
- py: 0.25,
558
- borderRadius: "4px",
559
- }}
560
- >
561
- {children}
562
- </Box>
563
- );
564
- }
565
- // Regular links - render normally
566
- return <a href={href}>{children}</a>;
567
- },
568
- // Process text nodes to handle @mentions
569
- p: ({ children }: { children?: React.ReactNode }) => {
570
- const processChildren = (child: React.ReactNode): React.ReactNode => {
571
- if (typeof child === "string") {
572
- return renderTextWithMentions(child);
573
- }
574
- if (Array.isArray(child)) {
575
- return child.map((c, i) => (
576
- <React.Fragment key={i}>{processChildren(c)}</React.Fragment>
577
- ));
578
- }
579
- return child;
580
- };
581
- return <p>{processChildren(children)}</p>;
582
- },
583
- li: ({ children }: { children?: React.ReactNode }) => {
584
- const processChildren = (child: React.ReactNode): React.ReactNode => {
585
- if (typeof child === "string") {
586
- return renderTextWithMentions(child);
587
- }
588
- if (Array.isArray(child)) {
589
- return child.map((c, i) => (
590
- <React.Fragment key={i}>{processChildren(c)}</React.Fragment>
591
- ));
592
- }
593
- return child;
594
- };
595
- return <li>{processChildren(children)}</li>;
596
- },
597
- };
598
- }, [agentsByName, onAgentClick, onTaskClick, colors, isDark]);
599
-
600
- return (
601
- <Box
602
- sx={{
603
- display: "flex",
604
- flexDirection: "column",
605
- gap: 0.5,
606
- px: 1.5,
607
- py: 1,
608
- mx: 0.5,
609
- my: 0.25,
610
- borderRadius: "6px",
611
- border: "1px solid",
612
- borderColor: isSelected ? colors.amberBorder : "transparent",
613
- bgcolor: isSelected
614
- ? colors.selectedBg
615
- : isDark
616
- ? "rgba(26, 19, 14, 0.5)"
617
- : "rgba(255, 255, 255, 0.5)",
618
- transition: "all 0.2s ease",
619
- "&:hover": {
620
- bgcolor: isDark ? "rgba(245, 166, 35, 0.06)" : "rgba(212, 136, 6, 0.04)",
621
- "& .action-icons": {
622
- opacity: 1,
623
- },
624
- },
625
- }}
626
- >
627
- {/* Header row */}
628
- <Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
629
- {/* Indicator - hexagon for lead, dot for others */}
630
- {isLeadAgent ? (
631
- <Box
632
- sx={{
633
- width: 10,
634
- height: 12,
635
- clipPath: "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)",
636
- bgcolor: colors.honey,
637
- flexShrink: 0,
638
- boxShadow: isDark
639
- ? "0 0 8px rgba(255, 184, 77, 0.5)"
640
- : "0 0 6px rgba(184, 115, 0, 0.4)",
641
- }}
642
- />
643
- ) : (
644
- <Box
645
- sx={{
646
- width: 8,
647
- height: 8,
648
- borderRadius: "50%",
649
- bgcolor: message.agentId ? colors.amber : colors.blue,
650
- flexShrink: 0,
651
- boxShadow: message.agentId
652
- ? isDark
653
- ? "0 0 6px rgba(245, 166, 35, 0.4)"
654
- : "0 0 4px rgba(212, 136, 6, 0.3)"
655
- : "0 0 6px rgba(59, 130, 246, 0.4)",
656
- }}
657
- />
658
- )}
659
-
660
- {/* Agent name - clickable if agent */}
661
- {message.agentId && onAgentClick ? (
662
- <Link
663
- component="button"
664
- onClick={(e) => {
665
- e.stopPropagation();
666
- onAgentClick(message.agentId!);
667
- }}
668
- sx={{
669
- fontFamily: "'Space Grotesk', sans-serif",
670
- fontWeight: 600,
671
- fontSize: "0.85rem",
672
- color: isLeadAgent ? colors.honey : colors.amber,
673
- textDecoration: "none",
674
- cursor: "pointer",
675
- whiteSpace: "nowrap",
676
- "&:hover": {
677
- textDecoration: "underline",
678
- color: isLeadAgent ? "#FFD699" : colors.honey,
679
- },
680
- }}
681
- >
682
- {message.agentName || "Agent"}
683
- </Link>
684
- ) : (
685
- <Typography
686
- sx={{
687
- fontFamily: "'Space Grotesk', sans-serif",
688
- fontWeight: 600,
689
- fontSize: "0.85rem",
690
- color: message.agentId ? (isLeadAgent ? colors.honey : colors.amber) : colors.blue,
691
- whiteSpace: "nowrap",
692
- }}
693
- >
694
- {message.agentName || "Human"}
695
- </Typography>
696
- )}
697
-
698
- {/* Lead badge */}
699
- {isLeadAgent && (
700
- <Typography
701
- sx={{
702
- fontFamily: "'JetBrains Mono', monospace",
703
- fontSize: "0.55rem",
704
- fontWeight: 700,
705
- color: colors.honey,
706
- bgcolor: isDark ? "rgba(255, 184, 77, 0.15)" : "rgba(184, 115, 0, 0.1)",
707
- px: 0.75,
708
- py: 0.2,
709
- borderRadius: "4px",
710
- border: "1px solid",
711
- borderColor: isDark ? "rgba(255, 184, 77, 0.3)" : "rgba(184, 115, 0, 0.25)",
712
- letterSpacing: "0.05em",
713
- }}
714
- >
715
- LEAD
716
- </Typography>
717
- )}
718
-
719
- {/* Timestamp */}
720
- <Typography
721
- sx={{
722
- fontFamily: "'JetBrains Mono', monospace",
723
- fontSize: "0.7rem",
724
- color: "text.tertiary",
725
- letterSpacing: "0.02em",
726
- }}
727
- >
728
- {formatSmartTime(message.createdAt)}
729
- </Typography>
730
-
731
- {/* Spacer */}
732
- <Box sx={{ flex: 1 }} />
733
-
734
- {/* Reply count badge - clickable to open thread */}
735
- {!isThreadView && hasReplies && onOpenThread && (
736
- <Box
737
- component="button"
738
- onClick={onOpenThread}
739
- sx={{
740
- fontFamily: "'JetBrains Mono', monospace",
741
- fontSize: "0.65rem",
742
- fontWeight: 600,
743
- bgcolor: isDark ? "rgba(212, 165, 116, 0.15)" : "#FEF3C7",
744
- color: isDark ? "#D4A574" : "#B45309",
745
- border: "1px solid",
746
- borderColor: isDark ? "rgba(212, 165, 116, 0.3)" : "#FCD34D",
747
- borderRadius: "12px",
748
- px: 1.5,
749
- py: 0.25,
750
- cursor: "pointer",
751
- transition: "all 0.15s ease",
752
- "&:hover": {
753
- bgcolor: isDark ? "rgba(212, 165, 116, 0.25)" : "#FDE68A",
754
- borderColor: isDark ? "rgba(212, 165, 116, 0.5)" : "#FBBF24",
755
- },
756
- }}
757
- >
758
- {threadCount} {threadCount === 1 ? "reply" : "replies"}
759
- </Box>
760
- )}
761
-
762
- {/* Action icons - appear on hover */}
763
- <Box
764
- className="action-icons"
765
- sx={{ display: "flex", gap: 0.5, opacity: 0, transition: "opacity 0.2s ease" }}
766
- >
767
- {/* Toggle raw/markdown */}
768
- <Tooltip title={showRaw ? "Show formatted" : "Show raw"} placement="top">
769
- <IconButton
770
- size="sm"
771
- variant="plain"
772
- onClick={toggleRaw}
773
- sx={{
774
- color: showRaw ? colors.amber : "text.tertiary",
775
- fontSize: "0.75rem",
776
- fontFamily: "'JetBrains Mono', monospace",
777
- fontWeight: 600,
778
- width: 28,
779
- height: 28,
780
- "&:hover": {
781
- color: colors.amber,
782
- bgcolor: colors.hoverBg,
783
- },
784
- }}
785
- >
786
- {showRaw ? "MD" : "</>"}
787
- </IconButton>
788
- </Tooltip>
789
-
790
- {/* Copy button */}
791
- <Tooltip title={copied ? "Copied!" : "Copy message"} placement="top">
792
- <IconButton
793
- size="sm"
794
- variant="plain"
795
- onClick={handleCopy}
796
- sx={{
797
- color: copied ? "#22C55E" : "text.tertiary",
798
- fontSize: "0.9rem",
799
- width: 28,
800
- height: 28,
801
- "&:hover": {
802
- color: copied ? "#22C55E" : colors.amber,
803
- bgcolor: colors.hoverBg,
804
- },
805
- }}
806
- >
807
- {copied ? "✓" : "⧉"}
808
- </IconButton>
809
- </Tooltip>
810
-
811
- {/* Reply icon */}
812
- {!isThreadView && onOpenThread && (
813
- <Tooltip title="Open thread" placement="top">
814
- <IconButton
815
- size="sm"
816
- variant="plain"
817
- onClick={onOpenThread}
818
- sx={{
819
- color: "text.tertiary",
820
- fontSize: "1rem",
821
- width: 28,
822
- height: 28,
823
- "&:hover": {
824
- color: colors.amber,
825
- bgcolor: colors.hoverBg,
826
- },
827
- }}
828
- >
829
-
830
- </IconButton>
831
- </Tooltip>
832
- )}
833
- </Box>
834
- </Box>
835
-
836
- {/* Message content */}
837
- {showRaw ? (
838
- <Typography
839
- component="div"
840
- sx={{
841
- fontFamily: "'JetBrains Mono', monospace",
842
- fontSize: "0.8rem",
843
- color: "text.primary",
844
- lineHeight: 1.5,
845
- whiteSpace: "pre-wrap",
846
- wordBreak: "break-word",
847
- pl: 2.25,
848
- bgcolor: isDark ? "rgba(0, 0, 0, 0.2)" : "rgba(0, 0, 0, 0.03)",
849
- borderRadius: "6px",
850
- p: 1.5,
851
- ml: 2.25,
852
- mr: 1,
853
- }}
854
- >
855
- {message.content}
856
- </Typography>
857
- ) : (
858
- <Box
859
- sx={{
860
- pl: 2.25,
861
- fontFamily: "'Space Grotesk', sans-serif",
862
- fontSize: "0.85rem",
863
- color: "text.primary",
864
- lineHeight: 1.6,
865
- wordBreak: "break-word",
866
- "& p": {
867
- m: 0,
868
- mb: 0.5,
869
- "&:last-child": { mb: 0 },
870
- },
871
- "& h1, & h2, & h3, & h4, & h5, & h6": {
872
- fontFamily: "'Space Grotesk', sans-serif",
873
- fontWeight: 600,
874
- mt: 1,
875
- mb: 0.5,
876
- color: colors.amber,
877
- },
878
- "& h1": { fontSize: "1.25rem" },
879
- "& h2": { fontSize: "1.1rem" },
880
- "& h3": { fontSize: "1rem" },
881
- "& code": {
882
- fontFamily: "'JetBrains Mono', monospace",
883
- fontSize: "0.8rem",
884
- bgcolor: isDark ? "rgba(0, 0, 0, 0.3)" : "rgba(0, 0, 0, 0.06)",
885
- px: 0.5,
886
- py: 0.25,
887
- borderRadius: "4px",
888
- },
889
- "& pre": {
890
- bgcolor: isDark ? "rgba(0, 0, 0, 0.3)" : "rgba(0, 0, 0, 0.06)",
891
- borderRadius: "6px",
892
- p: 1.5,
893
- overflow: "auto",
894
- my: 1,
895
- "& code": {
896
- bgcolor: "transparent",
897
- p: 0,
898
- },
899
- },
900
- "& ul": {
901
- pl: 2.5,
902
- my: 0.5,
903
- listStyleType: "disc",
904
- },
905
- "& ol": {
906
- pl: 2.5,
907
- my: 0.5,
908
- listStyleType: "decimal",
909
- },
910
- "& li": {
911
- mb: 0.25,
912
- display: "list-item",
913
- },
914
- "& blockquote": {
915
- borderLeft: "3px solid",
916
- borderColor: colors.amber,
917
- pl: 1.5,
918
- ml: 0,
919
- my: 1,
920
- color: "text.secondary",
921
- fontStyle: "italic",
922
- },
923
- "& a": {
924
- color: colors.amber,
925
- textDecoration: "none",
926
- "&:hover": {
927
- textDecoration: "underline",
928
- },
929
- },
930
- "& table": {
931
- borderCollapse: "collapse",
932
- my: 1,
933
- fontSize: "0.8rem",
934
- },
935
- "& th, & td": {
936
- border: "1px solid",
937
- borderColor: "neutral.outlinedBorder",
938
- px: 1,
939
- py: 0.5,
940
- },
941
- "& th": {
942
- bgcolor: isDark ? "rgba(0, 0, 0, 0.2)" : "rgba(0, 0, 0, 0.04)",
943
- fontWeight: 600,
944
- },
945
- "& hr": {
946
- border: "none",
947
- borderTop: "1px solid",
948
- borderColor: "neutral.outlinedBorder",
949
- my: 1,
950
- },
951
- "& img": {
952
- maxWidth: "100%",
953
- borderRadius: "6px",
954
- },
955
- }}
956
- >
957
- <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
958
- {message.content}
959
- </ReactMarkdown>
960
- </Box>
961
- )}
962
- </Box>
963
- );
964
- }
965
-
966
- interface ChatPanelProps {
967
- selectedChannelId?: string | null;
968
- selectedThreadId?: string | null;
969
- onSelectChannel?: (channelId: string | null) => void;
970
- onSelectThread?: (threadId: string | null) => void;
971
- onNavigateToAgent?: (agentId: string) => void;
972
- onNavigateToTask?: (taskId: string) => void;
973
- }
974
-
975
- export default function ChatPanel({
976
- selectedChannelId: controlledChannelId,
977
- selectedThreadId: controlledThreadId,
978
- onSelectChannel,
979
- onSelectThread,
980
- onNavigateToAgent,
981
- onNavigateToTask,
982
- }: ChatPanelProps) {
983
- // Internal state for uncontrolled mode
984
- const [internalChannelId, setInternalChannelId] = useState<string | null>(null);
985
- const [internalThreadId, setInternalThreadId] = useState<string | null>(null);
986
- const [messageInput, setMessageInput] = useState("");
987
- const [threadMessageInput, setThreadMessageInput] = useState("");
988
- const [channelDrawerOpen, setChannelDrawerOpen] = useState(false);
989
- const messagesEndRef = useRef<HTMLDivElement>(null);
990
- const threadEndRef = useRef<HTMLDivElement>(null);
991
- const messagesContainerRef = useRef<HTMLDivElement>(null);
992
- const previousNewestMessageIdRef = useRef<string | null>(null);
993
- const scrollPositionRef = useRef<{ scrollTop: number; scrollHeight: number } | null>(null);
994
-
995
- // Use controlled or internal state
996
- const selectedChannelId =
997
- controlledChannelId !== undefined ? controlledChannelId : internalChannelId;
998
- const selectedThreadId = controlledThreadId !== undefined ? controlledThreadId : internalThreadId;
999
-
1000
- const setSelectedChannelId = useCallback(
1001
- (id: string | null) => {
1002
- if (onSelectChannel) {
1003
- onSelectChannel(id);
1004
- } else {
1005
- setInternalChannelId(id);
1006
- }
1007
- },
1008
- [onSelectChannel],
1009
- );
1010
-
1011
- const setSelectedThreadId = useCallback(
1012
- (id: string | null) => {
1013
- if (onSelectThread) {
1014
- onSelectThread(id);
1015
- } else {
1016
- setInternalThreadId(id);
1017
- }
1018
- },
1019
- [onSelectThread],
1020
- );
1021
-
1022
- const { mode } = useColorScheme();
1023
- const isDark = mode === "dark";
1024
-
1025
- const colors = useMemo(
1026
- () => ({
1027
- amber: isDark ? "#F5A623" : "#D48806",
1028
- gold: isDark ? "#D4A574" : "#8B6914",
1029
- honey: isDark ? "#FFB84D" : "#B87300",
1030
- blue: "#3B82F6",
1031
- dormant: isDark ? "#6B5344" : "#A89A7C",
1032
- amberGlow: isDark ? "0 0 8px rgba(245, 166, 35, 0.5)" : "0 0 6px rgba(212, 136, 6, 0.3)",
1033
- hoverBg: isDark ? "rgba(245, 166, 35, 0.05)" : "rgba(212, 136, 6, 0.05)",
1034
- selectedBg: isDark ? "rgba(245, 166, 35, 0.1)" : "rgba(212, 136, 6, 0.08)",
1035
- amberBorder: isDark ? "rgba(245, 166, 35, 0.3)" : "rgba(212, 136, 6, 0.25)",
1036
- inputBg: isDark ? "rgba(13, 9, 6, 0.6)" : "rgba(255, 255, 255, 0.8)",
1037
- inputBorder: isDark ? "#3A2D1F" : "#E5D9CA",
1038
- }),
1039
- [isDark],
1040
- );
1041
-
1042
- const { data: channels, isLoading: channelsLoading } = useChannels();
1043
- const {
1044
- data: messages,
1045
- isLoading: messagesLoading,
1046
- fetchNextPage,
1047
- hasNextPage,
1048
- isFetchingNextPage,
1049
- } = useInfiniteMessages(selectedChannelId || "");
1050
- const { data: threadMessages } = useThreadMessages(
1051
- selectedChannelId || "",
1052
- selectedThreadId || "",
1053
- );
1054
- const postMessageMutation = usePostMessage(selectedChannelId || "");
1055
- const { data: agents } = useAgents();
1056
- const agentsList = useMemo(() => agents || [], [agents]);
1057
-
1058
- // Create name -> id mapping for @mention links
1059
- const agentsByName = useMemo(() => {
1060
- const map = new Map<string, string>();
1061
- agents?.forEach((agent) => map.set(agent.name, agent.id));
1062
- return map;
1063
- }, [agents]);
1064
-
1065
- // Create set of lead agent IDs
1066
- const leadAgentIds = useMemo(() => {
1067
- const set = new Set<string>();
1068
- agents?.forEach((agent) => {
1069
- if (agent.isLead) set.add(agent.id);
1070
- });
1071
- return set;
1072
- }, [agents]);
1073
-
1074
- // Track mentions for main and thread inputs
1075
- const [messageMentions, setMessageMentions] = useState<string[]>([]);
1076
- const [threadMentions, setThreadMentions] = useState<string[]>([]);
1077
-
1078
- const selectedChannel = channels?.find((c) => c.id === selectedChannelId);
1079
-
1080
- // Find thread message from messages
1081
- const selectedThreadMessage = useMemo(() => {
1082
- if (!selectedThreadId || !messages) return null;
1083
- return messages.find((m) => m.id === selectedThreadId) || null;
1084
- }, [selectedThreadId, messages]);
1085
-
1086
- // Auto-select first channel only if no channel is selected
1087
- useEffect(() => {
1088
- if (channels && channels.length > 0 && !selectedChannelId) {
1089
- const firstChannel = channels[0];
1090
- if (firstChannel) {
1091
- setSelectedChannelId(firstChannel.id);
1092
- }
1093
- }
1094
- }, [channels, selectedChannelId, setSelectedChannelId]);
1095
-
1096
- // Reset scroll tracking when channel changes
1097
- useEffect(() => {
1098
- previousNewestMessageIdRef.current = null;
1099
- scrollPositionRef.current = null;
1100
- }, [selectedChannelId]);
1101
-
1102
- // Get the newest message ID (last in the sorted array)
1103
- const newestMessageId = messages?.[messages.length - 1]?.id ?? null;
1104
-
1105
- // Scroll to bottom only when a NEW message arrives AND user is near the bottom
1106
- useEffect(() => {
1107
- // If the newest message ID changed, a new message arrived
1108
- if (newestMessageId && newestMessageId !== previousNewestMessageIdRef.current) {
1109
- const isInitialLoad = previousNewestMessageIdRef.current === null;
1110
- previousNewestMessageIdRef.current = newestMessageId;
1111
-
1112
- const container = messagesContainerRef.current;
1113
-
1114
- if (isInitialLoad) {
1115
- // On initial load, scroll immediately without animation
1116
- messagesEndRef.current?.scrollIntoView();
1117
- } else if (container) {
1118
- // Check if user is near the bottom (within 150px)
1119
- const isNearBottom =
1120
- container.scrollHeight - container.scrollTop - container.clientHeight < 150;
1121
-
1122
- if (isNearBottom) {
1123
- // User is near bottom, scroll to show new message
1124
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
1125
- }
1126
- // If user is scrolled up, don't auto-scroll - let them read in peace
1127
- }
1128
- }
1129
- }, [newestMessageId]);
1130
-
1131
- // Preserve scroll position when loading older messages
1132
- useEffect(() => {
1133
- const container = messagesContainerRef.current;
1134
- if (!container || !scrollPositionRef.current) return;
1135
-
1136
- // Restore scroll position after older messages are prepended
1137
- const { scrollTop, scrollHeight: oldScrollHeight } = scrollPositionRef.current;
1138
- const newScrollHeight = container.scrollHeight;
1139
- const heightDiff = newScrollHeight - oldScrollHeight;
1140
-
1141
- if (heightDiff > 0) {
1142
- container.scrollTop = scrollTop + heightDiff;
1143
- }
1144
-
1145
- scrollPositionRef.current = null;
1146
- }, [messages]);
1147
-
1148
- // Scroll to bottom of thread when thread opens or messages change
1149
- useEffect(() => {
1150
- if (selectedThreadId && threadMessages) {
1151
- setTimeout(() => {
1152
- threadEndRef.current?.scrollIntoView({ behavior: "smooth" });
1153
- }, 100);
1154
- }
1155
- }, [selectedThreadId, threadMessages]);
1156
-
1157
- // Count replies per message
1158
- const replyCounts = new Map<string, number>();
1159
- messages?.forEach((msg) => {
1160
- if (msg.replyToId) {
1161
- replyCounts.set(msg.replyToId, (replyCounts.get(msg.replyToId) || 0) + 1);
1162
- }
1163
- });
1164
-
1165
- // Filter out threaded replies from main view (only show top-level messages)
1166
- const topLevelMessages = messages?.filter((msg) => !msg.replyToId) || [];
1167
-
1168
- const handleSendMessage = useCallback(() => {
1169
- if (!messageInput.trim() || !selectedChannelId) return;
1170
-
1171
- postMessageMutation.mutate({
1172
- content: messageInput.trim(),
1173
- mentions: messageMentions.length > 0 ? messageMentions : undefined,
1174
- });
1175
- setMessageInput("");
1176
- setMessageMentions([]);
1177
- }, [messageInput, selectedChannelId, postMessageMutation, messageMentions]);
1178
-
1179
- const handleSendThreadMessage = useCallback(() => {
1180
- if (!threadMessageInput.trim() || !selectedChannelId || !selectedThreadMessage) return;
1181
-
1182
- postMessageMutation.mutate({
1183
- content: threadMessageInput.trim(),
1184
- replyToId: selectedThreadMessage.id,
1185
- mentions: threadMentions.length > 0 ? threadMentions : undefined,
1186
- });
1187
- setThreadMessageInput("");
1188
- setThreadMentions([]);
1189
- }, [
1190
- threadMessageInput,
1191
- selectedChannelId,
1192
- selectedThreadMessage,
1193
- postMessageMutation,
1194
- threadMentions,
1195
- ]);
1196
-
1197
- const handleOpenThread = useCallback(
1198
- (message: ChannelMessage) => {
1199
- setSelectedThreadId(message.id);
1200
- },
1201
- [setSelectedThreadId],
1202
- );
1203
-
1204
- const handleCloseThread = useCallback(() => {
1205
- setSelectedThreadId(null);
1206
- }, [setSelectedThreadId]);
1207
-
1208
- const handleAgentClick = useCallback(
1209
- (agentId: string) => {
1210
- if (onNavigateToAgent) {
1211
- onNavigateToAgent(agentId);
1212
- }
1213
- },
1214
- [onNavigateToAgent],
1215
- );
1216
-
1217
- // Input styles shared between main and thread (memoized to prevent re-renders)
1218
- const inputStyles = useMemo(
1219
- () => ({
1220
- flex: 1,
1221
- fontFamily: "'Space Grotesk', sans-serif",
1222
- fontSize: "16px", // 16px prevents iOS zoom on focus
1223
- bgcolor: colors.inputBg,
1224
- borderColor: colors.inputBorder,
1225
- borderRadius: "8px",
1226
- "--Textarea-focusedThickness": "2px",
1227
- "--Textarea-focusedHighlight": colors.amber,
1228
- "&:hover": {
1229
- borderColor: isDark ? "#4A3A2F" : "#D1C5B4",
1230
- },
1231
- "&:focus-within": {
1232
- borderColor: colors.amber,
1233
- boxShadow: isDark
1234
- ? "0 0 0 2px rgba(245, 166, 35, 0.15)"
1235
- : "0 0 0 2px rgba(212, 136, 6, 0.1)",
1236
- },
1237
- "& textarea": {
1238
- fontFamily: "'Space Grotesk', sans-serif",
1239
- fontSize: "16px", // 16px prevents iOS zoom on focus
1240
- color: isDark ? "#FFF8E7" : "#1A130E",
1241
- },
1242
- "& textarea::placeholder": {
1243
- color: isDark ? "#8B7355" : "#8B7355",
1244
- fontFamily: "'Space Grotesk', sans-serif",
1245
- },
1246
- }),
1247
- [colors.inputBg, colors.inputBorder, colors.amber, isDark],
1248
- );
1249
-
1250
- const sendButtonStyles = useMemo(
1251
- () => ({
1252
- fontFamily: "'Space Grotesk', sans-serif",
1253
- fontSize: "0.9rem",
1254
- fontWeight: 600,
1255
- letterSpacing: "0.03em",
1256
- px: 3,
1257
- py: 1.5,
1258
- minHeight: 44, // Good touch target size
1259
- borderRadius: "8px",
1260
- bgcolor: colors.amber,
1261
- color: isDark ? "#1A130E" : "#FFFFFF",
1262
- border: "none",
1263
- cursor: "pointer",
1264
- transition: "all 0.2s ease",
1265
- "&:hover": {
1266
- bgcolor: colors.honey,
1267
- transform: "translateY(-1px)",
1268
- boxShadow: isDark
1269
- ? "0 4px 12px rgba(245, 166, 35, 0.3)"
1270
- : "0 4px 12px rgba(212, 136, 6, 0.2)",
1271
- },
1272
- "&:active": {
1273
- transform: "translateY(0)",
1274
- },
1275
- "&:disabled": {
1276
- opacity: 0.5,
1277
- cursor: "not-allowed",
1278
- transform: "none",
1279
- boxShadow: "none",
1280
- },
1281
- }),
1282
- [colors.amber, colors.honey, isDark],
1283
- );
1284
-
1285
- // Channel list content - reused in drawer and desktop sidebar
1286
- const channelListContent = (
1287
- <Box sx={{ flex: 1, overflow: "auto", p: 1 }}>
1288
- {channelsLoading ? (
1289
- <Typography
1290
- sx={{
1291
- fontFamily: "'Space Grotesk', sans-serif",
1292
- fontSize: "0.8rem",
1293
- color: "text.tertiary",
1294
- p: 1.5,
1295
- }}
1296
- >
1297
- Loading...
1298
- </Typography>
1299
- ) : !channels || channels.length === 0 ? (
1300
- <Typography
1301
- sx={{
1302
- fontFamily: "'Space Grotesk', sans-serif",
1303
- fontSize: "0.8rem",
1304
- color: "text.tertiary",
1305
- p: 1.5,
1306
- }}
1307
- >
1308
- No channels
1309
- </Typography>
1310
- ) : (
1311
- channels.map((channel) => (
1312
- <Box
1313
- key={channel.id}
1314
- onClick={() => {
1315
- setSelectedChannelId(channel.id);
1316
- setSelectedThreadId(null);
1317
- setChannelDrawerOpen(false);
1318
- }}
1319
- sx={{
1320
- px: 1.5,
1321
- py: 1.5,
1322
- borderRadius: "6px",
1323
- cursor: "pointer",
1324
- bgcolor: selectedChannelId === channel.id ? colors.selectedBg : "transparent",
1325
- border: "1px solid",
1326
- borderColor: selectedChannelId === channel.id ? colors.amberBorder : "transparent",
1327
- transition: "all 0.15s ease",
1328
- mb: 0.5,
1329
- minHeight: 44,
1330
- display: "flex",
1331
- alignItems: "center",
1332
- "&:hover": {
1333
- bgcolor: selectedChannelId === channel.id ? colors.selectedBg : colors.hoverBg,
1334
- },
1335
- }}
1336
- >
1337
- <Typography
1338
- sx={{
1339
- fontFamily: "'JetBrains Mono', monospace",
1340
- fontSize: "0.8rem",
1341
- fontWeight: selectedChannelId === channel.id ? 600 : 400,
1342
- color: selectedChannelId === channel.id ? colors.amber : "text.secondary",
1343
- }}
1344
- >
1345
- # {channel.name}
1346
- </Typography>
1347
- </Box>
1348
- ))
1349
- )}
1350
- </Box>
1351
- );
1352
-
1353
- return (
1354
- <Card
1355
- variant="outlined"
1356
- sx={{
1357
- p: 0,
1358
- height: "100%",
1359
- display: "flex",
1360
- flexDirection: "row",
1361
- overflow: "hidden",
1362
- bgcolor: "background.surface",
1363
- borderColor: "neutral.outlinedBorder",
1364
- borderRadius: { xs: 0, md: "12px" },
1365
- gap: 0,
1366
- }}
1367
- >
1368
- {/* Mobile Channel Drawer */}
1369
- <Drawer
1370
- open={channelDrawerOpen}
1371
- onClose={() => setChannelDrawerOpen(false)}
1372
- sx={{
1373
- display: { xs: "block", md: "none" },
1374
- "& .MuiDrawer-content": {
1375
- width: 280,
1376
- bgcolor: isDark ? "#1A130E" : "#FDF8F3",
1377
- },
1378
- }}
1379
- >
1380
- <Box
1381
- sx={{
1382
- height: "100%",
1383
- display: "flex",
1384
- flexDirection: "column",
1385
- }}
1386
- >
1387
- {/* Drawer header */}
1388
- <Box
1389
- sx={{
1390
- px: 2,
1391
- py: 1.5,
1392
- borderBottom: "1px solid",
1393
- borderColor: "neutral.outlinedBorder",
1394
- bgcolor: "background.level1",
1395
- display: "flex",
1396
- alignItems: "center",
1397
- justifyContent: "space-between",
1398
- minHeight: 56,
1399
- }}
1400
- >
1401
- <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
1402
- <Box
1403
- sx={{
1404
- width: 8,
1405
- height: 10,
1406
- clipPath: "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)",
1407
- bgcolor: colors.amber,
1408
- boxShadow: colors.amberGlow,
1409
- }}
1410
- />
1411
- <Typography
1412
- sx={{
1413
- fontFamily: "'Space Grotesk', sans-serif",
1414
- fontWeight: 600,
1415
- color: colors.amber,
1416
- letterSpacing: "0.05em",
1417
- fontSize: "0.85rem",
1418
- }}
1419
- >
1420
- CHANNELS
1421
- </Typography>
1422
- </Box>
1423
- <IconButton
1424
- size="sm"
1425
- variant="plain"
1426
- onClick={() => setChannelDrawerOpen(false)}
1427
- sx={{
1428
- color: "text.tertiary",
1429
- minWidth: 44,
1430
- minHeight: 44,
1431
- "&:hover": { color: "text.primary", bgcolor: colors.hoverBg },
1432
- }}
1433
- >
1434
-
1435
- </IconButton>
1436
- </Box>
1437
- {channelListContent}
1438
- </Box>
1439
- </Drawer>
1440
-
1441
- {/* Desktop Channel List - Fixed width, hidden on mobile */}
1442
- <Box
1443
- sx={{
1444
- width: 220,
1445
- flexShrink: 0,
1446
- borderRight: "1px solid",
1447
- borderColor: "neutral.outlinedBorder",
1448
- display: { xs: "none", md: "flex" },
1449
- flexDirection: "column",
1450
- overflow: "hidden",
1451
- bgcolor: isDark ? "rgba(13, 9, 6, 0.3)" : "rgba(245, 237, 228, 0.5)",
1452
- }}
1453
- >
1454
- {/* Channels header */}
1455
- <Box
1456
- sx={{
1457
- px: 2,
1458
- py: 1.5,
1459
- borderBottom: "1px solid",
1460
- borderColor: "neutral.outlinedBorder",
1461
- bgcolor: "background.level1",
1462
- height: 64,
1463
- display: "flex",
1464
- alignItems: "center",
1465
- }}
1466
- >
1467
- <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
1468
- <Box
1469
- sx={{
1470
- width: 8,
1471
- height: 10,
1472
- clipPath: "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)",
1473
- bgcolor: colors.amber,
1474
- boxShadow: colors.amberGlow,
1475
- }}
1476
- />
1477
- <Typography
1478
- sx={{
1479
- fontFamily: "'Space Grotesk', sans-serif",
1480
- fontWeight: 600,
1481
- color: colors.amber,
1482
- letterSpacing: "0.05em",
1483
- fontSize: "0.85rem",
1484
- }}
1485
- >
1486
- CHANNELS
1487
- </Typography>
1488
- </Box>
1489
- </Box>
1490
- {channelListContent}
1491
- </Box>
1492
-
1493
- {/* Messages Panel - Flex, equal split with thread, hidden when thread is open on mobile */}
1494
- <Box
1495
- sx={{
1496
- flex: 1,
1497
- display: {
1498
- xs: selectedThreadMessage ? "none" : "flex",
1499
- md: "flex",
1500
- },
1501
- flexDirection: "column",
1502
- overflow: "hidden",
1503
- minWidth: 0,
1504
- }}
1505
- >
1506
- {/* Channel header with title and description */}
1507
- <Box
1508
- sx={{
1509
- px: { xs: 1.5, md: 2.5 },
1510
- py: 1.5,
1511
- borderBottom: "1px solid",
1512
- borderColor: "neutral.outlinedBorder",
1513
- bgcolor: "background.level1",
1514
- height: { xs: 56, md: 64 },
1515
- display: "flex",
1516
- alignItems: "center",
1517
- gap: 1,
1518
- }}
1519
- >
1520
- {/* Mobile hamburger menu */}
1521
- <IconButton
1522
- size="sm"
1523
- variant="plain"
1524
- onClick={() => setChannelDrawerOpen(true)}
1525
- sx={{
1526
- display: { xs: "flex", md: "none" },
1527
- color: colors.amber,
1528
- minWidth: 44,
1529
- minHeight: 44,
1530
- "&:hover": { bgcolor: colors.hoverBg },
1531
- }}
1532
- >
1533
-
1534
- </IconButton>
1535
-
1536
- <Box sx={{ flex: 1, minWidth: 0 }}>
1537
- <Typography
1538
- sx={{
1539
- fontFamily: "'Space Grotesk', sans-serif",
1540
- fontWeight: 600,
1541
- fontSize: { xs: "0.9rem", md: "1rem" },
1542
- color: "text.primary",
1543
- mb: 0.25,
1544
- overflow: "hidden",
1545
- textOverflow: "ellipsis",
1546
- whiteSpace: "nowrap",
1547
- }}
1548
- >
1549
- # {selectedChannel?.name || "Select a channel"}
1550
- </Typography>
1551
- {selectedChannel?.description && (
1552
- <Typography
1553
- sx={{
1554
- fontFamily: "'Space Grotesk', sans-serif",
1555
- fontSize: "0.7rem",
1556
- color: "text.tertiary",
1557
- lineHeight: 1.4,
1558
- display: { xs: "none", sm: "block" },
1559
- overflow: "hidden",
1560
- textOverflow: "ellipsis",
1561
- whiteSpace: "nowrap",
1562
- }}
1563
- >
1564
- {selectedChannel.description}
1565
- </Typography>
1566
- )}
1567
- </Box>
1568
- </Box>
1569
-
1570
- {/* Messages list */}
1571
- <Box
1572
- ref={messagesContainerRef}
1573
- sx={{
1574
- flex: 1,
1575
- overflow: "auto",
1576
- py: 1,
1577
- bgcolor: isDark ? "rgba(13, 9, 6, 0.2)" : "rgba(253, 248, 243, 0.5)",
1578
- }}
1579
- >
1580
- {messagesLoading ? (
1581
- <Typography
1582
- sx={{
1583
- fontFamily: "'Space Grotesk', sans-serif",
1584
- fontSize: "0.85rem",
1585
- color: "text.tertiary",
1586
- p: 3,
1587
- }}
1588
- >
1589
- Loading messages...
1590
- </Typography>
1591
- ) : topLevelMessages.length === 0 ? (
1592
- <Box sx={{ p: 3, textAlign: "center" }}>
1593
- <Typography
1594
- sx={{
1595
- fontFamily: "'Space Grotesk', sans-serif",
1596
- fontSize: "0.9rem",
1597
- color: "text.tertiary",
1598
- mb: 1,
1599
- }}
1600
- >
1601
- No messages yet
1602
- </Typography>
1603
- <Typography
1604
- sx={{
1605
- fontFamily: "'Space Grotesk', sans-serif",
1606
- fontSize: "0.8rem",
1607
- color: "text.tertiary",
1608
- }}
1609
- >
1610
- Start the conversation!
1611
- </Typography>
1612
- </Box>
1613
- ) : (
1614
- <>
1615
- {/* Load more button */}
1616
- {hasNextPage && (
1617
- <Box sx={{ display: "flex", justifyContent: "center", py: 2 }}>
1618
- <Box
1619
- component="button"
1620
- onClick={() => {
1621
- // Save scroll position before loading older messages
1622
- const container = messagesContainerRef.current;
1623
- if (container) {
1624
- scrollPositionRef.current = {
1625
- scrollTop: container.scrollTop,
1626
- scrollHeight: container.scrollHeight,
1627
- };
1628
- }
1629
- fetchNextPage();
1630
- }}
1631
- disabled={isFetchingNextPage}
1632
- sx={{
1633
- fontFamily: "'Space Grotesk', sans-serif",
1634
- fontSize: "0.8rem",
1635
- fontWeight: 600,
1636
- color: colors.amber,
1637
- bgcolor: "transparent",
1638
- border: "1px solid",
1639
- borderColor: colors.amberBorder,
1640
- borderRadius: "6px",
1641
- px: 3,
1642
- py: 1,
1643
- cursor: isFetchingNextPage ? "wait" : "pointer",
1644
- transition: "all 0.2s ease",
1645
- opacity: isFetchingNextPage ? 0.6 : 1,
1646
- "&:hover": {
1647
- bgcolor: colors.hoverBg,
1648
- borderColor: colors.amber,
1649
- },
1650
- }}
1651
- >
1652
- {isFetchingNextPage ? "Loading..." : "Load older messages"}
1653
- </Box>
1654
- </Box>
1655
- )}
1656
-
1657
- {/* Messages with date dividers */}
1658
- {(() => {
1659
- let lastDateKey = "";
1660
- return topLevelMessages.map((message) => {
1661
- const dateKey = getDateKey(message.createdAt);
1662
- const showDivider = dateKey !== lastDateKey;
1663
- lastDateKey = dateKey;
1664
- return (
1665
- <React.Fragment key={message.id}>
1666
- {showDivider && (
1667
- <DateDivider date={message.createdAt} isDark={isDark} colors={colors} />
1668
- )}
1669
- <MessageItem
1670
- message={message}
1671
- isDark={isDark}
1672
- colors={colors}
1673
- onOpenThread={() => handleOpenThread(message)}
1674
- threadCount={replyCounts.get(message.id)}
1675
- onAgentClick={handleAgentClick}
1676
- onTaskClick={onNavigateToTask}
1677
- isSelected={selectedThreadMessage?.id === message.id}
1678
- agentsByName={agentsByName}
1679
- isLeadAgent={message.agentId ? leadAgentIds.has(message.agentId) : false}
1680
- />
1681
- </React.Fragment>
1682
- );
1683
- });
1684
- })()}
1685
- <div ref={messagesEndRef} />
1686
- </>
1687
- )}
1688
- </Box>
1689
-
1690
- {/* Message input */}
1691
- <Box
1692
- sx={{
1693
- p: { xs: 1.5, md: 2 },
1694
- borderTop: "1px solid",
1695
- borderColor: "neutral.outlinedBorder",
1696
- bgcolor: "background.level1",
1697
- }}
1698
- >
1699
- <MentionInput
1700
- value={messageInput}
1701
- onChange={setMessageInput}
1702
- onSend={handleSendMessage}
1703
- onMentionsChange={setMessageMentions}
1704
- placeholder="Type a message... (use @ to mention)"
1705
- agents={agentsList}
1706
- inputStyles={inputStyles}
1707
- sendButtonStyles={sendButtonStyles}
1708
- sendLabel="Send"
1709
- disabled={!messageInput.trim() || postMessageMutation.isPending}
1710
- colors={colors}
1711
- isDark={isDark}
1712
- />
1713
- </Box>
1714
- </Box>
1715
-
1716
- {/* Thread Panel - Full screen on mobile, equal width on desktop */}
1717
- {selectedThreadMessage && (
1718
- <Box
1719
- sx={{
1720
- position: { xs: "fixed", md: "relative" },
1721
- inset: { xs: 0, md: "auto" },
1722
- zIndex: { xs: 1300, md: "auto" },
1723
- flex: { xs: "none", md: 1 },
1724
- width: { xs: "100%", md: "auto" },
1725
- minWidth: 0,
1726
- borderLeft: { xs: "none", md: "1px solid" },
1727
- borderColor: "neutral.outlinedBorder",
1728
- display: "flex",
1729
- flexDirection: "column",
1730
- overflow: "hidden",
1731
- bgcolor: isDark ? "#1A130E" : "#FDF8F3",
1732
- }}
1733
- >
1734
- {/* Thread header */}
1735
- <Box
1736
- sx={{
1737
- px: { xs: 1.5, md: 2.5 },
1738
- py: 1.5,
1739
- borderBottom: "1px solid",
1740
- borderColor: "neutral.outlinedBorder",
1741
- bgcolor: "background.level1",
1742
- display: "flex",
1743
- alignItems: "center",
1744
- justifyContent: "space-between",
1745
- height: { xs: 56, md: 64 },
1746
- }}
1747
- >
1748
- <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
1749
- {/* Mobile back button */}
1750
- <IconButton
1751
- size="sm"
1752
- variant="plain"
1753
- onClick={handleCloseThread}
1754
- sx={{
1755
- display: { xs: "flex", md: "none" },
1756
- color: colors.gold,
1757
- minWidth: 44,
1758
- minHeight: 44,
1759
- "&:hover": { bgcolor: colors.hoverBg },
1760
- }}
1761
- >
1762
-
1763
- </IconButton>
1764
- <Box
1765
- sx={{
1766
- width: 8,
1767
- height: 10,
1768
- clipPath: "polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)",
1769
- bgcolor: colors.gold,
1770
- boxShadow: isDark
1771
- ? "0 0 6px rgba(212, 165, 116, 0.4)"
1772
- : "0 0 4px rgba(139, 105, 20, 0.3)",
1773
- display: { xs: "none", md: "block" },
1774
- }}
1775
- />
1776
- <Typography
1777
- sx={{
1778
- fontFamily: "'Space Grotesk', sans-serif",
1779
- fontWeight: 600,
1780
- fontSize: "0.85rem",
1781
- color: colors.gold,
1782
- letterSpacing: "0.05em",
1783
- }}
1784
- >
1785
- THREAD
1786
- </Typography>
1787
- </Box>
1788
- {/* Desktop close button */}
1789
- <Tooltip title="Close thread" placement="bottom">
1790
- <IconButton
1791
- size="sm"
1792
- variant="plain"
1793
- onClick={handleCloseThread}
1794
- sx={{
1795
- display: { xs: "none", md: "flex" },
1796
- color: "text.tertiary",
1797
- fontSize: "1.1rem",
1798
- "&:hover": {
1799
- color: "text.primary",
1800
- bgcolor: colors.hoverBg,
1801
- },
1802
- }}
1803
- >
1804
-
1805
- </IconButton>
1806
- </Tooltip>
1807
- </Box>
1808
-
1809
- {/* Original message */}
1810
- <Box
1811
- sx={{
1812
- borderBottom: "1px solid",
1813
- borderColor: "neutral.outlinedBorder",
1814
- bgcolor: isDark ? "rgba(245, 166, 35, 0.03)" : "rgba(212, 136, 6, 0.02)",
1815
- maxHeight: "30vh",
1816
- overflow: "auto",
1817
- }}
1818
- >
1819
- <MessageItem
1820
- message={selectedThreadMessage}
1821
- isDark={isDark}
1822
- colors={colors}
1823
- isThreadView
1824
- onAgentClick={handleAgentClick}
1825
- onTaskClick={onNavigateToTask}
1826
- agentsByName={agentsByName}
1827
- isLeadAgent={
1828
- selectedThreadMessage.agentId
1829
- ? leadAgentIds.has(selectedThreadMessage.agentId)
1830
- : false
1831
- }
1832
- />
1833
- </Box>
1834
-
1835
- {/* Thread divider */}
1836
- <Box sx={{ px: 2, py: 1.5, display: "flex", alignItems: "center", gap: 2 }}>
1837
- <Box sx={{ flex: 1, height: 1, bgcolor: "neutral.outlinedBorder" }} />
1838
- <Typography
1839
- sx={{
1840
- fontFamily: "'JetBrains Mono', monospace",
1841
- fontSize: "0.65rem",
1842
- color: "text.tertiary",
1843
- letterSpacing: "0.05em",
1844
- }}
1845
- >
1846
- {threadMessages?.length || 0}{" "}
1847
- {(threadMessages?.length || 0) === 1 ? "REPLY" : "REPLIES"}
1848
- </Typography>
1849
- <Box sx={{ flex: 1, height: 1, bgcolor: "neutral.outlinedBorder" }} />
1850
- </Box>
1851
-
1852
- {/* Thread replies */}
1853
- <Box
1854
- sx={{
1855
- flex: 1,
1856
- overflow: "auto",
1857
- py: 0.5,
1858
- }}
1859
- >
1860
- {threadMessages && threadMessages.length > 0 ? (
1861
- <>
1862
- {threadMessages.map((message) => (
1863
- <MessageItem
1864
- key={message.id}
1865
- message={message}
1866
- isDark={isDark}
1867
- colors={colors}
1868
- isThreadView
1869
- onAgentClick={handleAgentClick}
1870
- onTaskClick={onNavigateToTask}
1871
- agentsByName={agentsByName}
1872
- isLeadAgent={message.agentId ? leadAgentIds.has(message.agentId) : false}
1873
- />
1874
- ))}
1875
- <div ref={threadEndRef} />
1876
- </>
1877
- ) : (
1878
- <Box sx={{ p: 3, textAlign: "center" }}>
1879
- <Typography
1880
- sx={{
1881
- fontFamily: "'Space Grotesk', sans-serif",
1882
- fontSize: "0.85rem",
1883
- color: "text.tertiary",
1884
- }}
1885
- >
1886
- No replies yet
1887
- </Typography>
1888
- </Box>
1889
- )}
1890
- </Box>
1891
-
1892
- {/* Thread message input */}
1893
- <Box
1894
- sx={{
1895
- p: { xs: 1.5, md: 2 },
1896
- borderTop: "1px solid",
1897
- borderColor: "neutral.outlinedBorder",
1898
- bgcolor: "background.level1",
1899
- }}
1900
- >
1901
- <MentionInput
1902
- value={threadMessageInput}
1903
- onChange={setThreadMessageInput}
1904
- onSend={handleSendThreadMessage}
1905
- onMentionsChange={setThreadMentions}
1906
- placeholder="Reply to thread... (use @ to mention)"
1907
- agents={agentsList}
1908
- inputStyles={inputStyles}
1909
- sendButtonStyles={sendButtonStyles}
1910
- sendLabel="Reply"
1911
- disabled={!threadMessageInput.trim() || postMessageMutation.isPending}
1912
- colors={colors}
1913
- isDark={isDark}
1914
- />
1915
- </Box>
1916
- </Box>
1917
- )}
1918
- </Card>
1919
- );
1920
- }