diff --git a/extensions/void/package.json b/extensions/void/package.json index f0223115..4d6d54b0 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -15,35 +15,297 @@ "configuration": { "title": "API Keys", "properties": { - "void.whichApi": { + "void.provider": { "type": "string", "default": "anthropic", - "description": "Choose a model to use (anthropic | openai | greptile | ollama)" + "description": "Choose a provider (openai | anthropic | azure | greptile | ollama)", + "enum": [ + "openai", + "anthropic", + "azure", + "greptile", + "ollama" + ] }, - "void.anthropicApiKey": { - "type": "string", - "default": "", - "description": "Anthropic API Key" + "void.anthropic": { + "type": "object", + "properties": { + "model": { + "type": "string", + "default": "claude-3-5-sonnet-20240620", + "description": "Choose a model ('claude-3-5-sonnet-20240620' | 'claude-3-opus-20240229' | 'claude-3-sonnet-20240229' | 'claude-3-haiku-20240307')", + "enum": [ + "claude-3-5-sonnet-20240620", + "claude-3-opus-20240229", + "claude-3-sonnet-20240229", + "claude-3-haiku-20240307" + ] + }, + "providerSettings": { + "type": "object", + "properties": { + "baseURL": { + "type": "string", + "description": "Use a different URL prefix for API calls, e.g. to use proxy servers." + }, + "apiKey": { + "type": "string", + "description": "Anthropic API key." + }, + "headers": { + "type": "object", + "description": "Custom headers to include in the requests." + } + } + } + } }, - "void.openAIApiKey": { - "type": "string", - "default": "", - "description": "OpenAI API Key" + "void.openai": { + "type": "object", + "properties": { + "model": { + "type": "string", + "default": "gpt-4o", + "description": "Choose a model ('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')", + "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" + ] + }, + "providerSettings": { + "type": "object", + "properties": { + "baseURL": { + "type": "string", + "description": "Use a different URL prefix for API calls, e.g. to use proxy servers." + }, + "apiKey": { + "type": "string", + "description": "OpenAI API key." + }, + "headers": { + "type": "object", + "description": "Custom headers to include in the requests." + }, + "organization": { + "type": "string", + "description": "OpenAI Organization." + }, + "project": { + "type": "string", + "description": "OpenAI project." + }, + "compatibility": { + "type": "string", + "description": "OpenAI compatibility mode. Should be set to `strict` when using the OpenAI API, and `compatible` when using 3rd party providers. In `compatible` mode, newer information such as streamOptions are not being sent. Defaults to 'compatible'.", + "enum": [ + "strict", + "compatible" + ] + } + } + } + } }, - "void.greptileApiKey": { - "type": "string", - "default": "", - "description": "Greptile API Key" + "void.azure": { + "type": "object", + "properties": { + "deploymentId": { + "type": "string", + "description": "Azure API deployment ID." + }, + "providerSettings": { + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "description": "Azure API key." + }, + "baseURL": { + "type": "string", + "description": "Azure API base URL." + }, + "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}`" + }, + "headers": { + "type": "object", + "description": "Custom headers to include in the requests." + } + } + } + } }, - "void.githubPAT": { - "type": "string", - "default": "", - "description": "Greptile - Github PAT (gives Greptile access to your repo)" + "void.ollama": { + "type": "object", + "properties": { + "model": { + "type": "string", + "description": "Ollama model. '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'", + "default": "llama3.1", + "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" + ] + }, + "providerSettings": { + "type": "object", + "properties": { + "baseURL": { + "type": "string", + "description": "Ollama base URL. Local API server can be started with `OLLAMA_ORIGINS=* ollama serve`" + }, + "headers": { + "type": "object", + "description": "Custom headers to include in the requests." + } + } + } + } }, - "void.ollamaSettings": { - "type": "string", - "default": "", - "description": "Ollama settings (coming soon...)" + "void.greptile": { + "type": "object", + "properties": { + "providerSettings": { + "type": "object", + "properties": { + "apikey": { + "type": "string", + "description": "Greptile API key." + }, + "headers": { + "type": "object", + "description": "Custom headers to include in the requests.", + "properties": { + "X-Github-Token": { + "type": "string", + "description": "GitHub PAT." + } + } + }, + "repoinfo": { + "type": "array", + "items": { + "type": "object", + "properties": { + "remote": { + "type": "string", + "description": "GitHub remote." + }, + "repository": { + "type": "string", + "description": "GitHub repository." + }, + "branch": { + "type": "string", + "description": "GitHub branch." + } + } + } + } + } + } + } } } }, @@ -145,4 +407,4 @@ "ollama-ai-provider": "^0.15.0", "openai": "^4.57.0" } -} +} \ No newline at end of file diff --git a/extensions/void/src/SidebarWebviewProvider.ts b/extensions/void/src/SidebarWebviewProvider.ts index 7fd8c83e..c95e1e12 100644 --- a/extensions/void/src/SidebarWebviewProvider.ts +++ b/extensions/void/src/SidebarWebviewProvider.ts @@ -1,5 +1,6 @@ // renders the code from `src/sidebar` +import { AzureOpenAIProviderSettings } from '@ai-sdk/azure'; import * as vscode from 'vscode'; function getNonce() { @@ -55,9 +56,20 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { const nonce = getNonce(); // only scripts with the nonce are allowed to run, this is a recommended security measure - // Allow Ollama endpoint - const ollamaEndpoint = vscode.workspace.getConfiguration('void').get('ollamaSettings.endpoint') || 'http://localhost:11434' - const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com', ollamaEndpoint ] + const ollamaBaseURL = new URL('/', vscode.workspace.getConfiguration('void').get('ollama.providerSettings.baseURL') || 'http://127.0.0.1:11434').toString() + const azureProviderSettings = vscode.workspace.getConfiguration('void').get('azure.providerSettings') as AzureOpenAIProviderSettings + const azureBaseURL = new URL('/', vscode.workspace.getConfiguration('void').get('azure.providerSettings.baseURL') || `https://${azureProviderSettings.resourceName}.openai.azure.com/openai/deployments`).toString() + const openaiBaseURL = new URL('/', vscode.workspace.getConfiguration('void').get('openai.providerSettings.baseURL') || 'https://api.openai.com').toString() + const greptileBaseURL = new URL('/', vscode.workspace.getConfiguration('void').get('greptile.providerSettings.baseURL') || 'https://api.mistral.ai').toString() + const anthropicBaseURL = new URL('/', vscode.workspace.getConfiguration('void').get('anthropic.providerSettings.baseURL') || 'https://api.mistral.ai').toString() + const allowed_urls = [ + ollamaBaseURL, + azureBaseURL, + openaiBaseURL, + greptileBaseURL, + anthropicBaseURL + ] + webview.html = ` diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index 758c4f8e..256f2ca2 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -5,66 +5,67 @@ import { AzureOpenAIProviderSettings, createAzure } from '@ai-sdk/azure'; import { createOllama, OllamaProviderSettings } from 'ollama-ai-provider'; export type ApiConfig = { - anthropic: { - apiKey: string, - /** @default 'claude-3-5-sonnet-20240620' */ - model?: string, - setting?: AnthropicProviderSettings - }, - openai: { - apiKey: string, - /** @default 'gpt-4o' */ - model?: string, - setting?: OpenAIProviderSettings - }, - azure: { - apiKey: string, - deploymentId: string, - setting?: AzureOpenAIProviderSettings - }, - greptile: { - apikey: string, - githubPAT: string, - repoinfo: { - remote: string, // e.g. 'github' - repository: string, // e.g. 'voideditor/void' - branch: string // e.g. 'main' - } - }, - ollama: { - /** @default 'llama3.1' */ - model: string - setting: OllamaProviderSettings - }, - whichApi: string + /** @default 'anthropic' */ + provider: 'anthropic' | 'openai' | 'azure' | 'greptile' | 'ollama' + anthropic: { + /** @default 'claude-3-5-sonnet-20240620' */ + model: string, + providerSettings?: AnthropicProviderSettings + }, + openai: { + /** @default 'gpt-4o' */ + model: string, + providerSettings?: OpenAIProviderSettings + }, + azure: { + deploymentId: string, + providerSettings?: AzureOpenAIProviderSettings + }, + greptile: { + providerSettings?: { + apikey?: string, + githubPAT?: string, + headers?: Record, + repoinfo?: { + remote?: string, // e.g. 'github' + repository?: string, // e.g. 'voideditor/void' + branch?: string // e.g. 'main' + }[] + } + }, + ollama: { + /** @default 'llama3.1' */ + model: string + providerSettings?: OllamaProviderSettings + }, } type OnText = (newText: string, fullText: string) => void export type LLMMessage = { - role: 'user' | 'assistant', - content: string + role: 'user' | 'assistant', + content: string } type SendLLMMessageFnTypeInternal = (params: { - messages: LLMMessage[], - onText: OnText, - onFinalMessage: (input: string) => void, - apiConfig: ApiConfig, + messages: LLMMessage[], + onText: OnText, + onFinalMessage: (input: string) => void, + apiConfig: ApiConfig, }) - => { - abort: () => void - } + => { + abort: () => void + } type SendLLMMessageFnTypeExternal = (params: { - messages: LLMMessage[], - onText: OnText, - onFinalMessage: (input: string) => void, - apiConfig: ApiConfig | null, + messages: LLMMessage[], + onText: OnText, + onFinalMessage: (input: string) => void, + apiConfig: ApiConfig | null, }) - => { - abort: () => void - } + => { + abort: () => void + } // Greptile // https://docs.greptile.com/api-reference/query @@ -72,107 +73,102 @@ type SendLLMMessageFnTypeExternal = (params: { const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { - let did_abort = false - let fullText = '' + let did_abort = false + let fullText = '' - // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - let abort: () => void = () => { did_abort = true } + // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either + let abort: () => void = () => { did_abort = true } - fetch('https://api.greptile.com/v2/query', { - method: 'POST', - headers: { - "Authorization": `Bearer ${apiConfig.greptile.apikey}`, - "X-Github-Token": `${apiConfig.greptile.githubPAT}`, - "Content-Type": `application/json`, - }, - body: JSON.stringify({ - messages, - stream: true, - repositories: [apiConfig.greptile.repoinfo] - }), - }) - // this is {message}\n{message}\n{message}...\n - .then(async response => { - const text = await response.text() - console.log('got greptile', text) - return JSON.parse(`[${text.trim().split('\n').join(',')}]`) - }) - // TODO make this actually stream, right now it just sends one message at the end - .then(async responseArr => { - if (did_abort) - return + fetch('https://api.greptile.com/v2/query', { + method: 'POST', + headers: { + "Authorization": `Bearer ${apiConfig.greptile.providerSettings?.apikey}`, + "Content-Type": `application/json`, + ...apiConfig.greptile.providerSettings?.headers + }, + body: JSON.stringify({ + messages, + stream: true, + repositories: apiConfig.greptile.providerSettings?.repoinfo + }), + }) + // this is {message}\n{message}\n{message}...\n + .then(async response => { + const text = await response.text() + console.log('got greptile', text) + return JSON.parse(`[${text.trim().split('\n').join(',')}]`) + }) + // TODO make this actually stream, right now it just sends one message at the end + .then(async responseArr => { + if (did_abort) + return - for (let response of responseArr) { + for (let response of responseArr) { - const type: string = response['type'] - const message = response['message'] + const type: string = response['type'] + const message = response['message'] - // when receive text - if (type === 'message') { - fullText += message - onText(message, fullText) - } - else if (type === 'sources') { - const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null } - fullText += filepath - onText(filepath, fullText) - } - // type: 'status' with an empty 'message' means last message - else if (type === 'status') { - if (!message) { - onFinalMessage(fullText) - } - } - } + // when receive text + if (type === 'message') { + fullText += message + onText(message, fullText) + } + else if (type === 'sources') { + const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null } + fullText += filepath + onText(filepath, fullText) + } + // type: 'status' with an empty 'message' means last message + else if (type === 'status') { + if (!message) { + onFinalMessage(fullText) + } + } + } - }) - .catch(e => { - console.error('Error in Greptile stream:', e); - onFinalMessage(fullText); - }); - return { abort } + }) + .catch(e => { + console.error('Error in Greptile stream:', e); + onFinalMessage(fullText); + }); + return { abort } } export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => { - if (!apiConfig) return { abort: () => { } } - const whichApi = apiConfig.whichApi - // TODO: create an @ai-sdk provider for greptile - if (whichApi === 'greptile') - return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig }) + if (!apiConfig) return { abort: () => { } } + const provider = apiConfig.provider + // TODO: create an @ai-sdk provider for greptile + if (provider === 'greptile') + return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig }) - const model = getAiModel(apiConfig) - const abortController = new AbortController() - const abortSignal = abortController.signal - streamText({ - model, - messages, - abortSignal, - }).then(async (result) => { - let fullText = '' - for await (const textPart of result.textStream) { - fullText += textPart - onText(textPart, fullText) - } - onFinalMessage(fullText) - }) + const model = getAiModel(apiConfig) + const abortController = new AbortController() + const abortSignal = abortController.signal + streamText({ + model, + messages, + abortSignal, + }).then(async (result) => { + let fullText = '' + for await (const textPart of result.textStream) { + fullText += textPart + onText(textPart, fullText) + } + onFinalMessage(fullText) + }) - return { abort: abortController.abort } + return { abort: abortController.abort } } export const getAiModel = (apiConfig: ApiConfig) => { - switch (apiConfig.whichApi) { - case 'openai': return createOpenAI(apiConfig.openai.setting)(apiConfig.openai.model || 'gpt-4o') - case 'anthropic': return createAnthropic(apiConfig.anthropic.setting)(apiConfig.anthropic.model || 'claude-3-5-sonnet-20240620') - case 'ollama': return createOllama(apiConfig.ollama.setting)(apiConfig.ollama.model || 'llama3.1') - case 'azure': { - if (!apiConfig.azure.deploymentId) { - throw new Error(`Error: azure deploymentId is not defined`) - } - return createAzure(apiConfig.azure.setting)(apiConfig.azure.deploymentId) - } - default: - throw new Error(`Error: provider was ${apiConfig.whichApi}, which is not recognized!`) - } + switch (apiConfig.provider) { + case 'openai': return createOpenAI(apiConfig.openai.providerSettings)(apiConfig.openai.model || 'gpt-4o') + case 'anthropic': return createAnthropic(apiConfig.anthropic.providerSettings)(apiConfig.anthropic.model || 'claude-3-5-sonnet-20240620') + case 'ollama': return createOllama(apiConfig.ollama.providerSettings)(apiConfig.ollama.model || 'llama3.1') + case 'azure': return createAzure(apiConfig.azure.providerSettings)(`${apiConfig.azure.deploymentId}`) + default: + throw new Error(`Error: provider was ${apiConfig.provider}, which is not recognized!`) + } } diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index 03016d0f..a0bdc9b5 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -1,156 +1,168 @@ import * as vscode from 'vscode'; import { WebviewMessage } from './shared_types'; -import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider'; import { getDiffedLines } from './getDiffedLines'; -import { ApprovalCodeLensProvider, SuggestedEdit } from './ApprovalCodeLensProvider'; +import { ApprovalCodeLensProvider } from './ApprovalCodeLensProvider'; 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'); // must remove windows \r or every line will appear different because of it + return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8').replace(/\r\n/g, '\n'); // must remove windows \r or every line will appear different because of it } const getApiConfig = () => { - const apiConfig: ApiConfig = { - anthropic: { apiKey: vscode.workspace.getConfiguration('void').get('anthropicApiKey') ?? '' }, - openai: { apiKey: vscode.workspace.getConfiguration('void').get('openAIApiKey') ?? '' }, - greptile: { - apikey: vscode.workspace.getConfiguration('void').get('greptileApiKey') ?? '', - githubPAT: vscode.workspace.getConfiguration('void').get('githubPAT') ?? '', - repoinfo: { - remote: 'github', - repository: 'TODO', - branch: 'main' - } - }, - ollama: { - model: vscode.workspace.getConfiguration('void').get('ollamaSettings.model') ?? '', - setting: { - baseURL: vscode.workspace.getConfiguration('void').get('ollamaSettings.baseURL') ?? '', + const apiConfig: ApiConfig = { + provider: vscode.workspace.getConfiguration('void').get('provider') ?? 'anthropic', + anthropic: { + model: vscode.workspace.getConfiguration('void').get('anthropic.model') ?? 'claude-3-5-sonnet-20240620', + providerSettings: { + ...vscode.workspace.getConfiguration('void').get('anthropic.providerSettings') ?? {}, }, - }, - whichApi: vscode.workspace.getConfiguration('void').get('whichApi') ?? '' - } - return apiConfig + }, + openai: { + model: vscode.workspace.getConfiguration('void').get('openai.model') ?? 'gpt-4o', + providerSettings: { + ...vscode.workspace.getConfiguration('void').get('openai.providerSettings') ?? {}, + }, + }, + azure: { + deploymentId: vscode.workspace.getConfiguration('void').get('azure.deploymentId') ?? '', + providerSettings: { + ...vscode.workspace.getConfiguration('void').get('azure.providerSettings') ?? {}, + }, + }, + ollama: { + model: vscode.workspace.getConfiguration('void').get('ollama.model') ?? 'llama3.1', + providerSettings: { + ...vscode.workspace.getConfiguration('void').get('ollama.providerSettings') ?? {}, + }, + }, + greptile: { + providerSettings: { + ...vscode.workspace.getConfiguration('void').get('greptile.providerSettings') ?? {}, + } + }, + } + console.log(apiConfig); + return apiConfig } export function activate(context: vscode.ExtensionContext) { - // 1. Mount the chat sidebar - const webviewProvider = new SidebarWebviewProvider(context); - context.subscriptions.push( - vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, webviewProvider, { webviewOptions: { retainContextWhenHidden: true } }) - ); + // 1. Mount the chat sidebar + const webviewProvider = new SidebarWebviewProvider(context); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, webviewProvider, { webviewOptions: { retainContextWhenHidden: true } }) + ); - // 2. Activate the sidebar on ctrl+l - context.subscriptions.push( - vscode.commands.registerCommand('void.ctrl+l', () => { + // 2. Activate the sidebar on ctrl+l + context.subscriptions.push( + vscode.commands.registerCommand('void.ctrl+l', () => { - const editor = vscode.window.activeTextEditor - if (!editor) - return + const editor = vscode.window.activeTextEditor + if (!editor) + return - // show the sidebar - vscode.commands.executeCommand('workbench.view.extension.voidViewContainer'); - // vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar + // show the sidebar + vscode.commands.executeCommand('workbench.view.extension.voidViewContainer'); + // vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar - // get the text the user is selecting - const selectionStr = editor.document.getText(editor.selection); + // get the text the user is selecting + const selectionStr = editor.document.getText(editor.selection); - // get the range of the selection - const selectionRange = editor.selection; + // get the range of the selection + const selectionRange = editor.selection; - // get the file the user is in - const filePath = editor.document.uri; + // get the file the user is in + const filePath = editor.document.uri; - // send message to the webview (Sidebar.tsx) - webviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies WebviewMessage)); - }) - ); + // send message to the webview (Sidebar.tsx) + webviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies WebviewMessage)); + }) + ); - // 3. Show an approve/reject codelens above each change - const approvalCodeLensProvider = new ApprovalCodeLensProvider(); - context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', approvalCodeLensProvider)); + // 3. Show an approve/reject codelens above each change + const approvalCodeLensProvider = new ApprovalCodeLensProvider(); + context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', approvalCodeLensProvider)); - // 4. Add approve/reject commands - context.subscriptions.push(vscode.commands.registerCommand('void.approveDiff', async (params) => { - approvalCodeLensProvider.approveDiff(params) - })); - context.subscriptions.push(vscode.commands.registerCommand('void.discardDiff', async (params) => { - approvalCodeLensProvider.discardDiff(params) - })); + // 4. Add approve/reject commands + context.subscriptions.push(vscode.commands.registerCommand('void.approveDiff', async (params) => { + approvalCodeLensProvider.approveDiff(params) + })); + context.subscriptions.push(vscode.commands.registerCommand('void.discardDiff', async (params) => { + approvalCodeLensProvider.discardDiff(params) + })); - // 5. - webviewProvider.webview.then( - webview => { + // 5. + webviewProvider.webview.then( + webview => { - // 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 WebviewMessage) - } - }) + // 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 WebviewMessage) + } + }) - // Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`) - webview.onDidReceiveMessage(async (m: WebviewMessage) => { + // Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`) + webview.onDidReceiveMessage(async (m: WebviewMessage) => { - if (m.type === 'requestFiles') { + if (m.type === 'requestFiles') { - // get contents of all file paths - const files = await Promise.all( - m.filepaths.map(async (filepath) => ({ filepath, content: await readFileContentOfUri(filepath) })) - ) + // get contents of all file paths + const files = await Promise.all( + m.filepaths.map(async (filepath) => ({ filepath, content: await readFileContentOfUri(filepath) })) + ) - // send contents to webview - webview.postMessage({ type: 'files', files, } satisfies WebviewMessage) + // send contents to webview + webview.postMessage({ type: 'files', files, } satisfies WebviewMessage) - } else if (m.type === 'applyCode') { + } else if (m.type === 'applyCode') { - const editor = vscode.window.activeTextEditor - if (!editor) { - vscode.window.showInformationMessage('No active editor!') - return - } - const oldContents = await readFileContentOfUri(editor.document.uri) - const suggestedEdits = getDiffedLines(oldContents, m.code) - await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits) - } - else if (m.type === 'getApiConfig') { + const editor = vscode.window.activeTextEditor + if (!editor) { + vscode.window.showInformationMessage('No active editor!') + return + } + const oldContents = await readFileContentOfUri(editor.document.uri) + const suggestedEdits = getDiffedLines(oldContents, m.code) + await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits) + } + else if (m.type === 'getApiConfig') { - const apiConfig = getApiConfig() - console.log('Api config:', apiConfig) + const apiConfig = getApiConfig() + console.log('Api config:', apiConfig) - webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage) + webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage) - } - else { + } + else { - console.error('unrecognized command', m.type, m) - } - }) - } - ) + console.error('unrecognized command', m.type, m) + } + }) + } + ) - // Gets called when user presses ctrl + k (mounts ctrl+k-style codelens) - // TODO need to build this - // const ctrlKCodeLensProvider = new CtrlKCodeLensProvider(); - // context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', ctrlKCodeLensProvider)); - // context.subscriptions.push( - // vscode.commands.registerCommand('void.ctrl+k', () => { - // const editor = vscode.window.activeTextEditor; - // if (!editor) - // return - // ctrlKCodeLensProvider.addNewCodeLens(editor.document, editor.selection); - // // vscode.commands.executeCommand('editor.action.showHover'); // apparently this refreshes the codelenses by having the internals call provideCodeLenses - // }) - // ) + // Gets called when user presses ctrl + k (mounts ctrl+k-style codelens) + // TODO need to build this + // const ctrlKCodeLensProvider = new CtrlKCodeLensProvider(); + // context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', ctrlKCodeLensProvider)); + // context.subscriptions.push( + // vscode.commands.registerCommand('void.ctrl+k', () => { + // const editor = vscode.window.activeTextEditor; + // if (!editor) + // return + // ctrlKCodeLensProvider.addNewCodeLens(editor.document, editor.selection); + // // vscode.commands.executeCommand('editor.action.showHover'); // apparently this refreshes the codelenses by having the internals call provideCodeLenses + // }) + // ) }