diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index 03f61915..26ab3349 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -15,6 +15,7 @@ "@types/diff": "^5.2.2", "@types/diff-match-patch": "^1.0.36", "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.12", "@types/mocha": "^10.0.8", "@types/node": "^22.5.1", "@types/react": "^18.3.4", @@ -33,11 +34,12 @@ "eslint-plugin-react": "^7.35.1", "eslint-plugin-react-hooks": "^4.6.2", "globals": "^15.9.0", + "lodash": "^4.17.21", "marked": "^14.1.0", "ollama": "^0.5.9", - "openai": "^4.68.1", + "openai": "^4.68.4", "postcss": "^8.4.41", - "posthog-js": "^1.174.0", + "posthog-js": "^1.176.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", @@ -1134,6 +1136,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/lodash": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.12.tgz", + "integrity": "sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==", + "dev": true + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -2409,7 +2417,6 @@ "integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -3369,8 +3376,7 @@ "version": "0.4.8", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/file-entry-cache": { "version": "6.0.1", @@ -5019,6 +5025,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6273,9 +6285,9 @@ } }, "node_modules/openai": { - "version": "4.68.2", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.68.2.tgz", - "integrity": "sha512-Ys3Jl9vkBUFtrFj4pgrF7rMte4JNekZoMgI6dWkkpOIwNUKGkc4I8jTqv86LB+TcoqkTPzV6DS269dPR9ILWsQ==", + "version": "4.68.4", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.68.4.tgz", + "integrity": "sha512-LRinV8iU9VQplkr25oZlyrsYGPGasIwYN8KFMAAFTHHLHjHhejtJ5BALuLFrkGzY4wfbKhOhuT+7lcHZ+F3iEA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6597,9 +6609,9 @@ } }, "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, "license": "ISC" }, @@ -6828,9 +6840,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.174.3", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.174.3.tgz", - "integrity": "sha512-fRLncd3jkT9Y7gLiyQe8v8sJ9yuTIiQBBWcYQ8l+vv+m504LWFtxl+/JZtHXPhaG3Eyf7AzZ/Kafkw8jorWV9w==", + "version": "1.176.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.176.0.tgz", + "integrity": "sha512-T5XKNtRzp7q6CGb7Vc7wAI76rWap9fiuDUPxPsyPBPDkreKya91x9RIsSapAVFafwD1AEin1QMczCmt9Le9BWw==", "dev": true, "license": "MIT", "dependencies": { @@ -6845,7 +6857,6 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "dev": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -8662,9 +8673,9 @@ } }, "node_modules/web-vitals": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", - "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.3.tgz", + "integrity": "sha512-/CFAm1mNxSmOj6i0Co+iGFJ58OS4NRGVP+AWS/l509uIK5a1bSoIVaHz/ZumpHTfHSZBpgrJ+wjfpAOrTHok5Q==", "dev": true, "license": "Apache-2.0" }, diff --git a/extensions/void/package.json b/extensions/void/package.json index 9e2b33ba..16553535 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -116,6 +116,7 @@ "@types/diff": "^5.2.2", "@types/diff-match-patch": "^1.0.36", "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.12", "@types/mocha": "^10.0.8", "@types/node": "^22.5.1", "@types/react": "^18.3.4", @@ -134,11 +135,12 @@ "eslint-plugin-react": "^7.35.1", "eslint-plugin-react-hooks": "^4.6.2", "globals": "^15.9.0", + "lodash": "^4.17.21", "marked": "^14.1.0", "ollama": "^0.5.9", - "openai": "^4.68.1", + "openai": "^4.68.4", "postcss": "^8.4.41", - "posthog-js": "^1.174.0", + "posthog-js": "^1.176.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", diff --git a/extensions/void/src/common/ctrlL.ts b/extensions/void/src/common/ctrlL.ts new file mode 100644 index 00000000..687c4546 --- /dev/null +++ b/extensions/void/src/common/ctrlL.ts @@ -0,0 +1,259 @@ +import * as vscode from 'vscode'; +import { OnFinalMessage, OnText, sendLLMMessage, SetAbort } from "./sendLLMMessage" +import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts'; +import { throttle } from 'lodash'; +import { VoidConfig } from '../webviews/common/contextForConfig'; +import { findDiffs } from '../extension/findDiffs'; + +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 +} +type Res = ((value: T) => void) + +const THRTOTLE_TIME = 100 // minimum time between edits +const LINES_PER_CHUNK = 20 // number of lines to search at a time + +const applyCtrlLChangesToFile = throttle( + ({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr, debug }: { fileUri: vscode.Uri, newCurrentLine: number, oldCurrentLine: number, fullCompletedStr: string, oldFileStr: string, debug?: string }) => { + + console.log('DEBUG: ', debug) + console.log('oldNext: ', oldCurrentLine) + console.log('newNext: ', newCurrentLine) + console.log('WRITE_TO_FILE1: ', fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n')) + console.log('WRITE_TO_FILE2: ', oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n')) + + // write the change to the file + const WRITE_TO_FILE = ( + fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n') // newFile[:newCurrentLine+1] + + oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n') // oldFile[oldCurrentLine+1:] + ) + const workspaceEdit = new vscode.WorkspaceEdit() + workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), WRITE_TO_FILE) + vscode.workspace.applyEdit(workspaceEdit) + + // highlight the `newCurrentLine` in white + // highlight the remaining part of the file in gray + + }, + THRTOTLE_TIME, { trailing: true } +) + + +// `next` is the line after the completed text +// `oldNext` is the same line but in the original file +type CompetedReturn = { isFinished: true, next?: undefined, oldNext?: undefined, } | { isFinished?: undefined, next: number, oldNext: number, } +const generateFileUsingDiffUntilMatchup = ({ fileUri, oldFileStr, completedStr, oldNext, next, diffStr, voidConfig, setAbort }: { fileUri: vscode.Uri, oldFileStr: string, completedStr: string, oldNext: number, next: number, diffStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { + + const NUM_MATCHUP_TOKENS = 20 + + const promptContent = `ORIGINAL_FILE +\`\`\` +${oldFileStr} +\`\`\` + +DIFF +\`\`\` +${diffStr} +\`\`\` + +INSTRUCTIONS +Please finish writing the new file \`NEW_FILE\`. Return ONLY the completion of the file, without any explanation. + +NEW_FILE +\`\`\` +${completedStr} +\`\`\` +` + // create a promise that can be awaited + let res: Res = () => { } + const promise = new Promise((resolve, reject) => { res = resolve }) + + // get the abort method + let _abort = () => { } + let did_abort = false + + // make LLM complete the file to include the diff + sendLLMMessage({ + messages: [{ role: 'system', content: writeFileWithDiffInstructions, }, { role: 'user', content: promptContent, }], + onText: (tokenStr, deltaStr) => { + + if (did_abort) return; + + const fullCompletedStr = completedStr + deltaStr + + // diff `originalFileStr` and `newFileStr` + const diffs = findDiffs(oldFileStr, fullCompletedStr) + const lastDiff = diffs[diffs.length - 1] + const oldLineAfterLastDiff = lastDiff.deletedRange.end.line + 1 + const newLineAfterLastDiff = lastDiff.insertedRange.end.line + 1 + + // check if we've generated a diff + const didGenerateDiff = newLineAfterLastDiff > next + + // get the line we are currently generating `newCurrentLine`; make sure it never goes past the last diff we've generated + // - if `deltaStr` contains a diff, then _next = newLineAfterLastDiff - 1 + // - if it does not contain a diff, then _next = next + deltaStr.split('\n').length - 1 + const newCurrentLine = didGenerateDiff ? newLineAfterLastDiff - 1 : next + deltaStr.split('\n').length - 1 + const oldCurrentLine = didGenerateDiff ? oldLineAfterLastDiff - 1 : oldNext + (newCurrentLine - next) + + // 1. Apply the changes and modify highlighting + + applyCtrlLChangesToFile({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr }) + + // 2. Check for early stopping + // the conditions for early stopping are: + // - we have generated a diff + // - there is matchup with the original file after the diff + const isMatchupAfterDiff = fullCompletedStr.split('\n').slice(newLineAfterLastDiff).join('\n').length > NUM_MATCHUP_TOKENS + if (didGenerateDiff && isMatchupAfterDiff) { + + // resolve the promise + res({ next: newCurrentLine + 1, oldNext: oldCurrentLine + 1, }); + + // abort the LLM call + _abort() + did_abort = true + + } else { + + } + + + + }, + onFinalMessage: (deltaStr) => { + + const newCompletedStr = completedStr + deltaStr + + applyCtrlLChangesToFile({ fileUri, newCurrentLine: Number.MAX_SAFE_INTEGER, oldCurrentLine: Number.MAX_SAFE_INTEGER, fullCompletedStr: newCompletedStr, oldFileStr, debug: 'FINAL' }) + + res({ isFinished: true }); + }, + onError: (e) => { + res({ isFinished: true }); + console.error('Error rewriting file with diff', e); + }, + voidConfig, + setAbort: (a) => { setAbort(a); _abort = a; }, + }) + + return promise + +} + + +const shouldApplyDiffFn = ({ diffStr, fileStr, speculationStr, voidConfig, setAbort }: { diffStr: string, fileStr: string, speculationStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { + + const promptContent = `DIFF +\`\`\` +${diffStr} +\`\`\` + +FILES +\`\`\` +${fileStr} +\`\`\` + +SELECTION +\`\`\` +${speculationStr} +\`\`\` + +Return \`true\` if ANY part of the chunk should be modified, and \`false\` if it should not be modified. You should respond only with \`true\` or \`false\` and nothing else. +` + + // create new promise + let res: Res = () => { } + const promise = new Promise((resolve, reject) => { res = resolve }) + + // send message to LLM + sendLLMMessage({ + messages: [{ role: 'system', content: searchDiffChunkInstructions, }, { role: 'user', content: promptContent, }], + onFinalMessage: (finalMessage) => { + + const containsTrue = finalMessage + .slice(-10) // check for `true` in last 10 characters + .toLowerCase() + .includes('true') + + res(containsTrue) + }, + onError: (e) => { + res(false); + console.error('Error in shouldApplyDiff: ', e) + }, + onText: () => { }, + voidConfig, + setAbort, + }) + + // return the promise + return promise + +} + + + +// lazily applies the diff to the file +// we chunk the text in the file, and ask an LLM whether it should edit each chunk +const applyDiffLazily = async ({ fileUri, oldFileStr, diffStr, voidConfig, setAbort }: { fileUri: vscode.Uri, oldFileStr: string, diffStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { + + + // stateful variables + let next = 0 + let oldNext = 0 + + while (next < oldFileStr.split('\n').length) { + + console.log('next line: ', next) + + // get the chunk + const chunkStr = oldFileStr.split('\n').slice(next, next + LINES_PER_CHUNK).join('\n') + + // ask LLM if we should apply the diff to the chunk + const __start = new Date().getTime() + + let shouldApplyDiff = await shouldApplyDiffFn({ fileStr: oldFileStr, speculationStr: chunkStr, diffStr, voidConfig, setAbort }) + + const __end = new Date().getTime() + + if (!shouldApplyDiff) { // should not change the chunk + console.log('KEEP CHUNK time: ', __end - __start) + + next += LINES_PER_CHUNK + oldNext += LINES_PER_CHUNK + + continue; + } + + + // ask LLM to rewrite file with diff (if there is significant matchup with the original file, we stop rewriting) + // make vscode read uri = 'asdasd' + + const ___start = new Date().getTime() + + + const completedStr = (await readFileContentOfUri(fileUri)).split('\n').slice(0, next).join('\n'); + const result = await generateFileUsingDiffUntilMatchup({ fileUri, oldFileStr, completedStr, oldNext, next, diffStr, voidConfig, setAbort, }) + + const ___end = new Date().getTime() + + console.log('EDIT CHUNK time: ', ___end - ___start); + + // if we are finished, stop the loop + if (result.isFinished) { + break; + } + + next = result.next + oldNext = result.oldNext + + } + + +} + + + +export { applyDiffLazily } \ No newline at end of file diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index 3065c2d7..b84fa543 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -6,23 +6,30 @@ import { VoidConfig } from '../webviews/common/contextForConfig' -type OnText = (newText: string, fullText: string) => void +export type OnText = (newText: string, fullText: string) => void + +export type OnFinalMessage = (input: string) => void + +export type SetAbort = (abort: () => void) => void + +export type LLMMessageAnthropic = { + role: 'user' | 'assistant', + content: string, +} export type LLMMessage = { - role: 'user' | 'assistant', - content: string + role: 'system' | 'user' | 'assistant', + content: string, } type SendLLMMessageFnTypeInternal = (params: { messages: LLMMessage[], onText: OnText, - onFinalMessage: (input: string) => void, + onFinalMessage: OnFinalMessage, onError: (error: string) => void, voidConfig: VoidConfig, -}) - => { - abort: () => void - } + setAbort: SetAbort, +}) => void type SendLLMMessageFnTypeExternal = (params: { messages: LLMMessage[], @@ -30,23 +37,33 @@ type SendLLMMessageFnTypeExternal = (params: { onFinalMessage: (input: string) => void, onError: (error: string) => void, voidConfig: VoidConfig | null, + setAbort: SetAbort, + }) - => { - abort: () => void - } + => void // Anthropic -const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { +const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => { 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: parseInt(voidConfig.anthropic.maxTokens), - messages: messages, + max_tokens: parseInt(voidConfig.default.maxTokens), }); let did_abort = false @@ -77,11 +94,10 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either const abort = () => { - // stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error did_abort = true + stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error } - - return { abort } + setAbort(abort) }; @@ -89,7 +105,7 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi // OpenAI, OpenRouter, OpenAICompatible -const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { +const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => { let didAbort = false let fullText = '' @@ -104,7 +120,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal if (voidConfig.default.whichApi === 'openAI') { openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true }); - options = { model: voidConfig.openAI.model, messages: messages, stream: true, } + options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.maxTokens) } } else if (voidConfig.default.whichApi === 'openRouter') { openai = new OpenAI({ @@ -114,11 +130,11 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal "X-Title": 'Void Editor', // Optional. Shows in rankings on openrouter.ai. }, }); - options = { model: voidConfig.openRouter.model, messages: messages, stream: true, } + options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.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, } + options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: parseInt(voidConfig.default.maxTokens) } } else { console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`) @@ -156,12 +172,12 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal } }) - return { abort }; + setAbort(abort) }; // Ollama -export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { +export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => { let didAbort = false let fullText = "" @@ -177,6 +193,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, model: voidConfig.ollama.model, messages: messages, stream: true, + options: { num_predict: parseInt(voidConfig.default.maxTokens) } // this is max_tokens }) .then(async stream => { abort = () => { @@ -198,7 +215,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onError(error) }) - return { abort }; + setAbort(abort); }; @@ -207,7 +224,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, onError, voidConfig }) => { +const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => { let didAbort = false let fullText = '' @@ -226,7 +243,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin body: JSON.stringify({ messages, stream: true, - repositories: [voidConfig.greptile.repoinfo] + repositories: [voidConfig.greptile.repoinfo], }), }) // this is {message}\n{message}\n{message}...\n @@ -268,28 +285,29 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin onError(e) }); - return { abort } - + setAbort(abort) } -export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { - if (!voidConfig) return { abort: () => { } } +export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }) => { + 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 }); + return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }); case 'openAI': case 'openRouter': case 'openAICompatible': - return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig }); + return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }); case 'ollama': - return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig }); + return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }); case 'greptile': - return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig }); + return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, setAbort }); default: onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) - return { abort: () => { } } } } diff --git a/extensions/void/src/common/shared_types.ts b/extensions/void/src/common/shared_types.ts index b707b249..de57ff9c 100644 --- a/extensions/void/src/common/shared_types.ts +++ b/extensions/void/src/common/shared_types.ts @@ -26,9 +26,9 @@ type DiffArea = BaseDiffArea & { diffareaid: number } // the return type of diff creator type BaseDiff = { code: string; // representation of the diff in text - deletedRange: vscode.Range; // relative to the file, inclusive + deletedRange: vscode.Range; // relative to the original file, inclusive insertedRange: vscode.Range; - deletedCode: string; + deletedCode: string; // relative to the new file, inclusive insertedCode: string; } @@ -83,7 +83,12 @@ type ChatMessage = | { role: "assistant"; content: string; // content received from LLM - displayContent: string; // content displayed to user (this is the same as content for now) + displayContent: string | undefined; // content displayed to user (this is the same as content for now) + } + | { + role: "system"; + content: string; + displayContent?: undefined; } export { diff --git a/extensions/void/src/common/systemPrompts.ts b/extensions/void/src/common/systemPrompts.ts new file mode 100644 index 00000000..7e443053 --- /dev/null +++ b/extensions/void/src/common/systemPrompts.ts @@ -0,0 +1,406 @@ + +const generateDiffInstructions = ` +You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`. + +Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead). + +All changes made to files must be outputted in unified diff format. +Unified diff format instructions: +1. Each diff must begin with \`\`\`@@ ... @@\`\`\`. +2. Each line must start with a \`+\` or \`-\` or \` \` symbol. +3. Make diffs more than a few lines. +4. Make high-level diffs rather than many one-line diffs. + +Here's an example of unified diff format: + +\`\`\` +@@ ... @@ +-def factorial(n): +- if n == 0: +- return 1 +- else: +- return n * factorial(n-1) ++def factorial(number): ++ if number == 0: ++ return 1 ++ else: ++ return number * factorial(number-1) +\`\`\` + +Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped: + +\`\`\` +@@ ... @@ # This is less preferred because edits are close together and should be grouped: +-def factorial(n): ++def factorial(number): +- if n == 0: ++ if number == 0: + return 1 + else: +- return n * factorial(n-1) ++ return number * factorial(number-1) +\`\`\` + +# Example 1: + +FILES +selected file \`test.ts\`: +\`\`\` +x = 1 + +{{selection}} + +z = 3 +\`\`\` + +SELECTION +\`\`\`const y = 2\`\`\` + +INSTRUCTIONS +\`\`\`y = 3\`\`\` + +EXPECTED RESULT + +We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`. +\`\`\` +@@ ... @@ +-x = 1 +- +-y = 2 ++x = 1 ++ ++y = 3 +\`\`\` + +# Example 2: + +FILES +selected file \`Sidebar.tsx\`: +\`\`\` +import React from 'react'; +import styles from './Sidebar.module.css'; + +interface SidebarProps { + items: { label: string; href: string }[]; + onItemSelect?: (label: string) => void; + onExtraButtonClick?: () => void; +} + +const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +
+
    + {items.map((item, index) => ( +
  • + {{selection}} + className={styles.sidebarButton} + onClick={() => onItemSelect?.(item.label)} + > + {item.label} + +
  • + ))} +
