vscode types in extension

This commit is contained in:
Andrew 2024-11-02 21:53:08 -07:00
parent fcc8de807f
commit e4e1c7fb6c
8 changed files with 410 additions and 82 deletions

View file

@ -23,7 +23,6 @@
"@types/react-dom": "^18.3.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0",
"@types/vscode": "1.92.0",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@vscode/test-cli": "^0.0.10",
@ -53,7 +52,7 @@
"uuid": "^10.0.0"
},
"engines": {
"vscode": "^1.92.0"
"vscode": "*"
}
},
"node_modules/@alloc/quick-lru": {
@ -1257,13 +1256,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/vscode": {
"version": "1.92.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.92.0.tgz",
"integrity": "sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@ -6024,6 +6016,14 @@
"node": ">=10"
}
},
"node_modules/monaco-editor": {
"version": "0.52.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz",
"integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View file

@ -5,7 +5,7 @@
"description": "",
"version": "0.0.1",
"engines": {
"vscode": "^1.92.0"
"vscode": "*"
},
"categories": [
"Other"
@ -127,7 +127,6 @@
"@types/react-dom": "^18.3.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0",
"@types/vscode": "1.92.0",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@vscode/test-cli": "^0.0.10",

View file

@ -1,6 +1,6 @@
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
import { Ollama } from 'ollama'
import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { VoidConfig } from '../webviews/common/contextForConfig'
@ -11,36 +11,36 @@ export type OnText = (newText: string, fullText: string) => void
export type OnFinalMessage = (input: string) => void
export type LLMMessageAnthropic = {
role: 'user' | 'assistant',
content: string,
role: 'user' | 'assistant';
content: string;
}
export type LLMMessage = {
role: 'system' | 'user' | 'assistant',
content: string,
role: 'system' | 'user' | 'assistant';
content: string;
}
type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[],
onText: OnText,
onFinalMessage: OnFinalMessage,
onError: (error: string) => void,
voidConfig: VoidConfig,
abortRef: AbortRef,
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: (error: string) => void;
voidConfig: VoidConfig;
abortRef: AbortRef;
}) => void
type SendLLMMessageFnTypeExternal = (params: {
messages: LLMMessage[],
onText: OnText,
onFinalMessage: (fullText: string) => void,
onError: (error: string) => void,
voidConfig: VoidConfig | null,
abortRef: AbortRef,
messages: LLMMessage[];
onText: OnText;
onFinalMessage: (fullText: string) => void;
onError: (error: string) => void;
voidConfig: VoidConfig | null;
abortRef: AbortRef;
}) => void
const parseMaxTokensStr = (maxTokensStr: string) => {
// parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
if (Number.isNaN(int))
return undefined
return int
@ -79,7 +79,7 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi
stream.on('finalMessage', (claude_response) => {
if (did_abort) return
// stringify the response's content
let content = claude_response.content.map(c => { if (c.type === 'text') { return c.text } }).join('\n');
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
onFinalMessage(content)
})
@ -117,7 +117,7 @@ const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, o
// remove system messages that get sent to Gemini
// str of all system messages
let systemMessage = messages
const systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
@ -173,7 +173,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
let maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
if (voidConfig.default.whichApi === 'openAI') {
openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
@ -181,10 +181,10 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
}
else if (voidConfig.default.whichApi === 'openRouter') {
openai = new OpenAI({
baseURL: "https://openrouter.ai/api/v1", apiKey: voidConfig.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.
'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: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
@ -235,7 +235,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ""
let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
abortRef.current = () => {
@ -289,9 +289,9 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
fetch('https://api.greptile.com/v2/query', {
method: 'POST',
headers: {
"Authorization": `Bearer ${voidConfig.greptile.apikey}`,
"X-Github-Token": `${voidConfig.greptile.githubPAT}`,
"Content-Type": `application/json`,
'Authorization': `Bearer ${voidConfig.greptile.apikey}`,
'X-Github-Token': `${voidConfig.greptile.githubPAT}`,
'Content-Type': `application/json`,
},
body: JSON.stringify({
messages,
@ -310,7 +310,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
if (didAbort)
return
for (let response of responseArr) {
for (const response of responseArr) {
const type: string = response['type']
const message = response['message']
@ -321,7 +321,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin
onText(message, fullText)
}
else if (type === 'sources') {
const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null }
const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
fullText += filepath
onText(filepath, fullText)
}

View file

@ -1,4 +1,5 @@
import * as vscode from 'vscode';
import type * as vscode from 'vscode';
import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage';
import { DiffArea } from '../common/shared_types';
import { writeFileWithDiffInstructions, searchDiffChunkInstructions } from '../common/systemPrompts';

View file

@ -10,28 +10,6 @@ import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider';
import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider';
// this comes from vscode.proposed.editorInsets.d.ts
declare module 'vscode' {
export interface WebviewEditorInset {
readonly editor: vscode.TextEditor;
readonly line: number;
readonly height: number;
readonly webview: vscode.Webview;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
}
}
// this comes from vscode.d.ts
declare module 'vscode' {
export namespace languages {
export function addInlineDiff(editor: vscode.TextEditor, originalText: string, modifiedRange: Range): void;
}
}
const roundRangeToLines = (selection: vscode.Selection) => {
let endLine = selection.end.character === 0 ? selection.end.line - 1 : selection.end.line // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
return new vscode.Range(selection.start.line, 0, endLine, Number.MAX_SAFE_INTEGER)

View file

@ -3,22 +3,6 @@
import * as vscode from 'vscode';
import { updateWebviewHTML as _updateWebviewHTML, updateWebviewHTML } from '../extensionLib/updateWebviewHTML';
// this comes from vscode.proposed.editorInsets.d.ts
declare module 'vscode' {
export interface WebviewEditorInset {
readonly editor: vscode.TextEditor;
readonly line: number;
readonly height: number;
readonly webview: vscode.Webview;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
}
}
export class CtrlKWebviewProvider {

View file

@ -1,6 +1,7 @@
{
"include": [
"src/**/*"
"src/**/*",
"../../src/vscode-dts/vscode.d.ts",
],
"exclude": [
"node_modules"
@ -27,4 +28,4 @@
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
}
}
}

View file

@ -0,0 +1,365 @@
// import Anthropic from '@anthropic-ai/sdk';
// import OpenAI from 'openai';
// import { Ollama } from 'ollama'
// import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai';
// import { VoidConfig } from '../webviews/common/contextForConfig'
// export type AbortRef = { current: (() => void) | null }
// export type OnText = (newText: string, fullText: string) => void
// export type OnFinalMessage = (input: string) => void
// export type LLMMessageAnthropic = {
// role: 'user' | 'assistant';
// content: string;
// }
// export type LLMMessage = {
// role: 'system' | 'user' | 'assistant';
// content: string;
// }
// type SendLLMMessageFnTypeInternal = (params: {
// messages: LLMMessage[];
// onText: OnText;
// onFinalMessage: OnFinalMessage;
// onError: (error: string) => void;
// voidConfig: VoidConfig;
// abortRef: AbortRef;
// }) => void
// type SendLLMMessageFnTypeExternal = (params: {
// messages: LLMMessage[];
// onText: OnText;
// onFinalMessage: (fullText: string) => void;
// onError: (error: string) => void;
// voidConfig: VoidConfig | null;
// abortRef: AbortRef;
// }) => void
// const parseMaxTokensStr = (maxTokensStr: string) => {
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
// if (Number.isNaN(int))
// return undefined
// return int
// }
// // Anthropic
// const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
// const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
// // find system messages and concatenate them
// const systemMessage = messages
// .filter(msg => msg.role === 'system')
// .map(msg => msg.content)
// .join('\n');
// // remove system messages for Anthropic
// const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
// const stream = anthropic.messages.stream({
// system: systemMessage,
// messages: anthropicMessages,
// model: voidConfig.anthropic.model,
// max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
// });
// let did_abort = false
// // when receive text
// stream.on('text', (newText, fullText) => {
// if (did_abort) return
// onText(newText, fullText)
// })
// // when we get the final message on this stream (or when error/fail)
// stream.on('finalMessage', (claude_response) => {
// if (did_abort) return
// // stringify the response's content
// const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
// onFinalMessage(content)
// })
// stream.on('error', (error) => {
// // the most common error will be invalid API key (401), so we handle this with a nice message
// if (error instanceof Anthropic.APIError && error.status === 401) {
// onError('Invalid API key.')
// }
// else {
// onError(error.message)
// }
// })
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
// const abort = () => {
// did_abort = true
// stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error
// }
// return { abort }
// };
// // Gemini
// const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
// let didAbort = false
// let fullText = ''
// abortRef.current = () => {
// didAbort = true
// }
// const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
// const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
// // remove system messages that get sent to Gemini
// // str of all system messages
// const systemMessage = messages
// .filter(msg => msg.role === 'system')
// .map(msg => msg.content)
// .join('\n');
// // Convert messages to Gemini format
// const geminiMessages: Content[] = messages
// .filter(msg => msg.role !== 'system')
// .map((msg, i) => ({
// parts: [{ text: msg.content }],
// role: msg.role === 'assistant' ? 'model' : 'user'
// }))
// model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
// .then(async response => {
// abortRef.current = () => {
// // response.stream.return(fullText)
// didAbort = true;
// }
// for await (const chunk of response.stream) {
// if (didAbort) return;
// const newText = chunk.text();
// fullText += newText;
// onText(newText, fullText);
// }
// onFinalMessage(fullText);
// })
// .catch((error) => {
// if (error instanceof GoogleGenerativeAIFetchError) {
// if (error.status === 400) {
// onError('Invalid API key.');
// }
// else {
// onError(`${error.name}:\n${error.message}`);
// }
// }
// else {
// onError(error);
// }
// })
// }
// // OpenAI, OpenRouter, OpenAICompatible
// const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
// let didAbort = false
// let fullText = ''
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
// abortRef.current = () => {
// didAbort = true;
// };
// let openai: OpenAI
// let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
// const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
// if (voidConfig.default.whichApi === 'openAI') {
// openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
// options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
// }
// else if (voidConfig.default.whichApi === 'openRouter') {
// openai = new OpenAI({
// 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: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
// }
// 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, max_completion_tokens: maxTokens }
// }
// else {
// console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
// throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
// }
// openai.chat.completions
// .create(options)
// .then(async response => {
// abortRef.current = () => {
// // response.controller.abort()
// didAbort = true;
// }
// // when receive text
// for await (const chunk of response) {
// if (didAbort) return;
// const newText = chunk.choices[0]?.delta?.content || '';
// fullText += newText;
// onText(newText, fullText);
// }
// onFinalMessage(fullText);
// })
// // when error/fail - this catches errors of both .create() and .then(for await)
// .catch(error => {
// if (error instanceof OpenAI.APIError) {
// if (error.status === 401) {
// onError('Invalid API key.');
// }
// else {
// onError(`${error.name}:\n${error.message}`);
// }
// }
// else {
// onError(error);
// }
// })
// };
// // Ollama
// export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
// let didAbort = false
// let fullText = ''
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
// abortRef.current = () => {
// didAbort = true;
// };
// const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
// ollama.chat({
// model: voidConfig.ollama.model,
// messages: messages,
// stream: true,
// options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
// })
// .then(async stream => {
// abortRef.current = () => {
// // stream.abort()
// didAbort = true
// }
// // iterate through the stream
// for await (const chunk of stream) {
// if (didAbort) return;
// const newText = chunk.message.content;
// fullText += newText;
// onText(newText, fullText);
// }
// onFinalMessage(fullText);
// })
// // when error/fail
// .catch(error => {
// onError(error)
// })
// };
// // Greptile
// // https://docs.greptile.com/api-reference/query
// // https://docs.greptile.com/quickstart#sample-response-streamed
// const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
// let didAbort = false
// let fullText = ''
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
// abortRef.current = () => {
// didAbort = true
// }
// fetch('https://api.greptile.com/v2/query', {
// method: 'POST',
// headers: {
// 'Authorization': `Bearer ${voidConfig.greptile.apikey}`,
// 'X-Github-Token': `${voidConfig.greptile.githubPAT}`,
// 'Content-Type': `application/json`,
// },
// body: JSON.stringify({
// messages,
// stream: true,
// repositories: [voidConfig.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 (didAbort)
// return
// for (const response of responseArr) {
// 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: _2 } = 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 => {
// onError(e)
// });
// }
// export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
// if (!voidConfig) return;
// // trim message content (Anthropic and other providers give an error if there is trailing whitespace)
// messages = messages.map(m => ({ ...m, content: m.content.trim() }))
// switch (voidConfig.default.whichApi) {
// case 'anthropic':
// return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
// case 'openAI':
// case 'openRouter':
// case 'openAICompatible':
// return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
// case 'gemini':
// return sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
// case 'ollama':
// return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
// case 'greptile':
// return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
// default:
// onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
// }
// }