conductor-oss 0.2.15 → 0.2.16

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 (643) hide show
  1. package/node_modules/@conductor-oss/plugin-agent-amp/package.json +1 -1
  2. package/node_modules/@conductor-oss/plugin-agent-ccr/package.json +1 -1
  3. package/node_modules/@conductor-oss/plugin-agent-claude-code/package.json +1 -1
  4. package/node_modules/@conductor-oss/plugin-agent-codex/package.json +1 -1
  5. package/node_modules/@conductor-oss/plugin-agent-cursor-cli/package.json +1 -1
  6. package/node_modules/@conductor-oss/plugin-agent-droid/package.json +1 -1
  7. package/node_modules/@conductor-oss/plugin-agent-gemini/package.json +1 -1
  8. package/node_modules/@conductor-oss/plugin-agent-github-copilot/package.json +1 -1
  9. package/node_modules/@conductor-oss/plugin-agent-opencode/package.json +1 -1
  10. package/node_modules/@conductor-oss/plugin-agent-qwen-code/package.json +1 -1
  11. package/node_modules/@conductor-oss/plugin-mcp-server/package.json +1 -1
  12. package/node_modules/@conductor-oss/plugin-notifier-desktop/package.json +1 -1
  13. package/node_modules/@conductor-oss/plugin-notifier-discord/package.json +1 -1
  14. package/node_modules/@conductor-oss/plugin-runtime-tmux/package.json +1 -1
  15. package/node_modules/@conductor-oss/plugin-scm-github/package.json +1 -1
  16. package/node_modules/@conductor-oss/plugin-terminal-web/package.json +1 -1
  17. package/node_modules/@conductor-oss/plugin-tracker-github/package.json +1 -1
  18. package/node_modules/@conductor-oss/plugin-webhook/package.json +1 -1
  19. package/node_modules/@conductor-oss/plugin-workspace-worktree/package.json +1 -1
  20. package/package.json +21 -21
  21. package/web/.next/standalone/packages/web/.env.example +16 -0
  22. package/web/.next/standalone/packages/web/.next/BUILD_ID +1 -1
  23. package/web/.next/standalone/packages/web/.next/app-path-routes-manifest.json +30 -0
  24. package/web/.next/standalone/packages/web/.next/build-manifest.json +2 -2
  25. package/web/.next/standalone/packages/web/.next/prerender-manifest.json +3 -3
  26. package/web/.next/standalone/packages/web/.next/routes-manifest.json +204 -0
  27. package/web/.next/standalone/packages/web/.next/server/app/_global-error/page.js +2 -2
  28. package/web/.next/standalone/packages/web/.next/server/app/_global-error/page.js.nft.json +1 -1
  29. package/web/.next/standalone/packages/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  30. package/web/.next/standalone/packages/web/.next/server/app/_global-error.html +2 -2
  31. package/web/.next/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
  32. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  33. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  35. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  36. package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  37. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page/server-reference-manifest.json +7 -7
  38. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page.js +2 -2
  39. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  40. package/web/.next/standalone/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  41. package/web/.next/standalone/packages/web/.next/server/app/_not-found.html +1 -1
  42. package/web/.next/standalone/packages/web/.next/server/app/_not-found.rsc +3 -3
  43. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  44. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  45. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  46. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  47. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  48. package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  49. package/web/.next/standalone/packages/web/.next/server/app/api/access/route/app-paths-manifest.json +3 -0
  50. package/web/.next/standalone/packages/web/.next/server/app/api/access/route/build-manifest.json +11 -0
  51. package/web/.next/standalone/packages/web/.next/server/app/api/access/route/server-reference-manifest.json +4 -0
  52. package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js +11 -0
  53. package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js.map +5 -0
  54. package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js.nft.json +1 -0
  55. package/web/.next/standalone/packages/web/.next/server/app/api/access/route_client-reference-manifest.js +2 -0
  56. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js +4 -3
  57. package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js.nft.json +1 -1
  58. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route/app-paths-manifest.json +3 -0
  59. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route/build-manifest.json +11 -0
  60. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route/server-reference-manifest.json +4 -0
  61. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js +10 -0
  62. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js.map +5 -0
  63. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js.nft.json +1 -0
  64. package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route_client-reference-manifest.js +2 -0
  65. package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route/app-paths-manifest.json +3 -0
  66. package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route/build-manifest.json +11 -0
  67. package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route/server-reference-manifest.json +4 -0
  68. package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route.js +8 -0
  69. package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route.js.map +5 -0
  70. package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route.js.nft.json +1 -0
  71. package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route_client-reference-manifest.js +2 -0
  72. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route/app-paths-manifest.json +3 -0
  73. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route/build-manifest.json +11 -0
  74. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route/server-reference-manifest.json +4 -0
  75. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js +12 -0
  76. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js.map +5 -0
  77. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js.nft.json +1 -0
  78. package/web/.next/standalone/packages/web/.next/server/app/api/boards/route_client-reference-manifest.js +2 -0
  79. package/web/.next/standalone/packages/web/.next/server/app/api/config/route/app-paths-manifest.json +3 -0
  80. package/web/.next/standalone/packages/web/.next/server/app/api/config/route/build-manifest.json +11 -0
  81. package/web/.next/standalone/packages/web/.next/server/app/api/config/route/server-reference-manifest.json +4 -0
  82. package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js +10 -0
  83. package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js.map +5 -0
  84. package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js.nft.json +1 -0
  85. package/web/.next/standalone/packages/web/.next/server/app/api/config/route_client-reference-manifest.js +2 -0
  86. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route/app-paths-manifest.json +3 -0
  87. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route/build-manifest.json +11 -0
  88. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route/server-reference-manifest.json +4 -0
  89. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js +10 -0
  90. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js.map +5 -0
  91. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js.nft.json +1 -0
  92. package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route_client-reference-manifest.js +2 -0
  93. package/web/.next/standalone/packages/web/.next/server/app/api/events/route/app-paths-manifest.json +3 -0
  94. package/web/.next/standalone/packages/web/.next/server/app/api/events/route/build-manifest.json +11 -0
  95. package/web/.next/standalone/packages/web/.next/server/app/api/events/route/server-reference-manifest.json +4 -0
  96. package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js +10 -0
  97. package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js.map +5 -0
  98. package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js.nft.json +1 -0
  99. package/web/.next/standalone/packages/web/.next/server/app/api/events/route_client-reference-manifest.js +2 -0
  100. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route/app-paths-manifest.json +3 -0
  101. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route/build-manifest.json +11 -0
  102. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route/server-reference-manifest.json +4 -0
  103. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js +10 -0
  104. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js.map +5 -0
  105. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js.nft.json +1 -0
  106. package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route_client-reference-manifest.js +2 -0
  107. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route/app-paths-manifest.json +3 -0
  108. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route/build-manifest.json +11 -0
  109. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route/server-reference-manifest.json +4 -0
  110. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js +10 -0
  111. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js.map +5 -0
  112. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js.nft.json +1 -0
  113. package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route_client-reference-manifest.js +2 -0
  114. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route/app-paths-manifest.json +3 -0
  115. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route/build-manifest.json +11 -0
  116. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route/server-reference-manifest.json +4 -0
  117. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js +10 -0
  118. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js.map +5 -0
  119. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js.nft.json +1 -0
  120. package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route_client-reference-manifest.js +2 -0
  121. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route/app-paths-manifest.json +3 -0
  122. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route/build-manifest.json +11 -0
  123. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route/server-reference-manifest.json +4 -0
  124. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js +10 -0
  125. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js.map +5 -0
  126. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js.nft.json +1 -0
  127. package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route_client-reference-manifest.js +2 -0
  128. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route/app-paths-manifest.json +3 -0
  129. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route/build-manifest.json +11 -0
  130. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route/server-reference-manifest.json +4 -0
  131. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js +10 -0
  132. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js.map +5 -0
  133. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js.nft.json +1 -0
  134. package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route_client-reference-manifest.js +2 -0
  135. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route/app-paths-manifest.json +3 -0
  136. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route/build-manifest.json +11 -0
  137. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route/server-reference-manifest.json +4 -0
  138. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js +11 -0
  139. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js.map +5 -0
  140. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js.nft.json +1 -0
  141. package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route_client-reference-manifest.js +2 -0
  142. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route/app-paths-manifest.json +3 -0
  143. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route/build-manifest.json +11 -0
  144. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route/server-reference-manifest.json +4 -0
  145. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js +12 -0
  146. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js.map +5 -0
  147. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js.nft.json +1 -0
  148. package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route_client-reference-manifest.js +2 -0
  149. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route/app-paths-manifest.json +3 -0
  150. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route/build-manifest.json +11 -0
  151. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route/server-reference-manifest.json +4 -0
  152. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js +10 -0
  153. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js.map +5 -0
  154. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js.nft.json +1 -0
  155. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route_client-reference-manifest.js +2 -0
  156. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route/app-paths-manifest.json +3 -0
  157. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route/build-manifest.json +11 -0
  158. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route/server-reference-manifest.json +4 -0
  159. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js +10 -0
  160. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js.map +5 -0
  161. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js.nft.json +1 -0
  162. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route_client-reference-manifest.js +2 -0
  163. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route/app-paths-manifest.json +3 -0
  164. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route/build-manifest.json +11 -0
  165. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route/server-reference-manifest.json +4 -0
  166. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js +11 -0
  167. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js.map +5 -0
  168. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js.nft.json +1 -0
  169. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route_client-reference-manifest.js +2 -0
  170. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route/app-paths-manifest.json +3 -0
  171. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route/build-manifest.json +11 -0
  172. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route/server-reference-manifest.json +4 -0
  173. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js +10 -0
  174. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js.map +5 -0
  175. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js.nft.json +1 -0
  176. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route_client-reference-manifest.js +2 -0
  177. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route/app-paths-manifest.json +3 -0
  178. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route/build-manifest.json +11 -0
  179. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route/server-reference-manifest.json +4 -0
  180. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js +10 -0
  181. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js.map +5 -0
  182. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js.nft.json +1 -0
  183. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route_client-reference-manifest.js +2 -0
  184. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route/app-paths-manifest.json +3 -0
  185. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route/build-manifest.json +11 -0
  186. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route/server-reference-manifest.json +4 -0
  187. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route.js +10 -0
  188. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route.js.map +5 -0
  189. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route.js.nft.json +1 -0
  190. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route_client-reference-manifest.js +2 -0
  191. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route/app-paths-manifest.json +3 -0
  192. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route/build-manifest.json +11 -0
  193. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route/server-reference-manifest.json +4 -0
  194. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js +10 -0
  195. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js.map +5 -0
  196. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -0
  197. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route_client-reference-manifest.js +2 -0
  198. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route/app-paths-manifest.json +3 -0
  199. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route/build-manifest.json +11 -0
  200. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route/server-reference-manifest.json +4 -0
  201. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js +10 -0
  202. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js.map +5 -0
  203. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js.nft.json +1 -0
  204. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route_client-reference-manifest.js +2 -0
  205. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route/app-paths-manifest.json +3 -0
  206. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route/build-manifest.json +11 -0
  207. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route/server-reference-manifest.json +4 -0
  208. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js +10 -0
  209. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js.map +5 -0
  210. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js.nft.json +1 -0
  211. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route_client-reference-manifest.js +2 -0
  212. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
  213. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
  214. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
  215. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js +10 -0
  216. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
  217. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
  218. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
  219. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route/app-paths-manifest.json +3 -0
  220. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route/build-manifest.json +11 -0
  221. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route/server-reference-manifest.json +4 -0
  222. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js +10 -0
  223. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js.map +5 -0
  224. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -0
  225. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +2 -0
  226. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route/app-paths-manifest.json +3 -0
  227. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route/build-manifest.json +11 -0
  228. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route/server-reference-manifest.json +4 -0
  229. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route.js +10 -0
  230. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route.js.map +5 -0
  231. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route.js.nft.json +1 -0
  232. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route_client-reference-manifest.js +2 -0
  233. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route/app-paths-manifest.json +3 -0
  234. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route/build-manifest.json +11 -0
  235. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route/server-reference-manifest.json +4 -0
  236. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js +10 -0
  237. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js.map +5 -0
  238. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js.nft.json +1 -0
  239. package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route_client-reference-manifest.js +2 -0
  240. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route/app-paths-manifest.json +3 -0
  241. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route/build-manifest.json +11 -0
  242. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route/server-reference-manifest.json +4 -0
  243. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js +11 -0
  244. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js.map +5 -0
  245. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js.nft.json +1 -0
  246. package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route_client-reference-manifest.js +2 -0
  247. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route/app-paths-manifest.json +3 -0
  248. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route/build-manifest.json +11 -0
  249. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route/server-reference-manifest.json +4 -0
  250. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js +10 -0
  251. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js.map +5 -0
  252. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js.nft.json +1 -0
  253. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route_client-reference-manifest.js +2 -0
  254. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route/app-paths-manifest.json +3 -0
  255. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route/build-manifest.json +11 -0
  256. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route/server-reference-manifest.json +4 -0
  257. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js +12 -0
  258. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js.map +5 -0
  259. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js.nft.json +1 -0
  260. package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route_client-reference-manifest.js +2 -0
  261. package/web/.next/standalone/packages/web/.next/server/app/auth/grant/route.js +4 -2
  262. package/web/.next/standalone/packages/web/.next/server/app/auth/grant/route.js.nft.json +1 -1
  263. package/web/.next/standalone/packages/web/.next/server/app/icon.svg/route.js +3 -1
  264. package/web/.next/standalone/packages/web/.next/server/app/icon.svg/route.js.nft.json +1 -1
  265. package/web/.next/standalone/packages/web/.next/server/app/index.html +1 -1
  266. package/web/.next/standalone/packages/web/.next/server/app/index.rsc +4 -4
  267. package/web/.next/standalone/packages/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  268. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
  269. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  270. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_index.segment.rsc +3 -3
  271. package/web/.next/standalone/packages/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  272. package/web/.next/standalone/packages/web/.next/server/app/page/react-loadable-manifest.json +1 -1
  273. package/web/.next/standalone/packages/web/.next/server/app/page/server-reference-manifest.json +7 -7
  274. package/web/.next/standalone/packages/web/.next/server/app/page.js +2 -2
  275. package/web/.next/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
  276. package/web/.next/standalone/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
  277. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page/server-reference-manifest.json +7 -7
  278. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page.js +2 -2
  279. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
  280. package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  281. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page/server-reference-manifest.json +7 -7
  282. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page.js +2 -2
  283. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page.js.nft.json +1 -1
  284. package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page_client-reference-manifest.js +1 -1
  285. package/web/.next/standalone/packages/web/.next/server/app/unlock/page/server-reference-manifest.json +7 -7
  286. package/web/.next/standalone/packages/web/.next/server/app/unlock/page.js +2 -2
  287. package/web/.next/standalone/packages/web/.next/server/app/unlock/page.js.nft.json +1 -1
  288. package/web/.next/standalone/packages/web/.next/server/app/unlock/page_client-reference-manifest.js +1 -1
  289. package/web/.next/standalone/packages/web/.next/server/app-paths-manifest.json +30 -0
  290. package/web/.next/standalone/packages/web/.next/server/chunks/43d70_next-internal_server_app_api_sessions_[id]_output_stream_route_actions_9bfa500e.js +3 -0
  291. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_filesystem_directory_route_actions_b1ce53b0.js +3 -0
  292. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_health_sessions_route_actions_0e6610db.js +3 -0
  293. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_sessions_[id]_actions_route_actions_e73b30eb.js +3 -0
  294. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_sessions_[id]_checks_route_actions_a3dedc1e.js +3 -0
  295. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_sessions_[id]_diff_route_actions_b95fa63b.js +3 -0
  296. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_sessions_[id]_feedback_route_actions_cb4b8489.js +3 -0
  297. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_sessions_[id]_files_route_actions_22820f6c.js +3 -0
  298. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_sessions_[id]_keys_route_actions_2c93e9dc.js +3 -0
  299. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_sessions_[id]_kill_route_actions_2b9b61d1.js +3 -0
  300. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_sessions_[id]_output_route_actions_e0641c75.js +3 -0
  301. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_sessions_[id]_restore_route_actions_41a4c356.js +3 -0
  302. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_sessions_[id]_send_route_actions_5c1d0b47.js +3 -0
  303. package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_workspaces_branches_route_actions_324a72cd.js +3 -0
  304. package/web/.next/standalone/packages/web/.next/server/chunks/[externals]__f25f12d4._.js +3 -0
  305. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__005aa909._.js +3 -0
  306. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__008d962e._.js +3 -0
  307. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__00cf28e7._.js +3 -0
  308. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__03ff76ee._.js +3 -0
  309. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__06365513._.js +3 -0
  310. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__075f8697._.js +3 -0
  311. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__097be5a7._.js +3 -0
  312. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__09c3bdaf._.js +7 -0
  313. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__0a0f0f01._.js +3 -0
  314. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1003246b._.js +3 -0
  315. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__11918223._.js +3 -0
  316. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__13ad088c._.js +3 -0
  317. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1458f8ec._.js +3 -0
  318. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__14b1d4a3._.js +3 -0
  319. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1741fb8a._.js +3 -0
  320. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1875f00e._.js +3 -0
  321. package/web/.next/standalone/packages/web/.next/server/chunks/{[root-of-the-server]__9fa7727e._.js → [root-of-the-server]__1aa339ea._.js} +2 -2
  322. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1beced71._.js +3 -0
  323. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1ed2e6c1._.js +226 -0
  324. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1f626a16._.js +129 -0
  325. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2062aaab._.js +3 -0
  326. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__206c3d8a._.js +3 -0
  327. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__22f8f0d6._.js +129 -0
  328. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__23bebaf9._.js +3 -0
  329. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2556a816._.js +3 -0
  330. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__25788ed3._.js +54 -0
  331. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__29be362d._.js +3 -0
  332. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2a83553a._.js +3 -0
  333. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2c3f6397._.js +3 -0
  334. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2c909fe3._.js +3 -0
  335. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2ce39d51._.js +3 -0
  336. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__30921756._.js +3 -0
  337. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__31eea08c._.js +3 -0
  338. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__3d7ad620._.js +3 -0
  339. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__3edd3509._.js +3 -0
  340. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__3f8b49ba._.js +3 -0
  341. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__3f8e82c2._.js +3 -0
  342. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__44d59fd0._.js +3 -0
  343. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__48875cbb._.js +7 -0
  344. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__49c6a20e._.js +3 -0
  345. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__4ac9d639._.js +3 -0
  346. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__4c3fb752._.js +3 -0
  347. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__4ca8b259._.js +3 -0
  348. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__4dab6a6a._.js +3 -0
  349. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__544326ac._.js +3 -0
  350. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__54a218f0._.js +54 -0
  351. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__55e337f7._.js +3 -0
  352. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5661ca72._.js +3 -0
  353. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5cd49624._.js +3 -0
  354. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5e0533de._.js +3 -0
  355. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5e161db4._.js +3 -0
  356. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5ebcb223._.js +3 -0
  357. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5fd609c9._.js +3 -0
  358. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6023a748._.js +3 -0
  359. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__60cf7bf4._.js +3 -0
  360. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__60ee4897._.js +3 -0
  361. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6559c1a4._.js +3 -0
  362. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__659938b1._.js +3 -0
  363. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6712f45a._.js +3 -0
  364. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__69227de6._.js +3 -0
  365. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6ab08a39._.js +129 -0
  366. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6cd54de1._.js +3 -0
  367. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6ce2b5d7._.js +3 -0
  368. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6cfd00d2._.js +3 -0
  369. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6d62709d._.js +129 -0
  370. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6e08e4e4._.js +3 -0
  371. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__749331b0._.js +54 -0
  372. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__74a53184._.js +3 -0
  373. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__7633d324._.js +4 -0
  374. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__76c16426._.js +3 -0
  375. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__778e46a1._.js +129 -0
  376. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__7a26e452._.js +3 -0
  377. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__7ba96fa5._.js +3 -0
  378. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__7e684560._.js +3 -0
  379. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__848eb266._.js +3 -0
  380. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__8596d782._.js +7 -0
  381. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__85d8966b._.js +3 -0
  382. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__86aff2b8._.js +3 -0
  383. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__879cf4d2._.js +3 -0
  384. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__881858fa._.js +3 -0
  385. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__885e3a00._.js +54 -0
  386. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__887ea436._.js +3 -0
  387. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__893f6b3b._.js +3 -0
  388. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__89dfcd05._.js +3 -0
  389. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__8cc5826f._.js +3 -0
  390. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__913af875._.js +3 -0
  391. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__91b42119._.js +3 -0
  392. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__93ce1b75._.js +3 -0
  393. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__96633022._.js +3 -0
  394. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__96951362._.js +3 -0
  395. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__971fcc75._.js +9 -0
  396. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__98c6a6f3._.js +3 -0
  397. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__99da6324._.js +3 -0
  398. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__9c1d09aa._.js +3 -0
  399. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__9f07930d._.js +3 -0
  400. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__9f36b7ec._.js +129 -0
  401. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__9fd08bbc._.js +3 -0
  402. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a05953cc._.js +3 -0
  403. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a3766ac1._.js +3 -0
  404. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a3bfaabf._.js +3 -0
  405. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a420e374._.js +3 -0
  406. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a4a33247._.js +54 -0
  407. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ac544bb5._.js +3 -0
  408. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ad84d2e0._.js +54 -0
  409. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__b3095e9d._.js +3 -0
  410. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__b59abfbd._.js +3 -0
  411. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__b9692a3c._.js +3 -0
  412. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__bcc08c70._.js +3 -0
  413. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__be051d52._.js +3 -0
  414. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__bea07e07._.js +3 -0
  415. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__bf8faac8._.js +4 -0
  416. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__c1759514._.js +3 -0
  417. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__c2ef2bfc._.js +3 -0
  418. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__c3c6a7bf._.js +3 -0
  419. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__c8618cb1._.js +3 -0
  420. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ca8a18f8._.js +3 -0
  421. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__cafd7bb3._.js +3 -0
  422. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__cb112606._.js +3 -0
  423. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__cc74ea40._.js +3 -0
  424. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__cd3692a0._.js +3 -0
  425. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__cf3cdecd._.js +3 -0
  426. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__d09e7a99._.js +3 -0
  427. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__d40b50e7._.js +3 -0
  428. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__d4721d30._.js +3 -0
  429. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__d538c110._.js +3 -0
  430. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__d73ea818._.js +3 -0
  431. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__db3d0133._.js +3 -0
  432. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__dd0cf085._.js +3 -0
  433. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__de0f2533._.js +7 -0
  434. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e113f6df._.js +3 -0
  435. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e1a418bc._.js +3 -0
  436. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e6c1d827._.js +3 -0
  437. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ea69c4ce._.js +3 -0
  438. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__eac16890._.js +3 -0
  439. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__eb1dfcc6._.js +3 -0
  440. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ee75d43a._.js +3 -0
  441. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ef773ea6._.js +3 -0
  442. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f0d15920._.js +3 -0
  443. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f15cbcfc._.js +3 -0
  444. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f48eb2d4._.js +3 -0
  445. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__fb6ab669._.js +3 -0
  446. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__fe8419b5._.js +129 -0
  447. package/web/.next/standalone/packages/web/.next/server/chunks/{[root-of-the-server]__667a63ee._.js → node_modules_07d85415._.js} +2 -2
  448. package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_f0726c19._.js +52 -0
  449. package/web/.next/standalone/packages/web/.next/server/chunks/{[root-of-the-server]__d94cee69._.js → node_modules_next_9a15df2c._.js} +2 -2
  450. package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_79f1aee4._.js +6 -0
  451. package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0e4dc4f7.js +3 -0
  452. package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_dd27f857.js +3 -0
  453. package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_e445b36c.js +3 -0
  454. package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_f375f3c4.js +28 -0
  455. package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_f2da0d3e._.js +14 -0
  456. package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_f5199d09._.js +3 -0
  457. package/web/.next/standalone/packages/web/.next/server/chunks/packages_core_dist_index_cdd30418.js +3 -0
  458. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-amp_dist_index_bf3d8239.js +3 -0
  459. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-ccr_dist_index_69cbf726.js +3 -0
  460. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-claude-code_dist_index_31314e43.js +125 -0
  461. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-cursor-cli_dist_index_8d5cc426.js +3 -0
  462. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-droid_dist_index_f731227a.js +3 -0
  463. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-gemini_dist_index_b2d219e8.js +54 -0
  464. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-github-copilot_dist_index_9f80cb45.js +3 -0
  465. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-opencode_dist_index_7c7015da.js +3 -0
  466. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-qwen-code_dist_index_46dd8653.js +3 -0
  467. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_notifier-desktop_dist_index_ef3473f0.js +3 -0
  468. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_notifier-discord_dist_index_34257226.js +3 -0
  469. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_scm-github_dist_index_3b5a621e.js +3 -0
  470. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_terminal-web_dist_index_1e0706d3.js +3 -0
  471. package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_tracker-github_dist_index_2d7af62f.js +3 -0
  472. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_access_route_actions_06740e99.js +3 -0
  473. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_attachments_route_actions_4dcb6bab.js +3 -0
  474. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_auth_session_route_actions_078c0ee4.js +3 -0
  475. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_boards_route_actions_160005aa.js +3 -0
  476. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_config_route_actions_ee96617d.js +3 -0
  477. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_context-files_route_actions_e0f9c4a1.js +3 -0
  478. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_events_route_actions_bde0da23.js +3 -0
  479. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_github_repos_route_actions_7eab8e08.js +3 -0
  480. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_health_boards_route_actions_7a65c06e.js +3 -0
  481. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_notifications_route_actions_c5e4154e.js +3 -0
  482. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_preferences_route_actions_858fa484.js +3 -0
  483. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_repositories_route_actions_0232075d.js +3 -0
  484. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_sessions_[id]_route_actions_6fdcb5a6.js +3 -0
  485. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_sessions_route_actions_c6df1747.js +3 -0
  486. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_spawn_route_actions_befa67f2.js +3 -0
  487. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_workspaces_route_actions_795ab8d9.js +3 -0
  488. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web_src_lib_13fee5eb._.js +3 -0
  489. package/web/.next/standalone/packages/web/.next/server/chunks/packages_web_src_lib_d64596cc._.js +3 -0
  490. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__09178ceb._.js +3 -0
  491. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__2b5e3ef2._.js → [root-of-the-server]__504277b5._.js} +2 -2
  492. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__b07a94f8._.js → [root-of-the-server]__525ee5d4._.js} +2 -2
  493. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__6622b514._.js +4 -0
  494. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__869d9ac0._.js +4 -0
  495. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__acdcadf9._.js → [root-of-the-server]__9726f664._.js} +2 -2
  496. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__b3463b25._.js +3 -0
  497. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__ca05f4a0._.js +4 -0
  498. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__dd8d1ff5._.js +3 -0
  499. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_0e1412de._.js +1 -1
  500. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_69e05fca._.js +1 -1
  501. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_80efe193._.js +1 -1
  502. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_b6d31783._.js +1 -1
  503. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_c0f0e227._.js +1 -1
  504. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_f36ddaa9._.js +1 -1
  505. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_8a444735._.js +3 -0
  506. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{node_modules_@clerk_nextjs_dist_esm_app-router_622c0d1e._.js → node_modules_@clerk_nextjs_dist_esm_app-router_bf11b471._.js} +2 -2
  507. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_f2ebd7a9._.js +1 -1
  508. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{packages_web_src_components_9f3d5f00._.js → packages_web_src_components_dd296b40._.js} +1 -1
  509. package/web/.next/standalone/packages/web/.next/server/middleware.js +2 -2
  510. package/web/.next/standalone/packages/web/.next/server/pages/404.html +1 -1
  511. package/web/.next/standalone/packages/web/.next/server/pages/500.html +2 -2
  512. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
  513. package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.json +8 -8
  514. package/web/.next/standalone/packages/web/.next/static/chunks/19d7d4e2cfe3261a.js +1 -0
  515. package/web/.next/standalone/packages/web/.next/static/chunks/{04f4c17a714eeae7.js → 86bce89c37cf6212.js} +1 -1
  516. package/web/.next/standalone/packages/web/.next/static/chunks/{9b34f9af98cb77d6.js → ab8ef6c7dfc78082.js} +1 -1
  517. package/web/.next/standalone/packages/web/.next/static/chunks/{8d30faf57f99d301.js → e8283780c26eaa91.js} +1 -1
  518. package/web/.next/{static/chunks/61ead0a3899b1761.js → standalone/packages/web/.next/static/chunks/e88fa6d41d743b9e.js} +2 -2
  519. package/web/.next/{static/chunks/c0b1f9af564807f3.css → standalone/packages/web/.next/static/chunks/fe557eb4039d8a8d.css} +1 -1
  520. package/web/.next/standalone/packages/web/next.config.ts +33 -0
  521. package/web/.next/standalone/packages/web/postcss.config.mjs +5 -0
  522. package/web/.next/standalone/packages/web/src/app/api/access/route.ts +214 -0
  523. package/web/.next/standalone/packages/web/src/app/api/agents/route.ts +223 -0
  524. package/web/.next/standalone/packages/web/src/app/api/attachments/route.ts +169 -0
  525. package/web/.next/standalone/packages/web/src/app/api/auth/session/route.ts +54 -0
  526. package/web/.next/standalone/packages/web/src/app/api/boards/route.ts +520 -0
  527. package/web/.next/standalone/packages/web/src/app/api/config/route.ts +81 -0
  528. package/web/.next/standalone/packages/web/src/app/api/context-files/route.ts +201 -0
  529. package/web/.next/standalone/packages/web/src/app/api/events/route.ts +125 -0
  530. package/web/.next/standalone/packages/web/src/app/api/filesystem/directory/route.ts +145 -0
  531. package/web/.next/standalone/packages/web/src/app/api/github/repos/route.ts +106 -0
  532. package/web/.next/standalone/packages/web/src/app/api/health/boards/route.ts +72 -0
  533. package/web/.next/standalone/packages/web/src/app/api/health/sessions/route.ts +77 -0
  534. package/web/.next/standalone/packages/web/src/app/api/notifications/route.ts +91 -0
  535. package/web/.next/standalone/packages/web/src/app/api/preferences/route.ts +194 -0
  536. package/web/.next/standalone/packages/web/src/app/api/repositories/route.ts +419 -0
  537. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/actions/route.ts +80 -0
  538. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/checks/route.ts +175 -0
  539. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/diff/route.ts +426 -0
  540. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/feedback/route.ts +47 -0
  541. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/files/route.ts +197 -0
  542. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/keys/route.ts +140 -0
  543. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/kill/route.ts +45 -0
  544. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/output/route.ts +262 -0
  545. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/output/stream/route.ts +111 -0
  546. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/restore/route.ts +49 -0
  547. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/route.ts +206 -0
  548. package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/send/route.ts +58 -0
  549. package/web/.next/standalone/packages/web/src/app/api/sessions/route.ts +105 -0
  550. package/web/.next/standalone/packages/web/src/app/api/spawn/route.ts +216 -0
  551. package/web/.next/standalone/packages/web/src/app/api/workspaces/branches/route.ts +233 -0
  552. package/web/.next/standalone/packages/web/src/app/api/workspaces/route.ts +592 -0
  553. package/web/.next/standalone/packages/web/src/app/auth/grant/route.ts +37 -0
  554. package/web/.next/standalone/packages/web/src/app/globals.css +206 -0
  555. package/web/.next/standalone/packages/web/src/app/icon.svg +11 -0
  556. package/web/.next/standalone/packages/web/src/app/layout.tsx +65 -0
  557. package/web/.next/standalone/packages/web/src/app/page.tsx +4559 -0
  558. package/web/.next/standalone/packages/web/src/app/sessions/[id]/page.tsx +120 -0
  559. package/web/.next/standalone/packages/web/src/app/sign-in/[[...sign-in]]/page.tsx +11 -0
  560. package/web/.next/standalone/packages/web/src/app/unlock/UnlockForm.tsx +77 -0
  561. package/web/.next/standalone/packages/web/src/app/unlock/page.tsx +41 -0
  562. package/web/.next/standalone/packages/web/src/components/ActivityDot.tsx +93 -0
  563. package/web/.next/standalone/packages/web/src/components/AgentTileIcon.tsx +193 -0
  564. package/web/.next/standalone/packages/web/src/components/Dashboard.tsx +3444 -0
  565. package/web/.next/standalone/packages/web/src/components/EmptyState.tsx +58 -0
  566. package/web/.next/standalone/packages/web/src/components/SessionCard.tsx +377 -0
  567. package/web/.next/standalone/packages/web/src/components/TerminalView.tsx +184 -0
  568. package/web/.next/standalone/packages/web/src/components/ThemeProvider.tsx +55 -0
  569. package/web/.next/standalone/packages/web/src/components/agents/AgentGrid.tsx +95 -0
  570. package/web/.next/standalone/packages/web/src/components/board/WorkspaceKanban.tsx +1316 -0
  571. package/web/.next/standalone/packages/web/src/components/layout/AppShell.tsx +74 -0
  572. package/web/.next/standalone/packages/web/src/components/layout/EmptyState.tsx +53 -0
  573. package/web/.next/standalone/packages/web/src/components/layout/Sidebar.tsx +318 -0
  574. package/web/.next/standalone/packages/web/src/components/layout/TopBar.tsx +34 -0
  575. package/web/.next/standalone/packages/web/src/components/layout/WorkspaceSidebarPanel.tsx +93 -0
  576. package/web/.next/standalone/packages/web/src/components/sessions/ChatPanel.tsx +1559 -0
  577. package/web/.next/standalone/packages/web/src/components/sessions/SessionDetail.tsx +132 -0
  578. package/web/.next/standalone/packages/web/src/components/sessions/SessionDiff.tsx +1197 -0
  579. package/web/.next/standalone/packages/web/src/components/sessions/SessionOverview.tsx +314 -0
  580. package/web/.next/standalone/packages/web/src/components/ui/Badge.tsx +33 -0
  581. package/web/.next/standalone/packages/web/src/components/ui/Button.tsx +53 -0
  582. package/web/.next/standalone/packages/web/src/components/ui/Card.tsx +35 -0
  583. package/web/.next/standalone/packages/web/src/components/ui/RunningDots.tsx +22 -0
  584. package/web/.next/standalone/packages/web/src/components/ui/ScrollArea.tsx +43 -0
  585. package/web/.next/standalone/packages/web/src/components/ui/Separator.tsx +23 -0
  586. package/web/.next/standalone/packages/web/src/components/ui/StatusDot.tsx +30 -0
  587. package/web/.next/standalone/packages/web/src/components/ui/Tabs.tsx +48 -0
  588. package/web/.next/standalone/packages/web/src/components/ui/Tooltip.tsx +44 -0
  589. package/web/.next/standalone/packages/web/src/hooks/useAgents.ts +155 -0
  590. package/web/.next/standalone/packages/web/src/hooks/useConfig.ts +116 -0
  591. package/web/.next/standalone/packages/web/src/hooks/usePreferences.ts +95 -0
  592. package/web/.next/standalone/packages/web/src/hooks/useSession.ts +191 -0
  593. package/web/.next/standalone/packages/web/src/hooks/useSessionFeed.ts +265 -0
  594. package/web/.next/standalone/packages/web/src/hooks/useSessionOutputStream.ts +155 -0
  595. package/web/.next/standalone/packages/web/src/hooks/useSessions.ts +123 -0
  596. package/web/.next/standalone/packages/web/src/lib/accessControl.test.ts +46 -0
  597. package/web/.next/standalone/packages/web/src/lib/accessControl.ts +115 -0
  598. package/web/.next/standalone/packages/web/src/lib/agentUtils.ts +4 -0
  599. package/web/.next/standalone/packages/web/src/lib/auth.ts +457 -0
  600. package/web/.next/standalone/packages/web/src/lib/chatFeed.ts +196 -0
  601. package/web/.next/standalone/packages/web/src/lib/cn.ts +6 -0
  602. package/web/.next/standalone/packages/web/src/lib/edgeAuth.test.ts +91 -0
  603. package/web/.next/standalone/packages/web/src/lib/edgeAuth.ts +205 -0
  604. package/web/.next/standalone/packages/web/src/lib/editorLinks.ts +51 -0
  605. package/web/.next/standalone/packages/web/src/lib/event-bus-singleton.ts +50 -0
  606. package/web/.next/standalone/packages/web/src/lib/liveEvents.ts +49 -0
  607. package/web/.next/standalone/packages/web/src/lib/modelAccess.ts +40 -0
  608. package/web/.next/standalone/packages/web/src/lib/projectConfigSync.ts +144 -0
  609. package/web/.next/standalone/packages/web/src/lib/remoteAuth.ts +104 -0
  610. package/web/.next/standalone/packages/web/src/lib/runtimeAgentModels.test.ts +78 -0
  611. package/web/.next/standalone/packages/web/src/lib/runtimeAgentModels.ts +788 -0
  612. package/web/.next/standalone/packages/web/src/lib/runtimeAgentModelsShared.ts +65 -0
  613. package/web/.next/standalone/packages/web/src/lib/serialize.ts +128 -0
  614. package/web/.next/standalone/packages/web/src/lib/services.ts +319 -0
  615. package/web/.next/standalone/packages/web/src/lib/types.ts +182 -0
  616. package/web/.next/standalone/packages/web/src/proxy.ts +141 -0
  617. package/web/.next/standalone/packages/web/tsconfig.json +42 -0
  618. package/web/.next/static/chunks/19d7d4e2cfe3261a.js +1 -0
  619. package/web/.next/static/chunks/{04f4c17a714eeae7.js → 86bce89c37cf6212.js} +1 -1
  620. package/web/.next/static/chunks/{9b34f9af98cb77d6.js → ab8ef6c7dfc78082.js} +1 -1
  621. package/web/.next/static/chunks/{8d30faf57f99d301.js → e8283780c26eaa91.js} +1 -1
  622. package/web/.next/{standalone/packages/web/.next/static/chunks/61ead0a3899b1761.js → static/chunks/e88fa6d41d743b9e.js} +2 -2
  623. package/web/.next/{standalone/packages/web/.next/static/chunks/c0b1f9af564807f3.css → static/chunks/fe557eb4039d8a8d.css} +1 -1
  624. package/web/.next/standalone/packages/web/.next/server/chunks/[externals]__0d8e2e8d._.js +0 -3
  625. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__273dc5fd._.js +0 -3
  626. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__30b98e6f._.js +0 -4
  627. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__bbaa7179._.js +0 -3
  628. package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f408c708._.js +0 -21
  629. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__434b20a4._.js +0 -3
  630. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__90837659._.js +0 -3
  631. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__977a17ba._.js +0 -3
  632. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__bce4b756._.js +0 -4
  633. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__cb11b5eb._.js +0 -4
  634. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__d1edf1ae._.js +0 -4
  635. package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_d6c6c559._.js +0 -3
  636. package/web/.next/standalone/packages/web/.next/static/chunks/2ddcd95a6de1ac4a.js +0 -1
  637. package/web/.next/static/chunks/2ddcd95a6de1ac4a.js +0 -1
  638. /package/web/.next/standalone/packages/web/.next/static/{fLVGQVTN7GT-GjBotJjTr → 7KQ5wRS3BC3qvrkz8yxuS}/_buildManifest.js +0 -0
  639. /package/web/.next/standalone/packages/web/.next/static/{fLVGQVTN7GT-GjBotJjTr → 7KQ5wRS3BC3qvrkz8yxuS}/_clientMiddlewareManifest.json +0 -0
  640. /package/web/.next/standalone/packages/web/.next/static/{fLVGQVTN7GT-GjBotJjTr → 7KQ5wRS3BC3qvrkz8yxuS}/_ssgManifest.js +0 -0
  641. /package/web/.next/static/{fLVGQVTN7GT-GjBotJjTr → 7KQ5wRS3BC3qvrkz8yxuS}/_buildManifest.js +0 -0
  642. /package/web/.next/static/{fLVGQVTN7GT-GjBotJjTr → 7KQ5wRS3BC3qvrkz8yxuS}/_clientMiddlewareManifest.json +0 -0
  643. /package/web/.next/static/{fLVGQVTN7GT-GjBotJjTr → 7KQ5wRS3BC3qvrkz8yxuS}/_ssgManifest.js +0 -0