+ +
+ ); +}; + +export default Sidebar; +\`\`\` + +SELECTION +\`\`\` +-
    +- {items.map((item, index) => ( +-
  • +- +-
  • +- ))} +-
+- +- ++
++
    ++ {items.map((item, index) => ( ++
  • ++
    onItemSelect?.(item.label)} ++ > ++ {item.label} ++
    ++
  • ++ ))} ++
++
++ Extra Action ++
++
+\`\`\` +`; + + +const searchDiffChunkInstructions = ` +You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file. + +Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it. + +# Example 1: + +FILES +selected file \`Sidebar.tsx\`: +\`\`\` +import React from 'react'; +import styles from './Sidebar.module.css'; + +interface SidebarProps { + items: { label: string; href: string }[]; + onItemSelect?: (label: string) => void; + onExtraButtonClick?: () => void; +} + +const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +
+
    + {items.map((item, index) => ( +
  • + +
  • + ))} +
+ +
+ ); +}; + +export default Sidebar; +\`\`\` + +DIFF +\`\`\` +@@ ... @@ +-
+-
    +- {items.map((item, index) => ( +-
  • +- +-
  • +- ))} +-
+- +-
++
++
    ++ {items.map((item, index) => ( ++
  • ++
    onItemSelect?.(item.label)} ++ > ++ {item.label} ++
    ++
  • ++ ))} ++
++
++ Extra Action ++
++
+\`\`\` + +SELECTION +\`\`\` +import React from 'react'; +import styles from './Sidebar.module.css'; + +interface SidebarProps { + items: { label: string; href: string }[]; + onItemSelect?: (label: string) => void; + onExtraButtonClick?: () => void; +} + +const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +
+
    + {items.map((item, index) => ( +\`\`\` + +RESULT +The output should be \`true\` because the diff begins on the line with \`
    \` and this line is present in the selection. + +OUTPUT +\`true\` +` + + +const writeFileWithDiffInstructions = ` +You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`. + +Please finish writing the new file \`new_file\`, according to the diff \`diff\`. + +Directions: +1. Continue exactly where the new file \`new_file\` left off. +2. Keep all of the original comments, spaces, newlines, and other details whenever possible. +3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change. + +# Example 1: + +ORIGINAL_FILE +\`Sidebar.tsx\`: +\`\`\` +import React from 'react'; +import styles from './Sidebar.module.css'; + +interface SidebarProps { + items: { label: string; href: string }[]; + onItemSelect?: (label: string) => void; + onExtraButtonClick?: () => void; +} + +const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +
    +
      + {items.map((item, index) => ( +
    • + +
    • + ))} +
    + +
    + ); +}; + +export default Sidebar; +\`\`\` + +DIFF +\`\`\` +@@ ... @@ +-
    +-
      +- {items.map((item, index) => ( +-
    • +- +-
    • +- ))} +-
    +- +-
    ++
    ++
      ++ {items.map((item, index) => ( ++
    • ++
      onItemSelect?.(item.label)} ++ > ++ {item.label} ++
      ++
    • ++ ))} ++
    ++
    ++ Extra Action ++
    ++
    +\`\`\` + +NEW_FILE +\`\`\` +import React from 'react'; +import styles from './Sidebar.module.css'; + +interface SidebarProps { + items: { label: string; href: string }[]; + onItemSelect?: (label: string) => void; + onExtraButtonClick?: () => void; +} + +const Sidebar: React.FC = ({ items, onItemSelect, onExtraButtonClick }) => { + return ( +\`\`\` + +COMPLETION +\`\`\` +
    +
      + {items.map((item, index) => ( +
    • +
      onItemSelect?.(item.label)} + > + {item.label} +
      +
    • + ))} +
    +
    + Extra Action +
    +
    + ); +}; + +export default Sidebar;\`\`\` +` + + + +export { + generateDiffInstructions, + searchDiffChunkInstructions, + writeFileWithDiffInstructions, +}; \ No newline at end of file diff --git a/extensions/void/src/extension/DisplayChangesProvider.ts b/extensions/void/src/extension/DisplayChangesProvider.ts index e65593ef..b3a97f74 100644 --- a/extensions/void/src/extension/DisplayChangesProvider.ts +++ b/extensions/void/src/extension/DisplayChangesProvider.ts @@ -150,13 +150,13 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { // compute the diffs const diffs = findDiffs(diffArea.originalCode, currentCode) - // print diffs - console.log('!CODEBefore:', JSON.stringify(diffArea.originalCode)) - console.log('!CODEAfter:', JSON.stringify(currentCode)) - // add the diffs to `this._diffsOfDocument[docUriStr]` this.addDiffs(editor.document.uri, diffs, diffArea) + // // print diffs + console.log('!CodeBefore:', JSON.stringify(diffArea.originalCode)) + console.log('!CodeAfter:', JSON.stringify(currentCode)) + console.log('DiffRepr: ', diffs.map(diff => diff.code).join('\n')) for (const diff of this._diffsOfDocument[docUriStr]) { console.log('------------') console.log('deletedCode:', JSON.stringify(diff.deletedCode)) @@ -165,7 +165,6 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { console.log('insertedRange:', diff.insertedRange.start.line, diff.insertedRange.end.line,) } - } // update green highlighting diff --git a/extensions/void/src/extension/extension.ts b/extensions/void/src/extension/extension.ts index 3f6d3ec2..d8e83b93 100644 --- a/extensions/void/src/extension/extension.ts +++ b/extensions/void/src/extension/extension.ts @@ -4,6 +4,8 @@ import { BaseDiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider'; import { v4 as uuidv4 } from 'uuid' import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider'; +import { getVoidConfig } from '../webviews/common/contextForConfig'; +import { applyDiffLazily } from '../common/ctrlL'; const readFileContentOfUri = async (uri: vscode.Uri) => { return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8') @@ -138,9 +140,19 @@ export function activate(context: vscode.ExtensionContext) { // await vscode.workspace.applyEdit(workspaceEdit) // await vscode.workspace.save(docUri) // this._weAreEditing = false - await editor.edit(editBuilder => { - editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER), m.code); - }); + const fileUri = editor.document.uri + const fileStr = await readFileContentOfUri(fileUri) + const voidConfig = getVoidConfig(context.globalState.get('partialVoidConfig') ?? {}) + + let abort = () => { } // TODO this is unused + + // apply the change + applyDiffLazily({ fileUri, oldFileStr: fileStr, diffStr: m.code, voidConfig, setAbort: (a) => { abort = a } }) + + // set the file equal to the change + // await editor.edit(editBuilder => { + // editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER), m.code); + // }); // rediff the changes based on the diffAreas displayChangesProvider.refreshDiffAreas(editor.document.uri) @@ -196,4 +208,3 @@ export function activate(context: vscode.ExtensionContext) { // ) } - diff --git a/extensions/void/src/extension/findDiffs.ts b/extensions/void/src/extension/findDiffs.ts index 8777d078..bf5fe0e7 100644 --- a/extensions/void/src/extension/findDiffs.ts +++ b/extensions/void/src/extension/findDiffs.ts @@ -1,35 +1,120 @@ import * as vscode from 'vscode'; // import { diffLines, Change } from 'diff'; +import { diff_match_patch } from 'diff-match-patch'; +import { diffLines } from 'diff'; import { BaseDiff } from '../common/shared_types'; -import { diff_match_patch } from 'diff-match-patch'; -const diffLines = (text1: string, text2: string) => { - var dmp = new diff_match_patch(); - var a = dmp.diff_linesToChars_(text1, text2); - var lineText1 = a.chars1; - var lineText2 = a.chars2; - var lineArray = a.lineArray; - var diffs = dmp.diff_main(lineText1, lineText2, false); - dmp.diff_charsToLines_(diffs, lineArray); - // dmp.diff_cleanupSemantic(diffs); - return diffs; -} +// const diffLinesOld = (text1: string, text2: string) => { +// var dmp = new diff_match_patch(); +// var a = dmp.diff_linesToChars_(text1, text2); +// var lineText1 = a.chars1; +// var lineText2 = a.chars2; +// var lineArray = a.lineArray; +// var diffs = dmp.diff_main(lineText1, lineText2, false); +// dmp.diff_charsToLines_(diffs, lineArray); +// // dmp.diff_cleanupSemantic(diffs); +// return diffs; +// } + + +// // TODO use a better diff algorithm +// export const findDiffsOld = (oldText: string, newText: string): BaseDiff[] => { + +// const diffs = diffLinesOld(oldText, newText); + +// const blocks: BaseDiff[] = []; +// let reprBlock: string[] = []; +// let deletedBlock: string[] = []; +// let insertedBlock: string[] = []; +// let insertedLine = 0; +// let deletedLine = 0; +// let insertedStart = 0; +// let deletedStart = 0; + +// diffs.forEach(([operation, text]) => { + +// const lines = text.split('\n'); + +// switch (operation) { + +// // insertion +// case 1: +// if (reprBlock.length === 0) { reprBlock.push('@@@@'); } +// if (insertedBlock.length === 0) insertedStart = insertedLine; +// insertedLine += lines.length - 1; // Update only the line count for new text +// insertedBlock.push(text); +// reprBlock.push(lines.map(line => `+ ${line}`).join('\n')); +// break; + +// // deletion +// case -1: +// if (reprBlock.length === 0) { reprBlock.push('@@@@'); } +// if (deletedBlock.length === 0) deletedStart = deletedLine; +// deletedLine += lines.length - 1; // Update only the line count for old text +// deletedBlock.push(text); +// reprBlock.push(lines.map(line => `- ${line}`).join('\n')); +// break; + +// // no change +// case 0: +// // If we have a pending block, add it to the blocks array +// if (insertedBlock.length > 0 || deletedBlock.length > 0) { +// blocks.push({ +// code: reprBlock.join(''), +// deletedCode: deletedBlock.join(''), +// insertedCode: insertedBlock.join(''), +// deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER), +// insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER), +// }); +// } + +// // Reset the block variables +// reprBlock = []; +// deletedBlock = []; +// insertedBlock = []; + +// // Update line counts for unchanged text +// insertedLine += lines.length - 1; +// deletedLine += lines.length - 1; + +// break; +// } +// }); + +// // Add any remaining blocks after the loop ends +// if (insertedBlock.length > 0 || deletedBlock.length > 0) { +// blocks.push({ +// code: reprBlock.join(''), +// deletedCode: deletedBlock.join(''), +// insertedCode: insertedBlock.join(''), +// deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER), +// insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER), +// }); +// } + +// return blocks; +// }; -// TODO use a better diff algorithm export const findDiffs = (oldText: string, newText: string): BaseDiff[] => { - const diffs = diffLines(oldText, newText); + let diffs = diffLines(oldText, newText) + .map(diff => { + const operation = diff.added ? 1 : diff.removed ? -1 : 0; + const text = diff.value; + return [operation, text] as const; + }) + const blocks: BaseDiff[] = []; let reprBlock: string[] = []; let deletedBlock: string[] = []; let insertedBlock: string[] = []; - let insertedLine = 0; - let deletedLine = 0; + let newFileLine = 0; + let oldFileLine = 0; let insertedStart = 0; let deletedStart = 0; @@ -42,8 +127,8 @@ export const findDiffs = (oldText: string, newText: string): BaseDiff[] => { // insertion case 1: if (reprBlock.length === 0) { reprBlock.push('@@@@'); } - if (insertedBlock.length === 0) insertedStart = insertedLine; - insertedLine += lines.length - 1; // Update only the line count for new text + if (insertedBlock.length === 0) insertedStart = newFileLine; + newFileLine += lines.length - 1; // update the line count for new text insertedBlock.push(text); reprBlock.push(lines.map(line => `+ ${line}`).join('\n')); break; @@ -51,33 +136,33 @@ export const findDiffs = (oldText: string, newText: string): BaseDiff[] => { // deletion case -1: if (reprBlock.length === 0) { reprBlock.push('@@@@'); } - if (deletedBlock.length === 0) deletedStart = deletedLine; - deletedLine += lines.length - 1; // Update only the line count for old text + if (deletedBlock.length === 0) deletedStart = oldFileLine; + oldFileLine += lines.length - 1; // update the line count for old text deletedBlock.push(text); reprBlock.push(lines.map(line => `- ${line}`).join('\n')); break; // no change case 0: - // If we have a pending block, add it to the blocks array + // add pending block to the blocks array if (insertedBlock.length > 0 || deletedBlock.length > 0) { blocks.push({ code: reprBlock.join(''), deletedCode: deletedBlock.join(''), insertedCode: insertedBlock.join(''), - deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER), - insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER), + deletedRange: new vscode.Range(deletedStart, 0, oldFileLine, Number.MAX_SAFE_INTEGER), + insertedRange: new vscode.Range(insertedStart, 0, newFileLine, Number.MAX_SAFE_INTEGER), }); } - // Reset the block variables + // update variables reprBlock = []; deletedBlock = []; insertedBlock = []; - - // Update line counts for unchanged text - insertedLine += lines.length - 1; - deletedLine += lines.length - 1; + deletedStart += lines.length - 1; + insertedStart += lines.length - 1; + newFileLine += lines.length - 1; + oldFileLine += lines.length - 1; break; } @@ -89,189 +174,11 @@ export const findDiffs = (oldText: string, newText: string): BaseDiff[] => { code: reprBlock.join(''), deletedCode: deletedBlock.join(''), insertedCode: insertedBlock.join(''), - deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER), - insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER), + deletedRange: new vscode.Range(deletedStart, 0, oldFileLine, Number.MAX_SAFE_INTEGER), + insertedRange: new vscode.Range(insertedStart, 0, newFileLine, Number.MAX_SAFE_INTEGER), }); } return blocks; }; - - -// export const findDiffs = (oldText: string, newText: string): DiffBlock[] => { - -// const diffs = diffLines(oldText, newText); - -// const blocks: DiffBlock[] = []; - -// let reprBlock: string[] = []; -// let deletedBlock: string[] = []; -// let insertedBlock: string[] = []; - -// let insertedEnd = 0; -// let deletedEnd = 0; -// let insertedStart = 0; -// let deletedStart = 0; - -// diffs.forEach(part => { - -// part.count = part.count ?? 0 - -// // if the part is an addition or deletion, add it to the current block -// if (part.added || part.removed) { -// if (reprBlock.length === 0) { reprBlock.push('@@@@'); } -// if (part.added) { -// if (insertedBlock.length === 0) insertedStart = insertedEnd; -// insertedEnd += part.count -// insertedBlock.push(part.value); -// reprBlock.push(part.value.split('\n').map(line => `+ ${line}`).join('\n')); -// } -// if (part.removed) { -// if (deletedBlock.length === 0) deletedStart = deletedEnd; -// deletedEnd += part.count -// deletedBlock.push(part.value); -// reprBlock.push(part.value.split('\n').map(line => `- ${line}`).join('\n')); -// } -// } - -// // if the part is unchanged, finalize the block and add it to the array -// else { -// // if the block is not null, add it to the array -// if (insertedBlock.length > 0 || deletedBlock.length > 0) { -// blocks.push({ -// code: reprBlock.join('\n'), -// deletedCode: deletedBlock.join(''), -// insertedCode: insertedBlock.join(''), -// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER), -// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER), -// }); -// } - -// // update block variables -// reprBlock = []; -// deletedBlock = []; -// insertedBlock = []; -// insertedEnd += part.count; -// deletedEnd += part.count; - -// } - -// }) - -// // finally, add the last block to the array -// if (insertedBlock.length > 0 || deletedBlock.length > 0) { -// blocks.push({ -// code: reprBlock.join('\n'), -// deletedCode: deletedBlock.join(''), -// insertedCode: insertedBlock.join(''), -// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER), -// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER), -// }); -// } - -// return blocks; - -// } - - - - - - - - - - - -// import { diffLines, Change } from 'diff'; - -// export type SuggestedEdit = { -// // start/end of current file -// startLine: number; -// endLine: number; - -// // start/end of original file -// originalStartLine: number, -// originalEndLine: number, - -// // original content (originalfile[originalStart...originalEnd]) -// originalContent: string; -// newContent: string; -// } - -// export function getDiffedLines(oldStr: string, newStr: string) { -// // an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it) -// const lineByLineChanges: Change[] = diffLines(oldStr, newStr); -// console.debug('Line by line changes', lineByLineChanges) - -// lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed) - -// let oldFileLineNum: number = 0; -// let newFileLineNum: number = 0; - -// let streakStartInNewFile: number | undefined = undefined -// let streakStartInOldFile: number | undefined = undefined - -// let oldStrLines = oldStr.split('\n') -// let newStrLines = newStr.split('\n') - -// const replacements: SuggestedEdit[] = [] - -// for (let line of lineByLineChanges) { -// // no change on this line -// if (!line.added && !line.removed) { -// // if we were on a streak, add it -// if (streakStartInNewFile !== undefined) { - -// const startLine = streakStartInNewFile -// const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it -// const newContent = newStrLines.slice(startLine, endLine + 1).join('\n') - -// const originalStartLine = streakStartInOldFile! -// const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it -// const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n') - -// const replacement: SuggestedEdit = { startLine, endLine, newContent, originalStartLine, originalEndLine, originalContent } - -// replacements.push(replacement) -// streakStartInNewFile = undefined -// streakStartInOldFile = undefined -// } - -// oldFileLineNum += line.count ?? 0; -// newFileLineNum += line.count ?? 0; -// } - - -// // line was removed from old file -// else if (line.removed) { - -// // if we weren't on a streak, start one on this current line num -// if (streakStartInNewFile === undefined) { -// streakStartInNewFile = newFileLineNum -// streakStartInOldFile = oldFileLineNum -// } - -// oldFileLineNum += line.count ?? 0 // we processed the line so add 1 -// } - -// // line was added to new file -// else if (line.added) { - -// // if we weren't on a streak, start one on this current line num -// if (streakStartInNewFile === undefined) { -// streakStartInNewFile = newFileLineNum -// streakStartInOldFile = oldFileLineNum -// } - -// newFileLineNum += line.count ?? 0; // we processed the line so add 1 -// } - -// } // end for - -// console.debug('Replacements', replacements) - -// return replacements - -// } \ No newline at end of file diff --git a/extensions/void/src/webviews/common/contextForConfig.tsx b/extensions/void/src/webviews/common/contextForConfig.tsx index c933e341..e5b5d6b9 100644 --- a/extensions/void/src/webviews/common/contextForConfig.tsx +++ b/extensions/void/src/webviews/common/contextForConfig.tsx @@ -45,6 +45,18 @@ const voidConfigInfo: Record< 'anthropic', configFields, ), + + maxTokens: configEnum( + "Max number of tokens to output.", + '1024', + [ + "1024", + "2048", + "4096", + "8192" + ] as const, + ), + }, anthropic: { apikey: configString('Anthropic API key.', ''), @@ -58,17 +70,6 @@ const voidConfigInfo: Record< "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.', ''), @@ -184,7 +185,7 @@ export type VoidConfig = { -const getVoidConfig = (currentConfig: PartialVoidConfig): VoidConfig => { +export const getVoidConfig = (currentConfig: PartialVoidConfig): VoidConfig => { const config = {} as PartialVoidConfig for (let field of [...configFields, 'default'] as const) { config[field] = {} diff --git a/extensions/void/src/webviews/common/contextForThreads.tsx b/extensions/void/src/webviews/common/contextForThreads.tsx index 457116dd..d2ab97be 100644 --- a/extensions/void/src/webviews/common/contextForThreads.tsx +++ b/extensions/void/src/webviews/common/contextForThreads.tsx @@ -5,8 +5,8 @@ import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi" // a "thread" means a chat message history type ConfigForThreadsValueType = { - readonly allThreads: ChatThreads | null, - readonly currentThread: ChatThreads[string] | null; + readonly getAllThreads: () => ChatThreads; + readonly getCurrentThread: () => ChatThreads[string] | null; addMessageToHistory: (message: ChatMessage) => void; switchToThread: (threadId: string) => void; startNewThread: () => void; @@ -39,8 +39,8 @@ const useInstantState = (initVal: T) => { export function ThreadsProvider({ children }: { children: ReactNode }) { - const [allThreads, setAllThreads] = useInstantState({}) - const [currentThreadId, setCurrentThreadId] = useInstantState(null) + const [allThreadsRef, setAllThreads] = useInstantState({}) + const [currentThreadIdRef, setCurrentThreadId] = useInstantState(null) // this loads allThreads in on mount useEffect(() => { @@ -55,12 +55,12 @@ export function ThreadsProvider({ children }: { children: ReactNode }) { return ( allThreadsRef.current ?? {}, + getCurrentThread: () => currentThreadIdRef.current ? allThreadsRef.current?.[currentThreadIdRef.current] ?? null : null, addMessageToHistory: (message: ChatMessage) => { let currentThread: ChatThreads[string] - if (!(currentThreadId.current === null || allThreads.current === null)) { - currentThread = allThreads.current[currentThreadId.current] + if (!(currentThreadIdRef.current === null || allThreadsRef.current === null)) { + currentThread = allThreadsRef.current[currentThreadIdRef.current] } else { currentThread = createNewThread() @@ -68,7 +68,7 @@ export function ThreadsProvider({ children }: { children: ReactNode }) { } setAllThreads({ - ...allThreads.current, + ...allThreadsRef.current, [currentThread.id]: { ...currentThread, lastModified: new Date().toISOString(), @@ -84,7 +84,7 @@ export function ThreadsProvider({ children }: { children: ReactNode }) { startNewThread: () => { const newThread = createNewThread() setAllThreads({ - ...allThreads.current, + ...allThreadsRef.current, [newThread.id]: newThread }) setCurrentThreadId(newThread.id) diff --git a/extensions/void/src/webviews/sidebar/Sidebar.tsx b/extensions/void/src/webviews/sidebar/Sidebar.tsx index bd482c0e..70bf1c3b 100644 --- a/extensions/void/src/webviews/sidebar/Sidebar.tsx +++ b/extensions/void/src/webviews/sidebar/Sidebar.tsx @@ -60,7 +60,6 @@ const Sidebar = () => {
- } diff --git a/extensions/void/src/webviews/sidebar/SidebarChat.tsx b/extensions/void/src/webviews/sidebar/SidebarChat.tsx index 11f14612..98cca33e 100644 --- a/extensions/void/src/webviews/sidebar/SidebarChat.tsx +++ b/extensions/void/src/webviews/sidebar/SidebarChat.tsx @@ -11,6 +11,7 @@ import { useThreads } from "../common/contextForThreads"; import { sendLLMMessage } from "../../common/sendLLMMessage"; import { useVoidConfig } from "../common/contextForConfig"; import { captureEvent } from "../common/posthog"; +import { generateDiffInstructions } from "../../common/systemPrompts"; @@ -160,7 +161,8 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject { const whichApi = voidConfig.default['whichApi'] - const messages = currentThread?.messages + const messages = getCurrentThread()?.messages captureEvent(eventId, { whichApi: whichApi, numMessages: messages?.length, - messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.displayContent.length })), + messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.displayContent?.length })), version: '2024-10-19', ...extras, }) - }, [currentThread?.messages, voidConfig.default]) + }, [getCurrentThread, voidConfig.default]) // if they pressed the + to add a new chat useOnVSCodeMessage('startNewThread', (m) => { + const allThreads = getAllThreads() // find a thread with 0 messages and switch to it for (let threadId in allThreads) { if (allThreads[threadId].messages.length === 0) { @@ -224,19 +227,20 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject ({ role: m.role, content: m.content })), { role: 'user', content: userContent }], + // send message to LLM + sendLLMMessage({ + messages: [...(getCurrentThread()?.messages ?? []).map(m => ({ role: m.role, content: m.content })),], onText: (newText, fullText) => setMessageStream(fullText), onFinalMessage: (content) => { captureChatEvent('Chat - Received Full Message', { messageLength: content.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() }) @@ -259,9 +263,12 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject { + abortFnRef.current = abort + }, + voidConfig, }) - abortFnRef.current = abort + } @@ -286,7 +293,7 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject
{/* previous messages */} - {currentThread !== null && currentThread.messages.map((message, i) => + {getCurrentThread() !== null && getCurrentThread()?.messages.map((message, i) => )} {/* message stream */} @@ -296,7 +303,6 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject {/* selection */}
-
{/* selection */} diff --git a/extensions/void/src/webviews/sidebar/SidebarSettings.tsx b/extensions/void/src/webviews/sidebar/SidebarSettings.tsx index 750756d6..f144fd11 100644 --- a/extensions/void/src/webviews/sidebar/SidebarSettings.tsx +++ b/extensions/void/src/webviews/sidebar/SidebarSettings.tsx @@ -69,6 +69,10 @@ export const SidebarSettings = () => { field='default' param='whichApi' /> +

diff --git a/extensions/void/src/webviews/sidebar/SidebarThreadSelector.tsx b/extensions/void/src/webviews/sidebar/SidebarThreadSelector.tsx index efc96ede..3d785d1b 100644 --- a/extensions/void/src/webviews/sidebar/SidebarThreadSelector.tsx +++ b/extensions/void/src/webviews/sidebar/SidebarThreadSelector.tsx @@ -12,7 +12,9 @@ const truncate = (s: string) => { export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => { - const { allThreads, currentThread, switchToThread } = useThreads() + const { getAllThreads, getCurrentThread, switchToThread } = useThreads() + + const allThreads = getAllThreads() // sorted by most recent to least recent const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? 1 : -1) @@ -62,7 +64,7 @@ export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => { return (