diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3f1a9f2..c6fae7e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -312,11 +312,11 @@ jobs: }, "arm64": { cpu: { - checksum: "51ce309f4f5d055fd226a154784812fce10396ad8f8b3478b94fff24569a5063", + checksum: "02be1bb313324e5f5d198acec61a68059f1694d4d13189ef52e9a3b427087ccb", url: "https://s3.firecoder.cc/macOS-arm64", }, metal: { - checksum: "51ce309f4f5d055fd226a154784812fce10396ad8f8b3478b94fff24569a5063", + checksum: "02be1bb313324e5f5d198acec61a68059f1694d4d13189ef52e9a3b427087ccb", url: "https://s3.firecoder.cc/macOS-arm64-metal" }, } diff --git a/README.md b/README.md index 78b7756..55e35f1 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ Feel free to share your feedback or report any issues on our [GitHub repository] - **Easy Installation**: Simply install extensions and start using FireCoder. - **Completion Auto Mode**: Enjoy the convenience of automatic code suggestions. - **Manual Mode**: Switch between auto mode and manual mode for code suggestions. -- **Platform Support**: FireCoder supports Windows, Linux, and macOS. +- **Chat Mode**: Interact with FireCoder through natural language, receiving code suggestions and guidance tailored to your needs. - **Multi-line Code Suggestions**: Enhance your coding experience with multi-line code suggestions. +- **Platform Support**: FireCoder supports Windows, Linux, and macOS. ### New Experimental Features: - **GPU Support**: You can now utilize GPU support by adjusting the `firecoder.experimental.useGpu` settings in configuration. -- **Chat Mode**: Explore the new chat mode functionality by adjusting the `firecoder.experimental.chat` settings in configuration. ## Getting Started @@ -27,29 +27,13 @@ Feel free to share your feedback or report any issues on our [GitHub repository] ## Roadmap -- **Support Chat Mode (WIP - Expected Soon):** - Enable a chat mode where users can interact with the FireCoder to get answers to their coding questions. A conversational interface to enhance your coding experience. - -- **Custom Commands (WIP - Expected Soon):** - Create a set of custom commands for common tasks such as writing tests, fixing easy bugs, and more. Tailor FireCoder to your specific development needs with ease. - -- **Generate Commit Descriptions (Scheduled for Q1 2024):** - Automate the process of generating descriptive commit messages. Improve the clarity and consistency of your version control history effortlessly. - -- **Easy GPU Support (Scheduled for Q1 2024):** - Simplify GPU support installation. Just install FireCoder, and let it seamlessly utilize GPU resources to enhance performance and accelerate your coding tasks. - -- **Pull Request Reviews (Scheduled for Q1 2024):** - Facilitate pull request reviews directly within FireCoder. Streamline the code review process and make collaboration smoother for your development team. - -- **Cloud Service (Scheduled for Q1 2024):** - Introduce a cloud service to provide optimal performance for users with limited resources on their local machines. Enjoy the power of FireCoder without worrying about hardware constraints. - -- **IntelliJ IDEA Support (Scheduled for Q2 2024):** - Extend FireCoder's compatibility to include support for IntelliJ IDEA, broadening the range of IDEs where you can seamlessly integrate our AI assistant. - -- **Self-Hosting for Teams (Scheduled for Q2 2024):** - Integrate a self-hosting service designed for larger teams. Users can install a central service, enabling a team of developers to leverage FireCoder collaboratively. +- Custom Commands +- Generate Commit Descriptions +- Easy GPU Support +- Pull Request Reviews +- Cloud Service +- IntelliJ IDEA Support +- Self-Hosting for Teams We're committed to making FireCoder an indispensable part of your coding toolkit. Stay tuned for updates as we bring these exciting features to life! @@ -58,7 +42,6 @@ We're committed to making FireCoder an indispensable part of your coding toolkit #### Minimal Requirements - **Disk Space:** Minimum 2 GB of free disk space. -- **CPU:** Support AVX2. - **RAM:** Minimum 1 GB of available memory. These are the minimum specifications to run the FireCoder. The extension should function, but performance may be limited. @@ -66,7 +49,6 @@ These are the minimum specifications to run the FireCoder. The extension should #### Optimal Requirements - **Disk Space:** 14 GB or more of free disk space. -- **CPU:** Support AVX2. - **RAM:** 6 GB or more of available memory. If you intend to utilize the high-power and large model features, it is advisable to use a system with enhanced specifications to achieve better performance. @@ -80,11 +62,4 @@ If you intend to utilize the high-power and large model features, it is advisabl ## Release Notes -### Version 0.0.4 - -- Auto download model and server. - -### Version 0.0.1 (Initial Release) - -- Welcome to the first release of FireCoder. -- Download now to explore the possibilities. +See [Github Releases](https://github.com/FireCoderAI/firecoder/releases) diff --git a/package-lock.json b/package-lock.json index 76d17c3..6204ef6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "firecoder", - "version": "0.0.30", + "version": "0.0.33", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "firecoder", - "version": "0.0.30", + "version": "0.0.33", "license": "MIT", "dependencies": { "@grafana/faro-core": "^1.3.5", "@grafana/faro-web-sdk": "^1.3.5", "@langchain/community": "^0.0.27", + "@supabase/supabase-js": "^2.42.7", "@xenova/transformers": "^2.17.1", "langchain": "^0.1.17" }, @@ -988,6 +989,73 @@ "optional": true, "peer": true }, + "node_modules/@supabase/auth-js": { + "version": "2.64.1", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.64.1.tgz", + "integrity": "sha512-tA2PXLoWEzhD0N1Vysree+HftfeWBbFV0E+taND5rj/pZTjkwKq/9GlrnXkbs5pnw+tsnABDRo2WLZmymihGdA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.3.1.tgz", + "integrity": "sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz", + "integrity": "sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.4.tgz", + "integrity": "sha512-wdq+2hZpgw0r2ldRs87d3U08Y8BrsO1bZxPNqbImpYshAEkusDz4vufR8KaqujKxqewmXS6YnUhuRVdvSEIKCA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.14.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz", + "integrity": "sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.42.7", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.42.7.tgz", + "integrity": "sha512-BEIEYe5KJpzd8Z3k4CKyjNuBmgSihDdE8MJO/Fg7O5h/lQg8qOp1MMWLWPP5aVKt4TYled/W82ePNJflqc2JbQ==", + "dependencies": { + "@supabase/auth-js": "2.64.1", + "@supabase/functions-js": "2.3.1", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.15.2", + "@supabase/realtime-js": "2.9.4", + "@supabase/storage-js": "2.5.5" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -1057,6 +1125,11 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.4.tgz", + "integrity": "sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA==" + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -1079,6 +1152,14 @@ "integrity": "sha512-LCe1FvCDMJKkPdLVGYhP0HRJ1PDop2gRVm/zFHiOKwYLBRS7vEV3uOOUId4HMV+L1IxqyS+IZXMmlSMRbZGIAw==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.12.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz", @@ -6782,6 +6863,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml2js": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", diff --git a/package.json b/package.json index 7e25865..1503ac7 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "bugs": "https://github.com/FireCoderAI/firecoder/issues", "publisher": "FireCoder", "license": "MIT", - "version": "0.0.30", + "version": "0.0.33", "engines": { "vscode": "^1.84.0" }, @@ -36,7 +36,8 @@ "view/title": [ { "command": "firecoder.startNewChat", - "group": "navigation@1" + "group": "navigation", + "when": "view === firecoder.chat-gui" } ] }, @@ -46,8 +47,7 @@ "type": "webview", "id": "firecoder.chat-gui", "name": "", - "visibility": "visible", - "when": "config.firecoder.experimental.chat || config.firecoder.cloud.use" + "visibility": "visible" } ] }, @@ -55,7 +55,7 @@ "activitybar": [ { "id": "firecoder", - "title": "Firecoder Chat", + "title": "FireCoder Chat", "icon": "images/fc.svg" } ] @@ -67,8 +67,14 @@ }, { "command": "firecoder.startNewChat", - "title": "New Chat", + "title": "FireCoder: New Chat", "icon": "$(add)" + }, + { + "command": "firecoder.login", + "title": "FireCoder: Login", + "icon": "$(key)", + "description": "Login to your account" } ], "keybindings": [ @@ -102,7 +108,7 @@ "default": false, "description": "Use experimental GPU Metal support for macOS." }, - "firecoder.experimental.chat": { + "firecoder.local.chat.use": { "type": "boolean", "default": false, "description": "Enable experimental chat feature." @@ -146,17 +152,22 @@ "firecoder.cloud.endpoint": { "type": "string", "default": "https://llm-api.firecoder.cc/v1", - "description": "" - }, - "firecoder.cloud.apiToken": { - "type": "string", - "default": "", - "description": "" + "description": "Endpoint to cloud." }, "firecoder.cloud.use": { "type": "boolean", "default": false, - "description": "" + "description": "Enable cloud feature. User should provide their own credentials by login. Enable this to use cloud features." + }, + "firecoder.cloud.autocomplete.use": { + "type": "boolean", + "default": false, + "markdownDescription": "Enable cloud autocomplete feature. Only applies when `#firecoder.cloud.use#` is set to `true`." + }, + "firecoder.cloud.chat.use": { + "type": "boolean", + "default": false, + "markdownDescription": "Enable cloud chat feature. Only applies when `#firecoder.cloud.use#` is set to `true`." }, "firecoder.server.usePreRelease": { "type": "boolean", @@ -201,6 +212,7 @@ "@grafana/faro-core": "^1.3.5", "@grafana/faro-web-sdk": "^1.3.5", "@langchain/community": "^0.0.27", + "@supabase/supabase-js": "^2.42.7", "@xenova/transformers": "^2.17.1", "langchain": "^0.1.17" } diff --git a/src/common/auth/authServer.ts b/src/common/auth/authServer.ts new file mode 100644 index 0000000..d63f516 --- /dev/null +++ b/src/common/auth/authServer.ts @@ -0,0 +1,130 @@ +import http from "node:http"; +import Logger from "../logger"; + +class AuthServer { + private server: http.Server | null = null; + private refreshToken: string | null = null; + private timeout: number = 1000 * 60 * 10; + constructor({ timeout = 10000 }: { timeout?: number } = {}) { + this.timeout = timeout; + } + + /** + * @description Start the auth server. + * The auth server will listen on the following URL: http://localhost:39729/refreshToken + * If the auth server is already running, the function will do nothing. + * + */ + public async startServer() { + if (this.server) { + Logger.info("Auth server is already running", { + component: "auth", + sendTelemetry: false, + }); + return; + } + + const requestListener = (req: any, res: any) => { + // get the token from the request + // example of request: + // GET http://localhost:39729/auth/v1/callback?refreshToken=Ju8HpwNlsNg5K3LPt0gqpw + if (req.url.includes("/auth/v1/callback") && req.method === "GET") { + const url = new URL(req.url, "http://localhost"); + const refreshToken = url.searchParams.get("refreshToken"); + if (refreshToken) { + this.refreshToken = refreshToken; + + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Auth server login success, please close the window."); + } else { + res.writeHead(400, { "Content-Type": "text/plain" }); + res.end("No refresh token found"); + Logger.error("No refresh token found", { + component: "auth", + sendTelemetry: false, + }); + } + } + }; + + const host = "localhost"; + const port = 39729; + + this.server = http.createServer(requestListener); + this.server.listen(port, host, () => { + Logger.debug(`Server is running on http://${host}:${port}`, { + component: "auth", + sendTelemetry: false, + }); + }); + + return new Promise((resolve, reject) => { + this.server?.on("listening", () => { + resolve(true); + }); + this.server?.on("error", (error) => { + Logger.error(error, { + component: "auth", + sendTelemetry: true, + }); + reject(error); + }); + }); + } + + /** + * @description + * Stop the auth server. + */ + public stopServer() { + this.server?.close(); + } + + /** + * @description + * Get the URL of the auth server. + * If the auth server is not running, the function will return null. + */ + public getAuthServerUrl() { + if (!this.server) { + Logger.error("Auth server is not running", { + component: "auth", + sendTelemetry: true, + }); + return null; + } + const addressServer = this.server.address(); + if (addressServer && typeof addressServer === "object") { + const { address, port } = addressServer; + + return `http://${address}:${port}/auth/v1/callback`; + } + return null; + } + + /** + * @description + * Get the refresh token from the auth server. + * If the refresh token is not available, the function will wait for the refresh token to be available. + * If the refresh token is not available after the timeout, the function will return null. + */ + public async getRefreshToken(): Promise { + return new Promise((resolve, reject) => { + const interval = setInterval(() => { + if (this.refreshToken) { + clearInterval(interval); + resolve(this.refreshToken); + } + }, 100); + + setTimeout(() => { + clearInterval(interval); + reject(null); + }, this.timeout); + }); + } +} +const authServer = new AuthServer(); +export { authServer }; + +export default AuthServer; diff --git a/src/common/auth/index.ts b/src/common/auth/index.ts new file mode 100644 index 0000000..7111bbc --- /dev/null +++ b/src/common/auth/index.ts @@ -0,0 +1 @@ +export { login } from "./login"; diff --git a/src/common/auth/login.ts b/src/common/auth/login.ts new file mode 100644 index 0000000..44f2c58 --- /dev/null +++ b/src/common/auth/login.ts @@ -0,0 +1,46 @@ +import * as vscode from "vscode"; +import Logger from "../logger"; +import { authServer } from "./authServer"; +import { secretsStorage } from "../utils/secretStore"; +import { getSuppabaseClient } from "./supabaseClient"; + +export const login = async () => { + try { + await authServer.startServer(); + + vscode.env.openExternal( + vscode.Uri.parse("https://dash.firecoder.cc/auth/extensions") + ); + + const refreshToken = await authServer.getRefreshToken(); + + if (!refreshToken) { + return { + error: "No refresh token found", + }; + } + + const supabase = getSuppabaseClient(); + + const { data, error } = await supabase.auth.refreshSession({ + refresh_token: refreshToken, + }); + if (error) { + Logger.error(error, { + component: "auth", + sendTelemetry: true, + }); + + return { + error: error.message, + }; + } + + await secretsStorage.update("token", data.session?.access_token ?? ""); + } catch (error) { + Logger.error(error, { + component: "auth", + sendTelemetry: true, + }); + } +}; diff --git a/src/common/auth/supabaseClient.ts b/src/common/auth/supabaseClient.ts new file mode 100644 index 0000000..285e915 --- /dev/null +++ b/src/common/auth/supabaseClient.ts @@ -0,0 +1,25 @@ +import { createClient, SupabaseClient } from "@supabase/supabase-js"; +import { secretsStorage } from "../utils/secretStore"; + +const supabase_url = "https://zuoncclntljpcqzcbkkp.supabase.co"; +const anon_key = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp1b25jY2xudGxqcGNxemNia2twIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTE2NjQwNzIsImV4cCI6MjAyNzI0MDA3Mn0._F49Jz7WG9zlglHB1gXnwhLcvo2VKg5UQuO2FB1HDUM"; + +let supabaseLocal: null | SupabaseClient = null; + +export const getSuppabaseClient = () => { + if (supabaseLocal) { + return supabaseLocal; + } else { + const supabase = createClient(supabase_url, anon_key, { + auth: { + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: false, + storage: secretsStorage, + }, + }); + supabaseLocal = supabase; + return supabase; + } +}; diff --git a/src/common/chat/cloudChat.ts b/src/common/chat/cloudChat.ts index 7088a9f..ad2c3f9 100644 --- a/src/common/chat/cloudChat.ts +++ b/src/common/chat/cloudChat.ts @@ -7,6 +7,7 @@ import { } from "@langchain/core/messages"; import { HistoryMessage } from "../prompt"; import { configuration } from "../utils/configuration"; +import { getSuppabaseClient } from "../auth/supabaseClient"; type Parameters = { temperature: number; @@ -18,7 +19,13 @@ export const sendChatRequestCloud = async ( history: HistoryMessage[], parameters: Parameters ) => { - const apiKey = configuration.get("cloud.apiToken"); + const supabase = getSuppabaseClient(); + const session = await supabase.auth.getSession(); + const apiKey = session.data?.session?.access_token; + + if (!apiKey) { + throw new Error("No API key found"); + } const model = new ChatOpenAI({ maxRetries: 0, diff --git a/src/common/chat/index.ts b/src/common/chat/index.ts index 120c768..0d3395e 100644 --- a/src/common/chat/index.ts +++ b/src/common/chat/index.ts @@ -66,7 +66,7 @@ export async function* chat( } } - if (configuration.get("cloud.use")) { + if (configuration.get("cloud.use") && configuration.get("cloud.chat.use")) { yield* await sendChatRequestCloud(history, parameters); } else { const prompt = getPromptChat(history); diff --git a/src/common/chat/localChat.ts b/src/common/chat/localChat.ts index 5306633..ead1d02 100644 --- a/src/common/chat/localChat.ts +++ b/src/common/chat/localChat.ts @@ -17,7 +17,7 @@ const defualtParameters = { temperature: 0.7, stop: [], repeat_last_n: 256, - repeat_penalty: 1.18, + repeat_penalty: 1, penalize_nl: false, top_k: 20, top_p: 0.5, diff --git a/src/common/completion/cloudCompletion.ts b/src/common/completion/cloudCompletion.ts index f83741a..6880c81 100644 --- a/src/common/completion/cloudCompletion.ts +++ b/src/common/completion/cloudCompletion.ts @@ -1,6 +1,7 @@ import { OpenAI } from "@langchain/openai"; import { configuration } from "../utils/configuration"; import Logger from "../logger"; +import { getSuppabaseClient } from "../auth/supabaseClient"; type Parameters = { temperature: number; @@ -12,7 +13,13 @@ export const sendCompletionsRequestCloud = async ( prompt: string, parameters: Parameters ) => { - const apiKey = configuration.get("cloud.apiToken"); + const supabase = getSuppabaseClient(); + const session = await supabase.auth.getSession(); + const apiKey = session.data?.session?.access_token; + + if (!apiKey) { + throw new Error("No API key found"); + } const model = new OpenAI({ maxRetries: 0, diff --git a/src/common/completion/index.ts b/src/common/completion/index.ts index b72c5a0..e707ecc 100644 --- a/src/common/completion/index.ts +++ b/src/common/completion/index.ts @@ -73,12 +73,16 @@ export const getInlineCompletionProvider = ( sendTelemetry: true, } ); - if (configuration.get("cloud.use")) { + const cloudUse = configuration.get("cloud.use"); + const cloudUseAutocomplete = configuration.get( + "cloud.autocomplete.use" + ); + if (cloudUse && cloudUseAutocomplete) { const prompt = await getPromptCompletion({ activeDocument: document, additionalDocuments: await getAdditionalDocuments(), position: position, - maxTokenExpect: 3300, + maxTokenExpect: 5300, }); const completion = await sendCompletionsRequestCloud(prompt, { n_predict: 512, diff --git a/src/common/download/index.ts b/src/common/download/index.ts index 86901b9..ca6bf1e 100644 --- a/src/common/download/index.ts +++ b/src/common/download/index.ts @@ -164,20 +164,10 @@ const getModelInfo = async ( checksum: "eb00372705e7d5d30442750e8a7c72919c8e243bee52e1cce97fcfc1008c6143", }, - "chat-small": { - url: "https://huggingface.co/TheBloke/deepseek-coder-1.3b-instruct-GGUF/resolve/main/deepseek-coder-1.3b-instruct.Q8_0.gguf", - checksum: - "36eb025121a50ee6d37fe900659393ff8fb5ea34adc0e3c11fc635e07624dcdb", - }, "chat-medium": { - url: "https://huggingface.co/TheBloke/deepseek-coder-6.7B-instruct-GGUF/resolve/main/deepseek-coder-6.7b-instruct.Q8_0.gguf", - checksum: - "02cd6ce7ccec670cf6d3dd147932f13e584f9e964d5a3297a74b401b658471ae", - }, - "chat-large": { - url: "https://huggingface.co/TheBloke/deepseek-coder-33B-instruct-GGUF/resolve/main/deepseek-coder-33b-instruct.Q8_0.gguf", + url: "https://huggingface.co/lmstudio-community/codegemma-1.1-7b-it-GGUF/resolve/main/codegemma-1.1-7b-it-Q5_K_M.gguf", checksum: - "86529f8eefc87a80bd20d62229ee5acdc32d5773be8575a143bc491924865c21", + "ec11bacb9e0b8c8e0f483f209c487939202b04bbf4f815f0a0945c5b256da895", }, }; diff --git a/src/common/panel/chat.ts b/src/common/panel/chat.ts index 6f6ccf8..e1ff08b 100644 --- a/src/common/panel/chat.ts +++ b/src/common/panel/chat.ts @@ -3,7 +3,11 @@ import * as vscode from "vscode"; import { getUri } from "../utils/getUri"; import { getNonce } from "../utils/getNonce"; import { chat } from "../chat"; -import { ChatMessage } from "../prompt/promptChat"; +import { Chat, ChatMessage } from "../prompt/promptChat"; +import { state } from "../utils/state"; +import { configuration } from "../utils/configuration"; +import { TypeModelsChat, modelsChat, servers } from "../server"; +import { getSuppabaseClient } from "../auth/supabaseClient"; export type MessageType = | { @@ -15,11 +19,49 @@ export type MessageType = | { type: "e2w-response"; id: string; - command: string; done: boolean; data: any; }; +type MessageToExtension = + | { + type: "send-message"; + data: ChatMessage[]; + } + | { + type: "abort-generate"; + id: string; + } + | { + type: "get-settings"; + } + | { + type: "enable-chat"; + } + | { + type: "get-chat"; + chatId: string; + } + | { + type: "save-chat"; + chatId: string; + data: Chat; + } + | { + type: "get-chats"; + } + | { + type: "delete-chat"; + chatId: string; + } + | { + type: "delete-chats"; + }; + +type MessageFromWebview = MessageToExtension & { + id: string; +}; + export class ChatPanel implements vscode.WebviewViewProvider { private disposables: Disposable[] = []; private webview: Webview | undefined; @@ -50,6 +92,7 @@ export class ChatPanel implements vscode.WebviewViewProvider { "css", "main.css", ]); + const scriptUri = getUri(webview, extensionUri, [ "webviews", "build", @@ -95,21 +138,62 @@ export class ChatPanel implements vscode.WebviewViewProvider { private setWebviewMessageListener(webview: Webview) { webview.onDidReceiveMessage( - async (message: any) => { + async (message: MessageFromWebview) => { if (message.type in this.messageCallback) { this.messageCallback[message.type](); return; } + const type = message.type; switch (type) { - case "sendMessage": + case "send-message": await this.handleStartGeneration({ + id: message.id, chatMessage: message.data, - messageId: message.messageId, - messageType: message.type, }); - return; + break; + case "get-chat": + await this.handleGetChat({ + id: message.id, + chatId: message.chatId, + }); + break; + case "save-chat": + await this.handleSaveChat({ + id: message.id, + chatId: message.chatId, + history: message.data, + }); + break; + case "delete-chat": + await this.handleDeleteChat({ + id: message.id, + chatId: message.chatId, + }); + break; + case "delete-chats": + await this.handleDeleteChats({ + id: message.id, + }); + break; + case "get-chats": + await this.handleGetChats({ + id: message.id, + }); + break; + case "get-settings": + await this.handleGetSettings({ + id: message.id, + }); + break; + case "enable-chat": + await this.handleEnableChat({ + id: message.id, + }); + break; + default: + break; } }, undefined, @@ -117,27 +201,17 @@ export class ChatPanel implements vscode.WebviewViewProvider { ); } - private addMessageListener( - commandOrMessageId: string, - callback: (message: any) => void - ) { - this.messageCallback[commandOrMessageId] = callback; - } - private async handleStartGeneration({ - messageId, - messageType, + id, chatMessage, }: { - messageId: string; - messageType: string; + id: string; chatMessage: ChatMessage[]; }) { - const sendResponse = (messageToResponse: any, done: boolean) => { + const sendResponse = (messageToResponse: string, done: boolean) => { this.postMessage({ type: "e2w-response", - id: messageId, - command: messageType, + id: id, data: messageToResponse, done: done, }); @@ -158,8 +232,138 @@ export class ChatPanel implements vscode.WebviewViewProvider { sendResponse("", true); } + private async handleGetSettings({ id }: { id: string }) { + const settigns = await this.getSettings(); + + await this.postMessage({ + type: "e2w-response", + id: id, + data: settigns, + done: true, + }); + } + + private async getSettings() { + const cloudUsing = configuration.get("cloud.use"); + const cloudChatUsing = configuration.get("cloud.chat.use"); + const chatServerIsWorking = Object.keys(modelsChat) + .map( + (chatModel) => servers[chatModel as TypeModelsChat].status === "started" + ) + .some((serverIsWorking) => serverIsWorking); + + const localChatUsing = configuration.get("local.chat.use"); + const supabase = getSuppabaseClient(); + const sesssion = await supabase.auth.getSession(); + const userLoggined = sesssion.data.session ? true : false; + + const chatEnabled = localChatUsing || (cloudUsing && cloudChatUsing); + const chatIsWorking = + (cloudUsing && cloudChatUsing && userLoggined) || + (chatServerIsWorking && localChatUsing); + + return { + chatEnabled: chatEnabled, + chatIsWorking: chatIsWorking, + userLoggined: userLoggined, + }; + } + + private async handleGetChat({ chatId, id }: { chatId: string; id: string }) { + const sendResponse = (messageToResponse: Chat | null, done: boolean) => { + this.postMessage({ + type: "e2w-response", + id: id, + data: messageToResponse, + done: done, + }); + }; + + const history = state.global.get(`chat-${chatId}`); + if (history) { + sendResponse(history, true); + } else { + sendResponse(null, true); + } + } + + private async handleSaveChat({ + chatId, + history, + id, + }: { + chatId: string; + history: Chat; + id: string; + }) { + await state.global.update(`chat-${chatId}`, history); + await this.postMessage({ + type: "e2w-response", + id: id, + data: "", + done: true, + }); + } + + private async handleDeleteChat({ + chatId, + id, + }: { + chatId: string; + id: string; + }) { + await state.global.delete(`chat-${chatId}`); + await this.postMessage({ + type: "e2w-response", + id: id, + data: "", + done: true, + }); + } + + private async handleDeleteChats({ id }: { id: string }) { + await state.global.deleteChats(); + await this.postMessage({ + type: "e2w-response", + id: id, + data: true, + done: true, + }); + } + + private async handleEnableChat({ id }: { id: string }) { + await configuration.set("local.chat.use", true); + await this.postMessage({ + type: "e2w-response", + id: id, + data: true, + done: true, + }); + } + + private async handleGetChats({ id }: { id: string }) { + const chats = state.global.getChats(); + await this.postMessage({ + type: "e2w-response", + id: id, + data: chats, + done: true, + }); + } + + private addMessageListener( + commandOrMessageId: string, + callback: (message: any) => void + ) { + this.messageCallback[commandOrMessageId] = callback; + } + + private async postMessage(message: MessageType) { + await this.webview?.postMessage(message); + } + public async sendMessageToWebview( - command: MessageType["command"], + command: string, data: MessageType["data"] ) { const message: MessageType = { @@ -170,8 +374,4 @@ export class ChatPanel implements vscode.WebviewViewProvider { }; await this.postMessage(message); } - - private async postMessage(message: MessageType) { - await this.webview?.postMessage(message); - } } diff --git a/src/common/prompt/promptChat.ts b/src/common/prompt/promptChat.ts index e3e5452..2d225d9 100644 --- a/src/common/prompt/promptChat.ts +++ b/src/common/prompt/promptChat.ts @@ -1,31 +1,32 @@ export type ChatMessage = { role: string; content: string; + // chatMessageId: string; }; -const promptBaseDefault = `You are an AI programming assistant, utilizing the DeepSeek Coder model, developed by DeepSeek Company, and you only answer questions related to computer science. For politically sensitive questions, security and privacy issues, and other non-computer science questions, you will refuse to answer. -`; +export type Chat = { + messages: ChatMessage[]; + chatId: string; + date: number; + title: string; +}; export const getPromptChat = (chatMessages: ChatMessage[]) => { + const systemPrompt = + chatMessages.find((message) => message.role === "system")?.content || ""; const promptHistory = chatMessages .filter((chatMessage) => chatMessage.role !== "system") - .map((chatMessage) => { + .map((chatMessage, index) => { const partOfPrompt = - chatMessage.role === "user" ? "### Instruction:\n" : "### Response:\n"; - return ( - partOfPrompt + - chatMessage.content + - "\n" + - (chatMessage.role === "ai" ? "<|EOT|>" : "") - ); + index === 0 && chatMessage.role === "user" + ? "user\n" + systemPrompt + "\n" + : chatMessage.role === "user" + ? "user\n" + : "model\n"; + return partOfPrompt + chatMessage.content + "\n"; }) .join(""); - const promptBase = - chatMessages.find((message) => message.role === "system")?.content ?? - promptBaseDefault; - - const prompt = promptBase + promptHistory + "### Response:\n"; - + const prompt = "" + promptHistory + "model\n"; return prompt; }; diff --git a/src/common/server/index.ts b/src/common/server/index.ts index 6ef2fd3..ea76d0d 100644 --- a/src/common/server/index.ts +++ b/src/common/server/index.ts @@ -17,16 +17,10 @@ const modelsBase = { }; export type TypeModelsBase = keyof typeof modelsBase; -const modelsChat = { - "chat-small": { - port: 39725, - }, +export const modelsChat = { "chat-medium": { port: 39726, }, - "chat-large": { - port: 39727, - }, }; export type TypeModelsChat = keyof typeof modelsChat; @@ -126,12 +120,12 @@ class Server { ...(isMacArm64 ? ["--nobrowser"] : []), ...(useGPU ? ["--n-gpu-layers", "100"] : []), "--cont-batching", - "--embedding", "--slots-endpoint-disable", "--log-disable", ], { detached: false, + shell: true, } ); @@ -191,11 +185,12 @@ class Server { this.serverProcess.on("close", (code) => { Logger.trace(`child process exited with code ${code}`, { component: "llama", + sendTelemetry: true, }); }); const isServerStarted = await this.checkServerStatusIntervalWithTimeout( - 10000 + 20000 ); if (!isServerStarted) { @@ -215,7 +210,7 @@ class Server { return true; } - public async stopServer() { + public stopServer() { if (this.serverProcess) { const result = this.serverProcess.kill(9); if (result === false) { @@ -233,19 +228,16 @@ class Server { const osplatform = os.platform(); const osmachine = os.machine(); const isMacArm64 = osplatform === "darwin" && osmachine === "arm64"; - const res = await fetch( - `${this.serverUrl}/${isMacArm64 ? "model.json" : "health"}`, - { - method: "GET", - } - ); + const res = await fetch(`${this.serverUrl}/health`, { + method: "GET", + }); if (res.ok) { if (isMacArm64) { this.status = "started"; return true; } const resJson = (await res.json()) as { status: string }; - if (resJson.status === "ok") { + if (resJson.status === "ok" || resJson.status === "no slot available") { this.status = "started"; return true; } @@ -312,7 +304,5 @@ class Server { export const servers = { "base-small": new Server("base-small"), "base-medium": new Server("base-medium"), - "chat-small": new Server("chat-small"), "chat-medium": new Server("chat-medium"), - "chat-large": new Server("chat-large"), }; diff --git a/src/common/telemetry/index.ts b/src/common/telemetry/index.ts index df8d3d9..e029c8a 100644 --- a/src/common/telemetry/index.ts +++ b/src/common/telemetry/index.ts @@ -39,6 +39,7 @@ class FirecoderTelemetrySender implements vscode.TelemetrySender { transports: [ new FetchTransport({ url: "https://faro-collector-prod-eu-west-0.grafana.net/collect/33a834c252bb6b780b5d242def445bbd", + bufferSize: 100, }), ], sessionTracking: { diff --git a/src/common/utils/configuration.ts b/src/common/utils/configuration.ts index 0bce5f9..d85d1aa 100644 --- a/src/common/utils/configuration.ts +++ b/src/common/utils/configuration.ts @@ -11,7 +11,7 @@ const ConfigurationProperties = { "experimental.useGpu.osx.metal": { default: false, }, - "experimental.chat": { + "local.chat.use": { default: false, }, "experimental.useopentabs": { @@ -29,15 +29,18 @@ const ConfigurationProperties = { "cloud.endpoint": { default: "https://llm-api.firecoder.cc/v1", }, - "cloud.apiToken": { - default: "", - }, "cloud.use": { default: false, }, "server.usePreRelease": { default: false, }, + "cloud.autocomplete.use": { + default: false, + }, + "cloud.chat.use": { + default: false, + }, } as const; interface ConfigurationPropertiesType @@ -51,7 +54,7 @@ interface ConfigurationPropertiesType "experimental.useGpu.osx.metal": { possibleValues: boolean; }; - "experimental.chat": { + "local.chat.use": { possibleValues: boolean; }; "experimental.useopentabs": { @@ -66,35 +69,40 @@ interface ConfigurationPropertiesType homedir: { possibleValues: string; }; - "cloud.endpoint": { - possibleValues: string; + "server.usePreRelease": { + possibleValues: boolean; }; - "cloud.apiToken": { + "cloud.endpoint": { possibleValues: string; }; "cloud.use": { possibleValues: boolean; }; - "server.usePreRelease": { + + "cloud.autocomplete.use": { + possibleValues: boolean; + }; + "cloud.chat.use": { possibleValues: boolean; }; } class Configuration { - private configuration: vscode.WorkspaceConfiguration; - constructor() { - this.configuration = vscode.workspace.getConfiguration("firecoder"); - } - public get( property: T ): ConfigurationPropertiesType[T]["possibleValues"] { - this.configuration = vscode.workspace.getConfiguration("firecoder"); + const configuration = vscode.workspace.getConfiguration("firecoder"); + const value = configuration.get(property) as any; + + return value ?? ConfigurationProperties[property]["default"]; + } - return ( - this.configuration.get(property) ?? - ConfigurationProperties[property]["default"] - ); + public async set( + property: T, + value: ConfigurationPropertiesType[T]["possibleValues"] + ) { + const configuration = vscode.workspace.getConfiguration("firecoder"); + await configuration.update(property, value, true); } } diff --git a/src/common/utils/secretStore.ts b/src/common/utils/secretStore.ts new file mode 100644 index 0000000..6c408c2 --- /dev/null +++ b/src/common/utils/secretStore.ts @@ -0,0 +1,61 @@ +import * as vscode from "vscode"; + +const SecretStorageValues = { + token: { + default: "", + }, + refreshToken: { + default: "", + }, +}; + +interface SecretStorageValuesType + extends Record { + token: { + possibleValues: string; + }; + refreshToken: { + possibleValues: string; + }; +} +class SecretsStorage { + secretStorage?: vscode.SecretStorage; + constructor() {} + + public init(secretStorage: vscode.SecretStorage) { + this.secretStorage = secretStorage; + } + + public async get( + key: T + ): Promise { + const value = await this.secretStorage?.get(key); + if (value) { + return value; + } else { + return SecretStorageValues[key]["default"]; + } + } + + public async update( + key: T, + value: SecretStorageValuesType[T]["possibleValues"] + ) { + await this.secretStorage?.store(key, value); + } + + public async getItem(key: string) { + const value = await this.secretStorage?.get(key); + return value ?? null; + } + + public async removeItem(key: string) { + await this.secretStorage?.delete(key); + } + + public async setItem(key: string, value: string) { + await this.secretStorage?.store(key, value); + } +} + +export const secretsStorage = new SecretsStorage(); diff --git a/src/common/utils/state.ts b/src/common/utils/state.ts index c327a4f..5d7cc14 100644 --- a/src/common/utils/state.ts +++ b/src/common/utils/state.ts @@ -1,23 +1,17 @@ import * as vscode from "vscode"; import type { Spec } from "../download"; +import { Chat } from "../prompt/promptChat"; const StateValues = { - inlineSuggestModeAuto: { - default: true, - }, - serverSpec: { - default: null, - }, + inlineSuggestModeAuto: true, + serverSpec: null, +}; +type StateValuesType = { + inlineSuggestModeAuto: boolean; + serverSpec: Spec | null; + [key: `chat-${string}`]: Chat | undefined; }; -interface StateValuesType extends Record { - inlineSuggestModeAuto: { - possibleValues: boolean; - }; - serverSpec: { - possibleValues: Spec | null; - }; -} class State { state?: vscode.Memento; constructor() {} @@ -26,18 +20,44 @@ class State { this.state = state; } - public get( + public get( key: T - ): StateValuesType[T]["possibleValues"] { - return this.state?.get(key) ?? StateValues[key]["default"]; + ): StateValuesType[T] { + // @ts-ignore + return this.state?.get(key) ?? StateValues[key]; } public async update( key: T, - value: StateValuesType[T]["possibleValues"] + value: StateValuesType[T] ) { await this.state?.update(key, value); } + + public getChats(): Chat[] { + const allKeys = (this.state?.keys() || + []) as unknown as (keyof StateValuesType)[]; + + return allKeys + .filter((key) => key.startsWith("chat-")) + .map((key) => { + return this.get(key as `chat-${string}`) as Chat; + }); + } + public async delete(key: T) { + await this.state?.update(key, undefined); + } + + public async deleteChats() { + const allKeys = (this.state?.keys() || + []) as unknown as (keyof StateValuesType)[]; + + await Promise.all( + allKeys + .filter((key) => key.startsWith("chat-")) + .map((key) => this.delete(key as `chat-${string}`)) + ); + } } export const state = { diff --git a/src/extension.ts b/src/extension.ts index b132532..a64fabf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,6 +8,9 @@ import { configuration } from "./common/utils/configuration"; import { state } from "./common/utils/state"; import { ChatPanel } from "./common/panel/chat"; import { tokenizer } from "./common/prompt/tokenizer"; +import { login } from "./common/auth"; +import { secretsStorage } from "./common/utils/secretStore"; +import { getSuppabaseClient } from "./common/auth/supabaseClient"; export async function activate(context: vscode.ExtensionContext) { FirecoderTelemetrySenderInstance.init(context); @@ -16,6 +19,8 @@ export async function activate(context: vscode.ExtensionContext) { state.global.init(context.globalState); state.workspace.init(context.workspaceState); + secretsStorage.init(context.secrets); + Logger.info("FireCoder is starting.", { component: "main", sendTelemetry: true, @@ -29,7 +34,7 @@ export async function activate(context: vscode.ExtensionContext) { ); context.subscriptions.push( vscode.commands.registerCommand("firecoder.startNewChat", async () => { - await provider.sendMessageToWebview("startNewChat", {}); + await provider.sendMessageToWebview("start-new-chat", {}); }) ); @@ -52,6 +57,30 @@ export async function activate(context: vscode.ExtensionContext) { ) ); + if (configuration.get("cloud.use")) { + const supabase = getSuppabaseClient(); + + const data = await supabase.auth.getUser(); + if (data.error) { + await login(); + return; + } + } + + vscode.workspace.onDidChangeConfiguration(async (event) => { + if (event.affectsConfiguration("firecoder.cloud.use")) { + const cloudUse = configuration.get("cloud.use"); + const supabase = getSuppabaseClient(); + if (cloudUse === true) { + const data = await supabase.auth.getUser(); + if (data.error) { + await login(); + return; + } + } + } + }); + context.subscriptions.push( vscode.commands.registerCommand("firecoder.inlineSuggest", async () => { await vscode.commands.executeCommand( @@ -60,82 +89,67 @@ export async function activate(context: vscode.ExtensionContext) { }) ); - vscode.workspace.onDidChangeConfiguration(async (event) => { - if ( - event.affectsConfiguration("firecoder.cloud.use") || - event.affectsConfiguration("firecoder.experimental.chat") || - event.affectsConfiguration("firecoder.completion.manuallyMode") || - event.affectsConfiguration("firecoder.completion.autoMode") || - event.affectsConfiguration( - "firecoder.experimental.useGpu.linux.nvidia" - ) || - event.affectsConfiguration("firecoder.experimental.useGpu.osx.metal") || - event.affectsConfiguration( - "firecoder.experimental.useGpu.windows.nvidia" - ) || - event.affectsConfiguration("firecoder.server.usePreRelease") - ) { - Object.values(servers).forEach((server) => server.stopServer()); - - const completionServers = configuration.get("cloud.use") - ? [] - : new Set([ - configuration.get("completion.autoMode"), - configuration.get("completion.manuallyMode"), - ]); - const serversToStart = [ - ...completionServers, - ...(configuration.get("experimental.chat") && - !configuration.get("cloud.use") - ? ["chat-medium" as const] - : []), - ]; - await Promise.all( - serversToStart.map((serverType) => servers[serverType].startServer()) - ); + const startChat = async () => { + if (configuration.get("cloud.use") && configuration.get("cloud.chat.use")) { + Logger.info("Use cloud for chat.", { + component: "main", + sendTelemetry: true, + }); + } else if (configuration.get("local.chat.use")) { + Logger.info("Use local for chat.", { + component: "main", + sendTelemetry: true, + }); + try { + await servers["chat-medium"].startServer(); + } catch (error) { + vscode.window.showErrorMessage((error as Error).message); + Logger.error(error, { + component: "server", + sendTelemetry: true, + }); + } + Logger.info("Chat is ready to start.", { + component: "main", + sendTelemetry: true, + }); + } else { + Logger.info("Chat is not enable", { + component: "main", + sendTelemetry: true, + }); } - }); + }; - (async () => { - if (configuration.get("cloud.use")) { - Logger.info("Use cloud for chat and completions", { + const startCompletion = async (registerCompletionProvider: boolean) => { + if ( + configuration.get("cloud.use") && + configuration.get("cloud.autocomplete.use") + ) { + Logger.info("Use cloud for auto completions.", { component: "main", sendTelemetry: true, }); - - const InlineCompletionProvider = getInlineCompletionProvider(context); - vscode.languages.registerInlineCompletionItemProvider( - { pattern: "**" }, - InlineCompletionProvider - ); } else { + Logger.info("Use local for auto completions.", { + component: "main", + sendTelemetry: true, + }); try { - const completionServers = configuration.get("cloud.use") - ? [] - : new Set([ - configuration.get("completion.autoMode"), - configuration.get("completion.manuallyMode"), - ]); const serversStarted = await Promise.all( [ - ...completionServers, - ...(configuration.get("experimental.chat") && - !configuration.get("cloud.use") - ? ["chat-medium" as const] - : []), + ...new Set([ + configuration.get("completion.autoMode"), + configuration.get("completion.manuallyMode"), + ]), ].map((serverType) => servers[serverType].startServer()) ); if (serversStarted.some((serverStarted) => serverStarted)) { - Logger.info("Server inited", { + Logger.info("Servers inited", { component: "main", sendTelemetry: true, }); - const InlineCompletionProvider = getInlineCompletionProvider(context); - vscode.languages.registerInlineCompletionItemProvider( - { pattern: "**" }, - InlineCompletionProvider - ); } } catch (error) { vscode.window.showErrorMessage((error as Error).message); @@ -145,12 +159,45 @@ export async function activate(context: vscode.ExtensionContext) { }); } } + if (registerCompletionProvider) { + const InlineCompletionProvider = getInlineCompletionProvider(context); + vscode.languages.registerInlineCompletionItemProvider( + { pattern: "**" }, + InlineCompletionProvider + ); + } + }; + + (async () => { + await Promise.all([startChat(), startCompletion(true)]); Logger.info("FireCoder is ready.", { component: "main", sendTelemetry: true, }); })(); + + vscode.workspace.onDidChangeConfiguration(async (event) => { + if ( + event.affectsConfiguration("firecoder.cloud.use") || + event.affectsConfiguration("firecoder.cloud.chat.use") || + event.affectsConfiguration("firecoder.cloud.autocomplete.use") || + event.affectsConfiguration("firecoder.local.chat.use") || + event.affectsConfiguration("firecoder.completion.manuallyMode") || + event.affectsConfiguration("firecoder.completion.autoMode") || + event.affectsConfiguration( + "firecoder.experimental.useGpu.linux.nvidia" + ) || + event.affectsConfiguration("firecoder.experimental.useGpu.osx.metal") || + event.affectsConfiguration( + "firecoder.experimental.useGpu.windows.nvidia" + ) || + event.affectsConfiguration("firecoder.server.usePreRelease") + ) { + Object.values(servers).forEach((server) => server.stopServer()); + await Promise.all([startChat(), startCompletion(false)]); + } + }); } export function deactivate() { diff --git a/webviews/package-lock.json b/webviews/package-lock.json index 2189ebd..a134925 100644 --- a/webviews/package-lock.json +++ b/webviews/package-lock.json @@ -10,10 +10,14 @@ "dependencies": { "@vscode/webview-ui-toolkit": "^1.2.2", "classnames": "^2.5.1", + "localforage": "^1.10.0", + "match-sorter": "^6.3.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^9.0.1", - "react-syntax-highlighter": "^15.5.0" + "react-router-dom": "^6.23.0", + "react-syntax-highlighter": "^15.5.0", + "sort-by": "^1.2.0" }, "devDependencies": { "@types/node": "^12.20.37", @@ -3619,6 +3623,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.0.tgz", + "integrity": "sha512-Quz1KOffeEf/zwkCBM3kBtH4ZoZ+pT3xIXBG4PPW/XFtDP7EGhtTiC2+gpL9GnR7+Qdet5Oa6cYSvwKYg6kN9Q==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -10151,6 +10163,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -13395,6 +13412,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -13433,6 +13458,14 @@ "node": ">=8.9.0" } }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dependencies": { + "lie": "3.1.1" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -13577,6 +13610,15 @@ "tmpl": "1.0.5" } }, + "node_modules/match-sorter": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", + "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", @@ -14589,6 +14631,14 @@ "node": ">= 0.4" } }, + "node_modules/object-path": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.6.0.tgz", + "integrity": "sha512-fxrwsCFi3/p+LeLOAwo/wyRMODZxdGBtUlWRzsEpsUVrisZbEfZ21arxLGfaWfcnqb8oHPNihIb4XPE8CQPN5A==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/object.assign": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", @@ -16955,6 +17005,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.0.tgz", + "integrity": "sha512-wPMZ8S2TuPadH0sF5irFGjkNLIcRvOSaEe7v+JER8508dyJumm6XZB1u5kztlX0RVq6AzRVndzqcUh6sFIauzA==", + "dependencies": { + "@remix-run/router": "1.16.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.0.tgz", + "integrity": "sha512-Q9YaSYvubwgbal2c9DJKfx6hTNoBp3iJDsl+Duva/DwxoJH+OTXkxGpql4iUK2sla/8z4RpjAm6EWx1qUDuopQ==", + "dependencies": { + "@remix-run/router": "1.16.0", + "react-router": "6.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -17359,6 +17439,11 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -18526,6 +18611,14 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sort-by": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sort-by/-/sort-by-1.2.0.tgz", + "integrity": "sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==", + "dependencies": { + "object-path": "0.6.0" + } + }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", diff --git a/webviews/package.json b/webviews/package.json index dc9e674..e4d90e1 100644 --- a/webviews/package.json +++ b/webviews/package.json @@ -11,10 +11,14 @@ "dependencies": { "@vscode/webview-ui-toolkit": "^1.2.2", "classnames": "^2.5.1", + "localforage": "^1.10.0", + "match-sorter": "^6.3.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^9.0.1", - "react-syntax-highlighter": "^15.5.0" + "react-router-dom": "^6.23.0", + "react-syntax-highlighter": "^15.5.0", + "sort-by": "^1.2.0" }, "devDependencies": { "@types/node": "^12.20.37", diff --git a/webviews/src/App.tsx b/webviews/src/App.tsx deleted file mode 100644 index d2e9d0e..0000000 --- a/webviews/src/App.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; -import "./App.css"; -import { ChatMessage } from "./components/ChatMessage"; -import { ChatHelloMessage } from "./components/ChatHelloMessage"; -import { AutoScrollDown } from "./components/AutoScrollDown"; -import { useMessageListener } from "./hooks/messageListener"; -import TextArea from "./components/TextArea"; -import { useChat } from "./hooks/useChat"; - -export const App = () => { - const { - handleSubmit, - isLoading, - chatMessages, - input, - setInput, - startNewChat, - stop, - } = useChat(); - - useMessageListener("startNewChat", () => { - startNewChat(); - }); - - return ( - <> -
-
- - {chatMessages.map((message) => ( - - ))} - -
-
-
-
-
- -
-
- - ); -}; diff --git a/webviews/src/components/AutoScrollDown/index.tsx b/webviews/src/components/auto-scroll-down/index.tsx similarity index 100% rename from webviews/src/components/AutoScrollDown/index.tsx rename to webviews/src/components/auto-scroll-down/index.tsx diff --git a/webviews/src/components/ChatHelloMessage/index.tsx b/webviews/src/components/chat-hello-message/index.tsx similarity index 85% rename from webviews/src/components/ChatHelloMessage/index.tsx rename to webviews/src/components/chat-hello-message/index.tsx index 641aa0d..009e4f2 100644 --- a/webviews/src/components/ChatHelloMessage/index.tsx +++ b/webviews/src/components/chat-hello-message/index.tsx @@ -1,4 +1,4 @@ -import { ChatMessage } from "../ChatMessage"; +import { ChatMessage } from "../chat-message"; const ChatHelloMessageContent = ` Hello! I'm FireCoder, your friendly AI assistant.\n diff --git a/webviews/src/components/ChatMessage/index.tsx b/webviews/src/components/chat-message/index.tsx similarity index 82% rename from webviews/src/components/ChatMessage/index.tsx rename to webviews/src/components/chat-message/index.tsx index df9b4d9..0635721 100644 --- a/webviews/src/components/ChatMessage/index.tsx +++ b/webviews/src/components/chat-message/index.tsx @@ -10,7 +10,19 @@ export const ChatMessage = memo((props: { role: string; content: string }) => { return ( <>
-

{title}

+
+

{title}

+ + navigator.clipboard.writeText( + String(props.content).replace(/\n$/, "") + ) + } + > + + +
void ) => { useEffect(() => { - vscode.addMessageListener(command, callback); - }); + const removeCallback = vscode.addMessageListener(command, callback); + return () => { + removeCallback(); + }; + }, [command]); }; diff --git a/webviews/src/hooks/useChat.ts b/webviews/src/hooks/useChat.ts index d5a3f95..41ca5aa 100644 --- a/webviews/src/hooks/useChat.ts +++ b/webviews/src/hooks/useChat.ts @@ -1,6 +1,7 @@ -import { useCallback, useRef, useState } from "react"; -import { randomMessageId } from "../utilities/messageId"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { randomId } from "../utilities/messageId"; import { vscode } from "../utilities/vscode"; +import { useChatMessages } from "./useChatMessages"; export type ChatMessage = { role: string; @@ -8,16 +9,38 @@ export type ChatMessage = { chatMessageId: string; }; -export const useChat = () => { - const [chatMessages, setChatMessages] = useState([]); +export type Chat = { + messages: ChatMessage[]; + chatId: string; + date: number; + title: string; +}; + +export const useChat = (chatId?: string) => { + const { chatMessages, setChatMessages } = useChatMessages(); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); + const [chatIdLocal, setChatIdLocal] = useState(chatId || randomId()); const abortController = useRef(new AbortController()); + useEffect(() => { + const getChatHistory = async () => { + if (chatId) { + const chat = await vscode.getChat(chatId); + if (chat) { + setChatMessages(chat.messages); + } + } + }; + if (chatId) { + getChatHistory(); + } + }, [chatId, setChatMessages]); + const sendMessage = async (chatHistoryLocal: ChatMessage[]) => { - const messageId = randomMessageId(); + const messageId = randomId(); for await (const newMessage of vscode.startGeneration(chatHistoryLocal, { signal: abortController.current.signal, })) { @@ -40,6 +63,18 @@ export const useChat = () => { ]; }); } + setChatMessages((chatHistoryLocal) => { + (async () => { + const chat = { + chatId: chatIdLocal, + date: Date.now(), + messages: chatHistoryLocal, + title: "Chat with AI", + } satisfies Chat; + await vscode.saveChatHistory(chatIdLocal, chat); + })(); + return chatHistoryLocal; + }); setIsLoading(false); }; @@ -55,7 +90,7 @@ export const useChat = () => { } setChatMessages((value) => { - const messageId = randomMessageId(); + const messageId = randomId(); const newChatMessage = [ ...value, @@ -80,7 +115,7 @@ export const useChat = () => { const startNewChat = useCallback(() => { setChatMessages([]); - }, []); + }, [setChatMessages]); return { chatMessages, diff --git a/webviews/src/hooks/useChatMessages.ts b/webviews/src/hooks/useChatMessages.ts new file mode 100644 index 0000000..e672f43 --- /dev/null +++ b/webviews/src/hooks/useChatMessages.ts @@ -0,0 +1,11 @@ +import { useState } from "react"; +import { ChatMessage } from "./useChat"; + +export const useChatMessages = () => { + const [chatMessages, setChatMessages] = useState([]); + + return { + chatMessages, + setChatMessages, + }; +}; diff --git a/webviews/src/hooks/useSettings.tsx b/webviews/src/hooks/useSettings.tsx new file mode 100644 index 0000000..fb59e27 --- /dev/null +++ b/webviews/src/hooks/useSettings.tsx @@ -0,0 +1,65 @@ +import { createContext, useContext, useEffect, useMemo, useState } from "react"; +import { vscode } from "../utilities/vscode"; + +type ConfigurationType = { + chatEnabled: boolean; + userLoggined: boolean; + chatIsWorking: boolean; +}; + +interface SettingsContextType { + configuration: ConfigurationType; +} + +const SettingsContext = createContext(null!); + +export const SettingsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [configuration, setConfiguration] = useState(null!); + + useEffect(() => { + let lastSettings: any = null; + + const getSettings = async () => { + const settings = await vscode.getSettings(); + if ( + settings.chatEnabled !== lastSettings?.chatEnabled || + settings.chatIsWorking !== lastSettings?.chatIsWorking || + settings.userLoggined !== lastSettings?.userLoggined + ) { + lastSettings = settings; + + setConfiguration({ + chatEnabled: settings.chatEnabled, + chatIsWorking: settings.chatIsWorking, + userLoggined: settings.userLoggined, + }); + } + }; + getSettings(); + + const interval = setInterval(getSettings, 1000); + return () => { + clearInterval(interval); + }; + }, []); + + const value = useMemo(() => ({ configuration }), [configuration]); + + if (configuration === null) { + return null; + } + + return ( + + {children} + + ); +}; + +export const useSettings = () => { + return useContext(SettingsContext); +}; diff --git a/webviews/src/index.css b/webviews/src/index.css new file mode 100644 index 0000000..9381f1a --- /dev/null +++ b/webviews/src/index.css @@ -0,0 +1,12 @@ +html { + height: 100%; +} + +body { + height: 100%; + padding: 0px; +} + +#root { + height: 100%; +} diff --git a/webviews/src/index.tsx b/webviews/src/index.tsx index 16e0258..b8f3664 100644 --- a/webviews/src/index.tsx +++ b/webviews/src/index.tsx @@ -1,10 +1,57 @@ import React from "react"; import ReactDOM from "react-dom"; -import { App } from "./App"; +import { createMemoryRouter, RouterProvider } from "react-router-dom"; +import "./index.css"; +import Root from "./routes/root"; +import { ChatInstance } from "./routes/chat"; +import ChatsHistory, { + loader as ChatsHistoryLoader, +} from "./routes/chatsHistory"; +import Init from "./routes/init"; +import { SettingsProvider } from "./hooks/useSettings"; +import { RequireInit } from "./routes/requireInit"; + +const router = createMemoryRouter( + [ + { + element: , + children: [ + { + path: "/init", + element: , + }, + { + path: "/chats", + element: , + children: [ + { + path: "/chats/history", + element: , + loader: ChatsHistoryLoader, + }, + { + path: "/chats/new-chat", + element: , + }, + { + path: "/chats/:chatId", + element: , + }, + ], + }, + ], + }, + ], + { + initialEntries: ["/chats/new-chat"], + } +); ReactDOM.render( - + + + , document.getElementById("root") ); diff --git a/webviews/src/routes/chat/index.tsx b/webviews/src/routes/chat/index.tsx new file mode 100644 index 0000000..7f764ec --- /dev/null +++ b/webviews/src/routes/chat/index.tsx @@ -0,0 +1,89 @@ +import { useNavigate, useParams } from "react-router-dom"; +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import { ChatMessage } from "../../components/chat-message"; +import { ChatHelloMessage } from "../../components/chat-hello-message"; +import { AutoScrollDown } from "../../components/auto-scroll-down"; +import { useMessageListener } from "../../hooks/messageListener"; +import TextArea from "../../components/text-area"; +import { useChat } from "../../hooks/useChat"; +import styles from "./style.module.css"; +import { useSettings } from "../../hooks/useSettings"; + +export const ChatInstance = () => { + const { chatId } = useParams() as { chatId?: string }; + + const { + handleSubmit, + isLoading, + chatMessages, + input, + setInput, + startNewChat, + stop, + } = useChat(chatId === "new-chat" ? undefined : chatId); + + const settings = useSettings(); + + useMessageListener("start-new-chat", () => { + startNewChat(); + }); + + const navigate = useNavigate(); + + return ( +
+
+ navigate("/chats/history")} + > + + +
+
+ + {chatMessages.map((message) => ( + + ))} + +
+
+
+
+
+ +
+
+ ); +}; diff --git a/webviews/src/App.css b/webviews/src/routes/chat/style.module.css similarity index 71% rename from webviews/src/App.css rename to webviews/src/routes/chat/style.module.css index 31d420f..ff031a6 100644 --- a/webviews/src/App.css +++ b/webviews/src/routes/chat/style.module.css @@ -1,23 +1,26 @@ -main { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: flex-start; - height: 100vh; +.chatRoot { + display: grid; + grid-template-rows: min-content 1fr min-content; + height: 100%; + grid-template-columns: 100%; } -body { - padding: 0px; +.chatBlockNavigation { + display: flex; + justify-content: flex-start; + align-items: center; + padding-left: 16px; + padding-top: 6px; } -.chat-history { +.chatHistory { width: 100%; overflow: auto; display: flex; flex-direction: column; } -.chat-input-block { +.chatInputBlock { display: flex; flex-direction: column; width: calc(100% - 40px); @@ -26,17 +29,17 @@ body { padding: 20px 20px 20px 20px; } -.chat-input { +.chatInput { flex-grow: 2; padding-right: 20px; } -.progress-container { +.progressContainer { height: 1px; width: calc(100% - 20px); } -.progress-container .progress-bit { +.progressBit { height: 1px; top: 1px; position: relative; diff --git a/webviews/src/routes/chatsHistory/index.tsx b/webviews/src/routes/chatsHistory/index.tsx new file mode 100644 index 0000000..ec51539 --- /dev/null +++ b/webviews/src/routes/chatsHistory/index.tsx @@ -0,0 +1,59 @@ +import { useLoaderData, useNavigate } from "react-router-dom"; +import { vscode } from "../../utilities/vscode"; +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import styles from "./style.module.css"; + +export async function loader() { + const chats = await vscode.getChats(); + return chats; +} + +type LoaderReturn = Awaited>; + +const ChatsHistory = () => { + const chats = useLoaderData() as LoaderReturn; + + const navigate = useNavigate(); + + return ( +
+
+ navigate("/chats/new-chat")}> + Open New Chat + + vscode.deleteChats()} + > + Remove All Chats + +
+
+ {chats.reverse().map((chat) => ( +
+

{chat.title}

+
+ navigate(`/chats/${chat.chatId}`)} + > + Open Chat + +

+ {new Date(chat.date).toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + })} +

+
+
+ ))} +
+
+ ); +}; + +export default ChatsHistory; diff --git a/webviews/src/routes/chatsHistory/style.module.css b/webviews/src/routes/chatsHistory/style.module.css new file mode 100644 index 0000000..8913312 --- /dev/null +++ b/webviews/src/routes/chatsHistory/style.module.css @@ -0,0 +1,24 @@ +.chatHistoryBlockNavigation { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; +} + +.chatHistoryBlock { + display: flex; + flex-direction: column; +} + +.chatHistoryChat { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 16px; +} + +.chatHistoryChatButtons { + display: flex; + flex-direction: column; +} diff --git a/webviews/src/routes/init/index.tsx b/webviews/src/routes/init/index.tsx new file mode 100644 index 0000000..fd812a0 --- /dev/null +++ b/webviews/src/routes/init/index.tsx @@ -0,0 +1,51 @@ +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"; +import styles from "./style.module.css"; +import { vscode } from "../../utilities/vscode"; +import { useState } from "react"; +import { useSettings } from "../../hooks/useSettings"; +import { Navigate } from "react-router-dom"; + +export default function Init() { + const [isChatEnabled, setIsChatEnabled] = useState(false); + const settings = useSettings(); + + if (settings.configuration.chatEnabled) { + return ; + } + + return ( +
+

+ FireCoder Chat is currently disabled. Please enable it to start + chatting. +

+

+ FireCoder needs to download the chat model and save it to your device's + local storage. +
+ This model is quite large, around 6GB, so the download may take a few + minutes. +

+ { + setIsChatEnabled(true); + vscode.enableChat(); + }} + > + Enable + {isChatEnabled ? ( + + ) : ( + + )} + +
+ ); +} diff --git a/webviews/src/routes/init/style.module.css b/webviews/src/routes/init/style.module.css new file mode 100644 index 0000000..d165127 --- /dev/null +++ b/webviews/src/routes/init/style.module.css @@ -0,0 +1,7 @@ +.init { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 16px; +} diff --git a/webviews/src/routes/requireInit/index.tsx b/webviews/src/routes/requireInit/index.tsx new file mode 100644 index 0000000..4f8c8c0 --- /dev/null +++ b/webviews/src/routes/requireInit/index.tsx @@ -0,0 +1,12 @@ +import { Navigate, Outlet } from "react-router-dom"; +import { useSettings } from "../../hooks/useSettings"; + +export function RequireInit() { + const settings = useSettings(); + + if (!settings.configuration.chatEnabled) { + return ; + } + + return ; +} diff --git a/webviews/src/routes/root/index.tsx b/webviews/src/routes/root/index.tsx new file mode 100644 index 0000000..2fe169c --- /dev/null +++ b/webviews/src/routes/root/index.tsx @@ -0,0 +1,24 @@ +import { useEffect } from "react"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import { useMessageListener } from "../../hooks/messageListener"; + +export default function Root() { + let location = useLocation(); + + useEffect(() => { + console.log(location); + }, [location]); + + const navigate = useNavigate(); + + useMessageListener("start-new-chat", () => { + console.log("callback start-new-chat"); + navigate("/chats/new-chat"); + }); + + return ( + <> + + + ); +} diff --git a/webviews/src/utilities/messageId.ts b/webviews/src/utilities/messageId.ts index ba87346..a56bf6c 100644 --- a/webviews/src/utilities/messageId.ts +++ b/webviews/src/utilities/messageId.ts @@ -1 +1 @@ -export const randomMessageId: () => string = () => global.crypto.randomUUID(); +export const randomId: () => string = () => global.crypto.randomUUID(); diff --git a/webviews/src/utilities/vscode.ts b/webviews/src/utilities/vscode.ts index 83d35e5..c4648cb 100644 --- a/webviews/src/utilities/vscode.ts +++ b/webviews/src/utilities/vscode.ts @@ -1,6 +1,6 @@ import type { WebviewApi } from "vscode-webview"; -import { randomMessageId } from "./messageId"; -import { ChatMessage } from "../hooks/useChat"; +import { randomId } from "./messageId"; +import { Chat, ChatMessage } from "../hooks/useChat"; import { Transform } from "./transformCallback2AsyncGenerator"; export type MessageType = @@ -12,15 +12,52 @@ export type MessageType = } | { type: "e2w-response"; - command: string; id: string; done: boolean; data: any; }; +type MessageToExtension = + | { + type: "send-message"; + data: ChatMessage[]; + } + | { + type: "abort-generate"; + id: string; + } + | { + type: "get-settings"; + } + | { + type: "enable-chat"; + } + | { + type: "get-chat"; + chatId: string; + } + | { + type: "save-chat"; + chatId: string; + data: Chat; + } + | { + type: "get-chats"; + } + | { + type: "delete-chat"; + chatId: string; + } + | { + type: "delete-chats"; + }; + class VSCodeAPIWrapper { private readonly vsCodeApi: WebviewApi | undefined; - private messageCallback: Record = {}; + private messageCallback: Record< + string, + Record any> + > = {}; constructor() { if (typeof acquireVsCodeApi === "function") { @@ -29,11 +66,22 @@ class VSCodeAPIWrapper { window.addEventListener("message", (message) => { const newMessage = (message as MessageEvent).data; + const callCallbacks = (commandOrMessageId: string, message: any) => { + if (commandOrMessageId in this.messageCallback) { + const callbacks = Object.values( + this.messageCallback[commandOrMessageId] + ); + callbacks.forEach((callback) => { + callback(newMessage); + }); + } + }; + if ( newMessage.type === "e2w-response" && newMessage.id in this.messageCallback ) { - this.messageCallback[newMessage.id](newMessage); + callCallbacks(newMessage.id, newMessage); return; } @@ -41,30 +89,30 @@ class VSCodeAPIWrapper { newMessage.type === "e2w" && newMessage.command in this.messageCallback ) { - this.messageCallback[newMessage.command](newMessage); + callCallbacks(newMessage.command, newMessage); return; } }); } public postMessageCallback( - message: { type: string; data: any }, + message: MessageToExtension, messageCallback?: (message: any) => void, config?: { signal?: AbortSignal } ) { if (this.vsCodeApi) { - const messageId = randomMessageId(); + const id = randomId(); if (messageCallback) { - this.addMessageListener(messageId, messageCallback); + this.addMessageListener(id, messageCallback); } config?.signal?.addEventListener("abort", () => { - this.abortOperation(messageId); + this.abortOperation(); }); this.vsCodeApi.postMessage({ ...message, - messageId, + id, }); } else { console.log(message); @@ -81,7 +129,7 @@ class VSCodeAPIWrapper { this.postMessageCallback( { data: chatHistory, - type: "sendMessage", + type: "send-message", }, (message) => { if (message.done) { @@ -98,17 +146,128 @@ class VSCodeAPIWrapper { return transform.stream(); } + public getChat(chatId: string) { + return new Promise((resolve) => { + this.postMessageCallback( + { + type: "get-chat", + chatId: chatId, + }, + (message) => { + resolve(message.data); + } + ); + }); + } + + public getSettings() { + return new Promise<{ + chatEnabled: boolean; + chatIsWorking: boolean; + userLoggined: boolean; + }>((resolve) => { + this.postMessageCallback( + { + type: "get-settings", + }, + (message) => { + resolve(message.data); + } + ); + }); + } + + public enableChat() { + return new Promise((resolve) => { + this.postMessageCallback( + { + type: "enable-chat", + }, + (message) => { + resolve(message.data); + } + ); + }); + } + + public saveChatHistory(chatId: string, history: Chat) { + return new Promise((resolve) => { + this.postMessageCallback( + { + type: "save-chat", + chatId: chatId, + data: history, + }, + (message) => { + resolve(message.data); + } + ); + }); + } + + public getChats() { + return new Promise((resolve) => { + this.postMessageCallback( + { + type: "get-chats", + }, + (message) => { + resolve(message.data); + } + ); + }); + } + + public deleteChat(chatId: string) { + return new Promise((resolve) => { + this.postMessageCallback( + { + type: "delete-chat", + chatId: chatId, + }, + (message) => { + resolve(message.data); + } + ); + }); + } + + public deleteChats() { + return new Promise((resolve) => { + this.postMessageCallback( + { + type: "delete-chats", + }, + (message) => { + resolve(message.data); + } + ); + }); + } + public addMessageListener( commandOrMessageId: string, callback: (message: any) => void ) { - this.messageCallback[commandOrMessageId] = callback; + const callbackId = randomId(); + if (commandOrMessageId in this.messageCallback) { + this.messageCallback[commandOrMessageId][callbackId] = callback; + } else { + this.messageCallback[commandOrMessageId] = { + [callbackId]: callback, + }; + } + // remove callback on dispose + return () => { + if (commandOrMessageId in this.messageCallback) { + delete this.messageCallback[commandOrMessageId][callbackId]; + } + }; } - private abortOperation(messageId: string) { + private abortOperation() { this.vsCodeApi?.postMessage({ type: "abort-generate", - id: messageId, }); } }