@@ -0,0 +1,3444 @@
1
+ // @ts-nocheck
2
+ // DEPRECATED: This monolith is no longer imported. Kept for reference only.
3
+ // New UI lives in components/layout/, components/sessions/, components/agents/, components/ui/
4
+ "use client";
5
+
6
+ import {
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ useCallback,
12
+ type FormEvent,
13
+ type ReactNode,
14
+ } from "react";
15
+ import { useRouter } from "next/navigation";
16
+ import type {
17
+ DashboardSession,
18
+ DashboardStats,
19
+ SSESnapshotEvent,
20
+ } from "@/lib/types";
21
+ import { getAttentionLevel } from "@/lib/types";
22
+ import { AgentTileIcon } from "./AgentTileIcon";
23
+ import { TERMINAL_STATUSES } from "@conductor-oss/core/types";
24
+ import { SessionCard } from "./SessionCard";
25
+ import { EmptyState } from "./EmptyState";
26
+ import { useTheme } from "./ThemeProvider";
27
+
28
+ type ConfigProject = {
29
+ id: string;
30
+ boardDir: string;
31
+ boardFile?: string;
32
+ repo: string | null;
33
+ iconUrl?: string | null;
34
+ description: string | null;
35
+ agent: string;
36
+ };
37
+ type AgentInfo = {
38
+ name: string;
39
+ description: string | null;
40
+ version: string | null;
41
+ homepage: string | null;
42
+ iconUrl: string | null;
43
+ };
44
+
45
+ type DashboardTab = "overview" | "chat" | "review" | "agents";
46
+ type ReviewDiffSource = "working-tree" | "remote-pr" | "not-found";
47
+
48
+ type AgentRoster = {
49
+ name: string;
50
+ label: string;
51
+ launchName: string;
52
+ known: boolean;
53
+ installed: boolean;
54
+ description: string | null;
55
+ version: string | null;
56
+ homepage: string | null;
57
+ iconUrl: string | null;
58
+ capabilities: string[];
59
+ commandHint: string | null;
60
+ totalSessions: number;
61
+ activeSessions: number;
62
+ attentionSessions: number;
63
+ };
64
+
65
+ type ReviewDiffKind = "meta" | "hunk" | "context" | "add" | "remove" | "info";
66
+
67
+ interface ReviewDiffLine {
68
+ kind: ReviewDiffKind;
69
+ oldLine: number | null;
70
+ newLine: number | null;
71
+ text: string;
72
+ }
73
+
74
+ interface ReviewDiffFile {
75
+ path: string;
76
+ status: "modified" | "added" | "deleted" | "renamed" | "copy" | "binary" | "unknown";
77
+ additions: number;
78
+ deletions: number;
79
+ lines: ReviewDiffLine[];
80
+ }
81
+
82
+ interface ReviewDiffPayload {
83
+ hasDiff: boolean;
84
+ generatedAt: string;
85
+ source: ReviewDiffSource;
86
+ truncated: boolean;
87
+ files: ReviewDiffFile[];
88
+ untracked: string[];
89
+ error?: string;
90
+ }
91
+
92
+ type ReviewDiffResponse = ReviewDiffPayload | { error: string };
93
+
94
+ type CICheckStatus = "pending" | "running" | "passed" | "failed" | "skipped" | "unknown";
95
+
96
+ interface CICheckInfo {
97
+ name: string;
98
+ status: CICheckStatus;
99
+ url?: string;
100
+ }
101
+
102
+ interface CIChecksPayload {
103
+ sessionId: string;
104
+ source: string;
105
+ ciStatus: "pending" | "passing" | "failing" | "none";
106
+ checks: CICheckInfo[];
107
+ generatedAt: string;
108
+ }
109
+
110
+ interface SessionChecksState {
111
+ loading: boolean;
112
+ loaded: boolean;
113
+ ciStatus: "pending" | "passing" | "failing" | "none";
114
+ checks: CICheckInfo[];
115
+ source: string;
116
+ generatedAt: string;
117
+ error: string | null;
118
+ }
119
+
120
+ type LogoIconProps = {
121
+ className?: string;
122
+ fillColor?: string;
123
+ };
124
+
125
+ interface ReviewDiffState {
126
+ loading: boolean;
127
+ loaded: boolean;
128
+ hasDiff: boolean;
129
+ source: ReviewDiffSource;
130
+ truncated: boolean;
131
+ files: ReviewDiffFile[];
132
+ untracked: string[];
133
+ generatedAt: string;
134
+ selectedFilePath: string | null;
135
+ fileSearch: string;
136
+ search: string;
137
+ wrapLines: boolean;
138
+ error: string | null;
139
+ }
140
+
141
+ type KnownAgent = {
142
+ id: string;
143
+ label: string;
144
+ launchName: string;
145
+ aliases?: string[];
146
+ description: string;
147
+ homepage?: string;
148
+ iconUrl?: string;
149
+ installHint?: string;
150
+ launchCommand?: string;
151
+ capabilities?: string[];
152
+ };
153
+
154
+ type AgentIconSeed = {
155
+ label: string;
156
+ launchName: string;
157
+ iconUrl?: string | null;
158
+ homepage?: string | null;
159
+ };
160
+
161
+ function asSimpleIconSlug(value: string): string {
162
+ return normalizeAgentName(value);
163
+ }
164
+
165
+ function normalizeAgentName(value: string): string {
166
+ return value
167
+ .trim()
168
+ .toLowerCase()
169
+ .replace(/[^a-z0-9]+/g, "-")
170
+ .replace(/-+/g, "-")
171
+ .replace(/^-+|-+$/g, "");
172
+ }
173
+
174
+ function resolveRepoUrl(repo?: string | null): string | null {
175
+ if (!repo) return null;
176
+ const trimmed = repo.trim();
177
+ if (!trimmed) return null;
178
+
179
+ if (/^https?:\/\//i.test(trimmed)) {
180
+ return trimmed;
181
+ }
182
+
183
+ if (trimmed.startsWith("github.com/") || trimmed.startsWith("www.github.com/")) {
184
+ return `https://${trimmed}`;
185
+ }
186
+
187
+ if (trimmed.includes("/")) {
188
+ return `https://github.com/${trimmed}`;
189
+ }
190
+
191
+ return null;
192
+ }
193
+
194
+ function parseGithubRepo(repo: string | null): { owner: string; name: string } | null {
195
+ const resolved = resolveRepoUrl(repo);
196
+ if (!resolved) return null;
197
+ try {
198
+ const url = new URL(resolved);
199
+ const isGithub =
200
+ url.hostname === "github.com" ||
201
+ url.hostname === "www.github.com" ||
202
+ url.hostname.endsWith(".github.com");
203
+ if (!isGithub) return null;
204
+
205
+ const parts = url.pathname.replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
206
+ if (parts.length < 2) return null;
207
+
208
+ return {
209
+ owner: parts[0],
210
+ name: parts[1],
211
+ };
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+
217
+ function getProjectFaviconUrls(repo?: string | null, iconUrl?: string | null): string[] {
218
+ if (iconUrl) {
219
+ const normalized = iconUrl.trim();
220
+ if (!normalized) return [];
221
+ if (!/^https?:\/\//i.test(normalized)) return [];
222
+ return [iconUrl.trim()];
223
+ }
224
+
225
+ const resolved = resolveRepoUrl(repo);
226
+ try {
227
+ if (!resolved) return [];
228
+ const url = new URL(resolved);
229
+ const github = parseGithubRepo(repo ?? null);
230
+ if (github) {
231
+ const owner = encodeURIComponent(github.owner);
232
+ const project = encodeURIComponent(github.name);
233
+ const repoIconFiles = [
234
+ "favicon.ico",
235
+ "public/favicon.ico",
236
+ "assets/favicon.ico",
237
+ ".github/favicon.ico",
238
+ "static/favicon.ico",
239
+ "logo.png",
240
+ "public/logo.png",
241
+ "assets/logo.png",
242
+ ".github/logo.png",
243
+ ];
244
+ const repoAssetUrls = repoIconFiles.map((file) => `https://raw.githubusercontent.com/${owner}/${project}/HEAD/${file}`);
245
+ return [...new Set([...repoAssetUrls, `https://opengraph.githubassets.com/1/${owner}/${project}`])];
246
+ }
247
+
248
+ return [
249
+ `https://www.google.com/s2/favicons?sz=64&domain_url=${encodeURIComponent(url.toString())}`,
250
+ `https://icons.duckduckgo.com/ip3/${encodeURIComponent(url.hostname)}.ico`,
251
+ `https://api.faviconkit.com/${encodeURIComponent(url.hostname)}/64`,
252
+ ];
253
+ } catch {
254
+ return [];
255
+ }
256
+ }
257
+
258
+ function getProjectAbbrev(projectId: string): string {
259
+ const parts = projectId.split(/[-_\s/]+/).filter(Boolean);
260
+ if (parts.length === 1) {
261
+ return parts[0].slice(0, 2).toUpperCase();
262
+ }
263
+ return parts
264
+ .slice(0, 2)
265
+ .map((part) => part[0])
266
+ .join("")
267
+ .toUpperCase();
268
+ }
269
+
270
+ function DefaultProjectIcon({ projectId, color }: { projectId: string; color: string }) {
271
+ const fallback = getProjectAbbrev(projectId);
272
+ return (
273
+ <span
274
+ className="flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-[10px] font-semibold text-white"
275
+ style={{ backgroundColor: color }}
276
+ aria-hidden="true"
277
+ >
278
+ {fallback}
279
+ </span>
280
+ );
281
+ }
282
+
283
+ function AgentIcon({ agent, className = "h-5 w-5" }: { agent: AgentIconSeed; className?: string }) {
284
+ return (
285
+ <span className="inline-flex rounded-sm border border-[var(--color-border-subtle)] bg-white p-[1px]">
286
+ <AgentTileIcon
287
+ seed={{
288
+ label: agent.label,
289
+ homepage: agent.homepage,
290
+ iconUrl: agent.iconUrl,
291
+ }}
292
+ className={className}
293
+ />
294
+ </span>
295
+ );
296
+ }
297
+
298
+ const KNOWN_AGENTS: KnownAgent[] = [
299
+ {
300
+ id: "claude-code",
301
+ label: "Claude Code",
302
+ launchName: "claude-code",
303
+ iconUrl: "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/claude.svg",
304
+ aliases: [
305
+ "claude code",
306
+ "claude-code",
307
+ "claude_code",
308
+ "claude-code-cli",
309
+ "cc",
310
+ "claude",
311
+ "claude-cli",
312
+ "claudecode",
313
+ ],
314
+ description: "Claude Code CLI",
315
+ homepage: "https://www.anthropic.com/claude",
316
+ installHint: "npm install -g @anthropic-ai/claude-code",
317
+ launchCommand: "claude-code",
318
+ capabilities: ["chat", "review", "code review", "agentic"],
319
+ },
320
+ {
321
+ id: "codex",
322
+ label: "OpenAI Codex",
323
+ launchName: "codex",
324
+ aliases: [
325
+ "openai-codex",
326
+ "openai_codex",
327
+ "openai codex",
328
+ "openai",
329
+ "open-ai",
330
+ "open ai",
331
+ "openai-codex-cli",
332
+ "codexcli",
333
+ "codex",
334
+ ],
335
+ description: "OpenAI Codex CLI",
336
+ homepage: "https://github.com/openai/codex",
337
+ iconUrl: "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/openai.svg",
338
+ installHint: "npm install -g @openai/codex",
339
+ launchCommand: "codex",
340
+ capabilities: ["chat", "review", "terminal"],
341
+ },
342
+ {
343
+ id: "github-copilot",
344
+ label: "GitHub Copilot",
345
+ launchName: "github-copilot",
346
+ aliases: ["github copilot", "github_copilot", "copilot", "copilot-cli", "gh-copilot"],
347
+ description: "GitHub Copilot CLI",
348
+ homepage: "https://github.com/github/copilot-cli",
349
+ iconUrl: "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/githubcopilot.svg",
350
+ installHint: "npm install -g @githubnext/github-copilot-cli",
351
+ launchCommand: "github-copilot",
352
+ capabilities: ["chat", "suggestions", "pairing"],
353
+ },
354
+ {
355
+ id: "gemini",
356
+ label: "Gemini CLI",
357
+ launchName: "gemini",
358
+ aliases: [
359
+ "google-gemini",
360
+ "google_gemini",
361
+ "google-gemini-cli",
362
+ "gemini-cli",
363
+ "gemini_cli",
364
+ "gemini",
365
+ "gm",
366
+ "gemini cli",
367
+ ],
368
+ description: "Google Gemini CLI",
369
+ homepage: "https://ai.google.dev/gemini-api/docs",
370
+ iconUrl: "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/googlegemini.svg",
371
+ installHint: "npm install -g @google/gemini-cli",
372
+ launchCommand: "gemini",
373
+ capabilities: ["chat", "review", "research", "analysis"],
374
+ },
375
+ {
376
+ id: "amp",
377
+ label: "Amp",
378
+ launchName: "amp",
379
+ aliases: ["amp-cli", "amp cli", "amp"],
380
+ description: "Amp Code",
381
+ homepage: "https://www.ampcode.com",
382
+ iconUrl: "https://ampcode.com/amp-mark-color.svg",
383
+ launchCommand: "amp",
384
+ capabilities: ["chat", "automation", "code generation"],
385
+ },
386
+ {
387
+ id: "cursor-cli",
388
+ label: "Cursor",
389
+ launchName: "cursor-cli",
390
+ aliases: [
391
+ "cursor cli",
392
+ "cursor_cli",
393
+ "cursoragent",
394
+ "cursor-agent",
395
+ "cursor-agent-cli",
396
+ "cursor_agent",
397
+ "cursor",
398
+ ],
399
+ description: "Cursor Agent CLI",
400
+ homepage: "https://www.cursor.com",
401
+ iconUrl: "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/cursor.svg",
402
+ launchCommand: "cursor-cli",
403
+ capabilities: ["chat", "review", "multi-agent"],
404
+ },
405
+ {
406
+ id: "opencode",
407
+ label: "OpenCode",
408
+ launchName: "opencode",
409
+ aliases: ["open code", "open_code", "open-code", "open-code-cli", "opencode"],
410
+ description: "SST OpenCode",
411
+ homepage: "https://opencode.ai",
412
+ launchCommand: "opencode",
413
+ capabilities: ["chat", "review", "tooling"],
414
+ },
415
+ {
416
+ id: "droid",
417
+ label: "Droid CLI",
418
+ launchName: "droid",
419
+ description: "Factory Droid",
420
+ iconUrl: "https://raw.githubusercontent.com/Factory-AI/factory/main/docs/images/droid_logo_cli.png",
421
+ homepage: "https://github.com/Factory-AI/factory",
422
+ launchCommand: "droid",
423
+ capabilities: ["chat", "automation", "terminal"],
424
+ },
425
+ {
426
+ id: "ccr",
427
+ label: "Claude Code Router",
428
+ launchName: "ccr",
429
+ aliases: ["claude-code-router", "claude_code_router", "ccr", "ccr-cli"],
430
+ description: "Claude Code Router",
431
+ homepage: "https://github.com/mckaywrigley/claude-code-router",
432
+ iconUrl: "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/claude.svg",
433
+ launchCommand: "ccr",
434
+ capabilities: ["chat", "routing", "multi-provider"],
435
+ },
436
+ {
437
+ id: "qwen-code",
438
+ label: "Qwen Code",
439
+ launchName: "qwen-code",
440
+ aliases: ["qwen code", "qwen_code", "qwen", "qwen-code", "qwen-code-cli"],
441
+ description: "Qwen Code CLI",
442
+ homepage: "https://qwenlm.github.io/announcements/",
443
+ installHint: "npm install -g @qwen-code/qwen-code@latest",
444
+ launchCommand: "qwen",
445
+ capabilities: ["chat", "review", "reasoning", "analysis"],
446
+ },
447
+ ];
448
+
449
+ const KNOWN_AGENT_LAUNCH_BY_ID = Object.fromEntries(
450
+ KNOWN_AGENTS.map((agent) => [normalizeAgentName(agent.id), normalizeAgentName(agent.launchName)]),
451
+ ) as Record<string, string>;
452
+
453
+ const KNOWN_AGENT_ID_BY_LAUNCH = Object.fromEntries(
454
+ KNOWN_AGENTS.flatMap((agent) => [
455
+ [normalizeAgentName(agent.launchName), agent.id],
456
+ ...(agent.aliases ?? []).map((alias) => [normalizeAgentName(alias), agent.id] as const),
457
+ ]),
458
+ ) as Record<string, string>;
459
+
460
+ const KNOWN_AGENT_BY_ID = Object.fromEntries(
461
+ KNOWN_AGENTS.map((agent) => [normalizeAgentName(agent.id), agent.label]),
462
+ ) as Record<string, string>;
463
+
464
+ const KNOWN_AGENT_BY_LAUNCH = Object.fromEntries(
465
+ KNOWN_AGENTS.flatMap((agent) => [
466
+ [normalizeAgentName(agent.launchName), agent],
467
+ ...(agent.aliases ?? []).map((alias) => [normalizeAgentName(alias), agent] as const),
468
+ ]),
469
+ ) as Record<string, KnownAgent>;
470
+
471
+ function getKnownAgent(agentName: string): KnownAgent | undefined {
472
+ const normalized = normalizeAgentName(agentName);
473
+ if (!normalized) return undefined;
474
+ return KNOWN_AGENT_BY_LAUNCH[normalized] ?? KNOWN_AGENTS.find((agent) => normalizeAgentName(agent.id) === normalized);
475
+ }
476
+
477
+ const TAB_DEFINITIONS: Array<{ id: DashboardTab; label: string; subtitle: string }> = [
478
+ { id: "overview", label: "Overview", subtitle: "All sessions and controls" },
479
+ { id: "chat", label: "Chat", subtitle: "Respond to agents in need of action" },
480
+ { id: "review", label: "Review", subtitle: "Send review feedback quickly" },
481
+ { id: "agents", label: "Agents", subtitle: "Health and launch controls" },
482
+ ];
483
+
484
+ const EMPTY_REVIEW_DIFF: ReviewDiffState = {
485
+ loading: false,
486
+ loaded: false,
487
+ hasDiff: false,
488
+ source: "working-tree",
489
+ truncated: false,
490
+ files: [],
491
+ untracked: [],
492
+ generatedAt: "",
493
+ selectedFilePath: null,
494
+ fileSearch: "",
495
+ search: "",
496
+ wrapLines: false,
497
+ error: null,
498
+ };
499
+
500
+ const EMPTY_SESSION_CHECKS: SessionChecksState = {
501
+ loading: false,
502
+ loaded: false,
503
+ ciStatus: "none",
504
+ checks: [],
505
+ source: "not-loaded",
506
+ generatedAt: "",
507
+ error: null,
508
+ };
509
+
510
+ const CHAT_QUICK_ACTIONS = [
511
+ {
512
+ label: "Ask for status",
513
+ message: "Can you share the current blocker and exact next step?",
514
+ },
515
+ {
516
+ label: "Request progress",
517
+ message: "Give me a concise 3-point progress update with blockers.",
518
+ },
519
+ {
520
+ label: "Need clarifications",
521
+ message: "Please provide 2–3 concise clarifying questions before continuing.",
522
+ },
523
+ ] as const;
524
+
525
+ const REVIEW_QUICK_ACTIONS = [
526
+ {
527
+ label: "Request fix summary",
528
+ message: "Please summarize what changed and why this is still pending.",
529
+ },
530
+ {
531
+ label: "Run focused tests",
532
+ message: "Please run focused tests for touched files and report failures.",
533
+ },
534
+ {
535
+ label: "Rebase + clean checks",
536
+ message: "Please rebase with latest main and re-run checks cleanly.",
537
+ },
538
+ ] as const;
539
+
540
+ const CI_STATUS_META: Record<CICheckState, { label: string; dot: string; color: string }> = {
541
+ pending: {
542
+ label: "Pending",
543
+ dot: "bg-[var(--color-status-attention)]",
544
+ color: "rgba(245, 158, 11, 0.2)",
545
+ },
546
+ passing: {
547
+ label: "Passing",
548
+ dot: "bg-[var(--color-status-ready)]",
549
+ color: "rgba(74, 222, 128, 0.18)",
550
+ },
551
+ failing: {
552
+ label: "Failing",
553
+ dot: "bg-[var(--color-status-error)]",
554
+ color: "rgba(248, 113, 113, 0.2)",
555
+ },
556
+ none: {
557
+ label: "Not run",
558
+ dot: "bg-[var(--color-text-muted)]",
559
+ color: "rgba(148, 163, 184, 0.18)",
560
+ },
561
+ };
562
+
563
+ type CICheckState = "pending" | "passing" | "failing" | "none";
564
+ const CI_AUTO_REFRESH_MS = 30_000;
565
+ const CI_PENDING_REFRESH_MS = 7_000;
566
+ const DIFF_AUTO_LOAD_DELAY_MS = 1_250;
567
+
568
+ type AttentionGroup = "respond" | "review" | "merge" | "pending" | "working" | "done";
569
+ type StatusFilter = "all" | "active" | "terminal" | "attention";
570
+ type SortMode = "recent" | "oldest" | "cost" | "attention";
571
+ type ViewMode = "grid" | "lanes";
572
+ type CleanupDialogKind = "single" | "terminal-bulk";
573
+ type CleanupAction = "cleanup" | "kill" | "restore";
574
+
575
+ type CleanupDialogState = {
576
+ open: boolean;
577
+ kind: CleanupDialogKind;
578
+ action: CleanupAction;
579
+ title: string;
580
+ message: string;
581
+ sessionIds: string[];
582
+ isTerminalSession: boolean;
583
+ };
584
+
585
+ const FALLBACK_MERGEABILITY = {
586
+ mergeable: false,
587
+ ciPassing: false,
588
+ approved: false,
589
+ noConflicts: true,
590
+ blockers: [],
591
+ };
592
+
593
+ function metadataEqual(left: Record<string, string>, right: Record<string, string>): boolean {
594
+ const leftKeys = Object.keys(left);
595
+ if (leftKeys.length !== Object.keys(right).length) {
596
+ return false;
597
+ }
598
+ return leftKeys.every((key) => left[key] === right[key]);
599
+ }
600
+
601
+ function prEqual(left: DashboardSession["pr"], right: DashboardSession["pr"]): boolean {
602
+ if (left === right) return true;
603
+ if (!left || !right) return false;
604
+
605
+ if (
606
+ left.number !== right.number ||
607
+ left.url !== right.url ||
608
+ left.title !== right.title ||
609
+ left.branch !== right.branch ||
610
+ left.baseBranch !== right.baseBranch ||
611
+ left.isDraft !== right.isDraft ||
612
+ left.state !== right.state ||
613
+ left.ciStatus !== right.ciStatus ||
614
+ left.reviewDecision !== right.reviewDecision ||
615
+ left.previewUrl !== right.previewUrl
616
+ ) {
617
+ return false;
618
+ }
619
+
620
+ const leftMergeability = left.mergeability;
621
+ const rightMergeability = right.mergeability;
622
+ if (
623
+ leftMergeability.mergeable !== rightMergeability.mergeable ||
624
+ leftMergeability.ciPassing !== rightMergeability.ciPassing ||
625
+ leftMergeability.approved !== rightMergeability.approved ||
626
+ leftMergeability.noConflicts !== rightMergeability.noConflicts ||
627
+ leftMergeability.blockers.length !== rightMergeability.blockers.length
628
+ ) {
629
+ return false;
630
+ }
631
+
632
+ return leftMergeability.blockers.every((blocker, index) => blocker === rightMergeability.blockers[index]);
633
+ }
634
+
635
+ function normalizeSnapshotPr(
636
+ pr: NonNullable<SSESnapshotEvent["sessions"][number]["pr"]> | null,
637
+ ): DashboardSession["pr"] {
638
+ if (!pr) return null;
639
+ const mergeability = pr.mergeability
640
+ ? {
641
+ ...FALLBACK_MERGEABILITY,
642
+ ...pr.mergeability,
643
+ }
644
+ : FALLBACK_MERGEABILITY;
645
+
646
+ return {
647
+ number: pr.number,
648
+ url: pr.url,
649
+ title: pr.title,
650
+ branch: pr.branch,
651
+ baseBranch: pr.baseBranch,
652
+ isDraft: pr.isDraft,
653
+ state: pr.state,
654
+ ciStatus: pr.ciStatus,
655
+ reviewDecision: pr.reviewDecision,
656
+ mergeability,
657
+ previewUrl: pr.previewUrl ?? null,
658
+ };
659
+ }
660
+
661
+ interface DashboardProps {
662
+ sessions: DashboardSession[];
663
+ stats: DashboardStats;
664
+ configProjects?: ConfigProject[];
665
+ }
666
+
667
+ export function Dashboard({ sessions: initialSessions, stats: initialStats, configProjects: initialConfigProjects = [] }: DashboardProps) {
668
+ const router = useRouter();
669
+ const [sessions, setSessions] = useState<DashboardSession[]>(initialSessions);
670
+ const [stats, setStats] = useState<DashboardStats>(initialStats);
671
+ const [configProjects, setConfigProjects] = useState<ConfigProject[]>(initialConfigProjects);
672
+ const [connected, setConnected] = useState(false);
673
+ const [activeProject, setActiveProject] = useState<string | null>(null);
674
+ const [sidebarOpen, setSidebarOpen] = useState(true);
675
+ const [availableAgents, setAvailableAgents] = useState<AgentInfo[]>([]);
676
+ const [busySessionId, setBusySessionId] = useState<string | null>(null);
677
+ const [bulkBusy, setBulkBusy] = useState(false);
678
+ const [actionError, setActionError] = useState<string | null>(null);
679
+ const [launchMessage, setLaunchMessage] = useState<string | null>(null);
680
+ const [cleanupDialog, setCleanupDialog] = useState<CleanupDialogState>({
681
+ open: false,
682
+ kind: "single",
683
+ action: "cleanup",
684
+ title: "",
685
+ message: "",
686
+ sessionIds: [],
687
+ isTerminalSession: false,
688
+ });
689
+ const [search, setSearch] = useState("");
690
+ const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
691
+ const [agentFilter, setAgentFilter] = useState<string>("all");
692
+ const [sortMode, setSortMode] = useState<SortMode>("recent");
693
+ const [attentionOnly, setAttentionOnly] = useState(false);
694
+ const [viewMode, setViewMode] = useState<ViewMode>("grid");
695
+ const [dashboardTab, setDashboardTab] = useState<DashboardTab>("overview");
696
+ const [isLaunchCollapsed, setIsLaunchCollapsed] = useState(true);
697
+ const [commandOpen, setCommandOpen] = useState(false);
698
+ const [commandQuery, setCommandQuery] = useState("");
699
+ const [launchProjectId, setLaunchProjectId] = useState("");
700
+ const [launchIssueId, setLaunchIssueId] = useState("");
701
+ const [launchAgent, setLaunchAgent] = useState("auto");
702
+ const [launchModel, setLaunchModel] = useState("");
703
+ const [launchProfile, setLaunchProfile] = useState("");
704
+ const [launchBranch, setLaunchBranch] = useState("");
705
+ const [launchBaseBranch, setLaunchBaseBranch] = useState("");
706
+ const [launchPrompt, setLaunchPrompt] = useState("");
707
+ const [launchLoading, setLaunchLoading] = useState(false);
708
+ const [chatMessages, setChatMessages] = useState<Record<string, string>>({});
709
+ const [reviewMessages, setReviewMessages] = useState<Record<string, string>>({});
710
+ const [chatSendingSession, setChatSendingSession] = useState<string | null>(null);
711
+ const [reviewSendingSession, setReviewSendingSession] = useState<string | null>(null);
712
+ const [reviewDiffState, setReviewDiffState] = useState<Record<string, ReviewDiffState>>({});
713
+ const [sessionChecksState, setSessionChecksState] = useState<Record<string, SessionChecksState>>({});
714
+ const reviewDiffLoadTimersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
715
+ const eventSourceRef = useRef<EventSource | null>(null);
716
+ const searchInputRef = useRef<HTMLInputElement | null>(null);
717
+ const commandInputRef = useRef<HTMLInputElement | null>(null);
718
+ const { theme, toggleTheme } = useTheme();
719
+
720
+ const isTrackableSessionForChecks = useCallback((session: DashboardSession): boolean => {
721
+ return Boolean(
722
+ session.pr && session.pr.number > 0,
723
+ );
724
+ }, []);
725
+
726
+ const refreshConfigProjects = useCallback(async () => {
727
+ try {
728
+ const res = await fetch("/api/config");
729
+ if (!res.ok) return;
730
+ const data = (await res.json()) as { projects: ConfigProject[] };
731
+ if (Array.isArray(data.projects)) {
732
+ setConfigProjects(data.projects);
733
+ }
734
+ } catch {
735
+ // Keep previous projects if config endpoint is unavailable.
736
+ }
737
+ }, []);
738
+
739
+ // SSE connection for live updates
740
+ // Refresh configured projects every few seconds so board/project changes
741
+ // appear without requiring a full dashboard restart.
742
+ useEffect(() => {
743
+ void refreshConfigProjects();
744
+ const configPoller = setInterval(() => {
745
+ void refreshConfigProjects();
746
+ }, 5000);
747
+ return () => clearInterval(configPoller);
748
+ }, [refreshConfigProjects]);
749
+
750
+ useEffect(() => {
751
+ let canceled = false;
752
+ void (async () => {
753
+ try {
754
+ const res = await fetch("/api/agents");
755
+ if (!res.ok) return;
756
+ const data = (await res.json()) as { agents?: AgentInfo[] };
757
+ if (canceled) return;
758
+ if (Array.isArray(data.agents)) {
759
+ const dedupe = new Map<string, AgentInfo>();
760
+ for (const agent of data.agents) {
761
+ if (!agent?.name) continue;
762
+ const next = dedupe.get(agent.name) ?? {
763
+ name: agent.name,
764
+ description: agent.description ?? null,
765
+ version: agent.version ?? null,
766
+ homepage: agent.homepage ?? null,
767
+ iconUrl: agent.iconUrl ?? null,
768
+ };
769
+ dedupe.set(agent.name, next);
770
+ }
771
+ setAvailableAgents(Array.from(dedupe.values()));
772
+ }
773
+ } catch {
774
+ // Keep fallback behavior if agent catalog is unavailable.
775
+ }
776
+ })();
777
+ return () => {
778
+ canceled = true;
779
+ };
780
+ }, []);
781
+
782
+ useEffect(() => {
783
+ const es = new EventSource("/api/events");
784
+ eventSourceRef.current = es;
785
+
786
+ es.onopen = () => setConnected(true);
787
+ es.onerror = () => setConnected(false);
788
+
789
+ es.onmessage = (event) => {
790
+ try {
791
+ const data = JSON.parse(event.data as string) as SSESnapshotEvent;
792
+ if (data.type === "snapshot" && data.sessions) {
793
+ setSessions((prev) => {
794
+ const updates = new Map(data.sessions.map((s) => [s.id, s]));
795
+ const prevIds = new Set(prev.map((s) => s.id));
796
+ const removedIds = new Set<string>();
797
+ for (const id of prevIds) {
798
+ if (!updates.has(id)) removedIds.add(id);
799
+ }
800
+ const newSessionIds: string[] = [];
801
+ for (const id of updates.keys()) {
802
+ if (!prevIds.has(id)) newSessionIds.push(id);
803
+ }
804
+
805
+ let changed = removedIds.size > 0 || newSessionIds.length > 0;
806
+
807
+ const next = prev
808
+ .filter((s) => !removedIds.has(s.id))
809
+ .map((session) => {
810
+ const update = updates.get(session.id);
811
+ if (!update) return session;
812
+
813
+ const updatePr = normalizeSnapshotPr(update.pr ?? null);
814
+ const mergedSession: DashboardSession = {
815
+ ...session,
816
+ status: update.status,
817
+ activity: update.activity,
818
+ lastActivityAt: update.lastActivityAt,
819
+ createdAt: update.createdAt,
820
+ projectId: update.projectId,
821
+ issueId: update.issueId ?? null,
822
+ branch: update.branch ?? null,
823
+ metadata: update.metadata,
824
+ summary: update.summary === undefined ? session.summary : update.summary,
825
+ pr: updatePr,
826
+ };
827
+
828
+ const metadataChanged = !metadataEqual(session.metadata, mergedSession.metadata);
829
+ const summaryChanged = mergedSession.summary !== session.summary;
830
+ const prChanged = !prEqual(session.pr, mergedSession.pr);
831
+ const sessionChanged = (
832
+ update.status !== session.status ||
833
+ update.activity !== session.activity ||
834
+ mergedSession.lastActivityAt !== session.lastActivityAt ||
835
+ mergedSession.createdAt !== session.createdAt ||
836
+ mergedSession.projectId !== session.projectId ||
837
+ mergedSession.issueId !== session.issueId ||
838
+ mergedSession.branch !== session.branch ||
839
+ summaryChanged ||
840
+ metadataChanged ||
841
+ prChanged
842
+ );
843
+
844
+ if (sessionChanged) {
845
+ changed = true;
846
+ return mergedSession;
847
+ }
848
+
849
+ return session;
850
+ });
851
+
852
+ for (const id of newSessionIds) {
853
+ const update = updates.get(id)!;
854
+ next.push({
855
+ id,
856
+ status: update.status,
857
+ activity: update.activity,
858
+ createdAt: update.createdAt,
859
+ lastActivityAt: update.lastActivityAt,
860
+ summary: update.summary ?? null,
861
+ projectId: update.projectId,
862
+ issueId: update.issueId ?? null,
863
+ branch: update.branch ?? null,
864
+ metadata: update.metadata,
865
+ pr: normalizeSnapshotPr(update.pr ?? null),
866
+ });
867
+ }
868
+
869
+ return changed ? next : prev;
870
+ });
871
+ }
872
+ } catch {
873
+ // Ignore malformed SSE events
874
+ }
875
+ };
876
+
877
+ return () => {
878
+ es.close();
879
+ eventSourceRef.current = null;
880
+ };
881
+ }, []);
882
+
883
+ // Poll /api/sessions every 5s for full data refresh
884
+ const pollSessions = useCallback(async () => {
885
+ try {
886
+ const res = await fetch("/api/sessions");
887
+ if (!res.ok) return;
888
+ const data = (await res.json()) as { sessions: DashboardSession[]; stats: DashboardStats };
889
+ setSessions(data.sessions);
890
+ setStats(data.stats);
891
+ } catch {
892
+ // ignore poll errors
893
+ }
894
+ }, []);
895
+
896
+ useEffect(() => {
897
+ const interval = setInterval(() => void pollSessions(), 3000);
898
+ return () => clearInterval(interval);
899
+ }, [pollSessions]);
900
+
901
+ // Recompute stats when sessions change
902
+ useEffect(() => {
903
+ setStats({
904
+ totalSessions: sessions.length,
905
+ workingSessions: sessions.filter((s) => s.activity === "active").length,
906
+ openPRs: sessions.filter((s) => s.pr?.state === "open").length,
907
+ needsAttention: sessions.filter(
908
+ (s) =>
909
+ s.status === "needs_input" ||
910
+ s.status === "stuck" ||
911
+ s.status === "errored" ||
912
+ s.activity === "waiting_input" ||
913
+ s.activity === "blocked"
914
+ ).length,
915
+ });
916
+ }, [sessions]);
917
+
918
+ useEffect(() => {
919
+ if (activeProject) {
920
+ setLaunchProjectId(activeProject);
921
+ } else if (!launchProjectId && configProjects.length > 0) {
922
+ setLaunchProjectId(configProjects[0]?.id ?? "");
923
+ }
924
+ }, [activeProject, configProjects, launchProjectId]);
925
+
926
+ // Merge config projects + session-derived counts for sidebar
927
+ const projects = useMemo(() => {
928
+ const counts = new Map<string, number>();
929
+ for (const sess of sessions) {
930
+ const pid = sess.projectId || "default";
931
+ counts.set(pid, (counts.get(pid) ?? 0) + 1);
932
+ }
933
+ const allIds = new Set([...configProjects.map((p) => p.id), ...counts.keys()]);
934
+ return [...allIds].sort().map((id) => {
935
+ const cfg = configProjects.find((p) => p.id === id);
936
+ return {
937
+ id,
938
+ count: counts.get(id) ?? 0,
939
+ boardDir: cfg?.boardDir ?? id,
940
+ boardFile: cfg?.boardFile,
941
+ repo: cfg?.repo ?? null,
942
+ iconUrl: cfg?.iconUrl ?? null,
943
+ };
944
+ });
945
+ }, [sessions, configProjects]);
946
+
947
+ const sessionAgentOptions = useMemo(() => {
948
+ const unique = new Set<string>();
949
+ for (const session of sessions) {
950
+ const agent = normalizeAgentName(session.metadata["agent"] ?? "");
951
+ if (agent) unique.add(agent);
952
+ }
953
+ return [...unique].sort((a, b) => a.localeCompare(b));
954
+ }, [sessions]);
955
+
956
+ const discoveredAgentOptions = useMemo(() => {
957
+ const set = new Set<string>(sessionAgentOptions);
958
+ for (const agent of availableAgents) {
959
+ const normalized = normalizeAgentName(agent.name);
960
+ if (normalized) {
961
+ set.add(normalized);
962
+ }
963
+ }
964
+ const ordered = [...set].sort((a, b) => a.localeCompare(b));
965
+ return ordered;
966
+ }, [availableAgents, sessionAgentOptions]);
967
+
968
+ const launchAgentOptions = useMemo(() => {
969
+ const set = new Set<string>(discoveredAgentOptions);
970
+ for (const known of KNOWN_AGENTS) {
971
+ set.add(known.id);
972
+ set.add(known.launchName);
973
+ }
974
+ return [...set].sort((a, b) => a.localeCompare(b));
975
+ }, [discoveredAgentOptions]);
976
+
977
+ const trackedSessionIds = useMemo(
978
+ () => sessions
979
+ .filter(isTrackableSessionForChecks)
980
+ .map((session) => session.id)
981
+ .sort(),
982
+ [sessions, isTrackableSessionForChecks],
983
+ );
984
+
985
+ const availableAgentMetadata = useMemo(() => {
986
+ const map = new Map<string, AgentInfo>();
987
+ for (const agent of availableAgents) {
988
+ const normalized = normalizeAgentName(agent.name);
989
+ if (!normalized) continue;
990
+
991
+ map.set(normalized, agent);
992
+
993
+ const launchName = KNOWN_AGENT_LAUNCH_BY_ID[normalized];
994
+ if (launchName) {
995
+ map.set(normalizeAgentName(launchName), agent);
996
+ }
997
+
998
+ const knownId = KNOWN_AGENT_ID_BY_LAUNCH[normalized];
999
+ if (knownId) {
1000
+ map.set(normalizeAgentName(knownId), agent);
1001
+ }
1002
+ }
1003
+ return map;
1004
+ }, [availableAgents]);
1005
+
1006
+ const normalizeLaunchAgent = (rawAgent: string): string => {
1007
+ const normalized = normalizeAgentName(rawAgent);
1008
+ if (!normalized) return "";
1009
+ const canonicalId = KNOWN_AGENT_ID_BY_LAUNCH[normalized];
1010
+ if (canonicalId) {
1011
+ return KNOWN_AGENT_LAUNCH_BY_ID[normalizeAgentName(canonicalId)] ?? normalized;
1012
+ }
1013
+ return KNOWN_AGENT_LAUNCH_BY_ID[normalized] ?? normalized;
1014
+ };
1015
+
1016
+ // Filtered + sorted sessions
1017
+ const filteredSessions = useMemo(() => {
1018
+ let next = sessions;
1019
+
1020
+ if (activeProject) {
1021
+ next = next.filter((s) => (s.projectId || "default") === activeProject);
1022
+ }
1023
+
1024
+ if (statusFilter === "active") {
1025
+ next = next.filter((s) => !TERMINAL_STATUSES.has(s.status));
1026
+ } else if (statusFilter === "terminal") {
1027
+ next = next.filter((s) => TERMINAL_STATUSES.has(s.status));
1028
+ } else if (statusFilter === "attention") {
1029
+ next = next.filter((s) => {
1030
+ const level = getAttentionLevel(s);
1031
+ return level === "respond" || level === "review" || level === "merge";
1032
+ });
1033
+ }
1034
+
1035
+ if (attentionOnly) {
1036
+ next = next.filter((s) => {
1037
+ const level = getAttentionLevel(s);
1038
+ return level === "respond" || level === "review" || level === "merge";
1039
+ });
1040
+ }
1041
+
1042
+ if (agentFilter !== "all") {
1043
+ next = next.filter(
1044
+ (s) => normalizeAgentName(s.metadata["agent"] ?? "") === agentFilter,
1045
+ );
1046
+ }
1047
+
1048
+ const query = search.trim().toLowerCase();
1049
+ if (query.length > 0) {
1050
+ next = next.filter((s) => {
1051
+ const haystack = [
1052
+ s.id,
1053
+ s.projectId,
1054
+ s.issueId ?? "",
1055
+ s.branch ?? "",
1056
+ s.summary ?? "",
1057
+ s.status,
1058
+ s.activity ?? "",
1059
+ s.metadata["agent"] ?? "",
1060
+ s.pr?.title ?? "",
1061
+ s.pr?.url ?? "",
1062
+ ].join("\n").toLowerCase();
1063
+ return haystack.includes(query);
1064
+ });
1065
+ }
1066
+
1067
+ const ranked = [...next];
1068
+ ranked.sort((a, b) => {
1069
+ if (sortMode === "oldest") {
1070
+ return new Date(a.lastActivityAt).getTime() - new Date(b.lastActivityAt).getTime();
1071
+ }
1072
+ if (sortMode === "cost") {
1073
+ return parseEstimatedCost(b) - parseEstimatedCost(a);
1074
+ }
1075
+ if (sortMode === "attention") {
1076
+ return attentionRank(a) - attentionRank(b);
1077
+ }
1078
+ return new Date(b.lastActivityAt).getTime() - new Date(a.lastActivityAt).getTime();
1079
+ });
1080
+
1081
+ return ranked;
1082
+ }, [sessions, activeProject, statusFilter, attentionOnly, agentFilter, search, sortMode]);
1083
+
1084
+ const reviewSessions = useMemo(() => {
1085
+ return filteredSessions.filter((session) => {
1086
+ if (session.pr && session.pr.number > 0) return true;
1087
+ const diff = reviewDiffState[session.id];
1088
+ if (!diff) return true;
1089
+ if (!diff.loaded) return true;
1090
+ return diff.hasDiff || diff.untracked.length > 0;
1091
+ });
1092
+ }, [filteredSessions, reviewDiffState]);
1093
+
1094
+ const chatSessions = useMemo(
1095
+ () => filteredSessions.filter((session) => getAttentionLevel(session) === "respond"),
1096
+ [filteredSessions],
1097
+ );
1098
+
1099
+ const agentRoster = useMemo(() => {
1100
+ const map = new Map<string, AgentRoster>();
1101
+
1102
+ for (const known of KNOWN_AGENTS) {
1103
+ const launchName = KNOWN_AGENT_LAUNCH_BY_ID[known.id] ?? known.id;
1104
+ const info = availableAgentMetadata.get(launchName) ?? availableAgentMetadata.get(known.id);
1105
+ map.set(known.id, {
1106
+ name: known.id,
1107
+ label: known.label,
1108
+ launchName,
1109
+ known: true,
1110
+ installed: discoveredAgentOptions.includes(launchName) || discoveredAgentOptions.includes(known.id),
1111
+ description: info?.description ?? known.description,
1112
+ version: info?.version ?? null,
1113
+ homepage: info?.homepage ?? known.homepage ?? null,
1114
+ iconUrl: info?.iconUrl ?? known.iconUrl ?? null,
1115
+ capabilities: known.capabilities ?? [],
1116
+ commandHint: known.launchCommand ?? null,
1117
+ totalSessions: 0,
1118
+ activeSessions: 0,
1119
+ attentionSessions: 0,
1120
+ });
1121
+ }
1122
+
1123
+ for (const discovered of discoveredAgentOptions) {
1124
+ const canonical = KNOWN_AGENT_ID_BY_LAUNCH[discovered] ?? discovered;
1125
+ if (map.has(canonical)) continue;
1126
+ const info = availableAgentMetadata.get(discovered);
1127
+ const metadata = info ?? availableAgentMetadata.get(normalizeAgentName(canonical));
1128
+ map.set(canonical, {
1129
+ name: canonical,
1130
+ label: KNOWN_AGENT_BY_ID[canonical] ?? discovered,
1131
+ launchName: discovered,
1132
+ known: false,
1133
+ installed: discoveredAgentOptions.includes(discovered),
1134
+ description: metadata?.description ?? "Agent plugin currently detected",
1135
+ version: metadata?.version ?? null,
1136
+ homepage: metadata?.homepage ?? null,
1137
+ iconUrl: metadata?.iconUrl ?? null,
1138
+ capabilities: [],
1139
+ commandHint: null,
1140
+ totalSessions: 0,
1141
+ activeSessions: 0,
1142
+ attentionSessions: 0,
1143
+ });
1144
+ }
1145
+
1146
+ for (const session of filteredSessions) {
1147
+ const normalizedAgent = normalizeAgentName(session.metadata["agent"] ?? "");
1148
+ const agentName = normalizedAgent || "unassigned";
1149
+ const canonical = KNOWN_AGENT_ID_BY_LAUNCH[agentName] ?? agentName;
1150
+ const existing = map.get(canonical);
1151
+ const metadata = availableAgentMetadata.get(agentName) ?? availableAgentMetadata.get(KNOWN_AGENT_LAUNCH_BY_ID[agentName] ?? "");
1152
+ if (!existing) {
1153
+ map.set(canonical, {
1154
+ name: canonical,
1155
+ label: KNOWN_AGENT_BY_ID[canonical] ?? canonical,
1156
+ launchName: agentName,
1157
+ known: false,
1158
+ installed: discoveredAgentOptions.includes(agentName),
1159
+ description: metadata?.description ?? "Agent not currently discovered",
1160
+ version: metadata?.version ?? null,
1161
+ homepage: metadata?.homepage ?? null,
1162
+ iconUrl: metadata?.iconUrl ?? null,
1163
+ capabilities: [],
1164
+ commandHint: null,
1165
+ totalSessions: 1,
1166
+ activeSessions: 0,
1167
+ attentionSessions: 0,
1168
+ });
1169
+ } else {
1170
+ existing.totalSessions += 1;
1171
+ }
1172
+
1173
+ const bucket = map.get(canonical) ?? null;
1174
+ if (!bucket) continue;
1175
+
1176
+ if (!TERMINAL_STATUSES.has(session.status)) {
1177
+ bucket.activeSessions += 1;
1178
+ }
1179
+
1180
+ const level = getAttentionLevel(session);
1181
+ if (level === "respond" || level === "review" || level === "merge") {
1182
+ bucket.attentionSessions += 1;
1183
+ }
1184
+ }
1185
+
1186
+ return [...map.values()].sort((left, right) => left.name.localeCompare(right.name));
1187
+ }, [filteredSessions, discoveredAgentOptions, availableAgentMetadata]);
1188
+
1189
+ const sessionsByLane = useMemo(() => {
1190
+ const grouped: Record<AttentionGroup, DashboardSession[]> = {
1191
+ respond: [],
1192
+ review: [],
1193
+ merge: [],
1194
+ pending: [],
1195
+ working: [],
1196
+ done: [],
1197
+ };
1198
+ for (const session of filteredSessions) {
1199
+ const lane = getAttentionLevel(session) as AttentionGroup;
1200
+ grouped[lane].push(session);
1201
+ }
1202
+ return grouped;
1203
+ }, [filteredSessions]);
1204
+
1205
+ const cleanupCandidates = useMemo(
1206
+ () => filteredSessions.filter((s) => TERMINAL_STATUSES.has(s.status)),
1207
+ [filteredSessions],
1208
+ );
1209
+
1210
+ const postSessionMessage = useCallback(async (sessionId: string, message: string): Promise<boolean> => {
1211
+ const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/send`, {
1212
+ method: "POST",
1213
+ headers: { "Content-Type": "application/json" },
1214
+ body: JSON.stringify({ message }),
1215
+ });
1216
+ if (!res.ok) {
1217
+ console.error(`Failed to send message to ${sessionId}:`, await res.text());
1218
+ return false;
1219
+ }
1220
+ return true;
1221
+ }, []);
1222
+
1223
+ const postReviewFeedback = useCallback(async (sessionId: string, message: string): Promise<boolean> => {
1224
+ const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/feedback`, {
1225
+ method: "POST",
1226
+ headers: { "Content-Type": "application/json" },
1227
+ body: JSON.stringify({ message }),
1228
+ });
1229
+ if (!res.ok) {
1230
+ console.error(`Failed to submit review feedback for ${sessionId}:`, await res.text());
1231
+ return false;
1232
+ }
1233
+ return true;
1234
+ }, []);
1235
+
1236
+ const handleSend = async (sessionId: string, message: string) => {
1237
+ await postSessionMessage(sessionId, message);
1238
+ };
1239
+
1240
+ const handleChatSend = async (sessionId: string) => {
1241
+ if (chatSendingSession !== null) return;
1242
+ const message = chatMessages[sessionId]?.trim();
1243
+ if (!message) return;
1244
+ setChatSendingSession(sessionId);
1245
+ setActionError(null);
1246
+ const ok = await postSessionMessage(sessionId, message);
1247
+ setChatSendingSession((current) => (current === sessionId ? null : current));
1248
+ if (!ok) {
1249
+ setActionError(`Failed to send chat message for session ${sessionId}.`);
1250
+ return;
1251
+ }
1252
+ setChatMessages((prev) => ({ ...prev, [sessionId]: "" }));
1253
+ };
1254
+
1255
+ const handleReviewSend = async (sessionId: string) => {
1256
+ if (reviewSendingSession !== null) return;
1257
+ const draft = reviewMessages[sessionId]?.trim();
1258
+ if (!draft) return;
1259
+ setReviewSendingSession(sessionId);
1260
+ setActionError(null);
1261
+ const ok = await postReviewFeedback(sessionId, draft);
1262
+ setReviewSendingSession((current) => (current === sessionId ? null : current));
1263
+ if (!ok) {
1264
+ setActionError(`Failed to send review notes for session ${sessionId}.`);
1265
+ return;
1266
+ }
1267
+ setReviewMessages((prev) => ({ ...prev, [sessionId]: "" }));
1268
+ };
1269
+
1270
+ const updateReviewDiffState = useCallback((
1271
+ sessionId: string,
1272
+ patch: Partial<ReviewDiffState>,
1273
+ ) => {
1274
+ setReviewDiffState((prev) => {
1275
+ const current = prev[sessionId] ?? EMPTY_REVIEW_DIFF;
1276
+ return {
1277
+ ...prev,
1278
+ [sessionId]: { ...current, ...patch },
1279
+ };
1280
+ });
1281
+ }, []);
1282
+
1283
+ const handleLoadReviewDiff = useCallback(async (sessionId: string) => {
1284
+ updateReviewDiffState(sessionId, {
1285
+ loading: true,
1286
+ loaded: false,
1287
+ error: null,
1288
+ });
1289
+
1290
+ try {
1291
+ const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/diff`);
1292
+ if (!res.ok) {
1293
+ const data = (await res.json().catch(() => ({}))) as { error?: string };
1294
+ throw new Error(data.error || `Failed to load review diff (${res.status})`);
1295
+ }
1296
+
1297
+ const data = (await res.json()) as ReviewDiffResponse;
1298
+ if ("error" in data && typeof data.error === "string") {
1299
+ throw new Error(data.error);
1300
+ }
1301
+
1302
+ const payload = data as ReviewDiffPayload;
1303
+ const files = Array.isArray(payload.files) ? payload.files : [];
1304
+ const untracked = Array.isArray(payload.untracked) ? payload.untracked : [];
1305
+ const hasDiff = payload.hasDiff || files.length > 0 || untracked.length > 0;
1306
+
1307
+ setReviewDiffState((prev) => {
1308
+ const current = prev[sessionId] ?? EMPTY_REVIEW_DIFF;
1309
+ const firstFile = files[0]?.path ?? null;
1310
+ const nextSelected = firstFile !== null &&
1311
+ current.selectedFilePath &&
1312
+ files.some((file) => file.path === current.selectedFilePath)
1313
+ ? current.selectedFilePath
1314
+ : firstFile;
1315
+
1316
+ return {
1317
+ ...prev,
1318
+ [sessionId]: {
1319
+ ...current,
1320
+ loading: false,
1321
+ loaded: true,
1322
+ hasDiff,
1323
+ source: payload.source,
1324
+ truncated: payload.truncated,
1325
+ files,
1326
+ untracked,
1327
+ generatedAt: payload.generatedAt,
1328
+ selectedFilePath: nextSelected,
1329
+ error: null,
1330
+ },
1331
+ };
1332
+ });
1333
+ } catch (err) {
1334
+ updateReviewDiffState(sessionId, {
1335
+ loading: false,
1336
+ loaded: true,
1337
+ error: err instanceof Error ? err.message : "Failed to load diff",
1338
+ });
1339
+ }
1340
+ }, [updateReviewDiffState]);
1341
+
1342
+ const applyQuickMessage = useCallback(
1343
+ (sessionId: string, kind: string, template: string) => {
1344
+ if (kind === "review") {
1345
+ setReviewMessages((prev) => {
1346
+ const current = prev[sessionId] ?? "";
1347
+ const next = current.trim().length === 0
1348
+ ? template
1349
+ : `${current.trim()}\n\n${template}`;
1350
+ return {
1351
+ ...prev,
1352
+ [sessionId]: next,
1353
+ };
1354
+ });
1355
+ return;
1356
+ }
1357
+
1358
+ setChatMessages((prev) => {
1359
+ const current = prev[sessionId] ?? "";
1360
+ const next = current.trim().length === 0
1361
+ ? template
1362
+ : `${current.trim()}\n\n${template}`;
1363
+ return {
1364
+ ...prev,
1365
+ [sessionId]: next,
1366
+ };
1367
+ });
1368
+ },
1369
+ [],
1370
+ );
1371
+
1372
+ const updateSessionChecksState = useCallback((sessionId: string, patch: Partial<SessionChecksState>) => {
1373
+ setSessionChecksState((prev) => {
1374
+ const current = prev[sessionId] ?? EMPTY_SESSION_CHECKS;
1375
+ return {
1376
+ ...prev,
1377
+ [sessionId]: { ...current, ...patch },
1378
+ };
1379
+ });
1380
+ }, []);
1381
+
1382
+ const handleLoadSessionChecks = useCallback(async (sessionId: string, options?: { silent?: boolean }) => {
1383
+ const silent = options?.silent === true;
1384
+ if (!silent) {
1385
+ updateSessionChecksState(sessionId, { loading: true, error: null });
1386
+ }
1387
+
1388
+ try {
1389
+ const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/checks`);
1390
+ if (!res.ok) {
1391
+ const data = (await res.json().catch(() => ({}))) as { error?: string };
1392
+ throw new Error(data.error || `Failed to load CI checks (${res.status})`);
1393
+ }
1394
+
1395
+ const data = (await res.json()) as CIChecksPayload;
1396
+ updateSessionChecksState(sessionId, {
1397
+ loading: false,
1398
+ loaded: true,
1399
+ ciStatus: data.ciStatus,
1400
+ checks: Array.isArray(data.checks) ? data.checks : [],
1401
+ source: data.source,
1402
+ generatedAt: data.generatedAt,
1403
+ error: null,
1404
+ });
1405
+ } catch (err) {
1406
+ updateSessionChecksState(sessionId, {
1407
+ loading: false,
1408
+ loaded: false,
1409
+ source: "not-loaded",
1410
+ generatedAt: "",
1411
+ error: err instanceof Error ? err.message : "Failed to load CI checks",
1412
+ });
1413
+ }
1414
+ }, [updateSessionChecksState]);
1415
+
1416
+ useEffect(() => {
1417
+ if (trackedSessionIds.length === 0) return;
1418
+ let canceled = false;
1419
+
1420
+ const sessionIds = trackedSessionIds;
1421
+ const shouldRefresh = (sessionId: string) => {
1422
+ const state = sessionChecksState[sessionId];
1423
+ if (!state) {
1424
+ return true;
1425
+ }
1426
+ if (!state.loaded && !state.loading) {
1427
+ return true;
1428
+ }
1429
+ if (state.error) {
1430
+ return true;
1431
+ }
1432
+ if (state.loading) {
1433
+ return false;
1434
+ }
1435
+ if (!state.generatedAt) {
1436
+ return true;
1437
+ }
1438
+ const last = Date.parse(state.generatedAt);
1439
+ if (Number.isNaN(last)) {
1440
+ return true;
1441
+ }
1442
+ const refreshWindow = state.ciStatus === "pending" ? CI_PENDING_REFRESH_MS : CI_AUTO_REFRESH_MS;
1443
+ return Date.now() - last > refreshWindow;
1444
+ };
1445
+
1446
+ const refresh = async () => {
1447
+ if (canceled) return;
1448
+ const toRefresh = sessionIds.filter((sessionId) => shouldRefresh(sessionId));
1449
+ if (toRefresh.length === 0) {
1450
+ return;
1451
+ }
1452
+ await Promise.all(toRefresh.map((sessionId) => handleLoadSessionChecks(sessionId, { silent: true })));
1453
+ };
1454
+
1455
+ void refresh();
1456
+ const interval = setInterval(() => {
1457
+ void refresh();
1458
+ }, CI_PENDING_REFRESH_MS);
1459
+
1460
+ return () => {
1461
+ canceled = true;
1462
+ clearInterval(interval);
1463
+ };
1464
+ }, [trackedSessionIds.join(","), sessionChecksState, handleLoadSessionChecks]);
1465
+
1466
+ useEffect(() => {
1467
+ if (dashboardTab !== "review") {
1468
+ for (const sessionId of Object.keys(reviewDiffLoadTimersRef.current)) {
1469
+ clearTimeout(reviewDiffLoadTimersRef.current[sessionId]);
1470
+ delete reviewDiffLoadTimersRef.current[sessionId];
1471
+ }
1472
+ return;
1473
+ }
1474
+
1475
+ const activeSessionIds = new Set(reviewSessions.map((session) => session.id));
1476
+ for (const [sessionId, timer] of Object.entries(reviewDiffLoadTimersRef.current)) {
1477
+ if (!activeSessionIds.has(sessionId)) {
1478
+ clearTimeout(timer);
1479
+ delete reviewDiffLoadTimersRef.current[sessionId];
1480
+ }
1481
+ }
1482
+
1483
+ for (const session of reviewSessions) {
1484
+ const state = reviewDiffState[session.id] ?? EMPTY_REVIEW_DIFF;
1485
+ if (state.loaded || state.loading) continue;
1486
+ if (reviewDiffLoadTimersRef.current[session.id]) continue;
1487
+
1488
+ reviewDiffLoadTimersRef.current[session.id] = setTimeout(() => {
1489
+ void handleLoadReviewDiff(session.id)
1490
+ .catch(() => {})
1491
+ .finally(() => {
1492
+ delete reviewDiffLoadTimersRef.current[session.id];
1493
+ });
1494
+ }, DIFF_AUTO_LOAD_DELAY_MS);
1495
+ }
1496
+
1497
+ return () => {
1498
+ for (const [sessionId, timer] of Object.entries(reviewDiffLoadTimersRef.current)) {
1499
+ clearTimeout(timer);
1500
+ delete reviewDiffLoadTimersRef.current[sessionId];
1501
+ }
1502
+ };
1503
+ }, [dashboardTab, reviewSessions, reviewDiffState, handleLoadReviewDiff]);
1504
+
1505
+ const executeKill = useCallback(async (sessionId: string): Promise<{ ok: boolean; reason?: string }> => {
1506
+ setBusySessionId(sessionId);
1507
+ try {
1508
+ const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/kill`, {
1509
+ method: "POST",
1510
+ });
1511
+ if (!res.ok && res.status !== 404) {
1512
+ const detail = await res.text();
1513
+ return {
1514
+ ok: false,
1515
+ reason: detail || `Request failed with ${res.status}`,
1516
+ };
1517
+ }
1518
+
1519
+ setSessions((prev) => prev.filter((s) => s.id !== sessionId));
1520
+ return { ok: true };
1521
+ } catch (err) {
1522
+ const msg = err instanceof Error ? err.message : "Network error";
1523
+ return { ok: false, reason: msg };
1524
+ } finally {
1525
+ setBusySessionId((current) => (current === sessionId ? null : current));
1526
+ }
1527
+ }, []);
1528
+
1529
+ const executeRestore = useCallback(async (sessionId: string): Promise<{ ok: boolean; reason?: string }> => {
1530
+ setBusySessionId(sessionId);
1531
+ try {
1532
+ const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/restore`, {
1533
+ method: "POST",
1534
+ });
1535
+ if (!res.ok) {
1536
+ const detail = await res.text();
1537
+ return {
1538
+ ok: false,
1539
+ reason: detail || `Request failed with ${res.status}`,
1540
+ };
1541
+ }
1542
+ return { ok: true };
1543
+ } catch (err) {
1544
+ const msg = err instanceof Error ? err.message : "Network error";
1545
+ return { ok: false, reason: msg };
1546
+ } finally {
1547
+ setBusySessionId((current) => (current === sessionId ? null : current));
1548
+ }
1549
+ }, []);
1550
+
1551
+ const openCleanupDialog = (
1552
+ sessionIds: string[],
1553
+ kind: CleanupDialogKind,
1554
+ action: CleanupAction,
1555
+ title: string,
1556
+ message: string,
1557
+ isTerminalSession = false,
1558
+ ) => {
1559
+ if (busySessionId || bulkBusy || cleanupDialog.open) return;
1560
+ const uniqueIds = [...new Set(sessionIds.filter(Boolean))];
1561
+ if (uniqueIds.length === 0) return;
1562
+ setCleanupDialog({
1563
+ open: true,
1564
+ kind,
1565
+ action,
1566
+ title,
1567
+ message,
1568
+ sessionIds: uniqueIds,
1569
+ isTerminalSession,
1570
+ });
1571
+ };
1572
+
1573
+ const closeCleanupDialog = () => {
1574
+ setCleanupDialog((current) => ({ ...current, open: false }));
1575
+ };
1576
+
1577
+ const confirmCleanup = async () => {
1578
+ const current = cleanupDialog;
1579
+ if (!current.open || current.sessionIds.length === 0) {
1580
+ closeCleanupDialog();
1581
+ return;
1582
+ }
1583
+ if (current.kind === "single") {
1584
+ const sessionId = current.sessionIds[0]!;
1585
+ if (!sessionId || busySessionId || bulkBusy) return;
1586
+ setActionError(null);
1587
+ const result = current.action === "restore"
1588
+ ? await executeRestore(sessionId)
1589
+ : await executeKill(sessionId);
1590
+ if (!result.ok) {
1591
+ const reason = result.reason ?? "Unknown error";
1592
+ logActionError(sessionId, reason, current.action);
1593
+ }
1594
+ closeCleanupDialog();
1595
+ return;
1596
+ }
1597
+
1598
+ if (current.kind === "terminal-bulk") {
1599
+ if (bulkBusy || busySessionId) return;
1600
+ setActionError(null);
1601
+ setBulkBusy(true);
1602
+ const failures: string[] = [];
1603
+ for (const sessionId of current.sessionIds) {
1604
+ const result = await executeKill(sessionId);
1605
+ if (!result.ok) {
1606
+ failures.push(`${sessionId}: ${result.reason ?? "Unknown error"}`);
1607
+ }
1608
+ }
1609
+ setBulkBusy(false);
1610
+ setCleanupDialog((current) => ({ ...current, open: false }));
1611
+
1612
+ if (failures.length > 0) {
1613
+ setActionError(`Cleanup completed with ${failures.length} failure(s). ${failures[0]}`);
1614
+ return;
1615
+ }
1616
+ setActionError(null);
1617
+ }
1618
+ };
1619
+
1620
+ const handleCleanupTerminal = async () => {
1621
+ if (busySessionId || bulkBusy) return;
1622
+ if (cleanupCandidates.length === 0) return;
1623
+
1624
+ const ids = [...new Set(cleanupCandidates.map((s) => s.id))];
1625
+ const noun = ids.length === 1 ? "session" : "sessions";
1626
+ openCleanupDialog(
1627
+ ids,
1628
+ "terminal-bulk",
1629
+ "cleanup",
1630
+ `Clean up ${ids.length} terminal ${noun}`,
1631
+ `Clean up all terminal sessions in the current view (${ids.length} ${noun}).`,
1632
+ );
1633
+ };
1634
+
1635
+ const handleKill = (sessionId: string, isTerminalSession = false) => {
1636
+ if (busySessionId || bulkBusy) return;
1637
+ const normalizedSessionId = sessionId.trim();
1638
+ if (!normalizedSessionId) return;
1639
+ openCleanupDialog(
1640
+ [normalizedSessionId],
1641
+ "single",
1642
+ isTerminalSession ? "cleanup" : "kill",
1643
+ `${isTerminalSession ? "Cleanup" : "Kill"} session`,
1644
+ `${isTerminalSession ? "Clean up" : "Kill"} ${normalizedSessionId}.`,
1645
+ isTerminalSession,
1646
+ );
1647
+ };
1648
+
1649
+ const handleRestore = (sessionId: string) => {
1650
+ if (busySessionId || bulkBusy) return;
1651
+ const normalizedSessionId = sessionId.trim();
1652
+ if (!normalizedSessionId) return;
1653
+ openCleanupDialog(
1654
+ [normalizedSessionId],
1655
+ "single",
1656
+ "restore",
1657
+ "Restore session",
1658
+ `Restore ${normalizedSessionId}.`,
1659
+ false,
1660
+ );
1661
+ };
1662
+
1663
+ const logActionError = (sessionId: string, reason: string, action: CleanupAction) => {
1664
+ const actionLabel = action === "restore"
1665
+ ? "restore"
1666
+ : action === "kill"
1667
+ ? "kill"
1668
+ : "cleanup";
1669
+ setActionError(`Unable to ${actionLabel} session ${sessionId}: ${reason}`);
1670
+ console.error(`Failed to ${actionLabel} session ${sessionId}:`, reason);
1671
+ };
1672
+ const handleLaunchSession = async (event: FormEvent<HTMLFormElement>) => {
1673
+ event.preventDefault();
1674
+ setLaunchMessage(null);
1675
+
1676
+ if (launchLoading) return;
1677
+
1678
+ const projectId = launchProjectId || (configProjects[0]?.id ?? "");
1679
+ if (!projectId) {
1680
+ setLaunchMessage("Select a project to launch a session.");
1681
+ return;
1682
+ }
1683
+ if (!launchPrompt.trim()) {
1684
+ setLaunchMessage("Session prompt is required.");
1685
+ return;
1686
+ }
1687
+ const normalizedAgent = normalizeLaunchAgent(launchAgent).trim();
1688
+ const loweredLaunchAgent = launchAgent.trim().toLowerCase();
1689
+ const resolvedAgent = loweredLaunchAgent === "auto" || loweredLaunchAgent === "custom" || !normalizedAgent
1690
+ ? undefined
1691
+ : normalizedAgent;
1692
+
1693
+ setLaunchLoading(true);
1694
+
1695
+ try {
1696
+ const res = await fetch("/api/spawn", {
1697
+ method: "POST",
1698
+ headers: { "Content-Type": "application/json" },
1699
+ body: JSON.stringify({
1700
+ projectId,
1701
+ issueId: launchIssueId.trim().length > 0 ? launchIssueId.trim() : undefined,
1702
+ prompt: launchPrompt.trim(),
1703
+ agent: resolvedAgent,
1704
+ model: launchModel.trim().length > 0 ? launchModel.trim() : undefined,
1705
+ profile: launchProfile.trim().length > 0 ? launchProfile.trim() : undefined,
1706
+ branch: launchBranch.trim().length > 0 ? launchBranch.trim() : undefined,
1707
+ baseBranch:
1708
+ launchBaseBranch.trim().length > 0 ? launchBaseBranch.trim() : undefined,
1709
+ }),
1710
+ });
1711
+
1712
+ if (!res.ok) {
1713
+ const rawDetail = await res.text();
1714
+ let detail = rawDetail;
1715
+ if (rawDetail) {
1716
+ try {
1717
+ const data = JSON.parse(rawDetail) as { error?: string };
1718
+ if (typeof data.error === "string" && data.error.length > 0) {
1719
+ detail = data.error;
1720
+ }
1721
+ } catch {
1722
+ // keep plain-text response as-is
1723
+ }
1724
+ }
1725
+ const isCloneLimit = (typeof detail === "string" && detail.includes("already has") && detail.includes("active sessions"));
1726
+ setLaunchMessage(
1727
+ isCloneLimit
1728
+ ? `${detail} (configured by maxSessionsPerProject; increase in your CONDUCTOR config)`
1729
+ : detail || `Failed to launch session (${res.status})`,
1730
+ );
1731
+ return;
1732
+ }
1733
+
1734
+ setLaunchMessage("Session launched. Refreshing dashboard...");
1735
+ setLaunchIssueId("");
1736
+ setLaunchPrompt("");
1737
+ setLaunchModel("");
1738
+ setLaunchProfile("");
1739
+ setLaunchBranch("");
1740
+ setLaunchBaseBranch("");
1741
+ void pollSessions();
1742
+ } catch (err) {
1743
+ setLaunchMessage(err instanceof Error ? err.message : "Failed to launch session");
1744
+ } finally {
1745
+ setLaunchLoading(false);
1746
+ }
1747
+ };
1748
+
1749
+ useEffect(() => {
1750
+ if (!commandOpen) return;
1751
+ const id = window.setTimeout(() => {
1752
+ commandInputRef.current?.focus();
1753
+ }, 20);
1754
+ return () => window.clearTimeout(id);
1755
+ }, [commandOpen]);
1756
+
1757
+ useEffect(() => {
1758
+ const onKeyDown = (event: KeyboardEvent) => {
1759
+ const target = event.target as HTMLElement | null;
1760
+ const isTypingTarget = Boolean(
1761
+ target &&
1762
+ (target.closest("input, textarea, select") ||
1763
+ target.isContentEditable),
1764
+ );
1765
+
1766
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
1767
+ event.preventDefault();
1768
+ setCommandOpen(true);
1769
+ setCommandQuery("");
1770
+ return;
1771
+ }
1772
+
1773
+ if (event.key === "Escape" && commandOpen) {
1774
+ event.preventDefault();
1775
+ setCommandOpen(false);
1776
+ return;
1777
+ }
1778
+
1779
+ if (event.key === "Escape" && cleanupDialog.open) {
1780
+ event.preventDefault();
1781
+ closeCleanupDialog();
1782
+ return;
1783
+ }
1784
+
1785
+ if (!isTypingTarget && event.key === "/") {
1786
+ event.preventDefault();
1787
+ searchInputRef.current?.focus();
1788
+ }
1789
+ };
1790
+ window.addEventListener("keydown", onKeyDown);
1791
+ return () => window.removeEventListener("keydown", onKeyDown);
1792
+ }, [commandOpen, cleanupDialog.open]);
1793
+
1794
+ type CommandAction = { id: string; label: string; hint?: string; run: () => void };
1795
+ const commandActions: CommandAction[] = useMemo(() => {
1796
+ return [
1797
+ {
1798
+ id: "toggle-theme",
1799
+ label: `Switch to ${theme === "dark" ? "light" : "dark"} theme`,
1800
+ hint: "Appearance",
1801
+ run: () => toggleTheme(),
1802
+ },
1803
+ {
1804
+ id: "toggle-view",
1805
+ label: viewMode === "grid" ? "Switch to lane view" : "Switch to grid view",
1806
+ hint: "Layout",
1807
+ run: () => setViewMode((v) => (v === "grid" ? "lanes" : "grid")),
1808
+ },
1809
+ {
1810
+ id: "refresh",
1811
+ label: "Refresh sessions now",
1812
+ hint: "Live data",
1813
+ run: () => {
1814
+ void pollSessions();
1815
+ },
1816
+ },
1817
+ {
1818
+ id: "focus-attention",
1819
+ label: attentionOnly ? "Show all sessions" : "Focus needs attention",
1820
+ hint: "Filtering",
1821
+ run: () => setAttentionOnly((v) => !v),
1822
+ },
1823
+ {
1824
+ id: "clear-filters",
1825
+ label: "Clear filters",
1826
+ hint: "Filtering",
1827
+ run: () => {
1828
+ setSearch("");
1829
+ setStatusFilter("all");
1830
+ setAgentFilter("all");
1831
+ setAttentionOnly(false);
1832
+ setSortMode("recent");
1833
+ },
1834
+ },
1835
+ {
1836
+ id: "cleanup",
1837
+ label: `Cleanup terminal sessions (${cleanupCandidates.length})`,
1838
+ hint: "Agent control",
1839
+ run: () => {
1840
+ void handleCleanupTerminal();
1841
+ },
1842
+ },
1843
+ ];
1844
+ }, [theme, toggleTheme, viewMode, pollSessions, attentionOnly, cleanupCandidates.length, handleCleanupTerminal]);
1845
+
1846
+ const visibleCommandActions = useMemo(() => {
1847
+ const query = commandQuery.trim().toLowerCase();
1848
+ if (query.length === 0) return commandActions;
1849
+ return commandActions.filter((action) => {
1850
+ const text = `${action.label} ${action.hint ?? ""}`.toLowerCase();
1851
+ return text.includes(query);
1852
+ });
1853
+ }, [commandActions, commandQuery]);
1854
+
1855
+ const visibleTabSessions = useMemo(() => {
1856
+ if (dashboardTab === "chat") return chatSessions;
1857
+ if (dashboardTab === "review") return reviewSessions;
1858
+ return filteredSessions;
1859
+ }, [chatSessions, reviewSessions, filteredSessions, dashboardTab]);
1860
+
1861
+ const visibleAgentRoster = useMemo(() => {
1862
+ if (dashboardTab !== "agents") return agentRoster;
1863
+ const query = search.trim().toLowerCase();
1864
+ if (!query) return agentRoster;
1865
+ return agentRoster.filter((agent) => {
1866
+ const haystack = [agent.label, agent.name, agent.launchName, agent.description ?? ""]
1867
+ .join(" ")
1868
+ .toLowerCase();
1869
+ return haystack.includes(query);
1870
+ });
1871
+ }, [agentRoster, dashboardTab, search]);
1872
+
1873
+ const visibleKnownAgents = useMemo(
1874
+ () => visibleAgentRoster.filter((agent) => agent.known),
1875
+ [visibleAgentRoster],
1876
+ );
1877
+ const visibleDiscoveredAgents = useMemo(
1878
+ () => visibleAgentRoster.filter((agent) => !agent.known),
1879
+ [visibleAgentRoster],
1880
+ );
1881
+
1882
+ const searchPlaceholder = dashboardTab === "agents"
1883
+ ? "Search agents..."
1884
+ : dashboardTab === "chat"
1885
+ ? "Search sessions needing agent response..."
1886
+ : dashboardTab === "review"
1887
+ ? "Search sessions or changed files..."
1888
+ : "Search sessions, issues, branches, agents...";
1889
+
1890
+ const totalAgentCatalogCount = Math.max(
1891
+ visibleAgentRoster.length,
1892
+ availableAgents.length,
1893
+ discoveredAgentOptions.length,
1894
+ KNOWN_AGENTS.length,
1895
+ );
1896
+ const cleanupDialogLabel = cleanupDialog.kind === "terminal-bulk"
1897
+ ? `Cleanup ${cleanupDialog.sessionIds.length} session${cleanupDialog.sessionIds.length === 1 ? "" : "s"}`
1898
+ : cleanupDialog.action === "restore"
1899
+ ? "Restore session"
1900
+ : cleanupDialog.isTerminalSession
1901
+ ? "Cleanup session"
1902
+ : "Kill session";
1903
+ const cleanupDialogBusy = cleanupDialog.kind === "terminal-bulk"
1904
+ ? bulkBusy
1905
+ : Boolean(busySessionId) || bulkBusy;
1906
+
1907
+ const handleOpenTerminal = useCallback((sessionId: string) => {
1908
+ router.push(`/sessions/${encodeURIComponent(sessionId)}?tab=terminal`);
1909
+ }, [router]);
1910
+
1911
+ useEffect(() => {
1912
+ if (dashboardTab !== "chat" && dashboardTab !== "review") return;
1913
+ if (statusFilter !== "all") {
1914
+ setStatusFilter("all");
1915
+ }
1916
+ if (attentionOnly) {
1917
+ setAttentionOnly(false);
1918
+ }
1919
+ if (agentFilter !== "all") {
1920
+ setAgentFilter("all");
1921
+ }
1922
+ if (sortMode !== "recent") {
1923
+ setSortMode("recent");
1924
+ }
1925
+ }, [agentFilter, attentionOnly, dashboardTab, sortMode, statusFilter]);
1926
+
1927
+ return (
1928
+ <div className="flex h-dvh overflow-hidden">
1929
+ {/* Sidebar */}
1930
+ <aside
1931
+ className={`shrink-0 border-r border-[var(--color-sidebar-border)] bg-[var(--color-sidebar-bg)] flex flex-col transition-all duration-200 ${
1932
+ sidebarOpen ? "w-72" : "w-0 overflow-hidden border-r-0"
1933
+ }`}
1934
+ >
1935
+ {/* Sidebar header */}
1936
+ <div className="flex items-center gap-2 px-4 pt-4 pb-3">
1937
+ <span className="text-[14px] font-semibold text-[var(--color-text-primary)] tracking-tight">
1938
+ Conductor
1939
+ </span>
1940
+ </div>
1941
+
1942
+ {/* Nav */}
1943
+ <nav className="flex-1 overflow-y-auto px-2 pb-4">
1944
+ <div className="mb-1 px-2 pt-3 pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
1945
+ Projects
1946
+ </div>
1947
+
1948
+ {/* All sessions */}
1949
+ <button
1950
+ onClick={() => setActiveProject(null)}
1951
+ className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-[13px] transition-colors ${
1952
+ activeProject === null
1953
+ ? "bg-[var(--color-sidebar-active)] text-[var(--color-accent)] font-medium"
1954
+ : "text-[var(--color-text-secondary)] hover:bg-[var(--color-sidebar-hover)]"
1955
+ }`}
1956
+ >
1957
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" className="shrink-0 opacity-60">
1958
+ <path d="M1.5 1.75V13.5h13.25a.75.75 0 010 1.5H.75a.75.75 0 01-.75-.75V1.75a.75.75 0 011.5 0zm14.28 2.53l-5.25 5.25a.75.75 0 01-1.06 0L7 7.06 4.28 9.78a.75.75 0 01-1.06-1.06l3.25-3.25a.75.75 0 011.06 0L10 7.94l4.72-4.72a.75.75 0 111.06 1.06z" />
1959
+ </svg>
1960
+ <span>All Sessions</span>
1961
+ <span className="ml-auto text-[11px] text-[var(--color-text-muted)] tabular-nums">
1962
+ {sessions.length}
1963
+ </span>
1964
+ </button>
1965
+
1966
+ {/* Project list */}
1967
+ {projects.map((project) => {
1968
+ const boardDirHasPath = project.boardDir.includes("/");
1969
+ const inferredBoardFile = project.boardDir.endsWith(".md")
1970
+ ? (boardDirHasPath ? project.boardDir : `projects/${project.boardDir}`)
1971
+ : (boardDirHasPath
1972
+ ? `${project.boardDir}/CONDUCTOR.md`
1973
+ : `projects/${project.boardDir}/CONDUCTOR.md`);
1974
+ const obsidianFile = project.boardFile ?? inferredBoardFile;
1975
+ const obsidianUrl = `obsidian://open?vault=workspace&file=${encodeURIComponent(obsidianFile)}`;
1976
+ const githubUrl = project.repo ? `https://github.com/${project.repo}` : null;
1977
+ return (
1978
+ <div key={project.id} className="group relative flex w-full items-center">
1979
+ <button
1980
+ onClick={() => setActiveProject(activeProject === project.id ? null : project.id)}
1981
+ className={`mr-2 flex flex-1 items-center gap-2 rounded-lg px-3 py-2 text-[13px] transition-colors ${
1982
+ activeProject === project.id
1983
+ ? "bg-[var(--color-sidebar-active)] text-[var(--color-accent)] font-medium"
1984
+ : "text-[var(--color-text-secondary)] hover:bg-[var(--color-sidebar-hover)]"
1985
+ }`}
1986
+ >
1987
+ <ProjectFavicon
1988
+ projectId={project.id}
1989
+ repo={project.repo}
1990
+ iconUrl={project.iconUrl}
1991
+ />
1992
+ <span className="truncate">{project.id}</span>
1993
+ <span className="ml-auto text-[11px] text-[var(--color-text-muted)] tabular-nums">
1994
+ {project.count}
1995
+ </span>
1996
+ </button>
1997
+ {/* Quick-action icons — always visible */}
1998
+ <div className="project-action-icons mr-2 flex shrink-0 items-center gap-1">
1999
+ <button
2000
+ type="button"
2001
+ title="Open board in Obsidian"
2002
+ style={{
2003
+ color: "var(--color-accent-violet)",
2004
+ background: "transparent",
2005
+ outline: "none",
2006
+ boxShadow: "none",
2007
+ border: "none",
2008
+ padding: 0,
2009
+ }}
2010
+ onClick={() => {
2011
+ window.location.href = obsidianUrl;
2012
+ }}
2013
+ className="project-action-icon inline-flex cursor-pointer items-center justify-center p-1"
2014
+ aria-label="Open board in Obsidian"
2015
+ >
2016
+ <ObsidianLogo className="h-[18px] w-[18px]" fillColor="var(--color-accent-violet)" />
2017
+ </button>
2018
+ {githubUrl && (
2019
+ <button
2020
+ type="button"
2021
+ title="Open repo on GitHub"
2022
+ style={{
2023
+ color: "var(--color-text-muted)",
2024
+ background: "transparent",
2025
+ outline: "none",
2026
+ boxShadow: "none",
2027
+ border: "none",
2028
+ padding: 0,
2029
+ }}
2030
+ onClick={() => {
2031
+ window.open(githubUrl, "_blank", "noopener,noreferrer");
2032
+ }}
2033
+ className="project-action-icon inline-flex cursor-pointer items-center justify-center p-1"
2034
+ aria-label="Open repo on GitHub"
2035
+ >
2036
+ <GitHubLogo className="h-[18px] w-[18px]" fillColor="var(--color-text-muted)" />
2037
+ </button>
2038
+ )}
2039
+ </div>
2040
+ </div>
2041
+ );
2042
+ })}
2043
+ </nav>
2044
+
2045
+ <div className="px-2 pb-3">
2046
+ <div className="px-2 pt-3 pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
2047
+ Available Agents
2048
+ </div>
2049
+ {availableAgents.length === 0 ? (
2050
+ <div className="px-2 text-[11px] text-[var(--color-text-muted)]">
2051
+ Loading from /api/agents...
2052
+ </div>
2053
+ ) : (
2054
+ <div className="max-h-40 space-y-1 overflow-y-auto">
2055
+ {availableAgents.map((agent) => (
2056
+ <button
2057
+ key={agent.name}
2058
+ type="button"
2059
+ onClick={() => setLaunchAgent(agent.name)}
2060
+ className="w-full rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-[var(--color-sidebar-hover)]"
2061
+ >
2062
+ <div className="truncate text-[11px] font-medium text-[var(--color-text-secondary)]">
2063
+ {agent.name}
2064
+ </div>
2065
+ <div className="truncate text-[10px] text-[var(--color-text-muted)]">
2066
+ {agent.description ?? "agent plugin"}
2067
+ {agent.version ? ` (v${agent.version})` : ""}
2068
+ </div>
2069
+ </button>
2070
+ ))}
2071
+ </div>
2072
+ )}
2073
+ </div>
2074
+ </aside>
2075
+
2076
+ {/* Main content */}
2077
+ <div className="flex flex-1 flex-col min-w-0">
2078
+ {/* Header */}
2079
+ <header className="flex items-center gap-4 border-b border-[var(--color-border-subtle)] bg-[var(--color-bg-surface)] px-6 py-3">
2080
+ {/* Sidebar toggle */}
2081
+ <button
2082
+ onClick={() => setSidebarOpen(!sidebarOpen)}
2083
+ className="rounded-md p-1.5 text-[var(--color-text-muted)] hover:bg-[var(--color-bg-elevated)] hover:text-[var(--color-text-secondary)] transition-colors"
2084
+ title={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
2085
+ >
2086
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
2087
+ <path d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 010 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 010 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 000 1.5h12.5a.75.75 0 000-1.5H1.75z" />
2088
+ </svg>
2089
+ </button>
2090
+
2091
+ <h1 className="text-[15px] font-semibold text-[var(--color-text-primary)] tracking-tight">
2092
+ {activeProject ? activeProject : "Dashboard"}
2093
+ </h1>
2094
+
2095
+ <div className="ml-2 flex items-center gap-1 rounded-md bg-[var(--color-bg-elevated)] p-0.5">
2096
+ {TAB_DEFINITIONS.map((tab) => (
2097
+ <button
2098
+ key={tab.id}
2099
+ onClick={() => setDashboardTab(tab.id)}
2100
+ className={`rounded-md px-2.5 py-1 text-[10px] font-semibold transition-colors ${
2101
+ dashboardTab === tab.id
2102
+ ? "bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] shadow-[0_1px_0_rgba(255,255,255,0.12)_inset]"
2103
+ : "text-[var(--color-text-muted)] hover:bg-[var(--color-bg-surface)] hover:text-[var(--color-text-secondary)]"
2104
+ }`}
2105
+ title={tab.subtitle}
2106
+ >
2107
+ {tab.label}
2108
+ </button>
2109
+ ))}
2110
+ </div>
2111
+
2112
+ {/* Live indicator */}
2113
+ <div className="flex items-center gap-1.5">
2114
+ <span
2115
+ className={`h-1.5 w-1.5 rounded-full ${
2116
+ connected
2117
+ ? "bg-[var(--color-status-ready)] animate-[pulse_3s_ease-in-out_infinite]"
2118
+ : "bg-[var(--color-status-error)]"
2119
+ }`}
2120
+ />
2121
+ <span className="text-[10px] text-[var(--color-text-muted)] font-medium uppercase tracking-wider">
2122
+ {connected ? "Live" : "Offline"}
2123
+ </span>
2124
+ </div>
2125
+
2126
+ <div className="flex-1" />
2127
+
2128
+ {/* Stats pills */}
2129
+ <div className="hidden items-center gap-2 sm:flex">
2130
+ <StatPill label="Sessions" value={stats.totalSessions} />
2131
+ {stats.workingSessions > 0 && (
2132
+ <StatPill
2133
+ label="Working"
2134
+ value={stats.workingSessions}
2135
+ color="var(--color-status-working)"
2136
+ />
2137
+ )}
2138
+ {stats.openPRs > 0 && (
2139
+ <StatPill
2140
+ label="PRs"
2141
+ value={stats.openPRs}
2142
+ color="var(--color-accent-violet)"
2143
+ />
2144
+ )}
2145
+ {stats.needsAttention > 0 && (
2146
+ <StatPill
2147
+ label="Attention"
2148
+ value={stats.needsAttention}
2149
+ color="var(--color-status-attention)"
2150
+ />
2151
+ )}
2152
+ </div>
2153
+
2154
+ <button
2155
+ onClick={() => {
2156
+ setCommandOpen(true);
2157
+ setCommandQuery("");
2158
+ }}
2159
+ className="hidden items-center gap-2 rounded-md border border-[var(--color-border-default)] px-2.5 py-1.5 text-[11px] text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-bg-elevated)] hover:text-[var(--color-text-secondary)] sm:flex"
2160
+ title="Open command bar"
2161
+ >
2162
+ <span>Command</span>
2163
+ <kbd className="rounded border border-[var(--color-border-default)] bg-[var(--color-bg-subtle)] px-1 py-0.5 font-mono text-[10px] text-[var(--color-text-muted)]">
2164
+ Ctrl/Cmd+K
2165
+ </kbd>
2166
+ </button>
2167
+
2168
+ {/* Theme toggle */}
2169
+ <button
2170
+ onClick={toggleTheme}
2171
+ className="rounded-md border border-[var(--color-border-default)] p-1.5 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-bg-elevated)] hover:text-[var(--color-text-secondary)]"
2172
+ title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
2173
+ >
2174
+ {theme === "dark" ? (
2175
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
2176
+ <path d="M8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0V.75A.75.75 0 018 0zm5.657 2.343a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0zM16 8a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0116 8zm-2.343 5.657a.75.75 0 01-1.06 0l-1.061-1.06a.75.75 0 111.06-1.061l1.061 1.06a.75.75 0 010 1.061zM8 16a.75.75 0 01-.75-.75v-1.5a.75.75 0 011.5 0v1.5A.75.75 0 018 16zM2.343 13.657a.75.75 0 010-1.06l1.06-1.061a.75.75 0 111.061 1.06l-1.06 1.061a.75.75 0 01-1.061 0zM0 8a.75.75 0 01.75-.75h1.5a.75.75 0 010 1.5H.75A.75.75 0 010 8zm2.343-5.657a.75.75 0 011.06 0l1.061 1.06a.75.75 0 01-1.06 1.061L2.343 3.404a.75.75 0 010-1.061z" />
2177
+ </svg>
2178
+ ) : (
2179
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
2180
+ <path d="M9.598 1.591a.75.75 0 01.785-.175 7 7 0 11-8.967 8.967.75.75 0 01.961-.96 5.5 5.5 0 007.046-7.046.75.75 0 01.175-.786z" />
2181
+ </svg>
2182
+ )}
2183
+ </button>
2184
+ </header>
2185
+ {actionError && (
2186
+ <div className="mx-6 mt-3 rounded-md border border-[rgba(239,68,68,0.25)] bg-[rgba(239,68,68,0.12)] px-3 py-2 text-[11px] text-[var(--color-status-error)]">
2187
+ {actionError}
2188
+ </div>
2189
+ )}
2190
+ {launchMessage && (
2191
+ <div className="mx-6 mt-2 rounded-md border border-[rgba(59,130,246,0.3)] bg-[rgba(59,130,246,0.1)] px-3 py-2 text-[11px] text-[var(--color-accent)]">
2192
+ {launchMessage}
2193
+ </div>
2194
+ )}
2195
+
2196
+ <section className="border-b border-[var(--color-border-subtle)] bg-[var(--color-bg-surface)] px-6 py-3">
2197
+ <div className="mb-2 flex items-start justify-between gap-2">
2198
+ <div>
2199
+ <div className="text-[10px] font-semibold uppercase tracking-wider text-[var(--color-text-muted)]">
2200
+ Launch Session
2201
+ </div>
2202
+ <p className="mt-1 text-[10px] text-[var(--color-text-muted)]">
2203
+ Session cap note: each project is limited by <code>maxSessionsPerProject</code> (default 5). If you hit the limit, kill/cleanup old sessions first or increase it in your Conductor config.
2204
+ </p>
2205
+ </div>
2206
+ <button
2207
+ type="button"
2208
+ onClick={() => setIsLaunchCollapsed((current) => !current)}
2209
+ className="rounded-md border border-[var(--color-border-default)] px-2 py-1 text-[10px] font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-bg-elevated)] hover:text-[var(--color-text-primary)]"
2210
+ >
2211
+ {isLaunchCollapsed ? "Expand" : "Collapse"}
2212
+ </button>
2213
+ </div>
2214
+ {!isLaunchCollapsed && (
2215
+ <form
2216
+ className="space-y-2"
2217
+ onSubmit={(event) => void handleLaunchSession(event)}
2218
+ >
2219
+ <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
2220
+ <select
2221
+ value={launchProjectId}
2222
+ onChange={(event) => setLaunchProjectId(event.target.value)}
2223
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-secondary)] outline-none focus:border-[var(--color-accent)]"
2224
+ >
2225
+ {configProjects.length === 0 && <option value="">No projects</option>}
2226
+ {configProjects.map((project) => (
2227
+ <option key={project.id} value={project.id}>
2228
+ {project.id}
2229
+ </option>
2230
+ ))}
2231
+ </select>
2232
+ <input
2233
+ type="text"
2234
+ value={launchIssueId}
2235
+ onChange={(event) => setLaunchIssueId(event.target.value)}
2236
+ placeholder="Issue ID (optional)"
2237
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none focus:border-[var(--color-accent)]"
2238
+ />
2239
+ <select
2240
+ value={launchAgent}
2241
+ onChange={(event) => setLaunchAgent(event.target.value)}
2242
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-secondary)] outline-none focus:border-[var(--color-accent)]"
2243
+ >
2244
+ <option value="auto">Auto (project default)</option>
2245
+ {launchAgentOptions.map((agent) => (
2246
+ <option key={agent} value={agent}>
2247
+ {agent}
2248
+ </option>
2249
+ ))}
2250
+ <option value="custom">Custom agent</option>
2251
+ </select>
2252
+ <input
2253
+ list="launch-agent-list"
2254
+ type="text"
2255
+ value={launchAgent}
2256
+ onChange={(event) => setLaunchAgent(event.target.value)}
2257
+ placeholder="or type a custom agent name"
2258
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none focus:border-[var(--color-accent)]"
2259
+ />
2260
+ <datalist id="launch-agent-list">
2261
+ <option value="auto" />
2262
+ {launchAgentOptions.map((agent) => (
2263
+ <option key={`agent-list-${agent}`} value={agent} />
2264
+ ))}
2265
+ </datalist>
2266
+ </div>
2267
+ <div className="grid gap-2 sm:grid-cols-3">
2268
+ <input
2269
+ type="text"
2270
+ value={launchModel}
2271
+ onChange={(event) => setLaunchModel(event.target.value)}
2272
+ placeholder="Model (optional)"
2273
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none focus:border-[var(--color-accent)]"
2274
+ />
2275
+ <input
2276
+ type="text"
2277
+ value={launchProfile}
2278
+ onChange={(event) => setLaunchProfile(event.target.value)}
2279
+ placeholder="Profile (optional)"
2280
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none focus:border-[var(--color-accent)]"
2281
+ />
2282
+ <div className="grid gap-2 sm:grid-cols-2 sm:col-span-1">
2283
+ <input
2284
+ type="text"
2285
+ value={launchBranch}
2286
+ onChange={(event) => setLaunchBranch(event.target.value)}
2287
+ placeholder="Branch (optional)"
2288
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none focus:border-[var(--color-accent)]"
2289
+ />
2290
+ <input
2291
+ type="text"
2292
+ value={launchBaseBranch}
2293
+ onChange={(event) => setLaunchBaseBranch(event.target.value)}
2294
+ placeholder="Base branch (optional)"
2295
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none focus:border-[var(--color-accent)]"
2296
+ />
2297
+ </div>
2298
+ </div>
2299
+ <textarea
2300
+ value={launchPrompt}
2301
+ onChange={(event) => setLaunchPrompt(event.target.value)}
2302
+ rows={2}
2303
+ placeholder="Prompt for this task"
2304
+ className="min-h-[74px] w-full rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none focus:border-[var(--color-accent)]"
2305
+ />
2306
+ <div className="flex items-center gap-2">
2307
+ <button
2308
+ type="submit"
2309
+ disabled={launchLoading}
2310
+ className="rounded-md border border-[var(--color-accent)] px-2.5 py-1.5 text-[11px] font-semibold text-[var(--color-accent)] transition-colors hover:bg-[var(--color-accent-subtle)] disabled:opacity-50"
2311
+ >
2312
+ {launchLoading ? "Launching..." : "Launch Session"}
2313
+ </button>
2314
+ <button
2315
+ type="button"
2316
+ onClick={() => {
2317
+ setLaunchPrompt("");
2318
+ setLaunchIssueId("");
2319
+ setLaunchModel("");
2320
+ setLaunchProfile("");
2321
+ setLaunchBranch("");
2322
+ setLaunchBaseBranch("");
2323
+ setLaunchMessage(null);
2324
+ }}
2325
+ className="rounded-md border border-[var(--color-border-default)] px-2.5 py-1.5 text-[11px] text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-elevated)]"
2326
+ >
2327
+ Reset
2328
+ </button>
2329
+ </div>
2330
+ </form>
2331
+ )}
2332
+ </section>
2333
+
2334
+ <section className="border-b border-[var(--color-border-subtle)] bg-[var(--color-bg-surface)] px-6 py-3">
2335
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-center">
2336
+ <div className="flex min-w-0 flex-1 items-center gap-2">
2337
+ <input
2338
+ ref={searchInputRef}
2339
+ type="text"
2340
+ value={search}
2341
+ onChange={(event) => setSearch(event.target.value)}
2342
+ placeholder={searchPlaceholder}
2343
+ className="w-full rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-3 py-2 text-[12px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none focus:border-[var(--color-accent)]"
2344
+ />
2345
+ </div>
2346
+
2347
+ {dashboardTab !== "agents" && (
2348
+ <div className="flex flex-wrap items-center gap-2">
2349
+ <select
2350
+ value={statusFilter}
2351
+ onChange={(event) => setStatusFilter(event.target.value as StatusFilter)}
2352
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-secondary)] outline-none focus:border-[var(--color-accent)]"
2353
+ disabled={dashboardTab === "chat" || dashboardTab === "review"}
2354
+ >
2355
+ <option value="all">All statuses</option>
2356
+ <option value="active">Active only</option>
2357
+ <option value="terminal">Terminal only</option>
2358
+ <option value="attention">Needs attention</option>
2359
+ </select>
2360
+
2361
+ <select
2362
+ value={agentFilter}
2363
+ onChange={(event) => setAgentFilter(event.target.value)}
2364
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-secondary)] outline-none focus:border-[var(--color-accent)]"
2365
+ disabled={dashboardTab === "chat" || dashboardTab === "review"}
2366
+ >
2367
+ <option value="all">All agents</option>
2368
+ {discoveredAgentOptions.map((agent) => (
2369
+ <option key={agent} value={agent}>{agent}</option>
2370
+ ))}
2371
+ </select>
2372
+
2373
+ <select
2374
+ value={sortMode}
2375
+ onChange={(event) => setSortMode(event.target.value as SortMode)}
2376
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-secondary)] outline-none focus:border-[var(--color-accent)]"
2377
+ disabled={dashboardTab === "chat" || dashboardTab === "review"}
2378
+ >
2379
+ <option value="recent">Recent activity</option>
2380
+ <option value="oldest">Oldest activity</option>
2381
+ <option value="attention">Attention priority</option>
2382
+ <option value="cost">Cost high to low</option>
2383
+ </select>
2384
+
2385
+ <button
2386
+ onClick={() => setAttentionOnly((v) => !v)}
2387
+ disabled={dashboardTab === "chat" || dashboardTab === "review"}
2388
+ className={`rounded-md border px-2.5 py-2 text-[11px] font-medium transition-colors ${
2389
+ attentionOnly
2390
+ ? "border-[var(--color-accent)] bg-[var(--color-accent-subtle)] text-[var(--color-accent)]"
2391
+ : "border-[var(--color-border-default)] text-[var(--color-text-secondary)] hover:border-[var(--color-border-strong)]"
2392
+ }`}
2393
+ >
2394
+ Attention only
2395
+ </button>
2396
+
2397
+ <button
2398
+ onClick={() => setViewMode((v) => (v === "grid" ? "lanes" : "grid"))}
2399
+ className="rounded-md border border-[var(--color-border-default)] px-2.5 py-2 text-[11px] font-medium text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-strong)]"
2400
+ disabled={dashboardTab === "chat" || dashboardTab === "review"}
2401
+ >
2402
+ {viewMode === "grid" ? "Lane View" : "Grid View"}
2403
+ </button>
2404
+
2405
+ <button
2406
+ onClick={() => void handleCleanupTerminal()}
2407
+ disabled={cleanupCandidates.length === 0 || bulkBusy || busySessionId !== null || dashboardTab === "chat" || dashboardTab === "review"}
2408
+ className="rounded-md border border-[rgba(239,68,68,0.28)] px-2.5 py-2 text-[11px] font-medium text-[var(--color-status-error)] transition-colors hover:bg-[rgba(239,68,68,0.08)] disabled:cursor-not-allowed disabled:opacity-40"
2409
+ title={cleanupCandidates.length === 0 ? "No terminal sessions in current view" : "Clean up terminal sessions in current view"}
2410
+ >
2411
+ {bulkBusy ? "Cleaning..." : `Cleanup (${cleanupCandidates.length})`}
2412
+ </button>
2413
+ </div>
2414
+ )}
2415
+ </div>
2416
+
2417
+ <div className="mt-2 text-[11px] text-[var(--color-text-muted)]">
2418
+ Showing {dashboardTab === "agents"
2419
+ ? visibleAgentRoster.length
2420
+ : visibleTabSessions.length} {dashboardTab === "agents" ? "agents" : "sessions"} of {
2421
+ dashboardTab === "agents"
2422
+ ? totalAgentCatalogCount
2423
+ : sessions.length
2424
+ } {dashboardTab === "agents" ? "agents" : "sessions"}
2425
+ {activeProject ? ` in ${activeProject}` : ""}.
2426
+ </div>
2427
+ </section>
2428
+
2429
+ {/* Content */}
2430
+ <main className="flex-1 overflow-auto p-6">
2431
+ {dashboardTab === "agents" && (
2432
+ <div className="space-y-4">
2433
+ {visibleAgentRoster.length === 0 ? (
2434
+ <div className="rounded-md border border-dashed border-[var(--color-border-subtle)] bg-[var(--color-bg-surface)] px-4 py-6 text-[11px] text-[var(--color-text-muted)]">
2435
+ No agents discovered.
2436
+ </div>
2437
+ ) : (
2438
+ <>
2439
+ {visibleKnownAgents.length > 0 && (
2440
+ <section className="space-y-2">
2441
+ <h3 className="text-[12px] font-semibold uppercase tracking-wide text-[var(--color-text-muted)]">
2442
+ Known agents
2443
+ </h3>
2444
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
2445
+ {visibleKnownAgents.map((agent) => {
2446
+ const known = getKnownAgent(agent.name) ?? getKnownAgent(agent.launchName);
2447
+ const agentIconSource: AgentIconSeed = {
2448
+ label: agent.label,
2449
+ launchName: agent.launchName,
2450
+ homepage: agent.homepage,
2451
+ iconUrl: agent.iconUrl,
2452
+ };
2453
+ const capabilities = known?.capabilities ?? agent.capabilities ?? [];
2454
+ return (
2455
+ <article
2456
+ key={agent.name}
2457
+ className="rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-4"
2458
+ >
2459
+ <div className="mb-3 flex items-start justify-between gap-2">
2460
+ <div className="min-w-0 flex-1">
2461
+ <div className="mb-1 flex items-center gap-2">
2462
+ <AgentIcon agent={agentIconSource} className="h-6 w-6" />
2463
+ <h2 className="truncate text-[14px] font-semibold text-[var(--color-text-primary)]">
2464
+ {agent.label}
2465
+ </h2>
2466
+ <span className="rounded-full bg-[rgba(59,130,246,0.12)] px-2 py-0.5 text-[9px] text-[var(--color-accent)]">
2467
+ official
2468
+ </span>
2469
+ </div>
2470
+ <p className="truncate text-[10px] text-[var(--color-text-muted)]">
2471
+ launch: {agent.launchName} ·
2472
+ version {agent.version ?? "n/a"} ·
2473
+ source {agent.installed ? "installed" : "not-installed"}
2474
+ </p>
2475
+ </div>
2476
+ </div>
2477
+ <p className="text-[11px] text-[var(--color-text-muted)] mb-3">
2478
+ {agent.description ?? "Agent plugin metadata not available."}
2479
+ </p>
2480
+ {capabilities.length > 0 && (
2481
+ <div className="mb-3 flex flex-wrap gap-1">
2482
+ {capabilities.map((capability) => (
2483
+ <span
2484
+ key={`${agent.name}-${capability}`}
2485
+ className="rounded-full border border-[var(--color-border-subtle)] bg-[var(--color-bg-subtle)] px-2 py-0.5 text-[9px] uppercase tracking-wide text-[var(--color-text-muted)]"
2486
+ >
2487
+ {capability}
2488
+ </span>
2489
+ ))}
2490
+ </div>
2491
+ )}
2492
+ {known && !agent.installed && known.installHint && (
2493
+ <p className="text-[10px] text-[var(--color-text-muted)] mb-2">
2494
+ Install: <span className="font-mono text-[var(--color-text-secondary)]">{known.installHint}</span>
2495
+ </p>
2496
+ )}
2497
+ {agent.homepage && (
2498
+ <a
2499
+ href={agent.homepage}
2500
+ target="_blank"
2501
+ rel="noopener noreferrer"
2502
+ className="mb-3 inline-block text-[10px] text-[var(--color-accent)] hover:underline"
2503
+ >
2504
+ Documentation ↗
2505
+ </a>
2506
+ )}
2507
+ <div className="grid grid-cols-3 gap-2 text-[11px] mb-3">
2508
+ <div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg-base)] px-2 py-2">
2509
+ <div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">Sessions</div>
2510
+ <div className="text-[14px] font-semibold text-[var(--color-text-primary)]">{agent.totalSessions}</div>
2511
+ </div>
2512
+ <div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg-base)] px-2 py-2">
2513
+ <div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">Active</div>
2514
+ <div className="text-[14px] font-semibold text-[var(--color-text-primary)]">{agent.activeSessions}</div>
2515
+ </div>
2516
+ <div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg-base)] px-2 py-2">
2517
+ <div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">Needs attention</div>
2518
+ <div className="text-[14px] font-semibold text-[var(--color-status-error)]">{agent.attentionSessions}</div>
2519
+ </div>
2520
+ </div>
2521
+ <div className="grid grid-cols-2 gap-2 text-[11px]">
2522
+ <button
2523
+ onClick={() => setLaunchAgent(agent.launchName)}
2524
+ disabled={!agent.installed}
2525
+ className="rounded-md bg-[var(--color-accent)] px-2 py-1.5 font-medium text-white transition-colors hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
2526
+ >
2527
+ {agent.installed ? "Launch now" : "Not installed"}
2528
+ </button>
2529
+ <button
2530
+ type="button"
2531
+ onClick={async () => {
2532
+ const command = agent.commandHint ?? `conductor --agent ${agent.launchName}`;
2533
+ try {
2534
+ await navigator.clipboard.writeText(command);
2535
+ setLaunchMessage(`Copied launch command: ${command}`);
2536
+ setActionError(null);
2537
+ } catch {
2538
+ setActionError("Clipboard write failed.");
2539
+ }
2540
+ }}
2541
+ className="rounded-md border border-[var(--color-border-default)] px-2 py-1.5 text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-strong)]"
2542
+ >
2543
+ Copy launch command
2544
+ </button>
2545
+ </div>
2546
+ </article>
2547
+ );
2548
+ })}
2549
+ </div>
2550
+ </section>
2551
+ )}
2552
+
2553
+ {visibleDiscoveredAgents.length > 0 && (
2554
+ <section className="space-y-2">
2555
+ <h3 className="text-[12px] font-semibold uppercase tracking-wide text-[var(--color-text-muted)]">
2556
+ Discovered agents
2557
+ </h3>
2558
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
2559
+ {visibleDiscoveredAgents.map((agent) => {
2560
+ const agentIconSource: AgentIconSeed = {
2561
+ label: agent.label,
2562
+ launchName: agent.launchName,
2563
+ homepage: agent.homepage,
2564
+ iconUrl: agent.iconUrl,
2565
+ };
2566
+ return (
2567
+ <article
2568
+ key={agent.name}
2569
+ className="rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-4"
2570
+ >
2571
+ <div className="mb-3 flex items-start justify-between gap-2">
2572
+ <div className="min-w-0 flex-1">
2573
+ <div className="mb-1 flex items-center gap-2">
2574
+ <AgentIcon agent={agentIconSource} className="h-6 w-6" />
2575
+ <h2 className="truncate text-[14px] font-semibold text-[var(--color-text-primary)]">
2576
+ {agent.label}
2577
+ </h2>
2578
+ <span className="rounded-full bg-[rgba(239,68,68,0.12)] px-2 py-0.5 text-[9px] text-[var(--color-status-error)]">
2579
+ {agent.version ? `v${agent.version}` : "unknown"}
2580
+ </span>
2581
+ </div>
2582
+ <p className="truncate text-[10px] text-[var(--color-text-muted)]">
2583
+ launch: {agent.launchName} · source: detected runtime
2584
+ </p>
2585
+ </div>
2586
+ </div>
2587
+ <p className="text-[11px] text-[var(--color-text-muted)] mb-3">
2588
+ {agent.description ?? "Agent plugin currently detected."}
2589
+ </p>
2590
+ <div className="grid grid-cols-3 gap-2 text-[11px] mb-3">
2591
+ <div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg-base)] px-2 py-2">
2592
+ <div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">Sessions</div>
2593
+ <div className="text-[14px] font-semibold text-[var(--color-text-primary)]">{agent.totalSessions}</div>
2594
+ </div>
2595
+ <div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg-base)] px-2 py-2">
2596
+ <div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">Active</div>
2597
+ <div className="text-[14px] font-semibold text-[var(--color-text-primary)]">{agent.activeSessions}</div>
2598
+ </div>
2599
+ <div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg-base)] px-2 py-2">
2600
+ <div className="text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">Needs attention</div>
2601
+ <div className="text-[14px] font-semibold text-[var(--color-status-error)]">{agent.attentionSessions}</div>
2602
+ </div>
2603
+ </div>
2604
+ <button
2605
+ onClick={() => setLaunchAgent(agent.launchName)}
2606
+ className="w-full rounded-md border border-[var(--color-border-default)] px-2 py-1.5 text-[11px] text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-strong)]"
2607
+ >
2608
+ Use this agent for next launch
2609
+ </button>
2610
+ </article>
2611
+ );
2612
+ })}
2613
+ </div>
2614
+ </section>
2615
+ )}
2616
+ </>
2617
+ )}
2618
+ </div>
2619
+ )}
2620
+
2621
+ <>
2622
+ {visibleTabSessions.length === 0 ? (
2623
+ <EmptyState />
2624
+ ) : dashboardTab === "overview" && viewMode === "lanes" ? (
2625
+ <div className="flex min-w-max gap-4">
2626
+ {LANE_META.map((lane) => (
2627
+ <LaneColumn
2628
+ key={lane.id}
2629
+ title={lane.title}
2630
+ color={lane.color}
2631
+ count={sessionsByLane[lane.id].length}
2632
+ >
2633
+ {sessionsByLane[lane.id].length === 0 ? (
2634
+ <div className="rounded-lg border border-dashed border-[var(--color-border-subtle)] bg-[var(--color-bg-surface)] px-3 py-4 text-[11px] text-[var(--color-text-muted)]">
2635
+ No sessions
2636
+ </div>
2637
+ ) : (
2638
+ <div className="space-y-3">
2639
+ {sessionsByLane[lane.id].map((session) => (
2640
+ <SessionCard
2641
+ key={session.id}
2642
+ session={session}
2643
+ onSend={handleSend}
2644
+ onKill={handleKill}
2645
+ onRestore={handleRestore}
2646
+ onOpenTerminal={handleOpenTerminal}
2647
+ actionBusy={busySessionId === session.id || bulkBusy}
2648
+ />
2649
+ ))}
2650
+ </div>
2651
+ )}
2652
+ </LaneColumn>
2653
+ ))}
2654
+ </div>
2655
+ ) : dashboardTab === "overview" ? (
2656
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
2657
+ {filteredSessions.map((session) => (
2658
+ <SessionCard
2659
+ key={session.id}
2660
+ session={session}
2661
+ onSend={handleSend}
2662
+ onKill={handleKill}
2663
+ onRestore={handleRestore}
2664
+ onOpenTerminal={handleOpenTerminal}
2665
+ actionBusy={busySessionId === session.id || bulkBusy}
2666
+ />
2667
+ ))}
2668
+ </div>
2669
+ ) : (
2670
+ <div className="space-y-3">
2671
+ {visibleTabSessions.map((session) => {
2672
+ const reviewDiff = reviewDiffState[session.id] ?? EMPTY_REVIEW_DIFF;
2673
+ const activeChecks = sessionChecksState[session.id] ?? EMPTY_SESSION_CHECKS;
2674
+ const isTrackableForChecks = Boolean(session.pr && session.pr.number > 0);
2675
+ const quickActions = dashboardTab === "review" ? REVIEW_QUICK_ACTIONS : CHAT_QUICK_ACTIONS;
2676
+ const currentMessage = dashboardTab === "review"
2677
+ ? reviewMessages[session.id] ?? ""
2678
+ : chatMessages[session.id] ?? "";
2679
+ const fileSearchValue = reviewDiff.fileSearch.trim().toLowerCase();
2680
+ const lineSearchValue = reviewDiff.search.trim().toLowerCase();
2681
+ const filteredFiles = reviewDiff.files.filter((file) => {
2682
+ return fileSearchValue.length === 0 || file.path.toLowerCase().includes(fileSearchValue);
2683
+ });
2684
+ const selectedFile = filteredFiles.find((file) => file.path === reviewDiff.selectedFilePath)
2685
+ ?? filteredFiles[0];
2686
+ const visibleLines = selectedFile
2687
+ ? selectedFile.lines.filter((line) => {
2688
+ if (lineSearchValue.length === 0) return true;
2689
+ const query = lineSearchValue;
2690
+ if (line.text.toLowerCase().includes(query)) return true;
2691
+ if (line.oldLine != null && `${line.oldLine}`.includes(query)) return true;
2692
+ if (line.newLine != null && `${line.newLine}`.includes(query)) return true;
2693
+ return false;
2694
+ })
2695
+ : [];
2696
+
2697
+ return (
2698
+ <article key={session.id} className="overflow-hidden rounded-2xl border border-[var(--color-border-subtle)] bg-[var(--color-bg-surface)] p-3">
2699
+ <SessionCard
2700
+ session={session}
2701
+ onSend={handleSend}
2702
+ onKill={handleKill}
2703
+ onRestore={handleRestore}
2704
+ onOpenTerminal={handleOpenTerminal}
2705
+ actionBusy={busySessionId === session.id || bulkBusy}
2706
+ />
2707
+ <div className="mt-3 space-y-3 border-t border-[var(--color-border-subtle)] pt-3">
2708
+ <div className="rounded-xl border border-[var(--color-border-subtle)] bg-[linear-gradient(to_right,_rgba(59,130,246,0.1),_rgba(99,102,241,0.08))] p-3">
2709
+ <div className="mb-2 flex flex-wrap items-center gap-2">
2710
+ <span className="text-[11px] font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]">
2711
+ CI checks
2712
+ </span>
2713
+ {isTrackableForChecks ? (
2714
+ <>
2715
+ <span
2716
+ className="rounded-full px-2 py-0.5 text-[10px] text-[var(--color-text-muted)]"
2717
+ style={{ background: getCIBadgeStyle(activeChecks.ciStatus).color }}
2718
+ >
2719
+ {getCIBadgeStyle(activeChecks.ciStatus).label}
2720
+ </span>
2721
+ {activeChecks.source !== "not-loaded" ? (
2722
+ <span className="rounded-full border border-[rgba(148,163,184,0.5)] px-2 py-0.5 text-[9px] text-[var(--color-text-muted)]">
2723
+ {activeChecks.source}
2724
+ </span>
2725
+ ) : null}
2726
+ <button
2727
+ type="button"
2728
+ onClick={() => void handleLoadSessionChecks(session.id)}
2729
+ disabled={activeChecks.loading}
2730
+ className="rounded-md border border-[var(--color-accent)] px-2 py-1 text-[10px] font-medium text-[var(--color-accent)] disabled:cursor-not-allowed disabled:opacity-40"
2731
+ >
2732
+ {activeChecks.loaded
2733
+ ? (activeChecks.loading ? "Refreshing..." : "Refresh")
2734
+ : "Load checks"}
2735
+ </button>
2736
+ </>
2737
+ ) : null}
2738
+ <span className="ml-auto text-[10px] text-[var(--color-text-muted)]">
2739
+ {activeChecks.generatedAt ? formatGeneratedAt(activeChecks.generatedAt) : "not checked"}
2740
+ </span>
2741
+ </div>
2742
+ {activeChecks.loading && !activeChecks.loaded && (
2743
+ <div className="rounded-md bg-[var(--color-bg-subtle)] px-2.5 py-2 text-[10px] text-[var(--color-text-muted)]">
2744
+ Loading CI checks...
2745
+ </div>
2746
+ )}
2747
+ {activeChecks.error && (
2748
+ <div className="rounded-md border border-[rgba(239,68,68,0.2)] bg-[rgba(239,68,68,0.12)] px-2.5 py-2 text-[10px] text-[var(--color-status-error)]">
2749
+ {activeChecks.error}
2750
+ </div>
2751
+ )}
2752
+ {activeChecks.loaded && activeChecks.checks.length === 0 && (
2753
+ <div className="rounded-md border border-dashed border-[var(--color-border-subtle)] px-2.5 py-2 text-[10px] text-[var(--color-text-muted)]">
2754
+ No CI checks returned yet.
2755
+ </div>
2756
+ )}
2757
+ {activeChecks.loaded && activeChecks.checks.length > 0 && (
2758
+ <div className="space-y-1">
2759
+ {activeChecks.checks.map((check) => {
2760
+ const checkBadge = getCIBadgeStyle(getCheckBadgeStatus(check.status));
2761
+ return (
2762
+ <div
2763
+ key={`${session.id}-${check.name}`}
2764
+ className="flex items-center gap-2 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg-surface)] px-2 py-1.5 text-[10px]"
2765
+ >
2766
+ <span className={`h-1.5 w-1.5 rounded-full ${checkBadge.dot}`} />
2767
+ <span className="truncate text-[var(--color-text-secondary)]">{check.name}</span>
2768
+ <span className="ml-auto rounded-full px-1.5 py-0.5 text-[9px] uppercase tracking-wide text-[var(--color-text-muted)]">
2769
+ {check.status}
2770
+ </span>
2771
+ {check.url ? (
2772
+ <a
2773
+ href={check.url}
2774
+ target="_blank"
2775
+ rel="noopener noreferrer"
2776
+ title="Open check details"
2777
+ className="text-[9px] font-semibold text-[var(--color-accent)] hover:underline"
2778
+ onClick={(event) => event.stopPropagation()}
2779
+ >
2780
+ details
2781
+ </a>
2782
+ ) : null}
2783
+ </div>
2784
+ );
2785
+ })}
2786
+ </div>
2787
+ )}
2788
+ </div>
2789
+ {dashboardTab === "review" && (
2790
+ <div className="rounded-xl border border-[var(--color-border-subtle)] bg-[var(--color-bg-base)] p-3">
2791
+ <div className="mb-2 flex flex-wrap items-center gap-2">
2792
+ <span className="text-[11px] font-semibold text-[var(--color-text-secondary)]">
2793
+ Review diff
2794
+ </span>
2795
+ {reviewDiff.loaded && (
2796
+ <span className="rounded-full bg-[var(--color-bg-subtle)] px-2 py-0.5 text-[10px] text-[var(--color-text-muted)]">
2797
+ {reviewDiff.files.length} files · {reviewDiff.untracked.length} untracked
2798
+ </span>
2799
+ )}
2800
+ <span className="rounded-full border border-[rgba(148,163,184,0.5)] px-2 py-0.5 text-[9px] text-[var(--color-text-muted)]">
2801
+ {formatDiffSource(reviewDiff.source)}
2802
+ </span>
2803
+ {reviewDiff.loaded && reviewDiff.generatedAt && (
2804
+ <span className="ml-auto text-[10px] text-[var(--color-text-muted)]">
2805
+ {formatUTCTime(reviewDiff.generatedAt)}
2806
+ </span>
2807
+ )}
2808
+ </div>
2809
+
2810
+ <div className="mb-3 flex flex-wrap items-center gap-2">
2811
+ <button
2812
+ type="button"
2813
+ onClick={() => void handleLoadReviewDiff(session.id)}
2814
+ disabled={reviewDiff.loading}
2815
+ className="rounded-md border border-[var(--color-accent)] px-2.5 py-1.5 text-[10px] font-semibold text-[var(--color-accent)] transition-colors hover:bg-[var(--color-accent-subtle)] disabled:cursor-not-allowed disabled:opacity-40"
2816
+ >
2817
+ {reviewDiff.loaded ? (reviewDiff.loading ? "Refreshing..." : "Refresh diff") : "Load diff"}
2818
+ </button>
2819
+ <button
2820
+ type="button"
2821
+ onClick={() => updateReviewDiffState(session.id, { wrapLines: !reviewDiff.wrapLines })}
2822
+ className="rounded-md border border-[var(--color-border-default)] px-2.5 py-1.5 text-[10px] text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-strong)]"
2823
+ >
2824
+ {reviewDiff.wrapLines ? "Wrap: On" : "Wrap: Off"}
2825
+ </button>
2826
+ <input
2827
+ type="text"
2828
+ value={reviewDiff.fileSearch}
2829
+ onChange={(event) => {
2830
+ const next = event.target.value;
2831
+ updateReviewDiffState(session.id, {
2832
+ fileSearch: next,
2833
+ selectedFilePath: next.trim().length === 0
2834
+ ? reviewDiff.selectedFilePath
2835
+ : (reviewDiff.files.find((file) =>
2836
+ file.path.toLowerCase().includes(next.trim().toLowerCase()),
2837
+ )?.path ?? reviewDiff.selectedFilePath),
2838
+ });
2839
+ }}
2840
+ placeholder="Filter files..."
2841
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-1.5 text-[10px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none focus:border-[var(--color-accent)]"
2842
+ />
2843
+ <input
2844
+ type="text"
2845
+ value={reviewDiff.search}
2846
+ onChange={(event) => {
2847
+ updateReviewDiffState(session.id, {
2848
+ search: event.target.value,
2849
+ });
2850
+ }}
2851
+ placeholder="Filter diff lines..."
2852
+ className="rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-1.5 text-[10px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none focus:border-[var(--color-accent)]"
2853
+ />
2854
+ </div>
2855
+
2856
+ {reviewDiff.loading && (
2857
+ <div className="rounded-md bg-[var(--color-bg-subtle)] px-2.5 py-2 text-[10px] text-[var(--color-text-muted)]">
2858
+ Loading {formatDiffSource(reviewDiff.source)} diff for this session...
2859
+ </div>
2860
+ )}
2861
+
2862
+ {reviewDiff.error && (
2863
+ <div className="rounded-md border border-[rgba(239,68,68,0.2)] bg-[rgba(239,68,68,0.12)] px-2.5 py-2 text-[10px] text-[var(--color-status-error)]">
2864
+ {reviewDiff.error}
2865
+ </div>
2866
+ )}
2867
+
2868
+ {reviewDiff.loaded && !reviewDiff.hasDiff && (
2869
+ <div className="rounded-md border border-dashed border-[var(--color-border-subtle)] px-2.5 py-2 text-[10px] text-[var(--color-text-muted)]">
2870
+ No {formatDiffSource(reviewDiff.source)} diff detected for this session.
2871
+ </div>
2872
+ )}
2873
+
2874
+ {reviewDiff.loaded && reviewDiff.hasDiff && (
2875
+ <div className="grid gap-2 xl:grid-cols-[22rem_minmax(0,1fr)]">
2876
+ <div className="space-y-2">
2877
+ <div className="rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg-surface)] p-2 text-[9px] text-[var(--color-text-muted)]">
2878
+ File set {filteredFiles.length} / {reviewDiff.files.length}
2879
+ </div>
2880
+ <div className="max-h-[320px] overflow-y-auto rounded-md border border-[var(--color-border-subtle)]">
2881
+ {filteredFiles.length === 0 ? (
2882
+ <div className="px-2 py-2 text-[10px] text-[var(--color-text-muted)]">
2883
+ No files match this query.
2884
+ </div>
2885
+ ) : (
2886
+ filteredFiles.map((file) => {
2887
+ const isSelected = selectedFile?.path === file.path;
2888
+ const statusColor = file.status === "added"
2889
+ ? "rgba(34,197,94,0.16)"
2890
+ : file.status === "deleted"
2891
+ ? "rgba(239,68,68,0.16)"
2892
+ : file.status === "renamed" || file.status === "copy"
2893
+ ? "rgba(59,130,246,0.16)"
2894
+ : file.status === "binary"
2895
+ ? "rgba(217,119,6,0.16)"
2896
+ : "rgba(63,63,70,0.25)";
2897
+
2898
+ return (
2899
+ <button
2900
+ key={`review-file-${session.id}-${file.path}`}
2901
+ onClick={() => {
2902
+ updateReviewDiffState(session.id, {
2903
+ selectedFilePath: file.path,
2904
+ });
2905
+ }}
2906
+ className={`w-full border-b border-[var(--color-border-subtle)] px-2 py-2 text-left transition-colors last:border-b-0 ${
2907
+ isSelected
2908
+ ? "border-l-2 border-l-[var(--color-accent)] bg-[rgba(59,130,246,0.08)]"
2909
+ : "bg-[var(--color-bg-base)] hover:bg-[var(--color-bg-subtle)]"
2910
+ }`}
2911
+ >
2912
+ <div className="flex items-center justify-between gap-2">
2913
+ <span className="truncate text-[10px] text-[var(--color-text-secondary)]">{file.path}</span>
2914
+ <span
2915
+ className="rounded-full px-1.5 py-0.5 text-[9px] uppercase tracking-wider"
2916
+ style={{ background: statusColor }}
2917
+ >
2918
+ {file.status}
2919
+ </span>
2920
+ </div>
2921
+ <div className="mt-1 flex items-center gap-2 text-[9px] text-[var(--color-text-muted)]">
2922
+ <span>+{file.additions}</span>
2923
+ <span>−{file.deletions}</span>
2924
+ </div>
2925
+ </button>
2926
+ );
2927
+ })
2928
+ )}
2929
+ </div>
2930
+ </div>
2931
+
2932
+ <div className="space-y-2">
2933
+ {selectedFile ? (
2934
+ <div className="overflow-hidden rounded-md border border-[var(--color-border-subtle)]">
2935
+ <div className="grid grid-cols-[5rem_5rem_1rem_auto] border-b border-[var(--color-border-subtle)] bg-[var(--color-bg-subtle)] px-2 py-1 text-[9px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
2936
+ <div>old</div>
2937
+ <div>new</div>
2938
+ <div></div>
2939
+ <div>line</div>
2940
+ </div>
2941
+ <div
2942
+ className={`max-h-[320px] overflow-auto font-mono text-[11px] ${reviewDiff.wrapLines ? "whitespace-pre-wrap break-words" : "overflow-x-auto whitespace-nowrap"}`}
2943
+ >
2944
+ {visibleLines.length === 0 ? (
2945
+ <div className="px-2 py-2 text-[10px] text-[var(--color-text-muted)]">
2946
+ No lines match “{reviewDiff.search}”.
2947
+ </div>
2948
+ ) : (
2949
+ visibleLines.map((line, index) => {
2950
+ const marker = line.kind === "add"
2951
+ ? "+"
2952
+ : line.kind === "remove"
2953
+ ? "-"
2954
+ : line.kind === "hunk"
2955
+ ? "@"
2956
+ : line.kind === "meta"
2957
+ ? " "
2958
+ : line.kind === "info"
2959
+ ? "i"
2960
+ : " ";
2961
+ const lineBackground = line.kind === "add"
2962
+ ? "bg-[rgba(34,197,94,0.12)]"
2963
+ : line.kind === "remove"
2964
+ ? "bg-[rgba(239,68,68,0.12)]"
2965
+ : line.kind === "hunk"
2966
+ ? "bg-[rgba(59,130,246,0.09)]"
2967
+ : "bg-transparent";
2968
+ const lineTextColor = line.kind === "add"
2969
+ ? "text-[rgba(52,211,153,1)]"
2970
+ : line.kind === "remove"
2971
+ ? "text-[rgba(248,113,113,1)]"
2972
+ : line.kind === "hunk"
2973
+ ? "text-[rgba(96,165,250,1)]"
2974
+ : line.kind === "meta"
2975
+ ? "text-[var(--color-text-muted)]"
2976
+ : line.kind === "info"
2977
+ ? "text-[var(--color-text-muted)] italic"
2978
+ : "text-[var(--color-text-primary)]";
2979
+
2980
+ return (
2981
+ <div
2982
+ key={`${selectedFile.path}-${line.oldLine}-${line.newLine}-${index}`}
2983
+ className={`grid border-b border-[var(--color-border-subtle)] grid-cols-[5rem_5rem_1rem_auto] px-2 py-0.5 last:border-b-0 ${lineBackground}`}
2984
+ >
2985
+ <div className="text-[10px] text-[var(--color-text-muted)]">
2986
+ {line.oldLine ?? ""}
2987
+ </div>
2988
+ <div className="text-[10px] text-[var(--color-text-muted)]">
2989
+ {line.newLine ?? ""}
2990
+ </div>
2991
+ <div className={lineTextColor}>{marker}</div>
2992
+ <div className={lineTextColor}>{line.text}</div>
2993
+ </div>
2994
+ );
2995
+ })
2996
+ )}
2997
+ </div>
2998
+ </div>
2999
+ ) : (
3000
+ <div className="rounded-md border border-[var(--color-border-subtle)] px-2.5 py-2 text-[10px] text-[var(--color-text-muted)]">
3001
+ Select a file to inspect its diff.
3002
+ </div>
3003
+ )}
3004
+ </div>
3005
+ </div>
3006
+ )}
3007
+
3008
+ {reviewDiff.loaded && reviewDiff.untracked.length > 0 && (
3009
+ <div className="mt-2 text-[10px] text-[var(--color-text-muted)]">
3010
+ <span className="font-semibold text-[var(--color-text-secondary)]">Untracked:</span>{" "}
3011
+ {reviewDiff.untracked.join(", ")}
3012
+ </div>
3013
+ )}
3014
+ {reviewDiff.loaded && reviewDiff.truncated && (
3015
+ <div className="mt-2 text-[10px] text-[var(--color-text-muted)]">
3016
+ Diff output truncated due to size.
3017
+ </div>
3018
+ )}
3019
+ </div>
3020
+ )}
3021
+ <div className="rounded-2xl border border-[var(--color-border-subtle)] bg-[linear-gradient(to_right,_rgba(15,23,42,0.32),_rgba(15,23,42,0.15))] p-3 shadow-[0_10px_24px_rgba(2,6,23,0.2)]">
3022
+ <div className="mb-2 flex items-center justify-between gap-2">
3023
+ <div className="flex items-center gap-2">
3024
+ <span className="inline-flex h-2 w-2 rounded-full bg-[var(--color-accent)]" />
3025
+ <span className="text-[11px] font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]">
3026
+ {dashboardTab === "review" ? "Reviewer board" : "Chat board"}
3027
+ </span>
3028
+ </div>
3029
+ <span className="rounded-full border border-[rgba(148,163,184,0.4)] px-2 py-0.5 text-[9px] uppercase tracking-wide text-[var(--color-text-muted)]">
3030
+ {dashboardTab}
3031
+ </span>
3032
+ </div>
3033
+
3034
+ <div className="mb-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
3035
+ {quickActions.map((action) => (
3036
+ <button
3037
+ key={`${dashboardTab}-${session.id}-${action.label}`}
3038
+ type="button"
3039
+ onClick={() => {
3040
+ applyQuickMessage(session.id, dashboardTab, action.message);
3041
+ }}
3042
+ className="rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] px-2 py-1.5 text-[10px] font-medium text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-strong)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-elevated)]"
3043
+ >
3044
+ {action.label}
3045
+ </button>
3046
+ ))}
3047
+ </div>
3048
+
3049
+ <form
3050
+ className="space-y-2"
3051
+ onSubmit={(event) => {
3052
+ event.preventDefault();
3053
+ if (dashboardTab === "review") {
3054
+ void handleReviewSend(session.id);
3055
+ } else {
3056
+ void handleChatSend(session.id);
3057
+ }
3058
+ }}
3059
+ >
3060
+ <textarea
3061
+ value={currentMessage}
3062
+ onChange={(event) => {
3063
+ const next = event.target.value;
3064
+ if (dashboardTab === "review") {
3065
+ setReviewMessages((prev) => ({ ...prev, [session.id]: next }));
3066
+ } else {
3067
+ setChatMessages((prev) => ({ ...prev, [session.id]: next }));
3068
+ }
3069
+ }}
3070
+ onKeyDown={(event) => {
3071
+ if (event.key === "Enter" && !event.shiftKey) {
3072
+ event.preventDefault();
3073
+ if (dashboardTab === "review") {
3074
+ void handleReviewSend(session.id);
3075
+ } else {
3076
+ void handleChatSend(session.id);
3077
+ }
3078
+ }
3079
+ }}
3080
+ rows={3}
3081
+ placeholder={dashboardTab === "review" ? "Reviewer notes..." : "Reply to this agent..."}
3082
+ className="min-h-[84px] w-full resize-none rounded-md border border-[var(--color-border-default)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[11px] text-[var(--color-text-primary)] placeholder-[var(--color-text-muted)] outline-none transition-[background,color,opacity] focus:border-[var(--color-accent)] focus:bg-[var(--color-bg-surface)]"
3083
+ />
3084
+ <div className="flex items-center justify-between gap-2">
3085
+ <span className="text-[9px] text-[var(--color-text-muted)]">
3086
+ Shift+Enter for line breaks
3087
+ </span>
3088
+ <button
3089
+ type="submit"
3090
+ disabled={
3091
+ dashboardTab === "review"
3092
+ ? reviewSendingSession === session.id || (reviewMessages[session.id] ?? "").trim().length === 0
3093
+ : chatSendingSession === session.id || (chatMessages[session.id] ?? "").trim().length === 0
3094
+ }
3095
+ className="rounded-md bg-[var(--color-accent)] px-2.5 py-1.5 text-[10px] font-semibold text-white transition-colors hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
3096
+ >
3097
+ {dashboardTab === "review"
3098
+ ? reviewSendingSession === session.id
3099
+ ? "Sending..."
3100
+ : "Send review"
3101
+ : chatSendingSession === session.id
3102
+ ? "Sending..."
3103
+ : "Send"}
3104
+ </button>
3105
+ </div>
3106
+ </form>
3107
+ </div>
3108
+ </div>
3109
+ </article>
3110
+ );
3111
+ })}
3112
+ </div>
3113
+ )}
3114
+ </>
3115
+ </main>
3116
+
3117
+ {commandOpen && (
3118
+ <div
3119
+ className="fixed inset-0 z-50 flex items-start justify-center bg-[rgba(9,11,16,0.58)] p-4 pt-24 backdrop-blur-[1.5px]"
3120
+ onClick={() => setCommandOpen(false)}
3121
+ >
3122
+ <div
3123
+ className="w-full max-w-xl overflow-hidden rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] shadow-[0_20px_70px_rgba(0,0,0,0.35)]"
3124
+ onClick={(event) => event.stopPropagation()}
3125
+ >
3126
+ <div className="border-b border-[var(--color-border-subtle)] px-4 py-3">
3127
+ <input
3128
+ ref={commandInputRef}
3129
+ type="text"
3130
+ value={commandQuery}
3131
+ onChange={(event) => setCommandQuery(event.target.value)}
3132
+ placeholder="Run command..."
3133
+ className="w-full bg-transparent text-[13px] text-[var(--color-text-primary)] outline-none placeholder-[var(--color-text-muted)]"
3134
+ />
3135
+ </div>
3136
+ <div className="max-h-[60vh] overflow-y-auto p-2">
3137
+ {visibleCommandActions.length === 0 ? (
3138
+ <div className="px-3 py-2 text-[12px] text-[var(--color-text-muted)]">No commands found.</div>
3139
+ ) : (
3140
+ visibleCommandActions.map((action) => (
3141
+ <button
3142
+ key={action.id}
3143
+ onClick={() => {
3144
+ action.run();
3145
+ setCommandOpen(false);
3146
+ }}
3147
+ className="flex w-full items-center rounded-md px-3 py-2 text-left transition-colors hover:bg-[var(--color-bg-elevated)]"
3148
+ >
3149
+ <span className="text-[12px] text-[var(--color-text-primary)]">{action.label}</span>
3150
+ <span className="ml-auto text-[10px] uppercase tracking-wider text-[var(--color-text-muted)]">{action.hint ?? ""}</span>
3151
+ </button>
3152
+ ))
3153
+ )}
3154
+ </div>
3155
+ </div>
3156
+ </div>
3157
+ )}
3158
+
3159
+ {cleanupDialog.open && (
3160
+ <div
3161
+ className="fixed inset-0 z-50 flex items-center justify-center bg-[rgba(9,11,16,0.58)] p-4 backdrop-blur-[1.5px]"
3162
+ onClick={() => closeCleanupDialog()}
3163
+ >
3164
+ <div
3165
+ className="w-full max-w-md overflow-hidden rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] shadow-[0_20px_70px_rgba(0,0,0,0.35)]"
3166
+ onClick={(event) => event.stopPropagation()}
3167
+ >
3168
+ <div className="border-b border-[var(--color-border-subtle)] px-4 py-3">
3169
+ <h2 className="text-[13px] font-semibold text-[var(--color-text-primary)]">
3170
+ {cleanupDialog.title}
3171
+ </h2>
3172
+ </div>
3173
+ <div className="px-4 py-3 space-y-3">
3174
+ <p className="text-[11px] leading-relaxed text-[var(--color-text-secondary)]">
3175
+ {cleanupDialog.message}
3176
+ </p>
3177
+ <div className="max-h-28 overflow-y-auto rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-bg-base)] px-2.5 py-2 text-[10px] font-mono text-[var(--color-text-muted)]">
3178
+ {cleanupDialog.sessionIds.map((sessionId) => (
3179
+ <div key={sessionId} className="truncate py-0.5">
3180
+ {sessionId}
3181
+ </div>
3182
+ ))}
3183
+ </div>
3184
+ <div className="mt-1 flex items-center justify-end gap-2">
3185
+ <button
3186
+ type="button"
3187
+ onClick={closeCleanupDialog}
3188
+ disabled={cleanupDialogBusy}
3189
+ className="rounded-md border border-[var(--color-border-default)] px-2.5 py-1.5 text-[11px] font-medium text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border-strong)] disabled:opacity-50"
3190
+ >
3191
+ Cancel
3192
+ </button>
3193
+ <button
3194
+ type="button"
3195
+ onClick={() => {
3196
+ void confirmCleanup();
3197
+ }}
3198
+ disabled={cleanupDialogBusy}
3199
+ className="rounded-md bg-[var(--color-status-error)] px-2.5 py-1.5 text-[11px] font-medium text-white transition-opacity disabled:opacity-50"
3200
+ >
3201
+ {cleanupDialogBusy ? "Cleaning..." : cleanupDialogLabel}
3202
+ </button>
3203
+ </div>
3204
+ </div>
3205
+ </div>
3206
+ </div>
3207
+ )}
3208
+ </div>
3209
+ </div>
3210
+ );
3211
+ }
3212
+
3213
+ /** Stat pill badge for the header */
3214
+ function getCIBadgeStyle(status: CICheckState) {
3215
+ return CI_STATUS_META[status] ?? CI_STATUS_META.none;
3216
+ }
3217
+
3218
+ function getCheckBadgeStatus(status: CICheckInfo["status"]): CICheckState {
3219
+ if (status === "passed") return "passing";
3220
+ if (status === "failed") return "failing";
3221
+ if (status === "running" || status === "pending") return "pending";
3222
+ return "none";
3223
+ }
3224
+
3225
+ function formatDiffSource(source: ReviewDiffSource): string {
3226
+ return source === "working-tree"
3227
+ ? "Working tree"
3228
+ : source === "remote-pr"
3229
+ ? "Remote PR"
3230
+ : "No diff";
3231
+ }
3232
+
3233
+ function formatUTCDateTime(generatedAt: string): string {
3234
+ const date = new Date(generatedAt);
3235
+ if (Number.isNaN(date.getTime())) return "—";
3236
+
3237
+ const pad = (value: number) => `${value}`.padStart(2, "0");
3238
+ return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} UTC`;
3239
+ }
3240
+
3241
+ function formatUTCTime(generatedAt: string): string {
3242
+ const date = new Date(generatedAt);
3243
+ if (Number.isNaN(date.getTime())) return "—";
3244
+
3245
+ const pad = (value: number) => `${value}`.padStart(2, "0");
3246
+ return `${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} UTC`;
3247
+ }
3248
+
3249
+ function formatGeneratedAt(generatedAt: string): string {
3250
+ return formatUTCDateTime(generatedAt);
3251
+ }
3252
+
3253
+ function ProjectFavicon({ projectId, repo, iconUrl }: { projectId: string; repo: string | null; iconUrl?: string | null }) {
3254
+ const [iconErrorIndex, setIconErrorIndex] = useState(0);
3255
+ const [isLoaded, setIsLoaded] = useState(false);
3256
+ const faviconUrls = useMemo(() => getProjectFaviconUrls(repo, iconUrl), [repo, iconUrl]);
3257
+ useEffect(() => {
3258
+ setIconErrorIndex(0);
3259
+ setIsLoaded(false);
3260
+ }, [repo, iconUrl]);
3261
+ useEffect(() => setIsLoaded(false), [iconErrorIndex]);
3262
+ const FALLBACK_COLORS = useMemo(
3263
+ () => [
3264
+ "#14b8a6",
3265
+ "#22c55e",
3266
+ "#84cc16",
3267
+ "#eab308",
3268
+ "#f97316",
3269
+ "#f43f5e",
3270
+ "#a855f7",
3271
+ "#ec4899",
3272
+ "#06b6d4",
3273
+ "#0ea5e9",
3274
+ ],
3275
+ [],
3276
+ );
3277
+
3278
+ const accent = useMemo(() => {
3279
+ let hash = 0;
3280
+ for (let i = 0; i < projectId.length; i += 1) {
3281
+ hash = (hash * 31 + projectId.charCodeAt(i)) % 360;
3282
+ }
3283
+ return FALLBACK_COLORS[hash % FALLBACK_COLORS.length] ?? "#6b7280";
3284
+ }, [projectId]);
3285
+
3286
+ const onImageError = () => {
3287
+ setIsLoaded(false);
3288
+ setIconErrorIndex((current) => current + 1);
3289
+ };
3290
+
3291
+ const shouldUseFallback =
3292
+ !faviconUrls.length || iconErrorIndex >= faviconUrls.length || !faviconUrls[iconErrorIndex];
3293
+
3294
+ if (shouldUseFallback) {
3295
+ return <DefaultProjectIcon projectId={projectId} color={accent} />;
3296
+ }
3297
+
3298
+ return (
3299
+ <span className="relative inline-flex h-5 w-5 shrink-0">
3300
+ {!isLoaded && <DefaultProjectIcon projectId={projectId} color={accent} />}
3301
+ <img
3302
+ src={faviconUrls[iconErrorIndex]}
3303
+ alt={`${projectId} favicon`}
3304
+ className={`h-5 w-5 shrink-0 rounded-sm border border-[var(--color-border-subtle)] bg-white object-cover ${isLoaded ? "inline-flex" : "hidden"}`}
3305
+ onError={onImageError}
3306
+ onLoad={() => setIsLoaded(true)}
3307
+ loading="lazy"
3308
+ />
3309
+ </span>
3310
+ );
3311
+ }
3312
+
3313
+ function GitHubLogo({ className = "h-4 w-4", fillColor }: LogoIconProps) {
3314
+ return (
3315
+ <svg
3316
+ viewBox="0 0 24 24"
3317
+ fill="none"
3318
+ role="img"
3319
+ aria-hidden="true"
3320
+ xmlns="http://www.w3.org/2000/svg"
3321
+ className={className}
3322
+ style={fillColor ? { color: fillColor } : undefined}
3323
+ >
3324
+ <title>GitHub</title>
3325
+ <path
3326
+ d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
3327
+ fill={fillColor || "currentColor"}
3328
+ />
3329
+ </svg>
3330
+ );
3331
+ }
3332
+
3333
+ function ObsidianLogo({ className = "h-4 w-4", fillColor }: LogoIconProps) {
3334
+ return (
3335
+ <svg
3336
+ viewBox="0 0 24 24"
3337
+ fill="none"
3338
+ role="img"
3339
+ aria-hidden="true"
3340
+ xmlns="http://www.w3.org/2000/svg"
3341
+ className={className}
3342
+ style={fillColor ? { color: fillColor } : undefined}
3343
+ >
3344
+ <title>Obsidian</title>
3345
+ <path
3346
+ d="M19.355 18.538a68.967 68.959 0 0 0 1.858-2.954.81.81 0 0 0-.062-.9c-.516-.685-1.504-2.075-2.042-3.362-.553-1.321-.636-3.375-.64-4.377a1.707 1.707 0 0 0-.358-1.05l-3.198-4.064a3.744 3.744 0 0 1-.076.543c-.106.503-.307 1.004-.536 1.5-.134.29-.29.6-.446.914l-.31.626c-.516 1.068-.997 2.227-1.132 3.59-.124 1.26.046 2.73.815 4.481.128.011.257.025.386.044a6.363 6.363 0 0 1 3.326 1.505c.916.79 1.744 1.922 2.415 3.5zM8.199 22.569c.073.012.146.02.22.02.78.024 2.095.092 3.16.29.87.16 2.593.64 4.01 1.055 1.083.316 2.198-.548 2.355-1.664.114-.814.33-1.735.725-2.58l-.01.005c-.67-1.87-1.522-3.078-2.416-3.849a5.295 5.295 0 0 0-2.778-1.257c-1.54-.216-2.952.19-3.84.45.532 2.218.368 4.829-1.425 7.531zM5.533 9.938c-.023.1-.056.197-.098.29L2.82 16.059a1.602 1.602 0 0 0 .313 1.772l4.116 4.24c2.103-3.101 1.796-6.02.836-8.3-.728-1.73-1.832-3.081-2.55-3.831zM9.32 14.01c.615-.183 1.606-.465 2.745-.534-.683-1.725-.848-3.233-.716-4.577.154-1.552.7-2.847 1.235-3.95.113-.235.223-.454.328-.664.149-.297.288-.577.419-.86.217-.47.379-.885.46-1.27.08-.38.08-.72-.014-1.043-.095-.325-.297-.675-.68-1.06a1.6 1.6 0 0 0-1.475.36l-4.95 4.452a1.602 1.602 0 0 0-.513.952l-.427 2.83c.672.59 2.328 2.316 3.335 4.711.09.21.175.43.253.653z"
3347
+ fill={fillColor || "currentColor"}
3348
+ />
3349
+ </svg>
3350
+ );
3351
+ }
3352
+
3353
+ function StatPill({
3354
+ label,
3355
+ value,
3356
+ color,
3357
+ }: {
3358
+ label: string;
3359
+ value: number;
3360
+ color?: string;
3361
+ }) {
3362
+ return (
3363
+ <div
3364
+ className="flex items-center gap-1.5 rounded-full border px-2.5 py-1"
3365
+ style={{
3366
+ borderColor: color ? `color-mix(in srgb, ${color} 25%, transparent)` : "var(--color-border-default)",
3367
+ background: color ? `color-mix(in srgb, ${color} 8%, transparent)` : "transparent",
3368
+ }}
3369
+ >
3370
+ {color && (
3371
+ <span
3372
+ className="h-1.5 w-1.5 rounded-full"
3373
+ style={{ background: color }}
3374
+ />
3375
+ )}
3376
+ <span
3377
+ className="text-[12px] font-semibold tabular-nums"
3378
+ style={{ color: color ?? "var(--color-text-primary)" }}
3379
+ >
3380
+ {value}
3381
+ </span>
3382
+ <span className="text-[10px] text-[var(--color-text-muted)]">
3383
+ {label}
3384
+ </span>
3385
+ </div>
3386
+ );
3387
+ }
3388
+
3389
+ const ATTENTION_RANK: Record<AttentionGroup, number> = {
3390
+ respond: 0,
3391
+ review: 1,
3392
+ merge: 2,
3393
+ pending: 3,
3394
+ working: 4,
3395
+ done: 5,
3396
+ };
3397
+
3398
+ const LANE_META: Array<{ id: AttentionGroup; title: string; color: string }> = [
3399
+ { id: "respond", title: "Respond", color: "var(--color-status-error)" },
3400
+ { id: "review", title: "Review", color: "var(--color-accent-orange)" },
3401
+ { id: "merge", title: "Merge", color: "var(--color-status-ready)" },
3402
+ { id: "pending", title: "Pending", color: "var(--color-status-attention)" },
3403
+ { id: "working", title: "Working", color: "var(--color-status-working)" },
3404
+ { id: "done", title: "Done", color: "var(--color-text-muted)" },
3405
+ ];
3406
+
3407
+ function attentionRank(session: DashboardSession): number {
3408
+ const level = getAttentionLevel(session) as AttentionGroup;
3409
+ return ATTENTION_RANK[level] ?? 99;
3410
+ }
3411
+
3412
+ function parseEstimatedCost(session: DashboardSession): number {
3413
+ const raw = session.metadata["cost"];
3414
+ if (!raw) return 0;
3415
+ try {
3416
+ const parsed = JSON.parse(raw) as { estimatedCostUsd?: number; totalUSD?: number };
3417
+ return parsed.estimatedCostUsd ?? parsed.totalUSD ?? 0;
3418
+ } catch {
3419
+ return 0;
3420
+ }
3421
+ }
3422
+
3423
+ function LaneColumn({
3424
+ title,
3425
+ color,
3426
+ count,
3427
+ children,
3428
+ }: {
3429
+ title: string;
3430
+ color: string;
3431
+ count: number;
3432
+ children: ReactNode;
3433
+ }) {
3434
+ return (
3435
+ <section className="w-[360px] shrink-0 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-3">
3436
+ <div className="mb-3 flex items-center gap-2">
3437
+ <span className="h-2 w-2 rounded-full" style={{ background: color }} />
3438
+ <h2 className="text-[12px] font-semibold text-[var(--color-text-primary)]">{title}</h2>
3439
+ <span className="ml-auto rounded-full bg-[var(--color-bg-subtle)] px-2 py-0.5 text-[10px] text-[var(--color-text-muted)]">{count}</span>
3440
+ </div>
3441
+ {children}
3442
+ </section>
3443
+ );
3444
+ }