From b30937934a03576ead71ac0e08d253737be0f275 Mon Sep 17 00:00:00 2001 From: Andrew Date: Wed, 16 Oct 2024 01:12:57 -0700 Subject: [PATCH] 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)