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