agent-relay 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1313) hide show
  1. package/.cursor/mcp.json +11 -0
  2. package/.gitleaks.toml +26 -0
  3. package/.mcp.json +11 -0
  4. package/.nvmrc +1 -1
  5. package/.turbo/cache/013415461711937f-meta.json +1 -0
  6. package/.turbo/cache/013415461711937f.tar.zst +0 -0
  7. package/.turbo/cache/0562b1ff326acd6d-meta.json +1 -0
  8. package/.turbo/cache/0562b1ff326acd6d.tar.zst +0 -0
  9. package/.turbo/cache/0b46e0e17254882f-meta.json +1 -0
  10. package/.turbo/cache/0b46e0e17254882f.tar.zst +0 -0
  11. package/.turbo/cache/3799eda981d53d14-meta.json +1 -0
  12. package/.turbo/cache/3799eda981d53d14.tar.zst +0 -0
  13. package/.turbo/cache/47e9d8f404ed064d-meta.json +1 -0
  14. package/.turbo/cache/47e9d8f404ed064d.tar.zst +0 -0
  15. package/.turbo/cache/4cde1d1e5b298099-meta.json +1 -0
  16. package/.turbo/cache/4cde1d1e5b298099.tar.zst +0 -0
  17. package/.turbo/cache/538eea955c0936ef-meta.json +1 -0
  18. package/.turbo/cache/538eea955c0936ef.tar.zst +0 -0
  19. package/.turbo/cache/5dceac7f229f5d5d-meta.json +1 -0
  20. package/.turbo/cache/5dceac7f229f5d5d.tar.zst +0 -0
  21. package/.turbo/cache/64c15b201819367d-meta.json +1 -0
  22. package/.turbo/cache/64c15b201819367d.tar.zst +0 -0
  23. package/.turbo/cache/6d6a21a05efca434-meta.json +1 -0
  24. package/.turbo/cache/6d6a21a05efca434.tar.zst +0 -0
  25. package/.turbo/cache/7562610cb03ec040-meta.json +1 -0
  26. package/.turbo/cache/7562610cb03ec040.tar.zst +0 -0
  27. package/.turbo/cache/81a2456e17af4d7f-meta.json +1 -0
  28. package/.turbo/cache/81a2456e17af4d7f.tar.zst +0 -0
  29. package/.turbo/cache/823fc2a7b12f724c-meta.json +1 -0
  30. package/.turbo/cache/823fc2a7b12f724c.tar.zst +0 -0
  31. package/.turbo/cache/9daad16a073d1f91-meta.json +1 -0
  32. package/.turbo/cache/9daad16a073d1f91.tar.zst +0 -0
  33. package/.turbo/cache/b81ccbab0a606b60-meta.json +1 -0
  34. package/.turbo/cache/b81ccbab0a606b60.tar.zst +0 -0
  35. package/.turbo/cache/cf98487988bfcf91-meta.json +1 -0
  36. package/.turbo/cache/cf98487988bfcf91.tar.zst +0 -0
  37. package/.turbo/cache/cfdf7c57dca71f27-meta.json +1 -0
  38. package/.turbo/cache/cfdf7c57dca71f27.tar.zst +0 -0
  39. package/.turbo/cache/d3063ef43811b1e5-meta.json +1 -0
  40. package/.turbo/cache/d3063ef43811b1e5.tar.zst +0 -0
  41. package/.turbo/cache/de28892eb7678e65-meta.json +1 -0
  42. package/.turbo/cache/de28892eb7678e65.tar.zst +0 -0
  43. package/.turbo/cache/ec29adce408132ba-meta.json +1 -0
  44. package/.turbo/cache/ec29adce408132ba.tar.zst +0 -0
  45. package/.turbo/cache/f70450d8d305f172-meta.json +1 -0
  46. package/.turbo/cache/f70450d8d305f172.tar.zst +0 -0
  47. package/.turbo/cache/fe384d5d6b7a983a-meta.json +1 -0
  48. package/.turbo/cache/fe384d5d6b7a983a.tar.zst +0 -0
  49. package/ARCHITECTURE.md +10 -10
  50. package/CHANGELOG.md +38 -0
  51. package/LICENSE +185 -17
  52. package/README.md +43 -5
  53. package/SESSION_HANDOFF.md +67 -0
  54. package/bin/relay-pty +0 -0
  55. package/bin/relay-pty-darwin-arm64 +0 -0
  56. package/bin/relay-pty-darwin-x64 +0 -0
  57. package/bin/relay-pty-linux-x64 +0 -0
  58. package/deploy/workspace/entrypoint.sh +79 -11
  59. package/deploy/workspace/git-credential-relay +152 -27
  60. package/deploy/workspace/git-credential-relay.test.sh +230 -0
  61. package/dist/dashboard/out/404.html +1 -1
  62. package/dist/dashboard/out/_next/static/chunks/116-a883fca163f3a5bc.js +1 -0
  63. package/dist/dashboard/out/_next/static/chunks/320-900169c942e31422.js +1 -0
  64. package/dist/dashboard/out/_next/static/chunks/631-af51bad94027527a.js +1 -0
  65. package/dist/dashboard/out/_next/static/chunks/766-2aea80818f7eb0d8.js +1 -0
  66. package/dist/dashboard/out/_next/static/chunks/891-5cb1513eeb97a891.js +1 -0
  67. package/dist/dashboard/out/_next/static/chunks/app/app/page-2e525b1dcc790967.js +1 -0
  68. package/dist/dashboard/out/_next/static/chunks/app/page-4e64923d73c35bc9.js +1 -0
  69. package/dist/dashboard/out/_next/static/chunks/app/providers/page-e65a0010da6ea5be.js +1 -0
  70. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-84161c802b020a1f.js +1 -0
  71. package/dist/dashboard/out/_next/static/css/99c2552394077586.css +1 -0
  72. package/dist/dashboard/out/app/onboarding.html +1 -1
  73. package/dist/dashboard/out/app/onboarding.txt +1 -1
  74. package/dist/dashboard/out/app.html +1 -1
  75. package/dist/dashboard/out/app.txt +2 -2
  76. package/dist/dashboard/out/cloud/link.html +1 -1
  77. package/dist/dashboard/out/cloud/link.txt +2 -2
  78. package/dist/dashboard/out/connect-repos.html +1 -1
  79. package/dist/dashboard/out/connect-repos.txt +1 -1
  80. package/dist/dashboard/out/history.html +1 -1
  81. package/dist/dashboard/out/history.txt +2 -2
  82. package/dist/dashboard/out/index.html +1 -1
  83. package/dist/dashboard/out/index.txt +2 -2
  84. package/dist/dashboard/out/login.html +2 -2
  85. package/dist/dashboard/out/login.txt +1 -1
  86. package/dist/dashboard/out/metrics.html +1 -1
  87. package/dist/dashboard/out/metrics.txt +2 -2
  88. package/dist/dashboard/out/pricing.html +3 -3
  89. package/dist/dashboard/out/pricing.txt +2 -2
  90. package/dist/dashboard/out/providers/setup/claude.html +1 -1
  91. package/dist/dashboard/out/providers/setup/claude.txt +2 -2
  92. package/dist/dashboard/out/providers/setup/codex.html +1 -1
  93. package/dist/dashboard/out/providers/setup/codex.txt +2 -2
  94. package/dist/dashboard/out/providers/setup/cursor.html +1 -0
  95. package/dist/dashboard/out/providers/setup/cursor.txt +8 -0
  96. package/dist/dashboard/out/providers.html +1 -1
  97. package/dist/dashboard/out/providers.txt +2 -2
  98. package/dist/dashboard/out/signup.html +2 -2
  99. package/dist/dashboard/out/signup.txt +1 -1
  100. package/dist/src/bridge/index.d.ts +8 -0
  101. package/dist/src/bridge/index.js +8 -0
  102. package/dist/src/cli/index.js +3089 -0
  103. package/dist/src/cloud/index.d.ts +8 -0
  104. package/dist/src/cloud/index.js +8 -0
  105. package/dist/src/config/relay-config.d.ts +5 -0
  106. package/dist/src/config/relay-config.js +5 -0
  107. package/dist/src/continuity/index.d.ts +5 -0
  108. package/dist/src/continuity/index.js +5 -0
  109. package/dist/src/daemon/index.d.ts +8 -0
  110. package/dist/src/daemon/index.js +9 -0
  111. package/dist/src/dashboard-server/index.d.ts +8 -0
  112. package/dist/src/dashboard-server/index.js +8 -0
  113. package/dist/src/hooks/index.d.ts +10 -0
  114. package/dist/src/hooks/index.js +10 -0
  115. package/dist/src/index.d.ts +13 -0
  116. package/dist/src/index.js +16 -0
  117. package/dist/src/memory/index.d.ts +5 -0
  118. package/dist/src/memory/index.js +5 -0
  119. package/dist/src/policy/index.d.ts +5 -0
  120. package/dist/src/policy/index.js +5 -0
  121. package/dist/src/protocol/index.d.ts +8 -0
  122. package/dist/src/protocol/index.js +8 -0
  123. package/dist/src/resiliency/index.d.ts +5 -0
  124. package/dist/src/resiliency/index.js +5 -0
  125. package/dist/src/shared/cli-auth-config.d.ts +5 -0
  126. package/dist/src/shared/cli-auth-config.js +5 -0
  127. package/dist/src/state/index.d.ts +5 -0
  128. package/dist/src/state/index.js +5 -0
  129. package/dist/src/storage/index.d.ts +8 -0
  130. package/dist/src/storage/index.js +8 -0
  131. package/dist/src/trajectory/index.d.ts +5 -0
  132. package/dist/src/trajectory/index.js +5 -0
  133. package/dist/src/utils/index.d.ts +5 -0
  134. package/dist/src/utils/index.js +5 -0
  135. package/dist/src/wrapper/index.d.ts +8 -0
  136. package/dist/src/wrapper/index.js +11 -0
  137. package/package.json +64 -19
  138. package/packages/api-types/dist/index.d.ts +21 -0
  139. package/packages/api-types/dist/index.js +22 -0
  140. package/packages/api-types/dist/schemas/agent.d.ts +259 -0
  141. package/packages/api-types/dist/schemas/agent.js +102 -0
  142. package/packages/api-types/dist/schemas/api.d.ts +290 -0
  143. package/packages/api-types/dist/schemas/api.js +162 -0
  144. package/packages/api-types/dist/schemas/decision.d.ts +230 -0
  145. package/packages/api-types/dist/schemas/decision.js +104 -0
  146. package/packages/api-types/dist/schemas/fleet.d.ts +615 -0
  147. package/packages/api-types/dist/schemas/fleet.js +71 -0
  148. package/packages/api-types/dist/schemas/history.d.ts +180 -0
  149. package/packages/api-types/dist/schemas/history.js +72 -0
  150. package/packages/api-types/dist/schemas/index.d.ts +14 -0
  151. package/packages/api-types/dist/schemas/index.js +22 -0
  152. package/packages/api-types/dist/schemas/message.d.ts +456 -0
  153. package/packages/api-types/dist/schemas/message.js +88 -0
  154. package/packages/api-types/dist/schemas/session.d.ts +60 -0
  155. package/packages/api-types/dist/schemas/session.js +36 -0
  156. package/packages/api-types/dist/schemas/task.d.ts +111 -0
  157. package/packages/api-types/dist/schemas/task.js +64 -0
  158. package/packages/api-types/package.json +61 -0
  159. package/packages/api-types/scripts/generate-openapi.ts +106 -0
  160. package/packages/bridge/dist/index.d.ts +8 -0
  161. package/packages/bridge/dist/index.js +9 -0
  162. package/packages/bridge/dist/multi-project-client.d.ts +99 -0
  163. package/packages/bridge/dist/multi-project-client.js +389 -0
  164. package/packages/bridge/dist/shadow-cli.js +75 -0
  165. package/packages/bridge/dist/spawner.d.ts +210 -0
  166. package/packages/bridge/dist/spawner.js +1276 -0
  167. package/packages/bridge/dist/types.d.ts +131 -0
  168. package/packages/bridge/dist/utils.d.ts +15 -0
  169. package/packages/bridge/dist/utils.js +60 -0
  170. package/packages/bridge/package.json +40 -0
  171. package/packages/cloud/dist/api/admin.js +225 -0
  172. package/packages/cloud/dist/api/billing.js +564 -0
  173. package/packages/cloud/dist/api/cli-pty-runner.d.ts +53 -0
  174. package/packages/cloud/dist/api/cli-pty-runner.js +193 -0
  175. package/packages/cloud/dist/api/codex-auth-helper.js +327 -0
  176. package/packages/cloud/dist/api/consensus.js +261 -0
  177. package/packages/cloud/dist/api/coordinators.js +750 -0
  178. package/packages/cloud/dist/api/daemons.js +535 -0
  179. package/packages/cloud/dist/api/generic-webhooks.js +129 -0
  180. package/packages/cloud/dist/api/github-app.js +223 -0
  181. package/packages/cloud/dist/api/monitoring.js +578 -0
  182. package/packages/cloud/dist/api/nango-auth.js +674 -0
  183. package/packages/cloud/dist/api/onboarding.d.ts +15 -0
  184. package/packages/cloud/dist/api/onboarding.js +679 -0
  185. package/packages/cloud/dist/api/policy.js +229 -0
  186. package/packages/cloud/dist/api/provider-env.d.ts +14 -0
  187. package/packages/cloud/dist/api/provider-env.js +75 -0
  188. package/packages/cloud/dist/api/providers.js +564 -0
  189. package/packages/cloud/dist/api/repos.js +577 -0
  190. package/packages/cloud/dist/api/sessions.d.ts +11 -0
  191. package/packages/cloud/dist/api/sessions.js +302 -0
  192. package/packages/cloud/dist/api/teams.js +281 -0
  193. package/packages/cloud/dist/api/test-helpers.js +745 -0
  194. package/packages/cloud/dist/api/workspaces.js +1799 -0
  195. package/packages/cloud/dist/billing/plans.js +245 -0
  196. package/packages/cloud/dist/config.d.ts +5 -0
  197. package/packages/cloud/dist/config.js +5 -0
  198. package/packages/cloud/dist/db/drizzle.d.ts +256 -0
  199. package/packages/cloud/dist/db/drizzle.js +1286 -0
  200. package/packages/cloud/dist/db/schema.d.ts +4873 -0
  201. package/packages/cloud/dist/db/schema.js +620 -0
  202. package/packages/cloud/dist/index.d.ts +11 -0
  203. package/packages/cloud/dist/index.js +38 -0
  204. package/packages/cloud/dist/provisioner/index.d.ts +207 -0
  205. package/packages/cloud/dist/provisioner/index.js +2114 -0
  206. package/packages/cloud/dist/server.js +1924 -0
  207. package/packages/cloud/dist/services/index.d.ts +17 -0
  208. package/packages/cloud/dist/services/index.js +25 -0
  209. package/packages/cloud/dist/services/intro-expiration.d.ts +60 -0
  210. package/packages/cloud/dist/services/intro-expiration.js +252 -0
  211. package/packages/cloud/dist/services/nango.d.ts +201 -0
  212. package/packages/cloud/dist/services/nango.js +392 -0
  213. package/packages/cloud/dist/services/persistence.d.ts +131 -0
  214. package/packages/cloud/dist/shims/consensus.d.ts +23 -0
  215. package/packages/cloud/dist/shims/consensus.js +5 -0
  216. package/packages/cloud/package.json +55 -0
  217. package/packages/config/dist/bridge-config.d.ts +52 -0
  218. package/packages/config/dist/bridge-config.js +143 -0
  219. package/packages/config/dist/bridge-utils.d.ts +30 -0
  220. package/packages/config/dist/bridge-utils.js +54 -0
  221. package/packages/config/dist/cli-auth-config.js +391 -0
  222. package/packages/config/dist/cloud-config.d.ts +75 -0
  223. package/packages/config/dist/cloud-config.js +109 -0
  224. package/packages/config/dist/index.d.ts +13 -0
  225. package/packages/config/dist/index.js +13 -0
  226. package/packages/config/dist/project-namespace.d.ts +73 -0
  227. package/packages/config/dist/project-namespace.js +280 -0
  228. package/packages/config/dist/relay-config.d.ts +25 -0
  229. package/packages/config/dist/relay-config.js +25 -0
  230. package/packages/config/dist/relay-file-writer.d.ts +200 -0
  231. package/packages/config/dist/relay-file-writer.js +407 -0
  232. package/packages/config/dist/schemas.d.ts +672 -0
  233. package/packages/config/dist/schemas.js +180 -0
  234. package/packages/config/dist/shadow-config.d.ts +87 -0
  235. package/packages/config/dist/trajectory-config.d.ts +102 -0
  236. package/packages/config/dist/trajectory-config.js +185 -0
  237. package/packages/config/package.json +98 -0
  238. package/packages/continuity/dist/index.d.ts +9 -0
  239. package/packages/continuity/dist/index.js +9 -0
  240. package/packages/continuity/dist/types.d.ts +180 -0
  241. package/packages/continuity/dist/types.js +2 -0
  242. package/packages/continuity/package.json +32 -0
  243. package/packages/daemon/dist/agent-manager.d.ts +134 -0
  244. package/packages/daemon/dist/agent-manager.js +578 -0
  245. package/packages/daemon/dist/agent-registry.js +213 -0
  246. package/packages/daemon/dist/api.d.ts +106 -0
  247. package/packages/daemon/dist/api.js +876 -0
  248. package/packages/daemon/dist/channel-membership-store.d.ts +55 -0
  249. package/packages/daemon/dist/channel-membership-store.js +176 -0
  250. package/packages/daemon/dist/cli-auth.d.ts +89 -0
  251. package/packages/daemon/dist/cli-auth.js +792 -0
  252. package/packages/daemon/dist/cloud-sync.d.ts +150 -0
  253. package/packages/daemon/dist/cloud-sync.js +446 -0
  254. package/packages/daemon/dist/connection.d.ts +130 -0
  255. package/packages/daemon/dist/connection.js +438 -0
  256. package/packages/daemon/dist/consensus-integration.js +371 -0
  257. package/packages/daemon/dist/delivery-tracker.d.ts +34 -0
  258. package/packages/daemon/dist/delivery-tracker.js +104 -0
  259. package/packages/daemon/dist/enhanced-features.d.ts +118 -0
  260. package/packages/daemon/dist/enhanced-features.js +176 -0
  261. package/packages/daemon/dist/index.d.ts +31 -0
  262. package/packages/daemon/dist/index.js +37 -0
  263. package/packages/daemon/dist/migrations/index.d.ts +73 -0
  264. package/packages/daemon/dist/migrations/index.js +241 -0
  265. package/packages/daemon/dist/orchestrator.d.ts +217 -0
  266. package/packages/daemon/dist/orchestrator.js +1143 -0
  267. package/packages/daemon/dist/relay-ledger.d.ts +261 -0
  268. package/packages/daemon/dist/relay-ledger.js +532 -0
  269. package/packages/daemon/dist/relay-watchdog.d.ts +125 -0
  270. package/packages/daemon/dist/relay-watchdog.js +611 -0
  271. package/packages/daemon/dist/repo-manager.js +384 -0
  272. package/packages/daemon/dist/router.d.ts +370 -0
  273. package/packages/daemon/dist/router.js +1437 -0
  274. package/packages/daemon/dist/server.d.ts +174 -0
  275. package/packages/daemon/dist/server.js +1001 -0
  276. package/packages/daemon/dist/spawn-manager.d.ts +78 -0
  277. package/packages/daemon/dist/spawn-manager.js +165 -0
  278. package/packages/daemon/dist/sync-queue.d.ts +116 -0
  279. package/packages/daemon/dist/sync-queue.js +361 -0
  280. package/packages/daemon/dist/types.d.ts +133 -0
  281. package/packages/daemon/dist/workspace-manager.js +314 -0
  282. package/packages/daemon/package.json +52 -0
  283. package/packages/dashboard/README.md +48 -0
  284. package/packages/dashboard/dist/health-worker-manager.d.ts +62 -0
  285. package/packages/dashboard/dist/health-worker-manager.js +144 -0
  286. package/packages/dashboard/dist/health-worker.d.ts +9 -0
  287. package/packages/dashboard/dist/health-worker.js +79 -0
  288. package/packages/dashboard/dist/index.d.ts +20 -0
  289. package/packages/dashboard/dist/index.js +19 -0
  290. package/packages/dashboard/dist/metrics.d.ts +105 -0
  291. package/packages/dashboard/dist/metrics.js +193 -0
  292. package/packages/dashboard/dist/needs-attention.d.ts +24 -0
  293. package/packages/dashboard/dist/needs-attention.js +78 -0
  294. package/packages/dashboard/dist/server.d.ts +25 -0
  295. package/packages/dashboard/dist/server.js +5107 -0
  296. package/packages/dashboard/dist/start.d.ts +6 -0
  297. package/packages/dashboard/dist/start.js +13 -0
  298. package/packages/dashboard/dist/types/threading.d.ts +8 -0
  299. package/packages/dashboard/dist/types/threading.js +2 -0
  300. package/packages/dashboard/dist/user-bridge.d.ts +154 -0
  301. package/packages/dashboard/dist/user-bridge.js +372 -0
  302. package/packages/dashboard/package.json +72 -0
  303. package/packages/dashboard/ui/.next/BUILD_ID +1 -0
  304. package/packages/dashboard/ui/.next/app-build-manifest.json +135 -0
  305. package/packages/dashboard/ui/.next/app-path-routes-manifest.json +1 -0
  306. package/packages/dashboard/ui/.next/build-manifest.json +32 -0
  307. package/packages/dashboard/ui/.next/cache/config.json +7 -0
  308. package/packages/dashboard/ui/.next/cache/eslint/.cache_1asv1h5 +1 -0
  309. package/packages/dashboard/ui/.next/cache/webpack/client-production/0.pack +0 -0
  310. package/packages/dashboard/ui/.next/cache/webpack/client-production/index.pack +0 -0
  311. package/packages/dashboard/ui/.next/cache/webpack/edge-server-production/0.pack +0 -0
  312. package/packages/dashboard/ui/.next/cache/webpack/edge-server-production/index.pack +0 -0
  313. package/packages/dashboard/ui/.next/cache/webpack/server-production/0.pack +0 -0
  314. package/packages/dashboard/ui/.next/cache/webpack/server-production/index.pack +0 -0
  315. package/packages/dashboard/ui/.next/export-detail.json +1 -0
  316. package/packages/dashboard/ui/.next/export-marker.json +1 -0
  317. package/packages/dashboard/ui/.next/images-manifest.json +1 -0
  318. package/packages/dashboard/ui/.next/next-minimal-server.js.nft.json +1 -0
  319. package/packages/dashboard/ui/.next/next-server.js.nft.json +1 -0
  320. package/packages/dashboard/ui/.next/package.json +1 -0
  321. package/packages/dashboard/ui/.next/prerender-manifest.json +1 -0
  322. package/packages/dashboard/ui/.next/react-loadable-manifest.json +1970 -0
  323. package/packages/dashboard/ui/.next/required-server-files.json +1 -0
  324. package/packages/dashboard/ui/.next/routes-manifest.json +1 -0
  325. package/packages/dashboard/ui/.next/server/app/_not-found/page.js +1 -0
  326. package/packages/dashboard/ui/.next/server/app/_not-found/page.js.nft.json +1 -0
  327. package/packages/dashboard/ui/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  328. package/packages/dashboard/ui/.next/server/app/_not-found.html +1 -0
  329. package/packages/dashboard/ui/.next/server/app/_not-found.meta +6 -0
  330. package/packages/dashboard/ui/.next/server/app/_not-found.rsc +9 -0
  331. package/packages/dashboard/ui/.next/server/app/app/onboarding/page.js +6 -0
  332. package/packages/dashboard/ui/.next/server/app/app/onboarding/page.js.nft.json +1 -0
  333. package/packages/dashboard/ui/.next/server/app/app/onboarding/page_client-reference-manifest.js +1 -0
  334. package/packages/dashboard/ui/.next/server/app/app/onboarding.html +1 -0
  335. package/packages/dashboard/ui/.next/server/app/app/onboarding.meta +5 -0
  336. package/packages/dashboard/ui/.next/server/app/app/onboarding.rsc +7 -0
  337. package/packages/dashboard/ui/.next/server/app/app/page.js +436 -0
  338. package/packages/dashboard/ui/.next/server/app/app/page.js.nft.json +1 -0
  339. package/packages/dashboard/ui/.next/server/app/app/page_client-reference-manifest.js +1 -0
  340. package/packages/dashboard/ui/.next/server/app/app.html +1 -0
  341. package/packages/dashboard/ui/.next/server/app/app.meta +5 -0
  342. package/packages/dashboard/ui/.next/server/app/app.rsc +7 -0
  343. package/packages/dashboard/ui/.next/server/app/apple-icon.png/route.js +12 -0
  344. package/packages/dashboard/ui/.next/server/app/apple-icon.png/route.js.nft.json +1 -0
  345. package/packages/dashboard/ui/.next/server/app/apple-icon.png.body +0 -0
  346. package/packages/dashboard/ui/.next/server/app/apple-icon.png.meta +1 -0
  347. package/packages/dashboard/ui/.next/server/app/cloud/link/page.js +1 -0
  348. package/packages/dashboard/ui/.next/server/app/cloud/link/page.js.nft.json +1 -0
  349. package/packages/dashboard/ui/.next/server/app/cloud/link/page_client-reference-manifest.js +1 -0
  350. package/packages/dashboard/ui/.next/server/app/cloud/link.html +1 -0
  351. package/packages/dashboard/ui/.next/server/app/cloud/link.meta +5 -0
  352. package/packages/dashboard/ui/.next/server/app/cloud/link.rsc +7 -0
  353. package/packages/dashboard/ui/.next/server/app/connect-repos/page.js +6 -0
  354. package/packages/dashboard/ui/.next/server/app/connect-repos/page.js.nft.json +1 -0
  355. package/packages/dashboard/ui/.next/server/app/connect-repos/page_client-reference-manifest.js +1 -0
  356. package/packages/dashboard/ui/.next/server/app/connect-repos.html +1 -0
  357. package/packages/dashboard/ui/.next/server/app/connect-repos.meta +5 -0
  358. package/packages/dashboard/ui/.next/server/app/connect-repos.rsc +7 -0
  359. package/packages/dashboard/ui/.next/server/app/history/page.js +1 -0
  360. package/packages/dashboard/ui/.next/server/app/history/page.js.nft.json +1 -0
  361. package/packages/dashboard/ui/.next/server/app/history/page_client-reference-manifest.js +1 -0
  362. package/packages/dashboard/ui/.next/server/app/history.html +1 -0
  363. package/packages/dashboard/ui/.next/server/app/history.meta +5 -0
  364. package/packages/dashboard/ui/.next/server/app/history.rsc +7 -0
  365. package/packages/dashboard/ui/.next/server/app/index.html +1 -0
  366. package/packages/dashboard/ui/.next/server/app/index.meta +5 -0
  367. package/packages/dashboard/ui/.next/server/app/index.rsc +7 -0
  368. package/packages/dashboard/ui/.next/server/app/login/page.js +6 -0
  369. package/packages/dashboard/ui/.next/server/app/login/page.js.nft.json +1 -0
  370. package/packages/dashboard/ui/.next/server/app/login/page_client-reference-manifest.js +1 -0
  371. package/packages/dashboard/ui/.next/server/app/login.html +5 -0
  372. package/packages/dashboard/ui/.next/server/app/login.meta +5 -0
  373. package/packages/dashboard/ui/.next/server/app/login.rsc +7 -0
  374. package/packages/dashboard/ui/.next/server/app/metrics/page.js +1 -0
  375. package/packages/dashboard/ui/.next/server/app/metrics/page.js.nft.json +1 -0
  376. package/packages/dashboard/ui/.next/server/app/metrics/page_client-reference-manifest.js +1 -0
  377. package/packages/dashboard/ui/.next/server/app/metrics.html +1 -0
  378. package/packages/dashboard/ui/.next/server/app/metrics.meta +5 -0
  379. package/packages/dashboard/ui/.next/server/app/metrics.rsc +7 -0
  380. package/packages/dashboard/ui/.next/server/app/page.js +1 -0
  381. package/packages/dashboard/ui/.next/server/app/page.js.nft.json +1 -0
  382. package/packages/dashboard/ui/.next/server/app/page_client-reference-manifest.js +1 -0
  383. package/packages/dashboard/ui/.next/server/app/pricing/page.js +5 -0
  384. package/packages/dashboard/ui/.next/server/app/pricing/page.js.nft.json +1 -0
  385. package/packages/dashboard/ui/.next/server/app/pricing/page_client-reference-manifest.js +1 -0
  386. package/packages/dashboard/ui/.next/server/app/pricing.html +13 -0
  387. package/packages/dashboard/ui/.next/server/app/pricing.meta +5 -0
  388. package/packages/dashboard/ui/.next/server/app/pricing.rsc +7 -0
  389. package/packages/dashboard/ui/.next/server/app/providers/page.js +2 -0
  390. package/packages/dashboard/ui/.next/server/app/providers/page.js.nft.json +1 -0
  391. package/packages/dashboard/ui/.next/server/app/providers/page_client-reference-manifest.js +1 -0
  392. package/packages/dashboard/ui/.next/server/app/providers/setup/[provider]/page.js +1 -0
  393. package/packages/dashboard/ui/.next/server/app/providers/setup/[provider]/page.js.nft.json +1 -0
  394. package/packages/dashboard/ui/.next/server/app/providers/setup/[provider]/page_client-reference-manifest.js +1 -0
  395. package/packages/dashboard/ui/.next/server/app/providers/setup/claude.html +1 -0
  396. package/packages/dashboard/ui/.next/server/app/providers/setup/claude.meta +5 -0
  397. package/packages/dashboard/ui/.next/server/app/providers/setup/claude.rsc +8 -0
  398. package/packages/dashboard/ui/.next/server/app/providers/setup/codex.html +1 -0
  399. package/packages/dashboard/ui/.next/server/app/providers/setup/codex.meta +5 -0
  400. package/packages/dashboard/ui/.next/server/app/providers/setup/codex.rsc +8 -0
  401. package/packages/dashboard/ui/.next/server/app/providers/setup/cursor.html +1 -0
  402. package/packages/dashboard/ui/.next/server/app/providers/setup/cursor.meta +5 -0
  403. package/packages/dashboard/ui/.next/server/app/providers/setup/cursor.rsc +8 -0
  404. package/packages/dashboard/ui/.next/server/app/providers.html +1 -0
  405. package/packages/dashboard/ui/.next/server/app/providers.meta +5 -0
  406. package/packages/dashboard/ui/.next/server/app/providers.rsc +7 -0
  407. package/packages/dashboard/ui/.next/server/app/signup/page.js +6 -0
  408. package/packages/dashboard/ui/.next/server/app/signup/page.js.nft.json +1 -0
  409. package/packages/dashboard/ui/.next/server/app/signup/page_client-reference-manifest.js +1 -0
  410. package/packages/dashboard/ui/.next/server/app/signup.html +6 -0
  411. package/packages/dashboard/ui/.next/server/app/signup.meta +5 -0
  412. package/packages/dashboard/ui/.next/server/app/signup.rsc +7 -0
  413. package/packages/dashboard/ui/.next/server/app-paths-manifest.json +16 -0
  414. package/packages/dashboard/ui/.next/server/chunks/190.js +1 -0
  415. package/packages/dashboard/ui/.next/server/chunks/205.js +1 -0
  416. package/packages/dashboard/ui/.next/server/chunks/251.js +9 -0
  417. package/packages/dashboard/ui/.next/server/chunks/288.js +1 -0
  418. package/packages/dashboard/ui/.next/server/chunks/434.js +1 -0
  419. package/packages/dashboard/ui/.next/server/chunks/471.js +2 -0
  420. package/packages/dashboard/ui/.next/server/chunks/621.js +1 -0
  421. package/packages/dashboard/ui/.next/server/chunks/680.js +5 -0
  422. package/packages/dashboard/ui/.next/server/chunks/682.js +6 -0
  423. package/packages/dashboard/ui/.next/server/chunks/684.js +1 -0
  424. package/packages/dashboard/ui/.next/server/chunks/71.js +196 -0
  425. package/packages/dashboard/ui/.next/server/chunks/711.js +1 -0
  426. package/packages/dashboard/ui/.next/server/chunks/90.js +17 -0
  427. package/packages/dashboard/ui/.next/server/chunks/948.js +2 -0
  428. package/packages/dashboard/ui/.next/server/chunks/font-manifest.json +1 -0
  429. package/packages/dashboard/ui/.next/server/font-manifest.json +1 -0
  430. package/packages/dashboard/ui/.next/server/functions-config-manifest.json +1 -0
  431. package/packages/dashboard/ui/.next/server/interception-route-rewrite-manifest.js +1 -0
  432. package/packages/dashboard/ui/.next/server/middleware-build-manifest.js +1 -0
  433. package/packages/dashboard/ui/.next/server/middleware-manifest.json +6 -0
  434. package/packages/dashboard/ui/.next/server/middleware-react-loadable-manifest.js +1 -0
  435. package/packages/dashboard/ui/.next/server/next-font-manifest.js +1 -0
  436. package/packages/dashboard/ui/.next/server/next-font-manifest.json +1 -0
  437. package/packages/dashboard/ui/.next/server/pages/404.html +1 -0
  438. package/packages/dashboard/ui/.next/server/pages/500.html +1 -0
  439. package/packages/dashboard/ui/.next/server/pages/_app.js +1 -0
  440. package/packages/dashboard/ui/.next/server/pages/_app.js.nft.json +1 -0
  441. package/packages/dashboard/ui/.next/server/pages/_document.js +1 -0
  442. package/packages/dashboard/ui/.next/server/pages/_document.js.nft.json +1 -0
  443. package/packages/dashboard/ui/.next/server/pages/_error.js +1 -0
  444. package/packages/dashboard/ui/.next/server/pages/_error.js.nft.json +1 -0
  445. package/packages/dashboard/ui/.next/server/pages-manifest.json +1 -0
  446. package/packages/dashboard/ui/.next/server/server-reference-manifest.js +1 -0
  447. package/packages/dashboard/ui/.next/server/server-reference-manifest.json +1 -0
  448. package/packages/dashboard/ui/.next/server/webpack-runtime.js +1 -0
  449. package/packages/dashboard/ui/.next/static/HR7W9z1PPVPFqUboUVZFZ/_buildManifest.js +1 -0
  450. package/packages/dashboard/ui/.next/static/HR7W9z1PPVPFqUboUVZFZ/_ssgManifest.js +1 -0
  451. package/packages/dashboard/ui/.next/static/chunks/116-a883fca163f3a5bc.js +1 -0
  452. package/packages/dashboard/ui/.next/static/chunks/117-c8afed19e821a35d.js +2 -0
  453. package/packages/dashboard/ui/.next/static/chunks/282-980c2eb8fff20123.js +1 -0
  454. package/packages/dashboard/ui/.next/static/chunks/532-bace199897eeab37.js +9 -0
  455. package/packages/dashboard/ui/.next/static/chunks/631-af51bad94027527a.js +1 -0
  456. package/packages/dashboard/ui/.next/static/chunks/648-acb2ff9f77cbfbd3.js +1 -0
  457. package/packages/dashboard/ui/.next/static/chunks/677-30e60cb0b47875b6.js +1 -0
  458. package/packages/dashboard/ui/.next/static/chunks/766-2aea80818f7eb0d8.js +1 -0
  459. package/packages/dashboard/ui/.next/static/chunks/83-4f08122d4e7e79a6.js +1 -0
  460. package/packages/dashboard/ui/.next/static/chunks/847-f1f467060f32afff.js +1 -0
  461. package/packages/dashboard/ui/.next/static/chunks/891-5cb1513eeb97a891.js +1 -0
  462. package/packages/dashboard/ui/.next/static/chunks/app/_not-found/page-60501fddbafba9dc.js +1 -0
  463. package/packages/dashboard/ui/.next/static/chunks/app/app/onboarding/page-9914652442f7e4fb.js +1 -0
  464. package/packages/dashboard/ui/.next/static/chunks/app/app/page-44813aa26ad19681.js +1 -0
  465. package/packages/dashboard/ui/.next/static/chunks/app/cloud/link/page-fa1d5842aa90e8a6.js +1 -0
  466. package/packages/dashboard/ui/.next/static/chunks/app/connect-repos/page-113060009ef35bc2.js +1 -0
  467. package/packages/dashboard/ui/.next/static/chunks/app/history/page-9965d2483011b846.js +1 -0
  468. package/packages/dashboard/ui/.next/static/chunks/app/layout-6b91e33784c20610.js +1 -0
  469. package/packages/dashboard/ui/.next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +1 -0
  470. package/packages/dashboard/ui/.next/static/chunks/app/metrics/page-1e37ef8e73940b40.js +1 -0
  471. package/packages/dashboard/ui/.next/static/chunks/app/page-7993778218818ace.js +1 -0
  472. package/packages/dashboard/ui/.next/static/chunks/app/pricing/page-9db3ebdfa567a7c9.js +1 -0
  473. package/packages/dashboard/ui/.next/static/chunks/app/providers/page-bcf46064ac4474ce.js +1 -0
  474. package/packages/dashboard/ui/.next/static/chunks/app/providers/setup/[provider]/page-4dbe33f0f7691b7c.js +1 -0
  475. package/packages/dashboard/ui/.next/static/chunks/app/signup/page-1ede2205b58649ca.js +1 -0
  476. package/packages/dashboard/ui/.next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
  477. package/packages/dashboard/ui/.next/static/chunks/fd9d1056-609918ca7b6280bb.js +1 -0
  478. package/packages/dashboard/ui/.next/static/chunks/framework-f66176bb897dc684.js +1 -0
  479. package/packages/dashboard/ui/.next/static/chunks/main-5a40a5ae29646e1b.js +1 -0
  480. package/packages/dashboard/ui/.next/static/chunks/main-app-fdbeb09028f57c9f.js +1 -0
  481. package/packages/dashboard/ui/.next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  482. package/packages/dashboard/ui/.next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  483. package/packages/dashboard/ui/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  484. package/packages/dashboard/ui/.next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
  485. package/packages/dashboard/ui/.next/static/css/4034f236dd1a3178.css +1 -0
  486. package/packages/dashboard/ui/.next/static/css/99c2552394077586.css +1 -0
  487. package/packages/dashboard/ui/.next/trace +63 -0
  488. package/packages/dashboard/ui/.next/types/app/app/onboarding/page.ts +79 -0
  489. package/packages/dashboard/ui/.next/types/app/app/page.ts +79 -0
  490. package/packages/dashboard/ui/.next/types/app/cloud/link/page.ts +79 -0
  491. package/packages/dashboard/ui/.next/types/app/connect-repos/page.ts +79 -0
  492. package/packages/dashboard/ui/.next/types/app/history/page.ts +79 -0
  493. package/packages/dashboard/ui/.next/types/app/layout.ts +79 -0
  494. package/packages/dashboard/ui/.next/types/app/login/page.ts +79 -0
  495. package/packages/dashboard/ui/.next/types/app/metrics/page.ts +79 -0
  496. package/packages/dashboard/ui/.next/types/app/page.ts +79 -0
  497. package/packages/dashboard/ui/.next/types/app/pricing/page.ts +79 -0
  498. package/packages/dashboard/ui/.next/types/app/providers/page.ts +79 -0
  499. package/packages/dashboard/ui/.next/types/app/providers/setup/[provider]/page.ts +79 -0
  500. package/packages/dashboard/ui/.next/types/app/signup/page.ts +79 -0
  501. package/packages/dashboard/ui/.next/types/package.json +1 -0
  502. package/packages/dashboard/ui/app/app/onboarding/page.tsx +394 -0
  503. package/packages/dashboard/ui/app/app/page.tsx +667 -0
  504. package/packages/dashboard/ui/app/apple-icon.png +0 -0
  505. package/packages/dashboard/ui/app/cloud/link/page.tsx +464 -0
  506. package/packages/dashboard/ui/app/connect-repos/page.tsx +410 -0
  507. package/packages/dashboard/ui/app/favicon.png +0 -0
  508. package/packages/dashboard/ui/app/globals.css +59 -0
  509. package/packages/dashboard/ui/app/history/page.tsx +658 -0
  510. package/packages/dashboard/ui/app/layout.tsx +25 -0
  511. package/packages/dashboard/ui/app/login/page.tsx +280 -0
  512. package/packages/dashboard/ui/app/metrics/page.tsx +751 -0
  513. package/packages/dashboard/ui/app/page.tsx +59 -0
  514. package/packages/dashboard/ui/app/pricing/page.tsx +7 -0
  515. package/packages/dashboard/ui/app/providers/page.tsx +193 -0
  516. package/packages/dashboard/ui/app/providers/setup/[provider]/ProviderSetupClient.tsx +148 -0
  517. package/packages/dashboard/ui/app/providers/setup/[provider]/constants.ts +35 -0
  518. package/packages/dashboard/ui/app/providers/setup/[provider]/page.tsx +42 -0
  519. package/packages/dashboard/ui/app/signup/page.tsx +343 -0
  520. package/packages/dashboard/ui/index.ts +49 -0
  521. package/packages/dashboard/ui/landing/LandingPage.tsx +713 -0
  522. package/packages/dashboard/ui/landing/PricingPage.tsx +559 -0
  523. package/packages/dashboard/ui/landing/index.ts +6 -0
  524. package/packages/dashboard/ui/landing/styles.css +2850 -0
  525. package/packages/dashboard/ui/lib/agent-merge.ts +35 -0
  526. package/packages/dashboard/ui/lib/api.ts +1155 -0
  527. package/packages/dashboard/ui/lib/cloudApi.ts +876 -0
  528. package/packages/dashboard/ui/lib/colors.ts +218 -0
  529. package/packages/dashboard/ui/lib/hierarchy.ts +242 -0
  530. package/packages/dashboard/ui/lib/stuckDetection.ts +142 -0
  531. package/packages/dashboard/ui/next-env.d.ts +5 -0
  532. package/packages/dashboard/ui/next.config.js +41 -0
  533. package/packages/dashboard/ui/package-lock.json +2882 -0
  534. package/packages/dashboard/ui/package.json +33 -0
  535. package/packages/dashboard/ui/postcss.config.js +5 -0
  536. package/packages/dashboard/ui/react-components/ActivityFeed.tsx +216 -0
  537. package/packages/dashboard/ui/react-components/AddWorkspaceModal.tsx +170 -0
  538. package/packages/dashboard/ui/react-components/AgentCard.tsx +587 -0
  539. package/packages/dashboard/ui/react-components/AgentList.tsx +411 -0
  540. package/packages/dashboard/ui/react-components/AgentProfilePanel.tsx +564 -0
  541. package/packages/dashboard/ui/react-components/App.tsx +3447 -0
  542. package/packages/dashboard/ui/react-components/BillingPanel.tsx +922 -0
  543. package/packages/dashboard/ui/react-components/BillingResult.tsx +447 -0
  544. package/packages/dashboard/ui/react-components/BroadcastComposer.tsx +690 -0
  545. package/packages/dashboard/ui/react-components/ChannelAdminPanel.tsx +773 -0
  546. package/packages/dashboard/ui/react-components/ChannelBrowser.tsx +385 -0
  547. package/packages/dashboard/ui/react-components/ChannelChat.tsx +307 -0
  548. package/packages/dashboard/ui/react-components/ChannelSidebar.tsx +399 -0
  549. package/packages/dashboard/ui/react-components/CloudSessionProvider.tsx +130 -0
  550. package/packages/dashboard/ui/react-components/CommandPalette.tsx +815 -0
  551. package/packages/dashboard/ui/react-components/ConfirmationDialog.tsx +133 -0
  552. package/packages/dashboard/ui/react-components/ConversationHistory.tsx +518 -0
  553. package/packages/dashboard/ui/react-components/CoordinatorPanel.tsx +944 -0
  554. package/packages/dashboard/ui/react-components/DecisionQueue.tsx +717 -0
  555. package/packages/dashboard/ui/react-components/DirectMessageView.tsx +164 -0
  556. package/packages/dashboard/ui/react-components/FileAutocomplete.tsx +368 -0
  557. package/packages/dashboard/ui/react-components/FleetOverview.tsx +278 -0
  558. package/packages/dashboard/ui/react-components/LogViewer.tsx +310 -0
  559. package/packages/dashboard/ui/react-components/LogViewerPanel.tsx +482 -0
  560. package/packages/dashboard/ui/react-components/Logo.tsx +284 -0
  561. package/packages/dashboard/ui/react-components/MentionAutocomplete.tsx +384 -0
  562. package/packages/dashboard/ui/react-components/MessageList.tsx +649 -0
  563. package/packages/dashboard/ui/react-components/MessageSenderName.tsx +91 -0
  564. package/packages/dashboard/ui/react-components/MessageStatusIndicator.tsx +142 -0
  565. package/packages/dashboard/ui/react-components/NewConversationModal.tsx +400 -0
  566. package/packages/dashboard/ui/react-components/NotificationToast.tsx +488 -0
  567. package/packages/dashboard/ui/react-components/OnlineUsersIndicator.tsx +164 -0
  568. package/packages/dashboard/ui/react-components/Pagination.tsx +124 -0
  569. package/packages/dashboard/ui/react-components/PricingPlans.tsx +386 -0
  570. package/packages/dashboard/ui/react-components/ProjectList.tsx +625 -0
  571. package/packages/dashboard/ui/react-components/ProviderAuthFlow.tsx +843 -0
  572. package/packages/dashboard/ui/react-components/ProviderConnectionList.tsx +363 -0
  573. package/packages/dashboard/ui/react-components/ProvisioningProgress.tsx +730 -0
  574. package/packages/dashboard/ui/react-components/RepoAccessPanel.tsx +392 -0
  575. package/packages/dashboard/ui/react-components/ServerCard.tsx +202 -0
  576. package/packages/dashboard/ui/react-components/SessionExpiredModal.tsx +128 -0
  577. package/packages/dashboard/ui/react-components/SpawnModal.tsx +704 -0
  578. package/packages/dashboard/ui/react-components/TaskAssignmentUI.tsx +375 -0
  579. package/packages/dashboard/ui/react-components/TerminalProviderSetup.tsx +608 -0
  580. package/packages/dashboard/ui/react-components/ThemeProvider.tsx +325 -0
  581. package/packages/dashboard/ui/react-components/ThinkingIndicator.tsx +231 -0
  582. package/packages/dashboard/ui/react-components/ThreadList.tsx +198 -0
  583. package/packages/dashboard/ui/react-components/ThreadPanel.tsx +346 -0
  584. package/packages/dashboard/ui/react-components/TrajectoryViewer.tsx +698 -0
  585. package/packages/dashboard/ui/react-components/TypingIndicator.tsx +69 -0
  586. package/packages/dashboard/ui/react-components/UsageBanner.tsx +231 -0
  587. package/packages/dashboard/ui/react-components/UserProfilePanel.tsx +233 -0
  588. package/packages/dashboard/ui/react-components/WorkspaceContext.tsx +107 -0
  589. package/packages/dashboard/ui/react-components/WorkspaceSelector.tsx +234 -0
  590. package/packages/dashboard/ui/react-components/WorkspaceStatusIndicator.tsx +370 -0
  591. package/packages/dashboard/ui/react-components/XTermInteractive.tsx +510 -0
  592. package/packages/dashboard/ui/react-components/XTermLogViewer.tsx +719 -0
  593. package/packages/dashboard/ui/react-components/channels/ChannelDialogs.tsx +1411 -0
  594. package/packages/dashboard/ui/react-components/channels/ChannelHeader.tsx +317 -0
  595. package/packages/dashboard/ui/react-components/channels/ChannelMessageList.tsx +463 -0
  596. package/packages/dashboard/ui/react-components/channels/ChannelViewV1.tsx +146 -0
  597. package/packages/dashboard/ui/react-components/channels/MessageInput.tsx +288 -0
  598. package/packages/dashboard/ui/react-components/channels/SearchInput.tsx +172 -0
  599. package/packages/dashboard/ui/react-components/channels/SearchResults.tsx +336 -0
  600. package/packages/dashboard/ui/react-components/channels/api.ts +697 -0
  601. package/packages/dashboard/ui/react-components/channels/index.ts +76 -0
  602. package/packages/dashboard/ui/react-components/channels/mockApi.ts +344 -0
  603. package/packages/dashboard/ui/react-components/channels/types.ts +566 -0
  604. package/packages/dashboard/ui/react-components/hooks/index.ts +57 -0
  605. package/packages/dashboard/ui/react-components/hooks/useAgentLogs.ts +394 -0
  606. package/packages/dashboard/ui/react-components/hooks/useAgents.ts +127 -0
  607. package/packages/dashboard/ui/react-components/hooks/useBroadcastDedup.ts +86 -0
  608. package/packages/dashboard/ui/react-components/hooks/useChannelAdmin.ts +329 -0
  609. package/packages/dashboard/ui/react-components/hooks/useChannelBrowser.ts +239 -0
  610. package/packages/dashboard/ui/react-components/hooks/useChannelCommands.ts +138 -0
  611. package/packages/dashboard/ui/react-components/hooks/useChannels.ts +328 -0
  612. package/packages/dashboard/ui/react-components/hooks/useDebounce.ts +29 -0
  613. package/packages/dashboard/ui/react-components/hooks/useDirectMessage.ts +141 -0
  614. package/packages/dashboard/ui/react-components/hooks/useMessages.ts +309 -0
  615. package/packages/dashboard/ui/react-components/hooks/useOrchestrator.ts +364 -0
  616. package/packages/dashboard/ui/react-components/hooks/usePinnedAgents.ts +140 -0
  617. package/packages/dashboard/ui/react-components/hooks/usePresence.ts +340 -0
  618. package/packages/dashboard/ui/react-components/hooks/useRecentRepos.ts +130 -0
  619. package/packages/dashboard/ui/react-components/hooks/useSession.ts +209 -0
  620. package/packages/dashboard/ui/react-components/hooks/useTrajectory.ts +265 -0
  621. package/packages/dashboard/ui/react-components/hooks/useWebSocket.ts +169 -0
  622. package/packages/dashboard/ui/react-components/hooks/useWorkspaceMembers.ts +120 -0
  623. package/packages/dashboard/ui/react-components/hooks/useWorkspaceRepos.ts +73 -0
  624. package/packages/dashboard/ui/react-components/hooks/useWorkspaceStatus.ts +237 -0
  625. package/packages/dashboard/ui/react-components/index.ts +81 -0
  626. package/packages/dashboard/ui/react-components/layout/Header.tsx +355 -0
  627. package/packages/dashboard/ui/react-components/layout/RepoContextHeader.tsx +361 -0
  628. package/packages/dashboard/ui/react-components/layout/Sidebar.archive.test.tsx +126 -0
  629. package/packages/dashboard/ui/react-components/layout/Sidebar.test.tsx +691 -0
  630. package/packages/dashboard/ui/react-components/layout/Sidebar.tsx +930 -0
  631. package/packages/dashboard/ui/react-components/layout/index.ts +7 -0
  632. package/packages/dashboard/ui/react-components/settings/BillingSettingsPanel.tsx +564 -0
  633. package/packages/dashboard/ui/react-components/settings/SettingsPage.tsx +544 -0
  634. package/packages/dashboard/ui/react-components/settings/TeamSettingsPanel.tsx +560 -0
  635. package/packages/dashboard/ui/react-components/settings/WorkspaceSettingsPanel.tsx +1329 -0
  636. package/packages/dashboard/ui/react-components/settings/index.ts +11 -0
  637. package/packages/dashboard/ui/react-components/settings/types.ts +53 -0
  638. package/packages/dashboard/ui/react-components/utils/messageFormatting.tsx +370 -0
  639. package/packages/dashboard/ui/tailwind.config.js +148 -0
  640. package/packages/dashboard/ui/types/index.ts +304 -0
  641. package/packages/dashboard/ui/types/threading.ts +7 -0
  642. package/packages/dashboard/ui-dist/404.html +1 -0
  643. package/packages/dashboard/ui-dist/_next/static/HR7W9z1PPVPFqUboUVZFZ/_buildManifest.js +1 -0
  644. package/packages/dashboard/ui-dist/_next/static/HR7W9z1PPVPFqUboUVZFZ/_ssgManifest.js +1 -0
  645. package/packages/dashboard/ui-dist/_next/static/ZCFjHbkF8yDKS2md3lVgb/_buildManifest.js +1 -0
  646. package/packages/dashboard/ui-dist/_next/static/ZCFjHbkF8yDKS2md3lVgb/_ssgManifest.js +1 -0
  647. package/packages/dashboard/ui-dist/_next/static/chunks/116-a883fca163f3a5bc.js +1 -0
  648. package/packages/dashboard/ui-dist/_next/static/chunks/117-c8afed19e821a35d.js +2 -0
  649. package/packages/dashboard/ui-dist/_next/static/chunks/282-980c2eb8fff20123.js +1 -0
  650. package/packages/dashboard/ui-dist/_next/static/chunks/320-900169c942e31422.js +1 -0
  651. package/packages/dashboard/ui-dist/_next/static/chunks/532-bace199897eeab37.js +9 -0
  652. package/packages/dashboard/ui-dist/_next/static/chunks/631-af51bad94027527a.js +1 -0
  653. package/packages/dashboard/ui-dist/_next/static/chunks/648-acb2ff9f77cbfbd3.js +1 -0
  654. package/packages/dashboard/ui-dist/_next/static/chunks/677-30e60cb0b47875b6.js +1 -0
  655. package/packages/dashboard/ui-dist/_next/static/chunks/766-2aea80818f7eb0d8.js +1 -0
  656. package/packages/dashboard/ui-dist/_next/static/chunks/83-4f08122d4e7e79a6.js +1 -0
  657. package/packages/dashboard/ui-dist/_next/static/chunks/847-f1f467060f32afff.js +1 -0
  658. package/packages/dashboard/ui-dist/_next/static/chunks/891-5cb1513eeb97a891.js +1 -0
  659. package/packages/dashboard/ui-dist/_next/static/chunks/app/_not-found/page-60501fddbafba9dc.js +1 -0
  660. package/packages/dashboard/ui-dist/_next/static/chunks/app/app/onboarding/page-9914652442f7e4fb.js +1 -0
  661. package/packages/dashboard/ui-dist/_next/static/chunks/app/app/onboarding/page-f746f29e01fffc43.js +1 -0
  662. package/packages/dashboard/ui-dist/_next/static/chunks/app/app/page-2e525b1dcc790967.js +1 -0
  663. package/packages/dashboard/ui-dist/_next/static/chunks/app/app/page-44813aa26ad19681.js +1 -0
  664. package/packages/dashboard/ui-dist/_next/static/chunks/app/cloud/link/page-5011ae044b90449d.js +1 -0
  665. package/packages/dashboard/ui-dist/_next/static/chunks/app/cloud/link/page-fa1d5842aa90e8a6.js +1 -0
  666. package/packages/dashboard/ui-dist/_next/static/chunks/app/connect-repos/page-03ac6f35a6654ea6.js +1 -0
  667. package/packages/dashboard/ui-dist/_next/static/chunks/app/connect-repos/page-113060009ef35bc2.js +1 -0
  668. package/packages/dashboard/ui-dist/_next/static/chunks/app/history/page-9965d2483011b846.js +1 -0
  669. package/packages/dashboard/ui-dist/_next/static/chunks/app/history/page-b2ce7c96ed0931da.js +1 -0
  670. package/packages/dashboard/ui-dist/_next/static/chunks/app/layout-6b91e33784c20610.js +1 -0
  671. package/packages/dashboard/ui-dist/_next/static/chunks/app/layout-c0d118c0f92d969c.js +1 -0
  672. package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-6ec54eee75877971.js +1 -0
  673. package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +1 -0
  674. package/packages/dashboard/ui-dist/_next/static/chunks/app/metrics/page-1e37ef8e73940b40.js +1 -0
  675. package/packages/dashboard/ui-dist/_next/static/chunks/app/metrics/page-bf2cb1e5915bc92d.js +1 -0
  676. package/packages/dashboard/ui-dist/_next/static/chunks/app/page-4e64923d73c35bc9.js +1 -0
  677. package/packages/dashboard/ui-dist/_next/static/chunks/app/page-7993778218818ace.js +1 -0
  678. package/packages/dashboard/ui-dist/_next/static/chunks/app/pricing/page-0efa024c28ba4597.js +1 -0
  679. package/packages/dashboard/ui-dist/_next/static/chunks/app/pricing/page-9db3ebdfa567a7c9.js +1 -0
  680. package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/page-bcf46064ac4474ce.js +1 -0
  681. package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/page-e65a0010da6ea5be.js +1 -0
  682. package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/setup/[provider]/page-4dbe33f0f7691b7c.js +1 -0
  683. package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/setup/[provider]/page-84161c802b020a1f.js +1 -0
  684. package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-18a4665665f6be11.js +1 -0
  685. package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +1 -0
  686. package/packages/dashboard/ui-dist/_next/static/chunks/e868780c-48e5f147c90a3a41.js +18 -0
  687. package/packages/dashboard/ui-dist/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +1 -0
  688. package/packages/dashboard/ui-dist/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
  689. package/packages/dashboard/ui-dist/_next/static/chunks/main-5a40a5ae29646e1b.js +1 -0
  690. package/packages/dashboard/ui-dist/_next/static/chunks/main-app-6e8e8d3ef4e0192a.js +1 -0
  691. package/packages/dashboard/ui-dist/_next/static/chunks/main-app-fdbeb09028f57c9f.js +1 -0
  692. package/packages/dashboard/ui-dist/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
  693. package/packages/dashboard/ui-dist/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
  694. package/packages/dashboard/ui-dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  695. package/packages/dashboard/ui-dist/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +1 -0
  696. package/packages/dashboard/ui-dist/_next/static/css/4034f236dd1a3178.css +1 -0
  697. package/packages/dashboard/ui-dist/_next/static/css/99c2552394077586.css +1 -0
  698. package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-128.png +0 -0
  699. package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-256.png +0 -0
  700. package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-32.png +0 -0
  701. package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-512.png +0 -0
  702. package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-64.png +0 -0
  703. package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo.svg +45 -0
  704. package/packages/dashboard/ui-dist/alt-logos/logo.svg +38 -0
  705. package/packages/dashboard/ui-dist/alt-logos/monogram-logo-128.png +0 -0
  706. package/packages/dashboard/ui-dist/alt-logos/monogram-logo-256.png +0 -0
  707. package/packages/dashboard/ui-dist/alt-logos/monogram-logo-32.png +0 -0
  708. package/packages/dashboard/ui-dist/alt-logos/monogram-logo-512.png +0 -0
  709. package/packages/dashboard/ui-dist/alt-logos/monogram-logo-64.png +0 -0
  710. package/packages/dashboard/ui-dist/alt-logos/monogram-logo.svg +38 -0
  711. package/packages/dashboard/ui-dist/app/onboarding.html +1 -0
  712. package/packages/dashboard/ui-dist/app/onboarding.txt +7 -0
  713. package/packages/dashboard/ui-dist/app.html +1 -0
  714. package/packages/dashboard/ui-dist/app.txt +7 -0
  715. package/packages/dashboard/ui-dist/apple-icon.png +0 -0
  716. package/packages/dashboard/ui-dist/cloud/link.html +1 -0
  717. package/packages/dashboard/ui-dist/cloud/link.txt +7 -0
  718. package/packages/dashboard/ui-dist/connect-repos.html +1 -0
  719. package/packages/dashboard/ui-dist/connect-repos.txt +7 -0
  720. package/packages/dashboard/ui-dist/history.html +1 -0
  721. package/packages/dashboard/ui-dist/history.txt +7 -0
  722. package/packages/dashboard/ui-dist/index.html +1 -0
  723. package/packages/dashboard/ui-dist/index.txt +7 -0
  724. package/packages/dashboard/ui-dist/login.html +5 -0
  725. package/packages/dashboard/ui-dist/login.txt +7 -0
  726. package/packages/dashboard/ui-dist/metrics.html +1 -0
  727. package/packages/dashboard/ui-dist/metrics.txt +7 -0
  728. package/packages/dashboard/ui-dist/pricing.html +13 -0
  729. package/packages/dashboard/ui-dist/pricing.txt +7 -0
  730. package/packages/dashboard/ui-dist/providers/setup/claude.html +1 -0
  731. package/packages/dashboard/ui-dist/providers/setup/claude.txt +8 -0
  732. package/packages/dashboard/ui-dist/providers/setup/codex.html +1 -0
  733. package/packages/dashboard/ui-dist/providers/setup/codex.txt +8 -0
  734. package/packages/dashboard/ui-dist/providers/setup/cursor.html +1 -0
  735. package/packages/dashboard/ui-dist/providers/setup/cursor.txt +8 -0
  736. package/packages/dashboard/ui-dist/providers.html +1 -0
  737. package/packages/dashboard/ui-dist/providers.txt +7 -0
  738. package/packages/dashboard/ui-dist/signup.html +6 -0
  739. package/packages/dashboard/ui-dist/signup.txt +7 -0
  740. package/packages/dashboard-server/dist/health-worker-manager.d.ts +62 -0
  741. package/packages/dashboard-server/dist/health-worker-manager.js +144 -0
  742. package/packages/dashboard-server/dist/health-worker.d.ts +9 -0
  743. package/packages/dashboard-server/dist/health-worker.js +79 -0
  744. package/packages/dashboard-server/dist/index.d.ts +18 -0
  745. package/packages/dashboard-server/dist/index.js +17 -0
  746. package/packages/dashboard-server/dist/server.js +5099 -0
  747. package/packages/dashboard-server/dist/start.js +13 -0
  748. package/packages/dashboard-server/dist/types/threading.d.ts +8 -0
  749. package/packages/dashboard-server/dist/types/threading.js +2 -0
  750. package/packages/dashboard-server/dist/user-bridge.d.ts +154 -0
  751. package/packages/dashboard-server/dist/user-bridge.js +372 -0
  752. package/packages/dashboard-server/package.json +49 -0
  753. package/packages/hooks/dist/browser.d.ts +2 -0
  754. package/packages/hooks/dist/browser.js +3 -0
  755. package/packages/hooks/dist/index.d.ts +11 -0
  756. package/packages/hooks/dist/index.js +11 -0
  757. package/packages/hooks/dist/registry.js +476 -0
  758. package/packages/hooks/dist/trajectory-hooks.js +183 -0
  759. package/packages/hooks/dist/types.d.ts +285 -0
  760. package/packages/hooks/dist/types.js +10 -0
  761. package/packages/hooks/package.json +52 -0
  762. package/packages/mcp/LICENSE +190 -0
  763. package/packages/mcp/README.md +214 -0
  764. package/packages/mcp/SPEC.md +1922 -0
  765. package/packages/mcp/STAFFING_PLAN.md +294 -0
  766. package/packages/mcp/dist/bin.d.ts +12 -0
  767. package/packages/mcp/dist/bin.js +127 -0
  768. package/packages/mcp/dist/client.d.ts +68 -0
  769. package/packages/mcp/dist/client.js +115 -0
  770. package/packages/mcp/dist/cloud.d.ts +108 -0
  771. package/packages/mcp/dist/cloud.js +279 -0
  772. package/packages/mcp/dist/errors.d.ts +27 -0
  773. package/packages/mcp/dist/errors.js +48 -0
  774. package/packages/mcp/dist/index.d.ts +10 -0
  775. package/packages/mcp/dist/index.js +16 -0
  776. package/packages/mcp/dist/install-cli.d.ts +35 -0
  777. package/packages/mcp/dist/install-cli.js +157 -0
  778. package/packages/mcp/dist/install.d.ts +101 -0
  779. package/packages/mcp/dist/install.js +398 -0
  780. package/packages/mcp/dist/prompts/index.d.ts +2 -0
  781. package/packages/mcp/dist/prompts/index.js +2 -0
  782. package/packages/mcp/dist/prompts/protocol.d.ts +11 -0
  783. package/packages/mcp/dist/prompts/protocol.js +168 -0
  784. package/packages/mcp/dist/resources/agents.d.ts +11 -0
  785. package/packages/mcp/dist/resources/agents.js +17 -0
  786. package/packages/mcp/dist/resources/inbox.d.ts +11 -0
  787. package/packages/mcp/dist/resources/inbox.js +17 -0
  788. package/packages/mcp/dist/resources/index.d.ts +4 -0
  789. package/packages/mcp/dist/resources/index.js +4 -0
  790. package/packages/mcp/dist/resources/project.d.ts +11 -0
  791. package/packages/mcp/dist/resources/project.js +21 -0
  792. package/packages/mcp/dist/server.d.ts +19 -0
  793. package/packages/mcp/dist/server.js +215 -0
  794. package/packages/mcp/dist/simple.d.ts +173 -0
  795. package/packages/mcp/dist/simple.js +120 -0
  796. package/packages/mcp/dist/tools/index.d.ts +10 -0
  797. package/packages/mcp/dist/tools/index.js +10 -0
  798. package/packages/mcp/dist/tools/relay-health.d.ts +23 -0
  799. package/packages/mcp/dist/tools/relay-health.js +138 -0
  800. package/packages/mcp/dist/tools/relay-inbox.d.ts +26 -0
  801. package/packages/mcp/dist/tools/relay-inbox.js +58 -0
  802. package/packages/mcp/dist/tools/relay-logs.d.ts +20 -0
  803. package/packages/mcp/dist/tools/relay-logs.js +88 -0
  804. package/packages/mcp/dist/tools/relay-metrics.d.ts +20 -0
  805. package/packages/mcp/dist/tools/relay-metrics.js +135 -0
  806. package/packages/mcp/dist/tools/relay-release.d.ts +20 -0
  807. package/packages/mcp/dist/tools/relay-release.js +44 -0
  808. package/packages/mcp/dist/tools/relay-send.d.ts +29 -0
  809. package/packages/mcp/dist/tools/relay-send.js +71 -0
  810. package/packages/mcp/dist/tools/relay-spawn.d.ts +36 -0
  811. package/packages/mcp/dist/tools/relay-spawn.js +73 -0
  812. package/packages/mcp/dist/tools/relay-status.d.ts +11 -0
  813. package/packages/mcp/dist/tools/relay-status.js +43 -0
  814. package/packages/mcp/dist/tools/relay-who.d.ts +20 -0
  815. package/packages/mcp/dist/tools/relay-who.js +47 -0
  816. package/packages/mcp/package.json +69 -0
  817. package/packages/memory/dist/memory-hooks.d.ts +60 -0
  818. package/packages/memory/package.json +35 -0
  819. package/packages/policy/dist/agent-policy.js +665 -0
  820. package/packages/policy/dist/index.d.ts +12 -0
  821. package/packages/policy/dist/index.js +12 -0
  822. package/packages/policy/package.json +35 -0
  823. package/packages/protocol/dist/channels.d.ts +137 -0
  824. package/packages/protocol/dist/channels.js +154 -0
  825. package/packages/protocol/dist/framing.d.ts +80 -0
  826. package/packages/protocol/dist/framing.js +206 -0
  827. package/packages/protocol/dist/index.d.ts +5 -0
  828. package/packages/protocol/dist/index.js +5 -0
  829. package/packages/protocol/dist/relay-pty-schemas.d.ts +258 -0
  830. package/packages/protocol/dist/types.d.ts +341 -0
  831. package/packages/protocol/dist/types.js +8 -0
  832. package/packages/protocol/package.json +56 -0
  833. package/packages/resiliency/dist/memory-monitor.js +599 -0
  834. package/packages/resiliency/dist/provider-context.d.ts +100 -0
  835. package/packages/resiliency/package.json +33 -0
  836. package/packages/sdk/README.md +171 -0
  837. package/packages/sdk/dist/client.d.ts +181 -0
  838. package/packages/sdk/dist/client.js +695 -0
  839. package/packages/sdk/dist/index.d.ts +32 -0
  840. package/packages/sdk/dist/index.js +36 -0
  841. package/packages/sdk/dist/protocol/framing.d.ts +80 -0
  842. package/packages/sdk/dist/protocol/framing.js +206 -0
  843. package/packages/sdk/dist/protocol/index.d.ts +6 -0
  844. package/packages/sdk/dist/protocol/index.js +6 -0
  845. package/packages/sdk/dist/protocol/types.d.ts +341 -0
  846. package/packages/sdk/dist/protocol/types.js +8 -0
  847. package/packages/sdk/dist/standalone.d.ts +87 -0
  848. package/packages/sdk/dist/standalone.js +126 -0
  849. package/packages/sdk/package.json +80 -0
  850. package/packages/spawner/API.md +256 -0
  851. package/packages/spawner/dist/index.d.ts +8 -0
  852. package/packages/spawner/dist/index.js +8 -0
  853. package/packages/spawner/dist/types.d.ts +552 -0
  854. package/packages/spawner/dist/types.js +193 -0
  855. package/packages/spawner/package.json +47 -0
  856. package/packages/state/dist/agent-state.js +120 -0
  857. package/packages/state/dist/index.d.ts +8 -0
  858. package/packages/state/dist/index.js +8 -0
  859. package/packages/state/package.json +32 -0
  860. package/packages/storage/dist/adapter.d.ts +156 -0
  861. package/packages/storage/dist/batched-sqlite-adapter.d.ts +75 -0
  862. package/packages/storage/dist/batched-sqlite-adapter.js +189 -0
  863. package/packages/storage/dist/index.d.ts +5 -0
  864. package/packages/storage/dist/index.js +6 -0
  865. package/packages/storage/dist/sqlite-adapter.d.ts +113 -0
  866. package/packages/storage/dist/sqlite-adapter.js +752 -0
  867. package/packages/storage/package.json +69 -0
  868. package/packages/trajectory/dist/index.d.ts +2 -0
  869. package/packages/trajectory/dist/index.js +2 -0
  870. package/packages/trajectory/dist/integration.js +987 -0
  871. package/packages/trajectory/package.json +35 -0
  872. package/packages/user-directory/dist/index.d.ts +7 -0
  873. package/packages/user-directory/dist/index.js +7 -0
  874. package/packages/user-directory/dist/user-directory.d.ts +121 -0
  875. package/packages/user-directory/dist/user-directory.js +267 -0
  876. package/packages/user-directory/package.json +35 -0
  877. package/packages/utils/dist/command-resolver.js +80 -0
  878. package/packages/utils/dist/error-tracking.d.ts +103 -0
  879. package/packages/utils/dist/error-tracking.js +149 -0
  880. package/packages/utils/dist/index.d.ts +9 -0
  881. package/packages/utils/dist/index.js +9 -0
  882. package/packages/utils/dist/model-mapping.d.ts +28 -0
  883. package/packages/utils/dist/model-mapping.js +55 -0
  884. package/packages/utils/package.json +75 -0
  885. package/packages/wrapper/dist/__fixtures__/claude-outputs.d.ts +49 -0
  886. package/packages/wrapper/dist/__fixtures__/claude-outputs.js +443 -0
  887. package/packages/wrapper/dist/__fixtures__/codex-outputs.d.ts +9 -0
  888. package/packages/wrapper/dist/__fixtures__/codex-outputs.js +94 -0
  889. package/packages/wrapper/dist/__fixtures__/gemini-outputs.d.ts +19 -0
  890. package/packages/wrapper/dist/__fixtures__/gemini-outputs.js +144 -0
  891. package/packages/wrapper/dist/__fixtures__/index.d.ts +68 -0
  892. package/packages/wrapper/dist/__fixtures__/index.js +44 -0
  893. package/packages/wrapper/dist/base-wrapper.d.ts +225 -0
  894. package/packages/wrapper/dist/base-wrapper.js +572 -0
  895. package/packages/wrapper/dist/client.d.ts +254 -0
  896. package/packages/wrapper/dist/client.js +801 -0
  897. package/packages/wrapper/dist/id-generator.d.ts +35 -0
  898. package/packages/wrapper/dist/id-generator.js +60 -0
  899. package/packages/wrapper/dist/idle-detector.d.ts +110 -0
  900. package/packages/wrapper/dist/idle-detector.js +304 -0
  901. package/packages/wrapper/dist/index.d.ts +37 -0
  902. package/packages/wrapper/dist/index.js +47 -0
  903. package/packages/wrapper/dist/parser.d.ts +236 -0
  904. package/packages/wrapper/dist/parser.js +1238 -0
  905. package/packages/wrapper/dist/relay-pty-orchestrator.d.ts +407 -0
  906. package/packages/wrapper/dist/relay-pty-orchestrator.js +1885 -0
  907. package/packages/wrapper/dist/shared.d.ts +201 -0
  908. package/packages/wrapper/dist/shared.js +341 -0
  909. package/packages/wrapper/dist/stuck-detector.d.ts +161 -0
  910. package/packages/wrapper/dist/stuck-detector.js +402 -0
  911. package/packages/wrapper/dist/tmux-wrapper.d.ts +345 -0
  912. package/packages/wrapper/dist/tmux-wrapper.js +1747 -0
  913. package/packages/wrapper/dist/trajectory-integration.d.ts +292 -0
  914. package/packages/wrapper/dist/trajectory-integration.js +979 -0
  915. package/packages/wrapper/dist/wrapper-types.d.ts +41 -0
  916. package/packages/wrapper/dist/wrapper-types.js +7 -0
  917. package/packages/wrapper/package.json +63 -0
  918. package/scripts/setup-stripe-products.ts +312 -0
  919. package/scripts/stress-test-orchestrator-integration.mts +1366 -0
  920. package/scripts/stress-test-orchestrator.mjs +584 -0
  921. package/scripts/stress-test-relay-pty.sh +452 -0
  922. package/scripts/verify-schema.js +1 -1
  923. package/turbo.json +37 -0
  924. package/dist/bridge/config.d.ts +0 -41
  925. package/dist/bridge/config.js +0 -143
  926. package/dist/bridge/index.d.ts +0 -10
  927. package/dist/bridge/index.js +0 -10
  928. package/dist/bridge/multi-project-client.d.ts +0 -99
  929. package/dist/bridge/multi-project-client.js +0 -389
  930. package/dist/bridge/shadow-cli.js +0 -75
  931. package/dist/bridge/shadow-config.d.ts +0 -87
  932. package/dist/bridge/spawner.d.ts +0 -186
  933. package/dist/bridge/spawner.js +0 -920
  934. package/dist/bridge/types.d.ts +0 -129
  935. package/dist/bridge/utils.d.ts +0 -30
  936. package/dist/bridge/utils.js +0 -54
  937. package/dist/cli/index.js +0 -2784
  938. package/dist/cloud/api/admin.js +0 -225
  939. package/dist/cloud/api/billing.js +0 -564
  940. package/dist/cloud/api/cli-pty-runner.d.ts +0 -54
  941. package/dist/cloud/api/cli-pty-runner.js +0 -119
  942. package/dist/cloud/api/codex-auth-helper.js +0 -327
  943. package/dist/cloud/api/consensus.js +0 -259
  944. package/dist/cloud/api/coordinators.js +0 -749
  945. package/dist/cloud/api/daemons.js +0 -535
  946. package/dist/cloud/api/generic-webhooks.js +0 -129
  947. package/dist/cloud/api/github-app.js +0 -223
  948. package/dist/cloud/api/monitoring.js +0 -578
  949. package/dist/cloud/api/nango-auth.js +0 -658
  950. package/dist/cloud/api/onboarding.d.ts +0 -15
  951. package/dist/cloud/api/onboarding.js +0 -666
  952. package/dist/cloud/api/policy.js +0 -229
  953. package/dist/cloud/api/provider-env.d.ts +0 -5
  954. package/dist/cloud/api/provider-env.js +0 -27
  955. package/dist/cloud/api/providers.js +0 -511
  956. package/dist/cloud/api/repos.js +0 -576
  957. package/dist/cloud/api/teams.js +0 -279
  958. package/dist/cloud/api/test-helpers.js +0 -745
  959. package/dist/cloud/api/workspaces.js +0 -1783
  960. package/dist/cloud/billing/plans.js +0 -245
  961. package/dist/cloud/config.d.ts +0 -75
  962. package/dist/cloud/config.js +0 -109
  963. package/dist/cloud/db/drizzle.d.ts +0 -246
  964. package/dist/cloud/db/drizzle.js +0 -1249
  965. package/dist/cloud/db/schema.d.ts +0 -4854
  966. package/dist/cloud/db/schema.js +0 -610
  967. package/dist/cloud/index.d.ts +0 -11
  968. package/dist/cloud/index.js +0 -38
  969. package/dist/cloud/provisioner/index.d.ts +0 -207
  970. package/dist/cloud/provisioner/index.js +0 -2069
  971. package/dist/cloud/server.js +0 -1599
  972. package/dist/cloud/services/index.d.ts +0 -17
  973. package/dist/cloud/services/index.js +0 -25
  974. package/dist/cloud/services/intro-expiration.d.ts +0 -55
  975. package/dist/cloud/services/intro-expiration.js +0 -211
  976. package/dist/cloud/services/nango.d.ts +0 -199
  977. package/dist/cloud/services/nango.js +0 -382
  978. package/dist/cloud/services/persistence.d.ts +0 -131
  979. package/dist/config/relay-config.d.ts +0 -23
  980. package/dist/config/relay-config.js +0 -23
  981. package/dist/continuity/index.d.ts +0 -45
  982. package/dist/continuity/index.js +0 -48
  983. package/dist/continuity/types.d.ts +0 -180
  984. package/dist/continuity/types.js +0 -9
  985. package/dist/daemon/agent-manager.d.ts +0 -134
  986. package/dist/daemon/agent-manager.js +0 -564
  987. package/dist/daemon/agent-registry.js +0 -213
  988. package/dist/daemon/api.d.ts +0 -83
  989. package/dist/daemon/api.js +0 -780
  990. package/dist/daemon/channel-membership-store.d.ts +0 -48
  991. package/dist/daemon/channel-membership-store.js +0 -149
  992. package/dist/daemon/cli-auth.d.ts +0 -82
  993. package/dist/daemon/cli-auth.js +0 -700
  994. package/dist/daemon/cloud-sync.d.ts +0 -150
  995. package/dist/daemon/cloud-sync.js +0 -424
  996. package/dist/daemon/connection.d.ts +0 -130
  997. package/dist/daemon/connection.js +0 -438
  998. package/dist/daemon/consensus-integration.js +0 -371
  999. package/dist/daemon/delivery-tracker.d.ts +0 -34
  1000. package/dist/daemon/delivery-tracker.js +0 -104
  1001. package/dist/daemon/enhanced-features.d.ts +0 -118
  1002. package/dist/daemon/enhanced-features.js +0 -178
  1003. package/dist/daemon/index.d.ts +0 -14
  1004. package/dist/daemon/index.js +0 -17
  1005. package/dist/daemon/orchestrator.d.ts +0 -157
  1006. package/dist/daemon/orchestrator.js +0 -792
  1007. package/dist/daemon/repo-manager.js +0 -384
  1008. package/dist/daemon/router.d.ts +0 -358
  1009. package/dist/daemon/router.js +0 -1333
  1010. package/dist/daemon/server.d.ts +0 -159
  1011. package/dist/daemon/server.js +0 -788
  1012. package/dist/daemon/services/browser-testing.d.ts +0 -88
  1013. package/dist/daemon/services/browser-testing.js +0 -244
  1014. package/dist/daemon/services/container-spawner.d.ts +0 -135
  1015. package/dist/daemon/services/container-spawner.js +0 -313
  1016. package/dist/daemon/sync-queue.d.ts +0 -116
  1017. package/dist/daemon/sync-queue.js +0 -361
  1018. package/dist/daemon/types.d.ts +0 -131
  1019. package/dist/daemon/user-directory.d.ts +0 -111
  1020. package/dist/daemon/user-directory.js +0 -233
  1021. package/dist/daemon/workspace-manager.js +0 -314
  1022. package/dist/dashboard/out/_next/static/chunks/116-eacf84a131b80db9.js +0 -1
  1023. package/dist/dashboard/out/_next/static/chunks/64-f4268c2ac6f4d7d4.js +0 -1
  1024. package/dist/dashboard/out/_next/static/chunks/766-aa7c8c9900ff5f53.js +0 -1
  1025. package/dist/dashboard/out/_next/static/chunks/891-a024fbe4b619cf6f.js +0 -1
  1026. package/dist/dashboard/out/_next/static/chunks/app/app/page-ffad986adfcc8b31.js +0 -1
  1027. package/dist/dashboard/out/_next/static/chunks/app/page-671037943b2f2e43.js +0 -1
  1028. package/dist/dashboard/out/_next/static/chunks/app/providers/page-57cbd738c6a73859.js +0 -1
  1029. package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-5ab0854472b402b0.js +0 -1
  1030. package/dist/dashboard/out/_next/static/css/8f9ed310f454e5a5.css +0 -1
  1031. package/dist/dashboard-server/server.js +0 -4806
  1032. package/dist/dashboard-server/start.js +0 -13
  1033. package/dist/dashboard-server/user-bridge.d.ts +0 -138
  1034. package/dist/dashboard-server/user-bridge.js +0 -348
  1035. package/dist/hooks/index.d.ts +0 -10
  1036. package/dist/hooks/index.js +0 -10
  1037. package/dist/hooks/registry.js +0 -476
  1038. package/dist/hooks/trajectory-hooks.js +0 -183
  1039. package/dist/hooks/types.d.ts +0 -284
  1040. package/dist/hooks/types.js +0 -8
  1041. package/dist/index.d.ts +0 -13
  1042. package/dist/index.js +0 -16
  1043. package/dist/memory/memory-hooks.d.ts +0 -60
  1044. package/dist/policy/agent-policy.js +0 -665
  1045. package/dist/protocol/channels.d.ts +0 -211
  1046. package/dist/protocol/channels.js +0 -154
  1047. package/dist/protocol/framing.d.ts +0 -94
  1048. package/dist/protocol/framing.js +0 -240
  1049. package/dist/protocol/index.d.ts +0 -4
  1050. package/dist/protocol/index.js +0 -4
  1051. package/dist/protocol/relay-pty-schemas.d.ts +0 -209
  1052. package/dist/protocol/types.d.ts +0 -168
  1053. package/dist/protocol/types.js +0 -6
  1054. package/dist/resiliency/memory-monitor.js +0 -593
  1055. package/dist/resiliency/provider-context.d.ts +0 -100
  1056. package/dist/shared/cli-auth-config.js +0 -320
  1057. package/dist/state/agent-state.js +0 -120
  1058. package/dist/storage/adapter.d.ts +0 -154
  1059. package/dist/storage/batched-sqlite-adapter.d.ts +0 -71
  1060. package/dist/storage/batched-sqlite-adapter.js +0 -183
  1061. package/dist/storage/sqlite-adapter.d.ts +0 -107
  1062. package/dist/storage/sqlite-adapter.js +0 -717
  1063. package/dist/trajectory/config.d.ts +0 -102
  1064. package/dist/trajectory/config.js +0 -185
  1065. package/dist/trajectory/index.d.ts +0 -8
  1066. package/dist/trajectory/index.js +0 -8
  1067. package/dist/trajectory/integration.js +0 -987
  1068. package/dist/utils/command-resolver.js +0 -76
  1069. package/dist/utils/index.d.ts +0 -4
  1070. package/dist/utils/index.js +0 -4
  1071. package/dist/utils/project-namespace.d.ts +0 -70
  1072. package/dist/utils/project-namespace.js +0 -216
  1073. package/dist/wrapper/base-wrapper.d.ts +0 -217
  1074. package/dist/wrapper/base-wrapper.js +0 -538
  1075. package/dist/wrapper/client.d.ts +0 -199
  1076. package/dist/wrapper/client.js +0 -677
  1077. package/dist/wrapper/idle-detector.d.ts +0 -102
  1078. package/dist/wrapper/idle-detector.js +0 -279
  1079. package/dist/wrapper/index.d.ts +0 -4
  1080. package/dist/wrapper/index.js +0 -7
  1081. package/dist/wrapper/parser.d.ts +0 -230
  1082. package/dist/wrapper/parser.js +0 -1178
  1083. package/dist/wrapper/pty-wrapper.d.ts +0 -343
  1084. package/dist/wrapper/pty-wrapper.js +0 -1593
  1085. package/dist/wrapper/relay-pty-orchestrator.d.ts +0 -296
  1086. package/dist/wrapper/relay-pty-orchestrator.js +0 -1088
  1087. package/dist/wrapper/shared.d.ts +0 -168
  1088. package/dist/wrapper/shared.js +0 -291
  1089. package/dist/wrapper/stuck-detector.d.ts +0 -101
  1090. package/dist/wrapper/stuck-detector.js +0 -228
  1091. package/dist/wrapper/tmux-wrapper.d.ts +0 -344
  1092. package/dist/wrapper/tmux-wrapper.js +0 -1711
  1093. /package/dist/dashboard/out/_next/static/{BffXAqxm-_rUlj2mAnK26 → ZCFjHbkF8yDKS2md3lVgb}/_buildManifest.js +0 -0
  1094. /package/dist/dashboard/out/_next/static/{BffXAqxm-_rUlj2mAnK26 → ZCFjHbkF8yDKS2md3lVgb}/_ssgManifest.js +0 -0
  1095. /package/dist/dashboard/out/_next/static/chunks/app/cloud/link/{page-cfeb437f08a12ed9.js → page-5011ae044b90449d.js} +0 -0
  1096. /package/dist/dashboard/out/_next/static/chunks/app/history/{page-240f91e8b06ba8ac.js → page-b2ce7c96ed0931da.js} +0 -0
  1097. /package/dist/dashboard/out/_next/static/chunks/app/metrics/{page-82938ab8fcf44694.js → page-bf2cb1e5915bc92d.js} +0 -0
  1098. /package/dist/{cli → src/cli}/index.d.ts +0 -0
  1099. /package/dist/{health-worker-manager.d.ts → src/health-worker-manager.d.ts} +0 -0
  1100. /package/dist/{health-worker-manager.js → src/health-worker-manager.js} +0 -0
  1101. /package/dist/{health-worker.d.ts → src/health-worker.d.ts} +0 -0
  1102. /package/dist/{health-worker.js → src/health-worker.js} +0 -0
  1103. /package/{dist/bridge → packages/bridge/dist}/shadow-cli.d.ts +0 -0
  1104. /package/{dist/bridge → packages/bridge/dist}/types.js +0 -0
  1105. /package/{dist/cloud → packages/cloud/dist}/api/admin.d.ts +0 -0
  1106. /package/{dist/cloud → packages/cloud/dist}/api/auth.d.ts +0 -0
  1107. /package/{dist/cloud → packages/cloud/dist}/api/auth.js +0 -0
  1108. /package/{dist/cloud → packages/cloud/dist}/api/billing.d.ts +0 -0
  1109. /package/{dist/cloud → packages/cloud/dist}/api/codex-auth-helper.d.ts +0 -0
  1110. /package/{dist/cloud → packages/cloud/dist}/api/consensus.d.ts +0 -0
  1111. /package/{dist/cloud → packages/cloud/dist}/api/coordinators.d.ts +0 -0
  1112. /package/{dist/cloud → packages/cloud/dist}/api/daemons.d.ts +0 -0
  1113. /package/{dist/cloud → packages/cloud/dist}/api/generic-webhooks.d.ts +0 -0
  1114. /package/{dist/cloud → packages/cloud/dist}/api/git.d.ts +0 -0
  1115. /package/{dist/cloud → packages/cloud/dist}/api/git.js +0 -0
  1116. /package/{dist/cloud → packages/cloud/dist}/api/github-app.d.ts +0 -0
  1117. /package/{dist/cloud → packages/cloud/dist}/api/middleware/planLimits.d.ts +0 -0
  1118. /package/{dist/cloud → packages/cloud/dist}/api/middleware/planLimits.js +0 -0
  1119. /package/{dist/cloud → packages/cloud/dist}/api/monitoring.d.ts +0 -0
  1120. /package/{dist/cloud → packages/cloud/dist}/api/nango-auth.d.ts +0 -0
  1121. /package/{dist/cloud → packages/cloud/dist}/api/policy.d.ts +0 -0
  1122. /package/{dist/cloud → packages/cloud/dist}/api/providers.d.ts +0 -0
  1123. /package/{dist/cloud → packages/cloud/dist}/api/repos.d.ts +0 -0
  1124. /package/{dist/cloud → packages/cloud/dist}/api/teams.d.ts +0 -0
  1125. /package/{dist/cloud → packages/cloud/dist}/api/test-helpers.d.ts +0 -0
  1126. /package/{dist/cloud → packages/cloud/dist}/api/usage.d.ts +0 -0
  1127. /package/{dist/cloud → packages/cloud/dist}/api/usage.js +0 -0
  1128. /package/{dist/cloud → packages/cloud/dist}/api/webhooks.d.ts +0 -0
  1129. /package/{dist/cloud → packages/cloud/dist}/api/webhooks.js +0 -0
  1130. /package/{dist/cloud → packages/cloud/dist}/api/workspaces.d.ts +0 -0
  1131. /package/{dist/cloud → packages/cloud/dist}/billing/index.d.ts +0 -0
  1132. /package/{dist/cloud → packages/cloud/dist}/billing/index.js +0 -0
  1133. /package/{dist/cloud → packages/cloud/dist}/billing/plans.d.ts +0 -0
  1134. /package/{dist/cloud → packages/cloud/dist}/billing/service.d.ts +0 -0
  1135. /package/{dist/cloud → packages/cloud/dist}/billing/service.js +0 -0
  1136. /package/{dist/cloud → packages/cloud/dist}/billing/types.d.ts +0 -0
  1137. /package/{dist/cloud → packages/cloud/dist}/billing/types.js +0 -0
  1138. /package/{dist/cloud → packages/cloud/dist}/db/bulk-ingest.d.ts +0 -0
  1139. /package/{dist/cloud → packages/cloud/dist}/db/bulk-ingest.js +0 -0
  1140. /package/{dist/cloud → packages/cloud/dist}/db/index.d.ts +0 -0
  1141. /package/{dist/cloud → packages/cloud/dist}/db/index.js +0 -0
  1142. /package/{dist/cloud → packages/cloud/dist}/server.d.ts +0 -0
  1143. /package/{dist/cloud → packages/cloud/dist}/services/auto-scaler.d.ts +0 -0
  1144. /package/{dist/cloud → packages/cloud/dist}/services/auto-scaler.js +0 -0
  1145. /package/{dist/cloud → packages/cloud/dist}/services/capacity-manager.d.ts +0 -0
  1146. /package/{dist/cloud → packages/cloud/dist}/services/capacity-manager.js +0 -0
  1147. /package/{dist/cloud → packages/cloud/dist}/services/ci-agent-spawner.d.ts +0 -0
  1148. /package/{dist/cloud → packages/cloud/dist}/services/ci-agent-spawner.js +0 -0
  1149. /package/{dist/cloud → packages/cloud/dist}/services/cloud-message-bus.d.ts +0 -0
  1150. /package/{dist/cloud → packages/cloud/dist}/services/cloud-message-bus.js +0 -0
  1151. /package/{dist/cloud → packages/cloud/dist}/services/compute-enforcement.d.ts +0 -0
  1152. /package/{dist/cloud → packages/cloud/dist}/services/compute-enforcement.js +0 -0
  1153. /package/{dist/cloud → packages/cloud/dist}/services/coordinator.d.ts +0 -0
  1154. /package/{dist/cloud → packages/cloud/dist}/services/coordinator.js +0 -0
  1155. /package/{dist/cloud → packages/cloud/dist}/services/mention-handler.d.ts +0 -0
  1156. /package/{dist/cloud → packages/cloud/dist}/services/mention-handler.js +0 -0
  1157. /package/{dist/cloud → packages/cloud/dist}/services/persistence.js +0 -0
  1158. /package/{dist/cloud → packages/cloud/dist}/services/planLimits.d.ts +0 -0
  1159. /package/{dist/cloud → packages/cloud/dist}/services/planLimits.js +0 -0
  1160. /package/{dist/cloud → packages/cloud/dist}/services/presence-registry.d.ts +0 -0
  1161. /package/{dist/cloud → packages/cloud/dist}/services/presence-registry.js +0 -0
  1162. /package/{dist/cloud → packages/cloud/dist}/services/scaling-orchestrator.d.ts +0 -0
  1163. /package/{dist/cloud → packages/cloud/dist}/services/scaling-orchestrator.js +0 -0
  1164. /package/{dist/cloud → packages/cloud/dist}/services/scaling-policy.d.ts +0 -0
  1165. /package/{dist/cloud → packages/cloud/dist}/services/scaling-policy.js +0 -0
  1166. /package/{dist/cloud → packages/cloud/dist}/services/ssh-security.d.ts +0 -0
  1167. /package/{dist/cloud → packages/cloud/dist}/services/ssh-security.js +0 -0
  1168. /package/{dist/cloud → packages/cloud/dist}/services/workspace-keepalive.d.ts +0 -0
  1169. /package/{dist/cloud → packages/cloud/dist}/services/workspace-keepalive.js +0 -0
  1170. /package/{dist/cloud → packages/cloud/dist}/webhooks/index.d.ts +0 -0
  1171. /package/{dist/cloud → packages/cloud/dist}/webhooks/index.js +0 -0
  1172. /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/github.d.ts +0 -0
  1173. /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/github.js +0 -0
  1174. /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/index.d.ts +0 -0
  1175. /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/index.js +0 -0
  1176. /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/linear.d.ts +0 -0
  1177. /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/linear.js +0 -0
  1178. /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/slack.d.ts +0 -0
  1179. /package/{dist/cloud → packages/cloud/dist}/webhooks/parsers/slack.js +0 -0
  1180. /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/github.d.ts +0 -0
  1181. /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/github.js +0 -0
  1182. /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/index.d.ts +0 -0
  1183. /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/index.js +0 -0
  1184. /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/linear.d.ts +0 -0
  1185. /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/linear.js +0 -0
  1186. /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/slack.d.ts +0 -0
  1187. /package/{dist/cloud → packages/cloud/dist}/webhooks/responders/slack.js +0 -0
  1188. /package/{dist/cloud → packages/cloud/dist}/webhooks/router.d.ts +0 -0
  1189. /package/{dist/cloud → packages/cloud/dist}/webhooks/router.js +0 -0
  1190. /package/{dist/cloud → packages/cloud/dist}/webhooks/rules-engine.d.ts +0 -0
  1191. /package/{dist/cloud → packages/cloud/dist}/webhooks/rules-engine.js +0 -0
  1192. /package/{dist/cloud → packages/cloud/dist}/webhooks/types.d.ts +0 -0
  1193. /package/{dist/cloud → packages/cloud/dist}/webhooks/types.js +0 -0
  1194. /package/{dist/utils → packages/config/dist}/agent-config.d.ts +0 -0
  1195. /package/{dist/utils → packages/config/dist}/agent-config.js +0 -0
  1196. /package/{dist/shared → packages/config/dist}/cli-auth-config.d.ts +0 -0
  1197. /package/{dist/bridge → packages/config/dist}/shadow-config.js +0 -0
  1198. /package/{dist/bridge → packages/config/dist}/teams-config.d.ts +0 -0
  1199. /package/{dist/bridge → packages/config/dist}/teams-config.js +0 -0
  1200. /package/{dist/continuity → packages/continuity/dist}/formatter.d.ts +0 -0
  1201. /package/{dist/continuity → packages/continuity/dist}/formatter.js +0 -0
  1202. /package/{dist/continuity → packages/continuity/dist}/handoff-store.d.ts +0 -0
  1203. /package/{dist/continuity → packages/continuity/dist}/handoff-store.js +0 -0
  1204. /package/{dist/continuity → packages/continuity/dist}/ledger-store.d.ts +0 -0
  1205. /package/{dist/continuity → packages/continuity/dist}/ledger-store.js +0 -0
  1206. /package/{dist/continuity → packages/continuity/dist}/manager.d.ts +0 -0
  1207. /package/{dist/continuity → packages/continuity/dist}/manager.js +0 -0
  1208. /package/{dist/continuity → packages/continuity/dist}/parser.d.ts +0 -0
  1209. /package/{dist/continuity → packages/continuity/dist}/parser.js +0 -0
  1210. /package/{dist/daemon → packages/daemon/dist}/agent-registry.d.ts +0 -0
  1211. /package/{dist/daemon → packages/daemon/dist}/agent-signing.d.ts +0 -0
  1212. /package/{dist/daemon → packages/daemon/dist}/agent-signing.js +0 -0
  1213. /package/{dist/daemon → packages/daemon/dist}/auth.d.ts +0 -0
  1214. /package/{dist/daemon → packages/daemon/dist}/auth.js +0 -0
  1215. /package/{dist/daemon → packages/daemon/dist}/consensus-integration.d.ts +0 -0
  1216. /package/{dist/daemon → packages/daemon/dist}/consensus.d.ts +0 -0
  1217. /package/{dist/daemon → packages/daemon/dist}/consensus.js +0 -0
  1218. /package/{dist/daemon → packages/daemon/dist}/rate-limiter.d.ts +0 -0
  1219. /package/{dist/daemon → packages/daemon/dist}/rate-limiter.js +0 -0
  1220. /package/{dist/daemon → packages/daemon/dist}/registry.d.ts +0 -0
  1221. /package/{dist/daemon → packages/daemon/dist}/registry.js +0 -0
  1222. /package/{dist/daemon → packages/daemon/dist}/repo-manager.d.ts +0 -0
  1223. /package/{dist/daemon → packages/daemon/dist}/types.js +0 -0
  1224. /package/{dist/daemon → packages/daemon/dist}/workspace-manager.d.ts +0 -0
  1225. /package/{dist/dashboard-server → packages/dashboard-server/dist}/metrics.d.ts +0 -0
  1226. /package/{dist/dashboard-server → packages/dashboard-server/dist}/metrics.js +0 -0
  1227. /package/{dist/dashboard-server → packages/dashboard-server/dist}/needs-attention.d.ts +0 -0
  1228. /package/{dist/dashboard-server → packages/dashboard-server/dist}/needs-attention.js +0 -0
  1229. /package/{dist/dashboard-server → packages/dashboard-server/dist}/server.d.ts +0 -0
  1230. /package/{dist/dashboard-server → packages/dashboard-server/dist}/start.d.ts +0 -0
  1231. /package/{dist/hooks → packages/hooks/dist}/emitter.d.ts +0 -0
  1232. /package/{dist/hooks → packages/hooks/dist}/emitter.js +0 -0
  1233. /package/{dist/hooks → packages/hooks/dist}/inbox-check/hook.d.ts +0 -0
  1234. /package/{dist/hooks → packages/hooks/dist}/inbox-check/hook.js +0 -0
  1235. /package/{dist/hooks → packages/hooks/dist}/inbox-check/index.d.ts +0 -0
  1236. /package/{dist/hooks → packages/hooks/dist}/inbox-check/index.js +0 -0
  1237. /package/{dist/hooks → packages/hooks/dist}/inbox-check/types.d.ts +0 -0
  1238. /package/{dist/hooks → packages/hooks/dist}/inbox-check/types.js +0 -0
  1239. /package/{dist/hooks → packages/hooks/dist}/inbox-check/utils.d.ts +0 -0
  1240. /package/{dist/hooks → packages/hooks/dist}/inbox-check/utils.js +0 -0
  1241. /package/{dist/hooks → packages/hooks/dist}/registry.d.ts +0 -0
  1242. /package/{dist/hooks → packages/hooks/dist}/trajectory-hooks.d.ts +0 -0
  1243. /package/{dist/memory → packages/memory/dist}/adapters/index.d.ts +0 -0
  1244. /package/{dist/memory → packages/memory/dist}/adapters/index.js +0 -0
  1245. /package/{dist/memory → packages/memory/dist}/adapters/inmemory.d.ts +0 -0
  1246. /package/{dist/memory → packages/memory/dist}/adapters/inmemory.js +0 -0
  1247. /package/{dist/memory → packages/memory/dist}/adapters/supermemory.d.ts +0 -0
  1248. /package/{dist/memory → packages/memory/dist}/adapters/supermemory.js +0 -0
  1249. /package/{dist/memory → packages/memory/dist}/context-compaction.d.ts +0 -0
  1250. /package/{dist/memory → packages/memory/dist}/context-compaction.js +0 -0
  1251. /package/{dist/memory → packages/memory/dist}/factory.d.ts +0 -0
  1252. /package/{dist/memory → packages/memory/dist}/factory.js +0 -0
  1253. /package/{dist/memory → packages/memory/dist}/index.d.ts +0 -0
  1254. /package/{dist/memory → packages/memory/dist}/index.js +0 -0
  1255. /package/{dist/memory → packages/memory/dist}/memory-hooks.js +0 -0
  1256. /package/{dist/memory → packages/memory/dist}/service.d.ts +0 -0
  1257. /package/{dist/memory → packages/memory/dist}/service.js +0 -0
  1258. /package/{dist/memory → packages/memory/dist}/types.d.ts +0 -0
  1259. /package/{dist/memory → packages/memory/dist}/types.js +0 -0
  1260. /package/{dist/policy → packages/policy/dist}/agent-policy.d.ts +0 -0
  1261. /package/{dist/policy → packages/policy/dist}/cloud-policy-fetcher.d.ts +0 -0
  1262. /package/{dist/policy → packages/policy/dist}/cloud-policy-fetcher.js +0 -0
  1263. /package/{dist/utils → packages/protocol/dist}/id-generator.d.ts +0 -0
  1264. /package/{dist/utils → packages/protocol/dist}/id-generator.js +0 -0
  1265. /package/{dist/protocol → packages/protocol/dist}/relay-pty-schemas.js +0 -0
  1266. /package/{dist/resiliency → packages/resiliency/dist}/context-persistence.d.ts +0 -0
  1267. /package/{dist/resiliency → packages/resiliency/dist}/context-persistence.js +0 -0
  1268. /package/{dist/resiliency → packages/resiliency/dist}/crash-insights.d.ts +0 -0
  1269. /package/{dist/resiliency → packages/resiliency/dist}/crash-insights.js +0 -0
  1270. /package/{dist/resiliency → packages/resiliency/dist}/gossip-health.d.ts +0 -0
  1271. /package/{dist/resiliency → packages/resiliency/dist}/gossip-health.js +0 -0
  1272. /package/{dist/resiliency → packages/resiliency/dist}/health-monitor.d.ts +0 -0
  1273. /package/{dist/resiliency → packages/resiliency/dist}/health-monitor.js +0 -0
  1274. /package/{dist/resiliency → packages/resiliency/dist}/index.d.ts +0 -0
  1275. /package/{dist/resiliency → packages/resiliency/dist}/index.js +0 -0
  1276. /package/{dist/resiliency → packages/resiliency/dist}/leader-watchdog.d.ts +0 -0
  1277. /package/{dist/resiliency → packages/resiliency/dist}/leader-watchdog.js +0 -0
  1278. /package/{dist/resiliency → packages/resiliency/dist}/logger.d.ts +0 -0
  1279. /package/{dist/resiliency → packages/resiliency/dist}/logger.js +0 -0
  1280. /package/{dist/resiliency → packages/resiliency/dist}/memory-monitor.d.ts +0 -0
  1281. /package/{dist/resiliency → packages/resiliency/dist}/metrics.d.ts +0 -0
  1282. /package/{dist/resiliency → packages/resiliency/dist}/metrics.js +0 -0
  1283. /package/{dist/resiliency → packages/resiliency/dist}/provider-context.js +0 -0
  1284. /package/{dist/resiliency → packages/resiliency/dist}/stateless-lead.d.ts +0 -0
  1285. /package/{dist/resiliency → packages/resiliency/dist}/stateless-lead.js +0 -0
  1286. /package/{dist/resiliency → packages/resiliency/dist}/supervisor.d.ts +0 -0
  1287. /package/{dist/resiliency → packages/resiliency/dist}/supervisor.js +0 -0
  1288. /package/{dist/state → packages/state/dist}/agent-state.d.ts +0 -0
  1289. /package/{dist/storage → packages/storage/dist}/adapter.js +0 -0
  1290. /package/{dist/storage → packages/storage/dist}/dead-letter-queue.d.ts +0 -0
  1291. /package/{dist/storage → packages/storage/dist}/dead-letter-queue.js +0 -0
  1292. /package/{dist/storage → packages/storage/dist}/dlq-adapter.d.ts +0 -0
  1293. /package/{dist/storage → packages/storage/dist}/dlq-adapter.js +0 -0
  1294. /package/{dist/trajectory → packages/trajectory/dist}/integration.d.ts +0 -0
  1295. /package/{dist/utils → packages/utils/dist}/command-resolver.d.ts +0 -0
  1296. /package/{dist/utils → packages/utils/dist}/git-remote.d.ts +0 -0
  1297. /package/{dist/utils → packages/utils/dist}/git-remote.js +0 -0
  1298. /package/{dist/utils → packages/utils/dist}/logger.d.ts +0 -0
  1299. /package/{dist/utils → packages/utils/dist}/logger.js +0 -0
  1300. /package/{dist/utils → packages/utils/dist}/name-generator.d.ts +0 -0
  1301. /package/{dist/utils → packages/utils/dist}/name-generator.js +0 -0
  1302. /package/{dist/utils → packages/utils/dist}/precompiled-patterns.d.ts +0 -0
  1303. /package/{dist/utils → packages/utils/dist}/precompiled-patterns.js +0 -0
  1304. /package/{dist/utils → packages/utils/dist}/update-checker.d.ts +0 -0
  1305. /package/{dist/utils → packages/utils/dist}/update-checker.js +0 -0
  1306. /package/{dist/wrapper → packages/wrapper/dist}/auth-detection.d.ts +0 -0
  1307. /package/{dist/wrapper → packages/wrapper/dist}/auth-detection.js +0 -0
  1308. /package/{dist/wrapper → packages/wrapper/dist}/inbox.d.ts +0 -0
  1309. /package/{dist/wrapper → packages/wrapper/dist}/inbox.js +0 -0
  1310. /package/{dist/wrapper → packages/wrapper/dist}/prompt-composer.d.ts +0 -0
  1311. /package/{dist/wrapper → packages/wrapper/dist}/prompt-composer.js +0 -0
  1312. /package/{dist/utils → packages/wrapper/dist}/tmux-resolver.d.ts +0 -0
  1313. /package/{dist/utils → packages/wrapper/dist}/tmux-resolver.js +0 -0
