From 8b4887261dce9602145884a0c69514a6f8270907 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 1 May 2025 14:39:23 +0100 Subject: [PATCH] feat(site): add experimental chat UI Builds on https://github.com/coder/coder/pull/17570 Frontend portion of https://github.com/coder/coder/tree/chat originally authored by @kylecarbs Additional changes: - Addresses linter complaints - Brings `ChatToolInvocation` argument definitions in line with those defined in `codersdk/toolsdk` - Ensures chat-related features are not shown unless `ExperimentAgenticChat` is enabled. Co-authored-by: Kyle Carberry --- site/package.json | 3 + site/pnpm-lock.yaml | 216 +++ site/src/api/api.ts | 24 + site/src/api/queries/chats.ts | 25 + site/src/api/queries/deployment.ts | 7 + site/src/contexts/useAgenticChat.ts | 16 + .../modules/dashboard/Navbar/NavbarView.tsx | 31 +- site/src/pages/ChatPage/ChatLanding.tsx | 164 +++ site/src/pages/ChatPage/ChatLayout.tsx | 246 ++++ site/src/pages/ChatPage/ChatMessages.tsx | 491 +++++++ .../ChatPage/ChatToolInvocation.stories.tsx | 1211 +++++++++++++++++ .../src/pages/ChatPage/ChatToolInvocation.tsx | 872 ++++++++++++ .../pages/ChatPage/LanguageModelSelector.tsx | 73 + site/src/router.tsx | 8 + 14 files changed, 3381 insertions(+), 6 deletions(-) create mode 100644 site/src/api/queries/chats.ts create mode 100644 site/src/contexts/useAgenticChat.ts create mode 100644 site/src/pages/ChatPage/ChatLanding.tsx create mode 100644 site/src/pages/ChatPage/ChatLayout.tsx create mode 100644 site/src/pages/ChatPage/ChatMessages.tsx create mode 100644 site/src/pages/ChatPage/ChatToolInvocation.stories.tsx create mode 100644 site/src/pages/ChatPage/ChatToolInvocation.tsx create mode 100644 site/src/pages/ChatPage/LanguageModelSelector.tsx diff --git a/site/package.json b/site/package.json index 23c1cf9d22428..bc459ce79f7a1 100644 --- a/site/package.json +++ b/site/package.json @@ -35,6 +35,8 @@ "update-emojis": "cp -rf ./node_modules/emoji-datasource-apple/img/apple/64/* ./static/emojis" }, "dependencies": { + "@ai-sdk/provider-utils": "2.2.6", + "@ai-sdk/react": "1.2.6", "@emoji-mart/data": "1.2.1", "@emoji-mart/react": "1.1.1", "@emotion/cache": "11.14.0", @@ -111,6 +113,7 @@ "react-virtualized-auto-sizer": "1.0.24", "react-window": "1.8.11", "recharts": "2.15.0", + "rehype-raw": "7.0.0", "remark-gfm": "4.0.0", "resize-observer-polyfill": "1.5.1", "rollup-plugin-visualizer": "5.14.0", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 7b8e9c52ea4af..252d7809033ec 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -16,6 +16,12 @@ importers: .: dependencies: + '@ai-sdk/provider-utils': + specifier: 2.2.6 + version: 2.2.6(zod@3.24.3) + '@ai-sdk/react': + specifier: 1.2.6 + version: 1.2.6(react@18.3.1)(zod@3.24.3) '@emoji-mart/data': specifier: 1.2.1 version: 1.2.1 @@ -244,6 +250,9 @@ importers: recharts: specifier: 2.15.0 version: 2.15.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rehype-raw: + specifier: 7.0.0 + version: 7.0.0 remark-gfm: specifier: 4.0.0 version: 4.0.0 @@ -489,6 +498,42 @@ packages: '@adobe/css-tools@4.4.1': resolution: {integrity: sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==, tarball: https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz} + '@ai-sdk/provider-utils@2.2.4': + resolution: {integrity: sha512-13sEGBxB6kgaMPGOgCLYibF6r8iv8mgjhuToFrOTU09bBxbFQd8ZoARarCfJN6VomCUbUvMKwjTBLb1vQnN+WA==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.4.tgz} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider-utils@2.2.6': + resolution: {integrity: sha512-sUlZ7Gnq84DCGWMQRIK8XVbkzIBnvPR1diV4v6JwPgpn5armnLI/j+rqn62MpLrU5ZCQZlDKl/Lw6ed3ulYqaA==, tarball: https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-2.2.6.tgz} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider@1.1.0': + resolution: {integrity: sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.0.tgz} + engines: {node: '>=18'} + + '@ai-sdk/provider@1.1.2': + resolution: {integrity: sha512-ITdgNilJZwLKR7X5TnUr1BsQW6UTX5yFp0h66Nfx8XjBYkWD9W3yugr50GOz3CnE9m/U/Cd5OyEbTMI0rgi6ZQ==, tarball: https://registry.npmjs.org/@ai-sdk/provider/-/provider-1.1.2.tgz} + engines: {node: '>=18'} + + '@ai-sdk/react@1.2.6': + resolution: {integrity: sha512-5BFChNbcYtcY9MBStcDev7WZRHf0NpTrk8yfSoedWctB3jfWkFd1HECBvdc8w3mUQshF2MumLHtAhRO7IFtGGQ==, tarball: https://registry.npmjs.org/@ai-sdk/react/-/react-1.2.6.tgz} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + + '@ai-sdk/ui-utils@1.2.5': + resolution: {integrity: sha512-XDgqnJcaCkDez7qolvk+PDbs/ceJvgkNkxkOlc9uDWqxfDJxtvCZ+14MP/1qr4IBwGIgKVHzMDYDXvqVhSWLzg==, tarball: https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-1.2.5.tgz} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==, tarball: https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz} engines: {node: '>=10'} @@ -3942,18 +3987,33 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, tarball: https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz} engines: {node: '>= 0.4'} + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==, tarball: https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz} + hast-util-parse-selector@2.2.5: resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==, tarball: https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz} + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==, tarball: https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==, tarball: https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz} + hast-util-to-jsx-runtime@2.3.2: resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==, tarball: https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz} + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==, tarball: https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==, tarball: https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz} hastscript@6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==, tarball: https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz} + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==, tarball: https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz} + headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==, tarball: https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz} @@ -3976,6 +4036,9 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==, tarball: https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==, tarball: https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==, tarball: https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz} engines: {node: '>= 0.8'} @@ -4480,6 +4543,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, tarball: https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==, tarball: https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, tarball: https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz} @@ -5236,6 +5302,9 @@ packages: property-information@6.5.0: resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==, tarball: https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz} + property-information@7.0.0: + resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==, tarball: https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz} + protobufjs@7.4.0: resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==, tarball: https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz} engines: {node: '>=12.0.0'} @@ -5492,6 +5561,9 @@ packages: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==, tarball: https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz} engines: {node: '>= 0.4'} + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==, tarball: https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz} + remark-gfm@4.0.0: resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==, tarball: https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz} @@ -5599,6 +5671,9 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==, tarball: https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==, tarball: https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz} + semver@7.6.2: resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==, tarball: https://registry.npmjs.org/semver/-/semver-7.6.2.tgz} engines: {node: '>=10'} @@ -5840,6 +5915,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==, tarball: https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz} engines: {node: '>= 0.4'} + swr@2.3.3: + resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==, tarball: https://registry.npmjs.org/swr/-/swr-2.3.3.tgz} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, tarball: https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz} @@ -5877,6 +5957,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==, tarball: https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==, tarball: https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz} + engines: {node: '>=18'} + tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==, tarball: https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz} @@ -6163,6 +6247,9 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, tarball: https://registry.npmjs.org/vary/-/vary-1.1.2.tgz} engines: {node: '>= 0.8'} + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==, tarball: https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz} + vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==, tarball: https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz} @@ -6274,6 +6361,9 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==, tarball: https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz} + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==, tarball: https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==, tarball: https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz} engines: {node: '>=12'} @@ -6405,6 +6495,11 @@ packages: yup@1.6.1: resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==, tarball: https://registry.npmjs.org/yup/-/yup-1.6.1.tgz} + zod-to-json-schema@3.24.5: + resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==, tarball: https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz} + peerDependencies: + zod: ^3.24.1 + zod-validation-error@3.4.0: resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==, tarball: https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz} engines: {node: '>=18.0.0'} @@ -6424,6 +6519,45 @@ snapshots: '@adobe/css-tools@4.4.1': {} + '@ai-sdk/provider-utils@2.2.4(zod@3.24.3)': + dependencies: + '@ai-sdk/provider': 1.1.0 + nanoid: 3.3.8 + secure-json-parse: 2.7.0 + zod: 3.24.3 + + '@ai-sdk/provider-utils@2.2.6(zod@3.24.3)': + dependencies: + '@ai-sdk/provider': 1.1.2 + nanoid: 3.3.8 + secure-json-parse: 2.7.0 + zod: 3.24.3 + + '@ai-sdk/provider@1.1.0': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/provider@1.1.2': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@1.2.6(react@18.3.1)(zod@3.24.3)': + dependencies: + '@ai-sdk/provider-utils': 2.2.4(zod@3.24.3) + '@ai-sdk/ui-utils': 1.2.5(zod@3.24.3) + react: 18.3.1 + swr: 2.3.3(react@18.3.1) + throttleit: 2.1.0 + optionalDependencies: + zod: 3.24.3 + + '@ai-sdk/ui-utils@1.2.5(zod@3.24.3)': + dependencies: + '@ai-sdk/provider': 1.1.0 + '@ai-sdk/provider-utils': 2.2.4(zod@3.24.3) + zod: 3.24.3 + zod-to-json-schema: 3.24.5(zod@3.24.3) + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -10183,8 +10317,39 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.0.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + hast-util-parse-selector@2.2.5: {} + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.1.2 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.2: dependencies: '@types/estree': 1.0.6 @@ -10205,6 +10370,16 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -10217,6 +10392,14 @@ snapshots: property-information: 5.6.0 space-separated-tokens: 1.1.5 + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + headers-polyfill@4.0.3: {} highlight.js@10.7.3: {} @@ -10235,6 +10418,8 @@ snapshots: html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -10962,6 +11147,8 @@ snapshots: json-schema-traverse@0.4.1: optional: true + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: optional: true @@ -11986,6 +12173,8 @@ snapshots: property-information@6.5.0: {} + property-information@7.0.0: {} + protobufjs@7.4.0: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -12303,6 +12492,12 @@ snapshots: define-properties: 1.2.1 set-function-name: 2.0.1 + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + remark-gfm@4.0.0: dependencies: '@types/mdast': 4.0.3 @@ -12442,6 +12637,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + secure-json-parse@2.7.0: {} + semver@7.6.2: {} send@0.19.0: @@ -12695,6 +12892,12 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swr@2.3.3(react@18.3.1): + dependencies: + dequal: 2.0.3 + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + symbol-tree@3.2.4: {} tailwind-merge@2.6.0: {} @@ -12753,6 +12956,8 @@ snapshots: dependencies: any-promise: 1.3.0 + throttleit@2.1.0: {} + tiny-case@1.0.3: {} tiny-invariant@1.3.3: {} @@ -13043,6 +13248,11 @@ snapshots: vary@1.1.2: {} + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 @@ -13139,6 +13349,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-namespaces@2.0.1: {} + webidl-conversions@7.0.0: {} webpack-sources@3.2.3: {} @@ -13253,6 +13465,10 @@ snapshots: toposort: 2.0.2 type-fest: 2.19.0 + zod-to-json-schema@3.24.5(zod@3.24.3): + dependencies: + zod: 3.24.3 + zod-validation-error@3.4.0(zod@3.24.3): dependencies: zod: 3.24.3 diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ef15beb8166f5..688ba0432e22b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -827,6 +827,13 @@ class ApiMethods { return response.data; }; + getDeploymentLLMs = async (): Promise => { + const response = await this.axios.get( + "/api/v2/deployment/llms", + ); + return response.data; + }; + getOrganizationIdpSyncClaimFieldValues = async ( organization: string, field: string, @@ -2489,6 +2496,23 @@ class ApiMethods { markAllInboxNotificationsAsRead = async () => { await this.axios.put("/api/v2/notifications/inbox/mark-all-as-read"); }; + + createChat = async () => { + const res = await this.axios.post("/api/v2/chats"); + return res.data; + }; + + getChats = async () => { + const res = await this.axios.get("/api/v2/chats"); + return res.data; + }; + + getChatMessages = async (chatId: string) => { + const res = await this.axios.get( + `/api/v2/chats/${chatId}/messages`, + ); + return res.data; + }; } // This is a hard coded CSRF token/cookie pair for local development. In prod, diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts new file mode 100644 index 0000000000000..196bf4c603597 --- /dev/null +++ b/site/src/api/queries/chats.ts @@ -0,0 +1,25 @@ +import { API } from "api/api"; +import type { QueryClient } from "react-query"; + +export const createChat = (queryClient: QueryClient) => { + return { + mutationFn: API.createChat, + onSuccess: async () => { + await queryClient.invalidateQueries(["chats"]); + }, + }; +}; + +export const getChats = () => { + return { + queryKey: ["chats"], + queryFn: API.getChats, + }; +}; + +export const getChatMessages = (chatID: string) => { + return { + queryKey: ["chatMessages", chatID], + queryFn: () => API.getChatMessages(chatID), + }; +}; diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 999dd2ee4cbd5..463f555d57761 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -36,3 +36,10 @@ export const deploymentIdpSyncFieldValues = (field: string) => { queryFn: () => API.getDeploymentIdpSyncFieldValues(field), }; }; + +export const deploymentLanguageModels = () => { + return { + queryKey: ["deployment", "llms"], + queryFn: API.getDeploymentLLMs, + }; +}; diff --git a/site/src/contexts/useAgenticChat.ts b/site/src/contexts/useAgenticChat.ts new file mode 100644 index 0000000000000..97194b4512340 --- /dev/null +++ b/site/src/contexts/useAgenticChat.ts @@ -0,0 +1,16 @@ +import { experiments } from "api/queries/experiments"; + +import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; +import { useQuery } from "react-query"; + +interface AgenticChat { + readonly enabled: boolean; +} + +export const useAgenticChat = (): AgenticChat => { + const { metadata } = useEmbeddedMetadata(); + const enabledExperimentsQuery = useQuery(experiments(metadata.experiments)); + return { + enabled: enabledExperimentsQuery.data?.includes("agentic-chat") ?? false, + }; +}; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 0447e762ed67e..8cefde8cb86e3 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -4,6 +4,7 @@ import { Button } from "components/Button/Button"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { CoderIcon } from "components/Icons/CoderIcon"; import type { ProxyContextValue } from "contexts/ProxyContext"; +import { useAgenticChat } from "contexts/useAgenticChat"; import { useWebpushNotifications } from "contexts/useWebpushNotifications"; import { NotificationsInbox } from "modules/notifications/NotificationsInbox/NotificationsInbox"; import type { FC } from "react"; @@ -45,8 +46,7 @@ export const NavbarView: FC = ({ canViewAuditLog, proxyContextValue, }) => { - const { subscribed, enabled, loading, subscribe, unsubscribe } = - useWebpushNotifications(); + const webPush = useWebpushNotifications(); return (
@@ -76,13 +76,21 @@ export const NavbarView: FC = ({ />
- {enabled ? ( - subscribed ? ( - ) : ( - ) @@ -132,6 +140,7 @@ interface NavItemsProps { const NavItems: FC = ({ className }) => { const location = useLocation(); + const agenticChat = useAgenticChat(); return ( ); }; diff --git a/site/src/pages/ChatPage/ChatLanding.tsx b/site/src/pages/ChatPage/ChatLanding.tsx new file mode 100644 index 0000000000000..060752f895313 --- /dev/null +++ b/site/src/pages/ChatPage/ChatLanding.tsx @@ -0,0 +1,164 @@ +import { useTheme } from "@emotion/react"; +import SendIcon from "@mui/icons-material/Send"; +import Button from "@mui/material/Button"; +import IconButton from "@mui/material/IconButton"; +import Paper from "@mui/material/Paper"; +import Stack from "@mui/material/Stack"; +import TextField from "@mui/material/TextField"; +import { createChat } from "api/queries/chats"; +import type { Chat } from "api/typesGenerated"; +import { Margins } from "components/Margins/Margins"; +import { useAuthenticated } from "hooks"; +import { type FC, type FormEvent, useState } from "react"; +import { useMutation, useQueryClient } from "react-query"; +import { useNavigate } from "react-router-dom"; +import { LanguageModelSelector } from "./LanguageModelSelector"; + +export interface ChatLandingLocationState { + chat: Chat; + message: string; +} + +const ChatLanding: FC = () => { + const { user } = useAuthenticated(); + const theme = useTheme(); + const [input, setInput] = useState(""); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const createChatMutation = useMutation(createChat(queryClient)); + + return ( + +
+ {/* Initial Welcome Message Area */} +
+

+ Good evening, {user?.name.split(" ")[0]} +

+

+ How can I help you today? +

+
+ + {/* Input Form and Suggestions - Always Visible */} +
+ + + + + + + ) => { + e.preventDefault(); + setInput(""); + const chat = await createChatMutation.mutateAsync(); + navigate(`/chat/${chat.id}`, { + state: { + chat, + message: input, + }, + }); + }} + elevation={2} + css={{ + padding: "16px", + display: "flex", + alignItems: "center", + width: "100%", + borderRadius: "12px", + border: `1px solid ${theme.palette.divider}`, + }} + > + ) => { + setInput(event.target.value); + }} + placeholder="Ask Coder..." + required + fullWidth + variant="outlined" + multiline + maxRows={5} + css={{ + marginRight: theme.spacing(1), + "& .MuiOutlinedInput-root": { + borderRadius: "8px", + padding: "10px 14px", + }, + }} + autoFocus + /> + + + + +
+
+
+ ); +}; + +export default ChatLanding; diff --git a/site/src/pages/ChatPage/ChatLayout.tsx b/site/src/pages/ChatPage/ChatLayout.tsx new file mode 100644 index 0000000000000..77de96af01595 --- /dev/null +++ b/site/src/pages/ChatPage/ChatLayout.tsx @@ -0,0 +1,246 @@ +import { useTheme } from "@emotion/react"; +import AddIcon from "@mui/icons-material/Add"; +import Button from "@mui/material/Button"; +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; +import Paper from "@mui/material/Paper"; +import { createChat, getChats } from "api/queries/chats"; +import { deploymentLanguageModels } from "api/queries/deployment"; +import type { LanguageModelConfig } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Loader } from "components/Loader/Loader"; +import { Margins } from "components/Margins/Margins"; +import { useAgenticChat } from "contexts/useAgenticChat"; +import { + type FC, + type PropsWithChildren, + createContext, + useContext, + useEffect, + useState, +} from "react"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { Link, Outlet, useNavigate, useParams } from "react-router-dom"; + +export interface ChatContext { + selectedModel: string; + modelConfig: LanguageModelConfig; + + setSelectedModel: (model: string) => void; +} +export const useChatContext = (): ChatContext => { + const context = useContext(ChatContext); + if (!context) { + throw new Error("useChatContext must be used within a ChatProvider"); + } + return context; +}; + +export const ChatContext = createContext(undefined); + +const SELECTED_MODEL_KEY = "coder_chat_selected_model"; + +const ChatProvider: FC = ({ children }) => { + const [selectedModel, setSelectedModel] = useState(() => { + const savedModel = localStorage.getItem(SELECTED_MODEL_KEY); + return savedModel || ""; + }); + const modelConfigQuery = useQuery(deploymentLanguageModels()); + useEffect(() => { + if (!modelConfigQuery.data) { + return; + } + if (selectedModel === "") { + const firstModel = modelConfigQuery.data.models[0]?.id; // Handle empty models array + if (firstModel) { + setSelectedModel(firstModel); + localStorage.setItem(SELECTED_MODEL_KEY, firstModel); + } + } + }, [modelConfigQuery.data, selectedModel]); + + if (modelConfigQuery.error) { + return ; + } + + if (!modelConfigQuery.data) { + return ; + } + + const handleSetSelectedModel = (model: string) => { + setSelectedModel(model); + localStorage.setItem(SELECTED_MODEL_KEY, model); + }; + + return ( + + {children} + + ); +}; + +export const ChatLayout: FC = () => { + const agenticChat = useAgenticChat(); + const queryClient = useQueryClient(); + const { data: chats, isLoading: chatsLoading } = useQuery(getChats()); + const createChatMutation = useMutation(createChat(queryClient)); + const theme = useTheme(); + const navigate = useNavigate(); + const { chatID } = useParams<{ chatID?: string }>(); + + const handleNewChat = () => { + navigate("/chat"); + }; + + if (!agenticChat.enabled) { + return ( + +
+

Agentic Chat is not enabled

+

+ Agentic Chat is an experimental feature and is not enabled by + default. Please contact your administrator for more information. +

+
+
+ ); + } + + return ( + // Outermost container: controls height and prevents page scroll +
+ {/* Sidebar Container (using Paper for background/border) */} + + {/* Sidebar Header */} +
+ {/* Replaced Typography with div + styling */} +
+ Chats +
+ +
+ {/* Sidebar Scrollable List Area */} +
+ {chatsLoading ? ( + + ) : chats && chats.length > 0 ? ( + + {chats.map((chat) => ( + + + + + + ))} + + ) : ( + // Replaced Typography with div + styling +
+ No chats yet. Start a new one! +
+ )} +
+
+ + {/* Main Content Area Container */} +
+ + {/* Outlet renders ChatMessages, which should have its own internal scroll */} + + +
+
+ ); +}; diff --git a/site/src/pages/ChatPage/ChatMessages.tsx b/site/src/pages/ChatPage/ChatMessages.tsx new file mode 100644 index 0000000000000..928b3c9ee2724 --- /dev/null +++ b/site/src/pages/ChatPage/ChatMessages.tsx @@ -0,0 +1,491 @@ +import { type Message, useChat } from "@ai-sdk/react"; +import { type Theme, keyframes, useTheme } from "@emotion/react"; +import SendIcon from "@mui/icons-material/Send"; +import IconButton from "@mui/material/IconButton"; +import Paper from "@mui/material/Paper"; +import TextField from "@mui/material/TextField"; +import { getChatMessages } from "api/queries/chats"; +import type { ChatMessage, CreateChatMessageRequest } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { Loader } from "components/Loader/Loader"; +import { + type FC, + type KeyboardEvent, + memo, + useCallback, + useEffect, + useRef, +} from "react"; +import ReactMarkdown from "react-markdown"; +import { useQuery } from "react-query"; +import { useLocation, useParams } from "react-router-dom"; +import rehypeRaw from "rehype-raw"; +import remarkGfm from "remark-gfm"; +import type { ChatLandingLocationState } from "./ChatLanding"; +import { useChatContext } from "./ChatLayout"; +import { ChatToolInvocation } from "./ChatToolInvocation"; +import { LanguageModelSelector } from "./LanguageModelSelector"; + +const fadeIn = keyframes` + from { + opacity: 0; + transform: translateY(5px); + } + to { + opacity: 1; + transform: translateY(0); + } +`; + +const renderReasoning = (reasoning: string, theme: Theme) => ( +
+
+ 💭 Reasoning: +
+
+ {reasoning} +
+
+); + +interface MessageBubbleProps { + message: Message; +} + +const MessageBubble: FC = memo(({ message }) => { + const theme = useTheme(); + const isUser = message.role === "user"; + + return ( +
+ code)": { + backgroundColor: isUser + ? theme.palette.grey[700] + : theme.palette.action.hover, + color: isUser ? theme.palette.grey[50] : theme.palette.text.primary, + padding: theme.spacing(0.25, 0.75), + borderRadius: "4px", + fontSize: "0.875em", + fontFamily: "monospace", + }, + "& pre": { + backgroundColor: isUser + ? theme.palette.common.black + : theme.palette.grey[100], + color: isUser + ? theme.palette.grey[100] + : theme.palette.text.primary, + padding: theme.spacing(1.5), + borderRadius: "8px", + overflowX: "auto", + margin: theme.spacing(1.5, 0), + width: "100%", + "& code": { + backgroundColor: "transparent", + padding: 0, + fontSize: "0.875em", + fontFamily: "monospace", + color: "inherit", + }, + }, + "& a": { + color: isUser + ? theme.palette.grey[100] + : theme.palette.primary.main, + textDecoration: "underline", + fontWeight: 500, + "&:hover": { + textDecoration: "none", + color: isUser + ? theme.palette.grey[300] + : theme.palette.primary.dark, + }, + }, + }} + > + {message.role === "assistant" && message.parts ? ( +
+ {message.parts.map((part) => { + switch (part.type) { + case "text": + return ( + + {part.text} + + ); + case "tool-invocation": + return ( +
+ +
+ ); + case "reasoning": + return ( +
+ {renderReasoning(part.reasoning, theme)} +
+ ); + default: + return null; + } + })} +
+ ) : ( + + {message.content} + + )} +
+
+ ); +}); + +interface ChatViewProps { + messages: Message[]; + input: string; + handleInputChange: React.ChangeEventHandler< + HTMLInputElement | HTMLTextAreaElement + >; + handleSubmit: (e?: React.FormEvent) => void; + isLoading: boolean; + chatID: string; +} + +const ChatView: FC = ({ + messages, + input, + handleInputChange, + handleSubmit, + isLoading, +}) => { + const theme = useTheme(); + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + const chatContext = useChatContext(); + + useEffect(() => { + const timer = setTimeout(() => { + messagesEndRef.current?.scrollIntoView({ + behavior: "smooth", + block: "end", + }); + }, 50); + return () => clearTimeout(timer); + }, []); + + useEffect(() => { + inputRef.current?.focus(); + }, []); + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + handleSubmit(); + } + }; + + return ( +
+
+
+ {messages.map((message) => ( + + ))} +
+
+
+ +
+ +
+ +
+ + + + +
+
+
+ ); +}; + +export const ChatMessages: FC = () => { + const { chatID } = useParams(); + if (!chatID) { + throw new Error("Chat ID is required in URL path /chat/:chatID"); + } + + const { state } = useLocation(); + const transferredState = state as ChatLandingLocationState | undefined; + + const messagesQuery = useQuery(getChatMessages(chatID)); + + const chatContext = useChatContext(); + + const { + messages, + input, + handleInputChange, + handleSubmit: originalHandleSubmit, + isLoading, + setInput, + setMessages, + } = useChat({ + id: chatID, + api: `/api/v2/chats/${chatID}/messages`, + experimental_prepareRequestBody: (options): CreateChatMessageRequest => { + const userMessages = options.messages.filter( + (message) => message.role === "user", + ); + const mostRecentUserMessage = userMessages.at(-1); + return { + model: chatContext.selectedModel, + message: mostRecentUserMessage, + thinking: false, + }; + }, + initialInput: transferredState?.message, + initialMessages: messagesQuery.data as Message[] | undefined, + }); + + // Update messages from query data when it loads + useEffect(() => { + if (messagesQuery.data && messages.length === 0) { + setMessages(messagesQuery.data as Message[]); + } + }, [messagesQuery.data, messages.length, setMessages]); + + const handleSubmitCallback = useCallback( + (e?: React.FormEvent) => { + if (e) e.preventDefault(); + if (!input.trim()) return; + originalHandleSubmit(); + setInput(""); // Clear input after submit + }, + [input, originalHandleSubmit, setInput], + ); + + // Clear input and potentially submit on initial load with message + useEffect(() => { + if (transferredState?.message && input === transferredState.message) { + // Prevent submitting if messages already exist (e.g., browser back/forward) + if (messages.length === (messagesQuery.data?.length ?? 0)) { + handleSubmitCallback(); // Use the correct callback name + } + // Clear the state to prevent re-submission on subsequent renders/navigation + window.history.replaceState({}, document.title); + } + }, [ + transferredState?.message, + input, + handleSubmitCallback, + messages.length, + messagesQuery.data?.length, + ]); // Use the correct callback name + + useEffect(() => { + if (transferredState?.message) { + // Logic potentially related to transferredState can go here if needed, + } + }, [transferredState?.message]); + + if (messagesQuery.error) { + return ; + } + + if (messagesQuery.isLoading && messages.length === 0) { + return ; + } + + return ( + + ); +}; diff --git a/site/src/pages/ChatPage/ChatToolInvocation.stories.tsx b/site/src/pages/ChatPage/ChatToolInvocation.stories.tsx new file mode 100644 index 0000000000000..03bf31cb095fb --- /dev/null +++ b/site/src/pages/ChatPage/ChatToolInvocation.stories.tsx @@ -0,0 +1,1211 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + MockStartingWorkspace, + MockStoppedWorkspace, + MockStoppingWorkspace, + MockTemplate, + MockTemplateVersion, + MockUserMember, + MockWorkspace, + MockWorkspaceBuild, +} from "testHelpers/entities"; +import { ChatToolInvocation } from "./ChatToolInvocation"; + +const meta: Meta = { + title: "pages/ChatPage/ChatToolInvocation", + component: ChatToolInvocation, +}; + +export default meta; +type Story = StoryObj; + +export const GetWorkspace: Story = { + render: () => + renderInvocations( + "coder_get_workspace", + { + workspace_id: MockWorkspace.id, + }, + MockWorkspace, + ), +}; + +export const CreateWorkspace: Story = { + render: () => + renderInvocations( + "coder_create_workspace", + { + name: MockWorkspace.name, + rich_parameters: {}, + template_version_id: MockWorkspace.template_active_version_id, + user: MockWorkspace.owner_name, + }, + MockWorkspace, + ), +}; + +export const ListWorkspaces: Story = { + render: () => + renderInvocations( + "coder_list_workspaces", + { + owner: "me", + }, + [ + MockWorkspace, + MockStoppedWorkspace, + MockStoppingWorkspace, + MockStartingWorkspace, + ], + ), +}; + +export const ListTemplates: Story = { + render: () => + renderInvocations("coder_list_templates", {}, [ + { + id: MockTemplate.id, + name: MockTemplate.name, + description: MockTemplate.description, + active_version_id: MockTemplate.active_version_id, + active_user_count: MockTemplate.active_user_count, + }, + { + id: "another-template", + name: "Another Template", + description: "A different template for testing purposes.", + active_version_id: "v2.0", + active_user_count: 5, + }, + ]), +}; + +export const TemplateVersionParameters: Story = { + render: () => + renderInvocations( + "coder_template_version_parameters", + { + template_version_id: MockTemplateVersion.id, + }, + [ + { + name: "region", + display_name: "Region", + description: "Select the deployment region.", + description_plaintext: "Select the deployment region.", + type: "string", + mutable: false, + default_value: "us-west-1", + icon: "", + options: [ + { name: "US West", description: "", value: "us-west-1", icon: "" }, + { name: "US East", description: "", value: "us-east-1", icon: "" }, + ], + required: true, + ephemeral: false, + }, + { + name: "cpu_cores", + display_name: "CPU Cores", + description: "Number of CPU cores.", + description_plaintext: "Number of CPU cores.", + type: "number", + mutable: true, + default_value: "4", + icon: "", + options: [], + required: false, + ephemeral: false, + }, + ], + ), +}; + +export const GetAuthenticatedUser: Story = { + render: () => + renderInvocations("coder_get_authenticated_user", {}, MockUserMember), +}; + +export const CreateWorkspaceBuild: Story = { + render: () => + renderInvocations( + "coder_create_workspace_build", + { + workspace_id: MockWorkspace.id, + transition: "start", + }, + MockWorkspaceBuild, + ), +}; + +export const CreateTemplateVersion: Story = { + render: () => + renderInvocations( + "coder_create_template_version", + { + template_id: MockTemplate.id, + file_id: "file-123", + }, + MockTemplateVersion, + ), +}; + +const mockLogs = [ + "[INFO] Starting build process...", + "[DEBUG] Reading configuration file.", + "[WARN] Deprecated setting detected.", + "[INFO] Applying changes...", + "[ERROR] Failed to connect to database.", +]; + +export const GetWorkspaceAgentLogs: Story = { + render: () => + renderInvocations( + "coder_get_workspace_agent_logs", + { + workspace_agent_id: "agent-456", + }, + mockLogs, + ), +}; + +export const GetWorkspaceBuildLogs: Story = { + render: () => + renderInvocations( + "coder_get_workspace_build_logs", + { + workspace_build_id: MockWorkspaceBuild.id, + }, + mockLogs, + ), +}; + +export const GetTemplateVersionLogs: Story = { + render: () => + renderInvocations( + "coder_get_template_version_logs", + { + template_version_id: MockTemplateVersion.id, + }, + mockLogs, + ), +}; + +export const UpdateTemplateActiveVersion: Story = { + render: () => + renderInvocations( + "coder_update_template_active_version", + { + template_id: MockTemplate.id, + template_version_id: MockTemplateVersion.id, + }, + `Successfully updated active version for template ${MockTemplate.name}.`, + ), +}; + +export const UploadTarFile: Story = { + render: () => + renderInvocations( + "coder_upload_tar_file", + { + files: { "main.tf": templateTerraform, Dockerfile: templateDockerfile }, + }, + { + hash: "sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", + }, + ), +}; + +export const CreateTemplate: Story = { + render: () => + renderInvocations( + "coder_create_template", + { + name: "new-template", + }, + MockTemplate, + ), +}; + +export const DeleteTemplate: Story = { + render: () => + renderInvocations( + "coder_delete_template", + { + template_id: MockTemplate.id, + }, + `Successfully deleted template ${MockTemplate.name}.`, + ), +}; + +export const GetTemplateVersion: Story = { + render: () => + renderInvocations( + "coder_get_template_version", + { + template_version_id: MockTemplateVersion.id, + }, + MockTemplateVersion, + ), +}; + +export const DownloadTarFile: Story = { + render: () => + renderInvocations( + "coder_download_tar_file", + { + file_id: "file-789", + }, + { "main.tf": templateTerraform, "README.md": "# My Template\n" }, + ), +}; + +const renderInvocations = ( + toolName: T, + args: Extract["args"], + result: Extract< + ChatToolInvocation, + { toolName: T; state: "result" } + >["result"], + error?: string, +) => { + return ( + <> + + + + + + ); +}; + +const templateDockerfile = `FROM rust:slim@sha256:9abf10cc84dfad6ace1b0aae3951dc5200f467c593394288c11db1e17bb4d349 AS rust-utils +# Install rust helper programs +# ENV CARGO_NET_GIT_FETCH_WITH_CLI=true +ENV CARGO_INSTALL_ROOT=/tmp/ +RUN cargo install typos-cli watchexec-cli && \ + # Reduce image size. + rm -rf /usr/local/cargo/registry + +FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 AS go + +# Install Go manually, so that we can control the version +ARG GO_VERSION=1.24.1 + +# Boring Go is needed to build FIPS-compliant binaries. +RUN apt-get update && \ + apt-get install --yes curl && \ + curl --silent --show-error --location \ + "https://go.dev/dl/go\${GO_VERSION}.linux-amd64.tar.gz" \ + -o /usr/local/go.tar.gz && \ + rm -rf /var/lib/apt/lists/* + +ENV PATH=$PATH:/usr/local/go/bin +ARG GOPATH="/tmp/" +# Install Go utilities. +RUN apt-get update && \ + apt-get install --yes gcc && \ + mkdir --parents /usr/local/go && \ + tar --extract --gzip --directory=/usr/local/go --file=/usr/local/go.tar.gz --strip-components=1 && \ + mkdir --parents "$GOPATH" && \ + # moq for Go tests. + go install github.com/matryer/moq@v0.2.3 && \ + # swag for Swagger doc generation + go install github.com/swaggo/swag/cmd/swag@v1.7.4 && \ + # go-swagger tool to generate the go coder api client + go install github.com/go-swagger/go-swagger/cmd/swagger@v0.28.0 && \ + # goimports for updating imports + go install golang.org/x/tools/cmd/goimports@v0.31.0 && \ + # protoc-gen-go is needed to build sysbox from source + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30 && \ + # drpc support for v2 + go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.34 && \ + # migrate for migration support for v2 + go install github.com/golang-migrate/migrate/v4/cmd/migrate@v4.15.1 && \ + # goreleaser for compiling v2 binaries + go install github.com/goreleaser/goreleaser@v1.6.1 && \ + # Install the latest version of gopls for editors that support + # the language server protocol + go install golang.org/x/tools/gopls@v0.18.1 && \ + # gotestsum makes test output more readable + go install gotest.tools/gotestsum@v1.9.0 && \ + # goveralls collects code coverage metrics from tests + # and sends to Coveralls + go install github.com/mattn/goveralls@v0.0.11 && \ + # kind for running Kubernetes-in-Docker, needed for tests + go install sigs.k8s.io/kind@v0.10.0 && \ + # helm-docs generates our Helm README based on a template and the + # charts and values files + go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.5.0 && \ + # sqlc for Go code generation + (CGO_ENABLED=1 go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.27.0) && \ + # gcr-cleaner-cli used by CI to prune unused images + go install github.com/sethvargo/gcr-cleaner/cmd/gcr-cleaner-cli@v0.5.1 && \ + # ruleguard for checking custom rules, without needing to run all of + # golangci-lint. Check the go.mod in the release of golangci-lint that + # we're using for the version of go-critic that it embeds, then check + # the version of ruleguard in go-critic for that tag. + go install github.com/quasilyte/go-ruleguard/cmd/ruleguard@v0.3.13 && \ + # go-releaser for building 'fat binaries' that work cross-platform + go install github.com/goreleaser/goreleaser@v1.6.1 && \ + go install mvdan.cc/sh/v3/cmd/shfmt@v3.7.0 && \ + # nfpm is used with \`make build\` to make release packages + go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.35.1 && \ + # yq v4 is used to process yaml files in coder v2. Conflicts with + # yq v3 used in v1. + go install github.com/mikefarah/yq/v4@v4.44.3 && \ + mv /tmp/bin/yq /tmp/bin/yq4 && \ + go install go.uber.org/mock/mockgen@v0.5.0 && \ + # Reduce image size. + apt-get remove --yes gcc && \ + apt-get autoremove --yes && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + rm -rf /usr/local/go && \ + rm -rf /tmp/go/pkg && \ + rm -rf /tmp/go/src + +# alpine:3.18 +FROM gcr.io/coder-dev-1/alpine@sha256:25fad2a32ad1f6f510e528448ae1ec69a28ef81916a004d3629874104f8a7f70 AS proto +WORKDIR /tmp +RUN apk add curl unzip +RUN curl -L -o protoc.zip https://github.com/protocolbuffers/protobuf/releases/download/v23.4/protoc-23.4-linux-x86_64.zip && \ + unzip protoc.zip && \ + rm protoc.zip + +FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 + +SHELL ["/bin/bash", "-c"] + +# Install packages from apt repositories +ARG DEBIAN_FRONTEND="noninteractive" + +# Updated certificates are necessary to use the teraswitch mirror. +# This must be ran before copying in configuration since the config replaces +# the default mirror with teraswitch. +# Also enable the en_US.UTF-8 locale so that we don't generate multiple locales +# and unminimize to include man pages. +RUN apt-get update && \ + apt-get install --yes ca-certificates locales && \ + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen && \ + locale-gen && \ + yes | unminimize + +COPY files / + +# We used to copy /etc/sudoers.d/* in from files/ but this causes issues with +# permissions and layer caching. Instead, create the file directly. +RUN mkdir -p /etc/sudoers.d && \ + echo 'coder ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/nopasswd && \ + chmod 750 /etc/sudoers.d/ && \ + chmod 640 /etc/sudoers.d/nopasswd + +RUN apt-get update --quiet && apt-get install --yes \ + ansible \ + apt-transport-https \ + apt-utils \ + asciinema \ + bash \ + bash-completion \ + bat \ + bats \ + bind9-dnsutils \ + build-essential \ + ca-certificates \ + cargo \ + cmake \ + containerd.io \ + crypto-policies \ + curl \ + docker-ce \ + docker-ce-cli \ + docker-compose-plugin \ + exa \ + fd-find \ + file \ + fish \ + gettext-base \ + git \ + gnupg \ + google-cloud-sdk \ + google-cloud-sdk-datastore-emulator \ + graphviz \ + helix \ + htop \ + httpie \ + inetutils-tools \ + iproute2 \ + iputils-ping \ + iputils-tracepath \ + jq \ + kubectl \ + language-pack-en \ + less \ + libgbm-dev \ + libssl-dev \ + lsb-release \ + lsof \ + man \ + meld \ + ncdu \ + neovim \ + net-tools \ + openjdk-11-jdk-headless \ + openssh-server \ + openssl \ + packer \ + pkg-config \ + postgresql-16 \ + python3 \ + python3-pip \ + ripgrep \ + rsync \ + screen \ + shellcheck \ + strace \ + sudo \ + tcptraceroute \ + termshark \ + traceroute \ + unzip \ + vim \ + wget \ + xauth \ + zip \ + zsh \ + zstd && \ + # Delete package cache to avoid consuming space in layer + apt-get clean && \ + # Configure FIPS-compliant policies + update-crypto-policies --set FIPS + +# NOTE: In scripts/Dockerfile.base we specifically install Terraform version 1.11.3. +# Installing the same version here to match. +RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.11.3/terraform_1.11.3_linux_amd64.zip" && \ + unzip /tmp/terraform.zip -d /usr/local/bin && \ + rm -f /tmp/terraform.zip && \ + chmod +x /usr/local/bin/terraform && \ + terraform --version + +# Install the docker buildx component. +RUN DOCKER_BUILDX_VERSION=$(curl -s "https://api.github.com/repos/docker/buildx/releases/latest" | grep '"tag_name":' | sed -E 's/.*"(v[^"]+)".*/\\1/') && \ + mkdir -p /usr/local/lib/docker/cli-plugins && \ + curl -Lo /usr/local/lib/docker/cli-plugins/docker-buildx "https://github.com/docker/buildx/releases/download/\${DOCKER_BUILDX_VERSION}/buildx-\${DOCKER_BUILDX_VERSION}.linux-amd64" && \ + chmod a+x /usr/local/lib/docker/cli-plugins/docker-buildx + +# See https://github.com/cli/cli/issues/6175#issuecomment-1235984381 for proof +# the apt repository is unreliable +RUN GH_CLI_VERSION=$(curl -s "https://api.github.com/repos/cli/cli/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\\1/') && \ + curl -L https://github.com/cli/cli/releases/download/v\${GH_CLI_VERSION}/gh_\${GH_CLI_VERSION}_linux_amd64.deb -o gh.deb && \ + dpkg -i gh.deb && \ + rm gh.deb + +# Install Lazygit +# See https://github.com/jesseduffield/lazygit#ubuntu +RUN LAZYGIT_VERSION=$(curl -s "https://api.github.com/repos/jesseduffield/lazygit/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v*([^"]+)".*/\\1/') && \ + curl -Lo lazygit.tar.gz "https://github.com/jesseduffield/lazygit/releases/latest/download/lazygit_\${LAZYGIT_VERSION}_Linux_x86_64.tar.gz" && \ + tar xf lazygit.tar.gz -C /usr/local/bin lazygit && \ + rm lazygit.tar.gz + +# Install doctl +# See https://docs.digitalocean.com/reference/doctl/how-to/install +RUN DOCTL_VERSION=$(curl -s "https://api.github.com/repos/digitalocean/doctl/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\\1/') && \ + curl -L https://github.com/digitalocean/doctl/releases/download/v\${DOCTL_VERSION}/doctl-\${DOCTL_VERSION}-linux-amd64.tar.gz -o doctl.tar.gz && \ + tar xf doctl.tar.gz -C /usr/local/bin doctl && \ + rm doctl.tar.gz + +ARG NVM_INSTALL_SHA=bdea8c52186c4dd12657e77e7515509cda5bf9fa5a2f0046bce749e62645076d +# Install frontend utilities +ENV NVM_DIR=/usr/local/nvm +ENV NODE_VERSION=20.16.0 +RUN mkdir -p $NVM_DIR +RUN curl -o nvm_install.sh https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh && \ + echo "\${NVM_INSTALL_SHA} nvm_install.sh" | sha256sum -c && \ + bash nvm_install.sh && \ + rm nvm_install.sh +RUN source $NVM_DIR/nvm.sh && \ + nvm install $NODE_VERSION && \ + nvm use $NODE_VERSION +ENV PATH=$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH +# Allow patch updates for npm and pnpm +RUN npm install -g npm@10.8.1 --integrity=sha512-Dp1C6SvSMYQI7YHq/y2l94uvI+59Eqbu1EpuKQHQ8p16txXRuRit5gH3Lnaagk2aXDIjg/Iru9pd05bnneKgdw== +RUN npm install -g pnpm@9.15.1 --integrity=sha512-GstWXmGT7769p3JwKVBGkVDPErzHZCYudYfnHRncmKQj3/lTblfqRMSb33kP9pToPCe+X6oj1n4MAztYO+S/zw== + +RUN pnpx playwright@1.47.0 install --with-deps chromium + +# Ensure PostgreSQL binaries are in the users $PATH. +RUN update-alternatives --install /usr/local/bin/initdb initdb /usr/lib/postgresql/16/bin/initdb 100 && \ + update-alternatives --install /usr/local/bin/postgres postgres /usr/lib/postgresql/16/bin/postgres 100 + +# Create links for injected dependencies +RUN ln --symbolic /var/tmp/coder/coder-cli/coder /usr/local/bin/coder && \ + ln --symbolic /var/tmp/coder/code-server/bin/code-server /usr/local/bin/code-server + +# Disable the PostgreSQL systemd service. +# Coder uses a custom timescale container to test the database instead. +RUN systemctl disable \ + postgresql + +# Configure systemd services for CVMs +RUN systemctl enable \ + docker \ + ssh && \ + # Workaround for envbuilder cache probing not working unless the filesystem is modified. + touch /tmp/.envbuilder-systemctl-enable-docker-ssh-workaround + +# Install tools with published releases, where that is the +# preferred/recommended installation method. +ARG CLOUD_SQL_PROXY_VERSION=2.2.0 \ + DIVE_VERSION=0.10.0 \ + DOCKER_GCR_VERSION=2.1.8 \ + GOLANGCI_LINT_VERSION=1.64.8 \ + GRYPE_VERSION=0.61.1 \ + HELM_VERSION=3.12.0 \ + KUBE_LINTER_VERSION=0.6.3 \ + KUBECTX_VERSION=0.9.4 \ + STRIPE_VERSION=1.14.5 \ + TERRAGRUNT_VERSION=0.45.11 \ + TRIVY_VERSION=0.41.0 \ + SYFT_VERSION=1.20.0 \ + COSIGN_VERSION=2.4.3 + +# cloud_sql_proxy, for connecting to cloudsql instances +# the upstream go.mod prevents this from being installed with go install +RUN curl --silent --show-error --location --output /usr/local/bin/cloud_sql_proxy "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v\${CLOUD_SQL_PROXY_VERSION}/cloud-sql-proxy.linux.amd64" && \ + chmod a=rx /usr/local/bin/cloud_sql_proxy && \ + # dive for scanning image layer utilization metrics in CI + curl --silent --show-error --location "https://github.com/wagoodman/dive/releases/download/v\${DIVE_VERSION}/dive_\${DIVE_VERSION}_linux_amd64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- dive && \ + # docker-credential-gcr is a Docker credential helper for pushing/pulling + # images from Google Container Registry and Artifact Registry + curl --silent --show-error --location "https://github.com/GoogleCloudPlatform/docker-credential-gcr/releases/download/v\${DOCKER_GCR_VERSION}/docker-credential-gcr_linux_amd64-\${DOCKER_GCR_VERSION}.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- docker-credential-gcr && \ + # golangci-lint performs static code analysis for our Go code + curl --silent --show-error --location "https://github.com/golangci/golangci-lint/releases/download/v\${GOLANGCI_LINT_VERSION}/golangci-lint-\${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- --strip-components=1 "golangci-lint-\${GOLANGCI_LINT_VERSION}-linux-amd64/golangci-lint" && \ + # Anchore Grype for scanning container images for security issues + curl --silent --show-error --location "https://github.com/anchore/grype/releases/download/v\${GRYPE_VERSION}/grype_\${GRYPE_VERSION}_linux_amd64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- grype && \ + # Helm is necessary for deploying Coder + curl --silent --show-error --location "https://get.helm.sh/helm-v\${HELM_VERSION}-linux-amd64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- --strip-components=1 linux-amd64/helm && \ + # kube-linter for linting Kubernetes objects, including those + # that Helm generates from our charts + curl --silent --show-error --location "https://github.com/stackrox/kube-linter/releases/download/\${KUBE_LINTER_VERSION}/kube-linter-linux" --output /usr/local/bin/kube-linter && \ + # kubens and kubectx for managing Kubernetes namespaces and contexts + curl --silent --show-error --location "https://github.com/ahmetb/kubectx/releases/download/v\${KUBECTX_VERSION}/kubectx_v\${KUBECTX_VERSION}_linux_x86_64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- kubectx && \ + curl --silent --show-error --location "https://github.com/ahmetb/kubectx/releases/download/v\${KUBECTX_VERSION}/kubens_v\${KUBECTX_VERSION}_linux_x86_64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- kubens && \ + # stripe for coder.com billing API + curl --silent --show-error --location "https://github.com/stripe/stripe-cli/releases/download/v\${STRIPE_VERSION}/stripe_\${STRIPE_VERSION}_linux_x86_64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- stripe && \ + # terragrunt for running Terraform and Terragrunt files + curl --silent --show-error --location --output /usr/local/bin/terragrunt "https://github.com/gruntwork-io/terragrunt/releases/download/v\${TERRAGRUNT_VERSION}/terragrunt_linux_amd64" && \ + chmod a=rx /usr/local/bin/terragrunt && \ + # AquaSec Trivy for scanning container images for security issues + curl --silent --show-error --location "https://github.com/aquasecurity/trivy/releases/download/v\${TRIVY_VERSION}/trivy_\${TRIVY_VERSION}_Linux-64bit.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- trivy && \ + # Anchore Syft for SBOM generation + curl --silent --show-error --location "https://github.com/anchore/syft/releases/download/v\${SYFT_VERSION}/syft_\${SYFT_VERSION}_linux_amd64.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/bin --file=- syft && \ + # Sigstore Cosign for artifact signing and attestation + curl --silent --show-error --location --output /usr/local/bin/cosign "https://github.com/sigstore/cosign/releases/download/v\${COSIGN_VERSION}/cosign-linux-amd64" && \ + chmod a=rx /usr/local/bin/cosign + +# We use yq during "make deploy" to manually substitute out fields in +# our helm values.yaml file. See https://github.com/helm/helm/issues/3141 +# +# TODO: update to 4.x, we can't do this now because it included breaking +# changes (yq w doesn't work anymore) +# RUN curl --silent --show-error --location "https://github.com/mikefarah/yq/releases/download/v4.9.0/yq_linux_amd64.tar.gz" | \ +# tar --extract --gzip --directory=/usr/local/bin --file=- ./yq_linux_amd64 && \ +# mv /usr/local/bin/yq_linux_amd64 /usr/local/bin/yq + +RUN curl --silent --show-error --location --output /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/3.3.0/yq_linux_amd64" && \ + chmod a=rx /usr/local/bin/yq + +# Install GoLand. +RUN mkdir --parents /usr/local/goland && \ + curl --silent --show-error --location "https://download.jetbrains.com/go/goland-2021.2.tar.gz" | \ + tar --extract --gzip --directory=/usr/local/goland --file=- --strip-components=1 && \ + ln --symbolic /usr/local/goland/bin/goland.sh /usr/local/bin/goland + +# Install Antlrv4, needed to generate paramlang lexer/parser +RUN curl --silent --show-error --location --output /usr/local/lib/antlr-4.9.2-complete.jar "https://www.antlr.org/download/antlr-4.9.2-complete.jar" +ENV CLASSPATH="/usr/local/lib/antlr-4.9.2-complete.jar:\${PATH}" + +# Add coder user and allow use of docker/sudo +RUN useradd coder \ + --create-home \ + --shell=/bin/bash \ + --groups=docker \ + --uid=1000 \ + --user-group + +# Adjust OpenSSH config +RUN echo "PermitUserEnvironment yes" >>/etc/ssh/sshd_config && \ + echo "X11Forwarding yes" >>/etc/ssh/sshd_config && \ + echo "X11UseLocalhost no" >>/etc/ssh/sshd_config + +# We avoid copying the extracted directory since COPY slows to minutes when there +# are a lot of small files. +COPY --from=go /usr/local/go.tar.gz /usr/local/go.tar.gz +RUN mkdir /usr/local/go && \ + tar --extract --gzip --directory=/usr/local/go --file=/usr/local/go.tar.gz --strip-components=1 + +ENV PATH=$PATH:/usr/local/go/bin + +RUN update-alternatives --install /usr/local/bin/gofmt gofmt /usr/local/go/bin/gofmt 100 + +COPY --from=go /tmp/bin /usr/local/bin +COPY --from=rust-utils /tmp/bin /usr/local/bin +COPY --from=proto /tmp/bin /usr/local/bin +COPY --from=proto /tmp/include /usr/local/bin/include + +USER coder + +# Ensure go bins are in the 'coder' user's path. Note that no go bins are +# installed in this docker file, as they'd be mounted over by the persistent +# home volume. +ENV PATH="/home/coder/go/bin:\${PATH}" + +# This setting prevents Go from using the public checksum database for +# our module path prefixes. It is required because these are in private +# repositories that require authentication. +# +# For details, see: https://golang.org/ref/mod#private-modules +ENV GOPRIVATE="coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder" + +# Increase memory allocation to NodeJS +ENV NODE_OPTIONS="--max-old-space-size=8192" +`; + +const templateTerraform = `terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.2.0-pre0" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 3.0.0" + } + } +} + +locals { + // These are cluster service addresses mapped to Tailscale nodes. Ask Dean or + // Kyle for help. + docker_host = { + "" = "tcp://dogfood-ts-cdr-dev.tailscale.svc.cluster.local:2375" + "us-pittsburgh" = "tcp://dogfood-ts-cdr-dev.tailscale.svc.cluster.local:2375" + // For legacy reasons, this host is labelled \`eu-helsinki\` but it's + // actually in Germany now. + "eu-helsinki" = "tcp://katerose-fsn-cdr-dev.tailscale.svc.cluster.local:2375" + "ap-sydney" = "tcp://wolfgang-syd-cdr-dev.tailscale.svc.cluster.local:2375" + "sa-saopaulo" = "tcp://oberstein-sao-cdr-dev.tailscale.svc.cluster.local:2375" + "za-cpt" = "tcp://schonkopf-cpt-cdr-dev.tailscale.svc.cluster.local:2375" + } + + repo_base_dir = data.coder_parameter.repo_base_dir.value == "~" ? "/home/coder" : replace(data.coder_parameter.repo_base_dir.value, "/^~\\//", "/home/coder/") + repo_dir = replace(try(module.git-clone[0].repo_dir, ""), "/^~\\//", "/home/coder/") + container_name = "coder-\${data.coder_workspace_owner.me.name}-\${lower(data.coder_workspace.me.name)}" +} + +data "coder_parameter" "repo_base_dir" { + type = "string" + name = "Coder Repository Base Directory" + default = "~" + description = "The directory specified will be created (if missing) and [coder/coder](https://github.com/coder/coder) will be automatically cloned into [base directory]/coder 🪄." + mutable = true +} + +data "coder_parameter" "image_type" { + type = "string" + name = "Coder Image" + default = "codercom/oss-dogfood:latest" + description = "The Docker image used to run your workspace. Choose between nix and non-nix images." + option { + icon = "/icon/coder.svg" + name = "Dogfood (Default)" + value = "codercom/oss-dogfood:latest" + } + option { + icon = "/icon/nix.svg" + name = "Dogfood Nix (Experimental)" + value = "codercom/oss-dogfood-nix:latest" + } +} + +data "coder_parameter" "region" { + type = "string" + name = "Region" + icon = "/emojis/1f30e.png" + default = "us-pittsburgh" + option { + icon = "/emojis/1f1fa-1f1f8.png" + name = "Pittsburgh" + value = "us-pittsburgh" + } + option { + icon = "/emojis/1f1e9-1f1ea.png" + name = "Falkenstein" + // For legacy reasons, this host is labelled \`eu-helsinki\` but it's + // actually in Germany now. + value = "eu-helsinki" + } + option { + icon = "/emojis/1f1e6-1f1fa.png" + name = "Sydney" + value = "ap-sydney" + } + option { + icon = "/emojis/1f1e7-1f1f7.png" + name = "São Paulo" + value = "sa-saopaulo" + } + option { + icon = "/emojis/1f1ff-1f1e6.png" + name = "Cape Town" + value = "za-cpt" + } +} + +data "coder_parameter" "res_mon_memory_threshold" { + type = "number" + name = "Memory usage threshold" + default = 80 + description = "The memory usage threshold used in resources monitoring to trigger notifications." + mutable = true + validation { + min = 0 + max = 100 + } +} + +data "coder_parameter" "res_mon_volume_threshold" { + type = "number" + name = "Volume usage threshold" + default = 90 + description = "The volume usage threshold used in resources monitoring to trigger notifications." + mutable = true + validation { + min = 0 + max = 100 + } +} + +data "coder_parameter" "res_mon_volume_path" { + type = "string" + name = "Volume path" + default = "/home/coder" + description = "The path monitored in resources monitoring to trigger notifications." + mutable = true +} + +provider "docker" { + host = lookup(local.docker_host, data.coder_parameter.region.value) +} + +provider "coder" {} + +data "coder_external_auth" "github" { + id = "github" +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} +data "coder_workspace_tags" "tags" { + tags = { + "cluster" : "dogfood-v2" + "env" : "gke" + } +} + +module "slackme" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/slackme/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + auth_provider_id = "slack" +} + +module "dotfiles" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/dotfiles/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id +} + +module "git-clone" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/git-clone/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + url = "https://github.com/coder/coder" + base_dir = local.repo_base_dir +} + +module "personalize" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/personalize/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id +} + +module "code-server" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/code-server/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + folder = local.repo_dir + auto_install_extensions = true +} + +module "vscode-web" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/vscode-web/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + folder = local.repo_dir + extensions = ["github.copilot"] + auto_install_extensions = true # will install extensions from the repos .vscode/extensions.json file + accept_license = true +} + +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/jetbrains-gateway/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + agent_name = "dev" + folder = local.repo_dir + jetbrains_ides = ["GO", "WS"] + default = "GO" + latest = true +} + +module "filebrowser" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/filebrowser/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + agent_name = "dev" +} + +module "coder-login" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/coder-login/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id +} + +module "cursor" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/modules/cursor/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + folder = local.repo_dir +} + +module "zed" { + count = data.coder_workspace.me.start_count + source = "./zed" + agent_id = coder_agent.dev.id + folder = local.repo_dir +} + +resource "coder_agent" "dev" { + arch = "amd64" + os = "linux" + dir = local.repo_dir + env = { + OIDC_TOKEN : data.coder_workspace_owner.me.oidc_access_token, + } + startup_script_behavior = "blocking" + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + metadata { + display_name = "CPU Usage" + key = "cpu_usage" + order = 0 + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "ram_usage" + order = 1 + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "cpu_usage_host" + order = 2 + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage (Host)" + key = "ram_usage_host" + order = 3 + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Swap Usage (Host)" + key = "swap_usage_host" + order = 4 + script = <&1 | awk ' $0 ~ "Word of the Day: [A-z]+" { print $5; exit }' + EOT + interval = 86400 + timeout = 5 + } + + resources_monitoring { + memory { + enabled = true + threshold = data.coder_parameter.res_mon_memory_threshold.value + } + volume { + enabled = true + threshold = data.coder_parameter.res_mon_volume_threshold.value + path = data.coder_parameter.res_mon_volume_path.value + } + } + + startup_script = <<-EOT + #!/usr/bin/env bash + set -eux -o pipefail + + # Allow synchronization between scripts. + trap 'touch /tmp/.coder-startup-script.done' EXIT + + # Start Docker service + sudo service docker start + # Install playwright dependencies + # We want to use the playwright version from site/package.json + # Check if the directory exists At workspace creation as the coder_script runs in parallel so clone might not exist yet. + while ! [[ -f "\${local.repo_dir}/site/package.json" ]]; do + sleep 1 + done + cd "\${local.repo_dir}" && make clean + cd "\${local.repo_dir}/site" && pnpm install + EOT + + shutdown_script = <<-EOT + #!/usr/bin/env bash + set -eux -o pipefail + + # Stop the Docker service to prevent errors during workspace destroy. + sudo service docker stop + EOT +} + +# Add a cost so we get some quota usage in dev.coder.com +resource "coder_metadata" "home_volume" { + resource_id = docker_volume.home_volume.id + daily_cost = 1 +} + +resource "docker_volume" "home_volume" { + name = "coder-\${data.coder_workspace.me.id}-home" + # Protect the volume from being deleted due to changes in attributes. + lifecycle { + ignore_changes = all + } + # Add labels in Docker to keep track of orphan resources. + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + # This field becomes outdated if the workspace is renamed but can + # be useful for debugging or cleaning out dangling volumes. + labels { + label = "coder.workspace_name_at_creation" + value = data.coder_workspace.me.name + } +} + +data "docker_registry_image" "dogfood" { + name = data.coder_parameter.image_type.value +} + +resource "docker_image" "dogfood" { + name = "\${data.coder_parameter.image_type.value}@\${data.docker_registry_image.dogfood.sha256_digest}" + pull_triggers = [ + data.docker_registry_image.dogfood.sha256_digest, + sha1(join("", [for f in fileset(path.module, "files/*") : filesha1(f)])), + filesha1("Dockerfile"), + filesha1("nix.hash"), + ] + keep_locally = true +} + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = docker_image.dogfood.name + name = local.container_name + # Hostname makes the shell more user friendly: coder@my-workspace:~$ + hostname = data.coder_workspace.me.name + # Use the docker gateway if the access URL is 127.0.0.1 + entrypoint = ["sh", "-c", coder_agent.dev.init_script] + # CPU limits are unnecessary since Docker will load balance automatically + memory = data.coder_workspace_owner.me.name == "code-asher" ? 65536 : 32768 + runtime = "sysbox-runc" + # Ensure the workspace is given time to execute shutdown scripts. + destroy_grace_seconds = 60 + stop_timeout = 60 + stop_signal = "SIGINT" + env = [ + "CODER_AGENT_TOKEN=\${coder_agent.dev.token}", + "USE_CAP_NET_ADMIN=true", + "CODER_PROC_PRIO_MGMT=1", + "CODER_PROC_OOM_SCORE=10", + "CODER_PROC_NICE_SCORE=1", + "CODER_AGENT_DEVCONTAINERS_ENABLE=1", + ] + host { + host = "host.docker.internal" + ip = "host-gateway" + } + volumes { + container_path = "/home/coder/" + volume_name = docker_volume.home_volume.name + read_only = false + } + capabilities { + add = ["CAP_NET_ADMIN", "CAP_SYS_NICE"] + } + # Add labels in Docker to keep track of orphan resources. + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.owner_id" + value = data.coder_workspace_owner.me.id + } + labels { + label = "coder.workspace_id" + value = data.coder_workspace.me.id + } + labels { + label = "coder.workspace_name" + value = data.coder_workspace.me.name + } +} + +resource "coder_metadata" "container_info" { + count = data.coder_workspace.me.start_count + resource_id = docker_container.workspace[0].id + item { + key = "memory" + value = docker_container.workspace[0].memory + } + item { + key = "runtime" + value = docker_container.workspace[0].runtime + } + item { + key = "region" + value = data.coder_parameter.region.option[index(data.coder_parameter.region.option.*.value, data.coder_parameter.region.value)].name + } +} +`; diff --git a/site/src/pages/ChatPage/ChatToolInvocation.tsx b/site/src/pages/ChatPage/ChatToolInvocation.tsx new file mode 100644 index 0000000000000..6f418edabb4a5 --- /dev/null +++ b/site/src/pages/ChatPage/ChatToolInvocation.tsx @@ -0,0 +1,872 @@ +import type { ToolCall, ToolResult } from "@ai-sdk/provider-utils"; +import { useTheme } from "@emotion/react"; +import ArticleIcon from "@mui/icons-material/Article"; +import BuildIcon from "@mui/icons-material/Build"; +import CheckCircle from "@mui/icons-material/CheckCircle"; +import CodeIcon from "@mui/icons-material/Code"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ErrorIcon from "@mui/icons-material/Error"; +import FileUploadIcon from "@mui/icons-material/FileUpload"; +import PersonIcon from "@mui/icons-material/Person"; +import SettingsIcon from "@mui/icons-material/Settings"; +import CircularProgress from "@mui/material/CircularProgress"; +import Tooltip from "@mui/material/Tooltip"; +import type * as TypesGen from "api/typesGenerated"; +import { Avatar } from "components/Avatar/Avatar"; +import { InfoIcon } from "lucide-react"; +import type React from "react"; +import { type FC, memo, useMemo, useState } from "react"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism"; +import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism"; +import { TabLink, Tabs, TabsList } from "../../components/Tabs/Tabs"; + +interface ChatToolInvocationProps { + toolInvocation: ChatToolInvocation; +} + +export const ChatToolInvocation: FC = ({ + toolInvocation, +}) => { + const theme = useTheme(); + const friendlyName = useMemo(() => { + return toolInvocation.toolName + .replace("coder_", "") + .replace(/_/g, " ") + .replace(/\b\w/g, (char) => char.toUpperCase()); + }, [toolInvocation.toolName]); + + const hasError = useMemo(() => { + if (toolInvocation.state !== "result") { + return false; + } + return ( + typeof toolInvocation.result === "object" && + toolInvocation.result !== null && + "error" in toolInvocation.result + ); + }, [toolInvocation]); + const statusColor = useMemo(() => { + if (toolInvocation.state !== "result") { + return theme.palette.info.main; + } + return hasError ? theme.palette.error.main : theme.palette.success.main; + }, [toolInvocation, hasError, theme]); + const tooltipContent = useMemo(() => { + return ( + + {JSON.stringify(toolInvocation, null, 2)} + + ); + }, [toolInvocation, theme.shape.borderRadius, theme.spacing]); + + return ( +
+
+ {toolInvocation.state !== "result" && ( + + )} + {toolInvocation.state === "result" ? ( + hasError ? ( + + ) : ( + + ) + ) : null} +
+ {friendlyName} +
+ + + +
+ {toolInvocation.state === "result" ? ( + + ) : ( + + )} +
+ ); +}; + +const ChatToolInvocationCallPreview: FC<{ + toolInvocation: Extract< + ChatToolInvocation, + { state: "call" | "partial-call" } + >; +}> = memo(({ toolInvocation }) => { + const theme = useTheme(); + + let content: React.ReactNode; + switch (toolInvocation.toolName) { + case "coder_upload_tar_file": + content = ( + + ); + break; + } + + if (!content) { + return null; + } + + return
{content}
; +}); + +const ChatToolInvocationResultPreview: FC<{ + toolInvocation: Extract; +}> = memo(({ toolInvocation }) => { + const theme = useTheme(); + + if (!toolInvocation.result) { + return null; + } + + if ( + typeof toolInvocation.result === "object" && + "error" in toolInvocation.result + ) { + return null; + } + + let content: React.ReactNode; + switch (toolInvocation.toolName) { + case "coder_get_workspace": + case "coder_create_workspace": + content = ( +
+ {toolInvocation.result.template_icon && ( + {toolInvocation.result.template_display_name + )} +
+
+ {toolInvocation.result.name} +
+
+ {toolInvocation.result.template_display_name} +
+
+
+ ); + break; + case "coder_list_workspaces": + content = ( +
+ {toolInvocation.result.map((workspace) => ( +
+ {workspace.template_icon && ( + {workspace.template_display_name + )} +
+
+ {workspace.name} +
+
+ {workspace.template_display_name} +
+
+
+ ))} +
+ ); + break; + case "coder_list_templates": { + const templates = toolInvocation.result; + content = ( +
+ {templates.map((template) => ( +
+ +
+
+ {template.name} +
+
+ {template.description} +
+
+
+ ))} + {templates.length === 0 &&
No templates found.
} +
+ ); + break; + } + case "coder_template_version_parameters": { + const params = toolInvocation.result; + content = ( +
+ + {params.length > 0 + ? `${params.length} parameter(s)` + : "No parameters"} +
+ ); + break; + } + case "coder_get_authenticated_user": { + const user = toolInvocation.result; + content = ( +
+ + + +
+
+ {user.username} +
+
+ {user.email} +
+
+
+ ); + break; + } + case "coder_create_workspace_build": { + const build = toolInvocation.result; + content = ( +
+ + Build #{build.build_number} ({build.transition}) status:{" "} + {build.status} +
+ ); + break; + } + case "coder_create_template_version": { + const version = toolInvocation.result; + content = ( +
+ +
+
{version.name}
+ {version.message && ( +
+ {version.message} +
+ )} +
+
+ ); + break; + } + case "coder_get_workspace_agent_logs": + case "coder_get_workspace_build_logs": + case "coder_get_template_version_logs": { + const logs = toolInvocation.result; + const totalLines = logs.length; + const maxLinesToShow = 5; + const lastLogs = logs.slice(-maxLinesToShow); + const hiddenLines = totalLines - lastLogs.length; + + const totalLinesText = `${totalLines} log line${totalLines !== 1 ? "s" : ""}`; + const hiddenLinesText = + hiddenLines > 0 + ? `... hiding ${hiddenLines} more line${hiddenLines !== 1 ? "s" : ""} ...` + : null; + + const logsToShow = hiddenLinesText + ? [hiddenLinesText, ...lastLogs] + : lastLogs; + + content = ( +
+
+ + Retrieved {totalLinesText}. +
+ {logsToShow.length > 0 && ( + + {logsToShow.join("\n")} + + )} +
+ ); + break; + } + case "coder_update_template_active_version": + content = ( +
+ + {toolInvocation.result} +
+ ); + break; + case "coder_upload_tar_file": + content = ( + + ); + break; + case "coder_create_template": { + const template = toolInvocation.result; + content = ( +
+ {template.display_name +
+
+ {template.name} +
+
+ {template.display_name} +
+
+
+ ); + break; + } + case "coder_delete_template": + content = ( +
+ + {toolInvocation.result} +
+ ); + break; + case "coder_get_template_version": { + const version = toolInvocation.result; + content = ( +
+ +
+
{version.name}
+ {version.message && ( +
+ {version.message} +
+ )} +
+
+ ); + break; + } + case "coder_download_tar_file": { + const files = toolInvocation.result; + content = ; + break; + } + // Add default case or handle other tools if necessary + } + return ( +
+ {content} +
+ ); +}); + +// New component to preview files with tabs +const FilePreview: FC<{ files: Record; prefix?: string }> = + memo(({ files, prefix }) => { + const theme = useTheme(); + const [selectedTab, setSelectedTab] = useState(0); + const fileEntries = useMemo(() => Object.entries(files), [files]); + + if (fileEntries.length === 0) { + return null; + } + + const handleTabChange = (index: number) => { + setSelectedTab(index); + }; + + const getLanguage = (filename: string): string => { + if (filename.includes("Dockerfile")) { + return "dockerfile"; + } + const extension = filename.split(".").pop()?.toLowerCase(); + switch (extension) { + case "tf": + return "hcl"; + case "json": + return "json"; + case "yaml": + case "yml": + return "yaml"; + case "js": + case "jsx": + return "javascript"; + case "ts": + case "tsx": + return "typescript"; + case "py": + return "python"; + case "go": + return "go"; + case "rb": + return "ruby"; + case "java": + return "java"; + case "sh": + return "bash"; + case "md": + return "markdown"; + default: + return "plaintext"; + } + }; + + // Get filename and content based on the selectedTab index + const [selectedFilename, selectedContent] = fileEntries[selectedTab] ?? [ + "", + "", + ]; + + return ( +
+ {prefix && ( +
+ + {prefix} +
+ )} + {/* Use custom Tabs component with active prop */} + + + {fileEntries.map(([filename], index) => ( + { + e.preventDefault(); // Prevent any potential default link behavior + handleTabChange(index); + }} + > + {filename} + + ))} + + + + {selectedContent} + +
+ ); + }); + +// TODO: generate these from codersdk/toolsdk.go. +export type ChatToolInvocation = + | ToolInvocation< + "coder_get_workspace", + { + workspace_id: string; + }, + TypesGen.Workspace + > + | ToolInvocation< + "coder_create_workspace", + { + user: string; + template_version_id: string; + name: string; + rich_parameters: Record; + }, + TypesGen.Workspace + > + | ToolInvocation< + "coder_list_workspaces", + { + owner: string; + }, + Pick< + TypesGen.Workspace, + | "id" + | "name" + | "template_id" + | "template_name" + | "template_display_name" + | "template_icon" + | "template_active_version_id" + | "outdated" + >[] + > + | ToolInvocation< + "coder_list_templates", + Record, + Pick< + TypesGen.Template, + | "id" + | "name" + | "description" + | "active_version_id" + | "active_user_count" + >[] + > + | ToolInvocation< + "coder_template_version_parameters", + { + template_version_id: string; + }, + TypesGen.TemplateVersionParameter[] + > + | ToolInvocation< + "coder_get_authenticated_user", + Record, + TypesGen.User + > + | ToolInvocation< + "coder_create_workspace_build", + { + workspace_id: string; + template_version_id?: string; + transition: "start" | "stop" | "delete"; + }, + TypesGen.WorkspaceBuild + > + | ToolInvocation< + "coder_create_template_version", + { + template_id?: string; + file_id: string; + }, + TypesGen.TemplateVersion + > + | ToolInvocation< + "coder_get_workspace_agent_logs", + { + workspace_agent_id: string; + }, + string[] + > + | ToolInvocation< + "coder_get_workspace_build_logs", + { + workspace_build_id: string; + }, + string[] + > + | ToolInvocation< + "coder_get_template_version_logs", + { + template_version_id: string; + }, + string[] + > + | ToolInvocation< + "coder_get_template_version", + { + template_version_id: string; + }, + TypesGen.TemplateVersion + > + | ToolInvocation< + "coder_download_tar_file", + { + file_id: string; + }, + Record + > + | ToolInvocation< + "coder_update_template_active_version", + { + template_id: string; + template_version_id: string; + }, + string + > + | ToolInvocation< + "coder_upload_tar_file", + { + files: Record; + }, + TypesGen.UploadResponse + > + | ToolInvocation< + "coder_create_template", + { + name: string; + }, + TypesGen.Template + > + | ToolInvocation< + "coder_delete_template", + { + template_id: string; + }, + string + >; + +type ToolInvocation = + | ({ + state: "partial-call"; + step?: number; + } & ToolCall) + | ({ + state: "call"; + step?: number; + } & ToolCall) + | ({ + state: "result"; + step?: number; + } & ToolResult< + N, + A, + | R + | { + error: string; + } + >); diff --git a/site/src/pages/ChatPage/LanguageModelSelector.tsx b/site/src/pages/ChatPage/LanguageModelSelector.tsx new file mode 100644 index 0000000000000..2170be22b3196 --- /dev/null +++ b/site/src/pages/ChatPage/LanguageModelSelector.tsx @@ -0,0 +1,73 @@ +import { useTheme } from "@emotion/react"; +import FormControl from "@mui/material/FormControl"; +import InputLabel from "@mui/material/InputLabel"; +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; +import { deploymentLanguageModels } from "api/queries/deployment"; +import type { LanguageModel } from "api/typesGenerated"; // Assuming types live here based on project structure +import { Loader } from "components/Loader/Loader"; +import type { FC } from "react"; +import { useQuery } from "react-query"; +import { useChatContext } from "./ChatLayout"; + +export const LanguageModelSelector: FC = () => { + const theme = useTheme(); + const { setSelectedModel, modelConfig, selectedModel } = useChatContext(); + const { + data: languageModelConfig, + isLoading, + error, + } = useQuery(deploymentLanguageModels()); + + if (isLoading) { + return ; + } + + if (error || !languageModelConfig) { + console.error("Failed to load language models:", error); + return ( +
Error loading models.
+ ); + } + + const models = Array.from(languageModelConfig.models).toSorted((a, b) => { + // Sort by provider first, then by display name + const compareProvider = a.provider.localeCompare(b.provider); + if (compareProvider !== 0) { + return compareProvider; + } + return a.display_name.localeCompare(b.display_name); + }); + + if (models.length === 0) { + return ( +
+ No language models available. +
+ ); + } + + return ( + + Model + + + ); +}; diff --git a/site/src/router.tsx b/site/src/router.tsx index 76e9adfd00b09..534d4037d02b3 100644 --- a/site/src/router.tsx +++ b/site/src/router.tsx @@ -1,4 +1,6 @@ import { GlobalErrorBoundary } from "components/ErrorBoundary/GlobalErrorBoundary"; +import { ChatLayout } from "pages/ChatPage/ChatLayout"; +import { ChatMessages } from "pages/ChatPage/ChatMessages"; import { TemplateRedirectController } from "pages/TemplatePage/TemplateRedirectController"; import { Suspense, lazy } from "react"; import { @@ -31,6 +33,7 @@ const NotFoundPage = lazy(() => import("./pages/404Page/404Page")); const DeploymentSettingsLayout = lazy( () => import("./modules/management/DeploymentSettingsLayout"), ); +const ChatLanding = lazy(() => import("./pages/ChatPage/ChatLanding")); const DeploymentConfigProvider = lazy( () => import("./modules/management/DeploymentConfigProvider"), ); @@ -422,6 +425,11 @@ export const router = createBrowserRouter( } /> + }> + } /> + } /> + + }> } /> pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy