From b30937934a03576ead71ac0e08d253737be0f275 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Oct 2024 01:12:57 -0700 Subject: [PATCH 01/15] progress moving settings to be extension-native --- extensions/void/package.json | 247 +----------- extensions/void/src/common/sendLLMMessage.ts | 105 ++---- extensions/void/src/extension.ts | 60 +-- extensions/void/src/shared_types.ts | 7 +- extensions/void/src/sidebar/Sidebar.tsx | 21 +- extensions/void/src/sidebar/SidebarChat.tsx | 34 +- .../void/src/sidebar/SidebarSettings.tsx | 16 + .../src/sidebar/SidebarThreadSelector.tsx | 2 +- .../void/src/sidebar/contextForConfig.tsx | 354 ++++++++++++++++++ ...readsContext.tsx => contextForThreads.tsx} | 13 +- extensions/void/src/sidebar/getVscodeApi.ts | 4 +- extensions/void/src/sidebar/index.tsx | 7 +- 12 files changed, 456 insertions(+), 414 deletions(-) create mode 100644 extensions/void/src/sidebar/SidebarSettings.tsx create mode 100644 extensions/void/src/sidebar/contextForConfig.tsx rename extensions/void/src/sidebar/{threadsContext.tsx => contextForThreads.tsx} (86%) diff --git a/extensions/void/package.json b/extensions/void/package.json index 72bfb32a..e1d147c1 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -16,252 +16,7 @@ "configuration": { "title": "Void", "properties": { - "void.whichApi": { - "type": "string", - "default": "anthropic", - "description": "Choose an API provider.", - "enum": [ - "openAI", - "openRouter", - "openAICompatible", - "anthropic", - "azure", - "ollama", - "greptile" - ] - }, - "void.anthropic.apiKey": { - "type": "string", - "default": "", - "description": "Anthropic API key." - }, - "void.anthropic.model": { - "type": "string", - "default": "claude-3-5-sonnet-20240620", - "description": "Anthropic model to use.", - "enum": [ - "claude-3-5-sonnet-20240620", - "claude-3-opus-20240229", - "claude-3-sonnet-20240229", - "claude-3-haiku-20240307" - ] - }, - "void.anthropic.maxTokens": { - "type": "string", - "default": "8192", - "description": "Anthropic max number of tokens to output.", - "enum": [ - "1024", - "2048", - "4096", - "8192" - ] - }, - "void.openAI.apiKey": { - "type": "string", - "default": "", - "description": "OpenAI API key." - }, - "void.openAI.model": { - "type": "string", - "default": "gpt-4o", - "description": "OpenAI model to use.", - "enum": [ - "o1-preview", - "o1-mini", - "gpt-4o", - "gpt-4o-2024-05-13", - "gpt-4o-2024-08-06", - "gpt-4o-mini", - "gpt-4o-mini-2024-07-18", - "gpt-4-turbo", - "gpt-4-turbo-2024-04-09", - "gpt-4-turbo-preview", - "gpt-4-0125-preview", - "gpt-4-1106-preview", - "gpt-4", - "gpt-4-0613", - "gpt-3.5-turbo-0125", - "gpt-3.5-turbo", - "gpt-3.5-turbo-1106" - ] - }, - "void.greptile.apiKey": { - "type": "string", - "default": "", - "description": "Greptile API key." - }, - "void.greptile.githubPAT": { - "type": "string", - "default": "", - "description": "Github PAT given to Greptile to access your repository." - }, - "void.greptile.remote": { - "type": "string", - "description": "remote provider", - "enum": [ - "github", - "gitlab" - ] - }, - "void.greptile.repository": { - "type": "string", - "description": "Repository identifier in \"owner/repository\" format." - }, - "void.greptile.branch": { - "type": "string", - "default": "main", - "description": "Name of the git branch." - }, - "void.azure.apiKey": { - "type": "string", - "description": "Azure API key." - }, - "void.azure.deploymentId": { - "type": "string", - "description": "Azure API deployment ID." - }, - "void.azure.resourceName": { - "type": "string", - "description": "Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`" - }, - "void.azure.providerSettings": { - "type": "object", - "properties": { - "baseURL": { - "type": "string", - "default": "https://${resourceName}.openai.azure.com/openai/deployments", - "description": "Azure API base URL." - }, - "headers": { - "type": "object", - "description": "Custom headers to include in the requests." - } - } - }, - "void.ollama.endpoint": { - "type": "string", - "default": "http://127.0.0.1:11434", - "description": "The Ollama endpoint. Start Ollama by running `OLLAMA_ORIGINS=\"vscode-webview://*\" ollama serve`" - }, - "void.ollama.model": { - "type": "string", - "default": "llama3.1", - "description": "Ollama model to use.", - "enum": [ - "codegemma", - "codegemma:2b", - "codegemma:7b", - "codellama", - "codellama:7b", - "codellama:13b", - "codellama:34b", - "codellama:70b", - "codellama:code", - "codellama:python", - "command-r", - "command-r:35b", - "command-r-plus", - "command-r-plus:104b", - "deepseek-coder-v2", - "deepseek-coder-v2:16b", - "deepseek-coder-v2:236b", - "falcon2", - "falcon2:11b", - "firefunction-v2", - "firefunction-v2:70b", - "gemma", - "gemma:2b", - "gemma:7b", - "gemma2", - "gemma2:2b", - "gemma2:9b", - "gemma2:27b", - "llama2", - "llama2:7b", - "llama2:13b", - "llama2:70b", - "llama3", - "llama3:8b", - "llama3:70b", - "llama3-chatqa", - "llama3-chatqa:8b", - "llama3-chatqa:70b", - "llama3-gradient", - "llama3-gradient:8b", - "llama3-gradient:70b", - "llama3.1", - "llama3.1:8b", - "llama3.1:70b", - "llama3.1:405b", - "llava", - "llava:7b", - "llava:13b", - "llava:34b", - "llava-llama3", - "llava-llama3:8b", - "llava-phi3", - "llava-phi3:3.8b", - "mistral", - "mistral:7b", - "mistral-large", - "mistral-large:123b", - "mistral-nemo", - "mistral-nemo:12b", - "mixtral", - "mixtral:8x7b", - "mixtral:8x22b", - "moondream", - "moondream:1.8b", - "openhermes", - "openhermes:v2.5", - "phi3", - "phi3:3.8b", - "phi3:14b", - "phi3.5", - "phi3.5:3.8b", - "qwen", - "qwen:7b", - "qwen:14b", - "qwen:32b", - "qwen:72b", - "qwen:110b", - "qwen2", - "qwen2:0.5b", - "qwen2:1.5b", - "qwen2:7b", - "qwen2:72b", - "smollm", - "smollm:135m", - "smollm:360m", - "smollm:1.7b" - ] - }, - "void.openRouter.model": { - "type": "string", - "default": "openai/gpt-4o", - "description": "OpenRouter model to use." - }, - "void.openRouter.apiKey": { - "type": "string", - "default": "", - "description": "OpenRouter API key." - }, - "void.openAICompatible.endpoint": { - "type": "string", - "default": "http://127.0.0.1:11434/v1", - "description": "The endpoint." - }, - "void.openAICompatible.model": { - "type": "string", - "default": "gpt-4o", - "description": "The name of the model to use." - }, - "void.openAICompatible.apiKey": { - "type": "string", - "default": "", - "description": "Your API key." - } + } }, "commands": [ diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index f8ea1d97..764647b9 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -1,44 +1,9 @@ import Anthropic from '@anthropic-ai/sdk'; import OpenAI from 'openai'; import { Ollama } from 'ollama/browser' +import { VoidConfig } from '../sidebar/contextForConfig'; -// always compare these against package.json to make sure every setting in this type can actually be provided by the user -export type ApiConfig = { - anthropic: { - apikey: string, - model: string, - maxTokens: string - }, - openAI: { - apikey: string, - model: string, - }, - greptile: { - apikey: string, - githubPAT: string, - repoinfo: { - remote: string, // e.g. 'github' - repository: string, // e.g. 'voideditor/void' - branch: string // e.g. 'main' - } - }, - ollama: { - endpoint: string, - model: string - }, - openAICompatible: { - endpoint: string, - model: string, - apikey: string - }, - openRouter: { - model: string, - apikey: string - } - whichApi: string -} - type OnText = (newText: string, fullText: string) => void @@ -52,7 +17,7 @@ type SendLLMMessageFnTypeInternal = (params: { messages: LLMMessage[], onText: OnText, onFinalMessage: (input: string) => void, - apiConfig: ApiConfig, + voidConfig: VoidConfig, }) => { abort: () => void @@ -62,7 +27,7 @@ type SendLLMMessageFnTypeExternal = (params: { messages: LLMMessage[], onText: OnText, onFinalMessage: (input: string) => void, - apiConfig: ApiConfig | null, + voidConfig: VoidConfig | null, }) => { abort: () => void @@ -72,13 +37,13 @@ type SendLLMMessageFnTypeExternal = (params: { // Claude -const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { +const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, voidConfig }) => { - const anthropic = new Anthropic({ apiKey: apiConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] + const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] const stream = anthropic.messages.stream({ - model: apiConfig.anthropic.model, - max_tokens: parseInt(apiConfig.anthropic.maxTokens), + model: voidConfig.anthropic.model, + max_tokens: parseInt(voidConfig.anthropic.maxTokens), messages: messages, }); @@ -113,7 +78,7 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal // OpenAI, OpenRouter, OpenAICompatible -const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { +const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, voidConfig }) => { let didAbort = false let fullText = '' @@ -126,27 +91,27 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal let openai: OpenAI let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming - if (apiConfig.whichApi === 'openAI') { - openai = new OpenAI({ apiKey: apiConfig.openAI.apikey, dangerouslyAllowBrowser: true }); - options = { model: apiConfig.openAI.model, messages: messages, stream: true, } + if (voidConfig.default.whichApi === 'openAI') { + openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true }); + options = { model: voidConfig.openAI.model, messages: messages, stream: true, } } - else if (apiConfig.whichApi === 'openRouter') { + else if (voidConfig.default.whichApi === 'openRouter') { openai = new OpenAI({ - baseURL: "https://openrouter.ai/api/v1", apiKey: apiConfig.openRouter.apikey, dangerouslyAllowBrowser: true, + baseURL: "https://openrouter.ai/api/v1", apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true, defaultHeaders: { "HTTP-Referer": 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. "X-Title": 'Void Editor', // Optional. Shows in rankings on openrouter.ai. }, }); - options = { model: apiConfig.openRouter.model, messages: messages, stream: true, } + options = { model: voidConfig.openRouter.model, messages: messages, stream: true, } } - else if (apiConfig.whichApi === 'openAICompatible') { - openai = new OpenAI({ baseURL: apiConfig.openAICompatible.endpoint, apiKey: apiConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true }) - options = { model: apiConfig.openAICompatible.model, messages: messages, stream: true, } + else if (voidConfig.default.whichApi === 'openAICompatible') { + openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true }) + options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, } } else { - console.error(`sendOpenAIMsg: invalid whichApi: ${apiConfig.whichApi}`) - throw new Error(`apiConfig.whichAPI was invalid: ${apiConfig.whichApi}`) + console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`) + throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`) } openai.chat.completions @@ -177,7 +142,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal // Ollama -export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { +export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, voidConfig }) => { let didAbort = false let fullText = "" @@ -187,10 +152,10 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, didAbort = true; }; - const ollama = new Ollama({ host: apiConfig.ollama.endpoint }) + const ollama = new Ollama({ host: voidConfig.ollama.endpoint }) ollama.chat({ - model: apiConfig.ollama.model, + model: voidConfig.ollama.model, messages: messages, stream: true, }) @@ -224,7 +189,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, // https://docs.greptile.com/api-reference/query // https://docs.greptile.com/quickstart#sample-response-streamed -const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { +const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, voidConfig }) => { let didAbort = false let fullText = '' @@ -236,14 +201,14 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin fetch('https://api.greptile.com/v2/query', { method: 'POST', headers: { - "Authorization": `Bearer ${apiConfig.greptile.apikey}`, - "X-Github-Token": `${apiConfig.greptile.githubPAT}`, + "Authorization": `Bearer ${voidConfig.greptile.apikey}`, + "X-Github-Token": `${voidConfig.greptile.githubPAT}`, "Content-Type": `application/json`, }, body: JSON.stringify({ messages, stream: true, - repositories: [apiConfig.greptile.repoinfo] + repositories: [voidConfig.greptile.repoinfo] }), }) // this is {message}\n{message}\n{message}...\n @@ -293,23 +258,23 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin -export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => { - if (!apiConfig) return { abort: () => { } } +export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, voidConfig }) => { + if (!voidConfig) return { abort: () => { } } - switch (apiConfig.whichApi) { + switch (voidConfig.default.whichApi) { case 'anthropic': - return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); + return sendClaudeMsg({ messages, onText, onFinalMessage, voidConfig }); case 'openAI': case 'openRouter': case 'openAICompatible': - return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig }); + return sendOpenAIMsg({ messages, onText, onFinalMessage, voidConfig }); case 'greptile': - return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig }); + return sendGreptileMsg({ messages, onText, onFinalMessage, voidConfig }); case 'ollama': - return sendOllamaMsg({ messages, onText, onFinalMessage, apiConfig }); + return sendOllamaMsg({ messages, onText, onFinalMessage, voidConfig }); default: - console.error(`Error: whichApi was ${apiConfig.whichApi}, which is not recognized!`); + console.error(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`); return { abort: () => { } } - //return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO + //return sendClaudeMsg({ messages, onText, onFinalMessage, voidConfig }); // TODO } } diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index 55c53350..f42074b8 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -2,53 +2,12 @@ import * as vscode from 'vscode'; import { DisplayChangesProvider } from './DisplayChangesProvider'; import { BaseDiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './shared_types'; import { SidebarWebviewProvider } from './SidebarWebviewProvider'; -import { ApiConfig } from './common/sendLLMMessage'; const readFileContentOfUri = async (uri: vscode.Uri) => { return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8') .replace(/\r\n/g, '\n') // replace windows \r\n with \n } - -const getApiConfig = () => { - const apiConfig: ApiConfig = { - anthropic: { - apikey: vscode.workspace.getConfiguration('void.anthropic').get('apiKey') ?? '', - model: vscode.workspace.getConfiguration('void.anthropic').get('model') ?? '', - maxTokens: vscode.workspace.getConfiguration('void.anthropic').get('maxTokens') ?? '', - }, - openAI: { - apikey: vscode.workspace.getConfiguration('void.openAI').get('apiKey') ?? '', - model: vscode.workspace.getConfiguration('void.openAI').get('model') ?? '', - }, - greptile: { - apikey: vscode.workspace.getConfiguration('void.greptile').get('apiKey') ?? '', - githubPAT: vscode.workspace.getConfiguration('void.greptile').get('githubPAT') ?? '', - repoinfo: { - remote: 'github', - repository: 'TODO', - branch: 'main' - } - }, - ollama: { - endpoint: vscode.workspace.getConfiguration('void.ollama').get('endpoint') ?? '', - model: vscode.workspace.getConfiguration('void.ollama').get('model') ?? '', - }, - openAICompatible: { - endpoint: vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint') ?? '', - model: vscode.workspace.getConfiguration('void.openAICompatible').get('model') ?? '', - apikey: vscode.workspace.getConfiguration('void.openAICompatible').get('apiKey') ?? '', - }, - openRouter: { - model: vscode.workspace.getConfiguration('void.openRouter').get('model') ?? '', - apikey: vscode.workspace.getConfiguration('void.openRouter').get('apiKey') ?? '', - }, - whichApi: vscode.workspace.getConfiguration('void').get('whichApi') ?? '' - } - return apiConfig -} - - export function activate(context: vscode.ExtensionContext) { // 1. Mount the chat sidebar @@ -111,15 +70,6 @@ export function activate(context: vscode.ExtensionContext) { webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar) })) - // when config changes, send it to the sidebar - vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('void')) { - const apiConfig = getApiConfig() - webview.postMessage({ type: 'apiConfig', apiConfig } satisfies MessageToSidebar) - } - }) - - // Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`) webview.onDidReceiveMessage(async (m: MessageFromSidebar) => { @@ -166,9 +116,13 @@ export function activate(context: vscode.ExtensionContext) { displayChangesProvider.refreshDiffAreas(editor.document.uri) } - else if (m.type === 'getApiConfig') { - const apiConfig = getApiConfig() - webview.postMessage({ type: 'apiConfig', apiConfig } satisfies MessageToSidebar) + else if (m.type === 'getPartialVoidConfig') { + const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {} + webview.postMessage({ type: 'partialVoidConfig', partialVoidConfig } satisfies MessageToSidebar) + } + else if (m.type === 'persistPartialVoidConfig') { + const partialVoidConfig = m.partialVoidConfig + context.workspaceState.update('partialVoidConfig', partialVoidConfig) } else if (m.type === 'getAllThreads') { const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {} diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index 0fcbef42..eed9d07b 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/shared_types.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -import { ApiConfig } from './common/sendLLMMessage'; +import { PartialVoidConfig } from './sidebar/contextForConfig'; @@ -42,7 +42,7 @@ type Diff = { type MessageToSidebar = ( | { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor | { type: 'files', files: { filepath: vscode.Uri, content: string }[] } - | { type: 'apiConfig', apiConfig: ApiConfig } + | { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig } | { type: 'allThreads', threads: ChatThreads } | { type: 'startNewThread' } | { type: 'toggleThreadSelector' } @@ -52,7 +52,8 @@ type MessageToSidebar = ( type MessageFromSidebar = ( | { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar | { type: 'requestFiles', filepaths: vscode.Uri[] } - | { type: 'getApiConfig' } + | { type: 'getPartialVoidConfig' } + | { type: 'persistPartialVoidConfig', partialVoidConfig: PartialVoidConfig } | { type: 'getAllThreads' } | { type: 'persistThread', thread: ChatThreads[string] } ) diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index e41e87e4..0ad474fb 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -1,10 +1,9 @@ import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react" -import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage" import { CodeSelection, ChatMessage, MessageToSidebar } from "../shared_types" -import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode } from "./getVscodeApi" +import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi" import { SidebarThreadSelector } from "./SidebarThreadSelector"; -import { useThreads } from "./threadsContext"; +import { useThreads } from "./contextForThreads"; import { SidebarChat } from "./SidebarChat"; @@ -12,10 +11,16 @@ import { SidebarChat } from "./SidebarChat"; const Sidebar = () => { const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false) - // get Api Config on mount - useEffect(() => { - getVSCodeAPI().postMessage({ type: 'getApiConfig' }) - }, []) + // if they pressed the + to add a new chat + useOnVSCodeMessage('startNewThread', (m) => { + setIsThreadSelectorOpen(false) + }) + + // if they toggled thread selector + useOnVSCodeMessage('toggleThreadSelector', (m) => { + setIsThreadSelectorOpen(v => !v) + }) + // Receive messages from the VSCode extension useEffect(() => { @@ -36,7 +41,7 @@ const Sidebar = () => { )} - + diff --git a/extensions/void/src/sidebar/SidebarChat.tsx b/extensions/void/src/sidebar/SidebarChat.tsx index 4b029a66..272a369c 100644 --- a/extensions/void/src/sidebar/SidebarChat.tsx +++ b/extensions/void/src/sidebar/SidebarChat.tsx @@ -8,8 +8,9 @@ import { SelectedFiles } from "./components/SelectedFiles"; import { File, ChatMessage, CodeSelection } from "../shared_types"; import * as vscode from 'vscode' import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi"; -import { useThreads } from "./threadsContext"; -import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage"; +import { useThreads } from "./contextForThreads"; +import { sendLLMMessage } from "../common/sendLLMMessage"; +import { useVoidConfig } from "./contextForConfig"; @@ -70,7 +71,7 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => { } -export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOpen: (v: boolean | ((v: boolean) => boolean)) => void }) => { +export const SidebarChat = () => { // state of current message @@ -85,8 +86,13 @@ export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOp // higher level state const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads() - const [apiConfig, setApiConfig] = useState(null) + const { voidConfig } = useVoidConfig() + // if they pressed the + to add a new chat + useOnVSCodeMessage('startNewThread', (m) => { + if (currentThread?.messages.length !== 0) + startNewThread() + }) // if user pressed ctrl+l, add their selection to the sidebar useOnVSCodeMessage('ctrl+l', (m) => { @@ -98,24 +104,6 @@ export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOp setFiles(files => [...files, filepath]) }) - // when get apiConfig, set - useOnVSCodeMessage('apiConfig', (m) => { - setApiConfig(m.apiConfig) - }) - - // if they pressed the + to add a new chat - useOnVSCodeMessage('startNewThread', (m) => { - setIsThreadSelectorOpen(false) - if (currentThread?.messages.length !== 0) - startNewThread() - - }) - - // if they opened thread selector - useOnVSCodeMessage('toggleThreadSelector', (m) => { - setIsThreadSelectorOpen(v => !v) - }) - const formRef = useRef(null) const onSubmit = async (e: FormEvent) => { @@ -152,7 +140,7 @@ export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOp setMessageStream('') setIsLoading(false) }, - apiConfig: apiConfig + voidConfig: voidConfig }) abortFnRef.current = abort diff --git a/extensions/void/src/sidebar/SidebarSettings.tsx b/extensions/void/src/sidebar/SidebarSettings.tsx new file mode 100644 index 00000000..56118f0b --- /dev/null +++ b/extensions/void/src/sidebar/SidebarSettings.tsx @@ -0,0 +1,16 @@ +import React, { useState } from "react"; +import { useVoidConfig } from "./contextForConfig"; + +export const SidebarSettings = () => { + + const { voidConfig, setConfigParam } = useVoidConfig() + + + + // only show the settings relevant to the current field + + voidConfig.default.whichApi + +} + + diff --git a/extensions/void/src/sidebar/SidebarThreadSelector.tsx b/extensions/void/src/sidebar/SidebarThreadSelector.tsx index 287f0420..59faa41f 100644 --- a/extensions/void/src/sidebar/SidebarThreadSelector.tsx +++ b/extensions/void/src/sidebar/SidebarThreadSelector.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { ThreadsProvider, useThreads } from "./threadsContext"; +import { ThreadsProvider, useThreads } from "./contextForThreads"; export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => { const { allThreads, currentThread, switchToThread } = useThreads() diff --git a/extensions/void/src/sidebar/contextForConfig.tsx b/extensions/void/src/sidebar/contextForConfig.tsx new file mode 100644 index 00000000..708ca286 --- /dev/null +++ b/extensions/void/src/sidebar/contextForConfig.tsx @@ -0,0 +1,354 @@ +import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react" +import { awaitVSCodeResponse, getVSCodeAPI, useOnVSCodeMessage } from "./getVscodeApi" + +const configEnum = (description: string, defaultVal: EnumArr[number], enumArr: EnumArr) => { + return { + description, + defaultVal, + enumArr, + } +} + +const configString = (description: string, defaultVal: string) => { + return { + description, + defaultVal, + enumArr: undefined, + } +} + + + + +const configFields = [ + 'anthropic', + 'openAI', + 'greptile', + 'ollama', + 'openRouter', + 'openAICompatible', + 'azure' +] as const + + + +const voidConfigInfo: Record< + typeof configFields[number] | 'default', { + [prop: string]: { + description: string, + enumArr?: readonly string[] | undefined, + defaultVal: string, + }, + } +> = { + default: { + whichApi: configEnum( + "Choose an API provider.", + 'anthropic', + configFields, + ), + }, + anthropic: { + apikey: configString('Anthropic API key.', ''), + model: configEnum( + "Anthropic model to use.", + 'claude-3-5-sonnet-20240620', + [ + "claude-3-5-sonnet-20240620", + "claude-3-opus-20240229", + "claude-3-sonnet-20240229", + "claude-3-haiku-20240307" + ] as const, + ), + + maxTokens: configEnum( + "Anthropic max number of tokens to output.", + '8192', + [ + "1024", + "2048", + "4096", + "8192" + ] as const, + ), + }, + openAI: { + apikey: configString('OpenAI API key.', ''), + model: configEnum( + 'OpenAI model to use.', + 'gpt-4o', + [ + "o1-preview", + "o1-mini", + "gpt-4o", + "gpt-4o-2024-05-13", + "gpt-4o-2024-08-06", + "gpt-4o-mini", + "gpt-4o-mini-2024-07-18", + "gpt-4-turbo", + "gpt-4-turbo-2024-04-09", + "gpt-4-turbo-preview", + "gpt-4-0125-preview", + "gpt-4-1106-preview", + "gpt-4", + "gpt-4-0613", + "gpt-3.5-turbo-0125", + "gpt-3.5-turbo", + "gpt-3.5-turbo-1106" + ] as const + ), + }, + greptile: { + apikey: configString('Greptile API key.', ''), + githubPAT: configString('Github PAT that Greptile uses to access your repository', ''), + remote: configEnum( + 'Repo location', + 'github', + [ + 'github', + 'gitlab' + ] as const + ), + repository: configString('Repository identifier in "owner/repository" format.', ''), + branch: configString('Name of the branch to use.', 'main'), + }, + ollama: { + endpoint: configString( + 'The Ollama endpoint. Start Ollama by running `OLLAMA_ORIGINS="vscode-webview://*" ollama serve`', + 'http://127.0.0.1:11434' + ), + model: configEnum( + 'Ollama model to use.', + 'llama3.1', + [ + "codegemma", + "codegemma:2b", + "codegemma:7b", + "codellama", + "codellama:7b", + "codellama:13b", + "codellama:34b", + "codellama:70b", + "codellama:code", + "codellama:python", + "command-r", + "command-r:35b", + "command-r-plus", + "command-r-plus:104b", + "deepseek-coder-v2", + "deepseek-coder-v2:16b", + "deepseek-coder-v2:236b", + "falcon2", + "falcon2:11b", + "firefunction-v2", + "firefunction-v2:70b", + "gemma", + "gemma:2b", + "gemma:7b", + "gemma2", + "gemma2:2b", + "gemma2:9b", + "gemma2:27b", + "llama2", + "llama2:7b", + "llama2:13b", + "llama2:70b", + "llama3", + "llama3:8b", + "llama3:70b", + "llama3-chatqa", + "llama3-chatqa:8b", + "llama3-chatqa:70b", + "llama3-gradient", + "llama3-gradient:8b", + "llama3-gradient:70b", + "llama3.1", + "llama3.1:8b", + "llama3.1:70b", + "llama3.1:405b", + "llava", + "llava:7b", + "llava:13b", + "llava:34b", + "llava-llama3", + "llava-llama3:8b", + "llava-phi3", + "llava-phi3:3.8b", + "mistral", + "mistral:7b", + "mistral-large", + "mistral-large:123b", + "mistral-nemo", + "mistral-nemo:12b", + "mixtral", + "mixtral:8x7b", + "mixtral:8x22b", + "moondream", + "moondream:1.8b", + "openhermes", + "openhermes:v2.5", + "phi3", + "phi3:3.8b", + "phi3:14b", + "phi3.5", + "phi3.5:3.8b", + "qwen", + "qwen:7b", + "qwen:14b", + "qwen:32b", + "qwen:72b", + "qwen:110b", + "qwen2", + "qwen2:0.5b", + "qwen2:1.5b", + "qwen2:7b", + "qwen2:72b", + "smollm", + "smollm:135m", + "smollm:360m", + "smollm:1.7b" + ] as const + ), + }, + openRouter: { + model: configString( + 'OpenRouter model to use.', + 'openai/gpt-4o' + ), + apikey: configString('OpenRouter API key.', ''), + }, + openAICompatible: { + endpoint: configString('The endpoint.', 'http://127.0.0.1:11434/v1'), + model: configString('The name of the model to use.', 'gpt-4o'), + apikey: configString('Your API key.', ''), + }, + azure: { + // "void.azure.apiKey": { + // "type": "string", + // "description": "Azure API key." + // }, + // "void.azure.deploymentId": { + // "type": "string", + // "description": "Azure API deployment ID." + // }, + // "void.azure.resourceName": { + // "type": "string", + // "description": "Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`" + // }, + // "void.azure.providerSettings": { + // "type": "object", + // "properties": { + // "baseURL": { + // "type": "string", + // "default": "https://${resourceName}.openai.azure.com/openai/deployments", + // "description": "Azure API base URL." + // }, + // "headers": { + // "type": "object", + // "description": "Custom headers to include in the requests." + // } + // } + // }, + }, +} + + +// this is the type that comes with metadata like desc, default val, etc +type VoidConfigInfo = typeof voidConfigInfo + +// this is the type that specifies the user's actual config +export type PartialVoidConfig = { + [K in keyof typeof voidConfigInfo]?: { + [P in keyof typeof voidConfigInfo[K]]?: string + } +} + +export type VoidConfig = { + [K in keyof typeof voidConfigInfo]: { + [P in keyof typeof voidConfigInfo[K]]: string + } +} + + + +const getVoidConfig = (currentConfig: PartialVoidConfig): VoidConfig => { + const config = {} as PartialVoidConfig + for (let field of configFields) { + config[field] = {} + for (let prop in voidConfigInfo[field]) { + config[field][prop] = currentConfig[field]?.[prop] || voidConfigInfo[field][prop].defaultVal + } + } + return config as VoidConfig +} + +const defaultVoidConfig: VoidConfig = getVoidConfig({}) + +// 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 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 +} + + + +type ConfigValueType = { + voidConfig: VoidConfig, + setConfigParam: (field: K, param: keyof VoidConfigInfo[K], newVal: string) => void +} + + +const ConfigContext = createContext(undefined as unknown as ConfigValueType) + +export function ConfigProvider({ children }: { children: ReactNode }) { + const [partialVoidConfig, setPartialVoidConfig] = useInstantState({}) // only used internally here, and to communicate with the extension + const [voidConfig, setVoidConfig] = useState(defaultVoidConfig) + + + // get the config on mount + useEffect(() => { + getVSCodeAPI().postMessage({ type: 'getPartialVoidConfig' }) + awaitVSCodeResponse('partialVoidConfig').then((m) => { + setPartialVoidConfig(m.partialVoidConfig) + const newFullConfig = getVoidConfig(m.partialVoidConfig) + setVoidConfig(newFullConfig) + }) + }, [setPartialVoidConfig]) + + // return the provider + return ( { + const newPartialConfig: PartialVoidConfig = { + ...partialVoidConfig.current, + [field]: { + ...partialVoidConfig.current?.[field], + [param]: newVal + } + } + setPartialVoidConfig(newPartialConfig) + const newFullConfig = getVoidConfig(newPartialConfig) + setVoidConfig(newFullConfig) + } + }} + > + {children} + + ) +} + +export function useVoidConfig(): ConfigValueType { + const context = useContext(ConfigContext) + if (context === undefined) { + throw new Error("useVoidConfig missing Provider") + } + return context +} + diff --git a/extensions/void/src/sidebar/threadsContext.tsx b/extensions/void/src/sidebar/contextForThreads.tsx similarity index 86% rename from extensions/void/src/sidebar/threadsContext.tsx rename to extensions/void/src/sidebar/contextForThreads.tsx index 5ade37f4..5e9b5fd9 100644 --- a/extensions/void/src/sidebar/threadsContext.tsx +++ b/extensions/void/src/sidebar/contextForThreads.tsx @@ -3,7 +3,8 @@ import { ChatMessage, ChatThreads } from "../shared_types" import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi" -type ThreadsContextValue = { +// a "thread" means a chat message history +type ConfigForThreadsValueType = { readonly allThreads: ChatThreads | null, readonly currentThread: ChatThreads[string] | null; addMessageToHistory: (message: ChatMessage) => void; @@ -11,7 +12,7 @@ type ThreadsContextValue = { startNewThread: () => void; } -const ThreadsContext = createContext(undefined as unknown as ThreadsContextValue) +const ThreadsContext = createContext(undefined as unknown as ConfigForThreadsValueType) const createNewThread = () => ({ id: new Date().getTime().toString(), @@ -39,7 +40,7 @@ export function ThreadsProvider({ children }: { children: ReactNode }) { // this loads allThreads in on mount useEffect(() => { - getVSCodeAPI().postMessage({ type: "getAllThreads" }) + getVSCodeAPI().postMessage({ type: 'getAllThreads' }) awaitVSCodeResponse('allThreads') .then(response => { setAllThreads(response.threads) @@ -90,10 +91,10 @@ export function ThreadsProvider({ children }: { children: ReactNode }) { ) } -export function useThreads(): ThreadsContextValue { - const context = useContext(ThreadsContext) +export function useThreads(): ConfigForThreadsValueType { + const context = useContext(ThreadsContext) if (context === undefined) { - throw new Error("useThreads must be used within a ThreadsProvider") + throw new Error("useThreads missing Provider") } return context } diff --git a/extensions/void/src/sidebar/getVscodeApi.ts b/extensions/void/src/sidebar/getVscodeApi.ts index bdc5b2ed..ffe8a18c 100644 --- a/extensions/void/src/sidebar/getVscodeApi.ts +++ b/extensions/void/src/sidebar/getVscodeApi.ts @@ -9,7 +9,7 @@ type Command = MessageToSidebar['type'] const onetimeCallbacks: { [C in Command]: ((res: any) => void)[] } = { "ctrl+l": [], "files": [], - "apiConfig": [], + "partialVoidConfig": [], "startNewThread": [], "allThreads": [], "toggleThreadSelector": [] @@ -19,7 +19,7 @@ const onetimeCallbacks: { [C in Command]: ((res: any) => void)[] } = { const callbacks: { [C in Command]: { [id: string]: ((res: any) => void) } } = { "ctrl+l": {}, "files": {}, - "apiConfig": {}, + "partialVoidConfig": {}, "startNewThread": {}, "allThreads": {}, "toggleThreadSelector": {} diff --git a/extensions/void/src/sidebar/index.tsx b/extensions/void/src/sidebar/index.tsx index 7b01c5db..ac226c95 100644 --- a/extensions/void/src/sidebar/index.tsx +++ b/extensions/void/src/sidebar/index.tsx @@ -1,7 +1,8 @@ import * as React from "react" import * as ReactDOM from "react-dom/client" import Sidebar from "./Sidebar" -import { ThreadsProvider } from "./threadsContext" +import { ThreadsProvider } from "./contextForThreads" +import { ConfigProvider } from "./contextForConfig" // mount the sidebar on the id="root" element if (typeof document === "undefined") { @@ -13,7 +14,9 @@ console.log("Void root Element:", rootElement) const extension = ( - + + + ) const root = ReactDOM.createRoot(rootElement) From 0072c554fbb922d0b6b896e00dcd4805dcf45a5e Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Oct 2024 14:52:49 -0700 Subject: [PATCH 02/15] tabs --- extensions/void/src/sidebar/Sidebar.tsx | 47 ++++++++++++------- .../void/src/sidebar/SidebarSettings.tsx | 5 +- .../void/src/sidebar/contextForConfig.tsx | 2 - 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index 0ad474fb..684dbe71 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -5,21 +5,25 @@ import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMess import { SidebarThreadSelector } from "./SidebarThreadSelector"; import { useThreads } from "./contextForThreads"; import { SidebarChat } from "./SidebarChat"; +import { SidebarSettings } from './SidebarSettings'; const Sidebar = () => { - const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false) + const [tab, setTab] = useState<'threadSelector' | 'chat' | 'settings'>('chat') - // if they pressed the + to add a new chat - useOnVSCodeMessage('startNewThread', (m) => { - setIsThreadSelectorOpen(false) - }) + // if they pressed the + to add a new chat + useOnVSCodeMessage('startNewThread', (m) => { + setTab('chat') + }) - // if they toggled thread selector - useOnVSCodeMessage('toggleThreadSelector', (m) => { - setIsThreadSelectorOpen(v => !v) - }) + // if they toggled thread selector + useOnVSCodeMessage('toggleThreadSelector', (m) => { + if (tab === 'threadSelector') + setTab('chat') + else + setTab('threadSelector') + }) // Receive messages from the VSCode extension @@ -33,15 +37,24 @@ const Sidebar = () => { }, []) - return <> -
- {isThreadSelectorOpen && ( -
- setIsThreadSelectorOpen(false)} /> -
- )} - + return <> +
+ +
+ setTab('chat')} /> +
+ +
+ +
+ +
+ +
+ + +
diff --git a/extensions/void/src/sidebar/SidebarSettings.tsx b/extensions/void/src/sidebar/SidebarSettings.tsx index 56118f0b..dfb5918e 100644 --- a/extensions/void/src/sidebar/SidebarSettings.tsx +++ b/extensions/void/src/sidebar/SidebarSettings.tsx @@ -9,7 +9,10 @@ export const SidebarSettings = () => { // only show the settings relevant to the current field - voidConfig.default.whichApi + // voidConfig.default.whichApi + + + return null } diff --git a/extensions/void/src/sidebar/contextForConfig.tsx b/extensions/void/src/sidebar/contextForConfig.tsx index 708ca286..f2e86a7f 100644 --- a/extensions/void/src/sidebar/contextForConfig.tsx +++ b/extensions/void/src/sidebar/contextForConfig.tsx @@ -18,8 +18,6 @@ const configString = (description: string, defaultVal: string) => { } - - const configFields = [ 'anthropic', 'openAI', From d3b862ae910dd6a15824f35cac5e18b9acf0d641 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Oct 2024 16:19:32 -0700 Subject: [PATCH 03/15] history display slightly nicer --- extensions/void/src/sidebar/Sidebar.tsx | 6 +- .../src/sidebar/SidebarThreadSelector.tsx | 65 +++++++++++++++---- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index 684dbe71..c24510be 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -39,13 +39,13 @@ const Sidebar = () => { return <> -
+
-
+
setTab('chat')} />
-
+
diff --git a/extensions/void/src/sidebar/SidebarThreadSelector.tsx b/extensions/void/src/sidebar/SidebarThreadSelector.tsx index 59faa41f..323d66e5 100644 --- a/extensions/void/src/sidebar/SidebarThreadSelector.tsx +++ b/extensions/void/src/sidebar/SidebarThreadSelector.tsx @@ -1,10 +1,26 @@ import React from "react"; import { ThreadsProvider, useThreads } from "./contextForThreads"; + +const truncate = (s: string) => { + let len = s.length + const TRUNC_AFTER = 16 + if (len >= TRUNC_AFTER) + s = s.substring(0, TRUNC_AFTER) + '...' + return s +} + + export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => { const { allThreads, currentThread, switchToThread } = useThreads() + + // sorted by most recent to least recent + const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].createdAt > allThreads![threadId2].createdAt ? -1 : 1) + return ( -
+
+ + {/* X button at top right */}
- {/* iterate through all past threads */} - {Object.keys(allThreads ?? {}).map((threadId) => { - const pastThread = (allThreads ?? {})[threadId]; - return ( - - ) - })} + + {/* a list of all the past threads */} +
+ {sortedThreadIds.map((threadId) => { + if (!allThreads) + return <>Error: Threads not found. + const pastThread = allThreads[threadId] + + let btnStringArr = [] + + let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)') + btnStringArr.push(msg1) + + let msg2 = truncate(allThreads[threadId].messages[1]?.displayContent ?? '') + if (msg2) + btnStringArr.push(msg2) + + btnStringArr.push(allThreads[threadId].messages.length) + + const btnString = btnStringArr.join(' / ') + + return ( + + ) + })} +
+
) } \ No newline at end of file From 154a82ab0b0ed3e40f61b83370c379a75d1e176a Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Oct 2024 19:32:21 -0700 Subject: [PATCH 04/15] add settings UI --- extensions/void/package.json | 4 +- extensions/void/src/DisplayChangesProvider.ts | 2 +- extensions/void/src/extension.ts | 7 +- extensions/void/src/shared_types.ts | 1 + extensions/void/src/sidebar/Sidebar.tsx | 8 ++ .../void/src/sidebar/SidebarSettings.tsx | 73 ++++++++++++++++--- .../void/src/sidebar/contextForConfig.tsx | 21 ++++-- extensions/void/src/sidebar/getVscodeApi.ts | 6 +- 8 files changed, 95 insertions(+), 27 deletions(-) diff --git a/extensions/void/package.json b/extensions/void/package.json index e1d147c1..ea73322c 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -47,7 +47,7 @@ "icon": "$(history)" }, { - "command": "void.openSettings", + "command": "void.toggleSettings", "title": "Void settings", "icon": "$(settings-gear)" } @@ -95,7 +95,7 @@ "group": "navigation" }, { - "command": "void.openSettings", + "command": "void.toggleSettings", "when": "view == 'void.viewnumberone'", "group": "navigation" } diff --git a/extensions/void/src/DisplayChangesProvider.ts b/extensions/void/src/DisplayChangesProvider.ts index 4166d5b4..9394b410 100644 --- a/extensions/void/src/DisplayChangesProvider.ts +++ b/extensions/void/src/DisplayChangesProvider.ts @@ -28,7 +28,7 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { // used internally by vscode public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult { const docUriStr = document.uri.toString() - return this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) + return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? [] } // declared by us, registered with vscode.languages.registerCodeLensProvider() diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index f42074b8..de2c699a 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -54,10 +54,6 @@ export function activate(context: vscode.ExtensionContext) { displayChangesProvider.rejectDiff(params) })); - context.subscriptions.push(vscode.commands.registerCommand('void.openSettings', async () => { - vscode.commands.executeCommand('workbench.action.openSettings', '@ext:void.void'); - })); - // 5. Receive messages from sidebar webviewProvider.webview.then( webview => { @@ -69,6 +65,9 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => { webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar) })) + context.subscriptions.push(vscode.commands.registerCommand('void.toggleSettings', async () => { + webview.postMessage({ type: 'toggleSettings' } satisfies MessageToSidebar) + })); // Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`) webview.onDidReceiveMessage(async (m: MessageFromSidebar) => { diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index eed9d07b..a1e3122e 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/shared_types.ts @@ -46,6 +46,7 @@ type MessageToSidebar = ( | { type: 'allThreads', threads: ChatThreads } | { type: 'startNewThread' } | { type: 'toggleThreadSelector' } + | { type: 'toggleSettings' } ) // sidebar -> editor diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index c24510be..36029f01 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -25,6 +25,14 @@ const Sidebar = () => { setTab('threadSelector') }) + // if they toggled settings + useOnVSCodeMessage('toggleSettings', (m) => { + if (tab === 'settings') + setTab('chat') + else + setTab('settings') + }) + // Receive messages from the VSCode extension useEffect(() => { diff --git a/extensions/void/src/sidebar/SidebarSettings.tsx b/extensions/void/src/sidebar/SidebarSettings.tsx index dfb5918e..700ed1da 100644 --- a/extensions/void/src/sidebar/SidebarSettings.tsx +++ b/extensions/void/src/sidebar/SidebarSettings.tsx @@ -1,19 +1,70 @@ import React, { useState } from "react"; -import { useVoidConfig } from "./contextForConfig"; +import { configFields, useVoidConfig, VoidConfigField } from "./contextForConfig"; + + +const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => { + const { voidConfig, partialVoidConfig, voidConfigInfo, setConfigParam } = useVoidConfig() + + const [val, setVal] = useState(partialVoidConfig[field]?.[param]) + + const { enumArr } = voidConfigInfo[field][param] + + const updateState = (e: React.ChangeEvent) => { setVal(e.target.value); }; + const commitConfigParam = () => { if (val) setConfigParam(field, param, val); }; + + + // string + if (enumArr === undefined) { + return ( +
+ + +
+ ) + } + // enum + else { + return ( +
+ + +
+ ) + } +} export const SidebarSettings = () => { - const { voidConfig, setConfigParam } = useVoidConfig() + const { voidConfig, voidConfigInfo } = useVoidConfig() + const current_field = voidConfig.default['whichApi'] as VoidConfigField + const params = Object.keys(voidConfigInfo[current_field]) - - // only show the settings relevant to the current field - - // voidConfig.default.whichApi - - - return null - + return ( +
+ {params.map((param) => ( + + ))} +
+ ) } - diff --git a/extensions/void/src/sidebar/contextForConfig.tsx b/extensions/void/src/sidebar/contextForConfig.tsx index f2e86a7f..14b1cce7 100644 --- a/extensions/void/src/sidebar/contextForConfig.tsx +++ b/extensions/void/src/sidebar/contextForConfig.tsx @@ -17,8 +17,8 @@ const configString = (description: string, defaultVal: string) => { } } - -const configFields = [ +// fields you can customize (don't forget 'default' - it isn't included here!) +export const configFields = [ 'anthropic', 'openAI', 'greptile', @@ -253,17 +253,18 @@ const voidConfigInfo: Record< // this is the type that comes with metadata like desc, default val, etc type VoidConfigInfo = typeof voidConfigInfo +export type VoidConfigField = keyof typeof voidConfigInfo // typeof configFields[number] // this is the type that specifies the user's actual config export type PartialVoidConfig = { [K in keyof typeof voidConfigInfo]?: { - [P in keyof typeof voidConfigInfo[K]]?: string + [P in keyof typeof voidConfigInfo[K]]?: typeof voidConfigInfo[K][P]['defaultVal'] } } export type VoidConfig = { [K in keyof typeof voidConfigInfo]: { - [P in keyof typeof voidConfigInfo[K]]: string + [P in keyof typeof voidConfigInfo[K]]: typeof voidConfigInfo[K][P]['defaultVal'] } } @@ -271,7 +272,7 @@ export type VoidConfig = { const getVoidConfig = (currentConfig: PartialVoidConfig): VoidConfig => { const config = {} as PartialVoidConfig - for (let field of configFields) { + for (let field of [...configFields, 'default'] as const) { config[field] = {} for (let prop in voidConfigInfo[field]) { config[field][prop] = currentConfig[field]?.[prop] || voidConfigInfo[field][prop].defaultVal @@ -296,16 +297,20 @@ const useInstantState = (initVal: T) => { +type SetConfigParamType = (field: K, param: keyof VoidConfigInfo[K], newVal: string) => void + type ConfigValueType = { voidConfig: VoidConfig, - setConfigParam: (field: K, param: keyof VoidConfigInfo[K], newVal: string) => void + voidConfigInfo: VoidConfigInfo, + partialVoidConfig: PartialVoidConfig, + setConfigParam: SetConfigParamType } const ConfigContext = createContext(undefined as unknown as ConfigValueType) export function ConfigProvider({ children }: { children: ReactNode }) { - const [partialVoidConfig, setPartialVoidConfig] = useInstantState({}) // only used internally here, and to communicate with the extension + const [partialVoidConfig, setPartialVoidConfig] = useInstantState({}) // the user's selections const [voidConfig, setVoidConfig] = useState(defaultVoidConfig) @@ -323,6 +328,8 @@ export function ConfigProvider({ children }: { children: ReactNode }) { return ( { const newPartialConfig: PartialVoidConfig = { ...partialVoidConfig.current, diff --git a/extensions/void/src/sidebar/getVscodeApi.ts b/extensions/void/src/sidebar/getVscodeApi.ts index ffe8a18c..80f29ff0 100644 --- a/extensions/void/src/sidebar/getVscodeApi.ts +++ b/extensions/void/src/sidebar/getVscodeApi.ts @@ -12,7 +12,8 @@ const onetimeCallbacks: { [C in Command]: ((res: any) => void)[] } = { "partialVoidConfig": [], "startNewThread": [], "allThreads": [], - "toggleThreadSelector": [] + "toggleThreadSelector": [], + "toggleSettings": [], } // messageType -> id -> res @@ -22,7 +23,8 @@ const callbacks: { [C in Command]: { [id: string]: ((res: any) => void) } } = { "partialVoidConfig": {}, "startNewThread": {}, "allThreads": {}, - "toggleThreadSelector": {} + "toggleThreadSelector": {}, + "toggleSettings": {}, } From 63c6b6bec52846340c3c7d2fbb107c08da186d44 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Oct 2024 20:00:07 -0700 Subject: [PATCH 05/15] vscode styles --- extensions/void/src/sidebar/SidebarSettings.tsx | 2 ++ extensions/void/src/sidebar/styles.css | 4 ++++ extensions/void/tailwind.config.js | 6 ++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/extensions/void/src/sidebar/SidebarSettings.tsx b/extensions/void/src/sidebar/SidebarSettings.tsx index 700ed1da..a8dc040d 100644 --- a/extensions/void/src/sidebar/SidebarSettings.tsx +++ b/extensions/void/src/sidebar/SidebarSettings.tsx @@ -19,6 +19,7 @@ const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, para
-
- ) - } - // enum - else { - return ( -
- - -
- ) - } + const resetButton = + + const inputElement = enumArr === undefined ? + // string + ( updateState(e.target.value)} + />) + : + // enum + () + + return
+ + {description} +
+ {inputElement} + {resetButton} +
+
} export const SidebarSettings = () => { @@ -55,17 +61,38 @@ export const SidebarSettings = () => { const { voidConfig, voidConfigInfo } = useVoidConfig() const current_field = voidConfig.default['whichApi'] as VoidConfigField - const params = Object.keys(voidConfigInfo[current_field]) + return ( -
- {params.map((param) => ( +
+ + {/* choose the field */} +
- ))} +
+ +
+ + {/* render all fields, but hide the ones not visible for fast tab switching */} + {configFields.map(field => { + return
+ {Object.keys(voidConfigInfo[field]).map((param) => ( + + ))} +
+ })} + +
) } diff --git a/extensions/void/src/sidebar/contextForConfig.tsx b/extensions/void/src/sidebar/contextForConfig.tsx index 14b1cce7..30c2219d 100644 --- a/extensions/void/src/sidebar/contextForConfig.tsx +++ b/extensions/void/src/sidebar/contextForConfig.tsx @@ -41,7 +41,7 @@ const voidConfigInfo: Record< > = { default: { whichApi: configEnum( - "Choose an API provider.", + "API Provider.", 'anthropic', configFields, ), From fcd0eb30c60c20ff4a7f282428270dd920b76658 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Oct 2024 23:06:34 -0700 Subject: [PATCH 09/15] fix --- extensions/void/src/sidebar/SidebarSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/void/src/sidebar/SidebarSettings.tsx b/extensions/void/src/sidebar/SidebarSettings.tsx index 47d657f0..cb764733 100644 --- a/extensions/void/src/sidebar/SidebarSettings.tsx +++ b/extensions/void/src/sidebar/SidebarSettings.tsx @@ -6,7 +6,7 @@ const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, para const { voidConfig, partialVoidConfig, voidConfigInfo, setConfigParam } = useVoidConfig() const { defaultVal, description } = voidConfigInfo[field][param] - const [val, setVal] = useState(partialVoidConfig[field]?.[param]) + const [val, setVal] = useState(partialVoidConfig[field]?.[param] ?? defaultVal) const { enumArr } = voidConfigInfo[field][param] From 84ff9784ca22147cde1bff6711c921d38c76d73f Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Oct 2024 01:20:59 -0700 Subject: [PATCH 10/15] fix sidebar settings --- extensions/void/src/SidebarWebviewProvider.ts | 33 ++++--------------- extensions/void/src/extension.ts | 2 +- .../void/src/sidebar/SidebarSettings.tsx | 14 ++------ .../void/src/sidebar/contextForConfig.tsx | 1 + 4 files changed, 11 insertions(+), 39 deletions(-) diff --git a/extensions/void/src/SidebarWebviewProvider.ts b/extensions/void/src/SidebarWebviewProvider.ts index a6cdf85c..c8aaffba 100644 --- a/extensions/void/src/SidebarWebviewProvider.ts +++ b/extensions/void/src/SidebarWebviewProvider.ts @@ -19,7 +19,7 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { private readonly _extensionUri: vscode.Uri - private _webviewView?: vscode.WebviewView; // only used inside onDidChangeConfiguration + // private _webviewView?: vscode.WebviewView; private _webviewDeps: string[] = []; constructor(context: vscode.ExtensionContext) { @@ -28,35 +28,14 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { let temp_res: typeof this._res | undefined = undefined this.webview = new Promise((res, rej) => { temp_res = res }) - if (!temp_res) throw new Error("sidebar provider: resolver was undefined") + if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined") this._res = temp_res - - // if it affects one of the config items webview depends on, update the webview - // TODO should be able to move this entirely to React - make updateWebviewHTML mount once, and then send updates via postMessage from then on - vscode.workspace.onDidChangeConfiguration(event => { - if (this._webviewDeps.map(dep => event.affectsConfiguration(dep)).some(v => !!v)) { - if (this._webviewView) { - this.updateWebviewHTML(this._webviewView.webview); - } - } - }); } - // this is updated - private updateWebviewHTML(webview: vscode.Webview) { - const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com']; + // called by us + updateWebviewHTML(webview: vscode.Webview) { this._webviewDeps = [] - const ollamaEndpoint: string | undefined = vscode.workspace.getConfiguration('void.ollama').get('endpoint'); - this._webviewDeps.push('void.ollama.endpoint'); - if (ollamaEndpoint) - allowed_urls.push(ollamaEndpoint); - - const openAICompatibleEndpoint: string | undefined = vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint'); - this._webviewDeps.push('void.openAICompatible.endpoint'); - if (openAICompatibleEndpoint) - allowed_urls.push(openAICompatibleEndpoint+'/chat/completions'); - 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)); @@ -68,7 +47,7 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { Custom View - + @@ -100,6 +79,6 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { // resolve webview and _webviewView this._res(webview); - this._webviewView = webviewView; + // this._webviewView = webviewView; } } diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index de2c699a..a2724435 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -121,7 +121,7 @@ export function activate(context: vscode.ExtensionContext) { } else if (m.type === 'persistPartialVoidConfig') { const partialVoidConfig = m.partialVoidConfig - context.workspaceState.update('partialVoidConfig', partialVoidConfig) + context.globalState.update('partialVoidConfig', partialVoidConfig) } else if (m.type === 'getAllThreads') { const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {} diff --git a/extensions/void/src/sidebar/SidebarSettings.tsx b/extensions/void/src/sidebar/SidebarSettings.tsx index cb764733..a58af8ad 100644 --- a/extensions/void/src/sidebar/SidebarSettings.tsx +++ b/extensions/void/src/sidebar/SidebarSettings.tsx @@ -4,18 +4,10 @@ import { configFields, useVoidConfig, VoidConfigField } from "./contextForConfig const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => { const { voidConfig, partialVoidConfig, voidConfigInfo, setConfigParam } = useVoidConfig() + const { enumArr, defaultVal, description } = voidConfigInfo[field][param] + const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item - const { defaultVal, description } = voidConfigInfo[field][param] - const [val, setVal] = useState(partialVoidConfig[field]?.[param] ?? defaultVal) - - const { enumArr } = voidConfigInfo[field][param] - - const updateState = (newValue: string | undefined) => { - setVal(newValue) - if (newValue) - setConfigParam(field, param, newValue) - } - + const updateState = (newValue: string) => { setConfigParam(field, param, newValue) } const resetButton =
})} - + {/* Remove this after 10/21/24, this is just to give developers a heads up about the recent change */} +
+ {`We recently updated Settings. To copy your old Void settings over, press Ctrl+Shift+P, `} + {`type 'Open User Settings (JSON)',`} + {` and look for 'void.'. `} +
) } From 983e926a2376153ff51b99c62d164ae1610bfe0c Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Oct 2024 16:36:37 -0700 Subject: [PATCH 12/15] add error handling from #87 --- extensions/void/src/common/sendLLMMessage.ts | 99 +++++++++++--------- 1 file changed, 57 insertions(+), 42 deletions(-) diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index 764647b9..a0ab0df3 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -17,6 +17,7 @@ type SendLLMMessageFnTypeInternal = (params: { messages: LLMMessage[], onText: OnText, onFinalMessage: (input: string) => void, + onError: (error: string) => void, voidConfig: VoidConfig, }) => { @@ -27,6 +28,7 @@ type SendLLMMessageFnTypeExternal = (params: { messages: LLMMessage[], onText: OnText, onFinalMessage: (input: string) => void, + onError: (error: string) => void, voidConfig: VoidConfig | null, }) => { @@ -36,8 +38,8 @@ type SendLLMMessageFnTypeExternal = (params: { -// Claude -const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, voidConfig }) => { +// Anthropic +const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] @@ -63,10 +65,19 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal onFinalMessage(content) }) + stream.on('error', (error) => { + // the most common error will be invalid API key (401), so we handle this with a nice message + if (error instanceof Anthropic.APIError && error.status === 401) { + onError('Invalid API key.') + } + else { + onError(error.message) + } + }) // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either const abort = () => { - // stream.abort() // this doesnt appear to do anything, but it should try to stop claude from generating anymore + // stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error did_abort = true } @@ -78,7 +89,7 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal // OpenAI, OpenRouter, OpenAICompatible -const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, voidConfig }) => { +const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { let didAbort = false let fullText = '' @@ -122,27 +133,35 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal didAbort = true; } // when receive text - try { - for await (const chunk of response) { - if (didAbort) return; - const newText = chunk.choices[0]?.delta?.content || ''; - fullText += newText; - onText(newText, fullText); - } - onFinalMessage(fullText); + for await (const chunk of response) { + if (didAbort) return; + const newText = chunk.choices[0]?.delta?.content || ''; + fullText += newText; + onText(newText, fullText); } - // when error/fail - catch (error) { - console.error('Error in OpenAI stream:', error); - onFinalMessage(fullText); + onFinalMessage(fullText); + }) + // when error/fail - this catches errors of both .create() and .then(for await) + .catch(error => { + if (error instanceof OpenAI.APIError) { + if (error.status === 401) { + onError('Invalid API key.'); + } + else { + onError(error.message); + } + } + else { + onError(error); } }) + return { abort }; }; // Ollama -export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, voidConfig }) => { +export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { let didAbort = false let fullText = "" @@ -165,21 +184,20 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, didAbort = true } // iterate through the stream - try { - for await (const chunk of stream) { - if (didAbort) return; - const newText = chunk.message.content; - fullText += newText; - onText(newText, fullText); - } - onFinalMessage(fullText); - } - // when error/fail - catch (error) { - console.error('Error:', error); - onFinalMessage(fullText); + for await (const chunk of stream) { + if (didAbort) return; + const newText = chunk.message.content; + fullText += newText; + onText(newText, fullText); } + onFinalMessage(fullText); + }) + // when error/fail + .catch(error => { + onError(error) + }) + return { abort }; }; @@ -189,7 +207,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, // https://docs.greptile.com/api-reference/query // https://docs.greptile.com/quickstart#sample-response-streamed -const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, voidConfig }) => { +const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { let didAbort = false let fullText = '' @@ -247,9 +265,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin }) .catch(e => { - console.error('Error in Greptile stream:', e); - onFinalMessage(fullText); - + onError(e) }); return { abort } @@ -258,23 +274,22 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin -export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, voidConfig }) => { +export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { if (!voidConfig) return { abort: () => { } } switch (voidConfig.default.whichApi) { case 'anthropic': - return sendClaudeMsg({ messages, onText, onFinalMessage, voidConfig }); + return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig }); case 'openAI': case 'openRouter': case 'openAICompatible': - return sendOpenAIMsg({ messages, onText, onFinalMessage, voidConfig }); - case 'greptile': - return sendGreptileMsg({ messages, onText, onFinalMessage, voidConfig }); + return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig }); case 'ollama': - return sendOllamaMsg({ messages, onText, onFinalMessage, voidConfig }); + return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig }); + case 'greptile': + return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig }); default: - console.error(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`); + onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) return { abort: () => { } } - //return sendClaudeMsg({ messages, onText, onFinalMessage, voidConfig }); // TODO } } From 0f3a79ab540855aa4c702ae366f777343fec600f Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Oct 2024 16:43:25 -0700 Subject: [PATCH 13/15] error UI --- extensions/void/src/common/sendLLMMessage.ts | 4 +-- extensions/void/src/sidebar/SidebarChat.tsx | 27 +++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index a0ab0df3..b5005de0 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -68,7 +68,7 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi stream.on('error', (error) => { // the most common error will be invalid API key (401), so we handle this with a nice message if (error instanceof Anthropic.APIError && error.status === 401) { - onError('Invalid API key.') + onError('Unauthorized: Invalid API key.') } else { onError(error.message) @@ -145,7 +145,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal .catch(error => { if (error instanceof OpenAI.APIError) { if (error.status === 401) { - onError('Invalid API key.'); + onError('Unauthorized: Invalid API key.'); } else { onError(error.message); diff --git a/extensions/void/src/sidebar/SidebarChat.tsx b/extensions/void/src/sidebar/SidebarChat.tsx index 554e3bd2..87c361d4 100644 --- a/extensions/void/src/sidebar/SidebarChat.tsx +++ b/extensions/void/src/sidebar/SidebarChat.tsx @@ -50,7 +50,7 @@ Please edit the file following these instructions: Please edit the selected code following these instructions: `; } - + str += ` \t${instructions} `; @@ -106,6 +106,8 @@ export const SidebarChat = () => { const [isLoading, setIsLoading] = useState(false) const abortFnRef = useRef<(() => void) | null>(null) + const [latestError, setLatestError] = useState('') + // higher level state const { allThreads, currentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads() const { voidConfig } = useVoidConfig() @@ -142,33 +144,42 @@ export const SidebarChat = () => { setIsLoading(true) setInstructions(''); - formRef.current?.reset(); // reset the form's text + formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens setSelection(null) setFiles([]) + setLatestError('') // request file content from vscode and await response getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files }) const relevantFiles = await awaitVSCodeResponse('files') // add message to chat history - const content = userInstructionsStr(instructions, relevantFiles.files, selection) + const userContent = userInstructionsStr(instructions, relevantFiles.files, selection) // console.log('prompt:\n', content) - const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files } + const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files } addMessageToHistory(newHistoryElt) // send message to LLM let { abort } = sendLLMMessage({ - messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content }], + messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content: userContent }], onText: (newText, fullText) => setMessageStream(fullText), onFinalMessage: (content) => { // add assistant's message to chat history, and clear selection const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, } addMessageToHistory(newHistoryElt) - - // clear selection setMessageStream('') setIsLoading(false) }, + onError: (error) => { + // add assistant's message to chat history, and clear selection + let content = messageStream; // just use the current content + const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, } + addMessageToHistory(newHistoryElt) + setMessageStream('') + setIsLoading(false) + + setLatestError(error) + }, voidConfig: voidConfig }) abortFnRef.current = abort @@ -268,6 +279,8 @@ export const SidebarChat = () => {
+ + {latestError}
} From 5415f1e3cbc2e53a77baa72f6ffae110d42d5c33 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Oct 2024 16:46:52 -0700 Subject: [PATCH 14/15] amend --- extensions/void/src/common/sendLLMMessage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index b5005de0..a0ab0df3 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -68,7 +68,7 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi stream.on('error', (error) => { // the most common error will be invalid API key (401), so we handle this with a nice message if (error instanceof Anthropic.APIError && error.status === 401) { - onError('Unauthorized: Invalid API key.') + onError('Invalid API key.') } else { onError(error.message) @@ -145,7 +145,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal .catch(error => { if (error instanceof OpenAI.APIError) { if (error.status === 401) { - onError('Unauthorized: Invalid API key.'); + onError('Invalid API key.'); } else { onError(error.message); From 18649d32557e2bd3da814af52a66dc9e610e9ecb Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 17 Oct 2024 17:29:28 -0700 Subject: [PATCH 15/15] fix package --- extensions/void/package-lock.json | 348 ++++++++++++++++++++++++++++++ extensions/void/package.json | 3 + 2 files changed, 351 insertions(+) diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index d6a323be..73961d97 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -26,16 +26,19 @@ "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "2.4.1", "autoprefixer": "^10.4.20", + "diff-match-patch": "^1.0.5", "esbuild": "^0.23.1", "eslint": "^8.57.0", "eslint-plugin-react": "^7.35.1", "eslint-plugin-react-hooks": "^4.6.2", "globals": "^15.9.0", "marked": "^14.1.0", + "ollama": "^0.5.9", "postcss": "^8.4.41", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.6.1", "rimraf": "^6.0.1", "tailwindcss": "^3.4.10", "typescript": "5.5.4", @@ -197,6 +200,19 @@ "node": ">=4" } }, + "node_modules/@babel/runtime": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -2151,6 +2167,13 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -2843,6 +2866,20 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dev": true, + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3028,6 +3065,15 @@ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", "license": "MIT" }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/formdata-node": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", @@ -3328,6 +3374,17 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", @@ -3370,6 +3427,77 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/hastscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -3380,6 +3508,23 @@ "he": "bin/he" } }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4406,6 +4551,21 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5557,6 +5717,16 @@ "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, + "license": "MIT", + "dependencies": { + "whatwg-fetch": "^3.6.20" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6173,6 +6343,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6311,6 +6491,24 @@ "react": ">=18" } }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -6372,6 +6570,139 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -7769,6 +8100,13 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "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, + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -8010,6 +8348,16 @@ "dev": true, "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/extensions/void/package.json b/extensions/void/package.json index 7daf81b1..de2c5312 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -122,16 +122,19 @@ "@vscode/test-cli": "^0.0.10", "@vscode/test-electron": "2.4.1", "autoprefixer": "^10.4.20", + "diff-match-patch": "^1.0.5", "esbuild": "^0.23.1", "eslint": "^8.57.0", "eslint-plugin-react": "^7.35.1", "eslint-plugin-react-hooks": "^4.6.2", "globals": "^15.9.0", "marked": "^14.1.0", + "ollama": "^0.5.9", "postcss": "^8.4.41", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.6.1", "rimraf": "^6.0.1", "tailwindcss": "^3.4.10", "typescript": "5.5.4",