@@ -0,0 +1,3447 @@
1
+ /**
2
+ * Dashboard V2 - Main Application Component
3
+ *
4
+ * Root component that combines sidebar, header, and main content area.
5
+ * Manages global state via hooks and provides context to child components.
6
+ */
7
+
8
+ import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
9
+ import type { Agent, Project, Message, AgentSummary, ActivityEvent } from '../types';
10
+ import { ActivityFeed } from './ActivityFeed';
11
+ import { Sidebar } from './layout/Sidebar';
12
+ import { Header } from './layout/Header';
13
+ import { MessageList } from './MessageList';
14
+ import { ThreadPanel } from './ThreadPanel';
15
+ import { CommandPalette, type TaskCreateRequest, PRIORITY_CONFIG } from './CommandPalette';
16
+ import { SpawnModal, type SpawnConfig } from './SpawnModal';
17
+ import { NewConversationModal } from './NewConversationModal';
18
+ import { SettingsPage, defaultSettings, type Settings } from './settings';
19
+ import { ConversationHistory } from './ConversationHistory';
20
+ import { MentionAutocomplete, getMentionQuery, completeMentionInValue, type HumanUser } from './MentionAutocomplete';
21
+ import { NotificationToast, useToasts } from './NotificationToast';
22
+ import { FileAutocomplete, getFileQuery, completeFileInValue } from './FileAutocomplete';
23
+ import { WorkspaceSelector, type Workspace } from './WorkspaceSelector';
24
+ import { AddWorkspaceModal } from './AddWorkspaceModal';
25
+ import { LogViewerPanel } from './LogViewerPanel';
26
+ import { TrajectoryViewer } from './TrajectoryViewer';
27
+ import { DecisionQueue, type Decision } from './DecisionQueue';
28
+ import { FleetOverview } from './FleetOverview';
29
+ import type { ServerInfo } from './ServerCard';
30
+ import { TypingIndicator } from './TypingIndicator';
31
+ import { OnlineUsersIndicator } from './OnlineUsersIndicator';
32
+ import { UserProfilePanel } from './UserProfilePanel';
33
+ import { AgentProfilePanel } from './AgentProfilePanel';
34
+ import { useDirectMessage } from './hooks/useDirectMessage';
35
+ import { CoordinatorPanel } from './CoordinatorPanel';
36
+ import { BillingResult } from './BillingResult';
37
+ import { UsageBanner } from './UsageBanner';
38
+ import { useWebSocket } from './hooks/useWebSocket';
39
+ import { useAgents } from './hooks/useAgents';
40
+ import { useMessages } from './hooks/useMessages';
41
+ import { useOrchestrator } from './hooks/useOrchestrator';
42
+ import { useTrajectory } from './hooks/useTrajectory';
43
+ import { useRecentRepos } from './hooks/useRecentRepos';
44
+ import { useWorkspaceRepos } from './hooks/useWorkspaceRepos';
45
+ import { usePresence, type UserPresence } from './hooks/usePresence';
46
+ import {
47
+ ChannelViewV1,
48
+ SearchInput,
49
+ CreateChannelModal,
50
+ InviteToChannelModal,
51
+ MemberManagementPanel,
52
+ listChannels,
53
+ getMessages,
54
+ getChannelMembers,
55
+ removeMember as removeChannelMember,
56
+ sendMessage as sendChannelApiMessage,
57
+ markRead,
58
+ createChannel,
59
+ type Channel,
60
+ type ChannelMember,
61
+ type ChannelMessage as ChannelApiMessage,
62
+ type UnreadState,
63
+ type CreateChannelRequest,
64
+ } from './channels';
65
+ import { useWorkspaceMembers, filterOnlineUsersByWorkspace } from './hooks/useWorkspaceMembers';
66
+ import { useCloudSessionOptional } from './CloudSessionProvider';
67
+ import { WorkspaceProvider } from './WorkspaceContext';
68
+ import { api, convertApiDecision, setActiveWorkspaceId as setApiWorkspaceId, getActiveWorkspaceId, getCsrfToken } from '../lib/api';
69
+ import { cloudApi } from '../lib/cloudApi';
70
+ import { mergeAgentsForDashboard } from '../lib/agent-merge';
71
+ import type { CurrentUser } from './MessageList';
72
+
73
+ /**
74
+ * Check if a sender is a human user (not an agent or system name)
75
+ * Extracts the logic for identifying human users to avoid duplication
76
+ */
77
+ function isHumanSender(sender: string, agentNames: Set<string>): boolean {
78
+ return sender !== 'Dashboard' &&
79
+ sender !== '*' &&
80
+ !agentNames.has(sender.toLowerCase());
81
+ }
82
+
83
+ const SETTINGS_STORAGE_KEY = 'dashboard-settings';
84
+
85
+ /** Special ID for the Activity feed (broadcasts) */
86
+ export const ACTIVITY_FEED_ID = '__activity__';
87
+
88
+ type LegacyDashboardSettings = {
89
+ theme?: 'dark' | 'light' | 'system';
90
+ compactMode?: boolean;
91
+ showTimestamps?: boolean;
92
+ soundEnabled?: boolean;
93
+ notificationsEnabled?: boolean;
94
+ autoScrollMessages?: boolean;
95
+ };
96
+
97
+ function mergeSettings(base: Settings, partial: Partial<Settings>): Settings {
98
+ return {
99
+ ...base,
100
+ ...partial,
101
+ notifications: { ...base.notifications, ...partial.notifications },
102
+ display: { ...base.display, ...partial.display },
103
+ messages: { ...base.messages, ...partial.messages },
104
+ connection: { ...base.connection, ...partial.connection },
105
+ };
106
+ }
107
+
108
+ function migrateLegacySettings(raw: LegacyDashboardSettings): Settings {
109
+ const theme = raw.theme && ['dark', 'light', 'system'].includes(raw.theme)
110
+ ? raw.theme
111
+ : defaultSettings.theme;
112
+ const sound = raw.soundEnabled ?? defaultSettings.notifications.sound;
113
+ const desktop = raw.notificationsEnabled ?? defaultSettings.notifications.desktop;
114
+ return {
115
+ ...defaultSettings,
116
+ theme,
117
+ display: {
118
+ ...defaultSettings.display,
119
+ compactMode: raw.compactMode ?? defaultSettings.display.compactMode,
120
+ showTimestamps: raw.showTimestamps ?? defaultSettings.display.showTimestamps,
121
+ },
122
+ notifications: {
123
+ ...defaultSettings.notifications,
124
+ sound,
125
+ desktop,
126
+ enabled: sound || desktop || defaultSettings.notifications.mentionsOnly,
127
+ },
128
+ messages: {
129
+ ...defaultSettings.messages,
130
+ autoScroll: raw.autoScrollMessages ?? defaultSettings.messages.autoScroll,
131
+ },
132
+ };
133
+ }
134
+
135
+ function loadSettingsFromStorage(): Settings {
136
+ if (typeof window === 'undefined') return defaultSettings;
137
+ try {
138
+ const saved = localStorage.getItem(SETTINGS_STORAGE_KEY);
139
+ if (!saved) return defaultSettings;
140
+ const parsed = JSON.parse(saved);
141
+ if (!parsed || typeof parsed !== 'object') return defaultSettings;
142
+ if ('notifications' in parsed && 'display' in parsed) {
143
+ const merged = mergeSettings(defaultSettings, parsed as Partial<Settings>);
144
+ merged.notifications.enabled = merged.notifications.sound ||
145
+ merged.notifications.desktop ||
146
+ merged.notifications.mentionsOnly;
147
+ return merged;
148
+ }
149
+ if ('notificationsEnabled' in parsed || 'soundEnabled' in parsed || 'autoScrollMessages' in parsed) {
150
+ return migrateLegacySettings(parsed as LegacyDashboardSettings);
151
+ }
152
+ } catch {
153
+ // Fall back to defaults
154
+ }
155
+ return defaultSettings;
156
+ }
157
+
158
+ function saveSettingsToStorage(settings: Settings) {
159
+ if (typeof window === 'undefined') return;
160
+ try {
161
+ localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
162
+ } catch {
163
+ // Ignore localStorage failures
164
+ }
165
+ }
166
+
167
+ function playNotificationSound() {
168
+ if (typeof window === 'undefined') return;
169
+ const AudioContextConstructor =
170
+ window.AudioContext ||
171
+ (window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
172
+ if (!AudioContextConstructor) return;
173
+ try {
174
+ const context = new AudioContextConstructor();
175
+ const oscillator = context.createOscillator();
176
+ const gain = context.createGain();
177
+ oscillator.type = 'sine';
178
+ oscillator.frequency.value = 880;
179
+ gain.gain.value = 0.03;
180
+ oscillator.connect(gain);
181
+ gain.connect(context.destination);
182
+ oscillator.start();
183
+ oscillator.stop(context.currentTime + 0.12);
184
+ oscillator.onended = () => {
185
+ context.close().catch(() => undefined);
186
+ };
187
+ } catch {
188
+ // Audio might be blocked by browser autoplay policies
189
+ }
190
+ }
191
+
192
+ export interface AppProps {
193
+ /** Initial WebSocket URL (optional, defaults to current host) */
194
+ wsUrl?: string;
195
+ /** Orchestrator API URL (optional, defaults to localhost:3456) */
196
+ orchestratorUrl?: string;
197
+ }
198
+
199
+ export function App({ wsUrl, orchestratorUrl }: AppProps) {
200
+ // WebSocket connection for real-time data (per-project daemon)
201
+ const { data, isConnected, error: wsError } = useWebSocket({ url: wsUrl });
202
+
203
+ // Orchestrator for multi-workspace management
204
+ const {
205
+ workspaces,
206
+ activeWorkspaceId,
207
+ agents: orchestratorAgents,
208
+ isConnected: isOrchestratorConnected,
209
+ isLoading: isOrchestratorLoading,
210
+ error: orchestratorError,
211
+ switchWorkspace,
212
+ addWorkspace,
213
+ removeWorkspace,
214
+ spawnAgent: orchestratorSpawnAgent,
215
+ stopAgent: orchestratorStopAgent,
216
+ } = useOrchestrator({ apiUrl: orchestratorUrl });
217
+
218
+ // Cloud session for user info (GitHub avatar/username)
219
+ const cloudSession = useCloudSessionOptional();
220
+
221
+ // Derive current user from cloud session (falls back to undefined in non-cloud mode)
222
+ const currentUser: CurrentUser | undefined = cloudSession?.user
223
+ ? {
224
+ displayName: cloudSession.user.githubUsername,
225
+ avatarUrl: cloudSession.user.avatarUrl,
226
+ }
227
+ : undefined;
228
+
229
+ // Cloud workspaces state (for cloud mode)
230
+ // Includes owned, member, and contributor workspaces (via GitHub repo access)
231
+ const [cloudWorkspaces, setCloudWorkspaces] = useState<Array<{
232
+ id: string;
233
+ name: string;
234
+ status: string;
235
+ publicUrl?: string;
236
+ accessType?: 'owner' | 'member' | 'contributor';
237
+ permission?: 'admin' | 'write' | 'read';
238
+ }>>([]);
239
+ // Initialize from API module if already set (e.g., by DashboardPage when connecting to workspace)
240
+ const [activeCloudWorkspaceId, setActiveCloudWorkspaceId] = useState<string | null>(() => getActiveWorkspaceId());
241
+ const [isLoadingCloudWorkspaces, setIsLoadingCloudWorkspaces] = useState(false);
242
+
243
+ // Local agents from linked daemons
244
+ const [localAgents, setLocalAgents] = useState<Agent[]>([]);
245
+
246
+ // Fetch cloud workspaces when in cloud mode
247
+ // Uses getAccessibleWorkspaces to include contributor workspaces (via GitHub repos)
248
+ useEffect(() => {
249
+ if (!cloudSession?.user) return;
250
+
251
+ const fetchCloudWorkspaces = async () => {
252
+ setIsLoadingCloudWorkspaces(true);
253
+ try {
254
+ const result = await cloudApi.getAccessibleWorkspaces();
255
+ if (result.success && result.data.workspaces) {
256
+ setCloudWorkspaces(result.data.workspaces);
257
+ const workspaceIds = new Set(result.data.workspaces.map(w => w.id));
258
+ // Validate current selection exists, or auto-select first workspace
259
+ if (activeCloudWorkspaceId && !workspaceIds.has(activeCloudWorkspaceId)) {
260
+ // Current workspace no longer exists, clear selection to trigger auto-select
261
+ if (result.data.workspaces.length > 0) {
262
+ const firstWorkspaceId = result.data.workspaces[0].id;
263
+ setActiveCloudWorkspaceId(firstWorkspaceId);
264
+ setApiWorkspaceId(firstWorkspaceId);
265
+ } else {
266
+ setActiveCloudWorkspaceId(null);
267
+ setApiWorkspaceId(null);
268
+ }
269
+ } else if (!activeCloudWorkspaceId && result.data.workspaces.length > 0) {
270
+ // No selection yet, auto-select first workspace
271
+ const firstWorkspaceId = result.data.workspaces[0].id;
272
+ setActiveCloudWorkspaceId(firstWorkspaceId);
273
+ // Sync immediately with api module to avoid race conditions
274
+ setApiWorkspaceId(firstWorkspaceId);
275
+ }
276
+ }
277
+ } catch (err) {
278
+ console.error('Failed to fetch cloud workspaces:', err);
279
+ } finally {
280
+ setIsLoadingCloudWorkspaces(false);
281
+ }
282
+ };
283
+
284
+ fetchCloudWorkspaces();
285
+ // Poll for updates every 30 seconds
286
+ const interval = setInterval(fetchCloudWorkspaces, 30000);
287
+ return () => clearInterval(interval);
288
+ }, [cloudSession?.user, activeCloudWorkspaceId]);
289
+
290
+ // Fetch local agents for the active workspace
291
+ useEffect(() => {
292
+ if (!cloudSession?.user || !activeCloudWorkspaceId) {
293
+ setLocalAgents([]);
294
+ return;
295
+ }
296
+
297
+ const fetchLocalAgents = async () => {
298
+ try {
299
+ const result = await api.get<{
300
+ agents: Array<{
301
+ name: string;
302
+ status: string;
303
+ isLocal: boolean;
304
+ isHuman?: boolean;
305
+ avatarUrl?: string;
306
+ daemonId: string;
307
+ daemonName: string;
308
+ daemonStatus: string;
309
+ machineId: string;
310
+ lastSeenAt: string | null;
311
+ }>;
312
+ }>(`/api/daemons/workspace/${activeCloudWorkspaceId}/agents`);
313
+
314
+ if (result.agents) {
315
+ // Convert API response to Agent format
316
+ // Agent status is 'online' when daemon is online (agent is connected to daemon)
317
+ const agents: Agent[] = result.agents.map((a) => ({
318
+ name: a.name,
319
+ status: a.daemonStatus === 'online' ? 'online' : 'offline',
320
+ // Only mark AI agents as "local" (from linked daemon), not human users
321
+ isLocal: !a.isHuman,
322
+ isHuman: a.isHuman,
323
+ avatarUrl: a.avatarUrl,
324
+ // Don't include daemon info for human users
325
+ daemonName: a.isHuman ? undefined : a.daemonName,
326
+ machineId: a.isHuman ? undefined : a.machineId,
327
+ lastSeen: a.lastSeenAt || undefined,
328
+ }));
329
+ setLocalAgents(agents);
330
+ }
331
+ } catch (err) {
332
+ console.error('Failed to fetch local agents:', err);
333
+ setLocalAgents([]);
334
+ }
335
+ };
336
+
337
+ fetchLocalAgents();
338
+ // Poll for updates every 15 seconds
339
+ const interval = setInterval(fetchLocalAgents, 15000);
340
+ return () => clearInterval(interval);
341
+ }, [cloudSession?.user, activeCloudWorkspaceId]);
342
+
343
+ // Determine which workspaces to use (cloud mode or orchestrator)
344
+ const isCloudMode = Boolean(cloudSession?.user);
345
+ const effectiveWorkspaces = useMemo(() => {
346
+ if (isCloudMode && cloudWorkspaces.length > 0) {
347
+ // Convert cloud workspaces to the format expected by WorkspaceSelector
348
+ // Includes owned, member, and contributor workspaces
349
+ return cloudWorkspaces.map(ws => ({
350
+ id: ws.id,
351
+ name: ws.name,
352
+ path: ws.publicUrl || `/workspace/${ws.name}`,
353
+ status: ws.status === 'running' ? 'active' as const : 'inactive' as const,
354
+ provider: 'claude' as const,
355
+ lastActiveAt: new Date(),
356
+ }));
357
+ }
358
+ return workspaces;
359
+ }, [isCloudMode, cloudWorkspaces, workspaces]);
360
+
361
+ const effectiveActiveWorkspaceId = isCloudMode ? activeCloudWorkspaceId : activeWorkspaceId;
362
+ const effectiveIsLoading = isCloudMode ? isLoadingCloudWorkspaces : isOrchestratorLoading;
363
+
364
+ // Sync the active workspace ID with the api module for cloud mode proxying
365
+ // This useEffect serves as a safeguard and handles initial load/edge cases
366
+ // The immediate sync in handleEffectiveWorkspaceSelect handles user-initiated changes
367
+ useEffect(() => {
368
+ if (isCloudMode && activeCloudWorkspaceId) {
369
+ setApiWorkspaceId(activeCloudWorkspaceId);
370
+ } else if (isCloudMode && !activeCloudWorkspaceId) {
371
+ // In cloud mode but no workspace selected - clear the proxy
372
+ setApiWorkspaceId(null);
373
+ } else if (!isCloudMode) {
374
+ // Clear the workspace ID when not in cloud mode
375
+ setApiWorkspaceId(null);
376
+ }
377
+ }, [isCloudMode, activeCloudWorkspaceId]);
378
+
379
+ // Handle workspace selection (works for both cloud and orchestrator)
380
+ const handleEffectiveWorkspaceSelect = useCallback(async (workspace: { id: string; name: string }) => {
381
+ if (isCloudMode) {
382
+ setActiveCloudWorkspaceId(workspace.id);
383
+ // Sync immediately with api module to avoid race conditions
384
+ // This ensures spawn/release calls use the correct workspace before the useEffect runs
385
+ setApiWorkspaceId(workspace.id);
386
+ } else {
387
+ await switchWorkspace(workspace.id);
388
+ }
389
+ }, [isCloudMode, switchWorkspace]);
390
+
391
+ // Presence tracking for online users and typing indicators
392
+ // Memoize the user object to prevent reconnection on every render
393
+ const presenceUser = useMemo(() =>
394
+ currentUser
395
+ ? { username: currentUser.displayName, avatarUrl: currentUser.avatarUrl }
396
+ : undefined,
397
+ [currentUser?.displayName, currentUser?.avatarUrl]
398
+ );
399
+
400
+ // Channel state: selectedChannelId must be declared before callbacks that use it
401
+ // Default to Activity feed on load
402
+ const [selectedChannelId, setSelectedChannelId] = useState<string | undefined>(ACTIVITY_FEED_ID);
403
+
404
+ // Activity feed state - unified timeline of workspace events
405
+ const [activityEvents, setActivityEvents] = useState<ActivityEvent[]>([]);
406
+
407
+ // Helper to add activity events
408
+ const addActivityEvent = useCallback((event: Omit<ActivityEvent, 'id' | 'timestamp'>) => {
409
+ const newEvent: ActivityEvent = {
410
+ ...event,
411
+ id: `activity-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
412
+ timestamp: new Date().toISOString(),
413
+ };
414
+ setActivityEvents(prev => [newEvent, ...prev].slice(0, 200)); // Keep last 200 events
415
+ }, []);
416
+
417
+ // Member management state
418
+ const [showMemberPanel, setShowMemberPanel] = useState(false);
419
+ const [channelMembers, setChannelMembers] = useState<ChannelMember[]>([]);
420
+
421
+ const appendChannelMessage = useCallback((channelId: string, message: ChannelApiMessage, options?: { incrementUnread?: boolean }) => {
422
+ const incrementUnread = options?.incrementUnread ?? true;
423
+
424
+ setChannelMessageMap(prev => {
425
+ const list = prev[channelId] ?? [];
426
+ const isDuplicate = list.some((m) => {
427
+ if (m.id === message.id) return true;
428
+ if (m.from !== message.from) return false;
429
+ if (m.content !== message.content) return false;
430
+ if (m.threadId !== message.threadId) return false;
431
+ const timeDiff = Math.abs(new Date(m.timestamp).getTime() - new Date(message.timestamp).getTime());
432
+ return timeDiff < 2000;
433
+ });
434
+ if (isDuplicate) return prev;
435
+ return { ...prev, [channelId]: [...list, message] };
436
+ });
437
+
438
+ if (selectedChannelId === channelId) {
439
+ setChannelMessages(prev => [...prev, message]);
440
+ setChannelUnreadState(undefined);
441
+ } else if (incrementUnread) {
442
+ setChannelsList(prev => {
443
+ const existing = prev.find(c => c.id === channelId);
444
+ if (existing) {
445
+ return prev.map(c =>
446
+ c.id === channelId
447
+ ? { ...c, unreadCount: (c.unreadCount ?? 0) + 1 }
448
+ : c
449
+ );
450
+ }
451
+
452
+ const newChannel: Channel = {
453
+ id: channelId,
454
+ name: channelId.startsWith('#') ? channelId.slice(1) : channelId,
455
+ visibility: 'public',
456
+ status: 'active',
457
+ createdAt: new Date().toISOString(),
458
+ createdBy: currentUser?.displayName || 'Dashboard',
459
+ memberCount: 1,
460
+ unreadCount: 1,
461
+ hasMentions: false,
462
+ isDm: channelId.startsWith('dm:'),
463
+ };
464
+
465
+ return [...prev, newChannel];
466
+ });
467
+ }
468
+ }, [currentUser?.displayName, selectedChannelId]);
469
+
470
+ const handlePresenceEvent = useCallback((event: any) => {
471
+ // Activity feed: capture presence join/leave events
472
+ if (event?.type === 'presence_join' && event.user) {
473
+ const user = event.user;
474
+ // Skip self
475
+ if (user.username !== currentUser?.displayName) {
476
+ addActivityEvent({
477
+ type: 'user_joined',
478
+ actor: user.username,
479
+ actorAvatarUrl: user.avatarUrl,
480
+ actorType: 'user',
481
+ title: 'came online',
482
+ });
483
+ }
484
+ } else if (event?.type === 'presence_leave' && event.username) {
485
+ // Skip self
486
+ if (event.username !== currentUser?.displayName) {
487
+ addActivityEvent({
488
+ type: 'user_left',
489
+ actor: event.username,
490
+ actorType: 'user',
491
+ title: 'went offline',
492
+ });
493
+ }
494
+ } else if (event?.type === 'agent_spawned' && event.agent) {
495
+ // Agent spawned event from backend
496
+ addActivityEvent({
497
+ type: 'agent_spawned',
498
+ actor: event.agent.name || event.agent,
499
+ actorType: 'agent',
500
+ title: 'was spawned',
501
+ description: event.task,
502
+ metadata: { cli: event.cli, task: event.task, spawnedBy: event.spawnedBy },
503
+ });
504
+ } else if (event?.type === 'agent_released' && event.agent) {
505
+ // Agent released event from backend
506
+ addActivityEvent({
507
+ type: 'agent_released',
508
+ actor: event.agent.name || event.agent,
509
+ actorType: 'agent',
510
+ title: 'was released',
511
+ metadata: { releasedBy: event.releasedBy },
512
+ });
513
+ } else if (event?.type === 'channel_created') {
514
+ // Another user created a channel - add it to the list
515
+ const newChannel = event.channel;
516
+ if (!newChannel || !newChannel.id) return;
517
+
518
+ setChannelsList(prev => {
519
+ // Don't add if already exists
520
+ if (prev.some(c => c.id === newChannel.id)) return prev;
521
+
522
+ const channel: Channel = {
523
+ id: newChannel.id,
524
+ name: newChannel.name || newChannel.id,
525
+ description: newChannel.description,
526
+ visibility: newChannel.visibility || 'public',
527
+ status: newChannel.status || 'active',
528
+ createdAt: newChannel.createdAt || new Date().toISOString(),
529
+ createdBy: newChannel.createdBy || 'unknown',
530
+ memberCount: newChannel.memberCount || 1,
531
+ unreadCount: newChannel.unreadCount || 0,
532
+ hasMentions: newChannel.hasMentions || false,
533
+ isDm: newChannel.isDm || false,
534
+ };
535
+ console.log('[App] Channel created via WebSocket:', channel.id);
536
+ return [...prev, channel];
537
+ });
538
+ } else if (event?.type === 'channel_message') {
539
+ const channelId = event.channel as string | undefined;
540
+ if (!channelId) return;
541
+ const sender = event.from || 'unknown';
542
+ // Use server-provided entity type if available, otherwise derive locally
543
+ const fromEntityType = event.fromEntityType || (currentUser?.displayName && sender === currentUser.displayName ? 'user' : 'agent');
544
+ const msg: ChannelApiMessage = {
545
+ id: event.id ?? `ws-${Date.now()}`,
546
+ channelId,
547
+ from: sender,
548
+ fromEntityType,
549
+ fromAvatarUrl: event.fromAvatarUrl,
550
+ content: event.body ?? '',
551
+ timestamp: event.timestamp || new Date().toISOString(),
552
+ threadId: event.thread,
553
+ isRead: selectedChannelId === channelId,
554
+ };
555
+ appendChannelMessage(channelId, msg, { incrementUnread: selectedChannelId !== channelId });
556
+ } else if (event?.type === 'direct_message') {
557
+ // Handle direct messages sent to the user's GitHub username
558
+ const sender = event.from || 'unknown';
559
+ const recipient = currentUser?.displayName;
560
+ if (!recipient) return;
561
+
562
+ // Create DM channel ID with sorted participants for consistency
563
+ const participants = [sender, recipient].sort();
564
+ const dmChannelId = `dm:${participants.join(':')}`;
565
+
566
+ // Use server-provided entity type if available
567
+ const fromEntityType = event.fromEntityType || 'agent';
568
+ const msg: ChannelApiMessage = {
569
+ id: event.id ?? `dm-${Date.now()}`,
570
+ channelId: dmChannelId,
571
+ from: sender,
572
+ fromEntityType,
573
+ fromAvatarUrl: event.fromAvatarUrl,
574
+ content: event.body ?? '',
575
+ timestamp: event.timestamp || new Date().toISOString(),
576
+ threadId: event.thread,
577
+ isRead: selectedChannelId === dmChannelId,
578
+ };
579
+ appendChannelMessage(dmChannelId, msg, { incrementUnread: selectedChannelId !== dmChannelId });
580
+ }
581
+ }, [addActivityEvent, appendChannelMessage, currentUser?.displayName, selectedChannelId]);
582
+
583
+ const { onlineUsers: allOnlineUsers, typingUsers, sendTyping, isConnected: isPresenceConnected } = usePresence({
584
+ currentUser: presenceUser,
585
+ onEvent: handlePresenceEvent,
586
+ workspaceId: effectiveActiveWorkspaceId ?? undefined,
587
+ });
588
+
589
+ // Keep local username for channel API calls
590
+ useEffect(() => {
591
+ if (typeof window !== 'undefined' && currentUser?.displayName) {
592
+ localStorage.setItem('relay_username', currentUser.displayName);
593
+ }
594
+ }, [currentUser?.displayName]);
595
+
596
+ // Filter online users by workspace membership (cloud mode only)
597
+ const { memberUsernames } = useWorkspaceMembers({
598
+ workspaceId: effectiveActiveWorkspaceId ?? undefined,
599
+ enabled: isCloudMode && !!effectiveActiveWorkspaceId,
600
+ });
601
+
602
+ // Filter online users to only show those with access to current workspace
603
+ const onlineUsers = useMemo(
604
+ () => filterOnlineUsersByWorkspace(allOnlineUsers, memberUsernames),
605
+ [allOnlineUsers, memberUsernames]
606
+ );
607
+
608
+ // User profile panel state
609
+ const [selectedUserProfile, setSelectedUserProfile] = useState<UserPresence | null>(null);
610
+ const [pendingMention, setPendingMention] = useState<string | undefined>();
611
+
612
+ // Agent profile panel state
613
+ const [selectedAgentProfile, setSelectedAgentProfile] = useState<Agent | null>(null);
614
+
615
+ // Agent summaries lookup
616
+ const agentSummariesMap = useMemo(() => {
617
+ const map = new Map<string, AgentSummary>();
618
+ for (const summary of data?.summaries ?? []) {
619
+ map.set(summary.agentName.toLowerCase(), summary);
620
+ }
621
+ return map;
622
+ }, [data?.summaries]);
623
+
624
+ // View mode state: 'local' (agents), 'fleet' (multi-server), 'channels' (channel messaging)
625
+ const [viewMode, setViewMode] = useState<'local' | 'fleet' | 'channels'>('local');
626
+
627
+ // Channel state for V1 channels UI
628
+ const [channelsList, setChannelsList] = useState<Channel[]>([]);
629
+ const [archivedChannelsList, setArchivedChannelsList] = useState<Channel[]>([]);
630
+ const [channelMessages, setChannelMessages] = useState<ChannelApiMessage[]>([]);
631
+ const [channelMessageMap, setChannelMessageMap] = useState<Record<string, ChannelApiMessage[]>>({});
632
+ const fetchedChannelsRef = useRef<Set<string>>(new Set()); // Track channels already fetched to prevent loops
633
+ const [isChannelsLoading, setIsChannelsLoading] = useState(false);
634
+ const [hasMoreMessages, setHasMoreMessages] = useState(false);
635
+ const [channelUnreadState, setChannelUnreadState] = useState<UnreadState | undefined>();
636
+
637
+ // Default channel IDs that should always be visible
638
+ const DEFAULT_CHANNEL_IDS = ['#general', '#engineering'];
639
+
640
+ const setChannelListsFromResponse = useCallback((response: { channels: Channel[]; archivedChannels?: Channel[] }) => {
641
+ const archived = [
642
+ ...(response.archivedChannels || []),
643
+ ...response.channels.filter(c => c.status === 'archived'),
644
+ ];
645
+ const apiActive = response.channels.filter(c => c.status !== 'archived');
646
+
647
+ // Merge with default channels to ensure #general is always visible
648
+ // Default channels are added if not present in API response
649
+ const apiChannelIds = new Set(apiActive.map(c => c.id));
650
+ const defaultChannelsToAdd: Channel[] = DEFAULT_CHANNEL_IDS
651
+ .filter(id => !apiChannelIds.has(id))
652
+ .map(id => ({
653
+ id,
654
+ name: id.replace('#', ''),
655
+ description: id === '#general' ? 'General discussion for all agents' : 'Engineering discussion',
656
+ visibility: 'public' as const,
657
+ memberCount: 0,
658
+ unreadCount: 0,
659
+ hasMentions: false,
660
+ createdAt: new Date().toISOString(),
661
+ status: 'active' as const,
662
+ createdBy: 'system',
663
+ isDm: false,
664
+ }));
665
+
666
+ setChannelsList([...defaultChannelsToAdd, ...apiActive]);
667
+ setArchivedChannelsList(archived);
668
+ }, []);
669
+
670
+ // Find selected channel object
671
+ const selectedChannel = useMemo(() => {
672
+ if (!selectedChannelId) return undefined;
673
+ return channelsList.find(c => c.id === selectedChannelId) ||
674
+ archivedChannelsList.find(c => c.id === selectedChannelId);
675
+ }, [selectedChannelId, channelsList, archivedChannelsList]);
676
+
677
+ // Project state for unified navigation (converted from workspaces)
678
+ const [projects, setProjects] = useState<Project[]>([]);
679
+ const [currentProject, setCurrentProject] = useState<string | undefined>();
680
+
681
+ // Spawn modal state
682
+ const [isSpawnModalOpen, setIsSpawnModalOpen] = useState(false);
683
+ const [isSpawning, setIsSpawning] = useState(false);
684
+ const [spawnError, setSpawnError] = useState<string | null>(null);
685
+
686
+ // Add workspace modal state
687
+ const [isAddWorkspaceOpen, setIsAddWorkspaceOpen] = useState(false);
688
+ const [isAddingWorkspace, setIsAddingWorkspace] = useState(false);
689
+ const [addWorkspaceError, setAddWorkspaceError] = useState<string | null>(null);
690
+
691
+ // Create channel modal state
692
+ const [isCreateChannelOpen, setIsCreateChannelOpen] = useState(false);
693
+ const [isCreatingChannel, setIsCreatingChannel] = useState(false);
694
+
695
+ // Invite to channel modal state
696
+ const [isInviteChannelOpen, setIsInviteChannelOpen] = useState(false);
697
+ const [inviteChannelTarget, setInviteChannelTarget] = useState<Channel | null>(null);
698
+ const [isInvitingToChannel, setIsInvitingToChannel] = useState(false);
699
+
700
+ // Command palette state
701
+ const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
702
+
703
+ // Settings state (theme, display, notifications)
704
+ const [settings, setSettings] = useState<Settings>(() => loadSettingsFromStorage());
705
+ const updateSettings = useCallback((updater: (prev: Settings) => Settings) => {
706
+ setSettings((prev) => updater(prev));
707
+ }, []);
708
+
709
+ // Full settings page state
710
+ const [isFullSettingsOpen, setIsFullSettingsOpen] = useState(false);
711
+ const [settingsInitialTab, setSettingsInitialTab] = useState<'dashboard' | 'workspace' | 'team' | 'billing'>('dashboard');
712
+
713
+ // Conversation history panel state
714
+ const [isHistoryOpen, setIsHistoryOpen] = useState(false);
715
+
716
+ // New conversation modal state
717
+ const [isNewConversationOpen, setIsNewConversationOpen] = useState(false);
718
+
719
+ // DM participant selections (human -> invited agents) and removals
720
+ const [dmSelectedAgentsByHuman, setDmSelectedAgentsByHuman] = useState<Record<string, string[]>>({});
721
+ const [dmRemovedAgentsByHuman, setDmRemovedAgentsByHuman] = useState<Record<string, string[]>>({});
722
+
723
+ // Log viewer panel state
724
+ const [logViewerAgent, setLogViewerAgent] = useState<Agent | null>(null);
725
+
726
+ // Trajectory panel state
727
+ const [isTrajectoryOpen, setIsTrajectoryOpen] = useState(false);
728
+ const {
729
+ steps: trajectorySteps,
730
+ status: trajectoryStatus,
731
+ history: trajectoryHistory,
732
+ isLoading: isTrajectoryLoading,
733
+ selectTrajectory,
734
+ selectedTrajectoryId,
735
+ } = useTrajectory({
736
+ autoPoll: isTrajectoryOpen, // Only poll when panel is open
737
+ });
738
+
739
+ // Get the title of the selected trajectory from history
740
+ const selectedTrajectoryTitle = useMemo(() => {
741
+ if (!selectedTrajectoryId) return null;
742
+ return trajectoryHistory.find(t => t.id === selectedTrajectoryId)?.title ?? null;
743
+ }, [selectedTrajectoryId, trajectoryHistory]);
744
+
745
+ // Recent repos tracking
746
+ const { recentRepos, addRecentRepo, getRecentProjects } = useRecentRepos();
747
+
748
+ // Workspace repos for multi-repo workspaces
749
+ const { repos: workspaceRepos, refetch: refetchWorkspaceRepos } = useWorkspaceRepos({
750
+ workspaceId: effectiveActiveWorkspaceId ?? undefined,
751
+ apiBaseUrl: '/api',
752
+ enabled: isCloudMode && !!effectiveActiveWorkspaceId,
753
+ });
754
+
755
+ // Reset channel state when switching workspaces
756
+ useEffect(() => {
757
+ setChannelMessageMap({});
758
+ setChannelMessages([]);
759
+ setSelectedChannelId(undefined);
760
+ }, [effectiveActiveWorkspaceId]);
761
+
762
+ // Coordinator panel state
763
+ const [isCoordinatorOpen, setIsCoordinatorOpen] = useState(false);
764
+
765
+ // Decision queue state
766
+ const [isDecisionQueueOpen, setIsDecisionQueueOpen] = useState(false);
767
+ const [decisions, setDecisions] = useState<Decision[]>([]);
768
+ const [decisionProcessing, setDecisionProcessing] = useState<Record<string, boolean>>({});
769
+
770
+ // Fleet overview state
771
+ const [isFleetViewActive, setIsFleetViewActive] = useState(false);
772
+ const [fleetServers, setFleetServers] = useState<ServerInfo[]>([]);
773
+
774
+ // Auth revocation notification state
775
+ const { toasts, addToast, dismissToast } = useToasts();
776
+ const [authRevokedAgents, setAuthRevokedAgents] = useState<Set<string>>(new Set());
777
+ const [selectedServerId, setSelectedServerId] = useState<string | undefined>();
778
+
779
+ // Task creation state (tasks are stored in beads, not local state)
780
+ const [isCreatingTask, setIsCreatingTask] = useState(false);
781
+
782
+ // Mobile sidebar state
783
+ const [isSidebarOpen, setIsSidebarOpen] = useState(false);
784
+
785
+ // Unread message notification state for mobile
786
+ const [hasUnreadMessages, setHasUnreadMessages] = useState(false);
787
+ const lastSeenMessageCountRef = useRef<number>(0);
788
+ const sidebarClosedRef = useRef<boolean>(true); // Track if sidebar is currently closed
789
+ const [dmSeenAt, setDmSeenAt] = useState<Map<string, number>>(new Map());
790
+ const lastNotifiedMessageIdRef = useRef<string | null>(null);
791
+
792
+ // Close sidebar when selecting an agent or project on mobile
793
+ const closeSidebarOnMobile = useCallback(() => {
794
+ if (window.innerWidth <= 768) {
795
+ setIsSidebarOpen(false);
796
+ }
797
+ }, []);
798
+
799
+ // Merge AI agents, human users, and local agents from linked daemons
800
+ const combinedAgents = useMemo(() => {
801
+ return mergeAgentsForDashboard({
802
+ agents: data?.agents,
803
+ users: data?.users,
804
+ localAgents,
805
+ });
806
+ }, [data?.agents, data?.users, localAgents]);
807
+
808
+ // Mark a DM conversation as seen (used for unread badges)
809
+ const markDmSeen = useCallback((username: string) => {
810
+ setDmSeenAt((prev) => {
811
+ const next = new Map(prev);
812
+ next.set(username.toLowerCase(), Date.now());
813
+ return next;
814
+ });
815
+ }, []);
816
+
817
+ // Agent state management
818
+ const {
819
+ agents,
820
+ groups,
821
+ selectedAgent,
822
+ selectAgent,
823
+ searchQuery,
824
+ setSearchQuery,
825
+ totalCount,
826
+ onlineCount,
827
+ needsAttentionCount,
828
+ } = useAgents({
829
+ agents: combinedAgents,
830
+ });
831
+
832
+ // Message state management
833
+ const {
834
+ messages,
835
+ threadMessages,
836
+ currentChannel,
837
+ setCurrentChannel,
838
+ currentThread,
839
+ setCurrentThread,
840
+ activeThreads,
841
+ totalUnreadThreadCount,
842
+ sendMessage,
843
+ isSending,
844
+ sendError,
845
+ } = useMessages({
846
+ messages: data?.messages ?? [],
847
+ senderName: currentUser?.displayName,
848
+ });
849
+
850
+ // Human context (DM inline view)
851
+ const currentHuman = useMemo(() => {
852
+ if (!currentChannel) return null;
853
+ return combinedAgents.find(
854
+ (a) => a.isHuman && a.name.toLowerCase() === currentChannel.toLowerCase()
855
+ ) || null;
856
+ }, [combinedAgents, currentChannel]);
857
+
858
+ const selectedDmAgents = useMemo(
859
+ () => (currentHuman ? dmSelectedAgentsByHuman[currentHuman.name] ?? [] : []),
860
+ [currentHuman, dmSelectedAgentsByHuman]
861
+ );
862
+ const removedDmAgents = useMemo(
863
+ () => (currentHuman ? dmRemovedAgentsByHuman[currentHuman.name] ?? [] : []),
864
+ [currentHuman, dmRemovedAgentsByHuman]
865
+ );
866
+
867
+ // Use DM hook for message filtering and deduplication
868
+ const { visibleMessages: dedupedVisibleMessages, participantAgents: dmParticipantAgents } = useDirectMessage({
869
+ currentHuman,
870
+ currentUserName: currentUser?.displayName ?? null,
871
+ messages,
872
+ agents,
873
+ selectedDmAgents,
874
+ removedDmAgents,
875
+ });
876
+
877
+ // For local mode: convert relay messages to channel message format
878
+ // Filter messages by channel (checking multiple fields for compatibility)
879
+ const localChannelMessages = useMemo((): ChannelApiMessage[] => {
880
+ if (effectiveActiveWorkspaceId || !selectedChannelId) return [];
881
+
882
+ // Filter messages that belong to this channel
883
+ const filtered = messages.filter(m => {
884
+ // Activity feed shows broadcasts (to='*')
885
+ if (selectedChannelId === ACTIVITY_FEED_ID) {
886
+ return m.to === '*' || m.isBroadcast;
887
+ }
888
+ // Check if message is explicitly for this channel (CHANNEL_MESSAGE format)
889
+ if (m.to === selectedChannelId) return true;
890
+ // Check channel property for channel messages
891
+ if (m.channel === selectedChannelId) return true;
892
+ // Legacy: messages with this channel as thread
893
+ if (m.thread === selectedChannelId) return true;
894
+ return false;
895
+ });
896
+
897
+ // Convert to ChannelMessage format
898
+ return filtered.map(m => ({
899
+ id: m.id,
900
+ channelId: selectedChannelId,
901
+ from: m.from,
902
+ fromEntityType: (m.from === 'Dashboard' || m.from === currentUser?.displayName) ? 'user' : 'agent' as const,
903
+ content: m.content,
904
+ timestamp: m.timestamp,
905
+ isRead: m.isRead ?? true,
906
+ threadId: m.thread !== selectedChannelId ? m.thread : undefined,
907
+ }));
908
+ }, [messages, selectedChannelId, effectiveActiveWorkspaceId, currentUser?.displayName]);
909
+
910
+ // Use local or cloud messages depending on mode
911
+ const effectiveChannelMessages = effectiveActiveWorkspaceId ? channelMessages : localChannelMessages;
912
+
913
+ // Extract human users from messages (users who are not agents)
914
+ // This enables @ mentioning other human users in cloud mode
915
+ const humanUsers = useMemo((): HumanUser[] => {
916
+ const agentNames = new Set(agents.map((a) => a.name.toLowerCase()));
917
+ const seenUsers = new Map<string, HumanUser>();
918
+
919
+ // Include current user if in cloud mode
920
+ if (currentUser) {
921
+ seenUsers.set(currentUser.displayName.toLowerCase(), {
922
+ username: currentUser.displayName,
923
+ avatarUrl: currentUser.avatarUrl,
924
+ });
925
+ }
926
+
927
+ // Extract unique human users from message senders
928
+ for (const msg of data?.messages ?? []) {
929
+ const sender = msg.from;
930
+ if (sender && isHumanSender(sender, agentNames) && !seenUsers.has(sender.toLowerCase())) {
931
+ seenUsers.set(sender.toLowerCase(), {
932
+ username: sender,
933
+ // Note: We don't have avatar URLs for users from messages
934
+ // unless we fetch them separately
935
+ });
936
+ }
937
+ }
938
+
939
+ return Array.from(seenUsers.values());
940
+ }, [data?.messages, agents, currentUser]);
941
+
942
+ // Unread counts for human conversations (DMs)
943
+ const humanUnreadCounts = useMemo(() => {
944
+ if (!currentUser) return {};
945
+
946
+ const counts: Record<string, number> = {};
947
+ const humanNameSet = new Set(
948
+ combinedAgents.filter((a) => a.isHuman).map((a) => a.name.toLowerCase())
949
+ );
950
+
951
+ for (const msg of data?.messages ?? []) {
952
+ const sender = msg.from;
953
+ const recipient = msg.to;
954
+ if (!sender || !recipient) continue;
955
+
956
+ const isToCurrentUser = recipient === currentUser.displayName;
957
+ const senderIsHuman = humanNameSet.has(sender.toLowerCase());
958
+ if (!isToCurrentUser || !senderIsHuman) continue;
959
+
960
+ const seenAt = dmSeenAt.get(sender.toLowerCase()) ?? 0;
961
+ const ts = new Date(msg.timestamp).getTime();
962
+ if (ts > seenAt) {
963
+ counts[sender] = (counts[sender] || 0) + 1;
964
+ }
965
+ }
966
+
967
+ return counts;
968
+ }, [combinedAgents, currentUser, data?.messages, dmSeenAt]);
969
+
970
+ // Mark DM as seen when actively viewing a human channel
971
+ useEffect(() => {
972
+ if (!currentUser || !currentChannel) return;
973
+ const humanNameSet = new Set(
974
+ combinedAgents.filter((a) => a.isHuman).map((a) => a.name.toLowerCase())
975
+ );
976
+ if (humanNameSet.has(currentChannel.toLowerCase())) {
977
+ markDmSeen(currentChannel);
978
+ }
979
+ }, [combinedAgents, currentChannel, currentUser, markDmSeen]);
980
+
981
+ // Track unread messages when sidebar is closed on mobile
982
+ useEffect(() => {
983
+ // Only track on mobile viewport
984
+ const isMobile = window.innerWidth <= 768;
985
+ if (!isMobile) {
986
+ setHasUnreadMessages(false);
987
+ return;
988
+ }
989
+
990
+ const messageCount = messages.length;
991
+
992
+ // If sidebar is closed and we have new messages since last seen
993
+ if (!isSidebarOpen && messageCount > lastSeenMessageCountRef.current) {
994
+ setHasUnreadMessages(true);
995
+ }
996
+
997
+ // Update the ref based on current sidebar state
998
+ sidebarClosedRef.current = !isSidebarOpen;
999
+ }, [messages.length, isSidebarOpen]);
1000
+
1001
+ // Clear unread state and update last seen count when sidebar opens
1002
+ useEffect(() => {
1003
+ if (isSidebarOpen) {
1004
+ setHasUnreadMessages(false);
1005
+ lastSeenMessageCountRef.current = messages.length;
1006
+ }
1007
+ }, [isSidebarOpen, messages.length]);
1008
+
1009
+ // Initialize last seen message count on mount
1010
+ useEffect(() => {
1011
+ lastSeenMessageCountRef.current = messages.length;
1012
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1013
+ }, []);
1014
+
1015
+ // Detect auth revocation messages and show notification
1016
+ useEffect(() => {
1017
+ if (!data?.messages) return;
1018
+
1019
+ for (const msg of data.messages) {
1020
+ // Check for auth_revoked control messages
1021
+ if (msg.content?.includes('auth_revoked') || msg.content?.includes('authentication_error')) {
1022
+ try {
1023
+ const parsed = JSON.parse(msg.content);
1024
+ if (parsed.type === 'auth_revoked' && parsed.agent) {
1025
+ const agentName = parsed.agent;
1026
+ if (!authRevokedAgents.has(agentName)) {
1027
+ setAuthRevokedAgents(prev => new Set([...prev, agentName]));
1028
+ addToast({
1029
+ type: 'error',
1030
+ title: 'Authentication Expired',
1031
+ message: `${agentName}'s API credentials have expired. Please reconnect.`,
1032
+ agentName,
1033
+ duration: 0, // Don't auto-dismiss
1034
+ action: {
1035
+ label: 'Reconnect',
1036
+ onClick: () => {
1037
+ window.location.href = '/providers';
1038
+ },
1039
+ },
1040
+ });
1041
+ }
1042
+ }
1043
+ } catch {
1044
+ // Not JSON, check for plain text auth error patterns
1045
+ if (msg.content?.includes('OAuth token') && msg.content?.includes('expired')) {
1046
+ const agentName = msg.from;
1047
+ if (agentName && !authRevokedAgents.has(agentName)) {
1048
+ setAuthRevokedAgents(prev => new Set([...prev, agentName]));
1049
+ addToast({
1050
+ type: 'error',
1051
+ title: 'Authentication Expired',
1052
+ message: `${agentName}'s API credentials have expired. Please reconnect.`,
1053
+ agentName,
1054
+ duration: 0,
1055
+ action: {
1056
+ label: 'Reconnect',
1057
+ onClick: () => {
1058
+ window.location.href = '/providers';
1059
+ },
1060
+ },
1061
+ });
1062
+ }
1063
+ }
1064
+ }
1065
+ }
1066
+ }
1067
+ }, [data?.messages, authRevokedAgents, addToast]);
1068
+
1069
+ // Check if fleet view is available
1070
+ const isFleetAvailable = Boolean(data?.fleet?.servers?.length) || workspaces.length > 0;
1071
+
1072
+ // Convert workspaces/repos to projects for unified navigation
1073
+ useEffect(() => {
1074
+ if (workspaces.length > 0) {
1075
+ // If we have repos for the active workspace, show each repo as a project folder
1076
+ if (workspaceRepos.length > 1 && effectiveActiveWorkspaceId) {
1077
+ const projectList: Project[] = workspaceRepos.map((repo) => ({
1078
+ id: repo.id,
1079
+ path: repo.githubFullName,
1080
+ name: repo.githubFullName.split('/').pop() || repo.githubFullName,
1081
+ agents: orchestratorAgents
1082
+ .filter((a) => a.workspaceId === effectiveActiveWorkspaceId)
1083
+ .map((a) => ({
1084
+ name: a.name,
1085
+ status: a.status === 'running' ? 'online' : 'offline',
1086
+ isSpawned: true,
1087
+ cli: a.provider,
1088
+ })) as Agent[],
1089
+ lead: undefined,
1090
+ }));
1091
+ setProjects(projectList);
1092
+ // Set first repo as current if none selected
1093
+ if (!currentProject || !projectList.find(p => p.id === currentProject)) {
1094
+ setCurrentProject(projectList[0]?.id);
1095
+ }
1096
+ } else {
1097
+ // Single repo or no repos fetched yet - show workspace as single project
1098
+ const projectList: Project[] = workspaces.map((workspace) => ({
1099
+ id: workspace.id,
1100
+ path: workspace.path,
1101
+ name: workspace.name,
1102
+ agents: orchestratorAgents
1103
+ .filter((a) => a.workspaceId === workspace.id)
1104
+ .map((a) => ({
1105
+ name: a.name,
1106
+ status: a.status === 'running' ? 'online' : 'offline',
1107
+ isSpawned: true,
1108
+ cli: a.provider,
1109
+ })) as Agent[],
1110
+ lead: undefined,
1111
+ }));
1112
+ setProjects(projectList);
1113
+ setCurrentProject(activeWorkspaceId);
1114
+ }
1115
+ }
1116
+ }, [workspaces, orchestratorAgents, activeWorkspaceId, workspaceRepos, effectiveActiveWorkspaceId, currentProject]);
1117
+
1118
+ // Fetch bridge/project data for multi-project mode
1119
+ useEffect(() => {
1120
+ if (workspaces.length > 0) return; // Skip if using orchestrator
1121
+
1122
+ const fetchProjects = async () => {
1123
+ const result = await api.getBridgeData();
1124
+ if (result.success && result.data) {
1125
+ // Bridge data returns { projects, messages, connected }
1126
+ const bridgeData = result.data as {
1127
+ projects?: Array<{
1128
+ id: string;
1129
+ name?: string;
1130
+ path: string;
1131
+ connected?: boolean;
1132
+ agents?: Array<{ name: string; status: string; task?: string; cli?: string }>;
1133
+ lead?: { name: string; connected: boolean };
1134
+ }>;
1135
+ connected?: boolean;
1136
+ currentProjectPath?: string;
1137
+ };
1138
+
1139
+ if (bridgeData.projects && bridgeData.projects.length > 0) {
1140
+ const projectList: Project[] = bridgeData.projects.map((p) => ({
1141
+ id: p.id,
1142
+ path: p.path,
1143
+ name: p.name || p.path.split('/').pop(),
1144
+ agents: (p.agents || []).map((a) => ({
1145
+ name: a.name,
1146
+ status: a.status === 'online' || a.status === 'active' ? 'online' : 'offline',
1147
+ currentTask: a.task,
1148
+ cli: a.cli,
1149
+ })) as Agent[],
1150
+ lead: p.lead,
1151
+ }));
1152
+ setProjects(projectList);
1153
+ // Set first project as current if none selected
1154
+ if (!currentProject && projectList.length > 0) {
1155
+ setCurrentProject(projectList[0].id);
1156
+ }
1157
+ }
1158
+ }
1159
+ };
1160
+
1161
+ // Fetch immediately on mount
1162
+ fetchProjects();
1163
+ // Poll for updates
1164
+ const interval = setInterval(fetchProjects, 5000);
1165
+ return () => clearInterval(interval);
1166
+ }, [workspaces.length, currentProject]);
1167
+
1168
+ // Bridge-level agents (like Architect) that should be shown separately
1169
+ const BRIDGE_AGENT_NAMES = ['architect'];
1170
+
1171
+ // Separate bridge-level agents from regular project agents
1172
+ const { bridgeAgents, projectAgents } = useMemo(() => {
1173
+ const bridge: Agent[] = [];
1174
+ const project: Agent[] = [];
1175
+
1176
+ for (const agent of agents) {
1177
+ if (BRIDGE_AGENT_NAMES.includes(agent.name.toLowerCase())) {
1178
+ bridge.push(agent);
1179
+ } else {
1180
+ project.push(agent);
1181
+ }
1182
+ }
1183
+
1184
+ return { bridgeAgents: bridge, projectAgents: project };
1185
+ }, [agents]);
1186
+
1187
+ // Merge local daemon agents into their project when we have bridge projects
1188
+ // This prevents agents from appearing under "Local" instead of their project folder
1189
+ const mergedProjects = useMemo(() => {
1190
+ if (projects.length === 0) return projects;
1191
+
1192
+ // Get local agent names (excluding bridge agents)
1193
+ const localAgentNames = new Set(projectAgents.map((a) => a.name.toLowerCase()));
1194
+ if (localAgentNames.size === 0) return projects;
1195
+
1196
+ // Find the current project (the one whose daemon we're connected to)
1197
+ // This is typically the first project or the one marked as current
1198
+ return projects.map((project, index) => {
1199
+ // Merge local agents into the current/first project
1200
+ // Local agents should appear in their actual project, not "Local"
1201
+ const isCurrentDaemonProject = index === 0 || project.id === currentProject;
1202
+
1203
+ if (isCurrentDaemonProject) {
1204
+ // Merge local agents with project agents, avoiding duplicates
1205
+ const existingNames = new Set(project.agents.map((a) => a.name.toLowerCase()));
1206
+ const newAgents = projectAgents.filter((a) => !existingNames.has(a.name.toLowerCase()));
1207
+
1208
+ return {
1209
+ ...project,
1210
+ agents: [...project.agents, ...newAgents],
1211
+ };
1212
+ }
1213
+
1214
+ return project;
1215
+ });
1216
+ }, [projects, projectAgents, currentProject]);
1217
+
1218
+ // Determine if local agents should be shown separately
1219
+ // Only show "Local" folder if we don't have bridge projects to merge them into
1220
+ // But always include human users so they appear in the sidebar for DM
1221
+ const localAgentsForSidebar = useMemo(() => {
1222
+ // Human users should always be shown in sidebar for DM access
1223
+ const humanUsers = projectAgents.filter(a => a.isHuman);
1224
+
1225
+ if (mergedProjects.length > 0) {
1226
+ // Don't show AI agents separately - they're merged into projects
1227
+ // But keep human users visible for DM conversations
1228
+ return humanUsers;
1229
+ }
1230
+ return projectAgents;
1231
+ }, [mergedProjects, projectAgents]);
1232
+
1233
+ // Handle workspace selection
1234
+ const handleWorkspaceSelect = useCallback(async (workspace: Workspace) => {
1235
+ try {
1236
+ await switchWorkspace(workspace.id);
1237
+ } catch (err) {
1238
+ console.error('Failed to switch workspace:', err);
1239
+ }
1240
+ }, [switchWorkspace]);
1241
+
1242
+ // Handle add workspace
1243
+ const handleAddWorkspace = useCallback(async (path: string, name?: string) => {
1244
+ setIsAddingWorkspace(true);
1245
+ setAddWorkspaceError(null);
1246
+ try {
1247
+ await addWorkspace(path, name);
1248
+ setIsAddWorkspaceOpen(false);
1249
+ } catch (err) {
1250
+ setAddWorkspaceError(err instanceof Error ? err.message : 'Failed to add workspace');
1251
+ throw err;
1252
+ } finally {
1253
+ setIsAddingWorkspace(false);
1254
+ }
1255
+ }, [addWorkspace]);
1256
+
1257
+ // Handle project selection (also switches workspace if using orchestrator)
1258
+ const handleProjectSelect = useCallback((project: Project) => {
1259
+ setCurrentProject(project.id);
1260
+ // Switch to DM view mode and clear channel selection
1261
+ setViewMode('local');
1262
+ setSelectedChannelId(undefined);
1263
+
1264
+ // Track as recently accessed
1265
+ addRecentRepo(project);
1266
+
1267
+ // Switch workspace if using orchestrator
1268
+ if (workspaces.length > 0) {
1269
+ switchWorkspace(project.id).catch((err) => {
1270
+ console.error('Failed to switch workspace:', err);
1271
+ });
1272
+ }
1273
+
1274
+ if (project.agents.length > 0) {
1275
+ selectAgent(project.agents[0].name);
1276
+ setCurrentChannel(project.agents[0].name);
1277
+ }
1278
+ closeSidebarOnMobile();
1279
+ }, [selectAgent, setCurrentChannel, closeSidebarOnMobile, workspaces.length, switchWorkspace, addRecentRepo]);
1280
+
1281
+ // Handle agent selection
1282
+ const handleAgentSelect = useCallback((agent: Agent) => {
1283
+ // Switch to DM view mode and clear channel selection
1284
+ setViewMode('local');
1285
+ setSelectedChannelId(undefined);
1286
+ selectAgent(agent.name);
1287
+ setCurrentChannel(agent.name);
1288
+ closeSidebarOnMobile();
1289
+ }, [selectAgent, setCurrentChannel, closeSidebarOnMobile]);
1290
+
1291
+ // Handle spawn button click
1292
+ const handleSpawnClick = useCallback(() => {
1293
+ setSpawnError(null);
1294
+ setIsSpawnModalOpen(true);
1295
+ }, []);
1296
+
1297
+ // Handle settings click - opens full settings page
1298
+ const handleSettingsClick = useCallback(() => {
1299
+ setSettingsInitialTab('dashboard');
1300
+ setIsFullSettingsOpen(true);
1301
+ }, []);
1302
+
1303
+ // Handle workspace settings click - opens full settings page with workspace tab
1304
+ const handleWorkspaceSettingsClick = useCallback(() => {
1305
+ setSettingsInitialTab('workspace');
1306
+ setIsFullSettingsOpen(true);
1307
+ }, []);
1308
+
1309
+ // Handle billing click - opens full settings page with billing tab
1310
+ const handleBillingClick = useCallback(() => {
1311
+ setSettingsInitialTab('billing');
1312
+ setIsFullSettingsOpen(true);
1313
+ }, []);
1314
+
1315
+ // Handle history click
1316
+ const handleHistoryClick = useCallback(() => {
1317
+ setIsHistoryOpen(true);
1318
+ }, []);
1319
+
1320
+ // Handle new conversation click
1321
+ const handleNewConversationClick = useCallback(() => {
1322
+ setIsNewConversationOpen(true);
1323
+ }, []);
1324
+
1325
+ // Handle coordinator click
1326
+ const handleCoordinatorClick = useCallback(() => {
1327
+ setIsCoordinatorOpen(true);
1328
+ }, []);
1329
+
1330
+ // Open a DM with a human user from the sidebar
1331
+ const handleHumanSelect = useCallback((human: Agent) => {
1332
+ // Switch to DM view mode and clear channel selection
1333
+ setViewMode('local');
1334
+ setSelectedChannelId(undefined);
1335
+ setCurrentChannel(human.name);
1336
+ markDmSeen(human.name);
1337
+ closeSidebarOnMobile();
1338
+ }, [closeSidebarOnMobile, markDmSeen, setCurrentChannel]);
1339
+
1340
+ // Handle channel member click - switch to DM with that member
1341
+ const handleChannelMemberClick = useCallback((memberId: string, entityType: 'user' | 'agent') => {
1342
+ // Don't navigate to self
1343
+ if (memberId === currentUser?.displayName) return;
1344
+
1345
+ // Switch from channel view to local (DM) view
1346
+ setViewMode('local');
1347
+ setSelectedChannelId(undefined);
1348
+
1349
+ // Select the agent or user
1350
+ if (entityType === 'agent') {
1351
+ selectAgent(memberId);
1352
+ setCurrentChannel(memberId);
1353
+ } else {
1354
+ // For users, just set the channel
1355
+ setCurrentChannel(memberId);
1356
+ }
1357
+
1358
+ closeSidebarOnMobile();
1359
+ }, [currentUser?.displayName, selectAgent, setCurrentChannel, closeSidebarOnMobile]);
1360
+
1361
+ // =============================================================================
1362
+ // Channel V1 Handlers
1363
+ // =============================================================================
1364
+
1365
+ // Default channels that should always be visible - stable reference
1366
+ const defaultChannels = useMemo<Channel[]>(() => [
1367
+ {
1368
+ id: '#general',
1369
+ name: 'general',
1370
+ description: 'General discussion for all agents',
1371
+ visibility: 'public',
1372
+ memberCount: 0,
1373
+ unreadCount: 0,
1374
+ hasMentions: false,
1375
+ createdAt: '2024-01-01T00:00:00.000Z', // Static date for stability
1376
+ status: 'active',
1377
+ createdBy: 'system',
1378
+ isDm: false,
1379
+ },
1380
+ {
1381
+ id: '#engineering',
1382
+ name: 'engineering',
1383
+ description: 'Engineering discussion',
1384
+ visibility: 'public',
1385
+ memberCount: 0,
1386
+ unreadCount: 0,
1387
+ hasMentions: false,
1388
+ createdAt: '2024-01-01T00:00:00.000Z', // Static date for stability
1389
+ status: 'active',
1390
+ createdBy: 'system',
1391
+ isDm: false,
1392
+ },
1393
+ ], []);
1394
+
1395
+ // Load channels on mount (they're always visible in sidebar, collapsed by default)
1396
+ useEffect(() => {
1397
+ // Not in cloud mode or no workspace - show default channels only
1398
+ if (!isCloudMode || !effectiveActiveWorkspaceId) {
1399
+ setChannelsList(defaultChannels);
1400
+ setArchivedChannelsList([]);
1401
+ return;
1402
+ }
1403
+
1404
+ // Cloud mode with workspace - fetch from API and merge with defaults
1405
+ setChannelsList(defaultChannels);
1406
+ setArchivedChannelsList([]);
1407
+ setIsChannelsLoading(true);
1408
+
1409
+ const fetchChannels = async () => {
1410
+ try {
1411
+ const response = await listChannels(effectiveActiveWorkspaceId);
1412
+ setChannelListsFromResponse(response);
1413
+ } catch (err) {
1414
+ console.error('Failed to fetch channels:', err);
1415
+ } finally {
1416
+ setIsChannelsLoading(false);
1417
+ }
1418
+ };
1419
+
1420
+ fetchChannels();
1421
+ }, [effectiveActiveWorkspaceId, isCloudMode, defaultChannels, setChannelListsFromResponse]);
1422
+
1423
+ // Load messages when a channel is selected (persisted + live)
1424
+ useEffect(() => {
1425
+ if (!selectedChannelId || viewMode !== 'channels') return;
1426
+
1427
+ // Check if we already have messages cached
1428
+ const existing = channelMessageMap[selectedChannelId] ?? [];
1429
+ if (existing.length > 0) {
1430
+ setChannelMessages(existing);
1431
+ setHasMoreMessages(false);
1432
+ } else if (!fetchedChannelsRef.current.has(selectedChannelId)) {
1433
+ // Only fetch if we haven't already fetched this channel (prevents infinite loop)
1434
+ fetchedChannelsRef.current.add(selectedChannelId);
1435
+ (async () => {
1436
+ try {
1437
+ const response = await getMessages(effectiveActiveWorkspaceId || 'local', selectedChannelId, { limit: 200 });
1438
+ setChannelMessageMap(prev => ({ ...prev, [selectedChannelId]: response.messages }));
1439
+ setChannelMessages(response.messages);
1440
+ setHasMoreMessages(response.hasMore);
1441
+ } catch (err) {
1442
+ console.error('Failed to fetch channel messages:', err);
1443
+ setChannelMessages([]);
1444
+ setHasMoreMessages(false);
1445
+ }
1446
+ })();
1447
+ } else {
1448
+ // Already fetched but no messages - show empty state
1449
+ setChannelMessages([]);
1450
+ setHasMoreMessages(false);
1451
+ }
1452
+
1453
+ setChannelUnreadState(undefined);
1454
+ setChannelsList(prev =>
1455
+ prev.map(c =>
1456
+ c.id === selectedChannelId ? { ...c, unreadCount: 0, hasMentions: false } : c
1457
+ )
1458
+ );
1459
+ }, [selectedChannelId, viewMode, effectiveActiveWorkspaceId]); // Removed channelMessageMap to prevent infinite loop
1460
+
1461
+ // Channel selection handler - also joins the channel in local mode
1462
+ const handleSelectChannel = useCallback(async (channel: Channel) => {
1463
+ setSelectedChannelId(channel.id);
1464
+ closeSidebarOnMobile();
1465
+
1466
+ // Join the channel via the daemon (needed for local mode)
1467
+ // This ensures the user is a member before sending messages
1468
+ try {
1469
+ const { joinChannel: joinChannelApi } = await import('./channels');
1470
+ await joinChannelApi(effectiveActiveWorkspaceId || 'local', channel.id);
1471
+ } catch (err) {
1472
+ console.error('Failed to join channel:', err);
1473
+ }
1474
+ }, [closeSidebarOnMobile, effectiveActiveWorkspaceId]);
1475
+
1476
+ // Create channel handler - opens the create channel modal
1477
+ const handleCreateChannel = useCallback(() => {
1478
+ setIsCreateChannelOpen(true);
1479
+ }, []);
1480
+
1481
+ // Handler for creating a new channel via API
1482
+ const handleCreateChannelSubmit = useCallback(async (request: CreateChannelRequest) => {
1483
+ if (!effectiveActiveWorkspaceId) return;
1484
+ setIsCreatingChannel(true);
1485
+ try {
1486
+ const result = await createChannel(effectiveActiveWorkspaceId, request);
1487
+ // Refresh channels list after successful creation
1488
+ const response = await listChannels(effectiveActiveWorkspaceId);
1489
+ setChannelListsFromResponse(response);
1490
+ if (result.channel?.id) {
1491
+ setSelectedChannelId(result.channel.id);
1492
+ }
1493
+ setIsCreateChannelOpen(false);
1494
+ } catch (err) {
1495
+ console.error('Failed to create channel:', err);
1496
+ // Keep modal open on error so user can retry
1497
+ } finally {
1498
+ setIsCreatingChannel(false);
1499
+ }
1500
+ }, [effectiveActiveWorkspaceId]);
1501
+
1502
+ // Handler for opening the invite to channel modal
1503
+ const handleInviteToChannel = useCallback((channel: Channel) => {
1504
+ setInviteChannelTarget(channel);
1505
+ setIsInviteChannelOpen(true);
1506
+ }, []);
1507
+
1508
+ // Handler for inviting members to a channel
1509
+ // Note: InviteToChannelModal is given agents as availableMembers, so all invitees are agents
1510
+ const handleInviteSubmit = useCallback(async (members: string[]) => {
1511
+ if (!inviteChannelTarget) return;
1512
+ setIsInvitingToChannel(true);
1513
+ try {
1514
+ // Call the invite API endpoint with CSRF token
1515
+ const csrfToken = getCsrfToken();
1516
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
1517
+ if (csrfToken) {
1518
+ headers['X-CSRF-Token'] = csrfToken;
1519
+ }
1520
+
1521
+ // Send invites with type info - all members from invite modal are agents
1522
+ const invites = members.map(name => ({ id: name, type: 'agent' as const }));
1523
+
1524
+ const response = await fetch('/api/channels/invite', {
1525
+ method: 'POST',
1526
+ headers,
1527
+ credentials: 'include',
1528
+ body: JSON.stringify({
1529
+ channel: inviteChannelTarget.name,
1530
+ invites,
1531
+ workspaceId: effectiveActiveWorkspaceId,
1532
+ }),
1533
+ });
1534
+ if (!response.ok) {
1535
+ throw new Error('Failed to invite members');
1536
+ }
1537
+ setIsInviteChannelOpen(false);
1538
+ setInviteChannelTarget(null);
1539
+ } catch (err) {
1540
+ console.error('Failed to invite to channel:', err);
1541
+ } finally {
1542
+ setIsInvitingToChannel(false);
1543
+ }
1544
+ }, [inviteChannelTarget, effectiveActiveWorkspaceId]);
1545
+
1546
+ // Join channel handler
1547
+ const handleJoinChannel = useCallback(async (channelId: string) => {
1548
+ if (!effectiveActiveWorkspaceId) return;
1549
+ try {
1550
+ const { joinChannel } = await import('./channels');
1551
+ await joinChannel(effectiveActiveWorkspaceId, channelId);
1552
+ // Refresh channels list
1553
+ const response = await listChannels(effectiveActiveWorkspaceId);
1554
+ setChannelListsFromResponse(response);
1555
+ } catch (err) {
1556
+ console.error('Failed to join channel:', err);
1557
+ }
1558
+ }, [effectiveActiveWorkspaceId, setChannelListsFromResponse]);
1559
+
1560
+ // Leave channel handler
1561
+ const handleLeaveChannel = useCallback(async (channel: Channel) => {
1562
+ if (!effectiveActiveWorkspaceId) return;
1563
+ try {
1564
+ const { leaveChannel } = await import('./channels');
1565
+ await leaveChannel(effectiveActiveWorkspaceId, channel.id);
1566
+ // Clear selection if leaving current channel
1567
+ if (selectedChannelId === channel.id) {
1568
+ setSelectedChannelId(undefined);
1569
+ }
1570
+ // Refresh channels list
1571
+ const response = await listChannels(effectiveActiveWorkspaceId);
1572
+ setChannelListsFromResponse(response);
1573
+ } catch (err) {
1574
+ console.error('Failed to leave channel:', err);
1575
+ }
1576
+ }, [effectiveActiveWorkspaceId, selectedChannelId, setChannelListsFromResponse]);
1577
+
1578
+ // Show members panel handler
1579
+ const handleShowMembers = useCallback(async () => {
1580
+ if (!selectedChannel || !effectiveActiveWorkspaceId) return;
1581
+ try {
1582
+ const members = await getChannelMembers(effectiveActiveWorkspaceId, selectedChannel.id);
1583
+ setChannelMembers(members);
1584
+ setShowMemberPanel(true);
1585
+ } catch (err) {
1586
+ console.error('Failed to load channel members:', err);
1587
+ }
1588
+ }, [selectedChannel, effectiveActiveWorkspaceId]);
1589
+
1590
+ // Remove member handler
1591
+ const handleRemoveMember = useCallback(async (memberId: string, memberType: 'user' | 'agent') => {
1592
+ if (!selectedChannel || !effectiveActiveWorkspaceId) return;
1593
+ try {
1594
+ await removeChannelMember(effectiveActiveWorkspaceId, selectedChannel.id, memberId, memberType);
1595
+ // Refresh members list
1596
+ const members = await getChannelMembers(effectiveActiveWorkspaceId, selectedChannel.id);
1597
+ setChannelMembers(members);
1598
+ } catch (err) {
1599
+ console.error('Failed to remove member:', err);
1600
+ }
1601
+ }, [selectedChannel, effectiveActiveWorkspaceId]);
1602
+
1603
+ // Add member handler (for MemberManagementPanel)
1604
+ const handleAddMember = useCallback(async (memberId: string, memberType: 'user' | 'agent', _role: 'admin' | 'member' | 'read_only') => {
1605
+ if (!selectedChannel || !effectiveActiveWorkspaceId) return;
1606
+ try {
1607
+ const csrfToken = getCsrfToken();
1608
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
1609
+ if (csrfToken) {
1610
+ headers['X-CSRF-Token'] = csrfToken;
1611
+ }
1612
+
1613
+ const response = await fetch('/api/channels/invite', {
1614
+ method: 'POST',
1615
+ headers,
1616
+ credentials: 'include',
1617
+ body: JSON.stringify({
1618
+ channel: selectedChannel.name,
1619
+ invites: [{ id: memberId, type: memberType }],
1620
+ workspaceId: effectiveActiveWorkspaceId,
1621
+ }),
1622
+ });
1623
+
1624
+ if (!response.ok) {
1625
+ throw new Error('Failed to add member');
1626
+ }
1627
+
1628
+ // Refresh members list
1629
+ const members = await getChannelMembers(effectiveActiveWorkspaceId, selectedChannel.id);
1630
+ setChannelMembers(members);
1631
+ } catch (err) {
1632
+ console.error('Failed to add member:', err);
1633
+ }
1634
+ }, [selectedChannel, effectiveActiveWorkspaceId]);
1635
+
1636
+ // Archive channel handler
1637
+ const handleArchiveChannel = useCallback(async (channel: Channel) => {
1638
+ if (!effectiveActiveWorkspaceId) return;
1639
+ try {
1640
+ const { archiveChannel } = await import('./channels');
1641
+ await archiveChannel(effectiveActiveWorkspaceId, channel.id);
1642
+ // Clear selection if archiving current channel
1643
+ if (selectedChannelId === channel.id) {
1644
+ setSelectedChannelId(undefined);
1645
+ }
1646
+ // Refresh channels list
1647
+ const response = await listChannels(effectiveActiveWorkspaceId);
1648
+ setChannelListsFromResponse(response);
1649
+ } catch (err) {
1650
+ console.error('Failed to archive channel:', err);
1651
+ }
1652
+ }, [effectiveActiveWorkspaceId, selectedChannelId, setChannelListsFromResponse]);
1653
+
1654
+ // Unarchive channel handler
1655
+ const handleUnarchiveChannel = useCallback(async (channel: Channel) => {
1656
+ if (!effectiveActiveWorkspaceId) return;
1657
+ try {
1658
+ const { unarchiveChannel } = await import('./channels');
1659
+ await unarchiveChannel(effectiveActiveWorkspaceId, channel.id);
1660
+ // Refresh channels list
1661
+ const response = await listChannels(effectiveActiveWorkspaceId);
1662
+ setChannelListsFromResponse(response);
1663
+ } catch (err) {
1664
+ console.error('Failed to unarchive channel:', err);
1665
+ }
1666
+ }, [effectiveActiveWorkspaceId, setChannelListsFromResponse]);
1667
+
1668
+ // Send message to channel handler
1669
+ const handleSendChannelMessage = useCallback(async (content: string, threadId?: string) => {
1670
+ if (!selectedChannelId) return;
1671
+
1672
+ const senderName = currentUser?.displayName || 'Dashboard';
1673
+ const optimisticMessage: ChannelApiMessage = {
1674
+ id: `local-${Date.now()}`,
1675
+ channelId: selectedChannelId,
1676
+ from: senderName,
1677
+ fromEntityType: 'user',
1678
+ content,
1679
+ timestamp: new Date().toISOString(),
1680
+ threadId,
1681
+ isRead: true,
1682
+ };
1683
+
1684
+ // Optimistic append; daemon will echo back via WS
1685
+ appendChannelMessage(selectedChannelId, optimisticMessage, { incrementUnread: false });
1686
+
1687
+ try {
1688
+ await sendChannelApiMessage(
1689
+ effectiveActiveWorkspaceId || 'local',
1690
+ selectedChannelId,
1691
+ { content, threadId }
1692
+ );
1693
+ } catch (err) {
1694
+ console.error('Failed to send channel message:', err);
1695
+ }
1696
+ }, [effectiveActiveWorkspaceId, selectedChannelId, currentUser?.displayName, appendChannelMessage]);
1697
+
1698
+ // Load more messages (pagination) handler
1699
+ const handleLoadMoreMessages = useCallback(async () => {
1700
+ // Pagination not yet supported for daemon channels
1701
+ return;
1702
+ }, []);
1703
+
1704
+ // Mark channel as read handler (with debouncing via useRef)
1705
+ const markReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
1706
+ const handleMarkChannelRead = useCallback((channelId: string) => {
1707
+ if (!effectiveActiveWorkspaceId) return;
1708
+
1709
+ // Clear existing timeout to debounce
1710
+ if (markReadTimeoutRef.current) {
1711
+ clearTimeout(markReadTimeoutRef.current);
1712
+ }
1713
+
1714
+ // Debounce the markRead call (500ms delay)
1715
+ markReadTimeoutRef.current = setTimeout(async () => {
1716
+ try {
1717
+ await markRead(effectiveActiveWorkspaceId, channelId);
1718
+ // Update local unread state
1719
+ setChannelUnreadState(undefined);
1720
+ // Update channel list unread counts
1721
+ setChannelsList(prev => prev.map(c =>
1722
+ c.id === channelId ? { ...c, unreadCount: 0, hasMentions: false } : c
1723
+ ));
1724
+ } catch (err) {
1725
+ console.error('Failed to mark channel as read:', err);
1726
+ }
1727
+ }, 500);
1728
+ }, [effectiveActiveWorkspaceId]);
1729
+
1730
+ // Auto-mark channel as read when viewing it
1731
+ useEffect(() => {
1732
+ if (!selectedChannelId || !channelUnreadState || channelUnreadState.count === 0) return;
1733
+ if (viewMode !== 'channels') return;
1734
+
1735
+ // Mark as read when channel is viewed and has unread messages
1736
+ handleMarkChannelRead(selectedChannelId);
1737
+ }, [selectedChannelId, channelUnreadState, viewMode, handleMarkChannelRead]);
1738
+
1739
+ // Cleanup markRead timeout on unmount
1740
+ useEffect(() => {
1741
+ return () => {
1742
+ if (markReadTimeoutRef.current) {
1743
+ clearTimeout(markReadTimeoutRef.current);
1744
+ }
1745
+ };
1746
+ }, []);
1747
+
1748
+ const handleDmAgentToggle = useCallback((agentName: string) => {
1749
+ if (!currentHuman) return;
1750
+ const humanName = currentHuman.name;
1751
+ const isSelected = (dmSelectedAgentsByHuman[humanName] ?? []).includes(agentName);
1752
+
1753
+ setDmSelectedAgentsByHuman((prev) => {
1754
+ const currentList = prev[humanName] ?? [];
1755
+ const nextList = isSelected
1756
+ ? currentList.filter((a) => a !== agentName)
1757
+ : [...currentList, agentName];
1758
+ return { ...prev, [humanName]: nextList };
1759
+ });
1760
+
1761
+ setDmRemovedAgentsByHuman((prev) => {
1762
+ const currentList = prev[humanName] ?? [];
1763
+ if (isSelected) {
1764
+ // Mark as removed so derived participants don't auto-readd
1765
+ return currentList.includes(agentName)
1766
+ ? prev
1767
+ : { ...prev, [humanName]: [...currentList, agentName] };
1768
+ }
1769
+ // Re-adding clears removal
1770
+ return { ...prev, [humanName]: currentList.filter((a) => a !== agentName) };
1771
+ });
1772
+ }, [currentHuman, dmSelectedAgentsByHuman]);
1773
+
1774
+ const handleDmSend = useCallback(async (_to: string, content: string): Promise<boolean> => {
1775
+ if (!currentHuman) return false;
1776
+ const humanName = currentHuman.name;
1777
+
1778
+ // Always send to the human
1779
+ await sendMessage(humanName, content);
1780
+
1781
+ // Only send to agents if they were explicitly selected for this conversation
1782
+ // Don't send to agents in pure 1:1 human conversations
1783
+ if (selectedDmAgents.length > 0) {
1784
+ for (const agent of selectedDmAgents) {
1785
+ await sendMessage(agent, content);
1786
+ }
1787
+ }
1788
+
1789
+ return true;
1790
+ }, [currentHuman, selectedDmAgents, sendMessage]);
1791
+
1792
+ const dmInviteCommands = useMemo(() => {
1793
+ if (!currentHuman) return [];
1794
+ return agents
1795
+ .filter((a) => !a.isHuman)
1796
+ .map((agent) => {
1797
+ const isSelected = (dmSelectedAgentsByHuman[currentHuman.name] ?? []).includes(agent.name);
1798
+ return {
1799
+ id: `dm-toggle-${currentHuman.name}-${agent.name}`,
1800
+ label: `${isSelected ? 'Remove' : 'Invite'} ${agent.name} in DM`,
1801
+ description: `DM with ${currentHuman.name}`,
1802
+ category: 'actions' as const,
1803
+ action: () => handleDmAgentToggle(agent.name),
1804
+ };
1805
+ });
1806
+ }, [agents, currentHuman, dmSelectedAgentsByHuman, handleDmAgentToggle]);
1807
+
1808
+ // Channel commands for command palette
1809
+ const channelCommands = useMemo(() => {
1810
+ const commands: Array<{
1811
+ id: string;
1812
+ label: string;
1813
+ description?: string;
1814
+ category: 'channels';
1815
+ shortcut?: string;
1816
+ action: () => void;
1817
+ }> = [];
1818
+
1819
+ // Switch to channels view
1820
+ commands.push({
1821
+ id: 'channels-view',
1822
+ label: 'Go to Channels',
1823
+ description: 'Switch to channel messaging view',
1824
+ category: 'channels',
1825
+ shortcut: '⌘⇧C',
1826
+ action: () => {
1827
+ setViewMode('channels');
1828
+ },
1829
+ });
1830
+
1831
+ // Create new channel
1832
+ commands.push({
1833
+ id: 'channels-create',
1834
+ label: 'Create Channel',
1835
+ description: 'Create a new messaging channel',
1836
+ category: 'channels',
1837
+ action: () => {
1838
+ setViewMode('channels');
1839
+ handleCreateChannel();
1840
+ },
1841
+ });
1842
+
1843
+ // Add each channel as a quick-switch command
1844
+ channelsList.forEach((channel) => {
1845
+ const unreadBadge = channel.unreadCount > 0 ? ` (${channel.unreadCount} unread)` : '';
1846
+ commands.push({
1847
+ id: `channel-switch-${channel.id}`,
1848
+ label: channel.isDm ? `@${channel.name}` : `#${channel.name}`,
1849
+ description: channel.description || `Switch to ${channel.isDm ? 'DM' : 'channel'}${unreadBadge}`,
1850
+ category: 'channels',
1851
+ action: () => {
1852
+ setViewMode('channels');
1853
+ setSelectedChannelId(channel.id);
1854
+ },
1855
+ });
1856
+ });
1857
+
1858
+ return commands;
1859
+ }, [channelsList, handleCreateChannel]);
1860
+
1861
+ // Handle send from new conversation modal - select the channel after sending
1862
+ const handleNewConversationSend = useCallback(async (to: string, content: string): Promise<boolean> => {
1863
+ const success = await sendMessage(to, content);
1864
+ if (success) {
1865
+ // Switch to the channel we just messaged
1866
+ if (to === '*') {
1867
+ selectAgent(null);
1868
+ setSelectedChannelId(ACTIVITY_FEED_ID);
1869
+ setViewMode('channels');
1870
+ } else {
1871
+ const targetAgent = agents.find((a) => a.name === to);
1872
+ if (targetAgent) {
1873
+ selectAgent(targetAgent.name);
1874
+ setCurrentChannel(targetAgent.name);
1875
+ } else {
1876
+ setCurrentChannel(to);
1877
+ }
1878
+ }
1879
+ }
1880
+ return success;
1881
+ }, [sendMessage, selectAgent, setCurrentChannel, agents]);
1882
+
1883
+ // Handle server reconnect (restart workspace)
1884
+ const handleServerReconnect = useCallback(async (serverId: string) => {
1885
+ if (isCloudMode) {
1886
+ try {
1887
+ const result = await cloudApi.restartWorkspace(serverId);
1888
+ if (result.success) {
1889
+ // Update the fleet servers state to show the server is restarting
1890
+ setFleetServers(prev => prev.map(s =>
1891
+ s.id === serverId ? { ...s, status: 'connecting' as const } : s
1892
+ ));
1893
+ // Refresh cloud workspaces after a short delay to get updated status
1894
+ setTimeout(async () => {
1895
+ try {
1896
+ const workspacesResult = await cloudApi.getWorkspaceSummary();
1897
+ if (workspacesResult.success && workspacesResult.data.workspaces) {
1898
+ setCloudWorkspaces(workspacesResult.data.workspaces);
1899
+ }
1900
+ } catch (err) {
1901
+ console.error('Failed to refresh workspaces after reconnect:', err);
1902
+ }
1903
+ }, 2000);
1904
+ } else {
1905
+ console.error('Failed to restart workspace:', result.error);
1906
+ }
1907
+ } catch (err) {
1908
+ console.error('Failed to reconnect to server:', err);
1909
+ }
1910
+ } else {
1911
+ // For orchestrator mode, attempt to reconnect by removing and re-adding the workspace
1912
+ console.warn('Server reconnect not fully supported in orchestrator mode');
1913
+ // Refresh the workspace list as a fallback
1914
+ // The orchestrator's WebSocket will handle reconnection automatically
1915
+ }
1916
+ }, [isCloudMode]);
1917
+
1918
+ // Handle spawn agent
1919
+ const handleSpawn = useCallback(async (config: SpawnConfig): Promise<boolean> => {
1920
+ setIsSpawning(true);
1921
+ setSpawnError(null);
1922
+ try {
1923
+ // Use orchestrator if workspaces are available
1924
+ if (workspaces.length > 0 && activeWorkspaceId) {
1925
+ await orchestratorSpawnAgent(config.name, undefined, config.command);
1926
+ return true;
1927
+ }
1928
+
1929
+ // Fallback to legacy API
1930
+ const result = await api.spawnAgent({
1931
+ name: config.name,
1932
+ cli: config.command,
1933
+ team: config.team,
1934
+ shadowMode: config.shadowMode,
1935
+ shadowOf: config.shadowOf,
1936
+ shadowAgent: config.shadowAgent,
1937
+ shadowTriggers: config.shadowTriggers,
1938
+ shadowSpeakOn: config.shadowSpeakOn,
1939
+ });
1940
+ if (!result.success) {
1941
+ setSpawnError(result.error || 'Failed to spawn agent');
1942
+ return false;
1943
+ }
1944
+ return true;
1945
+ } catch (err) {
1946
+ setSpawnError(err instanceof Error ? err.message : 'Failed to spawn agent');
1947
+ return false;
1948
+ } finally {
1949
+ setIsSpawning(false);
1950
+ }
1951
+ }, [workspaces.length, activeWorkspaceId, orchestratorSpawnAgent]);
1952
+
1953
+ // Handle release/kill agent
1954
+ const handleReleaseAgent = useCallback(async (agent: Agent) => {
1955
+ if (!agent.isSpawned) return;
1956
+
1957
+ const confirmed = window.confirm(`Are you sure you want to release agent "${agent.name}"?`);
1958
+ if (!confirmed) return;
1959
+
1960
+ try {
1961
+ // Use orchestrator if workspaces are available
1962
+ if (workspaces.length > 0 && activeWorkspaceId) {
1963
+ await orchestratorStopAgent(agent.name);
1964
+ return;
1965
+ }
1966
+
1967
+ // Fallback to legacy API
1968
+ const result = await api.releaseAgent(agent.name);
1969
+ if (!result.success) {
1970
+ console.error('Failed to release agent:', result.error);
1971
+ }
1972
+ } catch (err) {
1973
+ console.error('Failed to release agent:', err);
1974
+ }
1975
+ }, [workspaces.length, activeWorkspaceId, orchestratorStopAgent]);
1976
+
1977
+ // Handle logs click - open log viewer panel
1978
+ const handleLogsClick = useCallback((agent: Agent) => {
1979
+ setLogViewerAgent(agent);
1980
+ }, []);
1981
+
1982
+ // Fetch fleet servers periodically when fleet view is active
1983
+ useEffect(() => {
1984
+ if (!isFleetViewActive) return;
1985
+
1986
+ const fetchFleetServers = async () => {
1987
+ const result = await api.getFleetServers();
1988
+ if (result.success && result.data) {
1989
+ // Convert FleetServer to ServerInfo format
1990
+ const servers: ServerInfo[] = result.data.servers.map((s) => ({
1991
+ id: s.id,
1992
+ name: s.name,
1993
+ url: s.id === 'local' ? window.location.origin : `http://${s.id}`,
1994
+ status: s.status === 'healthy' ? 'online' : s.status === 'degraded' ? 'degraded' : 'offline',
1995
+ agentCount: s.agents.length,
1996
+ uptime: s.uptime,
1997
+ lastSeen: s.lastHeartbeat,
1998
+ }));
1999
+ setFleetServers(servers);
2000
+ }
2001
+ };
2002
+
2003
+ fetchFleetServers();
2004
+ const interval = setInterval(fetchFleetServers, 5000);
2005
+ return () => clearInterval(interval);
2006
+ }, [isFleetViewActive]);
2007
+
2008
+ // Fetch decisions periodically when queue is open
2009
+ useEffect(() => {
2010
+ if (!isDecisionQueueOpen) return;
2011
+
2012
+ const fetchDecisions = async () => {
2013
+ const result = await api.getDecisions();
2014
+ if (result.success && result.data) {
2015
+ setDecisions(result.data.decisions.map(convertApiDecision));
2016
+ }
2017
+ };
2018
+
2019
+ fetchDecisions();
2020
+ const interval = setInterval(fetchDecisions, 5000);
2021
+ return () => clearInterval(interval);
2022
+ }, [isDecisionQueueOpen]);
2023
+
2024
+ // Decision queue handlers
2025
+ const handleDecisionApprove = useCallback(async (decisionId: string, optionId?: string) => {
2026
+ setDecisionProcessing((prev) => ({ ...prev, [decisionId]: true }));
2027
+ try {
2028
+ const result = await api.approveDecision(decisionId, optionId);
2029
+ if (result.success) {
2030
+ setDecisions((prev) => prev.filter((d) => d.id !== decisionId));
2031
+ } else {
2032
+ console.error('Failed to approve decision:', result.error);
2033
+ }
2034
+ } catch (err) {
2035
+ console.error('Failed to approve decision:', err);
2036
+ } finally {
2037
+ setDecisionProcessing((prev) => ({ ...prev, [decisionId]: false }));
2038
+ }
2039
+ }, []);
2040
+
2041
+ const handleDecisionReject = useCallback(async (decisionId: string, reason?: string) => {
2042
+ setDecisionProcessing((prev) => ({ ...prev, [decisionId]: true }));
2043
+ try {
2044
+ const result = await api.rejectDecision(decisionId, reason);
2045
+ if (result.success) {
2046
+ setDecisions((prev) => prev.filter((d) => d.id !== decisionId));
2047
+ } else {
2048
+ console.error('Failed to reject decision:', result.error);
2049
+ }
2050
+ } catch (err) {
2051
+ console.error('Failed to reject decision:', err);
2052
+ } finally {
2053
+ setDecisionProcessing((prev) => ({ ...prev, [decisionId]: false }));
2054
+ }
2055
+ }, []);
2056
+
2057
+ const handleDecisionDismiss = useCallback(async (decisionId: string) => {
2058
+ const result = await api.dismissDecision(decisionId);
2059
+ if (result.success) {
2060
+ setDecisions((prev) => prev.filter((d) => d.id !== decisionId));
2061
+ }
2062
+ }, []);
2063
+
2064
+ // Task creation handler - creates bead and sends relay notification
2065
+ const handleTaskCreate = useCallback(async (task: TaskCreateRequest) => {
2066
+ setIsCreatingTask(true);
2067
+ try {
2068
+ // Map UI priority to beads priority number
2069
+ const beadsPriority = PRIORITY_CONFIG[task.priority].beadsPriority;
2070
+
2071
+ // Create bead via API
2072
+ const result = await api.createBead({
2073
+ title: task.title,
2074
+ assignee: task.agentName,
2075
+ priority: beadsPriority,
2076
+ type: 'task',
2077
+ });
2078
+
2079
+ if (result.success && result.data?.bead) {
2080
+ // Send relay notification to agent (non-interrupting)
2081
+ await api.sendRelayMessage({
2082
+ to: task.agentName,
2083
+ content: `📋 New task assigned: "${task.title}" (P${beadsPriority})\nCheck \`bd ready\` for details.`,
2084
+ });
2085
+ console.log('Task created:', result.data.bead.id);
2086
+ } else {
2087
+ console.error('Failed to create task bead:', result.error);
2088
+ throw new Error(result.error || 'Failed to create task');
2089
+ }
2090
+ } catch (err) {
2091
+ console.error('Failed to create task:', err);
2092
+ throw err;
2093
+ } finally {
2094
+ setIsCreatingTask(false);
2095
+ }
2096
+ }, []);
2097
+
2098
+ // Handle command palette
2099
+ const handleCommandPaletteOpen = useCallback(() => {
2100
+ setIsCommandPaletteOpen(true);
2101
+ }, []);
2102
+
2103
+ const handleCommandPaletteClose = useCallback(() => {
2104
+ setIsCommandPaletteOpen(false);
2105
+ }, []);
2106
+
2107
+ // Persist settings changes
2108
+ useEffect(() => {
2109
+ saveSettingsToStorage(settings);
2110
+ }, [settings]);
2111
+
2112
+ // Apply theme to document
2113
+ React.useEffect(() => {
2114
+ const applyTheme = (theme: 'light' | 'dark' | 'system') => {
2115
+ let effectiveTheme: 'light' | 'dark';
2116
+
2117
+ if (theme === 'system') {
2118
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
2119
+ effectiveTheme = prefersDark ? 'dark' : 'light';
2120
+ } else {
2121
+ effectiveTheme = theme;
2122
+ }
2123
+
2124
+ document.documentElement.setAttribute('data-theme', effectiveTheme);
2125
+ };
2126
+
2127
+ applyTheme(settings.theme);
2128
+
2129
+ if (settings.theme === 'system') {
2130
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
2131
+ const handleChange = () => applyTheme('system');
2132
+ mediaQuery.addEventListener('change', handleChange);
2133
+ return () => mediaQuery.removeEventListener('change', handleChange);
2134
+ }
2135
+ }, [settings.theme]);
2136
+
2137
+ // Request browser notification permissions when enabled
2138
+ useEffect(() => {
2139
+ if (!settings.notifications.desktop) return;
2140
+ if (typeof window === 'undefined' || !('Notification' in window)) return;
2141
+
2142
+ if (Notification.permission === 'granted') return;
2143
+
2144
+ if (Notification.permission === 'denied') {
2145
+ updateSettings((prev) => ({
2146
+ ...prev,
2147
+ notifications: {
2148
+ ...prev.notifications,
2149
+ desktop: false,
2150
+ enabled: prev.notifications.sound || prev.notifications.mentionsOnly,
2151
+ },
2152
+ }));
2153
+ return;
2154
+ }
2155
+
2156
+ Notification.requestPermission().then((permission) => {
2157
+ if (permission !== 'granted') {
2158
+ updateSettings((prev) => ({
2159
+ ...prev,
2160
+ notifications: {
2161
+ ...prev.notifications,
2162
+ desktop: false,
2163
+ enabled: prev.notifications.sound || prev.notifications.mentionsOnly,
2164
+ },
2165
+ }));
2166
+ }
2167
+ }).catch(() => undefined);
2168
+ }, [settings.notifications.desktop, settings.notifications.sound, settings.notifications.mentionsOnly, updateSettings]);
2169
+
2170
+ // Browser notifications and sounds for new messages
2171
+ useEffect(() => {
2172
+ const messages = data?.messages;
2173
+ if (!messages || messages.length === 0) {
2174
+ lastNotifiedMessageIdRef.current = null;
2175
+ return;
2176
+ }
2177
+
2178
+ const latestMessage = messages[messages.length - 1];
2179
+
2180
+ if (!settings.notifications.enabled) {
2181
+ lastNotifiedMessageIdRef.current = latestMessage?.id ?? null;
2182
+ return;
2183
+ }
2184
+
2185
+ if (!lastNotifiedMessageIdRef.current) {
2186
+ lastNotifiedMessageIdRef.current = latestMessage.id;
2187
+ return;
2188
+ }
2189
+
2190
+ const lastNotifiedIndex = messages.findIndex((message) => (
2191
+ message.id === lastNotifiedMessageIdRef.current
2192
+ ));
2193
+
2194
+ if (lastNotifiedIndex === -1) {
2195
+ lastNotifiedMessageIdRef.current = latestMessage.id;
2196
+ return;
2197
+ }
2198
+
2199
+ const newMessages = messages.slice(lastNotifiedIndex + 1);
2200
+ if (newMessages.length === 0) {
2201
+ return;
2202
+ }
2203
+
2204
+ lastNotifiedMessageIdRef.current = latestMessage.id;
2205
+
2206
+ const isFromCurrentUser = (message: Message) =>
2207
+ message.from === 'Dashboard' ||
2208
+ (currentUser && message.from === currentUser.displayName);
2209
+
2210
+ const isMessageInCurrentChannel = (message: Message) => {
2211
+ if (currentChannel === 'general') {
2212
+ return message.to === '*' || message.isBroadcast || message.channel === 'general';
2213
+ }
2214
+ return message.from === currentChannel || message.to === currentChannel;
2215
+ };
2216
+
2217
+ const shouldNotifyForMessage = (message: Message) => {
2218
+ if (isFromCurrentUser(message)) return false;
2219
+ if (settings.notifications.mentionsOnly && currentUser?.displayName) {
2220
+ if (!message.content.includes(`@${currentUser.displayName}`)) {
2221
+ return false;
2222
+ }
2223
+ }
2224
+ const isActive = typeof document !== 'undefined' ? !document.hidden : false;
2225
+ if (isActive && isMessageInCurrentChannel(message)) return false;
2226
+ return true;
2227
+ };
2228
+
2229
+ let shouldPlaySound = false;
2230
+
2231
+ for (const message of newMessages) {
2232
+ if (!shouldNotifyForMessage(message)) continue;
2233
+
2234
+ if (settings.notifications.desktop && typeof window !== 'undefined' && 'Notification' in window) {
2235
+ if (Notification.permission === 'granted') {
2236
+ const channelLabel = message.to === '*' ? 'Activity' : message.to;
2237
+ const body = message.content.split('\n')[0].slice(0, 160);
2238
+ const notification = new Notification(`${message.from} → ${channelLabel}`, { body });
2239
+ notification.onclick = () => {
2240
+ window.focus();
2241
+ if (message.to === '*') {
2242
+ setSelectedChannelId(ACTIVITY_FEED_ID);
2243
+ setViewMode('channels');
2244
+ } else {
2245
+ setCurrentChannel(message.from);
2246
+ }
2247
+ notification.close();
2248
+ };
2249
+ }
2250
+ }
2251
+
2252
+ if (settings.notifications.sound) {
2253
+ shouldPlaySound = true;
2254
+ }
2255
+ }
2256
+
2257
+ if (shouldPlaySound) {
2258
+ playNotificationSound();
2259
+ }
2260
+ }, [data?.messages, settings.notifications, currentChannel, currentUser, setCurrentChannel]);
2261
+
2262
+ // Keyboard shortcuts
2263
+ React.useEffect(() => {
2264
+ const handleKeyDown = (e: KeyboardEvent) => {
2265
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
2266
+ e.preventDefault();
2267
+ setIsCommandPaletteOpen(true);
2268
+ }
2269
+
2270
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 's') {
2271
+ e.preventDefault();
2272
+ handleSpawnClick();
2273
+ }
2274
+
2275
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'c') {
2276
+ e.preventDefault();
2277
+ setViewMode('channels');
2278
+ }
2279
+
2280
+ if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
2281
+ e.preventDefault();
2282
+ handleNewConversationClick();
2283
+ }
2284
+
2285
+ if (e.key === 'Escape') {
2286
+ setIsCommandPaletteOpen(false);
2287
+ setIsSpawnModalOpen(false);
2288
+ setIsNewConversationOpen(false);
2289
+ setIsTrajectoryOpen(false);
2290
+ setIsFullSettingsOpen(false);
2291
+ }
2292
+ };
2293
+
2294
+ window.addEventListener('keydown', handleKeyDown);
2295
+ return () => window.removeEventListener('keydown', handleKeyDown);
2296
+ }, [handleSpawnClick, handleNewConversationClick]);
2297
+
2298
+ // Handle billing result routes (success/cancel after Stripe checkout)
2299
+ const pathname = typeof window !== 'undefined' ? window.location.pathname : '';
2300
+ const searchParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : new URLSearchParams();
2301
+
2302
+ if (pathname === '/billing/success') {
2303
+ return (
2304
+ <BillingResult
2305
+ type="success"
2306
+ sessionId={searchParams.get('session_id') || undefined}
2307
+ onClose={() => {
2308
+ window.location.href = '/';
2309
+ }}
2310
+ />
2311
+ );
2312
+ }
2313
+
2314
+ if (pathname === '/billing/canceled') {
2315
+ return (
2316
+ <BillingResult
2317
+ type="canceled"
2318
+ onClose={() => {
2319
+ window.location.href = '/';
2320
+ }}
2321
+ />
2322
+ );
2323
+ }
2324
+
2325
+ return (
2326
+ <WorkspaceProvider wsUrl={wsUrl}>
2327
+ <div className="flex h-screen bg-bg-deep font-sans text-text-primary">
2328
+ {/* Mobile Sidebar Overlay */}
2329
+ <div
2330
+ className={`
2331
+ fixed inset-0 bg-black/60 backdrop-blur-sm z-[999] transition-opacity duration-200
2332
+ md:hidden
2333
+ ${isSidebarOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}
2334
+ `}
2335
+ onClick={() => setIsSidebarOpen(false)}
2336
+ />
2337
+
2338
+ {/* Sidebar with Workspace Selector */}
2339
+ <div className={`
2340
+ flex flex-col w-[280px] max-md:w-[85vw] max-md:max-w-[280px] h-screen bg-bg-primary border-r border-border-subtle
2341
+ fixed left-0 top-0 z-[1000] transition-transform duration-200
2342
+ md:relative md:translate-x-0 md:flex-shrink-0
2343
+ ${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
2344
+ `}>
2345
+ {/* Workspace Selector */}
2346
+ <div className="p-3 border-b border-sidebar-border">
2347
+ <WorkspaceSelector
2348
+ workspaces={effectiveWorkspaces}
2349
+ activeWorkspaceId={effectiveActiveWorkspaceId ?? undefined}
2350
+ onSelect={handleEffectiveWorkspaceSelect}
2351
+ onAddWorkspace={() => setIsAddWorkspaceOpen(true)}
2352
+ onWorkspaceSettings={handleWorkspaceSettingsClick}
2353
+ isLoading={effectiveIsLoading}
2354
+ />
2355
+ </div>
2356
+
2357
+ {/* Unified Sidebar - Channels collapsed by default, Agents always visible */}
2358
+ <Sidebar
2359
+ agents={localAgentsForSidebar}
2360
+ bridgeAgents={bridgeAgents}
2361
+ projects={mergedProjects}
2362
+ currentUserName={currentUser?.displayName}
2363
+ humanUnreadCounts={humanUnreadCounts}
2364
+ currentProject={currentProject}
2365
+ selectedAgent={selectedAgent?.name}
2366
+ viewMode={viewMode}
2367
+ isFleetAvailable={isFleetAvailable}
2368
+ isConnected={isConnected || isOrchestratorConnected}
2369
+ isOpen={isSidebarOpen}
2370
+ activeThreads={activeThreads}
2371
+ currentThread={currentThread}
2372
+ totalUnreadThreadCount={totalUnreadThreadCount}
2373
+ channels={channelsList
2374
+ .filter(c => !c.isDm && !c.id.startsWith('dm:'))
2375
+ .map(c => ({
2376
+ id: c.id,
2377
+ name: c.name,
2378
+ unreadCount: c.unreadCount,
2379
+ hasMentions: c.hasMentions,
2380
+ }))}
2381
+ archivedChannels={archivedChannelsList
2382
+ .filter(c => !c.isDm && !c.id.startsWith('dm:'))
2383
+ .map((c) => ({
2384
+ id: c.id,
2385
+ name: c.name,
2386
+ unreadCount: c.unreadCount ?? 0,
2387
+ hasMentions: c.hasMentions,
2388
+ }))}
2389
+ selectedChannelId={selectedChannelId}
2390
+ isActivitySelected={selectedChannelId === ACTIVITY_FEED_ID}
2391
+ activityUnreadCount={0}
2392
+ onActivitySelect={() => {
2393
+ setSelectedChannelId(ACTIVITY_FEED_ID);
2394
+ selectAgent(null);
2395
+ setViewMode('channels');
2396
+ }}
2397
+ onChannelSelect={(channel) => {
2398
+ const fullChannel =
2399
+ channelsList.find(c => c.id === channel.id) ||
2400
+ archivedChannelsList.find(c => c.id === channel.id);
2401
+ if (fullChannel) {
2402
+ handleSelectChannel(fullChannel);
2403
+ setViewMode('channels');
2404
+ }
2405
+ }}
2406
+ onCreateChannel={handleCreateChannel}
2407
+ onInviteToChannel={(channel) => {
2408
+ const fullChannel = channelsList.find(c => c.id === channel.id);
2409
+ if (fullChannel) {
2410
+ handleInviteToChannel(fullChannel);
2411
+ }
2412
+ }}
2413
+ onArchiveChannel={(channel) => {
2414
+ const fullChannel = channelsList.find((c) => c.id === channel.id);
2415
+ if (fullChannel) {
2416
+ handleArchiveChannel(fullChannel);
2417
+ }
2418
+ }}
2419
+ onUnarchiveChannel={(channel) => {
2420
+ const fullChannel =
2421
+ archivedChannelsList.find((c) => c.id === channel.id) ||
2422
+ channelsList.find((c) => c.id === channel.id);
2423
+ if (fullChannel) {
2424
+ handleUnarchiveChannel(fullChannel);
2425
+ }
2426
+ }}
2427
+ onAgentSelect={handleAgentSelect}
2428
+ onHumanSelect={handleHumanSelect}
2429
+ onProjectSelect={handleProjectSelect}
2430
+ onViewModeChange={setViewMode}
2431
+ onSpawnClick={handleSpawnClick}
2432
+ onReleaseClick={handleReleaseAgent}
2433
+ onLogsClick={handleLogsClick}
2434
+ onProfileClick={setSelectedAgentProfile}
2435
+ onThreadSelect={setCurrentThread}
2436
+ onClose={() => setIsSidebarOpen(false)}
2437
+ onSettingsClick={handleSettingsClick}
2438
+ onTrajectoryClick={() => setIsTrajectoryOpen(true)}
2439
+ hasActiveTrajectory={trajectoryStatus?.active}
2440
+ onFleetClick={() => setIsFleetViewActive(!isFleetViewActive)}
2441
+ isFleetViewActive={isFleetViewActive}
2442
+ onCoordinatorClick={handleCoordinatorClick}
2443
+ hasMultipleProjects={mergedProjects.length > 1}
2444
+ />
2445
+ </div>
2446
+
2447
+ {/* Main Content */}
2448
+ <main className="flex-1 flex flex-col min-w-0 bg-bg-secondary/50 overflow-hidden">
2449
+ {/* Header - fixed on mobile for keyboard-safe positioning, sticky on desktop */}
2450
+ <div className="fixed top-0 left-0 right-0 z-50 md:sticky md:top-0 md:left-auto md:right-auto bg-bg-secondary">
2451
+ <Header
2452
+ currentChannel={currentChannel}
2453
+ selectedAgent={selectedAgent}
2454
+ projects={mergedProjects}
2455
+ currentProject={mergedProjects.find(p => p.id === currentProject) || null}
2456
+ recentProjects={getRecentProjects(mergedProjects)}
2457
+ viewMode={viewMode}
2458
+ selectedChannelName={selectedChannel?.name}
2459
+ onProjectChange={handleProjectSelect}
2460
+ onCommandPaletteOpen={handleCommandPaletteOpen}
2461
+ onSettingsClick={handleSettingsClick}
2462
+ onHistoryClick={handleHistoryClick}
2463
+ onNewConversationClick={handleNewConversationClick}
2464
+ onCoordinatorClick={handleCoordinatorClick}
2465
+ onFleetClick={() => setIsFleetViewActive(!isFleetViewActive)}
2466
+ isFleetViewActive={isFleetViewActive}
2467
+ onTrajectoryClick={() => setIsTrajectoryOpen(true)}
2468
+ hasActiveTrajectory={trajectoryStatus?.active}
2469
+ onMenuClick={() => setIsSidebarOpen(true)}
2470
+ hasUnreadNotifications={hasUnreadMessages}
2471
+ />
2472
+ {/* Usage banner for free tier users */}
2473
+ <UsageBanner onUpgradeClick={handleBillingClick} />
2474
+ </div>
2475
+ {/* Spacer for fixed header on mobile - matches header height (52px) */}
2476
+ <div className="h-[52px] flex-shrink-0 md:hidden" />
2477
+ {/* Online users indicator - outside fixed header so it scrolls with content on mobile */}
2478
+ {currentUser && onlineUsers.length > 0 && (
2479
+ <div className="flex items-center justify-end px-4 py-1 bg-bg-tertiary/80 border-b border-border-subtle flex-shrink-0">
2480
+ <OnlineUsersIndicator
2481
+ onlineUsers={onlineUsers}
2482
+ onUserClick={setSelectedUserProfile}
2483
+ />
2484
+ </div>
2485
+ )}
2486
+
2487
+ {/* Content Area */}
2488
+ <div className="flex-1 flex overflow-hidden min-h-0">
2489
+ {/* Message List */}
2490
+ <div className={`flex-1 min-h-0 overflow-y-auto ${currentThread ? 'hidden md:block md:flex-[2]' : ''}`}>
2491
+ {currentHuman && (
2492
+ <div className="px-4 py-2 border-b border-border-subtle bg-bg-secondary flex flex-col gap-2 sticky top-0 z-10">
2493
+ <div className="text-xs text-text-muted">
2494
+ DM with <span className="font-semibold text-text-primary">{currentHuman.name}</span>. Invite agents:
2495
+ </div>
2496
+ <div className="flex flex-wrap gap-2">
2497
+ {agents
2498
+ .filter((a) => !a.isHuman)
2499
+ .map((agent) => {
2500
+ const isSelected = (dmSelectedAgentsByHuman[currentHuman.name] ?? []).includes(agent.name);
2501
+ return (
2502
+ <button
2503
+ key={agent.name}
2504
+ onClick={() => handleDmAgentToggle(agent.name)}
2505
+ className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ${
2506
+ isSelected
2507
+ ? 'bg-accent-cyan text-bg-deep'
2508
+ : 'bg-bg-tertiary text-text-secondary hover:bg-bg-tertiary/80'
2509
+ }`}
2510
+ title={agent.name}
2511
+ >
2512
+ {isSelected ? '✓ ' : ''}{agent.name}
2513
+ </button>
2514
+ );
2515
+ })}
2516
+ {agents.filter((a) => !a.isHuman).length === 0 && (
2517
+ <span className="text-xs text-text-muted">No agents available</span>
2518
+ )}
2519
+ </div>
2520
+ </div>
2521
+ )}
2522
+ {wsError ? (
2523
+ <div className="flex flex-col items-center justify-center h-full text-text-muted text-center px-4">
2524
+ <ErrorIcon />
2525
+ <h2 className="m-0 mb-2 font-display text-text-primary">Connection Error</h2>
2526
+ <p className="text-text-secondary">{wsError.message}</p>
2527
+ <button
2528
+ className="mt-6 py-3 px-6 bg-gradient-to-r from-accent-cyan to-[#00b8d9] text-bg-deep font-semibold border-none rounded-xl cursor-pointer transition-all duration-150 hover:shadow-glow-cyan hover:-translate-y-0.5"
2529
+ onClick={() => window.location.reload()}
2530
+ >
2531
+ Retry Connection
2532
+ </button>
2533
+ </div>
2534
+ ) : !data ? (
2535
+ <div className="flex flex-col items-center justify-center h-full text-text-muted text-center">
2536
+ <LoadingSpinner />
2537
+ <p className="font-display text-text-secondary">Connecting to dashboard...</p>
2538
+ </div>
2539
+ ) : isFleetViewActive ? (
2540
+ <div className="p-4 h-full overflow-y-auto">
2541
+ <FleetOverview
2542
+ servers={fleetServers}
2543
+ agents={agents}
2544
+ selectedServerId={selectedServerId}
2545
+ onServerSelect={setSelectedServerId}
2546
+ onServerReconnect={handleServerReconnect}
2547
+ isLoading={!data}
2548
+ />
2549
+ </div>
2550
+ ) : selectedChannelId === ACTIVITY_FEED_ID ? (
2551
+ <ActivityFeed
2552
+ events={activityEvents}
2553
+ maxEvents={100}
2554
+ />
2555
+ ) : viewMode === 'channels' && selectedChannel ? (
2556
+ <ChannelViewV1
2557
+ channel={selectedChannel}
2558
+ messages={effectiveChannelMessages}
2559
+ currentUser={currentUser?.displayName || 'Anonymous'}
2560
+ isLoadingMore={false}
2561
+ hasMoreMessages={hasMoreMessages && !!effectiveActiveWorkspaceId}
2562
+ mentionSuggestions={agents.map(a => a.name)}
2563
+ unreadState={channelUnreadState}
2564
+ onSendMessage={handleSendChannelMessage}
2565
+ onLoadMore={handleLoadMoreMessages}
2566
+ onThreadClick={(messageId) => setCurrentThread(messageId)}
2567
+ onShowMembers={handleShowMembers}
2568
+ onMemberClick={handleChannelMemberClick}
2569
+ />
2570
+ ) : viewMode === 'channels' ? (
2571
+ <div className="flex flex-col items-center justify-center h-full text-text-muted text-center px-4">
2572
+ <HashIconLarge />
2573
+ <h2 className="m-0 mb-2 font-display text-text-primary">Select a channel</h2>
2574
+ <p className="text-text-secondary">Choose a channel from the sidebar to start messaging</p>
2575
+ </div>
2576
+ ) : (
2577
+ <MessageList
2578
+ messages={dedupedVisibleMessages}
2579
+ currentChannel={currentChannel}
2580
+ currentThread={currentThread}
2581
+ onThreadClick={(messageId) => setCurrentThread(messageId)}
2582
+ highlightedMessageId={currentThread ?? undefined}
2583
+ agents={combinedAgents}
2584
+ currentUser={currentUser}
2585
+ skipChannelFilter={currentHuman !== null}
2586
+ showTimestamps={settings.display.showTimestamps}
2587
+ autoScrollDefault={settings.messages.autoScroll}
2588
+ compactMode={settings.display.compactMode}
2589
+ onAgentClick={setSelectedAgentProfile}
2590
+ onUserClick={setSelectedUserProfile}
2591
+ onlineUsers={onlineUsers}
2592
+ />
2593
+ )}
2594
+ </div>
2595
+
2596
+ {/* Thread Panel */}
2597
+ {currentThread && (() => {
2598
+ // Determine which message list to search based on view mode
2599
+ const isChannelView = viewMode === 'channels';
2600
+
2601
+ // Helper to convert ChannelMessage to Message format for ThreadPanel
2602
+ const convertChannelMessage = (cm: ChannelApiMessage): Message => ({
2603
+ id: cm.id,
2604
+ from: cm.from,
2605
+ to: cm.channelId,
2606
+ content: cm.content,
2607
+ timestamp: cm.timestamp,
2608
+ thread: cm.threadId,
2609
+ isRead: cm.isRead,
2610
+ replyCount: cm.threadSummary?.replyCount,
2611
+ threadSummary: cm.threadSummary,
2612
+ });
2613
+
2614
+ let originalMessage: Message | null = null;
2615
+ let isTopicThread = false;
2616
+
2617
+ if (isChannelView) {
2618
+ const channelMsg = effectiveChannelMessages.find((m) => m.id === currentThread);
2619
+ if (channelMsg) {
2620
+ originalMessage = convertChannelMessage(channelMsg);
2621
+ } else {
2622
+ isTopicThread = true;
2623
+ const threadMsgs = effectiveChannelMessages
2624
+ .filter((m) => m.threadId === currentThread)
2625
+ .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
2626
+ if (threadMsgs[0]) {
2627
+ originalMessage = convertChannelMessage(threadMsgs[0]);
2628
+ }
2629
+ }
2630
+ } else {
2631
+ originalMessage = messages.find((m) => m.id === currentThread) ?? null;
2632
+ isTopicThread = !originalMessage;
2633
+ if (!originalMessage) {
2634
+ const threadMsgs = messages
2635
+ .filter((m) => m.thread === currentThread)
2636
+ .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
2637
+ originalMessage = threadMsgs[0] ?? null;
2638
+ }
2639
+ }
2640
+
2641
+ // Get thread replies based on view mode
2642
+ const replies: Message[] = isChannelView
2643
+ ? effectiveChannelMessages
2644
+ .filter((m) => m.threadId === currentThread)
2645
+ .map(convertChannelMessage)
2646
+ : threadMessages(currentThread);
2647
+
2648
+ return (
2649
+ <div className="w-full md:w-[400px] md:min-w-[320px] md:max-w-[500px] flex-shrink-0">
2650
+ <ThreadPanel
2651
+ originalMessage={originalMessage}
2652
+ replies={replies}
2653
+ onClose={() => setCurrentThread(null)}
2654
+ showTimestamps={settings.display.showTimestamps}
2655
+ onReply={async (content) => {
2656
+ if (isChannelView && selectedChannel) {
2657
+ // For channels, send threaded message
2658
+ await handleSendChannelMessage(content, currentThread);
2659
+ return true;
2660
+ }
2661
+ // For topic threads, broadcast to all; for reply chains, reply to the other participant
2662
+ let recipient = '*';
2663
+ if (!isTopicThread && originalMessage) {
2664
+ // If current user sent the original message, reply to the recipient
2665
+ // If someone else sent it, reply to the sender
2666
+ const isFromCurrentUser = originalMessage.from === 'Dashboard' ||
2667
+ (currentUser && originalMessage.from === currentUser.displayName);
2668
+ recipient = isFromCurrentUser
2669
+ ? originalMessage.to
2670
+ : originalMessage.from;
2671
+ }
2672
+ return sendMessage(recipient, content, currentThread);
2673
+ }}
2674
+ isSending={isSending}
2675
+ currentUser={currentUser}
2676
+ />
2677
+ </div>
2678
+ );
2679
+ })()}
2680
+ </div>
2681
+
2682
+ {/* Typing Indicator */}
2683
+ {typingUsers.length > 0 && (
2684
+ <div className="px-4 bg-bg-tertiary border-t border-border-subtle">
2685
+ <TypingIndicator typingUsers={typingUsers} />
2686
+ </div>
2687
+ )}
2688
+
2689
+ {/* Message Composer - hide in channels mode (ChannelViewV1 has its own input) */}
2690
+ {viewMode !== 'channels' && (
2691
+ <div className="p-2 sm:p-4 bg-bg-tertiary border-t border-border-subtle">
2692
+ <MessageComposer
2693
+ recipient={currentChannel === 'general' ? '*' : currentChannel}
2694
+ agents={agents}
2695
+ humanUsers={humanUsers}
2696
+ onSend={currentHuman ? handleDmSend : sendMessage}
2697
+ onTyping={sendTyping}
2698
+ isSending={isSending}
2699
+ error={sendError}
2700
+ insertMention={pendingMention}
2701
+ onMentionInserted={() => setPendingMention(undefined)}
2702
+ />
2703
+ </div>
2704
+ )}
2705
+ </main>
2706
+
2707
+ {/* Command Palette */}
2708
+ <CommandPalette
2709
+ isOpen={isCommandPaletteOpen}
2710
+ onClose={handleCommandPaletteClose}
2711
+ agents={agents}
2712
+ projects={projects}
2713
+ currentProject={currentProject}
2714
+ onAgentSelect={handleAgentSelect}
2715
+ onProjectSelect={handleProjectSelect}
2716
+ onSpawnClick={handleSpawnClick}
2717
+ onTaskCreate={handleTaskCreate}
2718
+ onGeneralClick={() => {
2719
+ selectAgent(null);
2720
+ setCurrentChannel('general');
2721
+ }}
2722
+ customCommands={[...dmInviteCommands, ...channelCommands]}
2723
+ />
2724
+
2725
+ {/* Spawn Modal */}
2726
+ <SpawnModal
2727
+ isOpen={isSpawnModalOpen}
2728
+ onClose={() => setIsSpawnModalOpen(false)}
2729
+ onSpawn={handleSpawn}
2730
+ existingAgents={agents.map((a) => a.name)}
2731
+ isSpawning={isSpawning}
2732
+ error={spawnError}
2733
+ isCloudMode={isCloudMode}
2734
+ workspaceId={effectiveActiveWorkspaceId ?? undefined}
2735
+ />
2736
+
2737
+ {/* Add Workspace Modal */}
2738
+ <AddWorkspaceModal
2739
+ isOpen={isAddWorkspaceOpen}
2740
+ onClose={() => {
2741
+ setIsAddWorkspaceOpen(false);
2742
+ setAddWorkspaceError(null);
2743
+ }}
2744
+ onAdd={handleAddWorkspace}
2745
+ isAdding={isAddingWorkspace}
2746
+ error={addWorkspaceError}
2747
+ />
2748
+
2749
+ {/* Create Channel Modal */}
2750
+ <CreateChannelModal
2751
+ isOpen={isCreateChannelOpen}
2752
+ onClose={() => setIsCreateChannelOpen(false)}
2753
+ onCreate={handleCreateChannelSubmit}
2754
+ isLoading={isCreatingChannel}
2755
+ existingChannels={channelsList.map(c => c.name)}
2756
+ availableMembers={agents.map(a => a.name)}
2757
+ />
2758
+
2759
+ {/* Invite to Channel Modal */}
2760
+ <InviteToChannelModal
2761
+ isOpen={isInviteChannelOpen}
2762
+ channelName={inviteChannelTarget?.name || ''}
2763
+ onClose={() => {
2764
+ setIsInviteChannelOpen(false);
2765
+ setInviteChannelTarget(null);
2766
+ }}
2767
+ onInvite={handleInviteSubmit}
2768
+ isLoading={isInvitingToChannel}
2769
+ availableMembers={agents.map(a => a.name)}
2770
+ />
2771
+
2772
+ {/* Member Management Panel */}
2773
+ {selectedChannel && (
2774
+ <MemberManagementPanel
2775
+ channel={selectedChannel}
2776
+ members={channelMembers}
2777
+ isOpen={showMemberPanel}
2778
+ onClose={() => setShowMemberPanel(false)}
2779
+ onAddMember={handleAddMember}
2780
+ onRemoveMember={handleRemoveMember}
2781
+ onUpdateRole={() => {}}
2782
+ currentUserId={currentUser?.displayName}
2783
+ availableAgents={agents.map(a => ({ name: a.name }))}
2784
+ workspaceId={effectiveActiveWorkspaceId ?? undefined}
2785
+ />
2786
+ )}
2787
+
2788
+ {/* Conversation History */}
2789
+ <ConversationHistory
2790
+ isOpen={isHistoryOpen}
2791
+ onClose={() => setIsHistoryOpen(false)}
2792
+ />
2793
+
2794
+ {/* New Conversation Modal */}
2795
+ <NewConversationModal
2796
+ isOpen={isNewConversationOpen}
2797
+ onClose={() => setIsNewConversationOpen(false)}
2798
+ onSend={handleNewConversationSend}
2799
+ agents={agents}
2800
+ isSending={isSending}
2801
+ error={sendError}
2802
+ />
2803
+
2804
+ {/* Log Viewer Panel */}
2805
+ {logViewerAgent && (
2806
+ <LogViewerPanel
2807
+ agent={logViewerAgent}
2808
+ isOpen={true}
2809
+ onClose={() => setLogViewerAgent(null)}
2810
+ availableAgents={agents}
2811
+ onAgentChange={setLogViewerAgent}
2812
+ />
2813
+ )}
2814
+
2815
+ {/* Trajectory Panel - Fullscreen slide-over */}
2816
+ {isTrajectoryOpen && (
2817
+ <div
2818
+ className="fixed inset-0 z-50 flex bg-black/50 backdrop-blur-sm"
2819
+ onClick={() => setIsTrajectoryOpen(false)}
2820
+ >
2821
+ <div
2822
+ className="ml-auto w-full max-w-3xl h-full bg-bg-primary shadow-2xl animate-in slide-in-from-right duration-300 flex flex-col"
2823
+ onClick={(e) => e.stopPropagation()}
2824
+ >
2825
+ {/* Header */}
2826
+ <div className="flex items-center justify-between px-6 py-4 border-b border-border-subtle bg-bg-secondary">
2827
+ <div className="flex items-center gap-3">
2828
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500/20 to-accent-cyan/20 flex items-center justify-center border border-blue-500/30">
2829
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-blue-500">
2830
+ <path d="M3 12h4l3 9 4-18 3 9h4" strokeLinecap="round" strokeLinejoin="round" />
2831
+ </svg>
2832
+ </div>
2833
+ <div>
2834
+ <h2 className="text-lg font-semibold text-text-primary m-0">Trajectory Viewer</h2>
2835
+ <p className="text-xs text-text-muted m-0">
2836
+ {trajectoryStatus?.active ? `Active: ${trajectoryStatus.task || 'Working...'}` : 'Browse past trajectories'}
2837
+ </p>
2838
+ </div>
2839
+ </div>
2840
+ <button
2841
+ onClick={() => setIsTrajectoryOpen(false)}
2842
+ className="w-10 h-10 rounded-lg bg-bg-tertiary border border-border-subtle flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-bg-hover hover:border-blue-500/50 transition-all"
2843
+ title="Close (Esc)"
2844
+ >
2845
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
2846
+ <path d="M18 6L6 18M6 6l12 12" />
2847
+ </svg>
2848
+ </button>
2849
+ </div>
2850
+
2851
+ {/* Content */}
2852
+ <div className="flex-1 overflow-hidden p-6">
2853
+ <TrajectoryViewer
2854
+ agentName={selectedTrajectoryTitle?.slice(0, 30) || trajectoryStatus?.task?.slice(0, 30) || 'Trajectories'}
2855
+ steps={trajectorySteps}
2856
+ history={trajectoryHistory}
2857
+ selectedTrajectoryId={selectedTrajectoryId}
2858
+ onSelectTrajectory={selectTrajectory}
2859
+ isLoading={isTrajectoryLoading}
2860
+ />
2861
+ </div>
2862
+ </div>
2863
+ </div>
2864
+ )}
2865
+
2866
+
2867
+ {/* Decision Queue Panel */}
2868
+ {isDecisionQueueOpen && (
2869
+ <div className="fixed left-4 bottom-4 w-[400px] max-h-[500px] z-50 shadow-modal">
2870
+ <div className="relative">
2871
+ <button
2872
+ onClick={() => setIsDecisionQueueOpen(false)}
2873
+ className="absolute -top-2 -right-2 w-6 h-6 bg-bg-elevated border border-border rounded-full flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-bg-hover z-10"
2874
+ title="Close decisions"
2875
+ >
2876
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
2877
+ <path d="M18 6L6 18M6 6l12 12" />
2878
+ </svg>
2879
+ </button>
2880
+ <DecisionQueue
2881
+ decisions={decisions}
2882
+ onApprove={handleDecisionApprove}
2883
+ onReject={handleDecisionReject}
2884
+ onDismiss={handleDecisionDismiss}
2885
+ isProcessing={decisionProcessing}
2886
+ />
2887
+ </div>
2888
+ </div>
2889
+ )}
2890
+
2891
+ {/* Decision Queue Toggle Button (bottom-left when panel is closed) */}
2892
+ {!isDecisionQueueOpen && decisions.length > 0 && (
2893
+ <button
2894
+ onClick={() => setIsDecisionQueueOpen(true)}
2895
+ className="fixed left-4 bottom-4 w-12 h-12 bg-warning text-bg-deep rounded-full shadow-[0_0_20px_rgba(255,107,53,0.4)] flex items-center justify-center hover:scale-105 transition-transform z-50"
2896
+ title={`${decisions.length} pending decision${decisions.length > 1 ? 's' : ''}`}
2897
+ >
2898
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
2899
+ <circle cx="12" cy="12" r="10" />
2900
+ <line x1="12" y1="8" x2="12" y2="12" />
2901
+ <line x1="12" y1="16" x2="12.01" y2="16" />
2902
+ </svg>
2903
+ {decisions.length > 0 && (
2904
+ <span className="absolute -top-1 -right-1 w-5 h-5 bg-error text-white text-[10px] font-bold rounded-full flex items-center justify-center">
2905
+ {decisions.length}
2906
+ </span>
2907
+ )}
2908
+ </button>
2909
+ )}
2910
+
2911
+ {/* User Profile Panel */}
2912
+ <UserProfilePanel
2913
+ user={selectedUserProfile}
2914
+ onClose={() => setSelectedUserProfile(null)}
2915
+ onMention={(username) => {
2916
+ // Set pending mention to trigger insertion in MessageComposer
2917
+ setPendingMention(username);
2918
+ setSelectedUserProfile(null);
2919
+ }}
2920
+ onSendMessage={(user) => {
2921
+ setCurrentChannel(user.username);
2922
+ markDmSeen(user.username);
2923
+ setSelectedUserProfile(null);
2924
+ }}
2925
+ />
2926
+
2927
+ {/* Agent Profile Panel */}
2928
+ <AgentProfilePanel
2929
+ agent={selectedAgentProfile}
2930
+ onClose={() => setSelectedAgentProfile(null)}
2931
+ onMessage={(agent) => {
2932
+ selectAgent(agent.name);
2933
+ setCurrentChannel(agent.name);
2934
+ setSelectedAgentProfile(null);
2935
+ }}
2936
+ onLogs={handleLogsClick}
2937
+ onRelease={handleReleaseAgent}
2938
+ summary={selectedAgentProfile ? agentSummariesMap.get(selectedAgentProfile.name.toLowerCase()) : null}
2939
+ />
2940
+
2941
+ {/* Coordinator Panel */}
2942
+ <CoordinatorPanel
2943
+ isOpen={isCoordinatorOpen}
2944
+ onClose={() => setIsCoordinatorOpen(false)}
2945
+ projects={mergedProjects}
2946
+ isCloudMode={!!currentUser}
2947
+ hasArchitect={bridgeAgents.some(a => a.name.toLowerCase() === 'architect')}
2948
+ onArchitectSpawned={() => {
2949
+ // Architect will appear via WebSocket update
2950
+ setIsCoordinatorOpen(false);
2951
+ }}
2952
+ />
2953
+
2954
+ {/* Full Settings Page */}
2955
+ {isFullSettingsOpen && (
2956
+ <SettingsPage
2957
+ currentUserId={cloudSession?.user?.id}
2958
+ initialTab={settingsInitialTab}
2959
+ onClose={() => setIsFullSettingsOpen(false)}
2960
+ settings={settings}
2961
+ onUpdateSettings={updateSettings}
2962
+ activeWorkspaceId={effectiveActiveWorkspaceId}
2963
+ />
2964
+ )}
2965
+
2966
+ {/* Toast Notifications */}
2967
+ <NotificationToast
2968
+ toasts={toasts}
2969
+ onDismiss={dismissToast}
2970
+ position="top-right"
2971
+ />
2972
+ </div>
2973
+ </WorkspaceProvider>
2974
+ );
2975
+ }
2976
+
2977
+ /**
2978
+ * Pending attachment interface for UI state
2979
+ */
2980
+ interface PendingAttachment {
2981
+ id: string;
2982
+ file: File;
2983
+ preview: string;
2984
+ isUploading: boolean;
2985
+ uploadedId?: string;
2986
+ error?: string;
2987
+ }
2988
+
2989
+ /**
2990
+ * Message Composer Component with @-mention autocomplete and image attachments
2991
+ */
2992
+ interface MessageComposerProps {
2993
+ recipient: string;
2994
+ agents: Agent[];
2995
+ humanUsers: HumanUser[];
2996
+ onSend: (to: string, content: string, thread?: string, attachmentIds?: string[]) => Promise<boolean>;
2997
+ onTyping?: (isTyping: boolean) => void;
2998
+ isSending: boolean;
2999
+ error: string | null;
3000
+ insertMention?: string;
3001
+ onMentionInserted?: () => void;
3002
+ }
3003
+
3004
+ function MessageComposer({ recipient, agents, humanUsers, onSend, onTyping, isSending, error, insertMention, onMentionInserted }: MessageComposerProps) {
3005
+ const [message, setMessage] = useState('');
3006
+ const [cursorPosition, setCursorPosition] = useState(0);
3007
+ const [showMentions, setShowMentions] = useState(false);
3008
+ const [showFiles, setShowFiles] = useState(false);
3009
+ const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
3010
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
3011
+ const fileInputRef = useRef<HTMLInputElement>(null);
3012
+
3013
+ // Handle insertMention prop - insert @username when triggered from outside
3014
+ useEffect(() => {
3015
+ if (insertMention && onMentionInserted) {
3016
+ const mentionText = `@${insertMention} `;
3017
+ // Insert at current cursor position or append to end
3018
+ const textarea = textareaRef.current;
3019
+ if (textarea) {
3020
+ const start = textarea.selectionStart || message.length;
3021
+ const newMessage = message.slice(0, start) + mentionText + message.slice(start);
3022
+ setMessage(newMessage);
3023
+ // Focus and set cursor position after the mention
3024
+ setTimeout(() => {
3025
+ textarea.focus();
3026
+ const newPos = start + mentionText.length;
3027
+ textarea.setSelectionRange(newPos, newPos);
3028
+ }, 0);
3029
+ } else {
3030
+ // Fallback: just append to message
3031
+ setMessage(prev => prev + mentionText);
3032
+ }
3033
+ onMentionInserted();
3034
+ }
3035
+ }, [insertMention, onMentionInserted, message]);
3036
+
3037
+ // Process image files (used by both paste and file input)
3038
+ const processImageFiles = useCallback(async (imageFiles: File[]) => {
3039
+ for (const file of imageFiles) {
3040
+ const id = crypto.randomUUID();
3041
+ const preview = URL.createObjectURL(file);
3042
+
3043
+ // Add to pending attachments
3044
+ setAttachments(prev => [...prev, {
3045
+ id,
3046
+ file,
3047
+ preview,
3048
+ isUploading: true,
3049
+ }]);
3050
+
3051
+ // Upload the file
3052
+ try {
3053
+ const result = await api.uploadAttachment(file);
3054
+ if (result.success && result.data) {
3055
+ setAttachments(prev => prev.map(a =>
3056
+ a.id === id
3057
+ ? { ...a, isUploading: false, uploadedId: result.data!.attachment.id }
3058
+ : a
3059
+ ));
3060
+ } else {
3061
+ setAttachments(prev => prev.map(a =>
3062
+ a.id === id
3063
+ ? { ...a, isUploading: false, error: result.error || 'Upload failed' }
3064
+ : a
3065
+ ));
3066
+ }
3067
+ } catch (err) {
3068
+ setAttachments(prev => prev.map(a =>
3069
+ a.id === id
3070
+ ? { ...a, isUploading: false, error: 'Upload failed' }
3071
+ : a
3072
+ ));
3073
+ }
3074
+ }
3075
+ }, []);
3076
+
3077
+ // Handle file selection from file input
3078
+ const handleFileSelect = useCallback((files: FileList | null) => {
3079
+ if (!files || files.length === 0) return;
3080
+
3081
+ const imageFiles = Array.from(files).filter(file =>
3082
+ file.type.startsWith('image/')
3083
+ );
3084
+
3085
+ if (imageFiles.length > 0) {
3086
+ processImageFiles(imageFiles);
3087
+ }
3088
+ }, [processImageFiles]);
3089
+
3090
+ // Handle paste for clipboard images
3091
+ const handlePaste = useCallback((e: React.ClipboardEvent) => {
3092
+ const clipboardData = e.clipboardData;
3093
+ if (!clipboardData) return;
3094
+
3095
+ // Collect image files from both sources
3096
+ let imageFiles: File[] = [];
3097
+
3098
+ // Method 1: Check clipboardData.files (works for file pastes)
3099
+ if (clipboardData.files && clipboardData.files.length > 0) {
3100
+ imageFiles = Array.from(clipboardData.files).filter(file =>
3101
+ file.type.startsWith('image/')
3102
+ );
3103
+ }
3104
+
3105
+ // Method 2: Check clipboardData.items (works for screenshots/copied images)
3106
+ // This is the primary method for pasted images from clipboard
3107
+ if (imageFiles.length === 0 && clipboardData.items) {
3108
+ const items = Array.from(clipboardData.items);
3109
+ for (const item of items) {
3110
+ // Check if this item is an image
3111
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
3112
+ const file = item.getAsFile();
3113
+ if (file) {
3114
+ imageFiles.push(file);
3115
+ }
3116
+ }
3117
+ }
3118
+ }
3119
+
3120
+ // Process any found images
3121
+ if (imageFiles.length > 0) {
3122
+ e.preventDefault();
3123
+ processImageFiles(imageFiles);
3124
+ }
3125
+ }, [processImageFiles]);
3126
+
3127
+ // Remove an attachment
3128
+ const removeAttachment = useCallback((id: string) => {
3129
+ setAttachments(prev => {
3130
+ const attachment = prev.find(a => a.id === id);
3131
+ if (attachment) {
3132
+ URL.revokeObjectURL(attachment.preview);
3133
+ }
3134
+ return prev.filter(a => a.id !== id);
3135
+ });
3136
+ }, []);
3137
+
3138
+ const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
3139
+ const value = e.target.value;
3140
+ const cursorPos = e.target.selectionStart || 0;
3141
+ setMessage(value);
3142
+ setCursorPosition(cursorPos);
3143
+
3144
+ // Send typing indicator when user has content
3145
+ onTyping?.(value.trim().length > 0);
3146
+
3147
+ // Check for file autocomplete first (@ followed by path-like pattern)
3148
+ const fileQuery = getFileQuery(value, cursorPos);
3149
+ if (fileQuery !== null) {
3150
+ setShowFiles(true);
3151
+ setShowMentions(false);
3152
+ return;
3153
+ }
3154
+
3155
+ // Check for mention autocomplete (@ at start without path patterns)
3156
+ const mentionQuery = getMentionQuery(value, cursorPos);
3157
+ if (mentionQuery !== null) {
3158
+ setShowMentions(true);
3159
+ setShowFiles(false);
3160
+ return;
3161
+ }
3162
+
3163
+ // Neither - hide both
3164
+ setShowMentions(false);
3165
+ setShowFiles(false);
3166
+ };
3167
+
3168
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
3169
+ // Don't handle Enter/Tab when autocomplete is visible (let autocomplete handle it)
3170
+ if ((showMentions || showFiles) && (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Tab')) {
3171
+ return; // Let the autocomplete component handle these keys
3172
+ }
3173
+
3174
+ if (e.key === 'Enter' && !e.shiftKey && !showMentions && !showFiles) {
3175
+ e.preventDefault();
3176
+ if ((message.trim() || attachments.length > 0) && !isSending) {
3177
+ handleSubmit(e as unknown as React.FormEvent);
3178
+ }
3179
+ }
3180
+ };
3181
+
3182
+ const handleMentionSelect = (mention: string, newValue: string) => {
3183
+ setMessage(newValue);
3184
+ setShowMentions(false);
3185
+ setShowFiles(false);
3186
+ setTimeout(() => {
3187
+ if (textareaRef.current) {
3188
+ textareaRef.current.focus();
3189
+ const pos = newValue.indexOf(' ') + 1;
3190
+ textareaRef.current.setSelectionRange(pos, pos);
3191
+ }
3192
+ }, 0);
3193
+ };
3194
+
3195
+ const handleFilePathSelect = (filePath: string, newValue: string) => {
3196
+ setMessage(newValue);
3197
+ setShowFiles(false);
3198
+ setShowMentions(false);
3199
+ setTimeout(() => {
3200
+ if (textareaRef.current) {
3201
+ textareaRef.current.focus();
3202
+ const pos = newValue.indexOf(' ', 1) + 1; // After @path/to/file<space>
3203
+ textareaRef.current.setSelectionRange(pos, pos);
3204
+ }
3205
+ }, 0);
3206
+ };
3207
+
3208
+ const handleSubmit = async (e: React.FormEvent) => {
3209
+ e.preventDefault();
3210
+
3211
+ // Need either message or attachments
3212
+ const hasMessage = message.trim().length > 0;
3213
+ const hasAttachments = attachments.length > 0;
3214
+ if ((!hasMessage && !hasAttachments) || isSending) return;
3215
+
3216
+ // Check if any attachments are still uploading
3217
+ const stillUploading = attachments.some(a => a.isUploading);
3218
+ if (stillUploading) return;
3219
+
3220
+ // Get uploaded attachment IDs
3221
+ const attachmentIds = attachments
3222
+ .filter(a => a.uploadedId)
3223
+ .map(a => a.uploadedId!);
3224
+
3225
+ const mentionMatch = message.match(/^@(\S+)\s*([\s\S]*)/);
3226
+ let target: string;
3227
+ let content: string;
3228
+
3229
+ if (mentionMatch) {
3230
+ const mentionedName = mentionMatch[1];
3231
+ content = mentionMatch[2] || '';
3232
+
3233
+ if (mentionedName === '*' || mentionedName.toLowerCase() === 'everyone' || mentionedName.toLowerCase() === 'all') {
3234
+ target = '*';
3235
+ } else if (mentionedName.toLowerCase().startsWith('team:')) {
3236
+ // Team mention - pass through to backend (e.g., team:frontend)
3237
+ target = mentionedName;
3238
+ } else {
3239
+ // Check if this is a file path mention (contains / or ends with common file extensions)
3240
+ // If so, keep the full message as content and use default recipient
3241
+ if (mentionedName.includes('/') || /\.(ts|tsx|js|jsx|json|md|py|go|rs|java|c|cpp|h|css|html|yaml|yml|toml)$/i.test(mentionedName)) {
3242
+ target = recipient;
3243
+ content = message; // Keep the @path/to/file in the message
3244
+ } else {
3245
+ target = mentionedName;
3246
+ }
3247
+ }
3248
+ } else {
3249
+ target = recipient;
3250
+ content = message;
3251
+ }
3252
+
3253
+ // If no message but has attachments, send with default text
3254
+ if (!content.trim() && attachmentIds.length > 0) {
3255
+ content = '[Screenshot attached]';
3256
+ }
3257
+
3258
+ const success = await onSend(
3259
+ target,
3260
+ content || message,
3261
+ undefined,
3262
+ attachmentIds.length > 0 ? attachmentIds : undefined
3263
+ );
3264
+
3265
+ if (success) {
3266
+ // Clean up previews
3267
+ attachments.forEach(a => URL.revokeObjectURL(a.preview));
3268
+ setMessage('');
3269
+ setAttachments([]);
3270
+ setShowMentions(false);
3271
+ setShowFiles(false);
3272
+ }
3273
+ };
3274
+
3275
+ // Check if we can send (have content or attachments, not uploading)
3276
+ const canSend = (message.trim() || attachments.length > 0) &&
3277
+ !isSending &&
3278
+ !attachments.some(a => a.isUploading);
3279
+
3280
+ return (
3281
+ <form className="flex flex-col gap-1.5 sm:gap-2" onSubmit={handleSubmit}>
3282
+ {/* Attachment previews */}
3283
+ {attachments.length > 0 && (
3284
+ <div className="flex flex-wrap gap-1.5 sm:gap-2 p-1.5 sm:p-2 bg-bg-card rounded-lg border border-border-subtle">
3285
+ {attachments.map(attachment => (
3286
+ <div
3287
+ key={attachment.id}
3288
+ className="relative group"
3289
+ >
3290
+ <img
3291
+ src={attachment.preview}
3292
+ alt={attachment.file.name}
3293
+ className={`h-16 w-auto rounded-lg object-cover ${attachment.isUploading ? 'opacity-50' : ''} ${attachment.error ? 'border-2 border-error' : ''}`}
3294
+ />
3295
+ {attachment.isUploading && (
3296
+ <div className="absolute inset-0 flex items-center justify-center">
3297
+ <svg className="animate-spin h-5 w-5 text-accent-cyan" viewBox="0 0 24 24">
3298
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" strokeDasharray="32" strokeLinecap="round" />
3299
+ </svg>
3300
+ </div>
3301
+ )}
3302
+ {attachment.error && (
3303
+ <div className="absolute bottom-0 left-0 right-0 bg-error/90 text-white text-[10px] px-1 py-0.5 truncate">
3304
+ {attachment.error}
3305
+ </div>
3306
+ )}
3307
+ <button
3308
+ type="button"
3309
+ onClick={() => removeAttachment(attachment.id)}
3310
+ className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-bg-tertiary border border-border-subtle rounded-full flex items-center justify-center text-text-muted hover:text-error hover:border-error transition-colors opacity-0 group-hover:opacity-100"
3311
+ title="Remove"
3312
+ >
3313
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
3314
+ <line x1="18" y1="6" x2="6" y2="18" />
3315
+ <line x1="6" y1="6" x2="18" y2="18" />
3316
+ </svg>
3317
+ </button>
3318
+ </div>
3319
+ ))}
3320
+ </div>
3321
+ )}
3322
+
3323
+ {/* Input row */}
3324
+ <div className="flex items-center gap-1.5 sm:gap-3">
3325
+ {/* Image upload button */}
3326
+ <input
3327
+ ref={fileInputRef}
3328
+ type="file"
3329
+ accept="image/*"
3330
+ multiple
3331
+ className="hidden"
3332
+ onChange={(e) => handleFileSelect(e.target.files)}
3333
+ />
3334
+ <button
3335
+ type="button"
3336
+ onClick={() => fileInputRef.current?.click()}
3337
+ className="p-2 sm:p-2.5 bg-bg-card border border-border-subtle rounded-lg sm:rounded-xl text-text-muted hover:text-accent-cyan hover:border-accent-cyan/50 transition-colors flex-shrink-0"
3338
+ title="Attach screenshot (or paste from clipboard)"
3339
+ >
3340
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="sm:w-[18px] sm:h-[18px]">
3341
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
3342
+ <circle cx="8.5" cy="8.5" r="1.5" />
3343
+ <polyline points="21 15 16 10 5 21" />
3344
+ </svg>
3345
+ </button>
3346
+
3347
+ <div className="flex-1 relative min-w-0">
3348
+ {/* Agent mention autocomplete */}
3349
+ <MentionAutocomplete
3350
+ agents={agents}
3351
+ humanUsers={humanUsers}
3352
+ inputValue={message}
3353
+ cursorPosition={cursorPosition}
3354
+ onSelect={handleMentionSelect}
3355
+ onClose={() => setShowMentions(false)}
3356
+ isVisible={showMentions}
3357
+ />
3358
+ {/* File path autocomplete */}
3359
+ <FileAutocomplete
3360
+ inputValue={message}
3361
+ cursorPosition={cursorPosition}
3362
+ onSelect={handleFilePathSelect}
3363
+ onClose={() => setShowFiles(false)}
3364
+ isVisible={showFiles}
3365
+ />
3366
+ <textarea
3367
+ ref={textareaRef}
3368
+ className="w-full py-2 sm:py-3 px-3 sm:px-4 bg-bg-card border border-border-subtle rounded-lg sm:rounded-xl text-sm font-sans text-text-primary outline-none transition-all duration-200 resize-none min-h-[40px] sm:min-h-[44px] max-h-[100px] sm:max-h-[120px] overflow-y-auto focus:border-accent-cyan/50 focus:shadow-[0_0_0_3px_rgba(0,217,255,0.1)] placeholder:text-text-muted"
3369
+ placeholder={`Message ${recipient === '*' ? 'everyone' : '@' + recipient}...`}
3370
+ value={message}
3371
+ onChange={handleInputChange}
3372
+ onKeyDown={handleKeyDown}
3373
+ onPaste={handlePaste}
3374
+ onSelect={(e) => setCursorPosition((e.target as HTMLTextAreaElement).selectionStart || 0)}
3375
+ disabled={isSending}
3376
+ rows={1}
3377
+ />
3378
+ </div>
3379
+ <button
3380
+ type="submit"
3381
+ className="py-2 sm:py-3 px-3 sm:px-5 bg-gradient-to-r from-accent-cyan to-[#00b8d9] text-bg-deep font-semibold border-none rounded-lg sm:rounded-xl text-xs sm:text-sm cursor-pointer transition-all duration-150 hover:shadow-glow-cyan hover:-translate-y-0.5 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0 disabled:hover:shadow-none flex-shrink-0"
3382
+ disabled={!canSend}
3383
+ title={isSending ? 'Sending...' : attachments.some(a => a.isUploading) ? 'Uploading...' : 'Send message'}
3384
+ >
3385
+ {isSending ? (
3386
+ <span className="hidden sm:inline">Sending...</span>
3387
+ ) : attachments.some(a => a.isUploading) ? (
3388
+ <span className="hidden sm:inline">Uploading...</span>
3389
+ ) : (
3390
+ <span className="flex items-center gap-1 sm:gap-2">
3391
+ <span className="hidden sm:inline">Send</span>
3392
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
3393
+ <line x1="22" y1="2" x2="11" y2="13"></line>
3394
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
3395
+ </svg>
3396
+ </span>
3397
+ )}
3398
+ </button>
3399
+ {error && <span className="text-error text-xs ml-2">{error}</span>}
3400
+ </div>
3401
+ </form>
3402
+ );
3403
+ }
3404
+
3405
+ function LoadingSpinner() {
3406
+ return (
3407
+ <svg className="animate-spin mb-4 text-accent-cyan" width="28" height="28" viewBox="0 0 24 24">
3408
+ <circle
3409
+ cx="12"
3410
+ cy="12"
3411
+ r="10"
3412
+ stroke="currentColor"
3413
+ strokeWidth="2"
3414
+ fill="none"
3415
+ strokeDasharray="32"
3416
+ strokeLinecap="round"
3417
+ />
3418
+ </svg>
3419
+ );
3420
+ }
3421
+
3422
+ function ErrorIcon() {
3423
+ return (
3424
+ <svg className="text-error mb-4" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
3425
+ <circle cx="12" cy="12" r="10" />
3426
+ <line x1="12" y1="8" x2="12" y2="12" />
3427
+ <line x1="12" y1="16" x2="12.01" y2="16" />
3428
+ </svg>
3429
+ );
3430
+ }
3431
+
3432
+ function HashIconLarge() {
3433
+ return (
3434
+ <svg className="text-text-muted mb-4" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
3435
+ <line x1="4" y1="9" x2="20" y2="9" />
3436
+ <line x1="4" y1="15" x2="20" y2="15" />
3437
+ <line x1="10" y1="3" x2="8" y2="21" />
3438
+ <line x1="16" y1="3" x2="14" y2="21" />
3439
+ </svg>
3440
+ );
3441
+ }
3442
+
3443
+ /**
3444
+ * Legacy CSS styles export - kept for backwards compatibility
3445
+ * @deprecated Use Tailwind classes directly instead
3446
+ */
3447
+ export const appStyles = '';