progress moving settings to be extension-native

This commit is contained in:
Andrew 2024-10-16 01:12:57 -07:00
parent cd9bca0451
commit b30937934a
12 changed files with 456 additions and 414 deletions

View file

@ -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": [

View file

@ -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
}
}

View file

@ -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') ?? {}

View file

@ -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] }
)

View file

@ -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 = () => {
</div>
)}
<SidebarChat setIsThreadSelectorOpen={setIsThreadSelectorOpen} />
<SidebarChat />
</div>
</>

View file

@ -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<ApiConfig | null>(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<HTMLFormElement | null>(null)
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
@ -152,7 +140,7 @@ export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOp
setMessageStream('')
setIsLoading(false)
},
apiConfig: apiConfig
voidConfig: voidConfig
})
abortFnRef.current = abort

View file

@ -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
}

View file

@ -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()

View file

@ -0,0 +1,354 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
import { awaitVSCodeResponse, getVSCodeAPI, useOnVSCodeMessage } from "./getVscodeApi"
const configEnum = <EnumArr extends readonly string[]>(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 = <T,>(initVal: T) => {
const stateRef = useRef<T>(initVal)
const [_, setS] = useState<T>(initVal)
const setState = useCallback((newVal: T) => {
setS(newVal);
stateRef.current = newVal;
}, [])
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
}
type ConfigValueType = {
voidConfig: VoidConfig,
setConfigParam: <K extends typeof configFields[number]>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => void
}
const ConfigContext = createContext<ConfigValueType>(undefined as unknown as ConfigValueType)
export function ConfigProvider({ children }: { children: ReactNode }) {
const [partialVoidConfig, setPartialVoidConfig] = useInstantState<PartialVoidConfig>({}) // only used internally here, and to communicate with the extension
const [voidConfig, setVoidConfig] = useState<VoidConfig>(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 (<ConfigContext.Provider
value={{
voidConfig,
setConfigParam: (field, param, newVal) => {
const newPartialConfig: PartialVoidConfig = {
...partialVoidConfig.current,
[field]: {
...partialVoidConfig.current?.[field],
[param]: newVal
}
}
setPartialVoidConfig(newPartialConfig)
const newFullConfig = getVoidConfig(newPartialConfig)
setVoidConfig(newFullConfig)
}
}}
>
{children}
</ConfigContext.Provider>
)
}
export function useVoidConfig(): ConfigValueType {
const context = useContext<ConfigValueType>(ConfigContext)
if (context === undefined) {
throw new Error("useVoidConfig missing Provider")
}
return context
}

View file

@ -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<ThreadsContextValue>(undefined as unknown as ThreadsContextValue)
const ThreadsContext = createContext<ConfigForThreadsValueType>(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<ThreadsContextValue>(ThreadsContext)
export function useThreads(): ConfigForThreadsValueType {
const context = useContext<ConfigForThreadsValueType>(ThreadsContext)
if (context === undefined) {
throw new Error("useThreads must be used within a ThreadsProvider")
throw new Error("useThreads missing Provider")
}
return context
}

View file

@ -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": {}

View file

@ -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 = (
<ThreadsProvider>
<Sidebar />
<ConfigProvider>
<Sidebar />
</ConfigProvider>
</ThreadsProvider>
)
const root = ReactDOM.createRoot(rootElement)