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.
- package/node_modules/@conductor-oss/plugin-agent-amp/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-ccr/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-claude-code/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-codex/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-cursor-cli/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-droid/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-gemini/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-github-copilot/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-opencode/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-agent-qwen-code/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-mcp-server/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-notifier-desktop/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-notifier-discord/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-runtime-tmux/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-scm-github/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-terminal-web/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-tracker-github/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-webhook/package.json +1 -1
- package/node_modules/@conductor-oss/plugin-workspace-worktree/package.json +1 -1
- package/package.json +21 -21
- package/web/.next/standalone/packages/web/.env.example +16 -0
- package/web/.next/standalone/packages/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/packages/web/.next/app-path-routes-manifest.json +30 -0
- package/web/.next/standalone/packages/web/.next/build-manifest.json +2 -2
- package/web/.next/standalone/packages/web/.next/prerender-manifest.json +3 -3
- package/web/.next/standalone/packages/web/.next/routes-manifest.json +204 -0
- package/web/.next/standalone/packages/web/.next/server/app/_global-error/page.js +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found/page/server-reference-manifest.json +7 -7
- package/web/.next/standalone/packages/web/.next/server/app/_not-found/page.js +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.rsc +3 -3
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/api/access/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/access/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/access/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/access/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/access/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js +4 -3
- package/web/.next/standalone/packages/web/.next/server/app/api/agents/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/attachments/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route.js +8 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/auth/session/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/boards/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/boards/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/boards/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js +12 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/boards/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/boards/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/config/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/config/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/config/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/config/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/config/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/context-files/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/events/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/events/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/events/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/events/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/events/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/filesystem/directory/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/github/repos/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/boards/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/health/sessions/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/notifications/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/preferences/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js +12 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/repositories/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/actions/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/checks/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/diff/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/feedback/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/files/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/keys/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/kill/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/output/stream/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/[id]/send/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/sessions/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/spawn/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js +10 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/branches/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route/app-paths-manifest.json +3 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route/build-manifest.json +11 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route/server-reference-manifest.json +4 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js +12 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js.map +5 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route.js.nft.json +1 -0
- package/web/.next/standalone/packages/web/.next/server/app/api/workspaces/route_client-reference-manifest.js +2 -0
- package/web/.next/standalone/packages/web/.next/server/app/auth/grant/route.js +4 -2
- package/web/.next/standalone/packages/web/.next/server/app/auth/grant/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/icon.svg/route.js +3 -1
- package/web/.next/standalone/packages/web/.next/server/app/icon.svg/route.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/index.rsc +4 -4
- package/web/.next/standalone/packages/web/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/web/.next/standalone/packages/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/web/.next/standalone/packages/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/page/react-loadable-manifest.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/page/server-reference-manifest.json +7 -7
- package/web/.next/standalone/packages/web/.next/server/app/page.js +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page/server-reference-manifest.json +7 -7
- package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page.js +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page/server-reference-manifest.json +7 -7
- package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page.js +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/sign-in/[[...sign-in]]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/unlock/page/server-reference-manifest.json +7 -7
- package/web/.next/standalone/packages/web/.next/server/app/unlock/page.js +2 -2
- package/web/.next/standalone/packages/web/.next/server/app/unlock/page.js.nft.json +1 -1
- package/web/.next/standalone/packages/web/.next/server/app/unlock/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/app-paths-manifest.json +30 -0
- 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
- package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_filesystem_directory_route_actions_b1ce53b0.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_health_sessions_route_actions_0e6610db.js +3 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/web/.next/standalone/packages/web/.next/server/chunks/730ea_web__next-internal_server_app_api_workspaces_branches_route_actions_324a72cd.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[externals]__f25f12d4._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__005aa909._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__008d962e._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__00cf28e7._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__03ff76ee._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__06365513._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__075f8697._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__097be5a7._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__09c3bdaf._.js +7 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__0a0f0f01._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1003246b._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__11918223._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__13ad088c._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1458f8ec._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__14b1d4a3._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1741fb8a._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1875f00e._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/{[root-of-the-server]__9fa7727e._.js → [root-of-the-server]__1aa339ea._.js} +2 -2
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1beced71._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1ed2e6c1._.js +226 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__1f626a16._.js +129 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2062aaab._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__206c3d8a._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__22f8f0d6._.js +129 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__23bebaf9._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2556a816._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__25788ed3._.js +54 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__29be362d._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2a83553a._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2c3f6397._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2c909fe3._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2ce39d51._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__30921756._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__31eea08c._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__3d7ad620._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__3edd3509._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__3f8b49ba._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__3f8e82c2._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__44d59fd0._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__48875cbb._.js +7 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__49c6a20e._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__4ac9d639._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__4c3fb752._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__4ca8b259._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__4dab6a6a._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__544326ac._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__54a218f0._.js +54 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__55e337f7._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5661ca72._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5cd49624._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5e0533de._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5e161db4._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5ebcb223._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__5fd609c9._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6023a748._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__60cf7bf4._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__60ee4897._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6559c1a4._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__659938b1._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6712f45a._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__69227de6._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6ab08a39._.js +129 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6cd54de1._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6ce2b5d7._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6cfd00d2._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6d62709d._.js +129 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__6e08e4e4._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__749331b0._.js +54 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__74a53184._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__7633d324._.js +4 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__76c16426._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__778e46a1._.js +129 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__7a26e452._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__7ba96fa5._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__7e684560._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__848eb266._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__8596d782._.js +7 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__85d8966b._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__86aff2b8._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__879cf4d2._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__881858fa._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__885e3a00._.js +54 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__887ea436._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__893f6b3b._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__89dfcd05._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__8cc5826f._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__913af875._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__91b42119._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__93ce1b75._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__96633022._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__96951362._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__971fcc75._.js +9 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__98c6a6f3._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__99da6324._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__9c1d09aa._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__9f07930d._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__9f36b7ec._.js +129 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__9fd08bbc._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a05953cc._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a3766ac1._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a3bfaabf._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a420e374._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__a4a33247._.js +54 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ac544bb5._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ad84d2e0._.js +54 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__b3095e9d._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__b59abfbd._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__b9692a3c._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__bcc08c70._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__be051d52._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__bea07e07._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__bf8faac8._.js +4 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__c1759514._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__c2ef2bfc._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__c3c6a7bf._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__c8618cb1._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ca8a18f8._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__cafd7bb3._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__cb112606._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__cc74ea40._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__cd3692a0._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__cf3cdecd._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__d09e7a99._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__d40b50e7._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__d4721d30._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__d538c110._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__d73ea818._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__db3d0133._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__dd0cf085._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__de0f2533._.js +7 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e113f6df._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e1a418bc._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e6c1d827._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ea69c4ce._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__eac16890._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__eb1dfcc6._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ee75d43a._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__ef773ea6._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f0d15920._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f15cbcfc._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f48eb2d4._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__fb6ab669._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__fe8419b5._.js +129 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/{[root-of-the-server]__667a63ee._.js → node_modules_07d85415._.js} +2 -2
- package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_f0726c19._.js +52 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/{[root-of-the-server]__d94cee69._.js → node_modules_next_9a15df2c._.js} +2 -2
- package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_79f1aee4._.js +6 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_0e4dc4f7.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_dd27f857.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_e445b36c.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_f375f3c4.js +28 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_f2da0d3e._.js +14 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/node_modules_next_f5199d09._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_core_dist_index_cdd30418.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-amp_dist_index_bf3d8239.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-ccr_dist_index_69cbf726.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-claude-code_dist_index_31314e43.js +125 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-cursor-cli_dist_index_8d5cc426.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-droid_dist_index_f731227a.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-gemini_dist_index_b2d219e8.js +54 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-github-copilot_dist_index_9f80cb45.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-opencode_dist_index_7c7015da.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_agent-qwen-code_dist_index_46dd8653.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_notifier-desktop_dist_index_ef3473f0.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_notifier-discord_dist_index_34257226.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_scm-github_dist_index_3b5a621e.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_terminal-web_dist_index_1e0706d3.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_plugins_tracker-github_dist_index_2d7af62f.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_access_route_actions_06740e99.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_attachments_route_actions_4dcb6bab.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_auth_session_route_actions_078c0ee4.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_boards_route_actions_160005aa.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_config_route_actions_ee96617d.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_context-files_route_actions_e0f9c4a1.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_events_route_actions_bde0da23.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_github_repos_route_actions_7eab8e08.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_health_boards_route_actions_7a65c06e.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_notifications_route_actions_c5e4154e.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_preferences_route_actions_858fa484.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_repositories_route_actions_0232075d.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_sessions_[id]_route_actions_6fdcb5a6.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_sessions_route_actions_c6df1747.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_spawn_route_actions_befa67f2.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web__next-internal_server_app_api_workspaces_route_actions_795ab8d9.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web_src_lib_13fee5eb._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/packages_web_src_lib_d64596cc._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__09178ceb._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__2b5e3ef2._.js → [root-of-the-server]__504277b5._.js} +2 -2
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__b07a94f8._.js → [root-of-the-server]__525ee5d4._.js} +2 -2
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__6622b514._.js +4 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__869d9ac0._.js +4 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{[root-of-the-server]__acdcadf9._.js → [root-of-the-server]__9726f664._.js} +2 -2
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__b3463b25._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__ca05f4a0._.js +4 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__dd8d1ff5._.js +3 -0
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_0e1412de._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_69e05fca._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_80efe193._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_b6d31783._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_c0f0e227._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/_f36ddaa9._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_8a444735._.js +3 -0
- 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
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_f2ebd7a9._.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/{packages_web_src_components_9f3d5f00._.js → packages_web_src_components_dd296b40._.js} +1 -1
- package/web/.next/standalone/packages/web/.next/server/middleware.js +2 -2
- package/web/.next/standalone/packages/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/packages/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/packages/web/.next/server/server-reference-manifest.json +8 -8
- package/web/.next/standalone/packages/web/.next/static/chunks/19d7d4e2cfe3261a.js +1 -0
- package/web/.next/standalone/packages/web/.next/static/chunks/{04f4c17a714eeae7.js → 86bce89c37cf6212.js} +1 -1
- package/web/.next/standalone/packages/web/.next/static/chunks/{9b34f9af98cb77d6.js → ab8ef6c7dfc78082.js} +1 -1
- package/web/.next/standalone/packages/web/.next/static/chunks/{8d30faf57f99d301.js → e8283780c26eaa91.js} +1 -1
- package/web/.next/{static/chunks/61ead0a3899b1761.js → standalone/packages/web/.next/static/chunks/e88fa6d41d743b9e.js} +2 -2
- package/web/.next/{static/chunks/c0b1f9af564807f3.css → standalone/packages/web/.next/static/chunks/fe557eb4039d8a8d.css} +1 -1
- package/web/.next/standalone/packages/web/next.config.ts +33 -0
- package/web/.next/standalone/packages/web/postcss.config.mjs +5 -0
- package/web/.next/standalone/packages/web/src/app/api/access/route.ts +214 -0
- package/web/.next/standalone/packages/web/src/app/api/agents/route.ts +223 -0
- package/web/.next/standalone/packages/web/src/app/api/attachments/route.ts +169 -0
- package/web/.next/standalone/packages/web/src/app/api/auth/session/route.ts +54 -0
- package/web/.next/standalone/packages/web/src/app/api/boards/route.ts +520 -0
- package/web/.next/standalone/packages/web/src/app/api/config/route.ts +81 -0
- package/web/.next/standalone/packages/web/src/app/api/context-files/route.ts +201 -0
- package/web/.next/standalone/packages/web/src/app/api/events/route.ts +125 -0
- package/web/.next/standalone/packages/web/src/app/api/filesystem/directory/route.ts +145 -0
- package/web/.next/standalone/packages/web/src/app/api/github/repos/route.ts +106 -0
- package/web/.next/standalone/packages/web/src/app/api/health/boards/route.ts +72 -0
- package/web/.next/standalone/packages/web/src/app/api/health/sessions/route.ts +77 -0
- package/web/.next/standalone/packages/web/src/app/api/notifications/route.ts +91 -0
- package/web/.next/standalone/packages/web/src/app/api/preferences/route.ts +194 -0
- package/web/.next/standalone/packages/web/src/app/api/repositories/route.ts +419 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/actions/route.ts +80 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/checks/route.ts +175 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/diff/route.ts +426 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/feedback/route.ts +47 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/files/route.ts +197 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/keys/route.ts +140 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/kill/route.ts +45 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/output/route.ts +262 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/output/stream/route.ts +111 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/restore/route.ts +49 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/route.ts +206 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/[id]/send/route.ts +58 -0
- package/web/.next/standalone/packages/web/src/app/api/sessions/route.ts +105 -0
- package/web/.next/standalone/packages/web/src/app/api/spawn/route.ts +216 -0
- package/web/.next/standalone/packages/web/src/app/api/workspaces/branches/route.ts +233 -0
- package/web/.next/standalone/packages/web/src/app/api/workspaces/route.ts +592 -0
- package/web/.next/standalone/packages/web/src/app/auth/grant/route.ts +37 -0
- package/web/.next/standalone/packages/web/src/app/globals.css +206 -0
- package/web/.next/standalone/packages/web/src/app/icon.svg +11 -0
- package/web/.next/standalone/packages/web/src/app/layout.tsx +65 -0
- package/web/.next/standalone/packages/web/src/app/page.tsx +4559 -0
- package/web/.next/standalone/packages/web/src/app/sessions/[id]/page.tsx +120 -0
- package/web/.next/standalone/packages/web/src/app/sign-in/[[...sign-in]]/page.tsx +11 -0
- package/web/.next/standalone/packages/web/src/app/unlock/UnlockForm.tsx +77 -0
- package/web/.next/standalone/packages/web/src/app/unlock/page.tsx +41 -0
- package/web/.next/standalone/packages/web/src/components/ActivityDot.tsx +93 -0
- package/web/.next/standalone/packages/web/src/components/AgentTileIcon.tsx +193 -0
- package/web/.next/standalone/packages/web/src/components/Dashboard.tsx +3444 -0
- package/web/.next/standalone/packages/web/src/components/EmptyState.tsx +58 -0
- package/web/.next/standalone/packages/web/src/components/SessionCard.tsx +377 -0
- package/web/.next/standalone/packages/web/src/components/TerminalView.tsx +184 -0
- package/web/.next/standalone/packages/web/src/components/ThemeProvider.tsx +55 -0
- package/web/.next/standalone/packages/web/src/components/agents/AgentGrid.tsx +95 -0
- package/web/.next/standalone/packages/web/src/components/board/WorkspaceKanban.tsx +1316 -0
- package/web/.next/standalone/packages/web/src/components/layout/AppShell.tsx +74 -0
- package/web/.next/standalone/packages/web/src/components/layout/EmptyState.tsx +53 -0
- package/web/.next/standalone/packages/web/src/components/layout/Sidebar.tsx +318 -0
- package/web/.next/standalone/packages/web/src/components/layout/TopBar.tsx +34 -0
- package/web/.next/standalone/packages/web/src/components/layout/WorkspaceSidebarPanel.tsx +93 -0
- package/web/.next/standalone/packages/web/src/components/sessions/ChatPanel.tsx +1559 -0
- package/web/.next/standalone/packages/web/src/components/sessions/SessionDetail.tsx +132 -0
- package/web/.next/standalone/packages/web/src/components/sessions/SessionDiff.tsx +1197 -0
- package/web/.next/standalone/packages/web/src/components/sessions/SessionOverview.tsx +314 -0
- package/web/.next/standalone/packages/web/src/components/ui/Badge.tsx +33 -0
- package/web/.next/standalone/packages/web/src/components/ui/Button.tsx +53 -0
- package/web/.next/standalone/packages/web/src/components/ui/Card.tsx +35 -0
- package/web/.next/standalone/packages/web/src/components/ui/RunningDots.tsx +22 -0
- package/web/.next/standalone/packages/web/src/components/ui/ScrollArea.tsx +43 -0
- package/web/.next/standalone/packages/web/src/components/ui/Separator.tsx +23 -0
- package/web/.next/standalone/packages/web/src/components/ui/StatusDot.tsx +30 -0
- package/web/.next/standalone/packages/web/src/components/ui/Tabs.tsx +48 -0
- package/web/.next/standalone/packages/web/src/components/ui/Tooltip.tsx +44 -0
- package/web/.next/standalone/packages/web/src/hooks/useAgents.ts +155 -0
- package/web/.next/standalone/packages/web/src/hooks/useConfig.ts +116 -0
- package/web/.next/standalone/packages/web/src/hooks/usePreferences.ts +95 -0
- package/web/.next/standalone/packages/web/src/hooks/useSession.ts +191 -0
- package/web/.next/standalone/packages/web/src/hooks/useSessionFeed.ts +265 -0
- package/web/.next/standalone/packages/web/src/hooks/useSessionOutputStream.ts +155 -0
- package/web/.next/standalone/packages/web/src/hooks/useSessions.ts +123 -0
- package/web/.next/standalone/packages/web/src/lib/accessControl.test.ts +46 -0
- package/web/.next/standalone/packages/web/src/lib/accessControl.ts +115 -0
- package/web/.next/standalone/packages/web/src/lib/agentUtils.ts +4 -0
- package/web/.next/standalone/packages/web/src/lib/auth.ts +457 -0
- package/web/.next/standalone/packages/web/src/lib/chatFeed.ts +196 -0
- package/web/.next/standalone/packages/web/src/lib/cn.ts +6 -0
- package/web/.next/standalone/packages/web/src/lib/edgeAuth.test.ts +91 -0
- package/web/.next/standalone/packages/web/src/lib/edgeAuth.ts +205 -0
- package/web/.next/standalone/packages/web/src/lib/editorLinks.ts +51 -0
- package/web/.next/standalone/packages/web/src/lib/event-bus-singleton.ts +50 -0
- package/web/.next/standalone/packages/web/src/lib/liveEvents.ts +49 -0
- package/web/.next/standalone/packages/web/src/lib/modelAccess.ts +40 -0
- package/web/.next/standalone/packages/web/src/lib/projectConfigSync.ts +144 -0
- package/web/.next/standalone/packages/web/src/lib/remoteAuth.ts +104 -0
- package/web/.next/standalone/packages/web/src/lib/runtimeAgentModels.test.ts +78 -0
- package/web/.next/standalone/packages/web/src/lib/runtimeAgentModels.ts +788 -0
- package/web/.next/standalone/packages/web/src/lib/runtimeAgentModelsShared.ts +65 -0
- package/web/.next/standalone/packages/web/src/lib/serialize.ts +128 -0
- package/web/.next/standalone/packages/web/src/lib/services.ts +319 -0
- package/web/.next/standalone/packages/web/src/lib/types.ts +182 -0
- package/web/.next/standalone/packages/web/src/proxy.ts +141 -0
- package/web/.next/standalone/packages/web/tsconfig.json +42 -0
- package/web/.next/static/chunks/19d7d4e2cfe3261a.js +1 -0
- package/web/.next/static/chunks/{04f4c17a714eeae7.js → 86bce89c37cf6212.js} +1 -1
- package/web/.next/static/chunks/{9b34f9af98cb77d6.js → ab8ef6c7dfc78082.js} +1 -1
- package/web/.next/static/chunks/{8d30faf57f99d301.js → e8283780c26eaa91.js} +1 -1
- package/web/.next/{standalone/packages/web/.next/static/chunks/61ead0a3899b1761.js → static/chunks/e88fa6d41d743b9e.js} +2 -2
- package/web/.next/{standalone/packages/web/.next/static/chunks/c0b1f9af564807f3.css → static/chunks/fe557eb4039d8a8d.css} +1 -1
- package/web/.next/standalone/packages/web/.next/server/chunks/[externals]__0d8e2e8d._.js +0 -3
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__273dc5fd._.js +0 -3
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__30b98e6f._.js +0 -4
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__bbaa7179._.js +0 -3
- package/web/.next/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f408c708._.js +0 -21
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__434b20a4._.js +0 -3
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__90837659._.js +0 -3
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__977a17ba._.js +0 -3
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__bce4b756._.js +0 -4
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__cb11b5eb._.js +0 -4
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__d1edf1ae._.js +0 -4
- package/web/.next/standalone/packages/web/.next/server/chunks/ssr/node_modules_@clerk_nextjs_dist_esm_app-router_d6c6c559._.js +0 -3
- package/web/.next/standalone/packages/web/.next/static/chunks/2ddcd95a6de1ac4a.js +0 -1
- package/web/.next/static/chunks/2ddcd95a6de1ac4a.js +0 -1
- /package/web/.next/standalone/packages/web/.next/static/{fLVGQVTN7GT-GjBotJjTr → 7KQ5wRS3BC3qvrkz8yxuS}/_buildManifest.js +0 -0
- /package/web/.next/standalone/packages/web/.next/static/{fLVGQVTN7GT-GjBotJjTr → 7KQ5wRS3BC3qvrkz8yxuS}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/packages/web/.next/static/{fLVGQVTN7GT-GjBotJjTr → 7KQ5wRS3BC3qvrkz8yxuS}/_ssgManifest.js +0 -0
- /package/web/.next/static/{fLVGQVTN7GT-GjBotJjTr → 7KQ5wRS3BC3qvrkz8yxuS}/_buildManifest.js +0 -0
- /package/web/.next/static/{fLVGQVTN7GT-GjBotJjTr → 7KQ5wRS3BC3qvrkz8yxuS}/_clientMiddlewareManifest.json +0 -0
- /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
|
+
}
|