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,2097 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useRouter } from "next/navigation";
|
|
4
|
-
import {
|
|
5
|
-
useCallback,
|
|
6
|
-
useEffect,
|
|
7
|
-
useLayoutEffect,
|
|
8
|
-
useMemo,
|
|
9
|
-
useRef,
|
|
10
|
-
useState,
|
|
11
|
-
type ChangeEvent,
|
|
12
|
-
type ComponentProps,
|
|
13
|
-
type KeyboardEvent,
|
|
14
|
-
} from "react";
|
|
15
|
-
import ReactMarkdown from "react-markdown";
|
|
16
|
-
import remarkGfm from "remark-gfm";
|
|
17
|
-
import {
|
|
18
|
-
ArrowRightLeft,
|
|
19
|
-
BrainCircuit,
|
|
20
|
-
ChevronDown,
|
|
21
|
-
ChevronRight,
|
|
22
|
-
Code2,
|
|
23
|
-
FileSearch2,
|
|
24
|
-
FileText,
|
|
25
|
-
Globe,
|
|
26
|
-
ListTodo,
|
|
27
|
-
Loader2,
|
|
28
|
-
Paperclip,
|
|
29
|
-
PencilLine,
|
|
30
|
-
Search,
|
|
31
|
-
Shield,
|
|
32
|
-
TerminalSquare,
|
|
33
|
-
UserRound,
|
|
34
|
-
Wrench,
|
|
35
|
-
type LucideIcon,
|
|
36
|
-
} from "lucide-react";
|
|
37
|
-
import { AgentTileIcon } from "@/components/AgentTileIcon";
|
|
38
|
-
import { useAgents } from "@/hooks/useAgents";
|
|
39
|
-
import { useSessionFeed } from "@/hooks/useSessionFeed";
|
|
40
|
-
import { normalizeChatText, type NormalizedChatEntry } from "@/lib/chatFeed";
|
|
41
|
-
import { normalizeAgentName } from "@/lib/agentUtils";
|
|
42
|
-
import {
|
|
43
|
-
formatCurrentModelLabel,
|
|
44
|
-
getAllStaticModelOptions,
|
|
45
|
-
} from "@/lib/sessionModelCatalog";
|
|
46
|
-
import type { RuntimeAgentModelCatalog } from "@/lib/runtimeAgentModelsShared";
|
|
47
|
-
import { SessionRuntimeStatusBar } from "./SessionRuntimeStatusBar";
|
|
48
|
-
import { uploadProjectAttachments } from "./attachmentUploads";
|
|
49
|
-
import {
|
|
50
|
-
getAgentModelCatalog,
|
|
51
|
-
getAvailableAgentModels,
|
|
52
|
-
getAvailableAgentReasoningEfforts,
|
|
53
|
-
type AgentModelOption,
|
|
54
|
-
type AgentReasoningOption,
|
|
55
|
-
type ModelAccessPreferences,
|
|
56
|
-
} from "@conductor-oss/core/types";
|
|
57
|
-
|
|
58
|
-
interface ChatPanelProps {
|
|
59
|
-
sessionId: string;
|
|
60
|
-
agentName?: string | null;
|
|
61
|
-
projectId?: string | null;
|
|
62
|
-
sessionModel?: string | null;
|
|
63
|
-
sessionReasoningEffort?: string | null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface AttachmentDraft {
|
|
67
|
-
file: File;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface ModelOption {
|
|
71
|
-
id: string;
|
|
72
|
-
label: string;
|
|
73
|
-
helper: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
interface SlashCommandOption {
|
|
77
|
-
command: string;
|
|
78
|
-
label: string;
|
|
79
|
-
description: string;
|
|
80
|
-
exact?: boolean;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const COMMON_SLASH_COMMANDS: SlashCommandOption[] = [
|
|
84
|
-
{
|
|
85
|
-
command: "/help",
|
|
86
|
-
label: "/help",
|
|
87
|
-
description: "Show the agent help and available commands.",
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
command: "/model",
|
|
91
|
-
label: "/model",
|
|
92
|
-
description: "Inspect or switch the active model.",
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
command: "/clear",
|
|
96
|
-
label: "/clear",
|
|
97
|
-
description: "Clear the current agent conversation context.",
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
command: "/review",
|
|
101
|
-
label: "/review",
|
|
102
|
-
description: "Ask the agent to review current changes.",
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
command: "/diff",
|
|
106
|
-
label: "/diff",
|
|
107
|
-
description: "Inspect the current diff from inside the agent.",
|
|
108
|
-
},
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
function findRuntimeCatalog(
|
|
112
|
-
agents: ReturnType<typeof useAgents>["agents"],
|
|
113
|
-
agentName: string,
|
|
114
|
-
): RuntimeAgentModelCatalog | null {
|
|
115
|
-
const normalizedAgentName = normalizeAgentName(agentName);
|
|
116
|
-
if (!normalizedAgentName) return null;
|
|
117
|
-
|
|
118
|
-
return agents.find((agent) => normalizeAgentName(agent.name) === normalizedAgentName)?.runtimeModelCatalog ?? null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
function getModelOptions(
|
|
123
|
-
runtimeCatalog: RuntimeAgentModelCatalog | null,
|
|
124
|
-
agentName: string,
|
|
125
|
-
currentModel: string,
|
|
126
|
-
): ModelOption[] {
|
|
127
|
-
const options = new Map<string, ModelOption>();
|
|
128
|
-
|
|
129
|
-
for (const model of getAllStaticModelOptions(agentName)) {
|
|
130
|
-
const id = model.id.trim();
|
|
131
|
-
if (!id || options.has(id)) continue;
|
|
132
|
-
options.set(id, {
|
|
133
|
-
id,
|
|
134
|
-
label: model.label.trim() || id,
|
|
135
|
-
helper: "Built-in catalog",
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
for (const modelList of Object.values(runtimeCatalog?.modelsByAccess ?? {})) {
|
|
140
|
-
if (!Array.isArray(modelList)) continue;
|
|
141
|
-
for (const model of modelList) {
|
|
142
|
-
const id = typeof model.id === "string" ? model.id.trim() : "";
|
|
143
|
-
if (!id || options.has(id)) continue;
|
|
144
|
-
const label = typeof model.label === "string" && model.label.trim().length > 0 ? model.label.trim() : id;
|
|
145
|
-
options.set(id, {
|
|
146
|
-
id,
|
|
147
|
-
label,
|
|
148
|
-
helper: "Detected locally",
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (currentModel.trim() && !options.has(currentModel.trim())) {
|
|
154
|
-
options.set(currentModel.trim(), {
|
|
155
|
-
id: currentModel.trim(),
|
|
156
|
-
label: formatCurrentModelLabel(agentName, currentModel),
|
|
157
|
-
helper: "Current session model",
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return [...options.values()].sort((left, right) => left.label.localeCompare(right.label));
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function getReasoningOptions(
|
|
165
|
-
agentName: string,
|
|
166
|
-
runtimeCatalog: RuntimeAgentModelCatalog | null,
|
|
167
|
-
model: string,
|
|
168
|
-
): AgentReasoningOption[] {
|
|
169
|
-
const normalizedModel = model.trim();
|
|
170
|
-
const options = new Map<string, AgentReasoningOption>();
|
|
171
|
-
|
|
172
|
-
const push = (candidate: AgentReasoningOption | null | undefined) => {
|
|
173
|
-
if (!candidate?.id || options.has(candidate.id)) return;
|
|
174
|
-
options.set(candidate.id, candidate);
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
if (normalizedModel) {
|
|
178
|
-
for (const option of runtimeCatalog?.reasoningOptionsByModel?.[normalizedModel] ?? []) {
|
|
179
|
-
push(option);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
for (const group of Object.values(runtimeCatalog?.reasoningOptionsByAccess ?? {})) {
|
|
184
|
-
if (!Array.isArray(group)) continue;
|
|
185
|
-
for (const option of group) {
|
|
186
|
-
push(option);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const catalog = getAgentModelCatalog(agentName);
|
|
191
|
-
if (catalog) {
|
|
192
|
-
for (const accessOption of catalog.accessOptions) {
|
|
193
|
-
const preferences = { [catalog.accessKey]: accessOption.id } as ModelAccessPreferences;
|
|
194
|
-
for (const option of getAvailableAgentReasoningEfforts(agentName, preferences)) {
|
|
195
|
-
push(option);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
} else {
|
|
199
|
-
for (const option of getAvailableAgentReasoningEfforts(agentName, undefined)) {
|
|
200
|
-
push(option);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return [...options.values()];
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function formatReasoningLabel(value: string): string {
|
|
208
|
-
const normalized = value.trim().toLowerCase();
|
|
209
|
-
if (!normalized) return "Session default";
|
|
210
|
-
if (normalized === "xhigh") return "Extra High";
|
|
211
|
-
return normalized
|
|
212
|
-
.split(/[_\s-]+/g)
|
|
213
|
-
.filter(Boolean)
|
|
214
|
-
.map((part) => part[0]?.toUpperCase() + part.slice(1))
|
|
215
|
-
.join(" ");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function getDefaultReasoningSelection(
|
|
219
|
-
agentName: string,
|
|
220
|
-
runtimeCatalog: RuntimeAgentModelCatalog | null,
|
|
221
|
-
model: string,
|
|
222
|
-
sessionReasoningEffort: string,
|
|
223
|
-
): string {
|
|
224
|
-
const options = getReasoningOptions(agentName, runtimeCatalog, model);
|
|
225
|
-
const normalizedSession = sessionReasoningEffort.trim().toLowerCase();
|
|
226
|
-
if (normalizedSession && options.some((option) => option.id === normalizedSession)) {
|
|
227
|
-
return normalizedSession;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const normalizedModel = model.trim();
|
|
231
|
-
const runtimeDefault = normalizedModel
|
|
232
|
-
? runtimeCatalog?.defaultReasoningByModel?.[normalizedModel] ?? null
|
|
233
|
-
: null;
|
|
234
|
-
const normalizedRuntimeDefault = runtimeDefault?.trim().toLowerCase() ?? "";
|
|
235
|
-
if (normalizedRuntimeDefault && options.some((option) => option.id === normalizedRuntimeDefault)) {
|
|
236
|
-
return normalizedRuntimeDefault;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
for (const candidate of Object.values(runtimeCatalog?.defaultReasoningByAccess ?? {})) {
|
|
240
|
-
const normalizedCandidate = candidate?.trim().toLowerCase() ?? "";
|
|
241
|
-
if (normalizedCandidate && options.some((option) => option.id === normalizedCandidate)) {
|
|
242
|
-
return normalizedCandidate;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return options[0]?.id ?? "";
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function getCustomModelPlaceholder(
|
|
250
|
-
agentName: string,
|
|
251
|
-
runtimeCatalog: RuntimeAgentModelCatalog | null,
|
|
252
|
-
): string {
|
|
253
|
-
const runtimePlaceholder = runtimeCatalog?.customModelPlaceholder?.trim();
|
|
254
|
-
if (runtimePlaceholder) return runtimePlaceholder;
|
|
255
|
-
const label = getAgentModelCatalog(agentName)?.label ?? "agent";
|
|
256
|
-
return `Enter exact ${label} model id`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function getSlashCommandOptions(agentName: string, message: string): SlashCommandOption[] {
|
|
260
|
-
const normalizedMessage = message.trimStart();
|
|
261
|
-
if (!normalizedMessage.startsWith("/")) {
|
|
262
|
-
return [];
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const normalizedAgent = agentName.trim() || "agent";
|
|
266
|
-
const query = normalizedMessage.toLowerCase();
|
|
267
|
-
const options = new Map<string, SlashCommandOption>();
|
|
268
|
-
|
|
269
|
-
options.set(normalizedMessage, {
|
|
270
|
-
command: normalizedMessage,
|
|
271
|
-
label: normalizedMessage,
|
|
272
|
-
description: `Send this raw slash command directly to ${normalizedAgent}.`,
|
|
273
|
-
exact: true,
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
for (const option of COMMON_SLASH_COMMANDS) {
|
|
277
|
-
const haystack = `${option.command} ${option.label} ${option.description}`.toLowerCase();
|
|
278
|
-
if (!query || haystack.includes(query)) {
|
|
279
|
-
options.set(option.command, option);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return [...options.values()];
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function formatTimestamp(value: string | null): string | null {
|
|
287
|
-
if (!value) return null;
|
|
288
|
-
const timestamp = Date.parse(value);
|
|
289
|
-
if (Number.isNaN(timestamp)) return null;
|
|
290
|
-
return new Intl.DateTimeFormat(undefined, {
|
|
291
|
-
hour: "numeric",
|
|
292
|
-
minute: "2-digit",
|
|
293
|
-
month: "short",
|
|
294
|
-
day: "numeric",
|
|
295
|
-
}).format(new Date(timestamp));
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function getStatusPresentation(text: string): {
|
|
299
|
-
Icon: LucideIcon;
|
|
300
|
-
compact: boolean;
|
|
301
|
-
showDot: boolean;
|
|
302
|
-
} {
|
|
303
|
-
const normalizedText = text.trim();
|
|
304
|
-
|
|
305
|
-
if (/^thinking\b/i.test(normalizedText)) {
|
|
306
|
-
return { Icon: Code2, compact: true, showDot: false };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (/^searched\b/i.test(normalizedText)) {
|
|
310
|
-
return { Icon: Search, compact: true, showDot: true };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (
|
|
314
|
-
/^(git|pnpm|npm|npx|yarn|cargo|bun|node|ls|cd|cat|rg|find|sed|touch|mkdir|rm|cp|mv|gh|python|uv)\b/i.test(normalizedText)
|
|
315
|
-
|| normalizedText.includes("&&")
|
|
316
|
-
) {
|
|
317
|
-
return { Icon: TerminalSquare, compact: true, showDot: true };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (/session status:/i.test(normalizedText)) {
|
|
321
|
-
return { Icon: Code2, compact: true, showDot: false };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return { Icon: Code2, compact: false, showDot: false };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function isCommandLikeStatus(text: string): boolean {
|
|
328
|
-
return /^(git|pnpm|npm|npx|yarn|cargo|bun|node|ls|cd|cat|rg|find|sed|touch|mkdir|rm|cp|mv|gh|python|uv)\b/i.test(text.trim())
|
|
329
|
-
|| text.includes("&&");
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function extractComposerSummary(entries: NormalizedChatEntry[]): string | null {
|
|
333
|
-
const candidates = [
|
|
334
|
-
...entries.map((entry) => entry.text),
|
|
335
|
-
].map((value) => value.trim()).filter(Boolean);
|
|
336
|
-
|
|
337
|
-
return candidates.find((value) => /files changed/i.test(value) || (/\+\d+/.test(value) && /-\d+/.test(value))) ?? null;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function parseComposerSummary(summary: string): {
|
|
341
|
-
label: string;
|
|
342
|
-
additions: string | null;
|
|
343
|
-
deletions: string | null;
|
|
344
|
-
} {
|
|
345
|
-
const match = summary.match(/^(.*?files changed)(?:\s+(\+\d+))?(?:\s+(-\d+))?$/i);
|
|
346
|
-
if (!match) {
|
|
347
|
-
return {
|
|
348
|
-
label: summary,
|
|
349
|
-
additions: null,
|
|
350
|
-
deletions: null,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return {
|
|
355
|
-
label: match[1]?.trim() || summary,
|
|
356
|
-
additions: match[2] ?? null,
|
|
357
|
-
deletions: match[3] ?? null,
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const markdownComponents = {
|
|
362
|
-
a: (props: ComponentProps<"a">) => (
|
|
363
|
-
<a
|
|
364
|
-
{...props}
|
|
365
|
-
target="_blank"
|
|
366
|
-
rel="noreferrer"
|
|
367
|
-
className="text-[#ea7a2a] underline underline-offset-2"
|
|
368
|
-
/>
|
|
369
|
-
),
|
|
370
|
-
p: (props: ComponentProps<"p">) => (
|
|
371
|
-
<p
|
|
372
|
-
{...props}
|
|
373
|
-
className={[props.className, "whitespace-pre-wrap"].filter(Boolean).join(" ")}
|
|
374
|
-
/>
|
|
375
|
-
),
|
|
376
|
-
li: (props: ComponentProps<"li">) => (
|
|
377
|
-
<li
|
|
378
|
-
{...props}
|
|
379
|
-
className={[props.className, "whitespace-pre-wrap"].filter(Boolean).join(" ")}
|
|
380
|
-
/>
|
|
381
|
-
),
|
|
382
|
-
pre: (props: ComponentProps<"pre">) => (
|
|
383
|
-
<pre
|
|
384
|
-
{...props}
|
|
385
|
-
className={[
|
|
386
|
-
props.className,
|
|
387
|
-
"max-w-full overflow-x-hidden whitespace-pre-wrap break-words rounded-[6px] border border-[#333] bg-[#1c1c1c] px-3 py-3 text-[12px] leading-[18px] [overflow-wrap:anywhere] sm:text-[13px] sm:leading-[20px]",
|
|
388
|
-
].filter(Boolean).join(" ")}
|
|
389
|
-
/>
|
|
390
|
-
),
|
|
391
|
-
code: (props: ComponentProps<"code"> & { inline?: boolean }) => (
|
|
392
|
-
<code
|
|
393
|
-
{...props}
|
|
394
|
-
className={[
|
|
395
|
-
props.className,
|
|
396
|
-
"font-mono whitespace-pre-wrap break-all [overflow-wrap:anywhere]",
|
|
397
|
-
props.inline ? "rounded bg-[rgba(0,0,0,0.22)] px-1 py-[1px] text-[#f1f1f1]" : "",
|
|
398
|
-
].filter(Boolean).join(" ")}
|
|
399
|
-
/>
|
|
400
|
-
),
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
function SetupScriptHint() {
|
|
404
|
-
return (
|
|
405
|
-
<div className="flex items-start gap-3 rounded-[3px] px-2 py-1 text-[#8f8f8f]">
|
|
406
|
-
<div className="flex h-[21px] w-[21px] items-center justify-center pt-[1px] text-[#8f8f8f]">
|
|
407
|
-
<TerminalSquare className="h-[15px] w-[15px]" strokeWidth={1.6} />
|
|
408
|
-
</div>
|
|
409
|
-
<div className="min-w-0 space-y-[2px]">
|
|
410
|
-
<p className="text-[14px] leading-[21px] text-[#8f8f8f]">Setup Script</p>
|
|
411
|
-
<p className="text-[12px] leading-[18px] text-[#c4c4c4]">
|
|
412
|
-
No setup script configured. Setup scripts run before the coding agent starts.
|
|
413
|
-
</p>
|
|
414
|
-
</div>
|
|
415
|
-
</div>
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function MarkdownBlock({ text, className }: { text: string; className: string }) {
|
|
420
|
-
return (
|
|
421
|
-
<ReactMarkdown
|
|
422
|
-
className={className}
|
|
423
|
-
remarkPlugins={[remarkGfm]}
|
|
424
|
-
components={markdownComponents}
|
|
425
|
-
>
|
|
426
|
-
{text}
|
|
427
|
-
</ReactMarkdown>
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
type ParsedReviewFinding = {
|
|
432
|
-
id: string;
|
|
433
|
-
severity: "critical" | "high" | "medium" | "low" | "neutral";
|
|
434
|
-
title: string;
|
|
435
|
-
body: string;
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
type ParsedReviewContent = {
|
|
439
|
-
intro: string | null;
|
|
440
|
-
findingsHeading: string;
|
|
441
|
-
findings: ParsedReviewFinding[];
|
|
442
|
-
closing: string | null;
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
type ParsedOutlineSection = {
|
|
446
|
-
id: string;
|
|
447
|
-
title: string;
|
|
448
|
-
items: string[];
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
function parseReviewContent(text: string): ParsedReviewContent | null {
|
|
452
|
-
const normalized = text.replace(/\r/g, "");
|
|
453
|
-
const lines = normalized.split("\n");
|
|
454
|
-
const findingsIndex = lines.findIndex((line) => /^(?:#{1,6}\s*)?findings\b/i.test(line.trim()));
|
|
455
|
-
if (findingsIndex < 0) {
|
|
456
|
-
return null;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const intro = lines.slice(0, findingsIndex).join("\n").trim() || null;
|
|
460
|
-
const findingsHeading = lines[findingsIndex]?.replace(/^#{1,6}\s*/, "").trim() || "Findings";
|
|
461
|
-
const remaining = lines.slice(findingsIndex + 1);
|
|
462
|
-
|
|
463
|
-
const trailingHeadingIndex = remaining.findIndex((line) =>
|
|
464
|
-
/^(?:#{1,6}\s*)?(assumptions|open questions|assumptions\s*\/\s*open questions|next steps)\b/i.test(line.trim())
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
const findingsLines = trailingHeadingIndex >= 0 ? remaining.slice(0, trailingHeadingIndex) : remaining;
|
|
468
|
-
const closing = trailingHeadingIndex >= 0 ? remaining.slice(trailingHeadingIndex).join("\n").trim() || null : null;
|
|
469
|
-
|
|
470
|
-
const chunks: string[][] = [];
|
|
471
|
-
let current: string[] = [];
|
|
472
|
-
|
|
473
|
-
for (const line of findingsLines) {
|
|
474
|
-
if (/^\s*\d+[.)]\s+/.test(line)) {
|
|
475
|
-
if (current.length > 0) {
|
|
476
|
-
chunks.push(current);
|
|
477
|
-
}
|
|
478
|
-
current = [line];
|
|
479
|
-
} else if (current.length > 0) {
|
|
480
|
-
current.push(line);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (current.length > 0) {
|
|
485
|
-
chunks.push(current);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const findings = chunks
|
|
489
|
-
.map((chunk, index) => {
|
|
490
|
-
const [firstLine, ...rest] = chunk;
|
|
491
|
-
const cleanedFirst = firstLine.replace(/^\s*\d+[.)]\s+/, "").replace(/\*\*/g, "").trim();
|
|
492
|
-
const severityMatch = cleanedFirst.match(/\b(critical|high|medium|low)\b/i);
|
|
493
|
-
const severity = (severityMatch?.[1]?.toLowerCase() ?? "neutral") as ParsedReviewFinding["severity"];
|
|
494
|
-
const title = cleanedFirst
|
|
495
|
-
.replace(/\b(critical|high|medium|low)\b\s*[—:-]\s*/i, "")
|
|
496
|
-
.replace(/\b(critical|high|medium|low)\b/i, "")
|
|
497
|
-
.trim();
|
|
498
|
-
const body = rest.join("\n").trim();
|
|
499
|
-
|
|
500
|
-
return {
|
|
501
|
-
id: `finding-${index}`,
|
|
502
|
-
severity,
|
|
503
|
-
title: title || cleanedFirst,
|
|
504
|
-
body,
|
|
505
|
-
};
|
|
506
|
-
})
|
|
507
|
-
.filter((finding) => finding.title.length > 0);
|
|
508
|
-
|
|
509
|
-
if (findings.length === 0) {
|
|
510
|
-
return null;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return {
|
|
514
|
-
intro,
|
|
515
|
-
findingsHeading,
|
|
516
|
-
findings,
|
|
517
|
-
closing,
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function isLikelyOutlineTitle(line: string): boolean {
|
|
522
|
-
const trimmed = line.trim();
|
|
523
|
-
if (!trimmed) return false;
|
|
524
|
-
if (/^[-*+]\s|^\d+[.)]\s|^#{1,6}\s|^>\s|^```/.test(trimmed)) return false;
|
|
525
|
-
if (trimmed.length > 72) return false;
|
|
526
|
-
if (/[.!?;]$/.test(trimmed)) return false;
|
|
527
|
-
const wordCount = trimmed.split(/\s+/).length;
|
|
528
|
-
return wordCount <= 8;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function parseOutlineContent(text: string): ParsedOutlineSection[] | null {
|
|
532
|
-
const normalized = text.replace(/\r/g, "");
|
|
533
|
-
const lines = normalized.split("\n");
|
|
534
|
-
if (lines.some((line) => /^\s*[-*+]\s|^\s*\d+[.)]\s|^\s*#{1,6}\s/.test(line))) {
|
|
535
|
-
return null;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const titleIndices = lines
|
|
539
|
-
.map((line, index) => ({ line, index }))
|
|
540
|
-
.filter(({ line, index }) => {
|
|
541
|
-
const next = lines[index + 1]?.trim() ?? "";
|
|
542
|
-
return isLikelyOutlineTitle(line) && next.length > 0 && !isLikelyOutlineTitle(next);
|
|
543
|
-
})
|
|
544
|
-
.map(({ index }) => index);
|
|
545
|
-
|
|
546
|
-
if (titleIndices.length < 2) {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const sections: ParsedOutlineSection[] = [];
|
|
551
|
-
for (let i = 0; i < titleIndices.length; i += 1) {
|
|
552
|
-
const start = titleIndices[i];
|
|
553
|
-
const end = titleIndices[i + 1] ?? lines.length;
|
|
554
|
-
const title = lines[start]!.trim().replace(/:$/, "");
|
|
555
|
-
const items = lines
|
|
556
|
-
.slice(start + 1, end)
|
|
557
|
-
.map((line) => line.trim())
|
|
558
|
-
.filter((line) => line.length > 0);
|
|
559
|
-
|
|
560
|
-
if (title.length === 0 || items.length === 0) {
|
|
561
|
-
continue;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
sections.push({
|
|
565
|
-
id: `outline-${i}`,
|
|
566
|
-
title,
|
|
567
|
-
items,
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return sections.length >= 2 ? sections : null;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
function getFindingTone(finding: ParsedReviewFinding): { badge: string; border: string; surface: string } {
|
|
575
|
-
switch (finding.severity) {
|
|
576
|
-
case "critical":
|
|
577
|
-
case "high":
|
|
578
|
-
return {
|
|
579
|
-
badge: "bg-[rgba(210,81,81,0.14)] text-[#f1b0b0]",
|
|
580
|
-
border: "border-[rgba(210,81,81,0.22)]",
|
|
581
|
-
surface: "bg-[rgba(210,81,81,0.05)]",
|
|
582
|
-
};
|
|
583
|
-
case "medium":
|
|
584
|
-
return {
|
|
585
|
-
badge: "bg-[rgba(214,164,60,0.14)] text-[#f0d28b]",
|
|
586
|
-
border: "border-[rgba(214,164,60,0.2)]",
|
|
587
|
-
surface: "bg-[rgba(214,164,60,0.05)]",
|
|
588
|
-
};
|
|
589
|
-
case "low":
|
|
590
|
-
return {
|
|
591
|
-
badge: "bg-[rgba(84,176,79,0.14)] text-[#b8dfb5]",
|
|
592
|
-
border: "border-[rgba(84,176,79,0.2)]",
|
|
593
|
-
surface: "bg-[rgba(84,176,79,0.05)]",
|
|
594
|
-
};
|
|
595
|
-
default:
|
|
596
|
-
return {
|
|
597
|
-
badge: "bg-[rgba(143,143,143,0.14)] text-[#c4c4c4]",
|
|
598
|
-
border: "border-[var(--vk-border)]",
|
|
599
|
-
surface: "bg-[rgba(255,255,255,0.03)]",
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function ReviewFindingsBlock({ text }: { text: string }) {
|
|
605
|
-
const parsed = parseReviewContent(text);
|
|
606
|
-
if (!parsed) {
|
|
607
|
-
return null;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
return (
|
|
611
|
-
<div className="space-y-5">
|
|
612
|
-
{parsed.intro ? (
|
|
613
|
-
<MarkdownBlock
|
|
614
|
-
text={parsed.intro}
|
|
615
|
-
className="prose prose-invert max-w-none text-[16px] leading-[28px] text-[#cfcfcf] prose-headings:mb-3 prose-headings:mt-6 prose-headings:text-[#f1f1f1] prose-p:my-4 prose-p:text-[#cfcfcf] prose-strong:text-[#f1f1f1] prose-ol:my-4 prose-ol:pl-6 prose-ul:my-4 prose-ul:pl-6 prose-li:my-2 prose-li:text-[#cfcfcf] prose-pre:my-4 prose-pre:overflow-x-auto prose-pre:rounded-[6px] prose-pre:border prose-pre:border-[#333] prose-pre:bg-[#1c1c1c] prose-code:text-[#d7d7d7]"
|
|
616
|
-
/>
|
|
617
|
-
) : null}
|
|
618
|
-
|
|
619
|
-
<div className="overflow-hidden rounded-[8px] border border-[var(--vk-border)] bg-[rgba(255,255,255,0.03)]">
|
|
620
|
-
<div className="border-b border-[var(--vk-border)] px-4 py-3">
|
|
621
|
-
<p className="text-[12px] uppercase tracking-[0.16em] text-[#8f8f8f]">{parsed.findingsHeading}</p>
|
|
622
|
-
</div>
|
|
623
|
-
<div className="space-y-4 p-4">
|
|
624
|
-
{parsed.findings.map((finding, index) => {
|
|
625
|
-
const tone = getFindingTone(finding);
|
|
626
|
-
return (
|
|
627
|
-
<div
|
|
628
|
-
key={finding.id}
|
|
629
|
-
className={`rounded-[6px] border px-4 py-3 ${tone.border} ${tone.surface}`}
|
|
630
|
-
>
|
|
631
|
-
<div className="flex flex-wrap items-center gap-3">
|
|
632
|
-
<span className="text-[13px] font-medium leading-[20px] text-[#8f8f8f]">
|
|
633
|
-
{index + 1}.
|
|
634
|
-
</span>
|
|
635
|
-
<span className={`rounded-full px-2 py-[3px] text-[11px] font-medium uppercase tracking-[0.12em] ${tone.badge}`}>
|
|
636
|
-
{finding.severity}
|
|
637
|
-
</span>
|
|
638
|
-
<p className="min-w-0 flex-1 text-[16px] leading-[24px] text-[#f1f1f1]">
|
|
639
|
-
{finding.title}
|
|
640
|
-
</p>
|
|
641
|
-
</div>
|
|
642
|
-
{finding.body ? (
|
|
643
|
-
<MarkdownBlock
|
|
644
|
-
text={finding.body}
|
|
645
|
-
className="prose prose-invert mt-3 max-w-none text-[14px] leading-[24px] text-[#c9c9c9] prose-p:my-3 prose-p:text-[#c9c9c9] prose-strong:text-[#f1f1f1] prose-ul:my-3 prose-ul:pl-5 prose-ol:my-3 prose-ol:pl-5 prose-li:my-1.5 prose-li:text-[#c9c9c9] prose-code:text-[#d7d7d7] prose-pre:my-3 prose-pre:overflow-x-auto prose-pre:rounded-[6px] prose-pre:border prose-pre:border-[#333] prose-pre:bg-[#1c1c1c]"
|
|
646
|
-
/>
|
|
647
|
-
) : null}
|
|
648
|
-
</div>
|
|
649
|
-
);
|
|
650
|
-
})}
|
|
651
|
-
</div>
|
|
652
|
-
</div>
|
|
653
|
-
|
|
654
|
-
{parsed.closing ? (
|
|
655
|
-
<div className="rounded-[8px] border border-[var(--vk-border)] bg-[rgba(255,255,255,0.03)] px-4 py-4">
|
|
656
|
-
<MarkdownBlock
|
|
657
|
-
text={parsed.closing}
|
|
658
|
-
className="prose prose-invert max-w-none text-[15px] leading-[25px] text-[#c9c9c9] prose-headings:mb-2 prose-headings:mt-5 prose-headings:text-[#f1f1f1] prose-p:my-3 prose-p:text-[#c9c9c9] prose-strong:text-[#f1f1f1] prose-ol:my-3 prose-ol:pl-5 prose-ul:my-3 prose-ul:pl-5 prose-li:my-1.5 prose-li:text-[#c9c9c9] prose-code:text-[#d7d7d7]"
|
|
659
|
-
/>
|
|
660
|
-
</div>
|
|
661
|
-
) : null}
|
|
662
|
-
</div>
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function OutlineSummaryBlock({ sections }: { sections: ParsedOutlineSection[] }) {
|
|
667
|
-
return (
|
|
668
|
-
<div className="space-y-6">
|
|
669
|
-
{sections.map((section) => (
|
|
670
|
-
<section key={section.id} className="space-y-3">
|
|
671
|
-
<h3 className="text-[22px] font-medium leading-[30px] text-[#f1f1f1]">
|
|
672
|
-
{section.title}
|
|
673
|
-
</h3>
|
|
674
|
-
<div className="space-y-3">
|
|
675
|
-
{section.items.map((item, index) => (
|
|
676
|
-
<div
|
|
677
|
-
key={`${section.id}-item-${index}`}
|
|
678
|
-
className="rounded-[6px] border border-[rgba(255,255,255,0.05)] bg-[rgba(255,255,255,0.02)] px-4 py-3"
|
|
679
|
-
>
|
|
680
|
-
<MarkdownBlock
|
|
681
|
-
text={item}
|
|
682
|
-
className="prose prose-invert max-w-none text-[16px] leading-[28px] text-[#cfcfcf] prose-p:my-0 prose-p:text-[#cfcfcf] prose-strong:text-[#f1f1f1] prose-code:text-[#d7d7d7]"
|
|
683
|
-
/>
|
|
684
|
-
</div>
|
|
685
|
-
))}
|
|
686
|
-
</div>
|
|
687
|
-
</section>
|
|
688
|
-
))}
|
|
689
|
-
</div>
|
|
690
|
-
);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function extractToolContent(entry: NormalizedChatEntry): string[] {
|
|
694
|
-
const raw = entry.metadata?.toolContent;
|
|
695
|
-
if (Array.isArray(raw)) {
|
|
696
|
-
return raw
|
|
697
|
-
.map((value) => typeof value === "string" ? normalizeChatText(value) : "")
|
|
698
|
-
.filter((value): value is string => value.trim().length > 0);
|
|
699
|
-
}
|
|
700
|
-
const normalized = normalizeChatText(entry.text);
|
|
701
|
-
return normalized.trim().length > 0 ? [normalized.trim()] : [];
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function getToolInlineSummary(entry: NormalizedChatEntry, content: string[]): string | null {
|
|
705
|
-
const title = typeof entry.metadata?.toolTitle === "string" ? normalizeChatText(entry.metadata.toolTitle).trim() : "";
|
|
706
|
-
const first = content[0]?.trim() ?? "";
|
|
707
|
-
if (!first) return null;
|
|
708
|
-
if (title && first.toLowerCase() === title.toLowerCase()) {
|
|
709
|
-
return null;
|
|
710
|
-
}
|
|
711
|
-
return first.length > 84 ? `${first.slice(0, 81)}...` : first;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function getToolStatusTone(status: string | null | undefined): "pending" | "running" | "success" | "error" | "cancelled" {
|
|
715
|
-
const lower = status?.trim().toLowerCase() ?? "";
|
|
716
|
-
if (lower.includes("complete") || lower.includes("success") || lower.includes("done")) {
|
|
717
|
-
return "success";
|
|
718
|
-
}
|
|
719
|
-
if (lower.includes("error") || lower.includes("fail")) {
|
|
720
|
-
return "error";
|
|
721
|
-
}
|
|
722
|
-
if (lower.includes("cancel")) {
|
|
723
|
-
return "cancelled";
|
|
724
|
-
}
|
|
725
|
-
if (lower.includes("running") || lower.includes("progress")) {
|
|
726
|
-
return "running";
|
|
727
|
-
}
|
|
728
|
-
return "pending";
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function getToolIcon(entry: NormalizedChatEntry): LucideIcon {
|
|
732
|
-
const toolKind = typeof entry.metadata?.toolKind === "string" ? entry.metadata.toolKind.toLowerCase() : "";
|
|
733
|
-
const title = (typeof entry.metadata?.toolTitle === "string" ? entry.metadata.toolTitle : entry.text).toLowerCase();
|
|
734
|
-
if (toolKind.includes("thinking") || title.includes("thinking")) {
|
|
735
|
-
return BrainCircuit;
|
|
736
|
-
}
|
|
737
|
-
if (toolKind.includes("web") || title.includes("web search") || title.includes("web fetch")) {
|
|
738
|
-
return Globe;
|
|
739
|
-
}
|
|
740
|
-
if (
|
|
741
|
-
toolKind.includes("grep")
|
|
742
|
-
|| toolKind.includes("glob")
|
|
743
|
-
|| toolKind.includes("search")
|
|
744
|
-
|| toolKind.includes("find")
|
|
745
|
-
|| title.includes("search")
|
|
746
|
-
|| title.includes("grep")
|
|
747
|
-
|| title.includes("glob")
|
|
748
|
-
|| title.includes("find")
|
|
749
|
-
) {
|
|
750
|
-
return FileSearch2;
|
|
751
|
-
}
|
|
752
|
-
if (toolKind.includes("read") || title.includes("read")) {
|
|
753
|
-
return FileText;
|
|
754
|
-
}
|
|
755
|
-
if (
|
|
756
|
-
toolKind.includes("edit")
|
|
757
|
-
|| toolKind.includes("write")
|
|
758
|
-
|| toolKind.includes("multiedit")
|
|
759
|
-
|| title.includes("edit")
|
|
760
|
-
|| title.includes("write")
|
|
761
|
-
) {
|
|
762
|
-
return PencilLine;
|
|
763
|
-
}
|
|
764
|
-
if (toolKind.includes("task") || toolKind.includes("todo") || title.includes("todo")) {
|
|
765
|
-
return ListTodo;
|
|
766
|
-
}
|
|
767
|
-
if (toolKind.includes("search") || title.includes("search") || title.includes("rg ") || title.includes("find ")) {
|
|
768
|
-
return Search;
|
|
769
|
-
}
|
|
770
|
-
if (toolKind.includes("permission") || title.includes("permission") || title.includes("auth")) {
|
|
771
|
-
return Shield;
|
|
772
|
-
}
|
|
773
|
-
if (toolKind.includes("command") || title.includes("bash") || title.includes("git ") || title.includes("bun ")) {
|
|
774
|
-
return TerminalSquare;
|
|
775
|
-
}
|
|
776
|
-
return Wrench;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function AttachmentPills({
|
|
780
|
-
attachments,
|
|
781
|
-
onRemove,
|
|
782
|
-
}: {
|
|
783
|
-
attachments: string[];
|
|
784
|
-
onRemove?: (index: number) => void;
|
|
785
|
-
}) {
|
|
786
|
-
if (!attachments.length) return null;
|
|
787
|
-
|
|
788
|
-
return (
|
|
789
|
-
<div className="mt-3 flex flex-wrap gap-2">
|
|
790
|
-
{attachments.map((attachment, index) => {
|
|
791
|
-
const label = attachment.split("/").pop() || attachment;
|
|
792
|
-
|
|
793
|
-
if (onRemove) {
|
|
794
|
-
return (
|
|
795
|
-
<button
|
|
796
|
-
key={`${attachment}-${index}`}
|
|
797
|
-
type="button"
|
|
798
|
-
onClick={() => onRemove(index)}
|
|
799
|
-
className="rounded-[3px] border border-[#333] bg-[#1c1c1c] px-2 py-1 text-[12px] leading-[18px] text-[#c4c4c4] transition hover:bg-[#292929]"
|
|
800
|
-
>
|
|
801
|
-
{label}
|
|
802
|
-
</button>
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
return (
|
|
807
|
-
<span
|
|
808
|
-
key={`${attachment}-${index}`}
|
|
809
|
-
className="rounded-[3px] border border-[#333] bg-[#1c1c1c] px-2 py-1 text-[12px] leading-[18px] text-[#c4c4c4]"
|
|
810
|
-
>
|
|
811
|
-
{label}
|
|
812
|
-
</span>
|
|
813
|
-
);
|
|
814
|
-
})}
|
|
815
|
-
</div>
|
|
816
|
-
);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function SummaryChip({ summary }: { summary: string }) {
|
|
820
|
-
const parsed = parseComposerSummary(summary);
|
|
821
|
-
|
|
822
|
-
return (
|
|
823
|
-
<div className="inline-flex min-h-[29px] items-center gap-1 rounded-[3px] bg-[#292929] px-3 py-[5px] text-[14px] leading-[21px] text-[#c4c4c4]">
|
|
824
|
-
<span>{parsed.label}</span>
|
|
825
|
-
{parsed.additions ? <span className="text-[#54b04f]">{parsed.additions}</span> : null}
|
|
826
|
-
{parsed.deletions ? <span className="text-[#d25151]">{parsed.deletions}</span> : null}
|
|
827
|
-
</div>
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function ParserStateBanner({
|
|
832
|
-
kind,
|
|
833
|
-
message,
|
|
834
|
-
command,
|
|
835
|
-
}: {
|
|
836
|
-
kind: string;
|
|
837
|
-
message: string;
|
|
838
|
-
command: string | null;
|
|
839
|
-
}) {
|
|
840
|
-
const title = kind === "auth_required"
|
|
841
|
-
? "Authentication required"
|
|
842
|
-
: kind === "interactive_required"
|
|
843
|
-
? "Terminal interaction required"
|
|
844
|
-
: "Waiting for input";
|
|
845
|
-
const border = kind === "auth_required" ? "border-[#6b5533] bg-[rgba(234,122,42,0.12)] text-[#f1c49f]" : "border-[#36506b] bg-[rgba(68,114,164,0.12)] text-[#bfd5ee]";
|
|
846
|
-
|
|
847
|
-
return (
|
|
848
|
-
<div className={`rounded-[3px] border px-3 py-2 ${border}`}>
|
|
849
|
-
<div className="flex items-start gap-3">
|
|
850
|
-
<div className="flex h-[20px] w-[20px] items-center justify-center pt-[1px]">
|
|
851
|
-
<TerminalSquare className="h-[15px] w-[15px]" strokeWidth={1.6} />
|
|
852
|
-
</div>
|
|
853
|
-
<div className="min-w-0 space-y-1">
|
|
854
|
-
<p className="text-[13px] font-medium leading-[20px]">{title}</p>
|
|
855
|
-
<p className="whitespace-pre-wrap break-words text-[13px] leading-[20px]">{message}</p>
|
|
856
|
-
{command ? (
|
|
857
|
-
<p className="text-[12px] leading-[18px] text-[#c4c4c4]">
|
|
858
|
-
Run locally: <code className="rounded bg-[rgba(0,0,0,0.22)] px-1 py-[1px] whitespace-pre-wrap break-all text-[#f1f1f1] [overflow-wrap:anywhere]">{command}</code>
|
|
859
|
-
</p>
|
|
860
|
-
) : null}
|
|
861
|
-
</div>
|
|
862
|
-
</div>
|
|
863
|
-
</div>
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
function UserEntryCard({ entry }: { entry: NormalizedChatEntry }) {
|
|
868
|
-
const timestamp = formatTimestamp(entry.createdAt);
|
|
869
|
-
|
|
870
|
-
return (
|
|
871
|
-
<div className="space-y-3">
|
|
872
|
-
<div className="flex items-center gap-2">
|
|
873
|
-
<UserRound className="h-[15px] w-[15px] shrink-0 text-[#8f8f8f]" strokeWidth={1.7} />
|
|
874
|
-
<span className="text-[14px] leading-[21px] text-[#c4c4c4]">You</span>
|
|
875
|
-
{timestamp ? (
|
|
876
|
-
<span className="text-[12px] leading-[18px] text-[#5f5f5f]">{timestamp}</span>
|
|
877
|
-
) : null}
|
|
878
|
-
</div>
|
|
879
|
-
<MarkdownBlock
|
|
880
|
-
text={entry.text}
|
|
881
|
-
className="prose prose-invert max-w-none text-[16px] leading-[24px] text-[#c4c4c4] prose-headings:text-[#c4c4c4] prose-p:my-0 prose-p:text-[#c4c4c4] prose-pre:my-3 prose-pre:overflow-x-auto prose-pre:rounded-[3px] prose-pre:border prose-pre:border-[#333] prose-pre:bg-[#1c1c1c] prose-code:text-[#c4c4c4]"
|
|
882
|
-
/>
|
|
883
|
-
<AttachmentPills attachments={entry.attachments} />
|
|
884
|
-
</div>
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function StatusEntry({ entry }: { entry: NormalizedChatEntry }) {
|
|
889
|
-
const presentation = getStatusPresentation(entry.text);
|
|
890
|
-
const commandLike = isCommandLikeStatus(entry.text);
|
|
891
|
-
|
|
892
|
-
if (!presentation.compact) {
|
|
893
|
-
return (
|
|
894
|
-
<div className="space-y-3">
|
|
895
|
-
<MarkdownBlock
|
|
896
|
-
text={entry.text}
|
|
897
|
-
className="prose prose-invert max-w-none text-[16px] leading-[24px] text-[#c4c4c4] prose-headings:text-[#c4c4c4] prose-p:my-0 prose-p:text-[#c4c4c4] prose-pre:my-3 prose-pre:overflow-x-auto prose-pre:rounded-[3px] prose-pre:border prose-pre:border-[#333] prose-pre:bg-[#1c1c1c] prose-code:text-[#c4c4c4]"
|
|
898
|
-
/>
|
|
899
|
-
</div>
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
const { Icon } = presentation;
|
|
904
|
-
|
|
905
|
-
return (
|
|
906
|
-
<div className="flex items-start gap-2 py-[2px]">
|
|
907
|
-
<div className="relative flex h-[22px] w-[20px] items-start pt-[2px] text-[#8f8f8f]">
|
|
908
|
-
<Icon className="h-[20px] w-[20px]" strokeWidth={1.6} />
|
|
909
|
-
{presentation.showDot ? (
|
|
910
|
-
<span className="absolute bottom-0 left-[-2px] h-[6px] w-[6px] rounded-full bg-[#54b04f]" />
|
|
911
|
-
) : null}
|
|
912
|
-
</div>
|
|
913
|
-
<p className={`min-w-0 flex-1 whitespace-pre-wrap break-words text-[#8f8f8f] ${commandLike ? "font-mono text-[13px] leading-[20px]" : "text-[14px] leading-[21px]"}`}>
|
|
914
|
-
{entry.text}
|
|
915
|
-
</p>
|
|
916
|
-
</div>
|
|
917
|
-
);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
function ToolEntry({ entry }: { entry: NormalizedChatEntry }) {
|
|
921
|
-
const [expanded, setExpanded] = useState(false);
|
|
922
|
-
const content = extractToolContent(entry);
|
|
923
|
-
const inlineSummary = getToolInlineSummary(entry, content);
|
|
924
|
-
const toolTitle = typeof entry.metadata?.toolTitle === "string" && entry.metadata.toolTitle.trim().length > 0
|
|
925
|
-
? normalizeChatText(entry.metadata.toolTitle).trim()
|
|
926
|
-
: normalizeChatText(entry.text).trim() || "Tool call";
|
|
927
|
-
const toolStatus = typeof entry.metadata?.toolStatus === "string" ? entry.metadata.toolStatus : null;
|
|
928
|
-
const statusTone = getToolStatusTone(toolStatus);
|
|
929
|
-
const Icon = getToolIcon(entry);
|
|
930
|
-
const statusIndicator = statusTone === "running"
|
|
931
|
-
? (
|
|
932
|
-
<span
|
|
933
|
-
className="h-[9px] w-[9px] shrink-0 rounded-full border-2 border-[rgba(148,163,184,0.35)] border-t-[rgba(148,163,184,0.95)] animate-spin"
|
|
934
|
-
aria-hidden="true"
|
|
935
|
-
/>
|
|
936
|
-
)
|
|
937
|
-
: (
|
|
938
|
-
<span
|
|
939
|
-
className={[
|
|
940
|
-
"h-[9px] w-[9px] shrink-0 rounded-full",
|
|
941
|
-
statusTone === "success"
|
|
942
|
-
? "bg-[var(--vk-green)]"
|
|
943
|
-
: statusTone === "error"
|
|
944
|
-
? "bg-[var(--vk-red)]"
|
|
945
|
-
: statusTone === "cancelled"
|
|
946
|
-
? "bg-[var(--vk-text-muted)]"
|
|
947
|
-
: "bg-[rgba(148,163,184,0.7)]",
|
|
948
|
-
].join(" ")}
|
|
949
|
-
aria-hidden="true"
|
|
950
|
-
/>
|
|
951
|
-
);
|
|
952
|
-
|
|
953
|
-
return (
|
|
954
|
-
<div className={`group ${expanded ? "is-expanded" : ""}`}>
|
|
955
|
-
<button
|
|
956
|
-
type="button"
|
|
957
|
-
onClick={() => setExpanded((current) => !current)}
|
|
958
|
-
className="flex w-full items-start gap-2 py-1 text-left text-[var(--vk-text-muted)] transition hover:text-[var(--vk-text-normal)]"
|
|
959
|
-
>
|
|
960
|
-
{statusIndicator}
|
|
961
|
-
<Icon className="mt-[2px] h-[14px] w-[14px] shrink-0 text-[var(--vk-text-muted)]" strokeWidth={1.7} />
|
|
962
|
-
<div className="min-w-0 flex-1">
|
|
963
|
-
<div className="flex min-w-0 flex-col items-start gap-1 sm:flex-row sm:flex-wrap sm:items-center sm:gap-2">
|
|
964
|
-
<span className="min-w-0 max-w-full break-words text-[13px] font-medium leading-[20px] text-[#d0d0d0] sm:text-[14px] sm:leading-[21px]">
|
|
965
|
-
{toolTitle}
|
|
966
|
-
</span>
|
|
967
|
-
{inlineSummary ? (
|
|
968
|
-
<span className="min-w-0 w-full whitespace-pre-wrap break-all rounded-[3px] bg-[rgba(255,255,255,0.06)] px-2 py-[2px] font-mono text-[11px] leading-[17px] text-[#a9a9a9] [overflow-wrap:anywhere] sm:w-auto sm:max-w-full sm:text-[12px] sm:leading-[18px]">
|
|
969
|
-
{inlineSummary}
|
|
970
|
-
</span>
|
|
971
|
-
) : null}
|
|
972
|
-
</div>
|
|
973
|
-
</div>
|
|
974
|
-
<ChevronRight
|
|
975
|
-
className={`mt-[2px] h-[14px] w-[14px] shrink-0 text-[var(--vk-text-muted)] transition-all duration-200 ${
|
|
976
|
-
expanded ? "rotate-90 opacity-100" : "opacity-0 group-hover:opacity-100"
|
|
977
|
-
}`}
|
|
978
|
-
strokeWidth={1.8}
|
|
979
|
-
/>
|
|
980
|
-
</button>
|
|
981
|
-
{expanded ? (
|
|
982
|
-
<div className="mt-2 max-h-[300px] overflow-y-auto border-t border-[var(--vk-border)] bg-[var(--vk-bg-surface)] px-3 py-3">
|
|
983
|
-
{content.length > 0 ? (
|
|
984
|
-
<div className="space-y-2">
|
|
985
|
-
{content
|
|
986
|
-
.filter((line, index) => !(inlineSummary && index === 0 && line.trim() === inlineSummary))
|
|
987
|
-
.map((line, index) => (
|
|
988
|
-
<p
|
|
989
|
-
key={`${entry.id}-tool-line-${index}`}
|
|
990
|
-
className="whitespace-pre-wrap break-words font-mono text-[12px] leading-[18px] text-[var(--vk-text-normal)]"
|
|
991
|
-
>
|
|
992
|
-
{line}
|
|
993
|
-
</p>
|
|
994
|
-
))}
|
|
995
|
-
</div>
|
|
996
|
-
) : null}
|
|
997
|
-
</div>
|
|
998
|
-
) : null}
|
|
999
|
-
</div>
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
type FeedRenderItem =
|
|
1004
|
-
| { kind: "entry"; entry: NormalizedChatEntry }
|
|
1005
|
-
| { kind: "tool-group"; id: string; entries: NormalizedChatEntry[] };
|
|
1006
|
-
|
|
1007
|
-
function buildFeedRenderItems(entries: NormalizedChatEntry[]): FeedRenderItem[] {
|
|
1008
|
-
const items: FeedRenderItem[] = [];
|
|
1009
|
-
let toolBuffer: NormalizedChatEntry[] = [];
|
|
1010
|
-
|
|
1011
|
-
const flushTools = () => {
|
|
1012
|
-
if (toolBuffer.length === 0) return;
|
|
1013
|
-
items.push({
|
|
1014
|
-
kind: "tool-group",
|
|
1015
|
-
id: `tool-group-${toolBuffer[0]?.id ?? items.length}`,
|
|
1016
|
-
entries: toolBuffer,
|
|
1017
|
-
});
|
|
1018
|
-
toolBuffer = [];
|
|
1019
|
-
};
|
|
1020
|
-
|
|
1021
|
-
for (const entry of entries) {
|
|
1022
|
-
if (entry.kind === "tool") {
|
|
1023
|
-
toolBuffer.push(entry);
|
|
1024
|
-
continue;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
flushTools();
|
|
1028
|
-
items.push({ kind: "entry", entry });
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
flushTools();
|
|
1032
|
-
return items;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
function ToolGroup({
|
|
1036
|
-
entries,
|
|
1037
|
-
autoCollapsed,
|
|
1038
|
-
}: {
|
|
1039
|
-
entries: NormalizedChatEntry[];
|
|
1040
|
-
autoCollapsed: boolean;
|
|
1041
|
-
}) {
|
|
1042
|
-
const [expanded, setExpanded] = useState(!autoCollapsed);
|
|
1043
|
-
const countLabel = `${entries.length} tool call${entries.length === 1 ? "" : "s"}`;
|
|
1044
|
-
|
|
1045
|
-
useEffect(() => {
|
|
1046
|
-
if (autoCollapsed) {
|
|
1047
|
-
setExpanded(false);
|
|
1048
|
-
}
|
|
1049
|
-
}, [autoCollapsed]);
|
|
1050
|
-
|
|
1051
|
-
return (
|
|
1052
|
-
<div className="space-y-3">
|
|
1053
|
-
<button
|
|
1054
|
-
type="button"
|
|
1055
|
-
onClick={() => setExpanded((current) => !current)}
|
|
1056
|
-
className="flex items-center gap-2 text-[15px] leading-[22px] text-[#9b9b9b] transition hover:text-[#c4c4c4]"
|
|
1057
|
-
>
|
|
1058
|
-
<ChevronDown
|
|
1059
|
-
className={`h-[14px] w-[14px] transition-transform ${expanded ? "" : "-rotate-90"}`}
|
|
1060
|
-
strokeWidth={1.8}
|
|
1061
|
-
/>
|
|
1062
|
-
<span>{countLabel}</span>
|
|
1063
|
-
</button>
|
|
1064
|
-
{expanded ? (
|
|
1065
|
-
<div className="space-y-1 pl-3 sm:pl-5">
|
|
1066
|
-
{entries.map((entry) => (
|
|
1067
|
-
<ToolEntry key={entry.id} entry={entry} />
|
|
1068
|
-
))}
|
|
1069
|
-
</div>
|
|
1070
|
-
) : null}
|
|
1071
|
-
</div>
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
function AssistantEntry({
|
|
1076
|
-
entry,
|
|
1077
|
-
agentName,
|
|
1078
|
-
}: {
|
|
1079
|
-
entry: NormalizedChatEntry;
|
|
1080
|
-
agentName: string;
|
|
1081
|
-
}) {
|
|
1082
|
-
const timestamp = formatTimestamp(entry.createdAt);
|
|
1083
|
-
const reviewBlock = parseReviewContent(entry.text);
|
|
1084
|
-
const outlineSections = reviewBlock ? null : parseOutlineContent(entry.text);
|
|
1085
|
-
|
|
1086
|
-
return (
|
|
1087
|
-
<div className="space-y-4">
|
|
1088
|
-
{entry.streaming ? (
|
|
1089
|
-
<div className="flex items-center gap-2 text-[#8f8f8f]">
|
|
1090
|
-
{agentName ? (
|
|
1091
|
-
<AgentTileIcon seed={{ label: agentName }} className="h-[20px] w-[20px] border-none bg-transparent" />
|
|
1092
|
-
) : (
|
|
1093
|
-
<Code2 className="h-[20px] w-[20px]" strokeWidth={1.6} />
|
|
1094
|
-
)}
|
|
1095
|
-
<span className="text-[14px] leading-[21px]">Thinking</span>
|
|
1096
|
-
</div>
|
|
1097
|
-
) : null}
|
|
1098
|
-
{reviewBlock ? (
|
|
1099
|
-
<ReviewFindingsBlock text={entry.text} />
|
|
1100
|
-
) : outlineSections ? (
|
|
1101
|
-
<OutlineSummaryBlock sections={outlineSections} />
|
|
1102
|
-
) : (
|
|
1103
|
-
<MarkdownBlock
|
|
1104
|
-
text={entry.text}
|
|
1105
|
-
className="prose prose-invert max-w-none text-[16px] leading-[28px] text-[#cfcfcf] prose-headings:mb-3 prose-headings:mt-6 prose-headings:text-[#f1f1f1] prose-p:my-4 prose-p:text-[#cfcfcf] prose-strong:text-[#f1f1f1] prose-ol:my-4 prose-ol:pl-6 prose-ul:my-4 prose-ul:pl-6 prose-li:my-2 prose-li:text-[#cfcfcf] prose-pre:my-4 prose-pre:overflow-x-auto prose-pre:rounded-[6px] prose-pre:border prose-pre:border-[#333] prose-pre:bg-[#1c1c1c] prose-code:text-[#d7d7d7] prose-blockquote:border-l prose-blockquote:border-[#333] prose-blockquote:pl-4 prose-blockquote:text-[#b7b7b7]"
|
|
1106
|
-
/>
|
|
1107
|
-
)}
|
|
1108
|
-
<AttachmentPills attachments={entry.attachments} />
|
|
1109
|
-
{timestamp ? (
|
|
1110
|
-
<p className="text-[12px] leading-[18px] text-[#5f5f5f]">{timestamp}</p>
|
|
1111
|
-
) : null}
|
|
1112
|
-
</div>
|
|
1113
|
-
);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
function asOptionalMetadataText(value: unknown): string | null {
|
|
1117
|
-
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
function extractSessionPreferenceUpdate(entry: NormalizedChatEntry) {
|
|
1121
|
-
const eventType = asOptionalMetadataText(entry.metadata?.eventType);
|
|
1122
|
-
if (eventType !== "session_preferences_updated" && entry.source !== "session_preferences") {
|
|
1123
|
-
return null;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
const previousModel = asOptionalMetadataText(entry.metadata?.previousModel);
|
|
1127
|
-
const nextModel = asOptionalMetadataText(entry.metadata?.model);
|
|
1128
|
-
const previousReasoningEffort = asOptionalMetadataText(entry.metadata?.previousReasoningEffort);
|
|
1129
|
-
const nextReasoningEffort = asOptionalMetadataText(entry.metadata?.reasoningEffort);
|
|
1130
|
-
const modelChanged = typeof entry.metadata?.modelChanged === "boolean"
|
|
1131
|
-
? entry.metadata.modelChanged
|
|
1132
|
-
: Boolean(nextModel && nextModel !== previousModel);
|
|
1133
|
-
const reasoningChanged = typeof entry.metadata?.reasoningChanged === "boolean"
|
|
1134
|
-
? entry.metadata.reasoningChanged
|
|
1135
|
-
: Boolean(nextReasoningEffort && nextReasoningEffort !== previousReasoningEffort);
|
|
1136
|
-
|
|
1137
|
-
if (!modelChanged && !reasoningChanged) {
|
|
1138
|
-
return null;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
return {
|
|
1142
|
-
previousModel,
|
|
1143
|
-
nextModel,
|
|
1144
|
-
previousReasoningEffort,
|
|
1145
|
-
nextReasoningEffort,
|
|
1146
|
-
modelChanged,
|
|
1147
|
-
reasoningChanged,
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
function ModelEventChip({
|
|
1152
|
-
agentName,
|
|
1153
|
-
model,
|
|
1154
|
-
tone,
|
|
1155
|
-
}: {
|
|
1156
|
-
agentName: string;
|
|
1157
|
-
model: string | null;
|
|
1158
|
-
tone: "muted" | "accent";
|
|
1159
|
-
}) {
|
|
1160
|
-
const palette = tone === "accent"
|
|
1161
|
-
? "border-[#5a452f] bg-[rgba(234,122,42,0.08)] text-[#f2c79f]"
|
|
1162
|
-
: "border-[#384244] bg-[rgba(255,255,255,0.04)] text-[#d6d6d6]";
|
|
1163
|
-
|
|
1164
|
-
return (
|
|
1165
|
-
<span className={`inline-flex items-center gap-2 rounded-full border px-2 py-1 text-[12px] leading-[18px] ${palette}`}>
|
|
1166
|
-
<span>{model ? formatCurrentModelLabel(agentName, model) : "Session default"}</span>
|
|
1167
|
-
</span>
|
|
1168
|
-
);
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
function SessionPreferenceEntry({
|
|
1172
|
-
entry,
|
|
1173
|
-
agentName,
|
|
1174
|
-
}: {
|
|
1175
|
-
entry: NormalizedChatEntry;
|
|
1176
|
-
agentName: string;
|
|
1177
|
-
}) {
|
|
1178
|
-
const update = extractSessionPreferenceUpdate(entry);
|
|
1179
|
-
const timestamp = formatTimestamp(entry.createdAt);
|
|
1180
|
-
|
|
1181
|
-
if (!update) {
|
|
1182
|
-
return null;
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
const heading = update.modelChanged
|
|
1186
|
-
? "Model switched"
|
|
1187
|
-
: "Reasoning updated";
|
|
1188
|
-
|
|
1189
|
-
return (
|
|
1190
|
-
<div className="rounded-[3px] border border-[#5a452f] bg-[rgba(234,122,42,0.08)] px-3 py-3">
|
|
1191
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
1192
|
-
<span className="inline-flex items-center gap-2 text-[12px] font-medium uppercase tracking-[0.18em] text-[#f2c79f]">
|
|
1193
|
-
<ArrowRightLeft className="h-[14px] w-[14px]" strokeWidth={1.8} />
|
|
1194
|
-
{heading}
|
|
1195
|
-
</span>
|
|
1196
|
-
{timestamp ? (
|
|
1197
|
-
<span className="text-[12px] leading-[18px] text-[#b38358]">{timestamp}</span>
|
|
1198
|
-
) : null}
|
|
1199
|
-
</div>
|
|
1200
|
-
|
|
1201
|
-
<div className="mt-3 space-y-3">
|
|
1202
|
-
{update.modelChanged ? (
|
|
1203
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
1204
|
-
<ModelEventChip agentName={agentName} model={update.previousModel} tone="muted" />
|
|
1205
|
-
<ChevronRight className="h-[14px] w-[14px] text-[#b38358]" strokeWidth={1.8} />
|
|
1206
|
-
<ModelEventChip agentName={agentName} model={update.nextModel} tone="accent" />
|
|
1207
|
-
</div>
|
|
1208
|
-
) : null}
|
|
1209
|
-
{update.reasoningChanged ? (
|
|
1210
|
-
<div className="flex flex-wrap items-center gap-2 text-[12px] leading-[18px] text-[#efd8c1]">
|
|
1211
|
-
<span className="rounded-full border border-[#384244] bg-[rgba(255,255,255,0.04)] px-2 py-1">
|
|
1212
|
-
{formatReasoningLabel(update.previousReasoningEffort ?? "")}
|
|
1213
|
-
</span>
|
|
1214
|
-
<ChevronRight className="h-[14px] w-[14px] text-[#b38358]" strokeWidth={1.8} />
|
|
1215
|
-
<span className="rounded-full border border-[#5a452f] bg-[rgba(234,122,42,0.08)] px-2 py-1 text-[#f2c79f]">
|
|
1216
|
-
{formatReasoningLabel(update.nextReasoningEffort ?? "")}
|
|
1217
|
-
</span>
|
|
1218
|
-
</div>
|
|
1219
|
-
) : null}
|
|
1220
|
-
</div>
|
|
1221
|
-
</div>
|
|
1222
|
-
);
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
function SystemEntry({
|
|
1226
|
-
entry,
|
|
1227
|
-
agentName,
|
|
1228
|
-
}: {
|
|
1229
|
-
entry: NormalizedChatEntry;
|
|
1230
|
-
agentName: string;
|
|
1231
|
-
}) {
|
|
1232
|
-
const preferenceEntry = extractSessionPreferenceUpdate(entry);
|
|
1233
|
-
if (preferenceEntry) {
|
|
1234
|
-
return <SessionPreferenceEntry entry={entry} agentName={agentName} />;
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
return (
|
|
1238
|
-
<div className="rounded-[3px] border border-[#315434] bg-[rgba(84,176,79,0.08)] px-3 py-2">
|
|
1239
|
-
<p className="text-[12px] uppercase tracking-[0.18em] text-[#8bc886]">System</p>
|
|
1240
|
-
<MarkdownBlock
|
|
1241
|
-
text={entry.text}
|
|
1242
|
-
className="prose prose-invert mt-2 max-w-none text-[14px] leading-[21px] text-[#d8ead6] prose-p:my-0 prose-p:text-[#d8ead6] prose-strong:text-white prose-code:text-[#d8ead6]"
|
|
1243
|
-
/>
|
|
1244
|
-
<AttachmentPills attachments={entry.attachments} />
|
|
1245
|
-
</div>
|
|
1246
|
-
);
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
function FeedEntry({
|
|
1250
|
-
entry,
|
|
1251
|
-
agentName,
|
|
1252
|
-
}: {
|
|
1253
|
-
entry: NormalizedChatEntry;
|
|
1254
|
-
agentName: string;
|
|
1255
|
-
}) {
|
|
1256
|
-
switch (entry.kind) {
|
|
1257
|
-
case "user":
|
|
1258
|
-
return <UserEntryCard entry={entry} />;
|
|
1259
|
-
case "assistant":
|
|
1260
|
-
return <AssistantEntry entry={entry} agentName={agentName} />;
|
|
1261
|
-
case "tool":
|
|
1262
|
-
return <ToolEntry entry={entry} />;
|
|
1263
|
-
case "system":
|
|
1264
|
-
return <SystemEntry entry={entry} agentName={agentName} />;
|
|
1265
|
-
default:
|
|
1266
|
-
return <StatusEntry entry={entry} />;
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
async function postSessionTerminalInterrupt(
|
|
1271
|
-
sessionId: string,
|
|
1272
|
-
): Promise<void> {
|
|
1273
|
-
const response = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/interrupt`, {
|
|
1274
|
-
method: "POST",
|
|
1275
|
-
headers: {
|
|
1276
|
-
"Content-Type": "application/json",
|
|
1277
|
-
},
|
|
1278
|
-
body: JSON.stringify({}),
|
|
1279
|
-
});
|
|
1280
|
-
const data = (await response.json().catch(() => null)) as { error?: string } | null;
|
|
1281
|
-
if (!response.ok) {
|
|
1282
|
-
throw new Error(data?.error ?? `Failed to interrupt session: ${response.status}`);
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
export function ChatPanel({
|
|
1287
|
-
sessionId,
|
|
1288
|
-
agentName,
|
|
1289
|
-
projectId,
|
|
1290
|
-
sessionModel,
|
|
1291
|
-
sessionReasoningEffort,
|
|
1292
|
-
}: ChatPanelProps) {
|
|
1293
|
-
const { agents } = useAgents();
|
|
1294
|
-
const { entries, error, loading, parserState, runtimeStatus, sessionStatus, refresh } = useSessionFeed(sessionId);
|
|
1295
|
-
|
|
1296
|
-
const [message, setMessage] = useState("");
|
|
1297
|
-
const [sending, setSending] = useState(false);
|
|
1298
|
-
const [interrupting, setInterrupting] = useState(false);
|
|
1299
|
-
const [sendError, setSendError] = useState<string | null>(null);
|
|
1300
|
-
const [modelMenuOpen, setModelMenuOpen] = useState(false);
|
|
1301
|
-
const [reasoningMenuOpen, setReasoningMenuOpen] = useState(false);
|
|
1302
|
-
const [selectedModel, setSelectedModel] = useState(sessionModel?.trim() || "");
|
|
1303
|
-
const [customModelInput, setCustomModelInput] = useState("");
|
|
1304
|
-
const [selectedReasoning, setSelectedReasoning] = useState(sessionReasoningEffort?.trim().toLowerCase() || "");
|
|
1305
|
-
const [attachments, setAttachments] = useState<AttachmentDraft[]>([]);
|
|
1306
|
-
const [selectedSlashIndex, setSelectedSlashIndex] = useState(0);
|
|
1307
|
-
|
|
1308
|
-
const endRef = useRef<HTMLDivElement | null>(null);
|
|
1309
|
-
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
|
1310
|
-
const composerRef = useRef<HTMLTextAreaElement | null>(null);
|
|
1311
|
-
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
1312
|
-
const modelMenuRef = useRef<HTMLDivElement | null>(null);
|
|
1313
|
-
const reasoningMenuRef = useRef<HTMLDivElement | null>(null);
|
|
1314
|
-
const shouldStickToBottomRef = useRef(true);
|
|
1315
|
-
const previousScrollTopRef = useRef(0);
|
|
1316
|
-
const normalizedAgentName = agentName?.trim() || "";
|
|
1317
|
-
const normalizedSessionStatus = sessionStatus?.trim().toLowerCase() ?? "";
|
|
1318
|
-
const runtimeCatalog = useMemo(
|
|
1319
|
-
() => findRuntimeCatalog(agents, normalizedAgentName),
|
|
1320
|
-
[agents, normalizedAgentName],
|
|
1321
|
-
);
|
|
1322
|
-
const modelOptions = useMemo(
|
|
1323
|
-
() => getModelOptions(runtimeCatalog, normalizedAgentName, sessionModel?.trim() || ""),
|
|
1324
|
-
[runtimeCatalog, normalizedAgentName, sessionModel],
|
|
1325
|
-
);
|
|
1326
|
-
const router = useRouter();
|
|
1327
|
-
const modelPlaceholder = useMemo(
|
|
1328
|
-
() => getCustomModelPlaceholder(normalizedAgentName, runtimeCatalog),
|
|
1329
|
-
[normalizedAgentName, runtimeCatalog],
|
|
1330
|
-
);
|
|
1331
|
-
const resolvedModelValue = selectedModel.trim();
|
|
1332
|
-
const reasoningOptions = useMemo(
|
|
1333
|
-
() => getReasoningOptions(
|
|
1334
|
-
normalizedAgentName,
|
|
1335
|
-
runtimeCatalog,
|
|
1336
|
-
resolvedModelValue || sessionModel?.trim() || "",
|
|
1337
|
-
),
|
|
1338
|
-
[normalizedAgentName, runtimeCatalog, resolvedModelValue, sessionModel],
|
|
1339
|
-
);
|
|
1340
|
-
const defaultReasoningSelection = useMemo(
|
|
1341
|
-
() => getDefaultReasoningSelection(
|
|
1342
|
-
normalizedAgentName,
|
|
1343
|
-
runtimeCatalog,
|
|
1344
|
-
resolvedModelValue || sessionModel?.trim() || "",
|
|
1345
|
-
sessionReasoningEffort?.trim() || "",
|
|
1346
|
-
),
|
|
1347
|
-
[normalizedAgentName, runtimeCatalog, resolvedModelValue, sessionModel, sessionReasoningEffort],
|
|
1348
|
-
);
|
|
1349
|
-
|
|
1350
|
-
useEffect(() => {
|
|
1351
|
-
setSelectedModel(sessionModel?.trim() || "");
|
|
1352
|
-
setCustomModelInput(sessionModel?.trim() || "");
|
|
1353
|
-
}, [sessionModel]);
|
|
1354
|
-
|
|
1355
|
-
useEffect(() => {
|
|
1356
|
-
setSelectedReasoning(sessionReasoningEffort?.trim().toLowerCase() || "");
|
|
1357
|
-
}, [sessionReasoningEffort]);
|
|
1358
|
-
|
|
1359
|
-
useEffect(() => {
|
|
1360
|
-
if (!selectedModel.trim()) {
|
|
1361
|
-
setCustomModelInput("");
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
if (modelOptions.some((option) => option.id === selectedModel.trim())) {
|
|
1365
|
-
setCustomModelInput("");
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
setCustomModelInput(selectedModel.trim());
|
|
1369
|
-
}, [modelOptions, selectedModel]);
|
|
1370
|
-
|
|
1371
|
-
useEffect(() => {
|
|
1372
|
-
if (!selectedReasoning) {
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
if (reasoningOptions.some((option) => option.id === selectedReasoning)) {
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
setSelectedReasoning(defaultReasoningSelection || "");
|
|
1379
|
-
}, [defaultReasoningSelection, reasoningOptions, selectedReasoning]);
|
|
1380
|
-
|
|
1381
|
-
// Clear transient error state when the session changes.
|
|
1382
|
-
useEffect(() => {
|
|
1383
|
-
setSendError(null);
|
|
1384
|
-
}, [sessionId]);
|
|
1385
|
-
|
|
1386
|
-
// Close model/reasoning menus on click outside.
|
|
1387
|
-
useEffect(() => {
|
|
1388
|
-
if (!modelMenuOpen && !reasoningMenuOpen) return;
|
|
1389
|
-
function onClickOutside(event: globalThis.MouseEvent) {
|
|
1390
|
-
if (modelMenuRef.current && !modelMenuRef.current.contains(event.target as Node)) {
|
|
1391
|
-
setModelMenuOpen(false);
|
|
1392
|
-
}
|
|
1393
|
-
if (reasoningMenuRef.current && !reasoningMenuRef.current.contains(event.target as Node)) {
|
|
1394
|
-
setReasoningMenuOpen(false);
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
document.addEventListener("mousedown", onClickOutside);
|
|
1398
|
-
return () => document.removeEventListener("mousedown", onClickOutside);
|
|
1399
|
-
}, [modelMenuOpen, reasoningMenuOpen]);
|
|
1400
|
-
|
|
1401
|
-
const displayEntries = useMemo(
|
|
1402
|
-
() => {
|
|
1403
|
-
if (normalizedSessionStatus !== "working" && normalizedSessionStatus !== "running") {
|
|
1404
|
-
return entries;
|
|
1405
|
-
}
|
|
1406
|
-
return entries.filter((entry) => !(entry.kind === "status" && entry.source === "session-status"));
|
|
1407
|
-
},
|
|
1408
|
-
[entries, normalizedSessionStatus],
|
|
1409
|
-
);
|
|
1410
|
-
|
|
1411
|
-
const hasStreamingEntry = displayEntries.some((entry) => entry.streaming);
|
|
1412
|
-
const isSessionRunning = normalizedSessionStatus === "running" || normalizedSessionStatus === "working" || hasStreamingEntry;
|
|
1413
|
-
const hasLiveTerminalSession = normalizedSessionStatus === "running"
|
|
1414
|
-
|| normalizedSessionStatus === "working"
|
|
1415
|
-
|| normalizedSessionStatus === "needs_input"
|
|
1416
|
-
|| normalizedSessionStatus === "stuck";
|
|
1417
|
-
const composerSummary = useMemo(
|
|
1418
|
-
() => extractComposerSummary(displayEntries),
|
|
1419
|
-
[displayEntries],
|
|
1420
|
-
);
|
|
1421
|
-
const slashCommandOptions = useMemo(
|
|
1422
|
-
() => getSlashCommandOptions(normalizedAgentName, message),
|
|
1423
|
-
[message, normalizedAgentName],
|
|
1424
|
-
);
|
|
1425
|
-
const showSlashCommandMenu = slashCommandOptions.length > 0;
|
|
1426
|
-
const feedItems = useMemo(
|
|
1427
|
-
() => buildFeedRenderItems(displayEntries),
|
|
1428
|
-
[displayEntries],
|
|
1429
|
-
);
|
|
1430
|
-
|
|
1431
|
-
const normalizeWhitespaceOnlyDraft = useCallback(() => {
|
|
1432
|
-
setMessage((current) => (current.trim().length === 0 ? "" : current));
|
|
1433
|
-
}, []);
|
|
1434
|
-
|
|
1435
|
-
const updateBottomStickiness = useCallback(() => {
|
|
1436
|
-
const container = scrollContainerRef.current;
|
|
1437
|
-
if (!container) {
|
|
1438
|
-
shouldStickToBottomRef.current = true;
|
|
1439
|
-
return;
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
previousScrollTopRef.current = container.scrollTop;
|
|
1443
|
-
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
1444
|
-
shouldStickToBottomRef.current = distanceFromBottom <= 16;
|
|
1445
|
-
}, []);
|
|
1446
|
-
|
|
1447
|
-
const scrollToBottom = useCallback((behavior: ScrollBehavior) => {
|
|
1448
|
-
shouldStickToBottomRef.current = true;
|
|
1449
|
-
const container = scrollContainerRef.current;
|
|
1450
|
-
if (!container) {
|
|
1451
|
-
endRef.current?.scrollIntoView({ behavior, block: "end" });
|
|
1452
|
-
return;
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
window.requestAnimationFrame(() => {
|
|
1456
|
-
container.scrollTo({
|
|
1457
|
-
top: container.scrollHeight,
|
|
1458
|
-
behavior,
|
|
1459
|
-
});
|
|
1460
|
-
});
|
|
1461
|
-
}, []);
|
|
1462
|
-
|
|
1463
|
-
useEffect(() => {
|
|
1464
|
-
shouldStickToBottomRef.current = true;
|
|
1465
|
-
previousScrollTopRef.current = 0;
|
|
1466
|
-
}, [sessionId]);
|
|
1467
|
-
|
|
1468
|
-
useLayoutEffect(() => {
|
|
1469
|
-
const container = scrollContainerRef.current;
|
|
1470
|
-
if (!container) {
|
|
1471
|
-
return;
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
if (shouldStickToBottomRef.current) {
|
|
1475
|
-
previousScrollTopRef.current = container.scrollTop;
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
container.scrollTop = previousScrollTopRef.current;
|
|
1480
|
-
}, [feedItems, hasStreamingEntry, parserState, runtimeStatus, sending]);
|
|
1481
|
-
|
|
1482
|
-
useEffect(() => {
|
|
1483
|
-
const handleVisibilityChange = () => {
|
|
1484
|
-
if (document.hidden) {
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
normalizeWhitespaceOnlyDraft();
|
|
1488
|
-
};
|
|
1489
|
-
|
|
1490
|
-
const handleWindowFocus = () => {
|
|
1491
|
-
normalizeWhitespaceOnlyDraft();
|
|
1492
|
-
};
|
|
1493
|
-
|
|
1494
|
-
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
1495
|
-
window.addEventListener("focus", handleWindowFocus);
|
|
1496
|
-
return () => {
|
|
1497
|
-
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
1498
|
-
window.removeEventListener("focus", handleWindowFocus);
|
|
1499
|
-
};
|
|
1500
|
-
}, [normalizeWhitespaceOnlyDraft]);
|
|
1501
|
-
|
|
1502
|
-
const prevEntryCountRef = useRef(0);
|
|
1503
|
-
useEffect(() => {
|
|
1504
|
-
const count = displayEntries.length;
|
|
1505
|
-
const countChanged = count !== prevEntryCountRef.current;
|
|
1506
|
-
if ((countChanged || hasStreamingEntry || sending) && shouldStickToBottomRef.current) {
|
|
1507
|
-
scrollToBottom(hasStreamingEntry ? "auto" : "smooth");
|
|
1508
|
-
}
|
|
1509
|
-
prevEntryCountRef.current = count;
|
|
1510
|
-
}, [displayEntries, hasStreamingEntry, scrollToBottom, sending]);
|
|
1511
|
-
|
|
1512
|
-
useEffect(() => {
|
|
1513
|
-
if (!showSlashCommandMenu) {
|
|
1514
|
-
setSelectedSlashIndex(0);
|
|
1515
|
-
return;
|
|
1516
|
-
}
|
|
1517
|
-
setSelectedSlashIndex((current) => Math.min(current, Math.max(slashCommandOptions.length - 1, 0)));
|
|
1518
|
-
}, [showSlashCommandMenu, slashCommandOptions.length]);
|
|
1519
|
-
|
|
1520
|
-
const selectedModelLabel = useMemo(() => {
|
|
1521
|
-
if (!selectedModel) {
|
|
1522
|
-
return sessionModel?.trim()
|
|
1523
|
-
? formatCurrentModelLabel(normalizedAgentName, sessionModel)
|
|
1524
|
-
: "Session default";
|
|
1525
|
-
}
|
|
1526
|
-
return modelOptions.find((option) => option.id === selectedModel)?.label ?? selectedModel;
|
|
1527
|
-
}, [modelOptions, normalizedAgentName, selectedModel, sessionModel]);
|
|
1528
|
-
const selectedReasoningLabel = useMemo(() => {
|
|
1529
|
-
if (!selectedReasoning) {
|
|
1530
|
-
return sessionReasoningEffort?.trim()
|
|
1531
|
-
? formatReasoningLabel(sessionReasoningEffort)
|
|
1532
|
-
: defaultReasoningSelection
|
|
1533
|
-
? formatReasoningLabel(defaultReasoningSelection)
|
|
1534
|
-
: "Reasoning";
|
|
1535
|
-
}
|
|
1536
|
-
return reasoningOptions.find((option) => option.id === selectedReasoning)?.label
|
|
1537
|
-
?? formatReasoningLabel(selectedReasoning);
|
|
1538
|
-
}, [defaultReasoningSelection, reasoningOptions, selectedReasoning, sessionReasoningEffort]);
|
|
1539
|
-
|
|
1540
|
-
const handleInterrupt = useCallback(async () => {
|
|
1541
|
-
if (!hasLiveTerminalSession || interrupting) return;
|
|
1542
|
-
|
|
1543
|
-
setInterrupting(true);
|
|
1544
|
-
setSendError(null);
|
|
1545
|
-
|
|
1546
|
-
try {
|
|
1547
|
-
const response = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/interrupt`, {
|
|
1548
|
-
method: "POST",
|
|
1549
|
-
});
|
|
1550
|
-
|
|
1551
|
-
const data = (await response.json().catch(() => null)) as
|
|
1552
|
-
| { error?: string }
|
|
1553
|
-
| null;
|
|
1554
|
-
|
|
1555
|
-
if (!response.ok) {
|
|
1556
|
-
throw new Error(data?.error ?? `Failed to interrupt agent: ${response.status}`);
|
|
1557
|
-
}
|
|
1558
|
-
} catch (err) {
|
|
1559
|
-
setSendError(err instanceof Error ? err.message : "Failed to interrupt agent");
|
|
1560
|
-
} finally {
|
|
1561
|
-
setInterrupting(false);
|
|
1562
|
-
}
|
|
1563
|
-
}, [hasLiveTerminalSession, interrupting, sessionId]);
|
|
1564
|
-
|
|
1565
|
-
const handleSendTerminalSpecial = useCallback(async (special: string) => {
|
|
1566
|
-
if (!hasLiveTerminalSession) return;
|
|
1567
|
-
|
|
1568
|
-
// Only Ctrl+C and Ctrl+D trigger an interrupt via the backend.
|
|
1569
|
-
// All other keys are handled directly by the TTyD WebSocket in the terminal.
|
|
1570
|
-
if (special !== "C-c" && special !== "C-d") return;
|
|
1571
|
-
|
|
1572
|
-
shouldStickToBottomRef.current = true;
|
|
1573
|
-
setSendError(null);
|
|
1574
|
-
|
|
1575
|
-
try {
|
|
1576
|
-
await postSessionTerminalInterrupt(sessionId);
|
|
1577
|
-
} catch (err) {
|
|
1578
|
-
setSendError(err instanceof Error ? err.message : "Failed to interrupt session");
|
|
1579
|
-
}
|
|
1580
|
-
}, [hasLiveTerminalSession, sessionId]);
|
|
1581
|
-
|
|
1582
|
-
const handleSend = useCallback(async () => {
|
|
1583
|
-
if (isSessionRunning || interrupting) return;
|
|
1584
|
-
const trimmedMessage = message.trim();
|
|
1585
|
-
if (!trimmedMessage && attachments.length === 0) return;
|
|
1586
|
-
|
|
1587
|
-
shouldStickToBottomRef.current = true;
|
|
1588
|
-
setSending(true);
|
|
1589
|
-
setSendError(null);
|
|
1590
|
-
|
|
1591
|
-
try {
|
|
1592
|
-
const attachmentPaths = await uploadProjectAttachments({
|
|
1593
|
-
files: attachments.map((attachment) => attachment.file),
|
|
1594
|
-
projectId: projectId ?? "",
|
|
1595
|
-
preferAbsolute: true,
|
|
1596
|
-
});
|
|
1597
|
-
|
|
1598
|
-
const response = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/send`, {
|
|
1599
|
-
method: "POST",
|
|
1600
|
-
headers: {
|
|
1601
|
-
"Content-Type": "application/json",
|
|
1602
|
-
},
|
|
1603
|
-
body: JSON.stringify({
|
|
1604
|
-
message: trimmedMessage,
|
|
1605
|
-
attachments: attachmentPaths,
|
|
1606
|
-
model: selectedModel || null,
|
|
1607
|
-
reasoningEffort: selectedReasoning || null,
|
|
1608
|
-
projectId: projectId || null,
|
|
1609
|
-
}),
|
|
1610
|
-
});
|
|
1611
|
-
|
|
1612
|
-
const data = (await response.json().catch(() => null)) as
|
|
1613
|
-
| { error?: string; sessionId?: string | null }
|
|
1614
|
-
| null;
|
|
1615
|
-
|
|
1616
|
-
if (!response.ok) {
|
|
1617
|
-
throw new Error(data?.error ?? `Failed to send message: ${response.status}`);
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
setMessage("");
|
|
1621
|
-
setAttachments([]);
|
|
1622
|
-
if (data?.sessionId && data.sessionId !== sessionId) {
|
|
1623
|
-
router.push(`/sessions/${encodeURIComponent(data.sessionId)}`);
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
await refresh();
|
|
1627
|
-
} catch (err) {
|
|
1628
|
-
setSendError(err instanceof Error ? err.message : "Failed to send message");
|
|
1629
|
-
} finally {
|
|
1630
|
-
setSending(false);
|
|
1631
|
-
}
|
|
1632
|
-
}, [isSessionRunning, interrupting, message, attachments, sessionId, selectedModel, selectedReasoning, projectId, router, refresh]);
|
|
1633
|
-
|
|
1634
|
-
function applySlashCommand(option: SlashCommandOption) {
|
|
1635
|
-
setMessage(option.command);
|
|
1636
|
-
setSendError(null);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
function handleComposerKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
|
|
1640
|
-
const composerEmpty = message.length === 0 && attachments.length === 0;
|
|
1641
|
-
const shouldPassthroughSpecial =
|
|
1642
|
-
hasLiveTerminalSession && composerEmpty && !showSlashCommandMenu;
|
|
1643
|
-
|
|
1644
|
-
if (showSlashCommandMenu && event.key === "ArrowDown") {
|
|
1645
|
-
event.preventDefault();
|
|
1646
|
-
setSelectedSlashIndex((current) => (
|
|
1647
|
-
current >= slashCommandOptions.length - 1 ? 0 : current + 1
|
|
1648
|
-
));
|
|
1649
|
-
return;
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
if (showSlashCommandMenu && event.key === "ArrowUp") {
|
|
1653
|
-
event.preventDefault();
|
|
1654
|
-
setSelectedSlashIndex((current) => (
|
|
1655
|
-
current <= 0 ? Math.max(slashCommandOptions.length - 1, 0) : current - 1
|
|
1656
|
-
));
|
|
1657
|
-
return;
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey && hasLiveTerminalSession) {
|
|
1661
|
-
if (event.key.toLowerCase() === "c") {
|
|
1662
|
-
event.preventDefault();
|
|
1663
|
-
void handleInterrupt();
|
|
1664
|
-
return;
|
|
1665
|
-
}
|
|
1666
|
-
if (event.key.toLowerCase() === "d") {
|
|
1667
|
-
event.preventDefault();
|
|
1668
|
-
void handleSendTerminalSpecial("C-d");
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
if (shouldPassthroughSpecial && event.key === "Escape") {
|
|
1674
|
-
event.preventDefault();
|
|
1675
|
-
void handleSendTerminalSpecial("Escape");
|
|
1676
|
-
return;
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
1680
|
-
event.preventDefault();
|
|
1681
|
-
if (showSlashCommandMenu) {
|
|
1682
|
-
const selectedOption = slashCommandOptions[selectedSlashIndex];
|
|
1683
|
-
if (selectedOption) {
|
|
1684
|
-
if (selectedOption.exact && !isSessionRunning) {
|
|
1685
|
-
void handleSend();
|
|
1686
|
-
} else {
|
|
1687
|
-
applySlashCommand(selectedOption);
|
|
1688
|
-
}
|
|
1689
|
-
return;
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
if (shouldPassthroughSpecial) {
|
|
1693
|
-
void handleSendTerminalSpecial("Enter");
|
|
1694
|
-
return;
|
|
1695
|
-
}
|
|
1696
|
-
if (!isSessionRunning) {
|
|
1697
|
-
void handleSend();
|
|
1698
|
-
}
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
if (shouldPassthroughSpecial && event.key === "Tab") {
|
|
1703
|
-
event.preventDefault();
|
|
1704
|
-
void handleSendTerminalSpecial("Tab");
|
|
1705
|
-
return;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
if (shouldPassthroughSpecial && (
|
|
1709
|
-
event.key === "ArrowUp"
|
|
1710
|
-
|| event.key === "ArrowDown"
|
|
1711
|
-
|| event.key === "ArrowLeft"
|
|
1712
|
-
|| event.key === "ArrowRight"
|
|
1713
|
-
|| event.key === "Backspace"
|
|
1714
|
-
)) {
|
|
1715
|
-
event.preventDefault();
|
|
1716
|
-
void handleSendTerminalSpecial(event.key);
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
function handleAttachmentChange(event: ChangeEvent<HTMLInputElement>) {
|
|
1721
|
-
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
1722
|
-
const files = Array.from(event.target.files ?? []);
|
|
1723
|
-
if (!files.length) return;
|
|
1724
|
-
|
|
1725
|
-
const validFiles: File[] = [];
|
|
1726
|
-
for (const file of files) {
|
|
1727
|
-
if (file.size > MAX_FILE_SIZE) {
|
|
1728
|
-
setSendError(`File "${file.name}" exceeds 10 MB limit`);
|
|
1729
|
-
continue;
|
|
1730
|
-
}
|
|
1731
|
-
validFiles.push(file);
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
if (validFiles.length > 0) {
|
|
1735
|
-
setAttachments((current) => [
|
|
1736
|
-
...current,
|
|
1737
|
-
...validFiles.map((file) => ({ file })),
|
|
1738
|
-
]);
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
event.target.value = "";
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
return (
|
|
1745
|
-
<div className="relative flex h-full min-h-0 flex-col overflow-hidden bg-[radial-gradient(circle_at_top,rgba(45,45,45,0.32),rgba(33,33,33,1)_40%)] text-[#c4c4c4]">
|
|
1746
|
-
<div className="mx-auto flex h-full min-h-0 w-full max-w-[855px] flex-col">
|
|
1747
|
-
<div
|
|
1748
|
-
ref={scrollContainerRef}
|
|
1749
|
-
onScroll={updateBottomStickiness}
|
|
1750
|
-
onWheelCapture={(event) => {
|
|
1751
|
-
if (event.deltaY < 0) {
|
|
1752
|
-
shouldStickToBottomRef.current = false;
|
|
1753
|
-
}
|
|
1754
|
-
}}
|
|
1755
|
-
className="flex-1 overflow-y-auto px-3 pt-3 sm:px-4 [overflow-anchor:none]"
|
|
1756
|
-
>
|
|
1757
|
-
<div className="mx-auto flex w-full max-w-[768px] flex-col gap-5 pb-6">
|
|
1758
|
-
{loading && displayEntries.length === 0 ? (
|
|
1759
|
-
<div className="flex items-center gap-2 px-4 text-[#8f8f8f]">
|
|
1760
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1761
|
-
<span className="text-[14px] leading-[21px]">Loading conversation</span>
|
|
1762
|
-
</div>
|
|
1763
|
-
) : null}
|
|
1764
|
-
|
|
1765
|
-
{!loading && displayEntries.length === 0 ? (
|
|
1766
|
-
<div className="px-4 py-2 text-[16px] leading-[24px] text-[#8f8f8f]">
|
|
1767
|
-
Start the next turn to stream follow-up work into this panel.
|
|
1768
|
-
</div>
|
|
1769
|
-
) : null}
|
|
1770
|
-
|
|
1771
|
-
{feedItems.map((item) => (
|
|
1772
|
-
item.kind === "tool-group" ? (
|
|
1773
|
-
<ToolGroup key={item.id} entries={item.entries} autoCollapsed={!isSessionRunning} />
|
|
1774
|
-
) : (
|
|
1775
|
-
<FeedEntry key={item.entry.id} entry={item.entry} agentName={normalizedAgentName} />
|
|
1776
|
-
)
|
|
1777
|
-
))}
|
|
1778
|
-
|
|
1779
|
-
{error ? (
|
|
1780
|
-
<div className="rounded-[3px] border border-[#603535] bg-[rgba(210,81,81,0.12)] px-3 py-2 text-[13px] leading-[20px] text-[#f0b5b5]">
|
|
1781
|
-
{error}
|
|
1782
|
-
</div>
|
|
1783
|
-
) : null}
|
|
1784
|
-
|
|
1785
|
-
{sendError ? (
|
|
1786
|
-
<div className="rounded-[3px] border border-[#603535] bg-[rgba(210,81,81,0.12)] px-3 py-2 text-[13px] leading-[20px] text-[#f0b5b5]">
|
|
1787
|
-
{sendError}
|
|
1788
|
-
</div>
|
|
1789
|
-
) : null}
|
|
1790
|
-
|
|
1791
|
-
{parserState ? (
|
|
1792
|
-
<ParserStateBanner
|
|
1793
|
-
kind={parserState.kind}
|
|
1794
|
-
message={parserState.message}
|
|
1795
|
-
command={parserState.command}
|
|
1796
|
-
/>
|
|
1797
|
-
) : null}
|
|
1798
|
-
|
|
1799
|
-
<div ref={endRef} />
|
|
1800
|
-
</div>
|
|
1801
|
-
</div>
|
|
1802
|
-
|
|
1803
|
-
<div className="shrink-0 px-3 pb-4 pt-0 sm:px-4">
|
|
1804
|
-
<div className="mx-auto w-full max-w-[768px]">
|
|
1805
|
-
<div className="rounded-[3px] border border-[#333] bg-[#1c1c1c] shadow-[0_12px_40px_rgba(0,0,0,0.22)]">
|
|
1806
|
-
<div className="flex flex-wrap items-center gap-2 border-b border-[#333] px-2 py-2">
|
|
1807
|
-
{composerSummary ? <SummaryChip summary={composerSummary} /> : <div className="min-h-[29px]" />}
|
|
1808
|
-
<div className="ml-auto flex min-w-0 items-center gap-2">
|
|
1809
|
-
{normalizedAgentName ? (
|
|
1810
|
-
<div className="hidden items-center overflow-hidden sm:flex">
|
|
1811
|
-
<AgentTileIcon seed={{ label: normalizedAgentName }} className="h-[25px] w-[25px] border-none bg-transparent" />
|
|
1812
|
-
</div>
|
|
1813
|
-
) : null}
|
|
1814
|
-
<div className="hidden h-[20px] w-[20px] items-center justify-center text-[#8f8f8f] sm:flex">
|
|
1815
|
-
<Code2 className="h-[15px] w-[15px]" strokeWidth={1.7} />
|
|
1816
|
-
</div>
|
|
1817
|
-
<div className="relative" ref={modelMenuRef}>
|
|
1818
|
-
<button
|
|
1819
|
-
type="button"
|
|
1820
|
-
onClick={() => {
|
|
1821
|
-
setReasoningMenuOpen(false);
|
|
1822
|
-
setModelMenuOpen((open) => !open);
|
|
1823
|
-
}}
|
|
1824
|
-
className="inline-flex min-h-[38px] sm:min-h-[31px] items-center gap-2 rounded-[3px] border border-[#333] bg-[#1c1c1c] px-[9px] py-[5px] text-[14px] leading-[21px] text-[#c4c4c4] transition hover:bg-[#292929]"
|
|
1825
|
-
>
|
|
1826
|
-
<span className="max-w-[140px] truncate">{selectedModelLabel}</span>
|
|
1827
|
-
<ChevronDown className="h-[10px] w-[10px] text-[#8f8f8f]" strokeWidth={1.8} />
|
|
1828
|
-
</button>
|
|
1829
|
-
|
|
1830
|
-
{modelMenuOpen ? (
|
|
1831
|
-
<div className="absolute bottom-[calc(100%+8px)] right-0 z-20 w-[calc(100vw-2rem)] max-w-[320px] sm:w-[320px] overflow-hidden rounded-[4px] border border-[#333] bg-[#1c1c1c] shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
|
|
1832
|
-
<div className="border-b border-[#333] px-3 py-2">
|
|
1833
|
-
<p className="text-[12px] font-medium uppercase tracking-[0.16em] text-[#8f8f8f]">Model</p>
|
|
1834
|
-
<p className="mt-1 text-[12px] leading-[18px] text-[#6f6f6f]">
|
|
1835
|
-
Presets come from the local runtime catalog. You can also force any exact model id.
|
|
1836
|
-
</p>
|
|
1837
|
-
</div>
|
|
1838
|
-
<div className="max-h-72 overflow-y-auto p-1">
|
|
1839
|
-
<button
|
|
1840
|
-
type="button"
|
|
1841
|
-
onClick={() => {
|
|
1842
|
-
setSelectedModel(sessionModel?.trim() || "");
|
|
1843
|
-
setCustomModelInput("");
|
|
1844
|
-
setModelMenuOpen(false);
|
|
1845
|
-
}}
|
|
1846
|
-
className="flex w-full flex-col rounded-[3px] px-3 py-2 text-left transition hover:bg-[#292929]"
|
|
1847
|
-
>
|
|
1848
|
-
<span className="text-[14px] leading-[21px] text-[#c4c4c4]">Session default</span>
|
|
1849
|
-
<span className="text-[12px] leading-[18px] text-[#8f8f8f]">
|
|
1850
|
-
{sessionModel?.trim()
|
|
1851
|
-
? `Use ${formatCurrentModelLabel(normalizedAgentName, sessionModel)} for follow-up turns.`
|
|
1852
|
-
: "Keep using the model configured on this session."}
|
|
1853
|
-
</span>
|
|
1854
|
-
</button>
|
|
1855
|
-
|
|
1856
|
-
{modelOptions.map((option) => (
|
|
1857
|
-
<button
|
|
1858
|
-
key={option.id}
|
|
1859
|
-
type="button"
|
|
1860
|
-
onClick={() => {
|
|
1861
|
-
setSelectedModel(option.id);
|
|
1862
|
-
setCustomModelInput("");
|
|
1863
|
-
setModelMenuOpen(false);
|
|
1864
|
-
}}
|
|
1865
|
-
className={`flex w-full flex-col rounded-[3px] px-3 py-2 text-left transition hover:bg-[#292929] ${selectedModel === option.id ? "bg-[#292929]" : ""}`}
|
|
1866
|
-
>
|
|
1867
|
-
<span className="text-[14px] leading-[21px] text-[#c4c4c4]">{option.label}</span>
|
|
1868
|
-
<span className="text-[12px] leading-[18px] text-[#8f8f8f]">{option.helper}</span>
|
|
1869
|
-
</button>
|
|
1870
|
-
))}
|
|
1871
|
-
</div>
|
|
1872
|
-
<div className="border-t border-[#333] p-3">
|
|
1873
|
-
<label className="mb-2 block text-[12px] leading-[18px] text-[#8f8f8f]">
|
|
1874
|
-
Exact model id
|
|
1875
|
-
</label>
|
|
1876
|
-
<div className="flex items-center gap-2">
|
|
1877
|
-
<input
|
|
1878
|
-
value={customModelInput}
|
|
1879
|
-
onChange={(event) => setCustomModelInput(event.target.value)}
|
|
1880
|
-
onKeyDown={(event) => {
|
|
1881
|
-
if (event.key !== "Enter") return;
|
|
1882
|
-
event.preventDefault();
|
|
1883
|
-
const nextModel = customModelInput.trim();
|
|
1884
|
-
if (!nextModel) return;
|
|
1885
|
-
setSelectedModel(nextModel);
|
|
1886
|
-
setModelMenuOpen(false);
|
|
1887
|
-
}}
|
|
1888
|
-
placeholder={modelPlaceholder}
|
|
1889
|
-
className="min-w-0 flex-1 rounded-[3px] border border-[#333] bg-[#171717] px-3 py-2 text-[13px] leading-[20px] text-[#d0d0d0] outline-none placeholder:text-[#666] focus:border-[#4a4a4a]"
|
|
1890
|
-
/>
|
|
1891
|
-
<button
|
|
1892
|
-
type="button"
|
|
1893
|
-
onClick={() => {
|
|
1894
|
-
const nextModel = customModelInput.trim();
|
|
1895
|
-
if (!nextModel) return;
|
|
1896
|
-
setSelectedModel(nextModel);
|
|
1897
|
-
setModelMenuOpen(false);
|
|
1898
|
-
}}
|
|
1899
|
-
className="rounded-[3px] border border-[#333] px-3 py-2 text-[12px] leading-[18px] text-[#c4c4c4] transition hover:bg-[#292929]"
|
|
1900
|
-
>
|
|
1901
|
-
Use
|
|
1902
|
-
</button>
|
|
1903
|
-
</div>
|
|
1904
|
-
</div>
|
|
1905
|
-
</div>
|
|
1906
|
-
) : null}
|
|
1907
|
-
</div>
|
|
1908
|
-
<div className="relative" ref={reasoningMenuRef}>
|
|
1909
|
-
<button
|
|
1910
|
-
type="button"
|
|
1911
|
-
onClick={() => {
|
|
1912
|
-
setModelMenuOpen(false);
|
|
1913
|
-
setReasoningMenuOpen((open) => !open);
|
|
1914
|
-
}}
|
|
1915
|
-
className="inline-flex min-h-[38px] sm:min-h-[31px] items-center gap-2 rounded-[3px] border border-[#333] bg-[#1c1c1c] px-[9px] py-[5px] text-[14px] leading-[21px] text-[#c4c4c4] transition hover:bg-[#292929]"
|
|
1916
|
-
>
|
|
1917
|
-
<span className="max-w-[120px] truncate">{selectedReasoningLabel}</span>
|
|
1918
|
-
<ChevronDown className="h-[10px] w-[10px] text-[#8f8f8f]" strokeWidth={1.8} />
|
|
1919
|
-
</button>
|
|
1920
|
-
|
|
1921
|
-
{reasoningMenuOpen ? (
|
|
1922
|
-
<div className="absolute bottom-[calc(100%+8px)] right-0 z-20 w-[calc(100vw-2rem)] max-w-[260px] sm:w-[260px] overflow-hidden rounded-[4px] border border-[#333] bg-[#1c1c1c] shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
|
|
1923
|
-
<div className="border-b border-[#333] px-3 py-2">
|
|
1924
|
-
<p className="text-[12px] font-medium uppercase tracking-[0.16em] text-[#8f8f8f]">Reasoning</p>
|
|
1925
|
-
<p className="mt-1 text-[12px] leading-[18px] text-[#6f6f6f]">
|
|
1926
|
-
Override how hard the agent thinks for the next turns.
|
|
1927
|
-
</p>
|
|
1928
|
-
</div>
|
|
1929
|
-
<div className="max-h-72 overflow-y-auto p-1">
|
|
1930
|
-
<button
|
|
1931
|
-
type="button"
|
|
1932
|
-
onClick={() => {
|
|
1933
|
-
setSelectedReasoning("");
|
|
1934
|
-
setReasoningMenuOpen(false);
|
|
1935
|
-
}}
|
|
1936
|
-
className="flex w-full flex-col rounded-[3px] px-3 py-2 text-left transition hover:bg-[#292929]"
|
|
1937
|
-
>
|
|
1938
|
-
<span className="text-[14px] leading-[21px] text-[#c4c4c4]">Session default</span>
|
|
1939
|
-
<span className="text-[12px] leading-[18px] text-[#8f8f8f]">
|
|
1940
|
-
{sessionReasoningEffort?.trim()
|
|
1941
|
-
? `Use ${formatReasoningLabel(sessionReasoningEffort)}.`
|
|
1942
|
-
: "Use the default reasoning level for the selected model."}
|
|
1943
|
-
</span>
|
|
1944
|
-
</button>
|
|
1945
|
-
|
|
1946
|
-
{reasoningOptions.map((option) => (
|
|
1947
|
-
<button
|
|
1948
|
-
key={option.id}
|
|
1949
|
-
type="button"
|
|
1950
|
-
onClick={() => {
|
|
1951
|
-
setSelectedReasoning(option.id.trim().toLowerCase());
|
|
1952
|
-
setReasoningMenuOpen(false);
|
|
1953
|
-
}}
|
|
1954
|
-
className={`flex w-full flex-col rounded-[3px] px-3 py-2 text-left transition hover:bg-[#292929] ${selectedReasoning === option.id.trim().toLowerCase() ? "bg-[#292929]" : ""}`}
|
|
1955
|
-
>
|
|
1956
|
-
<span className="text-[14px] leading-[21px] text-[#c4c4c4]">{option.label}</span>
|
|
1957
|
-
<span className="text-[12px] leading-[18px] text-[#8f8f8f]">{option.description}</span>
|
|
1958
|
-
</button>
|
|
1959
|
-
))}
|
|
1960
|
-
{reasoningOptions.length === 0 ? (
|
|
1961
|
-
<div className="px-3 py-2 text-[12px] leading-[18px] text-[#8f8f8f]">
|
|
1962
|
-
No explicit reasoning controls were detected for this agent.
|
|
1963
|
-
</div>
|
|
1964
|
-
) : null}
|
|
1965
|
-
</div>
|
|
1966
|
-
<div className="border-t border-[#333] px-3 py-2 text-[12px] leading-[18px] text-[#6f6f6f]">
|
|
1967
|
-
Reasoning options can change with the selected model.
|
|
1968
|
-
</div>
|
|
1969
|
-
</div>
|
|
1970
|
-
) : null}
|
|
1971
|
-
</div>
|
|
1972
|
-
</div>
|
|
1973
|
-
</div>
|
|
1974
|
-
|
|
1975
|
-
<div className="px-2 py-2">
|
|
1976
|
-
<textarea
|
|
1977
|
-
ref={composerRef}
|
|
1978
|
-
value={message}
|
|
1979
|
-
onChange={(event) => setMessage(event.target.value)}
|
|
1980
|
-
onFocus={() => {
|
|
1981
|
-
normalizeWhitespaceOnlyDraft();
|
|
1982
|
-
}}
|
|
1983
|
-
onKeyDown={handleComposerKeyDown}
|
|
1984
|
-
placeholder="Continue working on this task..."
|
|
1985
|
-
rows={1}
|
|
1986
|
-
className="min-h-[24px] w-full resize-none bg-transparent px-1 py-0 text-[16px] leading-[24px] text-[#8f8f8f] outline-none placeholder:text-[#8f8f8f]"
|
|
1987
|
-
/>
|
|
1988
|
-
|
|
1989
|
-
{showSlashCommandMenu ? (
|
|
1990
|
-
<div className="mt-3 max-w-full overflow-hidden rounded-[3px] border border-[#333] bg-[#171717]">
|
|
1991
|
-
<div className="border-b border-[#333] px-3 py-2 text-[11px] uppercase tracking-[0.18em] text-[#8f8f8f]">
|
|
1992
|
-
Slash commands
|
|
1993
|
-
</div>
|
|
1994
|
-
<div className="max-h-[min(220px,40vh)] max-w-full overflow-y-auto py-1">
|
|
1995
|
-
{slashCommandOptions.map((option, index) => (
|
|
1996
|
-
<button
|
|
1997
|
-
key={option.command}
|
|
1998
|
-
type="button"
|
|
1999
|
-
onClick={() => applySlashCommand(option)}
|
|
2000
|
-
className={`flex w-full flex-col gap-1 px-3 py-2 text-left transition ${
|
|
2001
|
-
index === selectedSlashIndex ? "bg-[#242424]" : "hover:bg-[#1f1f1f]"
|
|
2002
|
-
}`}
|
|
2003
|
-
>
|
|
2004
|
-
<span className="text-[14px] leading-[21px] text-[#f1f1f1]">{option.label}</span>
|
|
2005
|
-
<span className="text-[12px] leading-[18px] text-[#8f8f8f]">{option.description}</span>
|
|
2006
|
-
</button>
|
|
2007
|
-
))}
|
|
2008
|
-
</div>
|
|
2009
|
-
<div className="border-t border-[#333] px-3 py-2 text-[12px] leading-[18px] text-[#8f8f8f]">
|
|
2010
|
-
Slash commands are passed straight to the live agent session under the chat UI.
|
|
2011
|
-
</div>
|
|
2012
|
-
</div>
|
|
2013
|
-
) : null}
|
|
2014
|
-
|
|
2015
|
-
<AttachmentPills
|
|
2016
|
-
attachments={attachments.map((attachment) => attachment.file.name)}
|
|
2017
|
-
onRemove={(index) => {
|
|
2018
|
-
setAttachments((current) => {
|
|
2019
|
-
const next = [...current];
|
|
2020
|
-
next.splice(index, 1);
|
|
2021
|
-
return next;
|
|
2022
|
-
});
|
|
2023
|
-
}}
|
|
2024
|
-
/>
|
|
2025
|
-
|
|
2026
|
-
<div className="mt-3 flex items-end justify-between gap-3">
|
|
2027
|
-
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
2028
|
-
<input
|
|
2029
|
-
ref={fileInputRef}
|
|
2030
|
-
type="file"
|
|
2031
|
-
multiple
|
|
2032
|
-
className="hidden"
|
|
2033
|
-
onChange={handleAttachmentChange}
|
|
2034
|
-
/>
|
|
2035
|
-
<button
|
|
2036
|
-
type="button"
|
|
2037
|
-
onClick={() => fileInputRef.current?.click()}
|
|
2038
|
-
className="inline-flex h-[38px] w-[38px] sm:h-[29px] sm:w-[33px] items-center justify-center rounded-[3px] border border-[#333] bg-[#1c1c1c] text-[#c4c4c4] transition hover:bg-[#292929]"
|
|
2039
|
-
aria-label="Add attachment"
|
|
2040
|
-
>
|
|
2041
|
-
<Paperclip className="h-[15px] w-[15px]" strokeWidth={1.7} />
|
|
2042
|
-
</button>
|
|
2043
|
-
<div className="inline-flex h-[29px] items-center justify-center rounded-[3px] border border-[#333] bg-[#1c1c1c] px-[9px] text-[#8f8f8f]">
|
|
2044
|
-
<Code2 className="h-[15px] w-[15px]" strokeWidth={1.7} />
|
|
2045
|
-
</div>
|
|
2046
|
-
<div className="hidden min-h-[29px] items-center rounded-[3px] bg-[#1c1c1c] px-[9px] py-[5px] text-[12px] leading-[18px] text-[#8f8f8f] sm:inline-flex">
|
|
2047
|
-
{selectedReasoningLabel}
|
|
2048
|
-
</div>
|
|
2049
|
-
{hasLiveTerminalSession ? (
|
|
2050
|
-
<div className="hidden min-h-[29px] items-center rounded-[3px] border border-[#333] bg-[#171717] px-[9px] py-[5px] text-[12px] leading-[18px] text-[#8f8f8f] sm:inline-flex">
|
|
2051
|
-
Terminal keys: empty composer forwards Enter, Tab, arrows, Esc, Backspace. Ctrl+C interrupts.
|
|
2052
|
-
</div>
|
|
2053
|
-
) : null}
|
|
2054
|
-
</div>
|
|
2055
|
-
|
|
2056
|
-
<button
|
|
2057
|
-
type="button"
|
|
2058
|
-
onClick={() => void (isSessionRunning ? handleInterrupt() : handleSend())}
|
|
2059
|
-
disabled={interrupting || (!isSessionRunning && (sending || (!message.trim() && attachments.length === 0)))}
|
|
2060
|
-
className={`inline-flex min-h-[38px] sm:min-h-[29px] items-center justify-center rounded-[3px] px-4 py-[6px] text-[16px] leading-[16px] transition disabled:cursor-not-allowed disabled:opacity-40 ${
|
|
2061
|
-
isSessionRunning
|
|
2062
|
-
? "border border-[#603535] bg-[rgba(210,81,81,0.12)] text-[#f0b5b5] hover:bg-[rgba(210,81,81,0.18)]"
|
|
2063
|
-
: "bg-[#292929] text-[#c4c4c4] hover:bg-[#313131]"
|
|
2064
|
-
}`}
|
|
2065
|
-
>
|
|
2066
|
-
{interrupting ? (
|
|
2067
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
2068
|
-
) : isSessionRunning ? (
|
|
2069
|
-
<span className="inline-flex items-center gap-2">
|
|
2070
|
-
<span>Stop</span>
|
|
2071
|
-
<span className="hidden text-[11px] leading-[11px] text-[#8f8f8f] sm:inline">Ctrl+C</span>
|
|
2072
|
-
</span>
|
|
2073
|
-
) : sending ? (
|
|
2074
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
2075
|
-
) : (
|
|
2076
|
-
"Send"
|
|
2077
|
-
)}
|
|
2078
|
-
</button>
|
|
2079
|
-
</div>
|
|
2080
|
-
</div>
|
|
2081
|
-
</div>
|
|
2082
|
-
|
|
2083
|
-
<SessionRuntimeStatusBar
|
|
2084
|
-
agentName={normalizedAgentName}
|
|
2085
|
-
sessionModel={sessionModel?.trim() || null}
|
|
2086
|
-
sessionReasoningEffort={sessionReasoningEffort?.trim() || null}
|
|
2087
|
-
runtimeStatus={runtimeStatus}
|
|
2088
|
-
agents={agents}
|
|
2089
|
-
/>
|
|
2090
|
-
</div>
|
|
2091
|
-
</div>
|
|
2092
|
-
</div>
|
|
2093
|
-
</div>
|
|
2094
|
-
);
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
export default ChatPanel;
|