From f4ceb53738250b194c3231579eb8f826b57d366a Mon Sep 17 00:00:00 2001 From: w1gs Date: Wed, 18 Sep 2024 02:02:43 -0400 Subject: [PATCH 1/9] Added Ollama integration --- extensions/void/package.json | 10 +- extensions/void/src/SidebarWebviewProvider.ts | 5 +- extensions/void/src/common/sendLLMMessage.ts | 138 ++++++++++-------- extensions/void/src/extension.ts | 3 +- 4 files changed, 90 insertions(+), 66 deletions(-) diff --git a/extensions/void/package.json b/extensions/void/package.json index ebccfe721..21be74ed9 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -40,10 +40,15 @@ "default": "", "description": "Greptile - Github PAT (gives Greptile access to your repo)" }, - "void.ollamaSettings": { + "void.ollamaSettings.endpoint": { "type": "string", "default": "", - "description": "Ollama settings (coming soon...)" + "description": "Ollama Endpoint - Local API server can be started with `OLLAMA_ORIGINS=* ollama serve`" + }, + "void.ollamaSettings.model": { + "type": "string", + "default": "", + "description": "Ollama model to use" } } }, @@ -126,7 +131,6 @@ "eslint-plugin-react-hooks": "^4.6.2", "globals": "^15.9.0", "marked": "^14.1.0", - "ollama": "^0.5.8", "postcss": "^8.4.41", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/extensions/void/src/SidebarWebviewProvider.ts b/extensions/void/src/SidebarWebviewProvider.ts index fe01c8001..46c45085a 100644 --- a/extensions/void/src/SidebarWebviewProvider.ts +++ b/extensions/void/src/SidebarWebviewProvider.ts @@ -54,8 +54,9 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { const nonce = getNonce(); // only scripts with the nonce are allowed to run, this is a recommended security measure - - const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com'] + // Allow Ollama endpoint + const ollamaEndpoint = vscode.workspace.getConfiguration('void').get('ollamaSettings.endpoint') || 'http://localhost:11434' + const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com', ollamaEndpoint ] webview.html = ` diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index ca3c34bde..bf87f40ba 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -1,8 +1,6 @@ import Anthropic from '@anthropic-ai/sdk'; import OpenAI from 'openai'; -// import ollama from 'ollama' - export type ApiConfig = { anthropic: { apikey: string, @@ -20,7 +18,8 @@ export type ApiConfig = { } }, ollama: { - // TODO + endpoint: string, + model: string }, whichApi: string } @@ -220,66 +219,85 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => { if (!apiConfig) return { abort: () => { } } - const whichApi = apiConfig.whichApi - - if (whichApi === 'anthropic') { - return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) - } - else if (whichApi === 'openai') { - return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig }) - } - else if (whichApi === 'greptile') { - return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig }) + const whichApi = apiConfig.whichApi; + + switch (whichApi) { + case 'anthropic': + return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); + case 'openai': + return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig }); + case 'greptile': + return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig }); + case 'ollama': + return sendOllamaMsg({ messages, onText, onFinalMessage, apiConfig }); + default: + console.error(`Error: whichApi was ${whichApi}, which is not recognized!`); + return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO } - else if (whichApi === 'ollama') { - return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) // TODO - } - else { - console.error(`Error: whichApi was ${whichApi}, which is not recognized!`) - return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) // TODO - } - } // Ollama -// const sendOllamaMsg: sendMsgFnType = ({ messages, onText, onFinalMessage }) => { - -// let did_abort = false -// let fullText = '' - -// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either -// let abort: () => void = () => { -// did_abort = true -// } - -// ollama.chat({ model: 'llama3.1', messages: messages, stream: true }) -// .then(async response => { - -// abort = () => { -// // response.abort() // this isn't needed now, to keep consistency with claude will leave it commented for now -// did_abort = true; -// } - -// // when receive text -// try { -// for await (const part of response) { -// if (did_abort) return -// let newText = part.message.content -// fullText += newText -// onText(newText, fullText) -// } -// } -// // when error/fail -// catch (e) { -// onFinalMessage(fullText) -// return -// } - -// // when we get the final message on this stream -// onFinalMessage(fullText) -// }) - -// return { abort }; -// }; +export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { + let didAbort = false; + let fullText = ""; + // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either + const abort = () => { + didAbort = true; + }; + + const handleError = (error: any) => { + console.error('Error:', error); + onFinalMessage(fullText); + }; + + if (apiConfig.ollama.endpoint.endsWith('/')) { + apiConfig.ollama.endpoint = apiConfig.ollama.endpoint.slice(0, -1); + } + + fetch(`${apiConfig.ollama.endpoint}/api/chat`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: apiConfig.ollama.model, + messages: messages, + stream: true, + }), + }) + .then(response => { + if (didAbort) return; + const reader = response.body?.getReader(); + if (!reader) { + onFinalMessage(fullText); + return; + } + return reader; + }) + .then(reader => { + if (!reader) return; + + const readStream = async () => { + try { + let done, value; + while ({ done, value } = await reader.read(), !done) { + if (didAbort) return; + const stringedResponse = new TextDecoder().decode(value); + const newText = JSON.parse(stringedResponse).message.content; + fullText += newText; + onText(newText, fullText); + } + onFinalMessage(fullText); + } catch (error) { + handleError(error); + } + }; + + readStream(); + }) + .catch(handleError); + + return { abort }; +}; diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index 4cd8ac265..d95c28735 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -25,7 +25,8 @@ const getApiConfig = () => { } }, ollama: { - // apikey: vscode.workspace.getConfiguration('void').get('ollamaSettings') ?? '', + endpoint: vscode.workspace.getConfiguration('void').get('ollamaSettings.endpoint') ?? '', + model: vscode.workspace.getConfiguration('void').get('ollamaSettings.model') ?? '', }, whichApi: vscode.workspace.getConfiguration('void').get('whichApi') ?? '' } From 7bcefd45360bba91252d65857f78c35fc3e110ee Mon Sep 17 00:00:00 2001 From: Darion Date: Fri, 20 Sep 2024 13:56:21 -0400 Subject: [PATCH 2/9] Update extensions/void/package.json Co-authored-by: Bruce MacDonald --- extensions/void/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/void/package.json b/extensions/void/package.json index 21be74ed9..7d139bff3 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -43,7 +43,7 @@ "void.ollamaSettings.endpoint": { "type": "string", "default": "", - "description": "Ollama Endpoint - Local API server can be started with `OLLAMA_ORIGINS=* ollama serve`" + "description": "Ollama Endpoint - Local API server can be started with `OLLAMA_ORIGINS="vscode-webview://*" ollama serve`" }, "void.ollamaSettings.model": { "type": "string", From 2b3f82865427802b25c738b2f587ae90487eed1e Mon Sep 17 00:00:00 2001 From: w1gs Date: Sat, 21 Sep 2024 00:24:51 -0400 Subject: [PATCH 3/9] Fixed parsing error in package.json --- extensions/void/package-lock.json | 16 ---------------- extensions/void/package.json | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index cb1b8f871..1c49e24ad 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -32,7 +32,6 @@ "eslint-plugin-react-hooks": "^4.6.2", "globals": "^15.9.0", "marked": "^14.1.0", - "ollama": "^0.5.8", "postcss": "^8.4.41", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -5184,15 +5183,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ollama": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.9.tgz", - "integrity": "sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==", - "dev": true, - "dependencies": { - "whatwg-fetch": "^3.6.20" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7241,12 +7231,6 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "dev": true - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/extensions/void/package.json b/extensions/void/package.json index 7d139bff3..a02678968 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -43,7 +43,7 @@ "void.ollamaSettings.endpoint": { "type": "string", "default": "", - "description": "Ollama Endpoint - Local API server can be started with `OLLAMA_ORIGINS="vscode-webview://*" ollama serve`" + "description": "Ollama Endpoint - Local API server can be started with `OLLAMA_ORIGINS=\"vscode-webview://*\" ollama serve`" }, "void.ollamaSettings.model": { "type": "string", From e8cf6ee9e7260a9c545d572d5d8fac0c81ff28ad Mon Sep 17 00:00:00 2001 From: w1gs Date: Sat, 21 Sep 2024 00:36:11 -0400 Subject: [PATCH 4/9] Replaced fetch with Ollama client --- extensions/void/package-lock.json | 14 +++++ extensions/void/package.json | 1 + extensions/void/src/common/sendLLMMessage.ts | 66 ++++++-------------- 3 files changed, 33 insertions(+), 48 deletions(-) diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index 1c49e24ad..354cc7e76 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@anthropic-ai/sdk": "^0.27.1", + "ollama": "^0.5.9", "openai": "^4.57.0" }, "devDependencies": { @@ -5183,6 +5184,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ollama": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.9.tgz", + "integrity": "sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==", + "dependencies": { + "whatwg-fetch": "^3.6.20" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7231,6 +7240,11 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/extensions/void/package.json b/extensions/void/package.json index a02678968..4e66383d6 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -142,6 +142,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.27.1", + "ollama": "^0.5.9", "openai": "^4.57.0" } } diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index bf87f40ba..2fb3dd3df 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -1,5 +1,6 @@ import Anthropic from '@anthropic-ai/sdk'; import OpenAI from 'openai'; +import { Ollama } from 'ollama/browser' export type ApiConfig = { anthropic: { @@ -239,6 +240,8 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, // Ollama export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { + const ollamaClient = new Ollama({ host: apiConfig.ollama.endpoint }) + let didAbort = false; let fullText = ""; @@ -247,57 +250,24 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, didAbort = true; }; - const handleError = (error: any) => { - console.error('Error:', error); - onFinalMessage(fullText); - }; - - if (apiConfig.ollama.endpoint.endsWith('/')) { - apiConfig.ollama.endpoint = apiConfig.ollama.endpoint.slice(0, -1); - } - - fetch(`${apiConfig.ollama.endpoint}/api/chat`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: apiConfig.ollama.model, - messages: messages, - stream: true, - }), + ollamaClient.chat({ + model: apiConfig.ollama.model, + messages: messages, + stream: true, }) - .then(response => { - if (didAbort) return; - const reader = response.body?.getReader(); - if (!reader) { - onFinalMessage(fullText); - return; + .then(async (stream) => { + for await (const chunk of stream) { + if (didAbort) return; + const newText = chunk.message.content; + fullText += newText; + onText(newText, fullText); } - return reader; - }) - .then(reader => { - if (!reader) return; - - const readStream = async () => { - try { - let done, value; - while ({ done, value } = await reader.read(), !done) { - if (didAbort) return; - const stringedResponse = new TextDecoder().decode(value); - const newText = JSON.parse(stringedResponse).message.content; - fullText += newText; - onText(newText, fullText); - } - onFinalMessage(fullText); - } catch (error) { - handleError(error); - } - }; - - readStream(); + onFinalMessage(fullText); }) - .catch(handleError); + .catch((error) => { + console.error('Error:', error); + onFinalMessage(fullText); + }); return { abort }; }; From 5e5fe8090175a7bcf6ede71ea697e698087bc5c0 Mon Sep 17 00:00:00 2001 From: w1gs Date: Sat, 21 Sep 2024 04:56:41 -0400 Subject: [PATCH 5/9] allowed urls updating when configuration changes --- extensions/void/src/SidebarWebviewProvider.ts | 65 +++++++++++++------ extensions/void/src/common/sendLLMMessage.ts | 23 +++++-- extensions/void/src/extension.ts | 7 +- extensions/void/src/shared_types.ts | 3 + extensions/void/src/sidebar/Sidebar.tsx | 7 +- extensions/void/src/sidebar/getVscodeApi.ts | 3 +- 6 files changed, 80 insertions(+), 28 deletions(-) diff --git a/extensions/void/src/SidebarWebviewProvider.ts b/extensions/void/src/SidebarWebviewProvider.ts index 46c45085a..0d85fd7f6 100644 --- a/extensions/void/src/SidebarWebviewProvider.ts +++ b/extensions/void/src/SidebarWebviewProvider.ts @@ -18,6 +18,8 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { private readonly _extensionUri: vscode.Uri private _res: (c: vscode.Webview) => void // used to resolve the webview + private allowed_urls: string[] = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com', 'http://localhost:11434']; + private _webviewView?: vscode.WebviewView; constructor(context: vscode.ExtensionContext) { // const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later, not sure for what though... was included in webviewProvider code @@ -27,6 +29,45 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { this.webview = new Promise((res, rej) => { temp_res = res }) if (!temp_res) throw new Error("sidebar provider: resolver was undefined") this._res = temp_res + + vscode.workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration('void.ollamaSettings.endpoint')) { + this.updateAllowedUrls(); + // Regenerate the webview's HTML content + if (this._webviewView) { + this._webviewView.webview.html = this.getWebviewContent(this._webviewView.webview); + } + } + }); + } + + private getWebviewContent(webview: vscode.Webview): string { + const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js')); + const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css')); + const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri)); + const nonce = getNonce(); + + const allowed_urls = this.allowed_urls; + return ` + + + + + Custom View + + + + + +
+ + + `; + } + + private updateAllowedUrls() { + const ollamaEndpoint: string = vscode.workspace.getConfiguration('void').get('ollamaSettings.endpoint') || 'http://localhost:11434'; + this.allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com', ollamaEndpoint]; } // called internally by vscode @@ -35,6 +76,7 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken, ) { + this._webviewView = webviewView; const webview = webviewView.webview @@ -54,25 +96,10 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { const nonce = getNonce(); // only scripts with the nonce are allowed to run, this is a recommended security measure - // Allow Ollama endpoint - const ollamaEndpoint = vscode.workspace.getConfiguration('void').get('ollamaSettings.endpoint') || 'http://localhost:11434' - const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com', ollamaEndpoint ] - webview.html = ` - - - - - Custom View - - - - - -
- - - `; - + // Regenerate the Sidebar html whenever the allowed_urls changes + this.updateAllowedUrls(); + const allowed_urls = this.allowed_urls; + webview.html = this.getWebviewContent(webview); this._res(webview); } diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index 2fb3dd3df..de39feaa8 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -1,6 +1,8 @@ import Anthropic from '@anthropic-ai/sdk'; import OpenAI from 'openai'; import { Ollama } from 'ollama/browser' +import { getVSCodeAPI } from '../sidebar/getVscodeApi'; + export type ApiConfig = { anthropic: { @@ -220,9 +222,21 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => { if (!apiConfig) return { abort: () => { } } - const whichApi = apiConfig.whichApi; + if ( + apiConfig.anthropic.apikey === "" && + apiConfig.greptile.apikey === "" && + apiConfig.openai.apikey === "" && + apiConfig.ollama.endpoint === "" && + apiConfig.ollama.model === "" + ) { + getVSCodeAPI().postMessage({ type: 'displayError', message: 'Required API keys are not set.' }) + onFinalMessage("Required API keys are not set."); + return { abort: () => { }} + } + + - switch (whichApi) { + switch (apiConfig.whichApi) { case 'anthropic': return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); case 'openai': @@ -232,8 +246,9 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, case 'ollama': return sendOllamaMsg({ messages, onText, onFinalMessage, apiConfig }); default: - console.error(`Error: whichApi was ${whichApi}, which is not recognized!`); - return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO + console.error(`Error: whichApi was ${apiConfig.whichApi}, which is not recognized!`); + return { abort: () => { } } + //return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO } } diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index d95c28735..835dcade2 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -33,6 +33,7 @@ const getApiConfig = () => { return apiConfig } + export function activate(context: vscode.ExtensionContext) { // 1. Mount the chat sidebar @@ -116,14 +117,16 @@ export function activate(context: vscode.ExtensionContext) { const oldContents = await readFileContentOfUri(editor.document.uri) const suggestedEdits = getDiffedLines(oldContents, m.code) await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits) - } - else if (m.type === 'getApiConfig') { + } else if (m.type === 'getApiConfig') { const apiConfig = getApiConfig() console.log('Api config:', apiConfig) webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage) + } else if (m.type === 'displayError') { + + vscode.window.showWarningMessage(m.message, { modal: true }); } else { diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index 4d7c0dc9c..722c3218b 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/shared_types.ts @@ -27,6 +27,9 @@ type WebviewMessage = ( // editor -> sidebar | { type: 'apiConfig', apiConfig: ApiConfig } + // Display native vscode error + | { type: 'displayError', message: string } // + ) type Command = WebviewMessage['type'] diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index 0107e2284..614268696 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -166,6 +166,7 @@ const Sidebar = () => { // if user pressed ctrl+l, add their selection to the sidebar if (m.type === 'ctrl+l') { + setSelection(m.selection) const filepath = m.selection.filePath @@ -223,10 +224,12 @@ const Sidebar = () => { }, apiConfig: apiConfig }) - abortFnRef.current = abort + + abortFnRef.current = abort } + const onStop = useCallback(() => { // abort claude abortFnRef.current?.() @@ -266,7 +269,7 @@ const Sidebar = () => { {!selection?.selectionStr ? null : (
- +}; + +const FilesSelector = ({ + files, + setFiles, +}: { + files: vscode.Uri[]; + setFiles: (files: vscode.Uri[]) => void; +}) => { + return ( + files.length !== 0 && ( +
+ Include files: + {files.map((filename, i) => ( +
+ {/* X button on a file */} + +
+ ))}
- )} -
-} + ) + ); +}; const IncludedFiles = ({ files }: { files: vscode.Uri[] }) => { - return files.length !== 0 &&
- {files.map((filename, i) => -
- + return ( + files.length !== 0 && ( +
+ {files.map((filename, i) => ( +
+ +
+ ))}
- )} -
-} - + ) + ); +}; const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => { + const role = chatMessage.role; + const children = chatMessage.displayContent; - const role = chatMessage.role - const children = chatMessage.displayContent - - if (!children) - return null + if (!children) return null; - let chatbubbleContents: React.ReactNode + let chatbubbleContents: React.ReactNode; - if (role === 'user') { - chatbubbleContents = <> - - {chatMessage.selection?.selectionStr && } - {children} - - } - else if (role === 'assistant') { + if (role === "user") { + chatbubbleContents = ( + <> + + {chatMessage.selection?.selectionStr && ( + + )} + {children} + + ); + } else if (role === "assistant") { const tokens = marked.lexer(children); // https://marked.js.org/using_pro#renderer - chatbubbleContents = // sectionsHTML + chatbubbleContents = ; // sectionsHTML } - - return
-
- {chatbubbleContents} + return ( +
+
+ {chatbubbleContents} +
-
-} + ); +}; const getBasename = (pathStr: string) => { // "unixify" path - pathStr = pathStr.replace(/[/\\]+/g, '/'); // replace any / or \ or \\ with / - const parts = pathStr.split('/') // split on / - return parts[parts.length - 1] -} - -type ChatMessage = { - role: 'user' - content: string, // content sent to the llm - displayContent: string, // content displayed to user - selection: Selection | null, // the user's selection - files: vscode.Uri[], // the files sent in the message -} | { - role: 'assistant', - content: string, // content received from LLM - displayContent: string // content displayed to user (this is the same as content for now) -} - + pathStr = pathStr.replace(/[/\\]+/g, "/"); // replace any / or \ or \\ with / + const parts = pathStr.split("/"); // split on / + return parts[parts.length - 1]; +}; + +type ChatMessage = + | { + role: "user"; + content: string; // content sent to the llm + displayContent: string; // content displayed to user + selection: Selection | null; // the user's selection + files: vscode.Uri[]; // the files sent in the message + } + | { + role: "assistant"; + content: string; // content received from LLM + displayContent: string; // content displayed to user (this is the same as content for now) + }; // const [stateRef, setState] = useInstantState(initVal) // setState instantly changes the value of stateRef instead of having to wait until the next render + const useInstantState = (initVal: T) => { - const stateRef = useRef(initVal) - const [_, setS] = useState(initVal) + const stateRef = useRef(initVal); + const [_, setS] = useState(initVal); const setState = useCallback((newVal: T) => { setS(newVal); stateRef.current = newVal; - }, []) - return [stateRef as React.RefObject, setState] as const // make s.current readonly - setState handles all changes -} - - + }, []); + return [stateRef as React.RefObject, setState] as const; // make s.current readonly - setState handles all changes +}; const Sidebar = () => { - // state of current message - const [selection, setSelection] = useState(null) // the code the user is selecting - const [files, setFiles] = useState([]) // the names of the files in the chat - const [instructions, setInstructions] = useState('') // the user's instructions + const [selection, setSelection] = useState(null); // the code the user is selecting + const [files, setFiles] = useState([]); // the names of the files in the chat + const [instructions, setInstructions] = useState(""); // the user's instructions // state of chat - const [chatMessageHistory, setChatHistory] = useState([]) - const [messageStream, setMessageStream] = useState('') - const [isLoading, setIsLoading] = useState(false) - - const abortFnRef = useRef<(() => void) | null>(null) + const [chatMessageHistory, setChatHistory] = useState([]); + const [messageStream, setMessageStream] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [isDisabled, setIsDisabled] = useState(false); + const [errorShown, setErrorShown] = useState(false); + + + const abortFnRef = useRef<(() => void) | null>(null); + + const [apiConfig, setApiConfig] = useState(null); + + const checkApiConfig = (apiConfig: ApiConfig) => { + if ( + (apiConfig.anthropic.apikey === "" && + apiConfig.greptile.apikey === "" && + apiConfig.openai.apikey === "" && + (apiConfig.ollama.endpoint === "" || + apiConfig.ollama.model === "")) || + apiConfig.whichApi === "" + ) { + setIsDisabled(true); + } else { + setIsDisabled(false); + } - const [apiConfig, setApiConfig] = useState(null) + } // get Api Config on mount useEffect(() => { - getVSCodeAPI().postMessage({ type: 'getApiConfig' }) - }, []) + getVSCodeAPI().postMessage({ type: "getApiConfig" }); + }, []); // Receive messages from the extension useEffect(() => { const listener = (event: MessageEvent) => { - const m = event.data as WebviewMessage; // resolve any awaiting promises // eg. it will resolve the promise below for `await VSCodeResponse('files')` - resolveAwaitingVSCodeResponse(m) + resolveAwaitingVSCodeResponse(m); // if user pressed ctrl+l, add their selection to the sidebar - if (m.type === 'ctrl+l') { - - - setSelection(m.selection) - - const filepath = m.selection.filePath + if (m.type === "ctrl+l") { + if (isDisabled) { + getVSCodeAPI().postMessage({ + type: "displayError", + message: "Required API keys are not set.", + }); + return; + } + setSelection(m.selection); + + const filepath = m.selection.filePath; // add file if it's not a duplicate - if (!files.find(f => f.fsPath === filepath.fsPath)) setFiles(files => [...files, filepath]) - + if (!files.find((f) => f.fsPath === filepath.fsPath)) + setFiles((files) => [...files, filepath]); } // when get apiConfig, set - else if (m.type === 'apiConfig') { - setApiConfig(m.apiConfig) + else if (m.type === "apiConfig") { + setApiConfig(m.apiConfig); + checkApiConfig(m.apiConfig); } - - } - window.addEventListener('message', listener); - return () => { window.removeEventListener('message', listener) } - }, [files, selection]) - - - const formRef = useRef(null) + }; + window.addEventListener("message", listener); + return () => { + window.removeEventListener("message", listener); + }; + }, [files, selection, isDisabled]); + + const formRef = useRef(null); const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (isLoading || isDisabled) return; - e.preventDefault() - if (isLoading) return - - setIsLoading(true) - setInstructions(''); + setIsLoading(true); + setInstructions(""); formRef.current?.reset(); // reset the form's text - setSelection(null) - setFiles([]) + setSelection(null); + setFiles([]); // request file content from vscode and await response - getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files }) - const relevantFiles = await awaitVSCodeResponse('files') + getVSCodeAPI().postMessage({ type: "requestFiles", filepaths: files }); + const relevantFiles = await awaitVSCodeResponse("files"); // add message to chat history - const content = userInstructionsStr(instructions, relevantFiles.files, selection) + const content = userInstructionsStr( + instructions, + relevantFiles.files, + selection + ); // console.log('prompt:\n', content) - const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files } - setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) + const newHistoryElt: ChatMessage = { + role: "user", + content, + displayContent: instructions, + selection, + files, + }; + setChatHistory((chatMessageHistory) => [ + ...chatMessageHistory, + newHistoryElt, + ]); // send message to claude let { abort } = sendLLMMessage({ - messages: [...chatMessageHistory.map(m => ({ role: m.role, content: m.content })), { role: 'user', content }], + messages: [ + ...chatMessageHistory.map((m) => ({ + role: m.role, + content: m.content, + })), + { role: "user", content }, + ], onText: (newText, fullText) => setMessageStream(fullText), onFinalMessage: (content) => { - // add assistant's message to chat history - const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, } - setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) + const newHistoryElt: ChatMessage = { + role: "assistant", + content, + displayContent: content, + }; + setChatHistory((chatMessageHistory) => [ + ...chatMessageHistory, + newHistoryElt, + ]); // clear selection - setMessageStream('') - setIsLoading(false) + setMessageStream(""); + setIsLoading(false); }, - apiConfig: apiConfig - }) - - - abortFnRef.current = abort - } + apiConfig: apiConfig, + }); + abortFnRef.current = abort; + }; const onStop = useCallback(() => { // abort claude - abortFnRef.current?.() + abortFnRef.current?.(); // if messageStream was not empty, add it to the history - const llmContent = messageStream || '(canceled)' - const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent } - setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) - - setMessageStream('') - setIsLoading(false) - - }, [messageStream]) + const llmContent = messageStream || "(canceled)"; + const newHistoryElt: ChatMessage = { + role: "assistant", + displayContent: messageStream, + content: llmContent, + }; + setChatHistory((chatMessageHistory) => [ + ...chatMessageHistory, + newHistoryElt, + ]); + + setMessageStream(""); + setIsLoading(false); + }, [messageStream]); //Clear code selection const clearSelection = () => { setSelection(null); }; - return <> -
-
- {/* previous messages */} - {chatMessageHistory.map((message, i) => - - )} - {/* message stream */} - -
- {/* chatbar */} -
- {/* selection */} -
- {/* selected files */} - - {/* selected code */} - {!selection?.selectionStr ? null - : ( + return ( + <> +
+
+ {/* previous messages */} + {chatMessageHistory.map((message, i) => ( + + ))} + {/* message stream */} + +
+ {/* chatbar */} +
+ {/* selection */} +
+ {/* selected files */} + + {/* selected code */} + {!selection?.selectionStr ? null : (
- +
- )} + )} +
+
{ + if (e.key === "Enter" && !e.shiftKey) onSubmit(e); + }} + onSubmit={(e) => { + console.log("submit!"); + e.preventDefault(); + onSubmit(e); + }} + > + {/* input */} + +