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,4559 @@
1
+ "use client";
2
+
3
+ import dynamic from "next/dynamic";
4
+ import { type FormEvent, memo, useCallback, useEffect, useMemo, useState } from "react";
5
+ import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
6
+ import {
7
+ getAgentModelCatalog,
8
+ resolveAgentModelAccess,
9
+ supportsAgentModelSelection,
10
+ type AgentModelOption,
11
+ type AgentReasoningOption,
12
+ type DashboardRole,
13
+ type ModelAccessPreferences,
14
+ type TrustedHeaderAccessProvider,
15
+ } from "@conductor-oss/core/types";
16
+ import type { IconType } from "react-icons";
17
+ import { SiNotion, SiObsidian } from "react-icons/si";
18
+ import { VscVscode } from "react-icons/vsc";
19
+ import {
20
+ Bot,
21
+ BookText,
22
+ Building2,
23
+ ChevronsRight,
24
+ Check,
25
+ ChevronDown,
26
+ Copy,
27
+ Eye,
28
+ FolderOpen,
29
+ FolderGit2,
30
+ FolderKanban,
31
+ Github,
32
+ Hand,
33
+ List,
34
+ Loader2,
35
+ Paperclip,
36
+ PlugZap,
37
+ RefreshCcw,
38
+ Search,
39
+ SlidersHorizontal,
40
+ Settings2,
41
+ type LucideIcon,
42
+ Volume2,
43
+ VolumeX,
44
+ X,
45
+ } from "lucide-react";
46
+ import type { DashboardSession } from "@/lib/types";
47
+ import { normalizeAgentName } from "@/lib/agentUtils";
48
+ import { useSessions } from "@/hooks/useSessions";
49
+ import { useConfig, type ConfigProject } from "@/hooks/useConfig";
50
+ import { useAgents } from "@/hooks/useAgents";
51
+ import { AppShell } from "@/components/layout/AppShell";
52
+ import { TopBar } from "@/components/layout/TopBar";
53
+ import { AgentTileIcon } from "@/components/AgentTileIcon";
54
+ import { WorkspaceSidebarPanel } from "@/components/layout/WorkspaceSidebarPanel";
55
+ import { normalizeModelAccessPreferences } from "@/lib/modelAccess";
56
+ import {
57
+ getRuntimeCatalogDefaultModelForAccess,
58
+ getRuntimeCatalogDefaultReasoning,
59
+ getRuntimeCatalogModelsForAccess,
60
+ getRuntimeCatalogReasoningOptions,
61
+ type RuntimeAgentModelCatalog,
62
+ } from "@/lib/runtimeAgentModelsShared";
63
+
64
+ const EXECUTOR_ORDER = [
65
+ "codex",
66
+ "gemini",
67
+ "qwen-code",
68
+ "droid",
69
+ "claude-code",
70
+ "amp",
71
+ "opencode",
72
+ "github-copilot",
73
+ "cursor-cli",
74
+ "ccr",
75
+ ];
76
+
77
+ const EXECUTOR_LABELS: Record<string, string> = {
78
+ codex: "Codex",
79
+ gemini: "Gemini",
80
+ "qwen-code": "Qwen Code",
81
+ droid: "Droid",
82
+ "claude-code": "Claude Code",
83
+ amp: "Amp",
84
+ opencode: "Opencode",
85
+ "github-copilot": "Copilot",
86
+ "cursor-cli": "Cursor Agent",
87
+ ccr: "CCR",
88
+ };
89
+
90
+ const AGENT_SETUP_URLS: Record<string, string> = {
91
+ "claude-code": "https://claude.ai/",
92
+ codex: "https://chatgpt.com/codex",
93
+ gemini: "https://aistudio.google.com/",
94
+ "qwen-code": "https://chat.qwen.ai/",
95
+ opencode: "https://opencode.ai/",
96
+ "github-copilot": "https://github.com/settings/copilot",
97
+ };
98
+
99
+ const SessionDetail = dynamic(
100
+ () => import("@/components/sessions/SessionDetail").then((mod) => mod.SessionDetail),
101
+ {
102
+ loading: () => (
103
+ <div className="flex h-full items-center justify-center text-[13px] text-[var(--vk-text-muted)]">
104
+ Loading session...
105
+ </div>
106
+ ),
107
+ },
108
+ );
109
+
110
+ const WorkspaceKanban = dynamic(
111
+ () => import("@/components/board/WorkspaceKanban").then((mod) => mod.WorkspaceKanban),
112
+ {
113
+ loading: () => (
114
+ <div className="flex h-full items-center justify-center text-[13px] text-[var(--vk-text-muted)]">
115
+ Loading board...
116
+ </div>
117
+ ),
118
+ },
119
+ );
120
+
121
+ function getAgentLabel(value: string): string {
122
+ const normalized = normalizeAgentName(value);
123
+ if (EXECUTOR_LABELS[normalized]) return EXECUTOR_LABELS[normalized];
124
+ return value
125
+ .split(/[-_\s]+/g)
126
+ .filter(Boolean)
127
+ .map((part) => part[0]?.toUpperCase() + part.slice(1))
128
+ .join(" ");
129
+ }
130
+
131
+ function formatCurrentModelLabel(agentName: string, modelId: string): string {
132
+ const normalizedModel = modelId.trim();
133
+ const normalizedAgent = normalizeAgentName(agentName);
134
+ if (!normalizedModel) return normalizedModel;
135
+
136
+ if (normalizedAgent === "claude-code") {
137
+ const lower = normalizedModel.toLowerCase();
138
+ if (lower === "opus") return "Claude Opus";
139
+ if (lower === "sonnet") return "Claude Sonnet";
140
+ if (lower === "haiku") return "Claude Haiku";
141
+ const match = lower.match(/^claude-(sonnet|opus|haiku)-(\d+)-(\d+)(?:-\d{8})?$/);
142
+ if (match) {
143
+ const family = match[1];
144
+ return `Claude ${family[0]?.toUpperCase() + family.slice(1)} ${match[2]}.${match[3]}`;
145
+ }
146
+ }
147
+
148
+ return normalizedModel
149
+ .split(/[-_]+/g)
150
+ .filter(Boolean)
151
+ .map((segment) => {
152
+ const lower = segment.toLowerCase();
153
+ if (lower === "gpt") return "GPT";
154
+ if (/^\d+(?:\.\d+)?$/.test(segment)) return segment;
155
+ return segment[0]?.toUpperCase() + segment.slice(1);
156
+ })
157
+ .join("-");
158
+ }
159
+
160
+ type NewWorkspacePayload = {
161
+ mode: "git" | "local";
162
+ projectId?: string;
163
+ agent: string;
164
+ defaultBranch: string;
165
+ useWorktree?: boolean;
166
+ gitUrl?: string;
167
+ path?: string;
168
+ initializeGit?: boolean;
169
+ };
170
+
171
+ type CreatePermissionMode = "default" | "auto" | "ask" | "plan";
172
+
173
+ type CreateSessionOptions = {
174
+ projectId?: string;
175
+ branch?: string;
176
+ baseBranch?: string;
177
+ useWorktree?: boolean;
178
+ permissionMode?: CreatePermissionMode;
179
+ issueId?: string;
180
+ };
181
+
182
+ type LinkedBoardTask = {
183
+ id: string;
184
+ text: string;
185
+ taskRef: string | null;
186
+ type: string | null;
187
+ priority: string | null;
188
+ };
189
+
190
+ type LinkedBoardResponse = {
191
+ columns?: Array<{
192
+ tasks?: LinkedBoardTask[];
193
+ }>;
194
+ };
195
+
196
+ type GitHubRepo = {
197
+ name: string;
198
+ fullName: string;
199
+ httpsUrl: string;
200
+ sshUrl: string;
201
+ defaultBranch: string;
202
+ private: boolean;
203
+ };
204
+
205
+ type DirectoryEntry = {
206
+ name: string;
207
+ path: string;
208
+ isDirectory: boolean;
209
+ isGitRepo: boolean;
210
+ };
211
+
212
+ type PreferencesPayload = {
213
+ onboardingAcknowledged: boolean;
214
+ codingAgent: string;
215
+ ide: string;
216
+ remoteSshHost: string;
217
+ remoteSshUser: string;
218
+ markdownEditor: string;
219
+ modelAccess: ModelAccessPreferences;
220
+ notifications: {
221
+ soundEnabled: boolean;
222
+ soundFile: string | null;
223
+ };
224
+ };
225
+
226
+ type AccessIdentitySummary = {
227
+ authenticated: boolean;
228
+ role: DashboardRole | null;
229
+ email: string | null;
230
+ provider: string | null;
231
+ };
232
+
233
+ function getLinkedTaskValue(task: LinkedBoardTask): string {
234
+ return task.taskRef?.trim() || task.id;
235
+ }
236
+
237
+ function getLinkedTaskTitle(text: string): string {
238
+ const [title] = text.split(" - ");
239
+ return (title ?? text).trim();
240
+ }
241
+
242
+ type AccessSettingsPayload = {
243
+ requireAuth: boolean;
244
+ defaultRole: DashboardRole;
245
+ trustedHeaders: {
246
+ enabled: boolean;
247
+ provider: TrustedHeaderAccessProvider;
248
+ emailHeader: string;
249
+ jwtHeader: string;
250
+ teamDomain: string;
251
+ audience: string;
252
+ };
253
+ roles: {
254
+ viewers: string;
255
+ operators: string;
256
+ admins: string;
257
+ viewerDomains: string;
258
+ operatorDomains: string;
259
+ adminDomains: string;
260
+ };
261
+ current: AccessIdentitySummary;
262
+ };
263
+
264
+ type RepositoryPathHealth = {
265
+ exists: boolean;
266
+ isGitRepository: boolean;
267
+ suggestedPath: string | null;
268
+ };
269
+
270
+ type RepositorySettingsPayload = {
271
+ id: string;
272
+ displayName: string;
273
+ repo: string;
274
+ path: string;
275
+ agent: string;
276
+ agentModel: string;
277
+ agentReasoningEffort: string;
278
+ workspaceMode: string;
279
+ runtimeMode: string;
280
+ scmMode: string;
281
+ defaultWorkingDirectory: string;
282
+ defaultBranch: string;
283
+ devServerScript: string;
284
+ setupScript: string;
285
+ runSetupInParallel: boolean;
286
+ cleanupScript: string;
287
+ archiveScript: string;
288
+ copyFiles: string;
289
+ pathHealth: RepositoryPathHealth;
290
+ };
291
+
292
+ type ModelSelectionState = {
293
+ catalogModel: string;
294
+ customModel: string;
295
+ reasoningEffort: string;
296
+ };
297
+
298
+ type AgentSetupState = {
299
+ name: string;
300
+ ready: boolean;
301
+ installed: boolean;
302
+ configured: boolean;
303
+ homepage: string | null;
304
+ description: string | null;
305
+ };
306
+
307
+ type PreferencesDialogMode = "onboarding" | "settings";
308
+ type SettingsTabId =
309
+ | "general"
310
+ | "remote_access"
311
+ | "repositories"
312
+ | "organization"
313
+ | "projects"
314
+ | "agents"
315
+ | "mcp"
316
+ | "preferences";
317
+
318
+ type SettingsTab = {
319
+ id: SettingsTabId;
320
+ label: string;
321
+ icon: LucideIcon;
322
+ implemented: boolean;
323
+ };
324
+
325
+ const SETTINGS_TABS: SettingsTab[] = [
326
+ { id: "general", label: "General", icon: Settings2, implemented: true },
327
+ { id: "remote_access", label: "Remote Access", icon: SlidersHorizontal, implemented: true },
328
+ { id: "repositories", label: "Repositories", icon: FolderGit2, implemented: true },
329
+ { id: "organization", label: "Organization Settings", icon: Building2, implemented: true },
330
+ { id: "projects", label: "Projects", icon: FolderKanban, implemented: false },
331
+ { id: "agents", label: "Agents", icon: Bot, implemented: true },
332
+ { id: "mcp", label: "MCP Servers", icon: PlugZap, implemented: false },
333
+ { id: "preferences", label: "Preferences", icon: SlidersHorizontal, implemented: false },
334
+ ];
335
+
336
+ const ONBOARDING_TABS: SettingsTab[] = [
337
+ { id: "preferences", label: "Preferences", icon: SlidersHorizontal, implemented: true },
338
+ { id: "repositories", label: "Repository", icon: FolderGit2, implemented: true },
339
+ ];
340
+
341
+ const IDE_OPTIONS = [
342
+ { id: "vscode", label: "VS Code" },
343
+ { id: "vscode-insiders", label: "VS Code Insiders" },
344
+ { id: "cursor", label: "Cursor" },
345
+ { id: "windsurf", label: "Windsurf" },
346
+ { id: "intellij-idea", label: "IntelliJ IDEA" },
347
+ { id: "zed", label: "Zed" },
348
+ { id: "xcode", label: "Xcode" },
349
+ { id: "antigravity", label: "Antigravity" },
350
+ { id: "custom", label: "Custom" },
351
+ ];
352
+
353
+ const MARKDOWN_EDITOR_OPTIONS = [
354
+ { id: "obsidian", label: "Obsidian" },
355
+ { id: "vscode", label: "VS Code" },
356
+ { id: "notion", label: "Notion" },
357
+ { id: "typora", label: "Typora" },
358
+ { id: "logseq", label: "Logseq" },
359
+ { id: "custom", label: "Custom" },
360
+ ];
361
+
362
+ const NOTIFICATION_SOUND_OPTIONS = [
363
+ { id: "abstract-sound-1", label: "Abstract Sound 1" },
364
+ { id: "abstract-sound-2", label: "Abstract Sound 2" },
365
+ { id: "abstract-sound-3", label: "Abstract Sound 3" },
366
+ { id: "abstract-sound-4", label: "Abstract Sound 4" },
367
+ { id: "cow-mooing", label: "Cow Mooing" },
368
+ { id: "phone-vibration", label: "Phone Vibration" },
369
+ { id: "rooster", label: "Rooster" },
370
+ ];
371
+
372
+ function toObject(value: unknown): Record<string, unknown> {
373
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
374
+ return { ...(value as Record<string, unknown>) };
375
+ }
376
+
377
+ function normalizePreferences(value: unknown, fallbackAgent: string): PreferencesPayload {
378
+ const payload = toObject(value);
379
+ const notifications = toObject(payload["notifications"]);
380
+ const soundFileRaw = notifications["soundFile"];
381
+ const codingAgent = typeof payload["codingAgent"] === "string" && payload["codingAgent"].trim().length > 0
382
+ ? payload["codingAgent"].trim()
383
+ : fallbackAgent;
384
+ const ide = typeof payload["ide"] === "string" && payload["ide"].trim().length > 0
385
+ ? payload["ide"].trim()
386
+ : "vscode";
387
+ const remoteSshHost = typeof payload["remoteSshHost"] === "string" && payload["remoteSshHost"].trim().length > 0
388
+ ? payload["remoteSshHost"].trim()
389
+ : "";
390
+ const remoteSshUser = typeof payload["remoteSshUser"] === "string" && payload["remoteSshUser"].trim().length > 0
391
+ ? payload["remoteSshUser"].trim()
392
+ : "";
393
+ const markdownEditor = typeof payload["markdownEditor"] === "string" && payload["markdownEditor"].trim().length > 0
394
+ ? payload["markdownEditor"].trim()
395
+ : "obsidian";
396
+
397
+ return {
398
+ onboardingAcknowledged: payload["onboardingAcknowledged"] === true,
399
+ codingAgent,
400
+ ide,
401
+ remoteSshHost,
402
+ remoteSshUser,
403
+ markdownEditor,
404
+ modelAccess: normalizeModelAccessPreferences(payload["modelAccess"]),
405
+ notifications: {
406
+ soundEnabled: notifications["soundEnabled"] !== false,
407
+ soundFile: soundFileRaw === null
408
+ ? null
409
+ : typeof soundFileRaw === "string" && soundFileRaw.trim().length > 0
410
+ ? soundFileRaw.trim()
411
+ : "abstract-sound-4",
412
+ },
413
+ };
414
+ }
415
+
416
+ function normalizeMultilineList(value: unknown): string {
417
+ if (Array.isArray(value)) {
418
+ return value
419
+ .filter((item): item is string => typeof item === "string")
420
+ .map((item) => item.trim())
421
+ .filter(Boolean)
422
+ .join("\n");
423
+ }
424
+ if (typeof value !== "string") return "";
425
+ return value
426
+ .split(/[\n,]+/g)
427
+ .map((item) => item.trim())
428
+ .filter(Boolean)
429
+ .join("\n");
430
+ }
431
+
432
+ function normalizeAccessSettings(value: unknown, summary?: unknown): AccessSettingsPayload {
433
+ const payload = toObject(value);
434
+ const trustedHeaders = toObject(payload["trustedHeaders"]);
435
+ const roles = toObject(payload["roles"]);
436
+ const current = toObject(summary);
437
+ const defaultRoleRaw = payload["defaultRole"];
438
+ const defaultRole: DashboardRole =
439
+ defaultRoleRaw === "viewer" || defaultRoleRaw === "admin" || defaultRoleRaw === "operator"
440
+ ? defaultRoleRaw
441
+ : "operator";
442
+ const currentRoleRaw = current["role"];
443
+
444
+ return {
445
+ requireAuth: payload["requireAuth"] === true,
446
+ defaultRole,
447
+ trustedHeaders: {
448
+ enabled: trustedHeaders["enabled"] === true,
449
+ provider: trustedHeaders["provider"] === "generic" ? "generic" : "cloudflare-access",
450
+ emailHeader: typeof trustedHeaders["emailHeader"] === "string" && trustedHeaders["emailHeader"].trim().length > 0
451
+ ? trustedHeaders["emailHeader"].trim()
452
+ : "Cf-Access-Authenticated-User-Email",
453
+ jwtHeader: typeof trustedHeaders["jwtHeader"] === "string" && trustedHeaders["jwtHeader"].trim().length > 0
454
+ ? trustedHeaders["jwtHeader"].trim()
455
+ : "Cf-Access-Jwt-Assertion",
456
+ teamDomain: typeof trustedHeaders["teamDomain"] === "string" && trustedHeaders["teamDomain"].trim().length > 0
457
+ ? trustedHeaders["teamDomain"].trim()
458
+ : "",
459
+ audience: typeof trustedHeaders["audience"] === "string" && trustedHeaders["audience"].trim().length > 0
460
+ ? trustedHeaders["audience"].trim()
461
+ : "",
462
+ },
463
+ roles: {
464
+ viewers: normalizeMultilineList(roles["viewers"]),
465
+ operators: normalizeMultilineList(roles["operators"]),
466
+ admins: normalizeMultilineList(roles["admins"]),
467
+ viewerDomains: normalizeMultilineList(roles["viewerDomains"]),
468
+ operatorDomains: normalizeMultilineList(roles["operatorDomains"]),
469
+ adminDomains: normalizeMultilineList(roles["adminDomains"]),
470
+ },
471
+ current: {
472
+ authenticated: current["authenticated"] === true,
473
+ role: currentRoleRaw === "viewer" || currentRoleRaw === "operator" || currentRoleRaw === "admin"
474
+ ? currentRoleRaw
475
+ : null,
476
+ email: typeof current["email"] === "string" && current["email"].trim().length > 0
477
+ ? current["email"].trim()
478
+ : null,
479
+ provider: typeof current["provider"] === "string" && current["provider"].trim().length > 0
480
+ ? current["provider"].trim()
481
+ : null,
482
+ },
483
+ };
484
+ }
485
+
486
+ function emptyModelSelection(): ModelSelectionState {
487
+ return {
488
+ catalogModel: "",
489
+ customModel: "",
490
+ reasoningEffort: "",
491
+ };
492
+ }
493
+
494
+ function getRuntimeModelCatalog(
495
+ agent: string,
496
+ runtimeModelCatalogs: Record<string, RuntimeAgentModelCatalog>,
497
+ ): RuntimeAgentModelCatalog | null {
498
+ return runtimeModelCatalogs[normalizeAgentName(agent)] ?? null;
499
+ }
500
+
501
+ function getAllRuntimeCatalogModels(
502
+ runtimeCatalog: RuntimeAgentModelCatalog | null,
503
+ ): AgentModelOption[] {
504
+ if (!runtimeCatalog) return [];
505
+
506
+ const ordered: AgentModelOption[] = [];
507
+ const seen = new Set<string>();
508
+ for (const group of Object.values(runtimeCatalog.modelsByAccess)) {
509
+ if (!Array.isArray(group)) continue;
510
+ for (const model of group) {
511
+ if (!model?.id || seen.has(model.id)) continue;
512
+ seen.add(model.id);
513
+ ordered.push(model);
514
+ }
515
+ }
516
+ return ordered;
517
+ }
518
+
519
+ function getSelectableAgentModels(
520
+ agent: string,
521
+ modelAccess: ModelAccessPreferences,
522
+ runtimeModelCatalogs: Record<string, RuntimeAgentModelCatalog>,
523
+ ): AgentModelOption[] {
524
+ const runtimeCatalog = getRuntimeModelCatalog(agent, runtimeModelCatalogs);
525
+ const access = resolveAgentModelAccess(agent, modelAccess);
526
+ const scopedModels = getRuntimeCatalogModelsForAccess(runtimeCatalog, access);
527
+ return scopedModels.length > 0 ? scopedModels : getAllRuntimeCatalogModels(runtimeCatalog);
528
+ }
529
+
530
+ function getSelectableAgentReasoningOptions(
531
+ agent: string,
532
+ modelAccess: ModelAccessPreferences,
533
+ runtimeModelCatalogs: Record<string, RuntimeAgentModelCatalog>,
534
+ model: string | null | undefined,
535
+ ): AgentReasoningOption[] {
536
+ const runtimeCatalog = getRuntimeModelCatalog(agent, runtimeModelCatalogs);
537
+ const access = resolveAgentModelAccess(agent, modelAccess);
538
+ return getRuntimeCatalogReasoningOptions(runtimeCatalog, model, access);
539
+ }
540
+
541
+ function getSelectableDefaultAgentModel(
542
+ agent: string,
543
+ modelAccess: ModelAccessPreferences,
544
+ runtimeModelCatalogs: Record<string, RuntimeAgentModelCatalog>,
545
+ ): string {
546
+ const runtimeCatalog = getRuntimeModelCatalog(agent, runtimeModelCatalogs);
547
+ const access = resolveAgentModelAccess(agent, modelAccess);
548
+ return getRuntimeCatalogDefaultModelForAccess(runtimeCatalog, access)
549
+ ?? getAllRuntimeCatalogModels(runtimeCatalog)[0]?.id
550
+ ?? "";
551
+ }
552
+
553
+ function getSelectableDefaultReasoningEffort(
554
+ agent: string,
555
+ modelAccess: ModelAccessPreferences,
556
+ runtimeModelCatalogs: Record<string, RuntimeAgentModelCatalog>,
557
+ model: string | null | undefined,
558
+ ): string {
559
+ const runtimeCatalog = getRuntimeModelCatalog(agent, runtimeModelCatalogs);
560
+ const access = resolveAgentModelAccess(agent, modelAccess);
561
+ return getRuntimeCatalogDefaultReasoning(runtimeCatalog, model, access) ?? "";
562
+ }
563
+
564
+ function getSelectableModelPlaceholder(
565
+ agent: string,
566
+ runtimeModelCatalogs: Record<string, RuntimeAgentModelCatalog>,
567
+ ): string {
568
+ const runtimeCatalog = getRuntimeModelCatalog(agent, runtimeModelCatalogs);
569
+ if (runtimeCatalog?.customModelPlaceholder.trim()) {
570
+ return runtimeCatalog.customModelPlaceholder;
571
+ }
572
+ const label = getAgentModelCatalog(agent)?.label ?? "agent";
573
+ return `Enter exact ${label} model id`;
574
+ }
575
+
576
+ function buildModelSelection(
577
+ agent: string,
578
+ modelAccess: ModelAccessPreferences,
579
+ runtimeModelCatalogs: Record<string, RuntimeAgentModelCatalog>,
580
+ preferredModel?: string | null,
581
+ preferredReasoningEffort?: string | null,
582
+ ): ModelSelectionState {
583
+ const trimmedPreferred = preferredModel?.trim() ?? "";
584
+ const trimmedPreferredReasoning = preferredReasoningEffort?.trim().toLowerCase() ?? "";
585
+ const availableModels = getSelectableAgentModels(agent, modelAccess, runtimeModelCatalogs);
586
+ const defaultModel = getSelectableDefaultAgentModel(agent, modelAccess, runtimeModelCatalogs);
587
+ const resolveReasoningEffort = (resolvedModel: string | null | undefined): string => {
588
+ const options = getSelectableAgentReasoningOptions(agent, modelAccess, runtimeModelCatalogs, resolvedModel);
589
+ if (trimmedPreferredReasoning.length > 0 && options.some((option) => option.id === trimmedPreferredReasoning)) {
590
+ return trimmedPreferredReasoning;
591
+ }
592
+ return getSelectableDefaultReasoningEffort(agent, modelAccess, runtimeModelCatalogs, resolvedModel);
593
+ };
594
+
595
+ if (trimmedPreferred.length > 0) {
596
+ if (availableModels.some((model) => model.id === trimmedPreferred)) {
597
+ return {
598
+ catalogModel: trimmedPreferred,
599
+ customModel: "",
600
+ reasoningEffort: resolveReasoningEffort(trimmedPreferred),
601
+ };
602
+ }
603
+
604
+ return {
605
+ catalogModel: defaultModel,
606
+ customModel: trimmedPreferred,
607
+ reasoningEffort: resolveReasoningEffort(trimmedPreferred),
608
+ };
609
+ }
610
+
611
+ return {
612
+ catalogModel: defaultModel,
613
+ customModel: "",
614
+ reasoningEffort: resolveReasoningEffort(defaultModel),
615
+ };
616
+ }
617
+
618
+ function resolveModelSelectionValue(selection: ModelSelectionState): string | undefined {
619
+ const custom = selection.customModel.trim();
620
+ if (custom.length > 0) return custom;
621
+ const catalog = selection.catalogModel.trim();
622
+ return catalog.length > 0 ? catalog : undefined;
623
+ }
624
+
625
+ function resolveReasoningSelectionValue(selection: ModelSelectionState): string | undefined {
626
+ const reasoningEffort = selection.reasoningEffort.trim().toLowerCase();
627
+ return reasoningEffort.length > 0 ? reasoningEffort : undefined;
628
+ }
629
+
630
+ function getAgentModelAccessLabel(agent: string, modelAccess: ModelAccessPreferences): string | null {
631
+ const catalog = getAgentModelCatalog(agent);
632
+ const access = resolveAgentModelAccess(agent, modelAccess);
633
+ if (!catalog || !access) return null;
634
+
635
+ return catalog.accessOptions.find((option) => option.id === access)?.label ?? null;
636
+ }
637
+
638
+ const MARKDOWN_EDITOR_ICON_CLASS = "block h-4 w-4 shrink-0";
639
+ const CODE_EDITOR_ICON_CLASS = "block h-4 w-4 shrink-0 object-contain";
640
+
641
+ type CodeEditorIconSpec =
642
+ | { kind: "icon"; icon: IconType; className: string }
643
+ | { kind: "image"; imageSrc: string; className: string };
644
+
645
+ const CODE_EDITOR_ICON_MAP: Record<string, CodeEditorIconSpec> = {
646
+ vscode: { kind: "image", imageSrc: "/icons/ide/vscode-dark.svg", className: CODE_EDITOR_ICON_CLASS },
647
+ "vscode-insiders": { kind: "image", imageSrc: "/icons/ide/vscode-insiders.svg", className: CODE_EDITOR_ICON_CLASS },
648
+ cursor: { kind: "image", imageSrc: "/icons/ide/cursor-dark.svg", className: CODE_EDITOR_ICON_CLASS },
649
+ windsurf: { kind: "image", imageSrc: "/icons/ide/windsurf-dark.svg", className: CODE_EDITOR_ICON_CLASS },
650
+ "intellij-idea": { kind: "image", imageSrc: "/icons/ide/intellij.svg", className: CODE_EDITOR_ICON_CLASS },
651
+ zed: { kind: "image", imageSrc: "/icons/ide/zed-dark.svg", className: CODE_EDITOR_ICON_CLASS },
652
+ xcode: { kind: "image", imageSrc: "/icons/ide/xcode.svg", className: CODE_EDITOR_ICON_CLASS },
653
+ antigravity: { kind: "image", imageSrc: "/icons/ide/antigravity-dark.svg", className: CODE_EDITOR_ICON_CLASS },
654
+ custom: { kind: "icon", icon: Settings2, className: `${CODE_EDITOR_ICON_CLASS} text-[var(--vk-text-muted)]` },
655
+ };
656
+
657
+ function CodeEditorIcon({ editorId, label }: { editorId: string; label: string }) {
658
+ const iconSpec = CODE_EDITOR_ICON_MAP[editorId];
659
+ if (!iconSpec) {
660
+ return <Settings2 className={`${CODE_EDITOR_ICON_CLASS} text-[var(--vk-text-muted)]`} />;
661
+ }
662
+ if (iconSpec.kind === "image") {
663
+ return <img src={iconSpec.imageSrc} alt={`${label} logo`} className={iconSpec.className} />;
664
+ }
665
+ const Icon = iconSpec.icon;
666
+ return <Icon className={iconSpec.className} />;
667
+ }
668
+
669
+ function shellQuote(value: string): string {
670
+ return JSON.stringify(value);
671
+ }
672
+
673
+ function buildRepositoryBootstrapCommand(
674
+ repository: RepositorySettingsPayload,
675
+ preferences: Pick<PreferencesPayload, "ide" | "markdownEditor">,
676
+ ): string {
677
+ const initArgs = [
678
+ "npx conductor-oss@latest setup",
679
+ "--yes",
680
+ `--path ${shellQuote(repository.path)}`,
681
+ `--project-id ${shellQuote(repository.id)}`,
682
+ `--display-name ${shellQuote(repository.displayName)}`,
683
+ `--agent ${shellQuote(repository.agent || "claude-code")}`,
684
+ `--ide ${shellQuote(preferences.ide)}`,
685
+ `--markdown-editor ${shellQuote(preferences.markdownEditor)}`,
686
+ ];
687
+
688
+ if (repository.repo.trim().length > 0) {
689
+ initArgs.push(`--repo ${shellQuote(repository.repo.trim())}`);
690
+ }
691
+ if (repository.defaultBranch.trim().length > 0) {
692
+ initArgs.push(`--default-branch ${shellQuote(repository.defaultBranch.trim())}`);
693
+ }
694
+ if (repository.agentModel.trim().length > 0) {
695
+ initArgs.push(`--model ${shellQuote(repository.agentModel.trim())}`);
696
+ }
697
+ if (repository.agentReasoningEffort.trim().length > 0) {
698
+ initArgs.push(`--reasoning-effort ${shellQuote(repository.agentReasoningEffort.trim())}`);
699
+ }
700
+ if (repository.defaultWorkingDirectory.trim().length > 0) {
701
+ initArgs.push(`--default-working-directory ${shellQuote(repository.defaultWorkingDirectory.trim())}`);
702
+ }
703
+
704
+ return initArgs.join(" ");
705
+ }
706
+
707
+ type MarkdownEditorIconSpec =
708
+ | { kind: "icon"; icon: IconType; className: string }
709
+ | { kind: "image"; imageSrc: string; className: string };
710
+
711
+ const MARKDOWN_EDITOR_ICON_MAP: Record<string, MarkdownEditorIconSpec> = {
712
+ obsidian: { kind: "icon", icon: SiObsidian, className: `${MARKDOWN_EDITOR_ICON_CLASS} text-[#8b5cf6]` },
713
+ vscode: { kind: "icon", icon: VscVscode, className: `${MARKDOWN_EDITOR_ICON_CLASS} text-[#22a3f5]` },
714
+ notion: { kind: "icon", icon: SiNotion, className: `${MARKDOWN_EDITOR_ICON_CLASS} text-white` },
715
+ logseq: { kind: "image", imageSrc: "/icons/editors/logseq.svg", className: `${MARKDOWN_EDITOR_ICON_CLASS} object-contain` },
716
+ typora: {
717
+ kind: "image",
718
+ imageSrc: "/icons/editors/typora-32.png",
719
+ className: `${MARKDOWN_EDITOR_ICON_CLASS} rounded-[3px] bg-white/90 p-[1px] object-contain`,
720
+ },
721
+ custom: { kind: "icon", icon: Settings2, className: `${MARKDOWN_EDITOR_ICON_CLASS} text-[var(--vk-text-muted)]` },
722
+ };
723
+
724
+ function MarkdownEditorIcon({ editorId }: { editorId: string }) {
725
+ const iconSpec = MARKDOWN_EDITOR_ICON_MAP[editorId];
726
+ if (!iconSpec) {
727
+ return <BookText className={`${MARKDOWN_EDITOR_ICON_CLASS} text-[var(--vk-text-muted)]`} />;
728
+ }
729
+ if (iconSpec.kind === "image") {
730
+ return <img src={iconSpec.imageSrc} alt="" className={iconSpec.className} />;
731
+ }
732
+ const Icon = iconSpec.icon;
733
+ return <Icon className={iconSpec.className} />;
734
+ }
735
+
736
+ function AgentModelSelector({
737
+ agent,
738
+ modelAccess,
739
+ runtimeModelCatalogs,
740
+ selection,
741
+ onChange,
742
+ compact = false,
743
+ }: {
744
+ agent: string;
745
+ modelAccess: ModelAccessPreferences;
746
+ runtimeModelCatalogs: Record<string, RuntimeAgentModelCatalog>;
747
+ selection: ModelSelectionState;
748
+ onChange: (next: ModelSelectionState) => void;
749
+ compact?: boolean;
750
+ }) {
751
+ if (!supportsAgentModelSelection(agent)) return null;
752
+
753
+ const catalog = getAgentModelCatalog(agent);
754
+ const availableModels = getSelectableAgentModels(agent, modelAccess, runtimeModelCatalogs);
755
+ const resolvedModel = resolveModelSelectionValue(selection) ?? selection.catalogModel;
756
+ const availableReasoningOptions = getSelectableAgentReasoningOptions(
757
+ agent,
758
+ modelAccess,
759
+ runtimeModelCatalogs,
760
+ resolvedModel,
761
+ );
762
+ const accessLabel = getAgentModelAccessLabel(agent, modelAccess);
763
+
764
+ if (!catalog) return null;
765
+
766
+ return (
767
+ <div className={compact ? "grid gap-3 md:grid-cols-3" : "space-y-3"}>
768
+ <label className="block">
769
+ <span className="mb-1.5 block text-[12px] text-[var(--vk-text-muted)]">Model</span>
770
+ <select
771
+ value={selection.catalogModel}
772
+ disabled={availableModels.length === 0}
773
+ onChange={(event) => {
774
+ const nextCatalogModel = event.target.value;
775
+ const nextReasoningOptions = getSelectableAgentReasoningOptions(
776
+ agent,
777
+ modelAccess,
778
+ runtimeModelCatalogs,
779
+ nextCatalogModel,
780
+ );
781
+ onChange({
782
+ ...selection,
783
+ catalogModel: nextCatalogModel,
784
+ reasoningEffort: nextReasoningOptions.some((option) => option.id === selection.reasoningEffort)
785
+ ? selection.reasoningEffort
786
+ : getSelectableDefaultReasoningEffort(agent, modelAccess, runtimeModelCatalogs, nextCatalogModel),
787
+ });
788
+ }}
789
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)] disabled:opacity-60"
790
+ >
791
+ {availableModels.length === 0 && (
792
+ <option value="">No runtime models detected</option>
793
+ )}
794
+ {availableModels.map((model) => (
795
+ <option key={model.id} value={model.id}>
796
+ {model.label}
797
+ </option>
798
+ ))}
799
+ </select>
800
+ <p className="mt-1 text-[11px] text-[var(--vk-text-muted)]">
801
+ {accessLabel
802
+ ? `Filtered for ${accessLabel}.`
803
+ : "Filtered for your current access preference."} Leave custom override blank to use this selection.
804
+ </p>
805
+ </label>
806
+
807
+ {availableReasoningOptions.length > 0 && (
808
+ <label className="block">
809
+ <span className="mb-1.5 block text-[12px] text-[var(--vk-text-muted)]">Reasoning Effort</span>
810
+ <select
811
+ value={selection.reasoningEffort}
812
+ onChange={(event) => {
813
+ onChange({
814
+ ...selection,
815
+ reasoningEffort: event.target.value,
816
+ });
817
+ }}
818
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
819
+ >
820
+ {availableReasoningOptions.map((option) => (
821
+ <option key={option.id} value={option.id}>
822
+ {option.label}
823
+ </option>
824
+ ))}
825
+ </select>
826
+ <p className="mt-1 text-[11px] text-[var(--vk-text-muted)]">
827
+ Choose how much deliberate reasoning the CLI should use before it acts.
828
+ </p>
829
+ </label>
830
+ )}
831
+
832
+ <label className="block">
833
+ <span className="mb-1.5 block text-[12px] text-[var(--vk-text-muted)]">Custom Model Override</span>
834
+ <input
835
+ value={selection.customModel}
836
+ onChange={(event) => {
837
+ const nextCustomModel = event.target.value;
838
+ const nextResolvedModel = nextCustomModel.trim() || selection.catalogModel;
839
+ const nextReasoningOptions = getSelectableAgentReasoningOptions(
840
+ agent,
841
+ modelAccess,
842
+ runtimeModelCatalogs,
843
+ nextResolvedModel,
844
+ );
845
+ onChange({
846
+ ...selection,
847
+ customModel: nextCustomModel,
848
+ reasoningEffort: nextReasoningOptions.some((option) => option.id === selection.reasoningEffort)
849
+ ? selection.reasoningEffort
850
+ : getSelectableDefaultReasoningEffort(
851
+ agent,
852
+ modelAccess,
853
+ runtimeModelCatalogs,
854
+ nextResolvedModel,
855
+ ),
856
+ });
857
+ }}
858
+ placeholder={getSelectableModelPlaceholder(agent, runtimeModelCatalogs)}
859
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
860
+ />
861
+ <p className="mt-1 text-[11px] text-[var(--vk-text-muted)]">
862
+ Optional. Use this to force an exact model id from the installed CLI when you want to override the detected list.
863
+ </p>
864
+ </label>
865
+ </div>
866
+ );
867
+ }
868
+
869
+ export default function Home() {
870
+ const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
871
+ const { sessions, error: sessionsError, refresh: refreshSessions } = useSessions(selectedProjectId);
872
+ const { projects, error: configError, refresh: refreshConfig } = useConfig();
873
+ const { agents } = useAgents();
874
+
875
+ const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null);
876
+ const [sidebarOpen, setSidebarOpen] = useState(true);
877
+
878
+ const [prompt, setPrompt] = useState("");
879
+ const [selectedAgent, setSelectedAgent] = useState("");
880
+ const [launchModelSelection, setLaunchModelSelection] = useState<ModelSelectionState>(emptyModelSelection());
881
+ const [creating, setCreating] = useState(false);
882
+ const [createError, setCreateError] = useState<string | null>(null);
883
+ const [newWorkspaceOpen, setNewWorkspaceOpen] = useState(false);
884
+ const [creatingWorkspace, setCreatingWorkspace] = useState(false);
885
+ const [newWorkspaceError, setNewWorkspaceError] = useState<string | null>(null);
886
+ const [workspaceView, setWorkspaceView] = useState<"chat" | "board">("chat");
887
+ const [preferences, setPreferences] = useState<PreferencesPayload | null>(null);
888
+ const [preferencesLoading, setPreferencesLoading] = useState(true);
889
+ const [preferencesSaving, setPreferencesSaving] = useState(false);
890
+ const [preferencesError, setPreferencesError] = useState<string | null>(null);
891
+ const [preferencesDialogOpen, setPreferencesDialogOpen] = useState(false);
892
+ const [pendingWorkspaceSetup, setPendingWorkspaceSetup] = useState(false);
893
+
894
+ const dashboardSessions = sessions as unknown as DashboardSession[];
895
+ const workspaceError = createError ?? configError ?? sessionsError ?? preferencesError;
896
+
897
+ useEffect(() => {
898
+ if (typeof window === "undefined") return;
899
+ if (window.innerWidth < 1024) {
900
+ setSidebarOpen(false);
901
+ }
902
+ }, []);
903
+
904
+ useEffect(() => {
905
+ if (projects.length === 0) {
906
+ if (selectedProjectId !== null) setSelectedProjectId(null);
907
+ return;
908
+ }
909
+
910
+ if (!selectedProjectId || !projects.some((project) => project.id === selectedProjectId)) {
911
+ setSelectedProjectId(projects[0]?.id ?? null);
912
+ }
913
+ }, [projects, selectedProjectId]);
914
+
915
+ useEffect(() => {
916
+ if (!selectedSessionId) return;
917
+ if (!dashboardSessions.some((session) => session.id === selectedSessionId)) {
918
+ setSelectedSessionId(null);
919
+ }
920
+ }, [dashboardSessions, selectedSessionId]);
921
+
922
+ const selectedSession = useMemo(
923
+ () => dashboardSessions.find((s) => s.id === selectedSessionId) ?? null,
924
+ [dashboardSessions, selectedSessionId],
925
+ );
926
+ const selectedProject = useMemo(
927
+ () => projects.find((project) => project.id === selectedProjectId) ?? null,
928
+ [projects, selectedProjectId],
929
+ );
930
+
931
+ const agentOptions = useMemo(() => {
932
+ const safeAgents = Array.isArray(agents)
933
+ ? agents as Array<{ name?: string; ready?: boolean; configured?: boolean; installed?: boolean }>
934
+ : [];
935
+ const opts = new Set<string>();
936
+
937
+ for (const agent of safeAgents) {
938
+ if (agent.name) {
939
+ opts.add(agent.name);
940
+ }
941
+ }
942
+ for (const project of projects) {
943
+ if (project.agent) opts.add(project.agent);
944
+ }
945
+ if (preferences?.codingAgent) {
946
+ opts.add(preferences.codingAgent);
947
+ }
948
+ if (selectedAgent) {
949
+ opts.add(selectedAgent);
950
+ }
951
+ if (opts.size === 0) {
952
+ opts.add(preferences?.codingAgent || "qwen-code");
953
+ }
954
+ return [...opts];
955
+ }, [agents, preferences?.codingAgent, projects, selectedAgent]);
956
+
957
+ const agentStatesByName = useMemo(() => {
958
+ const states: Record<string, AgentSetupState> = {};
959
+ const safeAgents = Array.isArray(agents)
960
+ ? agents as Array<{
961
+ name?: string;
962
+ ready?: boolean;
963
+ installed?: boolean;
964
+ configured?: boolean;
965
+ homepage?: string | null;
966
+ description?: string | null;
967
+ }>
968
+ : [];
969
+
970
+ for (const agent of safeAgents) {
971
+ if (!agent.name) continue;
972
+ states[normalizeAgentName(agent.name)] = {
973
+ name: agent.name,
974
+ ready: agent.ready === true,
975
+ installed: agent.installed !== false,
976
+ configured: agent.configured !== false,
977
+ homepage: typeof agent.homepage === "string" ? agent.homepage : null,
978
+ description: typeof agent.description === "string" ? agent.description : null,
979
+ };
980
+ }
981
+
982
+ return states;
983
+ }, [agents]);
984
+
985
+ const runtimeModelCatalogs = useMemo(() => {
986
+ const catalogs: Record<string, RuntimeAgentModelCatalog> = {};
987
+ const safeAgents = Array.isArray(agents)
988
+ ? agents as Array<{ name?: string; runtimeModelCatalog?: RuntimeAgentModelCatalog | null }>
989
+ : [];
990
+
991
+ for (const agent of safeAgents) {
992
+ if (!agent.name || !agent.runtimeModelCatalog) continue;
993
+ catalogs[normalizeAgentName(agent.name)] = agent.runtimeModelCatalog;
994
+ }
995
+
996
+ return catalogs;
997
+ }, [agents]);
998
+
999
+ const openAgentSetup = useCallback((agentName: string) => {
1000
+ const normalized = normalizeAgentName(agentName);
1001
+ const target = agentStatesByName[normalized]?.homepage || AGENT_SETUP_URLS[normalized];
1002
+ if (!target || typeof window === "undefined") return;
1003
+ window.open(target, "_blank", "noopener,noreferrer");
1004
+ }, [agentStatesByName]);
1005
+
1006
+ useEffect(() => {
1007
+ let cancelled = false;
1008
+ async function loadPreferences() {
1009
+ setPreferencesLoading(true);
1010
+ try {
1011
+ const res = await fetch("/api/preferences");
1012
+ const data = (await res.json().catch(() => null)) as
1013
+ | { preferences?: unknown; error?: string }
1014
+ | null;
1015
+ if (!res.ok) {
1016
+ throw new Error(data?.error ?? `Failed to load preferences: ${res.status}`);
1017
+ }
1018
+ if (cancelled) return;
1019
+ const normalized = normalizePreferences(data?.preferences, "qwen-code");
1020
+ setPreferences(normalized);
1021
+ setPreferencesError(null);
1022
+ } catch (err) {
1023
+ if (cancelled) return;
1024
+ setPreferences(normalizePreferences(null, "qwen-code"));
1025
+ setPreferencesError(err instanceof Error ? err.message : "Failed to load preferences");
1026
+ } finally {
1027
+ if (!cancelled) {
1028
+ setPreferencesLoading(false);
1029
+ }
1030
+ }
1031
+ }
1032
+
1033
+ void loadPreferences();
1034
+ return () => {
1035
+ cancelled = true;
1036
+ };
1037
+ }, []);
1038
+
1039
+ useEffect(() => {
1040
+ if (!preferences) return;
1041
+ if (!selectedAgent) {
1042
+ setSelectedAgent(preferences.codingAgent);
1043
+ }
1044
+ }, [preferences, selectedAgent]);
1045
+
1046
+ useEffect(() => {
1047
+ if (preferencesLoading) return;
1048
+ if (!preferences) return;
1049
+ if (!preferences.onboardingAcknowledged) {
1050
+ setPreferencesDialogOpen(true);
1051
+ }
1052
+ }, [preferences, preferencesLoading]);
1053
+
1054
+ useEffect(() => {
1055
+ if (selectedAgent) return;
1056
+ const fromProject = projects.find((p) => p.id === selectedProjectId)?.agent;
1057
+ if (fromProject) {
1058
+ setSelectedAgent(fromProject);
1059
+ }
1060
+ }, [projects, selectedAgent, selectedProjectId]);
1061
+
1062
+ useEffect(() => {
1063
+ if (agentOptions.length === 0) return;
1064
+ if (!selectedAgent || !agentOptions.includes(selectedAgent)) {
1065
+ const fallbackAgent = preferences?.codingAgent || "qwen-code";
1066
+ setSelectedAgent(
1067
+ agentOptions.includes(fallbackAgent)
1068
+ ? fallbackAgent
1069
+ : agentOptions[0] ?? "qwen-code",
1070
+ );
1071
+ }
1072
+ }, [agentOptions, preferences?.codingAgent, selectedAgent]);
1073
+
1074
+ useEffect(() => {
1075
+ const effectiveAgent = selectedAgent || selectedProject?.agent || preferences?.codingAgent || "qwen-code";
1076
+ const preferredModel = selectedProject && normalizeAgentName(selectedProject.agent) === normalizeAgentName(effectiveAgent)
1077
+ ? selectedProject.agentModel
1078
+ : null;
1079
+ const preferredReasoningEffort = selectedProject && normalizeAgentName(selectedProject.agent) === normalizeAgentName(effectiveAgent)
1080
+ ? selectedProject.agentReasoningEffort
1081
+ : null;
1082
+
1083
+ setLaunchModelSelection(
1084
+ buildModelSelection(
1085
+ effectiveAgent,
1086
+ preferences?.modelAccess ?? normalizeModelAccessPreferences(null),
1087
+ runtimeModelCatalogs,
1088
+ preferredModel,
1089
+ preferredReasoningEffort,
1090
+ ),
1091
+ );
1092
+ }, [preferences?.modelAccess, preferences?.codingAgent, runtimeModelCatalogs, selectedAgent, selectedProject]);
1093
+
1094
+ async function handleSavePreferences(
1095
+ next: PreferencesPayload,
1096
+ options?: { closeDialog?: boolean },
1097
+ ): Promise<boolean> {
1098
+ setPreferencesSaving(true);
1099
+ setPreferencesError(null);
1100
+ try {
1101
+ const res = await fetch("/api/preferences", {
1102
+ method: "PUT",
1103
+ headers: { "Content-Type": "application/json" },
1104
+ body: JSON.stringify(next),
1105
+ });
1106
+ const data = (await res.json().catch(() => null)) as
1107
+ | { preferences?: unknown; error?: string }
1108
+ | null;
1109
+ if (!res.ok) {
1110
+ throw new Error(data?.error ?? `Failed to save preferences: ${res.status}`);
1111
+ }
1112
+ const normalized = normalizePreferences(data?.preferences, next.codingAgent || "qwen-code");
1113
+ setPreferences(normalized);
1114
+ setSelectedAgent(normalized.codingAgent);
1115
+ if (options?.closeDialog !== false) {
1116
+ setPreferencesDialogOpen(false);
1117
+ }
1118
+ return true;
1119
+ } catch (err) {
1120
+ setPreferencesError(err instanceof Error ? err.message : "Failed to save preferences");
1121
+ return false;
1122
+ } finally {
1123
+ setPreferencesSaving(false);
1124
+ }
1125
+ }
1126
+
1127
+ const toggleSidebar = useCallback(() => setSidebarOpen((prev) => !prev), []);
1128
+
1129
+ const closeSidebarOnMobile = useCallback(() => {
1130
+ if (typeof window !== "undefined" && window.innerWidth < 1024) {
1131
+ setSidebarOpen(false);
1132
+ }
1133
+ }, []);
1134
+
1135
+ const syncSidebarForViewport = useCallback(() => {
1136
+ if (typeof window !== "undefined" && window.innerWidth < 1024) {
1137
+ setSidebarOpen(false);
1138
+ return;
1139
+ }
1140
+ setSidebarOpen(true);
1141
+ }, []);
1142
+
1143
+ const openWorkspaceDialog = useCallback(() => {
1144
+ setNewWorkspaceError(null);
1145
+ setNewWorkspaceOpen(true);
1146
+ syncSidebarForViewport();
1147
+ }, [syncSidebarForViewport]);
1148
+
1149
+ useEffect(() => {
1150
+ if (!pendingWorkspaceSetup || preferencesDialogOpen) return;
1151
+ setPendingWorkspaceSetup(false);
1152
+ openWorkspaceDialog();
1153
+ }, [pendingWorkspaceSetup, preferencesDialogOpen]);
1154
+
1155
+ const handleCreateSession = useCallback(async (options?: CreateSessionOptions) => {
1156
+ const trimmedPrompt = prompt.trim();
1157
+ if (!trimmedPrompt) return;
1158
+ const resolvedModel = resolveModelSelectionValue(launchModelSelection);
1159
+ const resolvedReasoningEffort = resolveReasoningSelectionValue(launchModelSelection);
1160
+
1161
+ const projectId = options?.projectId ?? selectedProjectId ?? projects[0]?.id;
1162
+ if (!projectId) {
1163
+ setCreateError("No project is configured in conductor.yaml");
1164
+ return;
1165
+ }
1166
+
1167
+ const effectiveAgent = selectedAgent || "qwen-code";
1168
+ const selectedAgentState = agentStatesByName[normalizeAgentName(effectiveAgent)] ?? null;
1169
+ if (selectedAgentState && !selectedAgentState.ready) {
1170
+ setCreateError(
1171
+ selectedAgentState.installed
1172
+ ? `${getAgentLabel(effectiveAgent)} is not ready yet. Finish setup or authentication and try again.`
1173
+ : `${getAgentLabel(effectiveAgent)} is not installed on this machine yet. Open setup and try again.`,
1174
+ );
1175
+ openAgentSetup(effectiveAgent);
1176
+ return;
1177
+ }
1178
+
1179
+ setCreating(true);
1180
+ setCreateError(null);
1181
+
1182
+ try {
1183
+ const res = await fetch("/api/spawn", {
1184
+ method: "POST",
1185
+ headers: { "Content-Type": "application/json" },
1186
+ body: JSON.stringify({
1187
+ projectId,
1188
+ prompt: trimmedPrompt,
1189
+ ...(options?.issueId?.trim() ? { issueId: options.issueId.trim() } : {}),
1190
+ agent: effectiveAgent,
1191
+ ...(options?.branch ? { branch: options.branch } : {}),
1192
+ ...(options?.baseBranch ? { baseBranch: options.baseBranch } : {}),
1193
+ ...(typeof options?.useWorktree === "boolean" ? { useWorktree: options.useWorktree } : {}),
1194
+ ...(options?.permissionMode ? { permissionMode: options.permissionMode } : {}),
1195
+ ...(resolvedModel ? { model: resolvedModel } : {}),
1196
+ ...(resolvedReasoningEffort ? { reasoningEffort: resolvedReasoningEffort } : {}),
1197
+ }),
1198
+ });
1199
+
1200
+ const data = (await res.json().catch(() => null)) as
1201
+ | { session?: DashboardSession; error?: string }
1202
+ | null;
1203
+
1204
+ if (!res.ok) {
1205
+ throw new Error(data?.error ?? `Failed to create workspace: ${res.status}`);
1206
+ }
1207
+
1208
+ if (!data?.session?.id) {
1209
+ throw new Error("Session created but response is missing session id");
1210
+ }
1211
+
1212
+ setPrompt("");
1213
+ setWorkspaceView("chat");
1214
+ syncSidebarForViewport();
1215
+ await refreshSessions();
1216
+ setSelectedSessionId(data.session.id);
1217
+ } catch (err) {
1218
+ setCreateError(err instanceof Error ? err.message : "Failed to create workspace");
1219
+ } finally {
1220
+ setCreating(false);
1221
+ }
1222
+ }, [
1223
+ agentStatesByName,
1224
+ launchModelSelection,
1225
+ openAgentSetup,
1226
+ projects,
1227
+ prompt,
1228
+ refreshSessions,
1229
+ selectedAgent,
1230
+ selectedProjectId,
1231
+ syncSidebarForViewport,
1232
+ ]);
1233
+
1234
+ const handleArchiveSession = useCallback(async (sessionId: string) => {
1235
+ let res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/archive`, {
1236
+ method: "POST",
1237
+ });
1238
+ let data = (await res.json().catch(() => null)) as
1239
+ | { ok?: boolean; error?: string }
1240
+ | null;
1241
+
1242
+ if (res.status === 404) {
1243
+ res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/actions`, {
1244
+ method: "POST",
1245
+ headers: { "Content-Type": "application/json" },
1246
+ body: JSON.stringify({ action: "archive" }),
1247
+ });
1248
+ data = (await res.json().catch(() => null)) as
1249
+ | { ok?: boolean; error?: string }
1250
+ | null;
1251
+ }
1252
+
1253
+ if (!res.ok) {
1254
+ throw new Error(data?.error ?? `Failed to archive session: ${res.status}`);
1255
+ }
1256
+
1257
+ if (selectedSessionId === sessionId) {
1258
+ setSelectedSessionId(null);
1259
+ }
1260
+
1261
+ await refreshSessions();
1262
+ }, [refreshSessions, selectedSessionId]);
1263
+
1264
+ const handleCreateWorkspace = useCallback(async (payload: NewWorkspacePayload) => {
1265
+ setCreatingWorkspace(true);
1266
+ setNewWorkspaceError(null);
1267
+
1268
+ try {
1269
+ const res = await fetch("/api/workspaces", {
1270
+ method: "POST",
1271
+ headers: { "Content-Type": "application/json" },
1272
+ body: JSON.stringify(payload),
1273
+ });
1274
+
1275
+ const data = (await res.json().catch(() => null)) as
1276
+ | { project?: { id?: string }; error?: string }
1277
+ | null;
1278
+
1279
+ if (!res.ok) {
1280
+ throw new Error(data?.error ?? `Failed to add workspace: ${res.status}`);
1281
+ }
1282
+
1283
+ const createdProjectId = data?.project?.id;
1284
+ if (!createdProjectId) {
1285
+ throw new Error("Workspace created but response is missing project id");
1286
+ }
1287
+
1288
+ await refreshConfig();
1289
+ setSelectedProjectId(createdProjectId);
1290
+ setSelectedSessionId(null);
1291
+ setPrompt("");
1292
+ syncSidebarForViewport();
1293
+ setNewWorkspaceOpen(false);
1294
+ } catch (err) {
1295
+ setNewWorkspaceError(err instanceof Error ? err.message : "Failed to add workspace");
1296
+ } finally {
1297
+ setCreatingWorkspace(false);
1298
+ }
1299
+ }, [refreshConfig, syncSidebarForViewport]);
1300
+
1301
+ const onboardingRequired = !preferencesLoading && !!preferences && !preferences.onboardingAcknowledged;
1302
+ const resolvedPreferences = preferences ?? normalizePreferences(null, selectedAgent || "qwen-code");
1303
+ const resolvedCodingAgent = selectedAgent || resolvedPreferences.codingAgent || "qwen-code";
1304
+
1305
+ const handleSelectProject = useCallback((projectId: string | null) => {
1306
+ setSelectedProjectId(projectId);
1307
+ setSelectedSessionId(null);
1308
+ closeSidebarOnMobile();
1309
+ }, [closeSidebarOnMobile]);
1310
+
1311
+ const handleSelectSession = useCallback((id: string) => {
1312
+ setSelectedSessionId(id);
1313
+ closeSidebarOnMobile();
1314
+ }, [closeSidebarOnMobile]);
1315
+
1316
+ const handleOpenPreferences = useCallback(() => {
1317
+ setPreferencesDialogOpen(true);
1318
+ }, []);
1319
+
1320
+ const handleCloseNewWorkspaceDialog = useCallback(() => {
1321
+ if (creatingWorkspace) return;
1322
+ setNewWorkspaceOpen(false);
1323
+ }, [creatingWorkspace]);
1324
+
1325
+ const handleClosePreferencesDialog = useCallback(() => {
1326
+ if (preferencesSaving || onboardingRequired) return;
1327
+ setPreferencesDialogOpen(false);
1328
+ setPreferencesError(null);
1329
+ }, [onboardingRequired, preferencesSaving]);
1330
+
1331
+ const sidebarContent = useMemo(() => (
1332
+ <WorkspaceSidebarPanel
1333
+ orgLabel="conductor-oss"
1334
+ projects={projects}
1335
+ selectedProjectId={selectedProjectId}
1336
+ onSelectProject={handleSelectProject}
1337
+ sessions={dashboardSessions}
1338
+ selectedSessionId={selectedSessionId}
1339
+ onSelectSession={handleSelectSession}
1340
+ onArchiveSession={handleArchiveSession}
1341
+ onCreateWorkspace={openWorkspaceDialog}
1342
+ />
1343
+ ), [
1344
+ dashboardSessions,
1345
+ handleArchiveSession,
1346
+ handleSelectProject,
1347
+ handleSelectSession,
1348
+ openWorkspaceDialog,
1349
+ projects,
1350
+ selectedProjectId,
1351
+ selectedSessionId,
1352
+ ]);
1353
+
1354
+ const workspaceContent = useMemo(() => {
1355
+ if (selectedSessionId) {
1356
+ return <SessionDetail sessionId={selectedSessionId} />;
1357
+ }
1358
+
1359
+ return (
1360
+ <div className="flex h-full min-h-0 flex-col">
1361
+ <div className="border-b border-[var(--vk-border)] px-3 py-2">
1362
+ <div className="inline-flex rounded-[3px] border border-[var(--vk-border)] p-px">
1363
+ <button
1364
+ type="button"
1365
+ onClick={() => setWorkspaceView("chat")}
1366
+ className={`min-h-[28px] rounded-[2px] px-3 text-[13px] ${
1367
+ workspaceView === "chat"
1368
+ ? "bg-[var(--vk-bg-active)] text-[var(--vk-text-strong)]"
1369
+ : "text-[var(--vk-text-muted)] hover:bg-[var(--vk-bg-hover)]"
1370
+ }`}
1371
+ >
1372
+ Chat
1373
+ </button>
1374
+ <button
1375
+ type="button"
1376
+ onClick={() => setWorkspaceView("board")}
1377
+ className={`min-h-[28px] rounded-[2px] px-3 text-[13px] ${
1378
+ workspaceView === "board"
1379
+ ? "bg-[var(--vk-bg-active)] text-[var(--vk-text-strong)]"
1380
+ : "text-[var(--vk-text-muted)] hover:bg-[var(--vk-bg-hover)]"
1381
+ }`}
1382
+ >
1383
+ Board
1384
+ </button>
1385
+ </div>
1386
+ </div>
1387
+
1388
+ <div className="min-h-0 flex-1 overflow-hidden">
1389
+ {workspaceView === "board" ? (
1390
+ <WorkspaceKanban
1391
+ projectId={selectedProjectId}
1392
+ defaultAgent={resolvedCodingAgent}
1393
+ agentOptions={agentOptions}
1394
+ />
1395
+ ) : (
1396
+ <CreateWorkspacePanel
1397
+ prompt={prompt}
1398
+ setPrompt={setPrompt}
1399
+ selectedAgent={resolvedCodingAgent}
1400
+ setSelectedAgent={setSelectedAgent}
1401
+ agentStates={agentStatesByName}
1402
+ modelSelection={launchModelSelection}
1403
+ setModelSelection={setLaunchModelSelection}
1404
+ modelAccess={resolvedPreferences.modelAccess}
1405
+ runtimeModelCatalogs={runtimeModelCatalogs}
1406
+ agentOptions={agentOptions}
1407
+ projects={projects}
1408
+ selectedProjectId={selectedProjectId}
1409
+ onSelectProject={setSelectedProjectId}
1410
+ projectLabel={selectedProjectId ?? "No project selected"}
1411
+ hasProject={projects.length > 0}
1412
+ creating={creating}
1413
+ error={workspaceError}
1414
+ onOpenAddWorkspace={openWorkspaceDialog}
1415
+ onOpenAgentSetup={openAgentSetup}
1416
+ onCreate={handleCreateSession}
1417
+ />
1418
+ )}
1419
+ </div>
1420
+ </div>
1421
+ );
1422
+ }, [
1423
+ agentOptions,
1424
+ agentStatesByName,
1425
+ creating,
1426
+ handleCreateSession,
1427
+ launchModelSelection,
1428
+ openAgentSetup,
1429
+ openWorkspaceDialog,
1430
+ preferences?.modelAccess,
1431
+ projects,
1432
+ prompt,
1433
+ resolvedCodingAgent,
1434
+ resolvedPreferences.modelAccess,
1435
+ runtimeModelCatalogs,
1436
+ selectedProjectId,
1437
+ selectedSessionId,
1438
+ workspaceError,
1439
+ workspaceView,
1440
+ ]);
1441
+
1442
+ return (
1443
+ <>
1444
+ <AppShell
1445
+ sidebarOpen={sidebarOpen}
1446
+ onToggleSidebar={toggleSidebar}
1447
+ sidebar={sidebarContent}
1448
+ >
1449
+ <TopBar
1450
+ session={selectedSession}
1451
+ fallbackTitle={selectedProjectId ?? (workspaceView === "board" ? "Board" : "Create Workspace")}
1452
+ onOpenPreferences={handleOpenPreferences}
1453
+ />
1454
+
1455
+ <div className="min-h-0 flex-1 overflow-hidden">
1456
+ {workspaceContent}
1457
+ </div>
1458
+ </AppShell>
1459
+
1460
+ {newWorkspaceOpen ? (
1461
+ <NewWorkspaceDialog
1462
+ open={newWorkspaceOpen}
1463
+ onClose={handleCloseNewWorkspaceDialog}
1464
+ onCreate={handleCreateWorkspace}
1465
+ creating={creatingWorkspace}
1466
+ error={newWorkspaceError}
1467
+ defaultAgent={resolvedCodingAgent}
1468
+ agentOptions={agentOptions}
1469
+ />
1470
+ ) : null}
1471
+
1472
+ {preferencesDialogOpen || onboardingRequired ? (
1473
+ <SettingsDialog
1474
+ open={preferencesDialogOpen}
1475
+ mode={onboardingRequired ? "onboarding" : "settings"}
1476
+ creating={preferencesSaving}
1477
+ error={preferencesError}
1478
+ current={resolvedPreferences}
1479
+ projectCount={projects.length}
1480
+ agentOptions={agentOptions}
1481
+ runtimeModelCatalogs={runtimeModelCatalogs}
1482
+ onRepositoriesChanged={refreshConfig}
1483
+ onOnboardingComplete={({ needsProject }) => {
1484
+ if (needsProject) {
1485
+ setPendingWorkspaceSetup(true);
1486
+ }
1487
+ }}
1488
+ onClose={handleClosePreferencesDialog}
1489
+ onSave={handleSavePreferences}
1490
+ />
1491
+ ) : null}
1492
+ </>
1493
+ );
1494
+ }
1495
+
1496
+ function NewWorkspaceDialog({
1497
+ open,
1498
+ onClose,
1499
+ onCreate,
1500
+ creating,
1501
+ error,
1502
+ defaultAgent,
1503
+ agentOptions,
1504
+ }: {
1505
+ open: boolean;
1506
+ onClose: () => void;
1507
+ onCreate: (payload: NewWorkspacePayload) => Promise<void>;
1508
+ creating: boolean;
1509
+ error: string | null;
1510
+ defaultAgent: string;
1511
+ agentOptions: string[];
1512
+ }) {
1513
+ const [mode, setMode] = useState<"git" | "local">("git");
1514
+ const [projectId, setProjectId] = useState("");
1515
+ const [gitUrl, setGitUrl] = useState("");
1516
+ const [path, setPath] = useState("");
1517
+ const [defaultBranch, setDefaultBranch] = useState("main");
1518
+ const [agent, setAgent] = useState(defaultAgent);
1519
+ const [useWorktree, setUseWorktree] = useState(true);
1520
+ const [initializeGit, setInitializeGit] = useState(true);
1521
+ const [githubRepos, setGithubRepos] = useState<GitHubRepo[]>([]);
1522
+ const [githubReposLoading, setGithubReposLoading] = useState(false);
1523
+ const [githubReposError, setGithubReposError] = useState<string | null>(null);
1524
+ const [githubRepoSearch, setGithubRepoSearch] = useState("");
1525
+ const [selectedGithubRepo, setSelectedGithubRepo] = useState("");
1526
+ const [folderPickerOpen, setFolderPickerOpen] = useState(false);
1527
+ const [folderPickerTarget, setFolderPickerTarget] = useState<"clone" | "local">("local");
1528
+ const [branchOptions, setBranchOptions] = useState<string[]>([]);
1529
+ const [branchesLoading, setBranchesLoading] = useState(false);
1530
+ const [branchesError, setBranchesError] = useState<string | null>(null);
1531
+
1532
+ useEffect(() => {
1533
+ if (!open) return;
1534
+ setMode("git");
1535
+ setProjectId("");
1536
+ setGitUrl("");
1537
+ setPath("");
1538
+ setDefaultBranch("main");
1539
+ setInitializeGit(true);
1540
+ setUseWorktree(true);
1541
+ setAgent(defaultAgent);
1542
+ setGithubRepos([]);
1543
+ setGithubReposError(null);
1544
+ setGithubRepoSearch("");
1545
+ setSelectedGithubRepo("");
1546
+ setBranchOptions([]);
1547
+ setBranchesError(null);
1548
+ setBranchesLoading(false);
1549
+ setFolderPickerOpen(false);
1550
+ setFolderPickerTarget("local");
1551
+ }, [defaultAgent, open]);
1552
+
1553
+ useEffect(() => {
1554
+ if (!open) return;
1555
+ if (mode !== "local") return;
1556
+ if (path.trim().length > 0) return;
1557
+ setFolderPickerTarget("local");
1558
+ setFolderPickerOpen(true);
1559
+ }, [mode, open, path]);
1560
+
1561
+ const filteredGitHubRepos = useMemo(() => {
1562
+ if (githubRepoSearch.trim().length === 0) return githubRepos;
1563
+ const query = githubRepoSearch.trim().toLowerCase();
1564
+ return githubRepos.filter((repo) => {
1565
+ return repo.fullName.toLowerCase().includes(query)
1566
+ || repo.name.toLowerCase().includes(query)
1567
+ || repo.defaultBranch.toLowerCase().includes(query);
1568
+ });
1569
+ }, [githubRepoSearch, githubRepos]);
1570
+
1571
+ const orderedAgentOptions = useMemo(() => {
1572
+ const opts = [...new Set(agentOptions)];
1573
+ if (opts.length === 0) {
1574
+ opts.push(defaultAgent || "qwen-code");
1575
+ }
1576
+
1577
+ const rankMap = new Map(EXECUTOR_ORDER.map((name, index) => [name, index]));
1578
+ return opts.sort((left, right) => {
1579
+ const leftRank = rankMap.get(normalizeAgentName(left)) ?? Number.MAX_SAFE_INTEGER;
1580
+ const rightRank = rankMap.get(normalizeAgentName(right)) ?? Number.MAX_SAFE_INTEGER;
1581
+ if (leftRank !== rightRank) return leftRank - rightRank;
1582
+ return getAgentLabel(left).localeCompare(getAgentLabel(right));
1583
+ });
1584
+ }, [agentOptions, defaultAgent]);
1585
+
1586
+ useEffect(() => {
1587
+ if (!orderedAgentOptions.includes(agent)) {
1588
+ setAgent(orderedAgentOptions[0] ?? "qwen-code");
1589
+ }
1590
+ }, [agent, orderedAgentOptions]);
1591
+
1592
+ const handleFetchGitHubRepos = async () => {
1593
+ setGithubReposLoading(true);
1594
+ setGithubReposError(null);
1595
+ try {
1596
+ const query = githubRepoSearch.trim();
1597
+ const queryParam = query.length > 0 ? `?q=${encodeURIComponent(query)}` : "";
1598
+ const res = await fetch(`/api/github/repos${queryParam}`);
1599
+ const data = (await res.json().catch(() => null)) as
1600
+ | { repos?: GitHubRepo[]; error?: string }
1601
+ | null;
1602
+ if (!res.ok) {
1603
+ throw new Error(data?.error ?? `Failed to load GitHub repositories (${res.status})`);
1604
+ }
1605
+ setGithubRepos(Array.isArray(data?.repos) ? data.repos : []);
1606
+ } catch (err) {
1607
+ setGithubRepos([]);
1608
+ setGithubReposError(
1609
+ err instanceof Error ? err.message : "Failed to load GitHub repositories",
1610
+ );
1611
+ } finally {
1612
+ setGithubReposLoading(false);
1613
+ }
1614
+ };
1615
+
1616
+ const handleDetectBranches = async (
1617
+ sourceOverride?: { gitUrl?: string; path?: string },
1618
+ ) => {
1619
+ const effectiveGitUrl = sourceOverride?.gitUrl ?? (mode === "git" ? gitUrl.trim() : "");
1620
+ const effectivePath = sourceOverride?.path ?? (mode === "local" ? path.trim() : "");
1621
+
1622
+ if (effectiveGitUrl.length === 0 && effectivePath.length === 0) {
1623
+ setBranchesError(
1624
+ mode === "git"
1625
+ ? "Enter a Git URL first."
1626
+ : "Select a local repository path first.",
1627
+ );
1628
+ return;
1629
+ }
1630
+
1631
+ setBranchesLoading(true);
1632
+ setBranchesError(null);
1633
+ try {
1634
+ const params = new URLSearchParams();
1635
+ if (effectiveGitUrl.length > 0) {
1636
+ params.set("gitUrl", effectiveGitUrl);
1637
+ }
1638
+ if (effectivePath.length > 0) {
1639
+ params.set("path", effectivePath);
1640
+ }
1641
+
1642
+ const res = await fetch(`/api/workspaces/branches?${params.toString()}`);
1643
+ const data = (await res.json().catch(() => null)) as
1644
+ | { branches?: string[]; defaultBranch?: string | null; error?: string }
1645
+ | null;
1646
+
1647
+ if (!res.ok) {
1648
+ throw new Error(data?.error ?? `Failed to load branches (${res.status})`);
1649
+ }
1650
+
1651
+ const branches = Array.isArray(data?.branches)
1652
+ ? data.branches.filter((branch) => typeof branch === "string" && branch.trim().length > 0)
1653
+ : [];
1654
+ setBranchOptions(branches);
1655
+
1656
+ const suggestedDefault = typeof data?.defaultBranch === "string" && data.defaultBranch.trim().length > 0
1657
+ ? data.defaultBranch.trim()
1658
+ : branches[0] ?? null;
1659
+
1660
+ if (suggestedDefault && (defaultBranch.trim().length === 0 || !branches.includes(defaultBranch))) {
1661
+ setDefaultBranch(suggestedDefault);
1662
+ }
1663
+ } catch (err) {
1664
+ setBranchOptions([]);
1665
+ setBranchesError(err instanceof Error ? err.message : "Failed to load branches");
1666
+ } finally {
1667
+ setBranchesLoading(false);
1668
+ }
1669
+ };
1670
+
1671
+ const handleSelectGitHubRepo = async (httpsUrl: string) => {
1672
+ setSelectedGithubRepo(httpsUrl);
1673
+ const selected = githubRepos.find((repo) => repo.httpsUrl === httpsUrl);
1674
+ if (!selected) return;
1675
+
1676
+ setGitUrl(selected.httpsUrl);
1677
+ setDefaultBranch(selected.defaultBranch || "main");
1678
+ if (projectId.trim().length === 0) {
1679
+ const suggestedProjectId = selected.name
1680
+ .toLowerCase()
1681
+ .replace(/[^a-z0-9]+/g, "-")
1682
+ .replace(/^-+|-+$/g, "")
1683
+ .slice(0, 64);
1684
+ setProjectId(suggestedProjectId || projectId);
1685
+ }
1686
+
1687
+ await handleDetectBranches({ gitUrl: selected.httpsUrl });
1688
+ };
1689
+
1690
+ const openFolderPicker = (target: "clone" | "local") => {
1691
+ setFolderPickerTarget(target);
1692
+ setFolderPickerOpen(true);
1693
+ };
1694
+
1695
+ if (!open) return null;
1696
+
1697
+ const canSubmit = mode === "git"
1698
+ ? gitUrl.trim().length > 0 && defaultBranch.trim().length > 0
1699
+ : path.trim().length > 0 && defaultBranch.trim().length > 0;
1700
+
1701
+ async function handleSubmit(event: FormEvent<HTMLFormElement>) {
1702
+ event.preventDefault();
1703
+ if (!canSubmit || creating) return;
1704
+
1705
+ const payload: NewWorkspacePayload =
1706
+ mode === "git"
1707
+ ? {
1708
+ mode,
1709
+ projectId: projectId.trim() || undefined,
1710
+ agent,
1711
+ defaultBranch: defaultBranch.trim(),
1712
+ useWorktree,
1713
+ gitUrl: gitUrl.trim(),
1714
+ path: path.trim() || undefined,
1715
+ }
1716
+ : {
1717
+ mode,
1718
+ projectId: projectId.trim() || undefined,
1719
+ agent,
1720
+ defaultBranch: defaultBranch.trim(),
1721
+ useWorktree,
1722
+ path: path.trim(),
1723
+ initializeGit,
1724
+ };
1725
+
1726
+ await onCreate(payload);
1727
+ }
1728
+
1729
+ return (
1730
+ <>
1731
+ <div
1732
+ className="fixed inset-0 z-[80] flex items-start justify-center overflow-y-auto bg-black/65 px-3 py-3 sm:items-center sm:py-0"
1733
+ onClick={() => {
1734
+ if (creating || folderPickerOpen) return;
1735
+ onClose();
1736
+ }}
1737
+ role="presentation"
1738
+ >
1739
+ <form
1740
+ onSubmit={handleSubmit}
1741
+ onClick={(event) => event.stopPropagation()}
1742
+ className="flex max-h-[calc(100dvh-1.5rem)] w-full max-w-[760px] flex-col overflow-hidden rounded-[6px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
1743
+ >
1744
+ <header className="flex items-center border-b border-[var(--vk-border)] px-4 py-3">
1745
+ <div>
1746
+ <h2 className="text-[18px] leading-[22px] text-[var(--vk-text-strong)]">Add Workspace</h2>
1747
+ <p className="pt-1 text-[12px] text-[var(--vk-text-muted)]">
1748
+ Select a repository with a folder picker, then choose the target branch.
1749
+ </p>
1750
+ </div>
1751
+ <button
1752
+ type="button"
1753
+ onClick={onClose}
1754
+ disabled={creating}
1755
+ aria-label="Close dialog"
1756
+ className="ml-auto inline-flex h-8 w-8 items-center justify-center rounded-[4px] text-[var(--vk-text-muted)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-50"
1757
+ >
1758
+ <X className="h-4 w-4" />
1759
+ </button>
1760
+ </header>
1761
+
1762
+ <div className="min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-4">
1763
+ <div className="inline-flex rounded-[4px] border border-[var(--vk-border)] p-1">
1764
+ <button
1765
+ type="button"
1766
+ onClick={() => setMode("git")}
1767
+ className={`rounded-[3px] px-3 py-1.5 text-[13px] ${
1768
+ mode === "git"
1769
+ ? "bg-[var(--vk-bg-active)] text-[var(--vk-text-strong)]"
1770
+ : "text-[var(--vk-text-muted)] hover:bg-[var(--vk-bg-hover)]"
1771
+ }`}
1772
+ >
1773
+ Git Repository
1774
+ </button>
1775
+ <button
1776
+ type="button"
1777
+ onClick={() => setMode("local")}
1778
+ className={`rounded-[3px] px-3 py-1.5 text-[13px] ${
1779
+ mode === "local"
1780
+ ? "bg-[var(--vk-bg-active)] text-[var(--vk-text-strong)]"
1781
+ : "text-[var(--vk-text-muted)] hover:bg-[var(--vk-bg-hover)]"
1782
+ }`}
1783
+ >
1784
+ Local Folder
1785
+ </button>
1786
+ </div>
1787
+
1788
+ <label className="block">
1789
+ <span className="mb-1.5 block text-[12px] text-[var(--vk-text-muted)]">Project ID (optional)</span>
1790
+ <input
1791
+ value={projectId}
1792
+ onChange={(event) => setProjectId(event.target.value)}
1793
+ placeholder="auto-derived from repo/folder"
1794
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
1795
+ />
1796
+ </label>
1797
+
1798
+ {mode === "git" ? (
1799
+ <>
1800
+ <div className="rounded-[4px] border border-[var(--vk-border)] p-3">
1801
+ <div className="flex items-center gap-2">
1802
+ <Github className="h-4 w-4 text-[var(--vk-text-muted)]" />
1803
+ <span className="text-[12px] font-medium text-[var(--vk-text-normal)]">GitHub Integration</span>
1804
+ </div>
1805
+ <div className="mt-2 flex flex-wrap items-center gap-2">
1806
+ <button
1807
+ type="button"
1808
+ onClick={handleFetchGitHubRepos}
1809
+ disabled={githubReposLoading}
1810
+ className="inline-flex h-8 items-center rounded-[4px] border border-[var(--vk-border)] px-2 text-[12px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-50"
1811
+ >
1812
+ {githubReposLoading ? (
1813
+ <>
1814
+ <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
1815
+ Loading repos...
1816
+ </>
1817
+ ) : "Load My GitHub Repositories"}
1818
+ </button>
1819
+ <div className="relative min-w-[220px] flex-1">
1820
+ <Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-[var(--vk-text-muted)]" />
1821
+ <input
1822
+ value={githubRepoSearch}
1823
+ onChange={(event) => setGithubRepoSearch(event.target.value)}
1824
+ placeholder="Filter repos..."
1825
+ className="h-8 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent pl-7 pr-2 text-[12px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
1826
+ />
1827
+ </div>
1828
+ </div>
1829
+ {filteredGitHubRepos.length > 0 && (
1830
+ <label className="mt-2 block">
1831
+ <span className="mb-1 block text-[11px] text-[var(--vk-text-muted)]">Choose repository</span>
1832
+ <select
1833
+ value={selectedGithubRepo}
1834
+ onChange={(event) => {
1835
+ void handleSelectGitHubRepo(event.target.value);
1836
+ }}
1837
+ className="h-8 w-full rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-2 text-[12px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
1838
+ >
1839
+ <option value="">Select a GitHub repo...</option>
1840
+ {filteredGitHubRepos.map((repo) => (
1841
+ <option key={repo.httpsUrl} value={repo.httpsUrl}>
1842
+ {repo.fullName} ({repo.defaultBranch})
1843
+ </option>
1844
+ ))}
1845
+ </select>
1846
+ </label>
1847
+ )}
1848
+ {githubReposError && (
1849
+ <p className="mt-2 text-[11px] text-[var(--vk-red)]">{githubReposError}</p>
1850
+ )}
1851
+ </div>
1852
+
1853
+ <label className="block">
1854
+ <span className="mb-1.5 block text-[12px] text-[var(--vk-text-muted)]">Git URL</span>
1855
+ <input
1856
+ value={gitUrl}
1857
+ onChange={(event) => setGitUrl(event.target.value)}
1858
+ placeholder="https://github.com/org/repo.git"
1859
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
1860
+ />
1861
+ </label>
1862
+
1863
+ <label className="block">
1864
+ <span className="mb-1.5 block text-[12px] text-[var(--vk-text-muted)]">
1865
+ Local Path (optional, clone target)
1866
+ </span>
1867
+ <div className="flex items-center gap-2">
1868
+ <input
1869
+ value={path}
1870
+ readOnly
1871
+ onClick={() => openFolderPicker("clone")}
1872
+ placeholder="Use Browse to choose a clone target folder"
1873
+ className="h-9 w-full cursor-pointer rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
1874
+ />
1875
+ <button
1876
+ type="button"
1877
+ onClick={() => openFolderPicker("clone")}
1878
+ className="inline-flex h-9 items-center rounded-[4px] border border-[var(--vk-border)] px-2 text-[12px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
1879
+ title="Browse folders"
1880
+ >
1881
+ <FolderOpen className="h-4 w-4" />
1882
+ </button>
1883
+ </div>
1884
+ </label>
1885
+ </>
1886
+ ) : (
1887
+ <>
1888
+ <label className="block">
1889
+ <span className="mb-1.5 block text-[12px] text-[var(--vk-text-muted)]">Local Path</span>
1890
+ <div className="flex items-center gap-2">
1891
+ <input
1892
+ value={path}
1893
+ readOnly
1894
+ onClick={() => openFolderPicker("local")}
1895
+ placeholder="Use Browse to select a repository folder"
1896
+ className="h-9 w-full cursor-pointer rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
1897
+ />
1898
+ <button
1899
+ type="button"
1900
+ onClick={() => openFolderPicker("local")}
1901
+ className="inline-flex h-9 items-center rounded-[4px] border border-[var(--vk-border)] px-2 text-[12px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
1902
+ title="Browse folders"
1903
+ >
1904
+ <FolderOpen className="h-4 w-4" />
1905
+ </button>
1906
+ </div>
1907
+ </label>
1908
+ <label className="flex items-center gap-2 text-[13px] text-[var(--vk-text-normal)]">
1909
+ <input
1910
+ type="checkbox"
1911
+ checked={initializeGit}
1912
+ onChange={(event) => setInitializeGit(event.target.checked)}
1913
+ className="h-4 w-4 rounded border border-[var(--vk-border)] bg-transparent accent-[var(--vk-orange)]"
1914
+ />
1915
+ <span>Initialize git if this folder is non-git</span>
1916
+ </label>
1917
+ </>
1918
+ )}
1919
+
1920
+ <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
1921
+ <label className="block">
1922
+ <span className="mb-1.5 block text-[12px] text-[var(--vk-text-muted)]">Default Branch</span>
1923
+ <div className="flex items-center gap-2">
1924
+ <input
1925
+ value={defaultBranch}
1926
+ onChange={(event) => setDefaultBranch(event.target.value)}
1927
+ placeholder="main"
1928
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
1929
+ />
1930
+ <button
1931
+ type="button"
1932
+ onClick={() => {
1933
+ void handleDetectBranches();
1934
+ }}
1935
+ disabled={branchesLoading}
1936
+ className="inline-flex h-9 items-center rounded-[4px] border border-[var(--vk-border)] px-2 text-[12px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-50"
1937
+ title="Detect branches"
1938
+ >
1939
+ {branchesLoading ? (
1940
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
1941
+ ) : (
1942
+ <RefreshCcw className="h-3.5 w-3.5" />
1943
+ )}
1944
+ </button>
1945
+ </div>
1946
+ {branchOptions.length > 0 && (
1947
+ <select
1948
+ value={defaultBranch}
1949
+ onChange={(event) => setDefaultBranch(event.target.value)}
1950
+ className="mt-2 h-8 w-full rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-2 text-[12px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
1951
+ >
1952
+ {branchOptions.map((branch) => (
1953
+ <option key={branch} value={branch}>
1954
+ {branch}
1955
+ </option>
1956
+ ))}
1957
+ </select>
1958
+ )}
1959
+ {branchesError && (
1960
+ <p className="mt-1 text-[11px] text-[var(--vk-red)]">{branchesError}</p>
1961
+ )}
1962
+ </label>
1963
+
1964
+ <label className="block">
1965
+ <span className="mb-1.5 block text-[12px] text-[var(--vk-text-muted)]">Agent</span>
1966
+ <select
1967
+ value={agent}
1968
+ onChange={(event) => setAgent(event.target.value)}
1969
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
1970
+ >
1971
+ {orderedAgentOptions.map((item) => (
1972
+ <option key={item} value={item} className="bg-[var(--vk-bg-panel)] text-[var(--vk-text-normal)]">
1973
+ {getAgentLabel(item)}
1974
+ </option>
1975
+ ))}
1976
+ </select>
1977
+ </label>
1978
+ </div>
1979
+
1980
+ <label className="flex items-start gap-2 rounded-[4px] border border-[var(--vk-border)] px-2 py-2 text-[13px] text-[var(--vk-text-normal)]">
1981
+ <input
1982
+ type="checkbox"
1983
+ checked={useWorktree}
1984
+ onChange={(event) => setUseWorktree(event.target.checked)}
1985
+ className="mt-0.5 h-4 w-4 rounded border border-[var(--vk-border)] bg-transparent accent-[var(--vk-orange)]"
1986
+ />
1987
+ <span>
1988
+ Use worktree isolation
1989
+ <span className="block text-[11px] text-[var(--vk-text-muted)]">
1990
+ If unchecked, sessions run directly on the selected branch in the local repo.
1991
+ </span>
1992
+ </span>
1993
+ </label>
1994
+
1995
+ {error && <p className="text-[12px] text-[var(--vk-red)]">{error}</p>}
1996
+ </div>
1997
+
1998
+ <footer className="flex flex-wrap items-center justify-end gap-2 border-t border-[var(--vk-border)] px-4 py-3">
1999
+ <button
2000
+ type="button"
2001
+ onClick={onClose}
2002
+ disabled={creating}
2003
+ className="inline-flex h-9 items-center rounded-[4px] border border-[var(--vk-border)] px-3 text-[13px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-50"
2004
+ >
2005
+ Cancel
2006
+ </button>
2007
+ <button
2008
+ type="submit"
2009
+ disabled={!canSubmit || creating}
2010
+ className="inline-flex h-9 items-center rounded-[4px] bg-[var(--vk-bg-active)] px-3 text-[13px] text-[var(--vk-text-strong)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-50"
2011
+ >
2012
+ {creating ? (
2013
+ <>
2014
+ <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
2015
+ Adding...
2016
+ </>
2017
+ ) : "Add Workspace"}
2018
+ </button>
2019
+ </footer>
2020
+ </form>
2021
+ </div>
2022
+
2023
+ <FolderPickerDialog
2024
+ open={folderPickerOpen}
2025
+ initialPath={path}
2026
+ title={folderPickerTarget === "local" ? "Select Local Repository" : "Select Clone Target Folder"}
2027
+ description={folderPickerTarget === "local"
2028
+ ? "Choose the local repository folder."
2029
+ : "Choose where the git repository should be cloned."}
2030
+ onClose={() => setFolderPickerOpen(false)}
2031
+ onSelect={(selectedPath) => {
2032
+ setFolderPickerOpen(false);
2033
+ if (!selectedPath) return;
2034
+ setPath(selectedPath);
2035
+ if (mode === "local" || folderPickerTarget === "local") {
2036
+ void handleDetectBranches({ path: selectedPath });
2037
+ }
2038
+ }}
2039
+ />
2040
+ </>
2041
+ );
2042
+ }
2043
+
2044
+ function FolderPickerDialog({
2045
+ open,
2046
+ initialPath,
2047
+ title,
2048
+ description,
2049
+ onClose,
2050
+ onSelect,
2051
+ }: {
2052
+ open: boolean;
2053
+ initialPath?: string;
2054
+ title: string;
2055
+ description: string;
2056
+ onClose: () => void;
2057
+ onSelect: (path: string | null) => void;
2058
+ }) {
2059
+ const [currentPath, setCurrentPath] = useState("");
2060
+ const [manualPath, setManualPath] = useState(initialPath ?? "");
2061
+ const [entries, setEntries] = useState<DirectoryEntry[]>([]);
2062
+ const [search, setSearch] = useState("");
2063
+ const [loading, setLoading] = useState(false);
2064
+ const [error, setError] = useState<string | null>(null);
2065
+
2066
+ useEffect(() => {
2067
+ if (!open) return;
2068
+ setSearch("");
2069
+ setManualPath(initialPath ?? "");
2070
+ const targetPath = initialPath && initialPath.trim().length > 0 ? initialPath.trim() : undefined;
2071
+ const query = targetPath ? `?path=${encodeURIComponent(targetPath)}` : "";
2072
+ setLoading(true);
2073
+ setError(null);
2074
+ fetch(`/api/filesystem/directory${query}`)
2075
+ .then(async (res) => {
2076
+ const data = (await res.json().catch(() => null)) as
2077
+ | { currentPath?: string; entries?: DirectoryEntry[]; error?: string }
2078
+ | null;
2079
+ if (!res.ok) {
2080
+ throw new Error(data?.error ?? `Failed to load directory (${res.status})`);
2081
+ }
2082
+ setCurrentPath(typeof data?.currentPath === "string" ? data.currentPath : "");
2083
+ setEntries(Array.isArray(data?.entries) ? data.entries : []);
2084
+ })
2085
+ .catch((err) => {
2086
+ setError(err instanceof Error ? err.message : "Failed to load directory");
2087
+ setEntries([]);
2088
+ })
2089
+ .finally(() => {
2090
+ setLoading(false);
2091
+ });
2092
+ }, [initialPath, open]);
2093
+
2094
+ const filteredEntries = useMemo(() => {
2095
+ if (search.trim().length === 0) return entries;
2096
+ const query = search.trim().toLowerCase();
2097
+ return entries.filter((entry) => entry.name.toLowerCase().includes(query));
2098
+ }, [entries, search]);
2099
+
2100
+ const loadDirectory = async (path?: string) => {
2101
+ setLoading(true);
2102
+ setError(null);
2103
+ try {
2104
+ const query = path && path.trim().length > 0
2105
+ ? `?path=${encodeURIComponent(path.trim())}`
2106
+ : "";
2107
+ const res = await fetch(`/api/filesystem/directory${query}`);
2108
+ const data = (await res.json().catch(() => null)) as
2109
+ | { currentPath?: string; entries?: DirectoryEntry[]; error?: string }
2110
+ | null;
2111
+ if (!res.ok) {
2112
+ throw new Error(data?.error ?? `Failed to load directory (${res.status})`);
2113
+ }
2114
+ const nextPath = typeof data?.currentPath === "string" ? data.currentPath : "";
2115
+ setCurrentPath(nextPath);
2116
+ setEntries(Array.isArray(data?.entries) ? data.entries : []);
2117
+ setManualPath(nextPath);
2118
+ } catch (err) {
2119
+ setError(err instanceof Error ? err.message : "Failed to load directory");
2120
+ setEntries([]);
2121
+ } finally {
2122
+ setLoading(false);
2123
+ }
2124
+ };
2125
+
2126
+ const handleGoParent = () => {
2127
+ if (!currentPath) return;
2128
+ const normalized = currentPath.replace(/\\/g, "/");
2129
+ if (normalized === "/") return;
2130
+ const parts = normalized.split("/").filter(Boolean);
2131
+ const parent = parts.length > 1 ? `/${parts.slice(0, -1).join("/")}` : "/";
2132
+ void loadDirectory(parent);
2133
+ };
2134
+
2135
+ if (!open) return null;
2136
+
2137
+ return (
2138
+ <div
2139
+ className="fixed inset-0 z-[95] flex items-start justify-center overflow-y-auto bg-black/70 px-3 py-3 sm:items-center sm:py-0"
2140
+ onClick={() => {
2141
+ onClose();
2142
+ onSelect(null);
2143
+ }}
2144
+ role="presentation"
2145
+ >
2146
+ <div
2147
+ className="flex max-h-[calc(100dvh-1.5rem)] w-full max-w-[760px] flex-col overflow-hidden rounded-[6px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
2148
+ onClick={(event) => event.stopPropagation()}
2149
+ >
2150
+ <header className="border-b border-[var(--vk-border)] px-4 py-3">
2151
+ <h3 className="text-[16px] text-[var(--vk-text-strong)]">{title}</h3>
2152
+ <p className="pt-1 text-[12px] text-[var(--vk-text-muted)]">{description}</p>
2153
+ </header>
2154
+
2155
+ <div className="flex min-h-0 flex-1 flex-col gap-3 px-4 py-3">
2156
+ <div className="flex flex-wrap items-center gap-2">
2157
+ <input
2158
+ value={manualPath}
2159
+ onChange={(event) => setManualPath(event.target.value)}
2160
+ placeholder="/path/to/repository"
2161
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
2162
+ />
2163
+ <button
2164
+ type="button"
2165
+ onClick={() => {
2166
+ void loadDirectory(manualPath);
2167
+ }}
2168
+ className="inline-flex h-9 items-center rounded-[4px] border border-[var(--vk-border)] px-2 text-[12px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
2169
+ >
2170
+ Open
2171
+ </button>
2172
+ </div>
2173
+
2174
+ <div className="flex flex-wrap items-center gap-2">
2175
+ <button
2176
+ type="button"
2177
+ onClick={() => {
2178
+ void loadDirectory();
2179
+ }}
2180
+ className="inline-flex h-8 items-center rounded-[4px] border border-[var(--vk-border)] px-2 text-[12px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
2181
+ >
2182
+ Home
2183
+ </button>
2184
+ <button
2185
+ type="button"
2186
+ onClick={handleGoParent}
2187
+ className="inline-flex h-8 items-center rounded-[4px] border border-[var(--vk-border)] px-2 text-[12px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
2188
+ >
2189
+ Up
2190
+ </button>
2191
+ <div className="truncate text-[12px] text-[var(--vk-text-muted)]">{currentPath || "Home"}</div>
2192
+ </div>
2193
+
2194
+ <div className="relative">
2195
+ <Search className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-[var(--vk-text-muted)]" />
2196
+ <input
2197
+ value={search}
2198
+ onChange={(event) => setSearch(event.target.value)}
2199
+ placeholder="Filter folders"
2200
+ className="h-8 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent pl-7 pr-2 text-[12px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
2201
+ />
2202
+ </div>
2203
+
2204
+ <div className="min-h-0 flex-1 overflow-auto rounded-[4px] border border-[var(--vk-border)]">
2205
+ {loading ? (
2206
+ <div className="px-3 py-3 text-[12px] text-[var(--vk-text-muted)]">Loading...</div>
2207
+ ) : error ? (
2208
+ <div className="px-3 py-3 text-[12px] text-[var(--vk-red)]">{error}</div>
2209
+ ) : filteredEntries.length === 0 ? (
2210
+ <div className="px-3 py-3 text-[12px] text-[var(--vk-text-muted)]">No folders found.</div>
2211
+ ) : (
2212
+ <div className="p-1">
2213
+ {filteredEntries.map((entry) => (
2214
+ <button
2215
+ key={entry.path}
2216
+ type="button"
2217
+ onClick={() => {
2218
+ if (!entry.isDirectory) return;
2219
+ void loadDirectory(entry.path);
2220
+ }}
2221
+ className={`mb-1 flex w-full items-center gap-2 rounded-[4px] px-2 py-2 text-left text-[12px] ${
2222
+ entry.isDirectory
2223
+ ? "text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
2224
+ : "cursor-default text-[var(--vk-text-muted)]"
2225
+ }`}
2226
+ >
2227
+ <FolderOpen className="h-4 w-4 shrink-0" />
2228
+ <span className="truncate">{entry.name}</span>
2229
+ {entry.isGitRepo && (
2230
+ <span className="ml-auto rounded-[999px] border border-[var(--vk-border)] px-1.5 py-0.5 text-[10px]">
2231
+ git
2232
+ </span>
2233
+ )}
2234
+ </button>
2235
+ ))}
2236
+ </div>
2237
+ )}
2238
+ </div>
2239
+ </div>
2240
+
2241
+ <footer className="flex items-center justify-end gap-2 border-t border-[var(--vk-border)] px-4 py-3">
2242
+ <button
2243
+ type="button"
2244
+ onClick={() => {
2245
+ onClose();
2246
+ onSelect(null);
2247
+ }}
2248
+ className="inline-flex h-9 items-center rounded-[4px] border border-[var(--vk-border)] px-3 text-[13px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
2249
+ >
2250
+ Cancel
2251
+ </button>
2252
+ <button
2253
+ type="button"
2254
+ onClick={() => {
2255
+ const selectedPath = manualPath.trim().length > 0 ? manualPath.trim() : currentPath;
2256
+ onClose();
2257
+ onSelect(selectedPath || null);
2258
+ }}
2259
+ className="inline-flex h-9 items-center rounded-[4px] bg-[var(--vk-bg-active)] px-3 text-[13px] text-[var(--vk-text-strong)] hover:bg-[var(--vk-bg-hover)]"
2260
+ >
2261
+ Use this folder
2262
+ </button>
2263
+ </footer>
2264
+ </div>
2265
+ </div>
2266
+ );
2267
+ }
2268
+
2269
+ const CreateWorkspacePanel = memo(function CreateWorkspacePanel({
2270
+ prompt,
2271
+ setPrompt,
2272
+ selectedAgent,
2273
+ setSelectedAgent,
2274
+ agentStates,
2275
+ modelSelection,
2276
+ setModelSelection,
2277
+ modelAccess,
2278
+ runtimeModelCatalogs,
2279
+ agentOptions,
2280
+ projects,
2281
+ selectedProjectId,
2282
+ onSelectProject,
2283
+ projectLabel,
2284
+ hasProject,
2285
+ creating,
2286
+ error,
2287
+ onOpenAddWorkspace,
2288
+ onOpenAgentSetup,
2289
+ onCreate,
2290
+ }: {
2291
+ prompt: string;
2292
+ setPrompt: (value: string) => void;
2293
+ selectedAgent: string;
2294
+ setSelectedAgent: (value: string) => void;
2295
+ agentStates: Record<string, AgentSetupState>;
2296
+ modelSelection: ModelSelectionState;
2297
+ setModelSelection: (next: ModelSelectionState) => void;
2298
+ modelAccess: ModelAccessPreferences;
2299
+ runtimeModelCatalogs: Record<string, RuntimeAgentModelCatalog>;
2300
+ agentOptions: string[];
2301
+ projects: ConfigProject[];
2302
+ selectedProjectId: string | null;
2303
+ onSelectProject: (projectId: string | null) => void;
2304
+ projectLabel: string;
2305
+ hasProject: boolean;
2306
+ creating: boolean;
2307
+ error: string | null;
2308
+ onOpenAddWorkspace: () => void;
2309
+ onOpenAgentSetup: (agent: string) => void;
2310
+ onCreate: (options?: CreateSessionOptions) => void;
2311
+ }) {
2312
+ const orderedAgentOptions = useMemo(() => {
2313
+ const rankMap = new Map(EXECUTOR_ORDER.map((name, index) => [name, index]));
2314
+ return [...agentOptions].sort((left, right) => {
2315
+ const leftRank = rankMap.get(normalizeAgentName(left)) ?? Number.MAX_SAFE_INTEGER;
2316
+ const rightRank = rankMap.get(normalizeAgentName(right)) ?? Number.MAX_SAFE_INTEGER;
2317
+ if (leftRank !== rightRank) return leftRank - rightRank;
2318
+ return getAgentLabel(left).localeCompare(getAgentLabel(right));
2319
+ });
2320
+ }, [agentOptions]);
2321
+
2322
+ const selectedAgentLabel = getAgentLabel(selectedAgent);
2323
+ const selectedAgentState = agentStates[normalizeAgentName(selectedAgent)] ?? null;
2324
+ const projectOptions = useMemo(
2325
+ () => [...projects].sort((left, right) => left.id.localeCompare(right.id)),
2326
+ [projects],
2327
+ );
2328
+ const effectiveProjectId = selectedProjectId ?? projectOptions[0]?.id ?? null;
2329
+ const selectedProject = useMemo(
2330
+ () => projectOptions.find((project) => project.id === effectiveProjectId) ?? null,
2331
+ [effectiveProjectId, projectOptions],
2332
+ );
2333
+ const [branchOptions, setBranchOptions] = useState<string[]>([]);
2334
+ const [branchLoading, setBranchLoading] = useState(false);
2335
+ const [selectedBranch, setSelectedBranch] = useState("");
2336
+ const [issueId, setIssueId] = useState("");
2337
+ const [availableTasks, setAvailableTasks] = useState<LinkedBoardTask[]>([]);
2338
+ const [taskLoading, setTaskLoading] = useState(false);
2339
+ const [useWorktree, setUseWorktree] = useState(true);
2340
+ const [permissionMode, setPermissionMode] = useState<CreatePermissionMode>("default");
2341
+
2342
+ useEffect(() => {
2343
+ if (!selectedProject) {
2344
+ setBranchOptions([]);
2345
+ setSelectedBranch("");
2346
+ return;
2347
+ }
2348
+
2349
+ let cancelled = false;
2350
+ const project = selectedProject;
2351
+ const fallbackBranch = project.defaultBranch.trim() || "main";
2352
+
2353
+ async function loadBranches() {
2354
+ if (!project.path?.trim()) {
2355
+ setBranchOptions([fallbackBranch]);
2356
+ setSelectedBranch(fallbackBranch);
2357
+ return;
2358
+ }
2359
+
2360
+ setBranchLoading(true);
2361
+ try {
2362
+ const params = new URLSearchParams({ path: project.path });
2363
+ const res = await fetch(`/api/workspaces/branches?${params.toString()}`);
2364
+ const data = (await res.json().catch(() => null)) as
2365
+ | { branches?: string[]; defaultBranch?: string | null }
2366
+ | null;
2367
+
2368
+ const branches = Array.isArray(data?.branches)
2369
+ ? data.branches.filter((branch) => typeof branch === "string" && branch.trim().length > 0)
2370
+ : [];
2371
+ const resolvedDefault = typeof data?.defaultBranch === "string" && data.defaultBranch.trim().length > 0
2372
+ ? data.defaultBranch.trim()
2373
+ : fallbackBranch;
2374
+ const nextBranches = branches.length > 0 ? branches : [resolvedDefault];
2375
+
2376
+ if (cancelled) return;
2377
+ setBranchOptions(nextBranches);
2378
+ setSelectedBranch((current) => current.trim().length > 0 && nextBranches.includes(current) ? current : resolvedDefault);
2379
+ } catch {
2380
+ if (cancelled) return;
2381
+ setBranchOptions([fallbackBranch]);
2382
+ setSelectedBranch(fallbackBranch);
2383
+ } finally {
2384
+ if (!cancelled) {
2385
+ setBranchLoading(false);
2386
+ }
2387
+ }
2388
+ }
2389
+
2390
+ void loadBranches();
2391
+ return () => {
2392
+ cancelled = true;
2393
+ };
2394
+ }, [selectedProject]);
2395
+
2396
+ useEffect(() => {
2397
+ if (!effectiveProjectId) {
2398
+ setAvailableTasks([]);
2399
+ setIssueId("");
2400
+ return;
2401
+ }
2402
+
2403
+ let cancelled = false;
2404
+
2405
+ async function loadTasks() {
2406
+ setTaskLoading(true);
2407
+ try {
2408
+ const res = await fetch(`/api/boards?projectId=${encodeURIComponent(effectiveProjectId)}`);
2409
+ const payload = (await res.json().catch(() => null)) as LinkedBoardResponse | { error?: string } | null;
2410
+ if (!res.ok) {
2411
+ throw new Error((payload as { error?: string } | null)?.error ?? `Failed to load tasks: ${res.status}`);
2412
+ }
2413
+
2414
+ const boardPayload = payload as LinkedBoardResponse | null;
2415
+ const columns = Array.isArray(boardPayload?.columns) ? boardPayload.columns : [];
2416
+ const nextTasks = columns.flatMap((column: { tasks?: LinkedBoardTask[] }) =>
2417
+ Array.isArray(column.tasks) ? column.tasks : [],
2418
+ );
2419
+ const seen = new Set<string>();
2420
+ const deduped = nextTasks.filter((task: LinkedBoardTask) => {
2421
+ const key = getLinkedTaskValue(task);
2422
+ if (!key || seen.has(key)) return false;
2423
+ seen.add(key);
2424
+ return true;
2425
+ });
2426
+
2427
+ if (cancelled) return;
2428
+ setAvailableTasks(deduped);
2429
+ setIssueId((current) => deduped.some((task: LinkedBoardTask) => getLinkedTaskValue(task) === current) ? current : "");
2430
+ } catch {
2431
+ if (cancelled) return;
2432
+ setAvailableTasks([]);
2433
+ setIssueId("");
2434
+ } finally {
2435
+ if (!cancelled) {
2436
+ setTaskLoading(false);
2437
+ }
2438
+ }
2439
+ }
2440
+
2441
+ void loadTasks();
2442
+ return () => {
2443
+ cancelled = true;
2444
+ };
2445
+ }, [effectiveProjectId]);
2446
+
2447
+ const availableModels = useMemo(
2448
+ () => getSelectableAgentModels(selectedAgent, modelAccess, runtimeModelCatalogs),
2449
+ [modelAccess, runtimeModelCatalogs, selectedAgent],
2450
+ );
2451
+ const selectedTask = useMemo(
2452
+ () => availableTasks.find((task) => getLinkedTaskValue(task) === issueId) ?? null,
2453
+ [availableTasks, issueId],
2454
+ );
2455
+ const selectedModelValue = resolveModelSelectionValue(modelSelection) ?? "";
2456
+ const modelMenuOptions = useMemo(() => {
2457
+ const seen = new Set<string>();
2458
+ const merged: AgentModelOption[] = [];
2459
+ const currentModel = selectedModelValue.trim();
2460
+
2461
+ for (const option of availableModels) {
2462
+ if (seen.has(option.id)) continue;
2463
+ seen.add(option.id);
2464
+ merged.push(option);
2465
+ }
2466
+
2467
+ if (currentModel && !seen.has(currentModel)) {
2468
+ seen.add(currentModel);
2469
+ merged.unshift({
2470
+ id: currentModel,
2471
+ label: formatCurrentModelLabel(selectedAgent, currentModel),
2472
+ description: "Current selected model.",
2473
+ access: [],
2474
+ });
2475
+ }
2476
+
2477
+ return merged;
2478
+ }, [availableModels, selectedAgent, selectedModelValue]);
2479
+ const selectedModelLabel = useMemo(() => {
2480
+ if (selectedAgentState && !selectedAgentState.ready && !selectedModelValue) return "Setup required";
2481
+ if (!selectedModelValue) return "Default";
2482
+ return modelMenuOptions.find((option) => option.id === selectedModelValue)?.label ?? selectedModelValue;
2483
+ }, [modelMenuOptions, selectedAgentState, selectedModelValue]);
2484
+ const lightMenuClass = "z-50 min-w-[240px] rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] p-2 shadow-[0_18px_50px_rgba(0,0,0,0.35)]";
2485
+ const scrollMenuClass = `${lightMenuClass} max-h-[min(360px,50vh)] overflow-y-auto`;
2486
+ const lightMenuItemClass = "flex min-h-[36px] cursor-default items-center gap-2 rounded-[3px] px-3 py-2 text-[14px] leading-[21px] text-[var(--vk-text-normal)] outline-none hover:bg-[var(--vk-bg-hover)] focus:bg-[var(--vk-bg-hover)]";
2487
+ const permissionOptions: Array<{ id: CreatePermissionMode; label: string; icon: LucideIcon }> = [
2488
+ { id: "default", label: "Default", icon: SlidersHorizontal },
2489
+ { id: "auto", label: "Auto", icon: ChevronsRight },
2490
+ { id: "ask", label: "Ask", icon: Hand },
2491
+ { id: "plan", label: "Plan", icon: List },
2492
+ ];
2493
+ const selectedPermission = permissionOptions.find((option) => option.id === permissionMode) ?? permissionOptions[0];
2494
+ const getProjectDisplayName = (project: ConfigProject): string => {
2495
+ const repo = project.repo?.trim();
2496
+ if (repo) {
2497
+ const parts = repo.split("/").filter(Boolean);
2498
+ const label = parts[parts.length - 1]?.replace(/\.git$/i, "");
2499
+ if (label) return label;
2500
+ }
2501
+ return project.id;
2502
+ };
2503
+ const selectedProjectLabel = selectedProject ? getProjectDisplayName(selectedProject) : null;
2504
+ const currentProjectLabel = selectedProject
2505
+ ? `${selectedProjectLabel} · ${selectedBranch || selectedProject.defaultBranch || "main"}`
2506
+ : hasProject
2507
+ ? projectLabel
2508
+ : "Select project";
2509
+ const selectedTaskLabel = selectedTask?.taskRef?.trim() || "Link task";
2510
+ const selectedTaskSubtitle = selectedTask ? getLinkedTaskTitle(selectedTask.text) : "Choose a task, bug, or issue from this project's board";
2511
+
2512
+ return (
2513
+ <section className="flex h-full min-h-0 items-start justify-center overflow-auto bg-[var(--vk-bg-main)] px-3 py-4 sm:items-center sm:px-6 sm:py-6">
2514
+ <div className="w-full max-w-[768px]">
2515
+ <h1 className="pb-4 text-center text-[30px] font-medium leading-[34px] tracking-[-0.7px] text-[var(--vk-text-strong)] sm:text-[36px] sm:leading-[40px] sm:tracking-[-0.9px]">
2516
+ What would you like to work on?
2517
+ </h1>
2518
+
2519
+ <div className="mx-auto w-full rounded-[3px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] p-px">
2520
+ <div className="flex flex-wrap items-center gap-2 border-b border-[var(--vk-border)] px-2 pb-[9px] pt-2">
2521
+ <AgentTileIcon seed={{ label: selectedAgent }} className="h-[25px] w-[25px] border-none bg-transparent" />
2522
+ <DropdownMenu.Root>
2523
+ <DropdownMenu.Trigger asChild>
2524
+ <button
2525
+ type="button"
2526
+ className="inline-flex h-[31px] max-w-[70vw] items-center rounded-[3px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-[9px] py-[5px] text-[14px] leading-[21px] text-[var(--vk-text-normal)] outline-none hover:bg-[var(--vk-bg-hover)] data-[state=open]:bg-[var(--vk-bg-hover)] sm:max-w-none"
2527
+ aria-label="Select agent"
2528
+ >
2529
+ <span className="truncate pr-1">{selectedAgentLabel}</span>
2530
+ <ChevronDown className="h-3 w-3 text-[var(--vk-text-muted)]" />
2531
+ </button>
2532
+ </DropdownMenu.Trigger>
2533
+
2534
+ <DropdownMenu.Portal>
2535
+ <DropdownMenu.Content
2536
+ align="start"
2537
+ sideOffset={6}
2538
+ className={lightMenuClass}
2539
+ >
2540
+ <p className="px-3 pb-1 text-[14px] font-semibold leading-[21px] text-[var(--vk-text-muted)]">
2541
+ Agents
2542
+ </p>
2543
+
2544
+ {orderedAgentOptions.map((agent) => {
2545
+ const isSelected = agent === selectedAgent;
2546
+ const agentState = agentStates[normalizeAgentName(agent)] ?? null;
2547
+ return (
2548
+ <DropdownMenu.Item
2549
+ key={agent}
2550
+ onSelect={() => setSelectedAgent(agent)}
2551
+ className={lightMenuItemClass}
2552
+ >
2553
+ <AgentTileIcon seed={{ label: agent }} className="h-6 w-6 border-none bg-transparent" />
2554
+ <div className="min-w-0 flex-1">
2555
+ <div>{getAgentLabel(agent)}</div>
2556
+ {!agentState?.ready ? (
2557
+ <div className="truncate text-[12px] leading-[16px] text-[var(--vk-text-muted)]">
2558
+ {agentState?.installed ? "Setup required" : "Not installed"}
2559
+ </div>
2560
+ ) : null}
2561
+ </div>
2562
+ <span className="ml-auto inline-flex h-4 w-4 items-center justify-center text-[var(--vk-text-strong)]">
2563
+ {isSelected ? <Check className="h-4 w-4" /> : null}
2564
+ </span>
2565
+ </DropdownMenu.Item>
2566
+ );
2567
+ })}
2568
+ </DropdownMenu.Content>
2569
+ </DropdownMenu.Portal>
2570
+ </DropdownMenu.Root>
2571
+
2572
+ <DropdownMenu.Root>
2573
+ <DropdownMenu.Trigger asChild>
2574
+ <button
2575
+ type="button"
2576
+ disabled={!effectiveProjectId}
2577
+ className="ml-auto flex h-[31px] min-w-[220px] items-center rounded-[3px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-[9px] py-[5px] text-left disabled:cursor-not-allowed disabled:opacity-50 sm:ml-0 sm:w-[286px]"
2578
+ aria-label="Link task"
2579
+ >
2580
+ <span className="pr-2 text-[12px] uppercase tracking-[0.08em] text-[var(--vk-text-muted)]">Task</span>
2581
+ <span className="min-w-0 flex-1 truncate text-[14px] leading-[21px] text-[var(--vk-text-normal)]">
2582
+ {selectedTaskLabel}
2583
+ </span>
2584
+ <ChevronDown className="h-3 w-3 text-[var(--vk-text-muted)]" />
2585
+ </button>
2586
+ </DropdownMenu.Trigger>
2587
+ <DropdownMenu.Portal>
2588
+ <DropdownMenu.Content
2589
+ align="end"
2590
+ side="bottom"
2591
+ sideOffset={6}
2592
+ className={scrollMenuClass}
2593
+ >
2594
+ <p className="px-3 pb-1 text-[14px] font-semibold leading-[21px] text-[var(--vk-text-muted)]">
2595
+ Link task
2596
+ </p>
2597
+ <p className="px-3 pb-2 text-[12px] leading-[16px] text-[var(--text-faint)]">
2598
+ {selectedTaskSubtitle}
2599
+ </p>
2600
+ <DropdownMenu.Item
2601
+ onSelect={() => setIssueId("")}
2602
+ className={lightMenuItemClass}
2603
+ >
2604
+ <span>No linked task</span>
2605
+ <span className="ml-auto inline-flex h-4 w-4 items-center justify-center text-[var(--vk-text-strong)]">
2606
+ {!issueId ? <Check className="h-4 w-4" /> : null}
2607
+ </span>
2608
+ </DropdownMenu.Item>
2609
+ {taskLoading ? (
2610
+ <div className="px-3 py-2 text-[12px] leading-[18px] text-[var(--vk-text-muted)]">
2611
+ Loading board tasks...
2612
+ </div>
2613
+ ) : availableTasks.length > 0 ? (
2614
+ availableTasks.map((task) => {
2615
+ const taskValue = getLinkedTaskValue(task);
2616
+ const title = getLinkedTaskTitle(task.text);
2617
+ const secondary = [task.type, task.priority].filter(Boolean).join(" · ");
2618
+ return (
2619
+ <DropdownMenu.Item
2620
+ key={taskValue}
2621
+ onSelect={() => setIssueId(taskValue)}
2622
+ className={`${lightMenuItemClass} min-w-[320px] items-start`}
2623
+ >
2624
+ <div className="min-w-0 flex-1">
2625
+ <div className="truncate">
2626
+ {task.taskRef?.trim() || title}
2627
+ </div>
2628
+ <div className="truncate text-[12px] leading-[16px] text-[var(--text-faint)]">
2629
+ {task.taskRef?.trim() ? title : taskValue}
2630
+ {secondary ? ` · ${secondary}` : ""}
2631
+ </div>
2632
+ </div>
2633
+ <span className="ml-auto inline-flex h-4 w-4 items-center justify-center text-[var(--vk-text-strong)]">
2634
+ {issueId === taskValue ? <Check className="h-4 w-4" /> : null}
2635
+ </span>
2636
+ </DropdownMenu.Item>
2637
+ );
2638
+ })
2639
+ ) : (
2640
+ <div className="px-3 py-2 text-[12px] leading-[18px] text-[var(--vk-text-muted)]">
2641
+ No existing tasks were found for this project.
2642
+ </div>
2643
+ )}
2644
+ </DropdownMenu.Content>
2645
+ </DropdownMenu.Portal>
2646
+ </DropdownMenu.Root>
2647
+ </div>
2648
+
2649
+ <div className="rounded-[3.5px]">
2650
+ <div className="flex flex-col gap-3 p-2">
2651
+ <div className="relative w-full">
2652
+ <textarea
2653
+ value={prompt}
2654
+ onChange={(e) => setPrompt(e.target.value)}
2655
+ placeholder="Describe the task..."
2656
+ rows={1}
2657
+ className="min-h-[24px] w-full resize-none bg-transparent pr-8 text-[16px] leading-[24px] text-[var(--vk-text-normal)] outline-none placeholder:text-[var(--vk-text-muted)]"
2658
+ />
2659
+ <button
2660
+ type="button"
2661
+ aria-label="Preview"
2662
+ className="absolute right-0 top-0 inline-flex h-[24px] w-[24px] items-center justify-center rounded-[4px] text-[var(--vk-text-muted)] hover:bg-[var(--vk-bg-hover)]"
2663
+ >
2664
+ <Eye className="h-[14px] w-[14px]" />
2665
+ </button>
2666
+ </div>
2667
+
2668
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
2669
+ <div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-1 gap-y-2">
2670
+ <DropdownMenu.Root>
2671
+ <DropdownMenu.Trigger asChild>
2672
+ <button
2673
+ type="button"
2674
+ className="inline-flex h-[29px] w-[29px] items-center justify-center rounded-[3px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
2675
+ aria-label="Select workspace or project"
2676
+ >
2677
+ <SlidersHorizontal className="h-[15px] w-[15px]" />
2678
+ </button>
2679
+ </DropdownMenu.Trigger>
2680
+ <DropdownMenu.Portal>
2681
+ <DropdownMenu.Content align="start" sideOffset={6} className={lightMenuClass}>
2682
+ <p className="px-3 pb-1 text-[14px] font-semibold leading-[21px] text-[var(--vk-text-muted)]">Projects</p>
2683
+ {projectOptions.map((project) => {
2684
+ const displayName = getProjectDisplayName(project);
2685
+ const secondaryLabel = project.id !== displayName
2686
+ ? project.id
2687
+ : project.path?.trim() || project.repo?.trim() || null;
2688
+ return (
2689
+ <DropdownMenu.Item
2690
+ key={project.id}
2691
+ onSelect={() => onSelectProject(project.id)}
2692
+ className={`${lightMenuItemClass} min-w-[280px] items-start`}
2693
+ >
2694
+ <div className="min-w-0 flex-1">
2695
+ <div className="truncate">{displayName}</div>
2696
+ {secondaryLabel ? (
2697
+ <div className="truncate text-[12px] leading-[16px] text-[var(--text-faint)]">
2698
+ {secondaryLabel}
2699
+ </div>
2700
+ ) : null}
2701
+ </div>
2702
+ <span className="ml-auto inline-flex h-4 w-4 items-center justify-center text-[var(--vk-text-strong)]">
2703
+ {project.id === effectiveProjectId ? <Check className="h-4 w-4" /> : null}
2704
+ </span>
2705
+ </DropdownMenu.Item>
2706
+ );
2707
+ })}
2708
+ <DropdownMenu.Separator className="my-1 h-px bg-[var(--vk-border)]" />
2709
+ <DropdownMenu.Item onSelect={onOpenAddWorkspace} className={lightMenuItemClass}>
2710
+ <FolderOpen className="h-4 w-4" />
2711
+ <span>Add Workspace</span>
2712
+ </DropdownMenu.Item>
2713
+ </DropdownMenu.Content>
2714
+ </DropdownMenu.Portal>
2715
+ </DropdownMenu.Root>
2716
+
2717
+ <DropdownMenu.Root>
2718
+ <DropdownMenu.Trigger asChild>
2719
+ <button
2720
+ type="button"
2721
+ className="inline-flex h-[29px] items-center gap-[4px] rounded-[3px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-[9px] py-[5px] text-[14px] leading-[21px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)] disabled:cursor-not-allowed disabled:opacity-60"
2722
+ >
2723
+ <span>{selectedModelLabel}</span>
2724
+ <ChevronDown className="h-[10px] w-[10px] text-[var(--vk-text-muted)]" />
2725
+ </button>
2726
+ </DropdownMenu.Trigger>
2727
+ <DropdownMenu.Portal>
2728
+ <DropdownMenu.Content align="start" sideOffset={6} className={lightMenuClass}>
2729
+ <p className="px-3 pb-1 text-[14px] font-semibold leading-[21px] text-[var(--vk-text-muted)]">Model</p>
2730
+ <DropdownMenu.Item
2731
+ onSelect={() => setModelSelection(buildModelSelection(
2732
+ selectedAgent,
2733
+ modelAccess,
2734
+ runtimeModelCatalogs,
2735
+ selectedProject?.agentModel,
2736
+ selectedProject?.agentReasoningEffort,
2737
+ ))}
2738
+ className={lightMenuItemClass}
2739
+ >
2740
+ <span>Default</span>
2741
+ <span className="ml-auto inline-flex h-4 w-4 items-center justify-center text-[var(--vk-text-strong)]">
2742
+ {!selectedModelValue ? <Check className="h-4 w-4" /> : null}
2743
+ </span>
2744
+ </DropdownMenu.Item>
2745
+ {modelMenuOptions.map((option) => (
2746
+ <DropdownMenu.Item
2747
+ key={option.id}
2748
+ onSelect={() => setModelSelection({
2749
+ catalogModel: option.id,
2750
+ customModel: "",
2751
+ reasoningEffort: getSelectableDefaultReasoningEffort(
2752
+ selectedAgent,
2753
+ modelAccess,
2754
+ runtimeModelCatalogs,
2755
+ option.id,
2756
+ ),
2757
+ })}
2758
+ className={lightMenuItemClass}
2759
+ >
2760
+ <span>{option.label}</span>
2761
+ <span className="ml-auto inline-flex h-4 w-4 items-center justify-center text-[var(--vk-text-strong)]">
2762
+ {selectedModelValue === option.id ? <Check className="h-4 w-4" /> : null}
2763
+ </span>
2764
+ </DropdownMenu.Item>
2765
+ ))}
2766
+ {modelMenuOptions.length === 0 ? (
2767
+ <div className="px-3 py-2 text-[12px] leading-[18px] text-[var(--vk-text-muted)]">
2768
+ Models will appear here after the selected agent is installed and its runtime catalog is detected.
2769
+ </div>
2770
+ ) : null}
2771
+ {selectedAgentState && !selectedAgentState.ready ? (
2772
+ <>
2773
+ <DropdownMenu.Separator className="my-1 h-px bg-[var(--vk-border)]" />
2774
+ <button
2775
+ type="button"
2776
+ onClick={() => onOpenAgentSetup(selectedAgent)}
2777
+ className="flex w-full items-center rounded-[3px] px-3 py-2 text-left text-[13px] text-[var(--vk-orange)] transition hover:bg-[var(--vk-bg-hover)]"
2778
+ >
2779
+ {selectedAgentState.installed ? "Open setup" : "Open install guide"}
2780
+ </button>
2781
+ </>
2782
+ ) : null}
2783
+ </DropdownMenu.Content>
2784
+ </DropdownMenu.Portal>
2785
+ </DropdownMenu.Root>
2786
+
2787
+ <div className="inline-flex h-[29px] w-[29px] items-center justify-center rounded-[3px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] text-[var(--vk-text-normal)]">
2788
+ <ChevronsRight className="h-[15px] w-[15px]" />
2789
+ </div>
2790
+
2791
+ <DropdownMenu.Root>
2792
+ <DropdownMenu.Trigger asChild>
2793
+ <button
2794
+ type="button"
2795
+ className="inline-flex h-[29px] items-center gap-[4px] rounded-[3px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-[9px] py-[5px] text-[14px] leading-[21px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
2796
+ >
2797
+ <span>{selectedPermission.label}</span>
2798
+ <ChevronDown className="h-[10px] w-[10px] text-[var(--vk-text-muted)]" />
2799
+ </button>
2800
+ </DropdownMenu.Trigger>
2801
+ <DropdownMenu.Portal>
2802
+ <DropdownMenu.Content align="start" sideOffset={6} className={lightMenuClass}>
2803
+ <p className="px-3 pb-1 text-[14px] font-semibold leading-[21px] text-[var(--vk-text-muted)]">Permissions</p>
2804
+ {permissionOptions.map(({ id, label, icon: Icon }) => (
2805
+ <DropdownMenu.Item
2806
+ key={id}
2807
+ onSelect={() => setPermissionMode(id)}
2808
+ className={lightMenuItemClass}
2809
+ >
2810
+ <Icon className="h-4 w-4" />
2811
+ <span>{label}</span>
2812
+ <span className="ml-auto inline-flex h-4 w-4 items-center justify-center text-[var(--vk-text-strong)]">
2813
+ {permissionMode === id ? <Check className="h-4 w-4" /> : null}
2814
+ </span>
2815
+ </DropdownMenu.Item>
2816
+ ))}
2817
+ </DropdownMenu.Content>
2818
+ </DropdownMenu.Portal>
2819
+ </DropdownMenu.Root>
2820
+
2821
+ <button
2822
+ type="button"
2823
+ onClick={onOpenAddWorkspace}
2824
+ className="inline-flex h-[29px] w-[20px] items-center justify-center text-[var(--vk-text-muted)] hover:text-[var(--vk-text-normal)]"
2825
+ aria-label="Add workspace"
2826
+ >
2827
+ <Paperclip className="h-[18px] w-[18px]" />
2828
+ </button>
2829
+
2830
+ <DropdownMenu.Root>
2831
+ <DropdownMenu.Trigger asChild>
2832
+ <button
2833
+ type="button"
2834
+ disabled={!selectedProject}
2835
+ className="inline-flex min-h-[29px] max-w-[320px] items-center justify-center truncate text-[14px] leading-[21px] text-[var(--vk-text-normal)] hover:text-[var(--vk-text-strong)] disabled:cursor-not-allowed disabled:opacity-50"
2836
+ >
2837
+ {currentProjectLabel}
2838
+ </button>
2839
+ </DropdownMenu.Trigger>
2840
+ <DropdownMenu.Portal>
2841
+ <DropdownMenu.Content
2842
+ align="start"
2843
+ side="bottom"
2844
+ sideOffset={6}
2845
+ avoidCollisions={false}
2846
+ className={scrollMenuClass}
2847
+ >
2848
+ <p className="px-3 pb-1 text-[14px] font-semibold leading-[21px] text-[var(--vk-text-muted)]">Branch</p>
2849
+ {selectedProjectLabel ? (
2850
+ <p className="px-3 pb-2 text-[12px] leading-[16px] text-[var(--text-faint)]">
2851
+ {selectedProjectLabel}
2852
+ </p>
2853
+ ) : null}
2854
+ {branchLoading ? (
2855
+ <div className="px-3 py-2 text-[14px] leading-[21px] text-[var(--vk-text-muted)]">Loading branches...</div>
2856
+ ) : (
2857
+ branchOptions.map((branch) => (
2858
+ <DropdownMenu.Item
2859
+ key={branch}
2860
+ onSelect={() => setSelectedBranch(branch)}
2861
+ className={lightMenuItemClass}
2862
+ >
2863
+ <span>{branch}</span>
2864
+ <span className="ml-auto inline-flex h-4 w-4 items-center justify-center text-[var(--vk-text-strong)]">
2865
+ {selectedBranch === branch ? <Check className="h-4 w-4" /> : null}
2866
+ </span>
2867
+ </DropdownMenu.Item>
2868
+ ))
2869
+ )}
2870
+ </DropdownMenu.Content>
2871
+ </DropdownMenu.Portal>
2872
+ </DropdownMenu.Root>
2873
+ </div>
2874
+
2875
+ <div className="flex w-full justify-end sm:w-auto">
2876
+ <button
2877
+ type="button"
2878
+ onClick={() => onCreate({
2879
+ projectId: effectiveProjectId ?? undefined,
2880
+ ...(useWorktree
2881
+ ? { baseBranch: selectedBranch || selectedProject?.defaultBranch || undefined }
2882
+ : { branch: selectedBranch || selectedProject?.defaultBranch || undefined }),
2883
+ issueId: issueId.trim() || undefined,
2884
+ useWorktree,
2885
+ permissionMode,
2886
+ })}
2887
+ disabled={creating || prompt.trim().length === 0 || !effectiveProjectId}
2888
+ className="inline-flex min-h-[29px] items-center justify-center rounded-[3px] bg-[var(--vk-bg-hover)] px-[8px] py-[6.5px] text-[16px] leading-[16px] text-[var(--vk-text-strong)] transition-colors hover:bg-[var(--vk-bg-active)] disabled:cursor-not-allowed disabled:opacity-50"
2889
+ >
2890
+ {creating ? <Loader2 className="h-4 w-4 animate-spin" /> : "Create"}
2891
+ </button>
2892
+ </div>
2893
+ </div>
2894
+
2895
+ {selectedAgentState && !selectedAgentState.ready ? (
2896
+ <div className="rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-main)] px-3 py-2 text-[13px] text-[var(--vk-text-normal)]">
2897
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
2898
+ <div className="min-w-0">
2899
+ <p className="text-[13px] text-[var(--vk-text-strong)]">
2900
+ {selectedAgentLabel} is not ready on this machine.
2901
+ </p>
2902
+ <p className="pt-0.5 text-[12px] text-[var(--vk-text-muted)]">
2903
+ {selectedAgentState.installed
2904
+ ? "Finish login or local setup to load models and start streaming sessions."
2905
+ : "Install the CLI first, then its models and authentication state will appear here."}
2906
+ </p>
2907
+ </div>
2908
+ <button
2909
+ type="button"
2910
+ onClick={() => onOpenAgentSetup(selectedAgent)}
2911
+ className="inline-flex h-[29px] items-center justify-center rounded-[3px] border border-[var(--vk-border)] px-3 text-[12px] text-[var(--vk-orange)] hover:bg-[var(--vk-bg-hover)]"
2912
+ >
2913
+ {selectedAgentState.installed ? "Open setup" : "Open install"}
2914
+ </button>
2915
+ </div>
2916
+ </div>
2917
+ ) : null}
2918
+
2919
+ <label className="flex items-start gap-2 rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-main)] px-2 py-2 text-[13px] text-[var(--vk-text-normal)]">
2920
+ <input
2921
+ type="checkbox"
2922
+ checked={useWorktree}
2923
+ onChange={(event) => setUseWorktree(event.target.checked)}
2924
+ className="mt-0.5 h-4 w-4 rounded border border-[var(--vk-border)] bg-transparent accent-[var(--vk-orange)]"
2925
+ />
2926
+ <span>
2927
+ Use worktree isolation
2928
+ <span className="block text-[11px] text-[var(--vk-text-muted)]">
2929
+ If unchecked, the session runs directly on the selected branch in the local repo.
2930
+ </span>
2931
+ </span>
2932
+ </label>
2933
+ </div>
2934
+ </div>
2935
+ </div>
2936
+
2937
+ {error && <p className="pt-2 text-[12px] text-[var(--status-error)]">{error}</p>}
2938
+ </div>
2939
+ </section>
2940
+ );
2941
+ });
2942
+
2943
+ function CopySnippetButton({
2944
+ value,
2945
+ idleLabel = "Copy",
2946
+ copiedLabel = "Copied",
2947
+ }: {
2948
+ value: string;
2949
+ idleLabel?: string;
2950
+ copiedLabel?: string;
2951
+ }) {
2952
+ const [copied, setCopied] = useState(false);
2953
+
2954
+ async function handleCopy() {
2955
+ await navigator.clipboard.writeText(value);
2956
+ setCopied(true);
2957
+ window.setTimeout(() => setCopied(false), 1400);
2958
+ }
2959
+
2960
+ return (
2961
+ <button
2962
+ type="button"
2963
+ onClick={() => void handleCopy()}
2964
+ className="inline-flex h-8 items-center gap-1.5 rounded-[4px] border border-[var(--vk-border)] px-2 text-[12px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
2965
+ >
2966
+ {copied ? <Check className="h-3.5 w-3.5 text-[var(--vk-orange)]" /> : <Copy className="h-3.5 w-3.5" />}
2967
+ <span>{copied ? copiedLabel : idleLabel}</span>
2968
+ </button>
2969
+ );
2970
+ }
2971
+
2972
+ function SettingsDialog({
2973
+ open,
2974
+ mode,
2975
+ creating,
2976
+ error,
2977
+ current,
2978
+ projectCount,
2979
+ agentOptions,
2980
+ runtimeModelCatalogs,
2981
+ onRepositoriesChanged,
2982
+ onOnboardingComplete,
2983
+ onClose,
2984
+ onSave,
2985
+ }: {
2986
+ open: boolean;
2987
+ mode: PreferencesDialogMode;
2988
+ creating: boolean;
2989
+ error: string | null;
2990
+ current: PreferencesPayload;
2991
+ projectCount: number;
2992
+ agentOptions: string[];
2993
+ runtimeModelCatalogs: Record<string, RuntimeAgentModelCatalog>;
2994
+ onRepositoriesChanged?: () => Promise<void>;
2995
+ onOnboardingComplete?: (result: { needsProject: boolean }) => void;
2996
+ onClose: () => void;
2997
+ onSave: (next: PreferencesPayload, options?: { closeDialog?: boolean }) => Promise<boolean>;
2998
+ }) {
2999
+ const [activeTab, setActiveTab] = useState<SettingsTabId>("preferences");
3000
+ const [codingAgent, setCodingAgent] = useState(current.codingAgent);
3001
+ const [ide, setIde] = useState(current.ide);
3002
+ const [remoteSshHost, setRemoteSshHost] = useState(current.remoteSshHost);
3003
+ const [remoteSshUser, setRemoteSshUser] = useState(current.remoteSshUser);
3004
+ const [markdownEditor, setMarkdownEditor] = useState(current.markdownEditor);
3005
+ const [modelAccess, setModelAccess] = useState<ModelAccessPreferences>(current.modelAccess);
3006
+ const [soundEnabled, setSoundEnabled] = useState(current.notifications.soundEnabled);
3007
+ const [soundFile, setSoundFile] = useState<string | null>(current.notifications.soundFile);
3008
+ const [repositories, setRepositories] = useState<RepositorySettingsPayload[]>([]);
3009
+ const [repositoriesLoading, setRepositoriesLoading] = useState(false);
3010
+ const [repositoriesSaving, setRepositoriesSaving] = useState(false);
3011
+ const [repositoriesError, setRepositoriesError] = useState<string | null>(null);
3012
+ const [selectedRepositoryId, setSelectedRepositoryId] = useState("");
3013
+ const [repositoryDraft, setRepositoryDraft] = useState<RepositorySettingsPayload | null>(null);
3014
+ const [repositoryModelSelection, setRepositoryModelSelection] = useState<ModelSelectionState>(emptyModelSelection());
3015
+ const [repositoryBranchOptions, setRepositoryBranchOptions] = useState<string[]>([]);
3016
+ const [repositoryBranchesLoading, setRepositoryBranchesLoading] = useState(false);
3017
+ const [repositoryBranchesError, setRepositoryBranchesError] = useState<string | null>(null);
3018
+ const [repositoryFolderPickerOpen, setRepositoryFolderPickerOpen] = useState(false);
3019
+ const [accessSettings, setAccessSettings] = useState<AccessSettingsPayload>(() => normalizeAccessSettings(null));
3020
+ const [accessLoading, setAccessLoading] = useState(false);
3021
+ const [accessSaving, setAccessSaving] = useState(false);
3022
+ const [accessError, setAccessError] = useState<string | null>(null);
3023
+
3024
+ const isBusy = creating || repositoriesSaving || accessSaving;
3025
+
3026
+ function hydrateRepositoryDraft(value: RepositorySettingsPayload): RepositorySettingsPayload {
3027
+ return {
3028
+ ...value,
3029
+ pathHealth: {
3030
+ exists: value.pathHealth.exists,
3031
+ isGitRepository: value.pathHealth.isGitRepository,
3032
+ suggestedPath: value.pathHealth.suggestedPath,
3033
+ },
3034
+ };
3035
+ }
3036
+
3037
+ function parseMultilineRoleList(value: string): string[] {
3038
+ return value
3039
+ .split(/\n+/g)
3040
+ .map((item) => item.trim())
3041
+ .filter(Boolean);
3042
+ }
3043
+
3044
+ async function loadRepositories(preferredRepositoryId?: string): Promise<void> {
3045
+ setRepositoriesLoading(true);
3046
+ setRepositoriesError(null);
3047
+ try {
3048
+ const res = await fetch("/api/repositories");
3049
+ const data = (await res.json().catch(() => null)) as
3050
+ | { repositories?: RepositorySettingsPayload[]; error?: string }
3051
+ | null;
3052
+ if (!res.ok) {
3053
+ throw new Error(data?.error ?? `Failed to load repositories (${res.status})`);
3054
+ }
3055
+ const items = Array.isArray(data?.repositories) ? data.repositories : [];
3056
+ setRepositories(items);
3057
+
3058
+ const fallbackId = items[0]?.id ?? "";
3059
+ const selectedId = preferredRepositoryId && items.some((item) => item.id === preferredRepositoryId)
3060
+ ? preferredRepositoryId
3061
+ : selectedRepositoryId && items.some((item) => item.id === selectedRepositoryId)
3062
+ ? selectedRepositoryId
3063
+ : fallbackId;
3064
+
3065
+ setSelectedRepositoryId(selectedId);
3066
+ } catch (err) {
3067
+ setRepositories([]);
3068
+ setSelectedRepositoryId("");
3069
+ setRepositoryDraft(null);
3070
+ setRepositoryModelSelection(emptyModelSelection());
3071
+ setRepositoriesError(err instanceof Error ? err.message : "Failed to load repositories");
3072
+ } finally {
3073
+ setRepositoriesLoading(false);
3074
+ }
3075
+ }
3076
+
3077
+ async function detectRepositoryBranches(pathOverride?: string, preferredBranch?: string): Promise<void> {
3078
+ const repositoryPath = pathOverride ?? repositoryDraft?.path ?? "";
3079
+ const trimmedPath = repositoryPath.trim();
3080
+ if (trimmedPath.length === 0) {
3081
+ setRepositoryBranchesError("Select a repository path first.");
3082
+ setRepositoryBranchOptions([]);
3083
+ return;
3084
+ }
3085
+
3086
+ setRepositoryBranchesLoading(true);
3087
+ setRepositoryBranchesError(null);
3088
+ try {
3089
+ const params = new URLSearchParams({ path: trimmedPath });
3090
+ const res = await fetch(`/api/workspaces/branches?${params.toString()}`);
3091
+ const data = (await res.json().catch(() => null)) as
3092
+ | { branches?: string[]; defaultBranch?: string | null; error?: string }
3093
+ | null;
3094
+ if (!res.ok) {
3095
+ throw new Error(data?.error ?? `Failed to detect branches (${res.status})`);
3096
+ }
3097
+
3098
+ const branches = Array.isArray(data?.branches)
3099
+ ? data.branches.filter((branch) => typeof branch === "string" && branch.trim().length > 0)
3100
+ : [];
3101
+ setRepositoryBranchOptions(branches);
3102
+
3103
+ const suggestedDefault = preferredBranch?.trim()
3104
+ || (typeof data?.defaultBranch === "string" && data.defaultBranch.trim().length > 0
3105
+ ? data.defaultBranch.trim()
3106
+ : branches[0] ?? "");
3107
+
3108
+ if (!suggestedDefault) return;
3109
+ setRepositoryDraft((prev) => {
3110
+ if (!prev) return prev;
3111
+ if (prev.defaultBranch.trim().length > 0 && branches.includes(prev.defaultBranch)) {
3112
+ return prev;
3113
+ }
3114
+ return { ...prev, defaultBranch: suggestedDefault };
3115
+ });
3116
+ } catch (err) {
3117
+ setRepositoryBranchOptions([]);
3118
+ setRepositoryBranchesError(err instanceof Error ? err.message : "Failed to detect branches");
3119
+ } finally {
3120
+ setRepositoryBranchesLoading(false);
3121
+ }
3122
+ }
3123
+
3124
+ async function handleSaveRepository(): Promise<boolean> {
3125
+ if (!repositoryDraft || repositoriesSaving) return false;
3126
+ if (repositoryDraft.repo.trim().length === 0 || repositoryDraft.path.trim().length === 0) return false;
3127
+
3128
+ setRepositoriesSaving(true);
3129
+ setRepositoriesError(null);
3130
+ try {
3131
+ const res = await fetch("/api/repositories", {
3132
+ method: "PUT",
3133
+ headers: { "Content-Type": "application/json" },
3134
+ body: JSON.stringify({
3135
+ id: repositoryDraft.id,
3136
+ displayName: repositoryDraft.displayName,
3137
+ repo: repositoryDraft.repo,
3138
+ path: repositoryDraft.path,
3139
+ agent: repositoryDraft.agent,
3140
+ agentModel: resolveModelSelectionValue(repositoryModelSelection) ?? "",
3141
+ agentReasoningEffort: resolveReasoningSelectionValue(repositoryModelSelection) ?? "",
3142
+ defaultWorkingDirectory: repositoryDraft.defaultWorkingDirectory,
3143
+ defaultBranch: repositoryDraft.defaultBranch,
3144
+ devServerScript: repositoryDraft.devServerScript,
3145
+ setupScript: repositoryDraft.setupScript,
3146
+ runSetupInParallel: repositoryDraft.runSetupInParallel,
3147
+ cleanupScript: repositoryDraft.cleanupScript,
3148
+ archiveScript: repositoryDraft.archiveScript,
3149
+ copyFiles: repositoryDraft.copyFiles,
3150
+ }),
3151
+ });
3152
+
3153
+ const data = (await res.json().catch(() => null)) as
3154
+ | { repository?: RepositorySettingsPayload; error?: string }
3155
+ | null;
3156
+ if (!res.ok) {
3157
+ throw new Error(data?.error ?? `Failed to save repository settings (${res.status})`);
3158
+ }
3159
+
3160
+ const saved = data?.repository;
3161
+ if (!saved) {
3162
+ throw new Error("Repository saved but response is missing repository data");
3163
+ }
3164
+
3165
+ setRepositories((prev) => prev.map((item) => (item.id === saved.id ? saved : item)));
3166
+ setRepositoryDraft(hydrateRepositoryDraft(saved));
3167
+ setSelectedRepositoryId(saved.id);
3168
+ setRepositoryBranchesError(null);
3169
+
3170
+ await detectRepositoryBranches(saved.path, saved.defaultBranch);
3171
+
3172
+ if (onRepositoriesChanged) {
3173
+ await onRepositoriesChanged();
3174
+ }
3175
+ return true;
3176
+ } catch (err) {
3177
+ setRepositoriesError(err instanceof Error ? err.message : "Failed to save repository settings");
3178
+ return false;
3179
+ } finally {
3180
+ setRepositoriesSaving(false);
3181
+ }
3182
+ }
3183
+
3184
+ async function loadAccessSettings(): Promise<void> {
3185
+ setAccessLoading(true);
3186
+ setAccessError(null);
3187
+ try {
3188
+ const res = await fetch("/api/access");
3189
+ const data = (await res.json().catch(() => null)) as
3190
+ | { access?: unknown; current?: unknown; error?: string }
3191
+ | null;
3192
+ if (!res.ok) {
3193
+ throw new Error(data?.error ?? `Failed to load organization settings (${res.status})`);
3194
+ }
3195
+ setAccessSettings(normalizeAccessSettings(data?.access, data?.current));
3196
+ } catch (err) {
3197
+ setAccessSettings(normalizeAccessSettings(null));
3198
+ setAccessError(err instanceof Error ? err.message : "Failed to load organization settings");
3199
+ } finally {
3200
+ setAccessLoading(false);
3201
+ }
3202
+ }
3203
+
3204
+ async function handleSaveAccess(): Promise<boolean> {
3205
+ if (accessSaving) return false;
3206
+
3207
+ setAccessSaving(true);
3208
+ setAccessError(null);
3209
+ try {
3210
+ const res = await fetch("/api/access", {
3211
+ method: "PUT",
3212
+ headers: { "Content-Type": "application/json" },
3213
+ body: JSON.stringify({
3214
+ requireAuth: accessSettings.requireAuth,
3215
+ defaultRole: accessSettings.defaultRole,
3216
+ trustedHeaders: {
3217
+ enabled: accessSettings.trustedHeaders.enabled,
3218
+ provider: accessSettings.trustedHeaders.provider,
3219
+ emailHeader: accessSettings.trustedHeaders.emailHeader,
3220
+ jwtHeader: accessSettings.trustedHeaders.jwtHeader,
3221
+ teamDomain: accessSettings.trustedHeaders.teamDomain,
3222
+ audience: accessSettings.trustedHeaders.audience,
3223
+ },
3224
+ roles: {
3225
+ viewers: parseMultilineRoleList(accessSettings.roles.viewers),
3226
+ operators: parseMultilineRoleList(accessSettings.roles.operators),
3227
+ admins: parseMultilineRoleList(accessSettings.roles.admins),
3228
+ viewerDomains: parseMultilineRoleList(accessSettings.roles.viewerDomains),
3229
+ operatorDomains: parseMultilineRoleList(accessSettings.roles.operatorDomains),
3230
+ adminDomains: parseMultilineRoleList(accessSettings.roles.adminDomains),
3231
+ },
3232
+ }),
3233
+ });
3234
+ const data = (await res.json().catch(() => null)) as
3235
+ | { access?: unknown; current?: unknown; error?: string }
3236
+ | null;
3237
+ if (!res.ok) {
3238
+ throw new Error(data?.error ?? `Failed to save organization settings (${res.status})`);
3239
+ }
3240
+
3241
+ setAccessSettings(normalizeAccessSettings(data?.access, data?.current));
3242
+ return true;
3243
+ } catch (err) {
3244
+ setAccessError(err instanceof Error ? err.message : "Failed to save organization settings");
3245
+ return false;
3246
+ } finally {
3247
+ setAccessSaving(false);
3248
+ }
3249
+ }
3250
+
3251
+ useEffect(() => {
3252
+ if (!open) return;
3253
+ setActiveTab(mode === "onboarding" ? "preferences" : "general");
3254
+ setCodingAgent(current.codingAgent);
3255
+ setIde(current.ide);
3256
+ setRemoteSshHost(current.remoteSshHost);
3257
+ setRemoteSshUser(current.remoteSshUser);
3258
+ setMarkdownEditor(current.markdownEditor);
3259
+ setModelAccess(current.modelAccess);
3260
+ setSoundEnabled(current.notifications.soundEnabled);
3261
+ setSoundFile(current.notifications.soundFile);
3262
+ setRepositoryBranchOptions([]);
3263
+ setRepositoryBranchesError(null);
3264
+ setRepositoriesError(null);
3265
+ setRepositoryModelSelection(emptyModelSelection());
3266
+ setAccessError(null);
3267
+ }, [mode, open]);
3268
+
3269
+ useEffect(() => {
3270
+ if (!open) return;
3271
+ if (mode === "settings" || activeTab === "repositories") {
3272
+ void loadRepositories();
3273
+ }
3274
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3275
+ }, [activeTab, mode, open]);
3276
+
3277
+ useEffect(() => {
3278
+ if (!open || mode === "onboarding") return;
3279
+ void loadAccessSettings();
3280
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3281
+ }, [mode, open]);
3282
+
3283
+ useEffect(() => {
3284
+ if (!open) return;
3285
+ if (!selectedRepositoryId) {
3286
+ setRepositoryDraft(null);
3287
+ setRepositoryModelSelection(emptyModelSelection());
3288
+ return;
3289
+ }
3290
+ const selected = repositories.find((item) => item.id === selectedRepositoryId);
3291
+ if (!selected) return;
3292
+ setRepositoryDraft(hydrateRepositoryDraft(selected));
3293
+ setRepositoryModelSelection(
3294
+ buildModelSelection(
3295
+ selected.agent,
3296
+ modelAccess,
3297
+ runtimeModelCatalogs,
3298
+ selected.agentModel,
3299
+ selected.agentReasoningEffort,
3300
+ ),
3301
+ );
3302
+ setRepositoryBranchOptions([]);
3303
+ setRepositoryBranchesError(null);
3304
+ if (selected.path.trim().length > 0) {
3305
+ void detectRepositoryBranches(selected.path, selected.defaultBranch);
3306
+ }
3307
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3308
+ }, [modelAccess, open, repositories, runtimeModelCatalogs, selectedRepositoryId]);
3309
+
3310
+ const onboardingShouldShowRepositoryStep = mode === "onboarding" && projectCount > 0;
3311
+
3312
+ const visibleTabs = useMemo(() => {
3313
+ if (mode === "onboarding") {
3314
+ return onboardingShouldShowRepositoryStep
3315
+ ? ONBOARDING_TABS
3316
+ : ONBOARDING_TABS.filter((tab) => tab.id === "preferences");
3317
+ }
3318
+ return SETTINGS_TABS.filter((tab) => tab.implemented);
3319
+ }, [mode, onboardingShouldShowRepositoryStep]);
3320
+
3321
+ const activeTabItem = visibleTabs.find((tab) => tab.id === activeTab) ?? visibleTabs[0] ?? SETTINGS_TABS[0];
3322
+ const isOnboarding = mode === "onboarding";
3323
+ const isPreferencesTab = activeTabItem.id === "preferences";
3324
+ const isGeneralTab = activeTabItem.id === "general";
3325
+ const isRemoteAccessTab = activeTabItem.id === "remote_access";
3326
+ const isAgentsTab = activeTabItem.id === "agents";
3327
+ const isPreferenceFormTab = isPreferencesTab || isGeneralTab || isRemoteAccessTab || isAgentsTab;
3328
+ const isRepositoriesTab = activeTabItem.id === "repositories";
3329
+ const isOrganizationTab = activeTabItem.id === "organization";
3330
+ const onboardingStepIndex = visibleTabs.findIndex((tab) => tab.id === activeTabItem.id) + 1;
3331
+ const onboardingHasRepositoryStep = visibleTabs.some((tab) => tab.id === "repositories");
3332
+ const accessCanEdit = accessSettings.current.role === "admin";
3333
+
3334
+ const orderedAgentOptions = useMemo(() => {
3335
+ const opts = new Set(agentOptions);
3336
+ if (codingAgent.trim().length > 0) {
3337
+ opts.add(codingAgent);
3338
+ }
3339
+ if (opts.size === 0) {
3340
+ opts.add("qwen-code");
3341
+ }
3342
+ const rankMap = new Map(EXECUTOR_ORDER.map((name, index) => [name, index]));
3343
+ return [...opts].sort((left, right) => {
3344
+ const leftRank = rankMap.get(normalizeAgentName(left)) ?? Number.MAX_SAFE_INTEGER;
3345
+ const rightRank = rankMap.get(normalizeAgentName(right)) ?? Number.MAX_SAFE_INTEGER;
3346
+ if (leftRank !== rightRank) return leftRank - rightRank;
3347
+ return getAgentLabel(left).localeCompare(getAgentLabel(right));
3348
+ });
3349
+ }, [agentOptions, codingAgent]);
3350
+
3351
+ function handleModelAccessChange(agent: string, nextAccess: string) {
3352
+ const catalog = getAgentModelCatalog(agent);
3353
+ if (!catalog) return;
3354
+
3355
+ setModelAccess((prev) => ({
3356
+ ...prev,
3357
+ [catalog.accessKey]: nextAccess,
3358
+ } as ModelAccessPreferences));
3359
+ }
3360
+
3361
+ if (!open) return null;
3362
+
3363
+ const canSubmitPreferences = codingAgent.trim().length > 0
3364
+ && ide.trim().length > 0
3365
+ && markdownEditor.trim().length > 0;
3366
+ const canSaveRepository = !!repositoryDraft
3367
+ && repositoryDraft.displayName.trim().length > 0
3368
+ && repositoryDraft.repo.trim().length > 0
3369
+ && repositoryDraft.path.trim().length > 0
3370
+ && repositoryDraft.defaultBranch.trim().length > 0;
3371
+ const canSaveAccess = accessCanEdit && !accessLoading && (
3372
+ !accessSettings.trustedHeaders.enabled
3373
+ || accessSettings.trustedHeaders.provider === "generic"
3374
+ || (
3375
+ accessSettings.trustedHeaders.teamDomain.trim().length > 0
3376
+ && accessSettings.trustedHeaders.audience.trim().length > 0
3377
+ )
3378
+ );
3379
+ const dialogError = isRepositoriesTab
3380
+ ? repositoriesError
3381
+ : isOrganizationTab
3382
+ ? accessError
3383
+ : error;
3384
+ const accessRoleFields: Array<{
3385
+ label: string;
3386
+ key: keyof AccessSettingsPayload["roles"];
3387
+ placeholder: string;
3388
+ }> = [
3389
+ { label: "Viewer Emails", key: "viewers", placeholder: "alice@example.com" },
3390
+ { label: "Operator Emails", key: "operators", placeholder: "builder@example.com" },
3391
+ { label: "Admin Emails", key: "admins", placeholder: "owner@example.com" },
3392
+ { label: "Viewer Domains", key: "viewerDomains", placeholder: "guests.example.com" },
3393
+ { label: "Operator Domains", key: "operatorDomains", placeholder: "eng.example.com" },
3394
+ { label: "Admin Domains", key: "adminDomains", placeholder: "admins.example.com" },
3395
+ ];
3396
+ const repositoryBootstrapCommand = repositoryDraft
3397
+ ? buildRepositoryBootstrapCommand({
3398
+ ...repositoryDraft,
3399
+ agentModel: resolveModelSelectionValue(repositoryModelSelection) ?? "",
3400
+ agentReasoningEffort: resolveReasoningSelectionValue(repositoryModelSelection) ?? "",
3401
+ }, {
3402
+ ide,
3403
+ markdownEditor,
3404
+ })
3405
+ : "";
3406
+
3407
+ function buildNextPreferences(acknowledgeOnboarding: boolean): PreferencesPayload {
3408
+ const resolvedSoundFile = soundEnabled
3409
+ ? soundFile ?? NOTIFICATION_SOUND_OPTIONS[0]?.id ?? "abstract-sound-4"
3410
+ : null;
3411
+
3412
+ return {
3413
+ onboardingAcknowledged: acknowledgeOnboarding ? true : current.onboardingAcknowledged,
3414
+ codingAgent: codingAgent.trim(),
3415
+ ide: ide.trim(),
3416
+ remoteSshHost: remoteSshHost.trim(),
3417
+ remoteSshUser: remoteSshUser.trim(),
3418
+ markdownEditor: markdownEditor.trim(),
3419
+ modelAccess,
3420
+ notifications: {
3421
+ soundEnabled,
3422
+ soundFile: resolvedSoundFile,
3423
+ },
3424
+ };
3425
+ }
3426
+
3427
+ async function handleSubmitPreferences(
3428
+ acknowledgeOnboarding: boolean,
3429
+ options?: { closeDialog?: boolean },
3430
+ ): Promise<boolean> {
3431
+ if (!canSubmitPreferences || creating) return false;
3432
+ return onSave(buildNextPreferences(acknowledgeOnboarding), options);
3433
+ }
3434
+
3435
+ async function handleOnboardingContinue() {
3436
+ if (repositoriesLoading) return;
3437
+ if (!onboardingHasRepositoryStep) {
3438
+ const saved = await handleSubmitPreferences(true, { closeDialog: true });
3439
+ if (!saved) return;
3440
+ onOnboardingComplete?.({ needsProject: projectCount === 0 });
3441
+ return;
3442
+ }
3443
+
3444
+ const saved = await handleSubmitPreferences(false, { closeDialog: false });
3445
+ if (!saved) return;
3446
+ setActiveTab("repositories");
3447
+ }
3448
+
3449
+ async function handleFinishOnboarding() {
3450
+ if (isRepositoriesTab) {
3451
+ const saved = await handleSaveRepository();
3452
+ if (!saved) return;
3453
+ }
3454
+
3455
+ const saved = await handleSubmitPreferences(true, { closeDialog: true });
3456
+ if (!saved) return;
3457
+ onOnboardingComplete?.({ needsProject: false });
3458
+ }
3459
+
3460
+ return (
3461
+ <>
3462
+ <div
3463
+ className="fixed inset-0 z-[90] flex items-start justify-center overflow-y-auto bg-black/70 px-3 py-3 sm:items-center"
3464
+ onClick={() => {
3465
+ if (isBusy || mode === "onboarding" || repositoryFolderPickerOpen) return;
3466
+ onClose();
3467
+ }}
3468
+ role="presentation"
3469
+ >
3470
+ <div
3471
+ className="flex max-h-[calc(100dvh-1.5rem)] w-full max-w-[1120px] flex-col overflow-hidden rounded-[6px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:h-[min(92vh,760px)] sm:flex-row"
3472
+ onClick={(event) => event.stopPropagation()}
3473
+ >
3474
+ <aside className="flex w-full shrink-0 flex-col border-b border-[var(--vk-border)] bg-[rgba(28,28,28,0.8)] sm:w-[224px] sm:border-b-0 sm:border-r">
3475
+ <header className="border-b border-[var(--vk-border)] px-4 py-3 sm:py-4">
3476
+ <h2 className="text-[22px] leading-[24px] text-[var(--vk-text-strong)] sm:text-[27px] sm:leading-[27px]">
3477
+ {isOnboarding ? "Setup" : "Settings"}
3478
+ </h2>
3479
+ </header>
3480
+ <nav className="flex gap-1 overflow-x-auto p-2 sm:block sm:space-y-1 sm:overflow-auto">
3481
+ {visibleTabs.map((tab) => {
3482
+ const Icon = tab.icon;
3483
+ const selected = activeTabItem.id === tab.id;
3484
+ return (
3485
+ <button
3486
+ key={tab.id}
3487
+ type="button"
3488
+ onClick={() => setActiveTab(tab.id)}
3489
+ disabled={isBusy}
3490
+ className={`flex shrink-0 items-center gap-3 rounded-[3px] px-3 py-2 text-left text-[14px] leading-[21px] transition-colors sm:w-full ${
3491
+ selected
3492
+ ? "bg-[rgba(234,122,42,0.1)] text-[var(--vk-orange)]"
3493
+ : "text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
3494
+ } disabled:opacity-50`}
3495
+ >
3496
+ <Icon className="h-4 w-4 shrink-0" />
3497
+ <span>{tab.label}</span>
3498
+ </button>
3499
+ );
3500
+ })}
3501
+ </nav>
3502
+ </aside>
3503
+
3504
+ <div className="flex min-w-0 flex-1 flex-col">
3505
+ <header className="flex items-center justify-between border-b border-[var(--vk-border)] px-4 py-3 sm:py-4">
3506
+ <div>
3507
+ <h3 className="text-[20px] leading-[24px] text-[var(--vk-text-strong)] sm:text-[27px] sm:leading-[27px]">
3508
+ {isOnboarding
3509
+ ? isPreferencesTab
3510
+ ? "Choose your preferences"
3511
+ : "Review repository defaults"
3512
+ : activeTabItem.label}
3513
+ </h3>
3514
+ {isOnboarding && (
3515
+ <p className="mt-1 text-[12px] text-[var(--vk-text-muted)]">
3516
+ Step {onboardingStepIndex} of {visibleTabs.length}
3517
+ </p>
3518
+ )}
3519
+ </div>
3520
+ <button
3521
+ type="button"
3522
+ onClick={onClose}
3523
+ disabled={isBusy || mode === "onboarding"}
3524
+ aria-label="Close settings"
3525
+ className="inline-flex h-7 w-7 items-center justify-center rounded-[4px] text-[var(--vk-text-muted)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-40"
3526
+ >
3527
+ <X className="h-4 w-4" />
3528
+ </button>
3529
+ </header>
3530
+
3531
+ <div className="min-h-0 flex-1 overflow-auto px-4 py-3 sm:px-6 sm:py-4">
3532
+ {isPreferenceFormTab ? (
3533
+ <div className="space-y-5">
3534
+ {isOnboarding && (
3535
+ <section className="rounded-[6px] border border-[var(--vk-border)] bg-[rgba(234,122,42,0.08)] px-4 py-3">
3536
+ <p className="text-[13px] leading-5 text-[var(--vk-text-normal)]">
3537
+ Conductor is already running locally. Finish setup here in the dashboard, then you can start using
3538
+ chat and boards immediately.
3539
+ </p>
3540
+ </section>
3541
+ )}
3542
+
3543
+ {(isPreferencesTab || isAgentsTab) && (
3544
+ <>
3545
+ <section className="space-y-2">
3546
+ <h4 className="text-[15px] font-medium text-[var(--vk-text-strong)]">Choose Your Coding Agent</h4>
3547
+ <p className="text-[12px] text-[var(--vk-text-muted)]">Select the default coding agent configuration.</p>
3548
+ <div className="grid gap-2">
3549
+ {orderedAgentOptions.map((agent) => {
3550
+ const selected = codingAgent === agent;
3551
+ return (
3552
+ <button
3553
+ key={agent}
3554
+ type="button"
3555
+ onClick={() => setCodingAgent(agent)}
3556
+ className={`flex items-center gap-2 rounded-[4px] border px-3 py-2 text-left ${
3557
+ selected
3558
+ ? "border-[var(--vk-orange)] bg-[var(--vk-bg-hover)]"
3559
+ : "border-[var(--vk-border)] hover:bg-[var(--vk-bg-hover)]"
3560
+ }`}
3561
+ >
3562
+ <AgentTileIcon seed={{ label: agent }} className="h-5 w-5 border-none bg-transparent" />
3563
+ <span className="flex-1 text-[13px] text-[var(--vk-text-normal)]">{getAgentLabel(agent)}</span>
3564
+ {selected && <Check className="h-3.5 w-3.5 text-[var(--vk-orange)]" />}
3565
+ </button>
3566
+ );
3567
+ })}
3568
+ </div>
3569
+ </section>
3570
+
3571
+ <section className="space-y-3">
3572
+ <div className="space-y-1">
3573
+ <h4 className="text-[15px] font-medium text-[var(--vk-text-strong)]">Model Access</h4>
3574
+ <p className="text-[12px] text-[var(--vk-text-muted)]">
3575
+ Tell Conductor which account mode each agent is using so the model dropdown only shows options
3576
+ that make sense for that login path.
3577
+ </p>
3578
+ </div>
3579
+ <div className="grid gap-3">
3580
+ {orderedAgentOptions.filter((agent) => supportsAgentModelSelection(agent)).map((agent) => {
3581
+ const catalog = getAgentModelCatalog(agent);
3582
+ if (!catalog) return null;
3583
+ const selectedAccess = resolveAgentModelAccess(agent, modelAccess) ?? catalog.defaultAccess;
3584
+ return (
3585
+ <label key={agent} className="block rounded-[4px] border border-[var(--vk-border)] px-3 py-3">
3586
+ <span className="mb-1 block text-[13px] font-medium text-[var(--vk-text-normal)]">
3587
+ {catalog.label}
3588
+ </span>
3589
+ <select
3590
+ value={selectedAccess}
3591
+ onChange={(event) => handleModelAccessChange(agent, event.target.value)}
3592
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
3593
+ >
3594
+ {catalog.accessOptions.map((option) => (
3595
+ <option key={option.id} value={option.id}>
3596
+ {option.label}
3597
+ </option>
3598
+ ))}
3599
+ </select>
3600
+ <p className="mt-1.5 text-[11px] text-[var(--vk-text-muted)]">
3601
+ {catalog.accessOptions.find((option) => option.id === selectedAccess)?.description}
3602
+ </p>
3603
+ </label>
3604
+ );
3605
+ })}
3606
+ </div>
3607
+ </section>
3608
+ </>
3609
+ )}
3610
+
3611
+ {(isPreferencesTab || isGeneralTab) && (
3612
+ <>
3613
+
3614
+ <section className="space-y-2">
3615
+ <h4 className="text-[15px] font-medium text-[var(--vk-text-strong)]">Choose Your Code Editor</h4>
3616
+ <p className="text-[12px] text-[var(--vk-text-muted)]">This editor will be used when opening attempts and files.</p>
3617
+ <div className="grid gap-2 sm:grid-cols-2">
3618
+ {IDE_OPTIONS.map((option) => {
3619
+ const selected = ide === option.id;
3620
+ return (
3621
+ <button
3622
+ key={option.id}
3623
+ type="button"
3624
+ onClick={() => setIde(option.id)}
3625
+ className={`flex items-center gap-2 rounded-[4px] border px-3 py-2 text-left ${
3626
+ selected
3627
+ ? "border-[var(--vk-orange)] bg-[var(--vk-bg-hover)]"
3628
+ : "border-[var(--vk-border)] hover:bg-[var(--vk-bg-hover)]"
3629
+ }`}
3630
+ >
3631
+ <CodeEditorIcon editorId={option.id} label={option.label} />
3632
+ <span className="flex-1 text-[13px] text-[var(--vk-text-normal)]">{option.label}</span>
3633
+ {selected && <Check className="h-3.5 w-3.5 text-[var(--vk-orange)]" />}
3634
+ </button>
3635
+ );
3636
+ })}
3637
+ </div>
3638
+ </section>
3639
+
3640
+ {isPreferencesTab && (
3641
+ <section className="space-y-3">
3642
+ <div className="space-y-1">
3643
+ <h4 className="text-[15px] font-medium text-[var(--vk-text-strong)]">Remote Access</h4>
3644
+ <p className="text-[12px] text-[var(--vk-text-muted)]">
3645
+ Use your local Remote-SSH editor to jump straight into a remote worktree. This complements
3646
+ ngrok or Cloudflare Tunnel for dashboard access; it does not replace the tunnel.
3647
+ </p>
3648
+ </div>
3649
+
3650
+ <div className="grid gap-3 sm:grid-cols-2">
3651
+ <label className="block">
3652
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">SSH Host or Alias</span>
3653
+ <input
3654
+ value={remoteSshHost}
3655
+ onChange={(event) => setRemoteSshHost(event.target.value)}
3656
+ placeholder="e.g., conductor-dev or 203.0.113.10"
3657
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
3658
+ />
3659
+ </label>
3660
+
3661
+ <label className="block">
3662
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">SSH User (optional)</span>
3663
+ <input
3664
+ value={remoteSshUser}
3665
+ onChange={(event) => setRemoteSshUser(event.target.value)}
3666
+ placeholder="e.g., ubuntu"
3667
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
3668
+ />
3669
+ </label>
3670
+ </div>
3671
+
3672
+ <p className="text-[12px] text-[var(--vk-text-muted)]">
3673
+ One-click remote open currently supports VS Code and VS Code Insiders. Other editors will still
3674
+ save as your preference, but they will not get a remote launch button yet.
3675
+ </p>
3676
+ </section>
3677
+ )}
3678
+
3679
+ <section className="space-y-2">
3680
+ <h4 className="text-[15px] font-medium text-[var(--vk-text-strong)]">Markdown Editor</h4>
3681
+ <p className="text-[12px] text-[var(--vk-text-muted)]">
3682
+ Used as your second-brain markdown source when feeding context into tasks.
3683
+ </p>
3684
+ <div className="grid gap-2 sm:grid-cols-2">
3685
+ {MARKDOWN_EDITOR_OPTIONS.map((option) => {
3686
+ const selected = markdownEditor === option.id;
3687
+ return (
3688
+ <button
3689
+ key={option.id}
3690
+ type="button"
3691
+ onClick={() => setMarkdownEditor(option.id)}
3692
+ className={`flex items-center gap-2 rounded-[4px] border px-3 py-2 text-left ${
3693
+ selected
3694
+ ? "border-[var(--vk-orange)] bg-[var(--vk-bg-hover)]"
3695
+ : "border-[var(--vk-border)] hover:bg-[var(--vk-bg-hover)]"
3696
+ }`}
3697
+ >
3698
+ <MarkdownEditorIcon editorId={option.id} />
3699
+ <span className="flex-1 text-[13px] text-[var(--vk-text-normal)]">{option.label}</span>
3700
+ {selected && <Check className="h-3.5 w-3.5 text-[var(--vk-orange)]" />}
3701
+ </button>
3702
+ );
3703
+ })}
3704
+ </div>
3705
+ </section>
3706
+
3707
+ <section className="space-y-2">
3708
+ <h4 className="text-[15px] font-medium text-[var(--vk-text-strong)]">Notification Sound</h4>
3709
+ <p className="text-[12px] text-[var(--vk-text-muted)]">Pick a sound for notifications, or disable sound.</p>
3710
+ <div className="grid gap-2 sm:grid-cols-2">
3711
+ {NOTIFICATION_SOUND_OPTIONS.map((option) => {
3712
+ const selected = soundEnabled && soundFile === option.id;
3713
+ return (
3714
+ <button
3715
+ key={option.id}
3716
+ type="button"
3717
+ onClick={() => {
3718
+ setSoundEnabled(true);
3719
+ setSoundFile(option.id);
3720
+ }}
3721
+ className={`flex items-center gap-2 rounded-[4px] border px-3 py-2 text-left ${
3722
+ selected
3723
+ ? "border-[var(--vk-orange)] bg-[var(--vk-bg-hover)]"
3724
+ : "border-[var(--vk-border)] hover:bg-[var(--vk-bg-hover)]"
3725
+ }`}
3726
+ >
3727
+ <Volume2 className="h-4 w-4 text-[var(--vk-text-muted)]" />
3728
+ <span className="flex-1 text-[13px] text-[var(--vk-text-normal)]">{option.label}</span>
3729
+ {selected && <Check className="h-3.5 w-3.5 text-[var(--vk-orange)]" />}
3730
+ </button>
3731
+ );
3732
+ })}
3733
+ <button
3734
+ type="button"
3735
+ onClick={() => setSoundEnabled(false)}
3736
+ className={`flex items-center gap-2 rounded-[4px] border px-3 py-2 text-left ${
3737
+ !soundEnabled
3738
+ ? "border-[var(--vk-orange)] bg-[var(--vk-bg-hover)]"
3739
+ : "border-[var(--vk-border)] hover:bg-[var(--vk-bg-hover)]"
3740
+ }`}
3741
+ >
3742
+ <VolumeX className="h-4 w-4 text-[var(--vk-text-muted)]" />
3743
+ <span className="flex-1 text-[13px] text-[var(--vk-text-normal)]">No sound</span>
3744
+ {!soundEnabled && <Check className="h-3.5 w-3.5 text-[var(--vk-orange)]" />}
3745
+ </button>
3746
+ </div>
3747
+ </section>
3748
+ </>
3749
+ )}
3750
+
3751
+ {isRemoteAccessTab && (
3752
+ <section className="space-y-3">
3753
+ <div className="space-y-1">
3754
+ <h4 className="text-[15px] font-medium text-[var(--vk-text-strong)]">Remote Access</h4>
3755
+ <p className="text-[12px] text-[var(--vk-text-muted)]">
3756
+ Use your local Remote-SSH editor to jump straight into a remote worktree. This complements
3757
+ ngrok or Cloudflare Tunnel for dashboard access; it does not replace the tunnel.
3758
+ </p>
3759
+ </div>
3760
+
3761
+ <div className="grid gap-3 sm:grid-cols-2">
3762
+ <label className="block">
3763
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">SSH Host or Alias</span>
3764
+ <input
3765
+ value={remoteSshHost}
3766
+ onChange={(event) => setRemoteSshHost(event.target.value)}
3767
+ placeholder="e.g., conductor-dev or 203.0.113.10"
3768
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
3769
+ />
3770
+ </label>
3771
+
3772
+ <label className="block">
3773
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">SSH User (optional)</span>
3774
+ <input
3775
+ value={remoteSshUser}
3776
+ onChange={(event) => setRemoteSshUser(event.target.value)}
3777
+ placeholder="e.g., ubuntu"
3778
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
3779
+ />
3780
+ </label>
3781
+ </div>
3782
+
3783
+ <p className="text-[12px] text-[var(--vk-text-muted)]">
3784
+ One-click remote open currently supports VS Code and VS Code Insiders. Other editors will still
3785
+ save as your preference, but they will not get a remote launch button yet.
3786
+ </p>
3787
+ </section>
3788
+ )}
3789
+ </div>
3790
+ ) : isRepositoriesTab ? (
3791
+ <div className="space-y-5">
3792
+ <section className="space-y-1">
3793
+ <h4 className="text-[24px] leading-[24px] text-[var(--vk-text-strong)]">
3794
+ {isOnboarding ? "Repository Defaults" : "Repository Configuration"}
3795
+ </h4>
3796
+ <p className="text-[14px] text-[var(--vk-text-muted)]">
3797
+ {isOnboarding
3798
+ ? "Review the repository Conductor will use for this workspace. You can edit advanced scripts later from Settings."
3799
+ : "Configure scripts and defaults used whenever this repository is selected for workspaces."}
3800
+ </p>
3801
+ </section>
3802
+
3803
+ {(mode === "settings" || repositories.length > 1) && (
3804
+ <section className="space-y-2">
3805
+ <label className="block">
3806
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Select Repository</span>
3807
+ <select
3808
+ value={selectedRepositoryId}
3809
+ onChange={(event) => setSelectedRepositoryId(event.target.value)}
3810
+ disabled={repositoriesLoading || repositories.length === 0 || isBusy}
3811
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)] disabled:opacity-60"
3812
+ >
3813
+ {repositories.length === 0 && <option value="">No repositories configured</option>}
3814
+ {repositories.map((repository) => (
3815
+ <option key={repository.id} value={repository.id}>
3816
+ {repository.displayName}
3817
+ </option>
3818
+ ))}
3819
+ </select>
3820
+ </label>
3821
+ <p className="text-[12px] text-[var(--vk-text-muted)]">
3822
+ Select a repository to view and edit its configuration.
3823
+ </p>
3824
+ {repositoriesLoading && (
3825
+ <p className="text-[12px] text-[var(--vk-text-muted)]">Loading repositories...</p>
3826
+ )}
3827
+ </section>
3828
+ )}
3829
+
3830
+ {isOnboarding && repositories.length === 1 && repositoryDraft && (
3831
+ <label className="block">
3832
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Detected Repository</span>
3833
+ <div className="rounded-[4px] border border-[var(--vk-border)] bg-[rgba(15,15,15,0.52)] px-3 py-3 text-[13px] text-[var(--vk-text-normal)]">
3834
+ {repositoryDraft.displayName}
3835
+ <span className="ml-2 text-[var(--vk-text-muted)]">{repositoryDraft.path}</span>
3836
+ </div>
3837
+ </label>
3838
+ )}
3839
+
3840
+ {repositoryDraft && (
3841
+ <>
3842
+ {mode === "settings" && (
3843
+ <section className="space-y-3 border-t border-[var(--vk-border)] pt-4">
3844
+ <div className="space-y-1">
3845
+ <h5 className="text-[22px] leading-[22px] text-[var(--vk-text-strong)]">Repo-Preseed Bootstrap</h5>
3846
+ <p className="text-[13px] text-[var(--vk-text-muted)]">
3847
+ Use this when you already know the target repository and want one command to prefill it. The
3848
+ default first-run path is still `npx conductor-oss@latest`, which opens the dashboard and lets
3849
+ the user choose preferences before adding a project.
3850
+ </p>
3851
+ </div>
3852
+
3853
+ <div className="flex flex-wrap gap-2 text-[11px] text-[var(--vk-text-muted)]">
3854
+ <span className="rounded-[999px] border border-[var(--vk-border)] px-2 py-1">
3855
+ Workspace: {repositoryDraft.workspaceMode}
3856
+ </span>
3857
+ <span className="rounded-[999px] border border-[var(--vk-border)] px-2 py-1">
3858
+ Runtime: {repositoryDraft.runtimeMode}
3859
+ </span>
3860
+ <span className="rounded-[999px] border border-[var(--vk-border)] px-2 py-1">
3861
+ SCM: {repositoryDraft.scmMode}
3862
+ </span>
3863
+ </div>
3864
+
3865
+ <div className="rounded-[4px] border border-[var(--vk-border)] bg-[rgba(15,15,15,0.72)] p-3">
3866
+ <pre className="overflow-x-auto whitespace-pre-wrap break-all text-[12px] leading-5 text-[var(--vk-text-normal)]">
3867
+ {repositoryBootstrapCommand}
3868
+ </pre>
3869
+ </div>
3870
+
3871
+ <div className="flex flex-wrap items-center justify-between gap-2">
3872
+ <p className="text-[12px] text-[var(--vk-text-muted)]">
3873
+ This command uses your selected agent, editor, and notes app. Best on macOS with Homebrew.
3874
+ GitHub sign-in still opens a browser so the user can approve access.
3875
+ </p>
3876
+ <CopySnippetButton value={repositoryBootstrapCommand} idleLabel="Copy Setup Command" />
3877
+ </div>
3878
+ </section>
3879
+ )}
3880
+
3881
+ <section className="space-y-3 border-t border-[var(--vk-border)] pt-4">
3882
+ <h5 className="text-[22px] leading-[22px] text-[var(--vk-text-strong)]">General Settings</h5>
3883
+ <p className="text-[13px] text-[var(--vk-text-muted)]">Configure basic repository information.</p>
3884
+
3885
+ <label className="block">
3886
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Display Name</span>
3887
+ <input
3888
+ value={repositoryDraft.displayName}
3889
+ onChange={(event) => setRepositoryDraft((prev) => prev ? { ...prev, displayName: event.target.value } : prev)}
3890
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
3891
+ />
3892
+ <p className="mt-1 text-[12px] text-[var(--vk-text-muted)]">A friendly name for this repository.</p>
3893
+ </label>
3894
+
3895
+ <label className="block">
3896
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Repository Slug</span>
3897
+ <input
3898
+ value={repositoryDraft.repo}
3899
+ onChange={(event) => setRepositoryDraft((prev) => prev ? { ...prev, repo: event.target.value } : prev)}
3900
+ placeholder="e.g., your-org/your-repo"
3901
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
3902
+ />
3903
+ <p className="mt-1 text-[12px] text-[var(--vk-text-muted)]">Used for PR tracking, GitHub links, and onboarding defaults.</p>
3904
+ </label>
3905
+
3906
+ <label className="block">
3907
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Default Agent</span>
3908
+ <select
3909
+ value={repositoryDraft.agent}
3910
+ onChange={(event) => {
3911
+ const nextAgent = event.target.value;
3912
+ setRepositoryDraft((prev) => prev ? { ...prev, agent: nextAgent } : prev);
3913
+ setRepositoryModelSelection(buildModelSelection(nextAgent, modelAccess, runtimeModelCatalogs, null));
3914
+ }}
3915
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
3916
+ >
3917
+ {orderedAgentOptions.map((agent) => (
3918
+ <option key={agent} value={agent}>
3919
+ {getAgentLabel(agent)}
3920
+ </option>
3921
+ ))}
3922
+ </select>
3923
+ <p className="mt-1 text-[12px] text-[var(--vk-text-muted)]">Used by the one-line bootstrap and as the project default when tasks dispatch.</p>
3924
+ </label>
3925
+
3926
+ {supportsAgentModelSelection(repositoryDraft.agent) && (
3927
+ <div className="rounded-[4px] border border-[var(--vk-border)] px-3 py-3">
3928
+ <AgentModelSelector
3929
+ agent={repositoryDraft.agent}
3930
+ modelAccess={modelAccess}
3931
+ runtimeModelCatalogs={runtimeModelCatalogs}
3932
+ selection={repositoryModelSelection}
3933
+ onChange={setRepositoryModelSelection}
3934
+ />
3935
+ </div>
3936
+ )}
3937
+
3938
+ <label className="block">
3939
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Repository Path</span>
3940
+ <div className="flex items-center gap-2">
3941
+ <input
3942
+ value={repositoryDraft.path}
3943
+ readOnly
3944
+ onClick={() => setRepositoryFolderPickerOpen(true)}
3945
+ placeholder="Use Browse to select a repository folder"
3946
+ className="h-9 w-full cursor-pointer rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
3947
+ />
3948
+ <button
3949
+ type="button"
3950
+ onClick={() => setRepositoryFolderPickerOpen(true)}
3951
+ disabled={isBusy}
3952
+ className="inline-flex h-9 items-center rounded-[4px] border border-[var(--vk-border)] px-2 text-[12px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-60"
3953
+ >
3954
+ <FolderOpen className="h-4 w-4" />
3955
+ </button>
3956
+ </div>
3957
+ {!repositoryDraft.pathHealth.exists && (
3958
+ <p className="mt-1 text-[12px] text-[var(--vk-red)]">Configured path does not exist on disk.</p>
3959
+ )}
3960
+ {repositoryDraft.pathHealth.exists && !repositoryDraft.pathHealth.isGitRepository && (
3961
+ <p className="mt-1 text-[12px] text-[var(--vk-red)]">Configured path exists but is not a git repository.</p>
3962
+ )}
3963
+ {repositoryDraft.pathHealth.suggestedPath && (
3964
+ <button
3965
+ type="button"
3966
+ onClick={() => {
3967
+ const suggestedPath = repositoryDraft.pathHealth.suggestedPath ?? "";
3968
+ if (!suggestedPath) return;
3969
+ setRepositoryDraft((prev) => prev
3970
+ ? {
3971
+ ...prev,
3972
+ path: suggestedPath,
3973
+ pathHealth: {
3974
+ ...prev.pathHealth,
3975
+ exists: true,
3976
+ isGitRepository: true,
3977
+ suggestedPath: null,
3978
+ },
3979
+ }
3980
+ : prev);
3981
+ void detectRepositoryBranches(suggestedPath);
3982
+ }}
3983
+ className="mt-1 inline-flex h-7 items-center rounded-[4px] border border-[var(--vk-border)] px-2 text-[11px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
3984
+ >
3985
+ Use detected git repo path
3986
+ </button>
3987
+ )}
3988
+ </label>
3989
+
3990
+ <label className="block">
3991
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Default Working Directory</span>
3992
+ <input
3993
+ value={repositoryDraft.defaultWorkingDirectory}
3994
+ onChange={(event) => setRepositoryDraft((prev) => prev ? { ...prev, defaultWorkingDirectory: event.target.value } : prev)}
3995
+ placeholder="e.g., packages/frontend"
3996
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
3997
+ />
3998
+ <p className="mt-1 text-[12px] text-[var(--vk-text-muted)]">
3999
+ Subdirectory relative to the repository root where the coding agent starts.
4000
+ </p>
4001
+ </label>
4002
+
4003
+ <label className="block">
4004
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Default Target Branch</span>
4005
+ <div className="flex items-center gap-2">
4006
+ <input
4007
+ value={repositoryDraft.defaultBranch}
4008
+ onChange={(event) => setRepositoryDraft((prev) => prev ? { ...prev, defaultBranch: event.target.value } : prev)}
4009
+ placeholder="Select a branch"
4010
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[14px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
4011
+ />
4012
+ <button
4013
+ type="button"
4014
+ onClick={() => {
4015
+ void detectRepositoryBranches();
4016
+ }}
4017
+ disabled={repositoryBranchesLoading}
4018
+ className="inline-flex h-9 items-center rounded-[4px] border border-[var(--vk-border)] px-2 text-[12px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-60"
4019
+ title="Detect branches"
4020
+ >
4021
+ {repositoryBranchesLoading ? (
4022
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
4023
+ ) : (
4024
+ <RefreshCcw className="h-3.5 w-3.5" />
4025
+ )}
4026
+ </button>
4027
+ </div>
4028
+ {repositoryBranchOptions.length > 0 && (
4029
+ <select
4030
+ value={repositoryDraft.defaultBranch}
4031
+ onChange={(event) => setRepositoryDraft((prev) => prev ? { ...prev, defaultBranch: event.target.value } : prev)}
4032
+ className="mt-2 h-8 w-full rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-2 text-[12px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
4033
+ >
4034
+ {repositoryBranchOptions.map((branch) => (
4035
+ <option key={branch} value={branch}>
4036
+ {branch}
4037
+ </option>
4038
+ ))}
4039
+ </select>
4040
+ )}
4041
+ {repositoryBranchesError && (
4042
+ <p className="mt-1 text-[12px] text-[var(--vk-red)]">{repositoryBranchesError}</p>
4043
+ )}
4044
+ </label>
4045
+ </section>
4046
+
4047
+ {mode === "settings" && (
4048
+ <section className="space-y-3 border-t border-[var(--vk-border)] pt-4">
4049
+ <h5 className="text-[22px] leading-[22px] text-[var(--vk-text-strong)]">Scripts & Configuration</h5>
4050
+ <p className="text-[13px] text-[var(--vk-text-muted)]">
4051
+ Configure dev server, setup, cleanup, archive, and file-copy behavior for this repository.
4052
+ </p>
4053
+
4054
+ <label className="block">
4055
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Dev Server Script</span>
4056
+ <textarea
4057
+ rows={3}
4058
+ value={repositoryDraft.devServerScript}
4059
+ onChange={(event) => setRepositoryDraft((prev) => prev ? { ...prev, devServerScript: event.target.value } : prev)}
4060
+ placeholder="npm run dev"
4061
+ className="w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 py-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
4062
+ />
4063
+ <p className="mt-1 text-[12px] text-[var(--vk-text-muted)]">Starts a development server for this repository.</p>
4064
+ </label>
4065
+
4066
+ <label className="block">
4067
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Setup Script</span>
4068
+ <textarea
4069
+ rows={4}
4070
+ value={repositoryDraft.setupScript}
4071
+ onChange={(event) => setRepositoryDraft((prev) => prev ? { ...prev, setupScript: event.target.value } : prev)}
4072
+ placeholder="npm install"
4073
+ className="w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 py-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
4074
+ />
4075
+ <p className="mt-1 text-[12px] text-[var(--vk-text-muted)]">
4076
+ Runs in the worktree after creation and before/with coding-agent startup.
4077
+ </p>
4078
+ </label>
4079
+
4080
+ <label className="flex items-start gap-2 rounded-[4px] border border-[var(--vk-border)] px-3 py-2 text-[13px] text-[var(--vk-text-normal)]">
4081
+ <input
4082
+ type="checkbox"
4083
+ checked={repositoryDraft.runSetupInParallel}
4084
+ onChange={(event) => setRepositoryDraft((prev) => prev ? { ...prev, runSetupInParallel: event.target.checked } : prev)}
4085
+ className="mt-0.5 h-4 w-4 rounded border border-[var(--vk-border)] bg-transparent accent-[var(--vk-orange)]"
4086
+ />
4087
+ <span>Run setup script in parallel with coding agent</span>
4088
+ </label>
4089
+
4090
+ <label className="block">
4091
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Cleanup Script</span>
4092
+ <textarea
4093
+ rows={4}
4094
+ value={repositoryDraft.cleanupScript}
4095
+ onChange={(event) => setRepositoryDraft((prev) => prev ? { ...prev, cleanupScript: event.target.value } : prev)}
4096
+ placeholder="Runs when the workspace is archived and changes exist"
4097
+ className="w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 py-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
4098
+ />
4099
+ </label>
4100
+
4101
+ <label className="block">
4102
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Archive Script</span>
4103
+ <textarea
4104
+ rows={4}
4105
+ value={repositoryDraft.archiveScript}
4106
+ onChange={(event) => setRepositoryDraft((prev) => prev ? { ...prev, archiveScript: event.target.value } : prev)}
4107
+ placeholder="Runs when the workspace/session is archived"
4108
+ className="w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 py-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
4109
+ />
4110
+ </label>
4111
+
4112
+ <label className="block">
4113
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Copy Files</span>
4114
+ <input
4115
+ value={repositoryDraft.copyFiles}
4116
+ onChange={(event) => setRepositoryDraft((prev) => prev ? { ...prev, copyFiles: event.target.value } : prev)}
4117
+ placeholder=".env, config/*.json"
4118
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)]"
4119
+ />
4120
+ <p className="mt-1 text-[12px] text-[var(--vk-text-muted)]">
4121
+ Comma-separated relative file paths or glob patterns copied from the repo to each worktree.
4122
+ </p>
4123
+ </label>
4124
+ </section>
4125
+ )}
4126
+ </>
4127
+ )}
4128
+ </div>
4129
+ ) : isOrganizationTab ? (
4130
+ <div className="space-y-5">
4131
+ <section className="rounded-[6px] border border-[var(--vk-border)] bg-[rgba(234,122,42,0.06)] px-4 py-3">
4132
+ <h4 className="text-[15px] font-medium text-[var(--vk-text-strong)]">Security-First Remote Access</h4>
4133
+ <p className="mt-1 text-[12px] leading-5 text-[var(--vk-text-muted)]">
4134
+ The dashboard stays bound to localhost. For phone and team access, put a verified edge
4135
+ identity layer like Cloudflare Access in front of it, then map authenticated users into
4136
+ viewer, operator, or admin roles here.
4137
+ </p>
4138
+ </section>
4139
+
4140
+ {accessLoading ? (
4141
+ <section className="flex items-center gap-2 rounded-[6px] border border-[var(--vk-border)] px-4 py-4 text-[13px] text-[var(--vk-text-muted)]">
4142
+ <Loader2 className="h-4 w-4 animate-spin" />
4143
+ Loading organization access settings...
4144
+ </section>
4145
+ ) : (
4146
+ <>
4147
+ <section className="grid gap-3 lg:grid-cols-3">
4148
+ <div className="rounded-[6px] border border-[var(--vk-border)] px-4 py-3">
4149
+ <span className="text-[11px] uppercase tracking-[0.12em] text-[var(--vk-text-muted)]">
4150
+ Current Identity
4151
+ </span>
4152
+ <p className="mt-2 text-[14px] text-[var(--vk-text-normal)]">
4153
+ {accessSettings.current.email ?? "Anonymous local session"}
4154
+ </p>
4155
+ </div>
4156
+ <div className="rounded-[6px] border border-[var(--vk-border)] px-4 py-3">
4157
+ <span className="text-[11px] uppercase tracking-[0.12em] text-[var(--vk-text-muted)]">
4158
+ Effective Role
4159
+ </span>
4160
+ <p className="mt-2 text-[14px] text-[var(--vk-text-normal)]">
4161
+ {accessSettings.current.role ?? "No access"}
4162
+ </p>
4163
+ </div>
4164
+ <div className="rounded-[6px] border border-[var(--vk-border)] px-4 py-3">
4165
+ <span className="text-[11px] uppercase tracking-[0.12em] text-[var(--vk-text-muted)]">
4166
+ Auth Provider
4167
+ </span>
4168
+ <p className="mt-2 text-[14px] text-[var(--vk-text-normal)]">
4169
+ {accessSettings.current.provider ?? "Local only"}
4170
+ </p>
4171
+ </div>
4172
+ </section>
4173
+
4174
+ {!accessCanEdit && (
4175
+ <section className="rounded-[6px] border border-[var(--vk-border)] bg-[rgba(80,80,80,0.18)] px-4 py-3">
4176
+ <p className="text-[12px] leading-5 text-[var(--vk-text-muted)]">
4177
+ You can review organization security here, but only an admin session can save changes.
4178
+ Use the built-in unlock link, a local admin session, or an admin identity from your edge
4179
+ auth provider to modify access rules.
4180
+ </p>
4181
+ </section>
4182
+ )}
4183
+
4184
+ <section className="space-y-3 rounded-[6px] border border-[var(--vk-border)] px-4 py-4">
4185
+ <div className="space-y-1">
4186
+ <h5 className="text-[18px] leading-[20px] text-[var(--vk-text-strong)]">Baseline Access Rules</h5>
4187
+ <p className="text-[12px] text-[var(--vk-text-muted)]">
4188
+ Require authentication for every dashboard request and decide what authenticated users get
4189
+ by default before explicit role bindings are applied.
4190
+ </p>
4191
+ </div>
4192
+
4193
+ <label className="flex items-start gap-2 rounded-[4px] border border-[var(--vk-border)] px-3 py-2 text-[13px] text-[var(--vk-text-normal)]">
4194
+ <input
4195
+ type="checkbox"
4196
+ checked={accessSettings.requireAuth}
4197
+ onChange={(event) => setAccessSettings((prev) => ({
4198
+ ...prev,
4199
+ requireAuth: event.target.checked,
4200
+ }))}
4201
+ disabled={!accessCanEdit || accessSaving}
4202
+ className="mt-0.5 h-4 w-4 rounded border border-[var(--vk-border)] bg-transparent accent-[var(--vk-orange)]"
4203
+ />
4204
+ <span>Require authentication even on localhost</span>
4205
+ </label>
4206
+
4207
+ <label className="block">
4208
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Default Role</span>
4209
+ <select
4210
+ value={accessSettings.defaultRole}
4211
+ onChange={(event) => setAccessSettings((prev) => ({
4212
+ ...prev,
4213
+ defaultRole: event.target.value as DashboardRole,
4214
+ }))}
4215
+ disabled={!accessCanEdit || accessSaving}
4216
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)] disabled:opacity-60"
4217
+ >
4218
+ <option value="viewer">Viewer</option>
4219
+ <option value="operator">Operator</option>
4220
+ <option value="admin">Admin</option>
4221
+ </select>
4222
+ <p className="mt-1 text-[12px] text-[var(--vk-text-muted)]">
4223
+ This applies after identity verification when no explicit email or domain binding matches.
4224
+ </p>
4225
+ </label>
4226
+ </section>
4227
+
4228
+ <section className="space-y-3 rounded-[6px] border border-[var(--vk-border)] px-4 py-4">
4229
+ <div className="space-y-1">
4230
+ <h5 className="text-[18px] leading-[20px] text-[var(--vk-text-strong)]">Verified Edge Auth</h5>
4231
+ <p className="text-[12px] text-[var(--vk-text-muted)]">
4232
+ Recommended for secure public phone access and free team collaboration. Conductor verifies
4233
+ the Cloudflare Access JWT instead of trusting a raw email header.
4234
+ </p>
4235
+ </div>
4236
+
4237
+ <label className="flex items-start gap-2 rounded-[4px] border border-[var(--vk-border)] px-3 py-2 text-[13px] text-[var(--vk-text-normal)]">
4238
+ <input
4239
+ type="checkbox"
4240
+ checked={accessSettings.trustedHeaders.enabled}
4241
+ onChange={(event) => setAccessSettings((prev) => ({
4242
+ ...prev,
4243
+ trustedHeaders: {
4244
+ ...prev.trustedHeaders,
4245
+ enabled: event.target.checked,
4246
+ },
4247
+ }))}
4248
+ disabled={!accessCanEdit || accessSaving}
4249
+ className="mt-0.5 h-4 w-4 rounded border border-[var(--vk-border)] bg-transparent accent-[var(--vk-orange)]"
4250
+ />
4251
+ <span>Enable verified Cloudflare Access authentication</span>
4252
+ </label>
4253
+
4254
+ <div className="grid gap-3 lg:grid-cols-2">
4255
+ <label className="block">
4256
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Provider</span>
4257
+ <select
4258
+ value={accessSettings.trustedHeaders.provider}
4259
+ onChange={(event) => setAccessSettings((prev) => ({
4260
+ ...prev,
4261
+ trustedHeaders: {
4262
+ ...prev.trustedHeaders,
4263
+ provider: event.target.value as TrustedHeaderAccessProvider,
4264
+ },
4265
+ }))}
4266
+ disabled={!accessCanEdit || accessSaving}
4267
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-[var(--vk-bg-panel)] px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)] disabled:opacity-60"
4268
+ >
4269
+ <option value="cloudflare-access">Cloudflare Access (verified JWT)</option>
4270
+ <option value="generic">Generic header passthrough (advanced)</option>
4271
+ </select>
4272
+ </label>
4273
+
4274
+ <label className="block">
4275
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Identity Email Header</span>
4276
+ <input
4277
+ value={accessSettings.trustedHeaders.emailHeader}
4278
+ onChange={(event) => setAccessSettings((prev) => ({
4279
+ ...prev,
4280
+ trustedHeaders: {
4281
+ ...prev.trustedHeaders,
4282
+ emailHeader: event.target.value,
4283
+ },
4284
+ }))}
4285
+ disabled={!accessCanEdit || accessSaving}
4286
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)] disabled:opacity-60"
4287
+ />
4288
+ </label>
4289
+
4290
+ <label className="block">
4291
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">JWT Assertion Header</span>
4292
+ <input
4293
+ value={accessSettings.trustedHeaders.jwtHeader}
4294
+ onChange={(event) => setAccessSettings((prev) => ({
4295
+ ...prev,
4296
+ trustedHeaders: {
4297
+ ...prev.trustedHeaders,
4298
+ jwtHeader: event.target.value,
4299
+ },
4300
+ }))}
4301
+ disabled={!accessCanEdit || accessSaving}
4302
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)] disabled:opacity-60"
4303
+ />
4304
+ </label>
4305
+
4306
+ <label className="block">
4307
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Cloudflare Team Domain</span>
4308
+ <input
4309
+ value={accessSettings.trustedHeaders.teamDomain}
4310
+ onChange={(event) => setAccessSettings((prev) => ({
4311
+ ...prev,
4312
+ trustedHeaders: {
4313
+ ...prev.trustedHeaders,
4314
+ teamDomain: event.target.value,
4315
+ },
4316
+ }))}
4317
+ disabled={!accessCanEdit || accessSaving}
4318
+ placeholder="your-team.cloudflareaccess.com"
4319
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)] disabled:opacity-60"
4320
+ />
4321
+ </label>
4322
+
4323
+ <label className="block lg:col-span-2">
4324
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">Cloudflare Access Audience</span>
4325
+ <input
4326
+ value={accessSettings.trustedHeaders.audience}
4327
+ onChange={(event) => setAccessSettings((prev) => ({
4328
+ ...prev,
4329
+ trustedHeaders: {
4330
+ ...prev.trustedHeaders,
4331
+ audience: event.target.value,
4332
+ },
4333
+ }))}
4334
+ disabled={!accessCanEdit || accessSaving}
4335
+ placeholder="Copy the AUD value from your Cloudflare Access application"
4336
+ className="h-9 w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)] disabled:opacity-60"
4337
+ />
4338
+ </label>
4339
+ </div>
4340
+
4341
+ {accessSettings.trustedHeaders.provider === "generic" && (
4342
+ <p className="rounded-[4px] border border-[var(--vk-red)]/35 bg-[var(--vk-red)]/10 px-3 py-2 text-[12px] leading-5 text-[var(--vk-red)]">
4343
+ Generic header passthrough is only safe when your reverse proxy strips user-supplied headers
4344
+ and injects identity itself. Conductor blocks this mode by default unless
4345
+ `CONDUCTOR_ALLOW_INSECURE_TRUSTED_HEADERS=true` is also set.
4346
+ </p>
4347
+ )}
4348
+ </section>
4349
+
4350
+ <section className="space-y-3 rounded-[6px] border border-[var(--vk-border)] px-4 py-4">
4351
+ <div className="space-y-1">
4352
+ <h5 className="text-[18px] leading-[20px] text-[var(--vk-text-strong)]">Role Bindings</h5>
4353
+ <p className="text-[12px] text-[var(--vk-text-muted)]">
4354
+ Map verified team identities into least-privilege roles. `viewer` can inspect work, `operator`
4355
+ can control agents, and `admin` can change global settings.
4356
+ </p>
4357
+ </div>
4358
+
4359
+ <div className="grid gap-3 lg:grid-cols-2">
4360
+ {accessRoleFields.map(({ label, key, placeholder }) => (
4361
+ <label key={key} className="block">
4362
+ <span className="mb-1.5 block text-[12px] font-medium text-[var(--vk-text-normal)]">{label}</span>
4363
+ <textarea
4364
+ rows={4}
4365
+ value={accessSettings.roles[key]}
4366
+ onChange={(event) => setAccessSettings((prev) => ({
4367
+ ...prev,
4368
+ roles: {
4369
+ ...prev.roles,
4370
+ [key]: event.target.value,
4371
+ },
4372
+ }))}
4373
+ disabled={!accessCanEdit || accessSaving}
4374
+ placeholder={placeholder}
4375
+ className="w-full rounded-[4px] border border-[var(--vk-border)] bg-transparent px-2 py-2 text-[13px] text-[var(--vk-text-normal)] outline-none focus:border-[var(--vk-orange)] disabled:opacity-60"
4376
+ />
4377
+ <p className="mt-1 text-[11px] text-[var(--vk-text-muted)]">One entry per line.</p>
4378
+ </label>
4379
+ ))}
4380
+ </div>
4381
+ </section>
4382
+ </>
4383
+ )}
4384
+ </div>
4385
+ ) : (
4386
+ <section className="space-y-3">
4387
+ <h4 className="text-[16px] font-medium text-[var(--vk-text-strong)]">{activeTabItem.label}</h4>
4388
+ <p className="text-[14px] text-[var(--vk-text-muted)]">
4389
+ This section is queued for implementation. General, Agents, Remote Access, and repository settings are available now.
4390
+ </p>
4391
+ <button
4392
+ type="button"
4393
+ onClick={() => setActiveTab("general")}
4394
+ className="inline-flex h-9 items-center rounded-[4px] border border-[var(--vk-border)] px-3 text-[13px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)]"
4395
+ >
4396
+ Open General
4397
+ </button>
4398
+ </section>
4399
+ )}
4400
+ </div>
4401
+
4402
+ <footer className="flex flex-col gap-3 border-t border-[var(--vk-border)] px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:gap-2">
4403
+ <div className="min-w-0">
4404
+ {dialogError && (
4405
+ <p className="truncate rounded-[4px] border border-[var(--vk-red)]/35 bg-[var(--vk-red)]/10 px-2 py-1 text-[12px] text-[var(--vk-red)]">
4406
+ {dialogError}
4407
+ </p>
4408
+ )}
4409
+ {!dialogError && isPreferenceFormTab && (
4410
+ <p className="text-[11px] text-[var(--vk-text-muted)]">
4411
+ {isOnboarding
4412
+ ? "Finish setup once here. You can change these preferences any time from Settings."
4413
+ : "Preferences are saved to your conductor config and applied immediately."}
4414
+ </p>
4415
+ )}
4416
+ {!dialogError && isRepositoriesTab && (
4417
+ <p className="text-[11px] text-[var(--vk-text-muted)]">
4418
+ {isOnboarding
4419
+ ? "These defaults will be used the first time workspaces and tasks are created for this repo."
4420
+ : "Repository settings are saved to your conductor config and used for future workspaces."}
4421
+ </p>
4422
+ )}
4423
+ {!dialogError && isOrganizationTab && (
4424
+ <p className="text-[11px] text-[var(--vk-text-muted)]">
4425
+ Organization access settings are written into `conductor.yaml`. Use admin role bindings for full
4426
+ control, operator bindings for day-to-day agent usage, and viewer bindings for read-only access.
4427
+ </p>
4428
+ )}
4429
+ </div>
4430
+ <div className="flex w-full flex-wrap items-center justify-end gap-2 sm:w-auto">
4431
+ {!isOnboarding && (
4432
+ <button
4433
+ type="button"
4434
+ onClick={onClose}
4435
+ disabled={isBusy}
4436
+ className="inline-flex h-9 items-center rounded-[4px] border border-[var(--vk-border)] px-3 text-[13px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-50"
4437
+ >
4438
+ Close
4439
+ </button>
4440
+ )}
4441
+ {isOnboarding && isRepositoriesTab && (
4442
+ <button
4443
+ type="button"
4444
+ onClick={() => setActiveTab("preferences")}
4445
+ disabled={isBusy}
4446
+ className="inline-flex h-9 items-center rounded-[4px] border border-[var(--vk-border)] px-3 text-[13px] text-[var(--vk-text-normal)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-50"
4447
+ >
4448
+ Back
4449
+ </button>
4450
+ )}
4451
+ {isPreferenceFormTab && !isOnboarding && (
4452
+ <button
4453
+ type="button"
4454
+ onClick={() => {
4455
+ void handleSubmitPreferences(current.onboardingAcknowledged, { closeDialog: true });
4456
+ }}
4457
+ disabled={!canSubmitPreferences || creating}
4458
+ className="inline-flex h-9 items-center rounded-[4px] bg-[var(--vk-bg-active)] px-3 text-[13px] text-[var(--vk-text-strong)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-50"
4459
+ >
4460
+ {creating ? (
4461
+ <>
4462
+ <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
4463
+ Saving...
4464
+ </>
4465
+ ) : "Save"}
4466
+ </button>
4467
+ )}
4468
+ {isRepositoriesTab && !isOnboarding && (
4469
+ <button
4470
+ type="button"
4471
+ onClick={() => {
4472
+ void handleSaveRepository();
4473
+ }}
4474
+ disabled={!canSaveRepository || repositoriesSaving || repositoriesLoading}
4475
+ className="inline-flex h-9 items-center rounded-[4px] bg-[var(--vk-bg-active)] px-3 text-[13px] text-[var(--vk-text-strong)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-50"
4476
+ >
4477
+ {repositoriesSaving ? (
4478
+ <>
4479
+ <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
4480
+ Saving...
4481
+ </>
4482
+ ) : "Save Repository"}
4483
+ </button>
4484
+ )}
4485
+ {isOrganizationTab && !isOnboarding && (
4486
+ <button
4487
+ type="button"
4488
+ onClick={() => {
4489
+ void handleSaveAccess();
4490
+ }}
4491
+ disabled={!canSaveAccess || accessSaving}
4492
+ className="inline-flex h-9 items-center rounded-[4px] bg-[var(--vk-bg-active)] px-3 text-[13px] text-[var(--vk-text-strong)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-50"
4493
+ >
4494
+ {accessSaving ? (
4495
+ <>
4496
+ <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
4497
+ Saving...
4498
+ </>
4499
+ ) : "Save Access"}
4500
+ </button>
4501
+ )}
4502
+ {isOnboarding && (
4503
+ <button
4504
+ type="button"
4505
+ onClick={() => {
4506
+ void (isPreferencesTab ? handleOnboardingContinue() : handleFinishOnboarding());
4507
+ }}
4508
+ disabled={
4509
+ isPreferencesTab
4510
+ ? !canSubmitPreferences || creating || repositoriesLoading
4511
+ : !canSaveRepository || isBusy
4512
+ }
4513
+ className="inline-flex h-9 items-center rounded-[4px] bg-[var(--vk-bg-active)] px-3 text-[13px] text-[var(--vk-text-strong)] hover:bg-[var(--vk-bg-hover)] disabled:opacity-50"
4514
+ >
4515
+ {isBusy || (isPreferencesTab && repositoriesLoading) ? (
4516
+ <>
4517
+ <Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
4518
+ {isPreferencesTab && repositoriesLoading ? "Loading..." : "Saving..."}
4519
+ </>
4520
+ ) : isPreferencesTab ? (
4521
+ onboardingHasRepositoryStep ? "Continue" : "Finish Setup"
4522
+ ) : (
4523
+ "Finish Setup"
4524
+ )}
4525
+ </button>
4526
+ )}
4527
+ </div>
4528
+ </footer>
4529
+ </div>
4530
+ </div>
4531
+ </div>
4532
+
4533
+ <FolderPickerDialog
4534
+ open={repositoryFolderPickerOpen}
4535
+ initialPath={repositoryDraft?.path}
4536
+ title="Select Repository Path"
4537
+ description="Choose the local git repository folder."
4538
+ onClose={() => setRepositoryFolderPickerOpen(false)}
4539
+ onSelect={(selectedPath) => {
4540
+ setRepositoryFolderPickerOpen(false);
4541
+ if (!selectedPath) return;
4542
+ setRepositoryDraft((prev) => prev
4543
+ ? {
4544
+ ...prev,
4545
+ path: selectedPath,
4546
+ pathHealth: {
4547
+ ...prev.pathHealth,
4548
+ exists: true,
4549
+ isGitRepository: true,
4550
+ suggestedPath: null,
4551
+ },
4552
+ }
4553
+ : prev);
4554
+ void detectRepositoryBranches(selectedPath);
4555
+ }}
4556
+ />
4557
+ </>
4558
+ );
4559
+ }