From 7ce17a0854a5bbc2db91f822d071c6ec154c70a9 Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 18 Oct 2024 17:19:38 -0700 Subject: [PATCH 1/7] Initial draft for speculative edits --- extensions/void/src/common/ctrlL.ts | 676 ++++++++++++++++++ extensions/void/src/common/sendLLMMessage.ts | 62 +- extensions/void/src/extension.ts | 19 +- extensions/void/src/sidebar/SidebarChat.tsx | 9 +- .../void/src/sidebar/SidebarSettings.tsx | 4 + .../void/src/sidebar/contextForConfig.tsx | 25 +- 6 files changed, 745 insertions(+), 50 deletions(-) create mode 100644 extensions/void/src/common/ctrlL.ts diff --git a/extensions/void/src/common/ctrlL.ts b/extensions/void/src/common/ctrlL.ts new file mode 100644 index 00000000..7f2bd3e5 --- /dev/null +++ b/extensions/void/src/common/ctrlL.ts @@ -0,0 +1,676 @@ +import * as vscode from 'vscode'; +import { OnFinalMessage, OnText, sendLLMMessage, SetAbort } from "./sendLLMMessage" +import { VoidConfig } from '../sidebar/contextForConfig'; + +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 +Following the instructions, we should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`. Here is the expected output diff: +\`\`\` +@@ ... @@ +-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) => ( +\`\`\` + +EXPECTED RESULT +The expected output is \`true\`, because the diff begins on the line with \`
    \` and this line is present in the selection. + +\`true\` +` + + +const searchDiffLineInstructions = ` +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) => ( +\`\`\` + +EXPECTED RESULT +The expected output is \`true\`, because the diff begins on the line with \`
      \` and this line is present in the selection. + +\`true\` +` + + + +const rewriteFileWithDiffInstructions = ` +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 in the diff \`diff\`, \`+\` 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 ( +\`\`\` + +EXPECTED RESULT +The expected output should complete the new file \`new_file\`, following the diff \`diff\`. Here is the expected output: +\`\`\` +
      +
        + {items.map((item, index) => ( +
      • +
        onItemSelect?.(item.label)} + > + {item.label} +
        +
      • + ))} +
      +
      + Extra Action +
      +
      + ); +}; + +export default Sidebar; +\`\`\` +` + + +type Res = ((value: T) => void) + + +const rewriteFileWithDiff = ({ fileUri, originalFileStr, newFileStr, diff, voidConfig, onText, setAbort }: { fileUri: vscode.Uri, originalFileStr: string, newFileStr: string, diff: string, voidConfig: VoidConfig, onText: OnText, setAbort: SetAbort }) => { + + const EXTRA_TOKENS = 20 + + const promptContent = `ORIGINAL_FILE +\`\`\` +${originalFileStr} +\`\`\` + +DIFF +\`\`\` +${diff} +\`\`\` + +INSTRUCTIONS +Please finish writing the new file \`NEW_FILE\`. When + +NEW_FILE +\`\`\` +${newFileStr} +\`\`\` +` + // create a promise that can be awaited + let res: Res = () => { } + const promise = new Promise((resolve, reject) => { res = resolve }) + + + // make LLM rewrite file to include the diff + sendLLMMessage({ + messages: [{ role: 'assistant', content: rewriteFileWithDiffInstructions, }, { role: 'assistant', content: promptContent, }], + onText, + onFinalMessage: (finalMessage) => { res(finalMessage) }, + onError: () => { res(''); console.error('Error rewriting file with diff') }, + voidConfig: { + ...voidConfig, + default: { + // set `max_tokens` = (number of expected tokens) + (number of extra tokens) + maxTokens: Math.round((diff.split('\n').filter(l => !l.startsWith('-')).length) + EXTRA_TOKENS) + '' + } + }, + setAbort, + }) + + + return promise + +} + +const shouldApplyDiffToLine = async ({ diff, fileStr, lineStr, voidConfig, setAbort }: { diff: string, fileStr: string, lineStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { + + const promptContent = `DIFF +\`\`\` +${diff} +\`\`\` + +FILES +\`\`\` +${fileStr} +\`\`\` + +SELECTION +\`\`\`${lineStr}\`\`\` + +Return \`true\` if this line should be modified, and \`false\` if it should not be modified. +` + + // create new promise + let res: Res = () => { } + const promise = new Promise((resolve, reject) => { res = resolve }) + + sendLLMMessage({ + messages: [{ role: 'assistant', content: searchDiffLineInstructions, }, { role: 'assistant', content: promptContent, }], + onText: () => { }, + onFinalMessage: (finalMessage) => { + const containsTrue = finalMessage + .slice(-10) + .toLowerCase() + .includes('true') + res(containsTrue) + }, + onError: () => { + res(false); + console.error('Error applying diff to line') + }, + voidConfig, + setAbort + }) + + 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, fileStr, diff, voidConfig, setAbort }: { fileUri: vscode.Uri, fileStr: string, diff: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { + + const CHUNK_SIZE = 20 // number of lines to search at a time + + // read file content + const fileLines = fileStr.split('\n') + const completedLines = [] + + // search the file chunk-by-chunk + for (let chunkIdx = 0; chunkIdx * CHUNK_SIZE < fileLines.length; chunkIdx++) { + + // get the chunk + const chunkStart = chunkIdx * CHUNK_SIZE + const chunkEnd = (chunkIdx + 1) * CHUNK_SIZE + const chunkLines = fileLines.slice(chunkStart, chunkEnd) + const chunkStr = chunkLines.join('\n'); + + // ask LLM if we should apply the diff to the chunk + let shouldApplyDiff = await shouldApplyDiffToChunk({ chunkStr, diff, fileUri, setAbort }) + if (!shouldApplyDiff) { // should not change the chunk + completedLines.push(chunkStr); + // TODO update highlighting here + continue; + } + + // search the chunk line-by-line + for (const lineStr of chunkLines) { + + // ask LLM if we should apply the diff to the line + let shouldApplyDiff = await shouldApplyDiffToLine({ diff, fileStr, lineStr, voidConfig, setAbort }) + if (!shouldApplyDiff) { // should not change the line + completedLines.push(lineStr); + // TODO update highlighting here + continue; + } + + // ask LLM to apply the diff + const changeStr = await rewriteFileWithDiff({ // rewrite file with diff (if there is significant matchup with the original file, we stop rewriting) + originalFileStr: fileStr, + newFileStr: completedLines.join('\n'), + diff, + fileUri, + voidConfig, + onText: async (text) => { + // TODO! update highlighting here + // also make edits here + + }, + setAbort, + }) + completedLines.push(changeStr) + + + // if there's matchup with the file, we stop rewriting + // TODO! otherwise keep rewriting until there is matchup + + } + + } + +} + + + +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 a0ab0df3..f107c29b 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -6,7 +6,11 @@ import { VoidConfig } from '../sidebar/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 LLMMessage = { role: 'user' | 'assistant', @@ -16,13 +20,11 @@ export type LLMMessage = { 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,22 +32,22 @@ 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"] const stream = anthropic.messages.stream({ model: voidConfig.anthropic.model, - max_tokens: parseInt(voidConfig.anthropic.maxTokens), + max_tokens: parseInt(voidConfig.default.maxTokens), messages: messages, }); @@ -80,8 +82,7 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi // stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error did_abort = true } - - return { abort } + setAbort(abort) }; @@ -89,7 +90,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 +105,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 +115,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 +157,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 +178,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 +200,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onError(error) }) - return { abort }; + setAbort(abort); }; @@ -207,7 +209,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 +228,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 +270,26 @@ 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; 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/extension.ts b/extensions/void/src/extension.ts index 46e4266d..9f1ff5ec 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -3,6 +3,8 @@ import { DisplayChangesProvider } from './DisplayChangesProvider'; import { BaseDiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './shared_types'; import { SidebarWebviewProvider } from './SidebarWebviewProvider'; import { v4 as uuidv4 } from 'uuid' +import { applyDiffLazily } from './common/ctrlL'; +import { getVoidConfig } from './sidebar/contextForConfig'; const readFileContentOfUri = async (uri: vscode.Uri) => { return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8') @@ -108,9 +110,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, fileStr, diff: 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) @@ -166,4 +178,3 @@ export function activate(context: vscode.ExtensionContext) { // ) } - diff --git a/extensions/void/src/sidebar/SidebarChat.tsx b/extensions/void/src/sidebar/SidebarChat.tsx index 01014564..3e9aaa1b 100644 --- a/extensions/void/src/sidebar/SidebarChat.tsx +++ b/extensions/void/src/sidebar/SidebarChat.tsx @@ -163,7 +163,7 @@ export const SidebarChat = () => { addMessageToHistory(newHistoryElt) // send message to LLM - let { abort } = sendLLMMessage({ + sendLLMMessage({ messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content: userContent }], onText: (newText, fullText) => setMessageStream(fullText), onFinalMessage: (content) => { @@ -183,9 +183,12 @@ export const SidebarChat = () => { setLatestError(error) }, - voidConfig: voidConfig + setAbort: (abort) => { + abortFnRef.current = abort + }, + voidConfig, }) - abortFnRef.current = abort + } diff --git a/extensions/void/src/sidebar/SidebarSettings.tsx b/extensions/void/src/sidebar/SidebarSettings.tsx index 450f965e..39115a80 100644 --- a/extensions/void/src/sidebar/SidebarSettings.tsx +++ b/extensions/void/src/sidebar/SidebarSettings.tsx @@ -64,6 +64,10 @@ export const SidebarSettings = () => { field='default' param='whichApi' /> +

      diff --git a/extensions/void/src/sidebar/contextForConfig.tsx b/extensions/void/src/sidebar/contextForConfig.tsx index 0b0e569a..980e9678 100644 --- a/extensions/void/src/sidebar/contextForConfig.tsx +++ b/extensions/void/src/sidebar/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.', ''), @@ -270,7 +271,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] = {} From f6cad40a4498ed6f101f835b4f130df200184428 Mon Sep 17 00:00:00 2001 From: mp Date: Fri, 18 Oct 2024 19:18:11 -0700 Subject: [PATCH 2/7] Finish speculative draft --- extensions/void/package-lock.json | 9 +++--- extensions/void/package.json | 2 +- extensions/void/src/common/ctrlL.ts | 45 +++++++++++++++++++++++------ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index a7f8924e..b0076339 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -37,7 +37,7 @@ "marked": "^14.1.0", "ollama": "^0.5.9", "postcss": "^8.4.41", - "posthog-js": "^1.174.0", + "posthog-js": "^1.174.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", @@ -6345,11 +6345,10 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.174.0", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.174.0.tgz", - "integrity": "sha512-60qCn8bloCxVc3oBQ/JP77J40J7UD+cRGUfYXJdsqjUH82s2wmCx4MicuNrcn9Hd2dHM25nXmOAMLO5iwSq9gg==", + "version": "1.174.2", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.174.2.tgz", + "integrity": "sha512-UgS7eRcDVvVz2XSJ09NMX8zBcdpFnPayfiWDNF3xEbJTsIu1GipkkYNrVlsWlq8U1PIrviNm6i0Dyq8daaxssw==", "dev": true, - "license": "MIT", "dependencies": { "core-js": "^3.38.1", "fflate": "^0.4.8", diff --git a/extensions/void/package.json b/extensions/void/package.json index 32d9b22e..5ce9928b 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -133,7 +133,7 @@ "marked": "^14.1.0", "ollama": "^0.5.9", "postcss": "^8.4.41", - "posthog-js": "^1.174.0", + "posthog-js": "^1.174.2", "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 index 7f2bd3e5..60649218 100644 --- a/extensions/void/src/common/ctrlL.ts +++ b/extensions/void/src/common/ctrlL.ts @@ -549,7 +549,7 @@ ${newFileStr} voidConfig: { ...voidConfig, default: { - // set `max_tokens` = (number of expected tokens) + (number of extra tokens) + // set `maxTokens` = (number of expected tokens) + (number of extra tokens) maxTokens: Math.round((diff.split('\n').filter(l => !l.startsWith('-')).length) + EXTRA_TOKENS) + '' } }, @@ -561,9 +561,11 @@ ${newFileStr} } -const shouldApplyDiffToLine = async ({ diff, fileStr, lineStr, voidConfig, setAbort }: { diff: string, fileStr: string, lineStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { +const shouldApplyDiffFn = async ({ diff, fileStr, speculationStr, type, voidConfig, setAbort }: { diff: string, fileStr: string, speculationStr: string, type: 'line' | 'chunk', voidConfig: VoidConfig, setAbort: SetAbort }) => { - const promptContent = `DIFF + const promptContent = ( + // the speculation is a line + type === 'line' ? `DIFF \`\`\` ${diff} \`\`\` @@ -574,21 +576,46 @@ ${fileStr} \`\`\` SELECTION -\`\`\`${lineStr}\`\`\` +\`\`\`${speculationStr}\`\`\` Return \`true\` if this line should be modified, and \`false\` if it should not be modified. ` + // the speculation is a chunk + : `DIFF +\`\`\` +${diff} +\`\`\` + +FILES +\`\`\` +${fileStr} +\`\`\` + +SELECTION +\`\`\` +${speculationStr} +\`\`\` + +Return \`true\` if this any part of the chunk should be modified, and \`false\` if it should not be modified. +`) // create new promise let res: Res = () => { } const promise = new Promise((resolve, reject) => { res = resolve }) sendLLMMessage({ - messages: [{ role: 'assistant', content: searchDiffLineInstructions, }, { role: 'assistant', content: promptContent, }], + messages: [ + { + role: 'assistant', + content: type === 'line' ? searchDiffLineInstructions : searchDiffChunkInstructions, + }, { + role: 'assistant', + content: promptContent, + }], onText: () => { }, onFinalMessage: (finalMessage) => { const containsTrue = finalMessage - .slice(-10) + .slice(-10) // check for `true` in last 10 characters .toLowerCase() .includes('true') res(containsTrue) @@ -598,7 +625,7 @@ Return \`true\` if this line should be modified, and \`false\` if it should not console.error('Error applying diff to line') }, voidConfig, - setAbort + setAbort, }) return promise @@ -627,7 +654,7 @@ const applyDiffLazily = async ({ fileUri, fileStr, diff, voidConfig, setAbort }: const chunkStr = chunkLines.join('\n'); // ask LLM if we should apply the diff to the chunk - let shouldApplyDiff = await shouldApplyDiffToChunk({ chunkStr, diff, fileUri, setAbort }) + let shouldApplyDiff = await shouldApplyDiffFn({ speculationStr: chunkStr, type: 'chunk', diff, fileStr, voidConfig, setAbort }) if (!shouldApplyDiff) { // should not change the chunk completedLines.push(chunkStr); // TODO update highlighting here @@ -638,7 +665,7 @@ const applyDiffLazily = async ({ fileUri, fileStr, diff, voidConfig, setAbort }: for (const lineStr of chunkLines) { // ask LLM if we should apply the diff to the line - let shouldApplyDiff = await shouldApplyDiffToLine({ diff, fileStr, lineStr, voidConfig, setAbort }) + let shouldApplyDiff = await shouldApplyDiffFn({ speculationStr: lineStr, type: 'line', diff, fileStr, voidConfig, setAbort }) if (!shouldApplyDiff) { // should not change the line completedLines.push(lineStr); // TODO update highlighting here From 9ecf596cbba6a27822956d6fa5d33410051f09df Mon Sep 17 00:00:00 2001 From: mp Date: Sat, 19 Oct 2024 20:17:54 -0700 Subject: [PATCH 3/7] Prepare to debug --- extensions/void/src/common/ctrlL.ts | 14 +++++--------- extensions/void/src/common/sendLLMMessage.ts | 3 +++ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/extensions/void/src/common/ctrlL.ts b/extensions/void/src/common/ctrlL.ts index 60649218..6edc0839 100644 --- a/extensions/void/src/common/ctrlL.ts +++ b/extensions/void/src/common/ctrlL.ts @@ -545,7 +545,7 @@ ${newFileStr} messages: [{ role: 'assistant', content: rewriteFileWithDiffInstructions, }, { role: 'assistant', content: promptContent, }], onText, onFinalMessage: (finalMessage) => { res(finalMessage) }, - onError: () => { res(''); console.error('Error rewriting file with diff') }, + onError: (e) => { res(''); console.error('Error rewriting file with diff', e) }, voidConfig: { ...voidConfig, default: { @@ -561,7 +561,7 @@ ${newFileStr} } -const shouldApplyDiffFn = async ({ diff, fileStr, speculationStr, type, voidConfig, setAbort }: { diff: string, fileStr: string, speculationStr: string, type: 'line' | 'chunk', voidConfig: VoidConfig, setAbort: SetAbort }) => { +const shouldApplyDiffFn = ({ diff, fileStr, speculationStr, type, voidConfig, setAbort }: { diff: string, fileStr: string, speculationStr: string, type: 'line' | 'chunk', voidConfig: VoidConfig, setAbort: SetAbort }) => { const promptContent = ( // the speculation is a line @@ -620,9 +620,9 @@ Return \`true\` if this any part of the chunk should be modified, and \`false\` .includes('true') res(containsTrue) }, - onError: () => { + onError: (e) => { res(false); - console.error('Error applying diff to line') + console.error('Error applying diff to line: ', e) }, voidConfig, setAbort, @@ -679,7 +679,7 @@ const applyDiffLazily = async ({ fileUri, fileStr, diff, voidConfig, setAbort }: diff, fileUri, voidConfig, - onText: async (text) => { + onText: async (newText, fullText) => { // TODO! update highlighting here // also make edits here @@ -688,10 +688,6 @@ const applyDiffLazily = async ({ fileUri, fileStr, diff, voidConfig, setAbort }: }) completedLines.push(changeStr) - - // if there's matchup with the file, we stop rewriting - // TODO! otherwise keep rewriting until there is matchup - } } diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index f107c29b..2a29369f 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -278,6 +278,9 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin 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, setAbort }); From 7bd9e01a15491e07d6fe23de3b8cc4830f5fbfcf Mon Sep 17 00:00:00 2001 From: mp Date: Mon, 21 Oct 2024 18:33:00 -0700 Subject: [PATCH 4/7] Working draft of speculative edits --- extensions/void/package-lock.json | 39 +- extensions/void/package.json | 6 +- extensions/void/src/DisplayChangesProvider.ts | 23 +- extensions/void/src/common/ctrlL.ts | 710 +++--------------- extensions/void/src/common/sendLLMMessage.ts | 23 +- extensions/void/src/common/systemPrompts.ts | 406 ++++++++++ extensions/void/src/extension.ts | 2 +- extensions/void/src/shared_types.ts | 11 +- extensions/void/src/sidebar/Sidebar.tsx | 1 - extensions/void/src/sidebar/SidebarChat.tsx | 16 +- .../src/sidebar/SidebarThreadSelector.tsx | 6 +- .../void/src/sidebar/contextForThreads.tsx | 20 +- 12 files changed, 612 insertions(+), 651 deletions(-) create mode 100644 extensions/void/src/common/systemPrompts.ts diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index b0076339..7fdfd06f 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -9,7 +9,10 @@ "version": "0.0.1", "dependencies": { "@anthropic-ai/sdk": "^0.27.1", - "openai": "^4.57.0" + "@rrweb/types": "^2.0.0-alpha.17", + "openai": "^4.57.0", + "posthog-js": "^1.174.2", + "rrweb-snapshot": "^2.0.0-alpha.4" }, "devDependencies": { "@eslint/js": "^9.9.1", @@ -37,7 +40,6 @@ "marked": "^14.1.0", "ollama": "^0.5.9", "postcss": "^8.4.41", - "posthog-js": "^1.174.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", @@ -605,6 +607,14 @@ "node": ">=14" } }, + "node_modules/@rrweb/types": { + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.0.0-alpha.17.tgz", + "integrity": "sha512-AfDTVUuCyCaIG0lTSqYtrZqJX39ZEYzs4fYKnexhQ+id+kbZIpIJtaut5cto6dWZbB3SEe4fW0o90Po3LvTmfg==", + "dependencies": { + "rrweb-snapshot": "^2.0.0-alpha.17" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1967,9 +1977,7 @@ "version": "3.38.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz", "integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==", - "dev": true, "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -2915,9 +2923,7 @@ "node_modules/fflate": { "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" + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==" }, "node_modules/file-entry-cache": { "version": "6.0.1", @@ -5554,7 +5560,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -6117,7 +6122,6 @@ "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" }, "node_modules/picomatch": { @@ -6167,7 +6171,6 @@ "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6348,7 +6351,6 @@ "version": "1.174.2", "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.174.2.tgz", "integrity": "sha512-UgS7eRcDVvVz2XSJ09NMX8zBcdpFnPayfiWDNF3xEbJTsIu1GipkkYNrVlsWlq8U1PIrviNm6i0Dyq8daaxssw==", - "dev": true, "dependencies": { "core-js": "^3.38.1", "fflate": "^0.4.8", @@ -6360,8 +6362,6 @@ "version": "10.24.3", "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" @@ -6995,6 +6995,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrweb-snapshot": { + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.17.tgz", + "integrity": "sha512-GBg5pV8LHOTbeVmH2VHLEFR0mc2QpQMzAvcoxEGfPNWgWHc8UvKCyq7pqN1vA+fDZ+yXXbixeO0kB2pzVvFCBw==", + "dependencies": { + "postcss": "^8.4.38" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7213,7 +7221,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8159,9 +8166,7 @@ "node_modules/web-vitals": { "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" + "integrity": "sha512-/CFAm1mNxSmOj6i0Co+iGFJ58OS4NRGVP+AWS/l509uIK5a1bSoIVaHz/ZumpHTfHSZBpgrJ+wjfpAOrTHok5Q==" }, "node_modules/webidl-conversions": { "version": "3.0.1", diff --git a/extensions/void/package.json b/extensions/void/package.json index 5ce9928b..96af35ef 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -133,7 +133,6 @@ "marked": "^14.1.0", "ollama": "^0.5.9", "postcss": "^8.4.41", - "posthog-js": "^1.174.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^9.0.1", @@ -146,6 +145,9 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.27.1", - "openai": "^4.57.0" + "@rrweb/types": "^2.0.0-alpha.17", + "openai": "^4.57.0", + "posthog-js": "^1.174.2", + "rrweb-snapshot": "^2.0.0-alpha.4" } } diff --git a/extensions/void/src/DisplayChangesProvider.ts b/extensions/void/src/DisplayChangesProvider.ts index 9394b410..338b44ed 100644 --- a/extensions/void/src/DisplayChangesProvider.ts +++ b/extensions/void/src/DisplayChangesProvider.ts @@ -138,6 +138,8 @@ export class DisplayChangesProvider implements vscode.CodeLensProvider { const docUriStr = docUri.toString() const diffAreas = this._diffAreasOfDocument[docUriStr] || [] + console.log('DIFF AREAS', diffAreas) + // reset all diffs (we update them below) this._diffsOfDocument[docUriStr] = [] @@ -150,20 +152,19 @@ 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) - for (const diff of this._diffsOfDocument[docUriStr]) { - console.log('------------') - console.log('deletedCode:', JSON.stringify(diff.deletedCode)) - console.log('insertedCode:', JSON.stringify(diff.insertedCode)) - console.log('deletedRange:', diff.deletedRange.start.line, diff.deletedRange.end.line,) - console.log('insertedRange:', diff.insertedRange.start.line, diff.insertedRange.end.line,) - } + // // print diffs + // console.log('!CodeBefore:', JSON.stringify(diffArea.originalCode)) + // console.log('!CodeAfter:', JSON.stringify(currentCode)) + // for (const diff of this._diffsOfDocument[docUriStr]) { + // console.log('------------') + // console.log('deletedCode:', JSON.stringify(diff.deletedCode)) + // console.log('insertedCode:', JSON.stringify(diff.insertedCode)) + // console.log('deletedRange:', diff.deletedRange.start.line, diff.deletedRange.end.line,) + // console.log('insertedRange:', diff.insertedRange.start.line, diff.insertedRange.end.line,) + // } } diff --git a/extensions/void/src/common/ctrlL.ts b/extensions/void/src/common/ctrlL.ts index 6edc0839..7faaa1e8 100644 --- a/extensions/void/src/common/ctrlL.ts +++ b/extensions/void/src/common/ctrlL.ts @@ -1,521 +1,16 @@ import * as vscode from 'vscode'; import { OnFinalMessage, OnText, sendLLMMessage, SetAbort } from "./sendLLMMessage" import { VoidConfig } from '../sidebar/contextForConfig'; - -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 -Following the instructions, we should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`. Here is the expected output diff: -\`\`\` -@@ ... @@ --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) => ( -\`\`\` - -EXPECTED RESULT -The expected output is \`true\`, because the diff begins on the line with \`
      \` and this line is present in the selection. - -\`true\` -` - - -const searchDiffLineInstructions = ` -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) => ( -\`\`\` - -EXPECTED RESULT -The expected output is \`true\`, because the diff begins on the line with \`
        \` and this line is present in the selection. - -\`true\` -` - - - -const rewriteFileWithDiffInstructions = ` -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 in the diff \`diff\`, \`+\` 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 ( -\`\`\` - -EXPECTED RESULT -The expected output should complete the new file \`new_file\`, following the diff \`diff\`. Here is the expected output: -\`\`\` -
        -
          - {items.map((item, index) => ( -
        • -
          onItemSelect?.(item.label)} - > - {item.label} -
          -
        • - ))} -
        -
        - Extra Action -
        -
        - ); -}; - -export default Sidebar; -\`\`\` -` - +import { findDiffs } from '../findDiffs'; +import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from './systemPrompts'; type Res = ((value: T) => void) +const writeFileWithDiffUntilMatchup = ({ fileUri, originalFileStr, unfinishedFileStr, diffStr, voidConfig, setAbort }: { fileUri: vscode.Uri, originalFileStr: string, unfinishedFileStr: string, diffStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { -const rewriteFileWithDiff = ({ fileUri, originalFileStr, newFileStr, diff, voidConfig, onText, setAbort }: { fileUri: vscode.Uri, originalFileStr: string, newFileStr: string, diff: string, voidConfig: VoidConfig, onText: OnText, setAbort: SetAbort }) => { + console.log('WRITE FILE') - const EXTRA_TOKENS = 20 + const NUM_MATCHUP_TOKENS = 20 const promptContent = `ORIGINAL_FILE \`\`\` @@ -524,66 +19,101 @@ ${originalFileStr} DIFF \`\`\` -${diff} +${diffStr} \`\`\` INSTRUCTIONS -Please finish writing the new file \`NEW_FILE\`. When +Please finish writing the new file \`NEW_FILE\`. Return ONLY the completion of the file, without any explanation. NEW_FILE \`\`\` -${newFileStr} +${unfinishedFileStr} \`\`\` ` // create a promise that can be awaited - let res: Res = () => { } - const promise = new Promise((resolve, reject) => { res = resolve }) + let res: Res<{ deltaStr: string, matchupLine: number | undefined }> = () => { } + const promise = new Promise<{ deltaStr: string, matchupLine: number | undefined }>((resolve, reject) => { res = resolve }) + // get the abort method + let _abort = () => { } - // make LLM rewrite file to include the diff + // make LLM complete the file to include the diff sendLLMMessage({ - messages: [{ role: 'assistant', content: rewriteFileWithDiffInstructions, }, { role: 'assistant', content: promptContent, }], - onText, - onFinalMessage: (finalMessage) => { res(finalMessage) }, - onError: (e) => { res(''); console.error('Error rewriting file with diff', e) }, - voidConfig: { - ...voidConfig, - default: { - // set `maxTokens` = (number of expected tokens) + (number of extra tokens) - maxTokens: Math.round((diff.split('\n').filter(l => !l.startsWith('-')).length) + EXTRA_TOKENS) + '' - } - }, - setAbort, - }) + messages: [{ role: 'system', content: writeFileWithDiffInstructions, }, { role: 'user', content: promptContent, }], + onText: (tokenStr, deltaStr) => { + const newFileStr = unfinishedFileStr + deltaStr + + // 1. Apply the edit and modify highlighting + + console.log('EDIT START') + + const workspaceEdit = new vscode.WorkspaceEdit() + workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), newFileStr) + vscode.workspace.applyEdit(workspaceEdit) + + // 2. Check for matchup with original file + + // diff `originalFileStr` and `newFileStr` + const diffs = findDiffs(originalFileStr, newFileStr) + const lastDiff = diffs[diffs.length - 1] + const oldLineAfterLastDiff = lastDiff.deletedRange.end.line + 1 + const newLineAfterLastDiff = lastDiff.insertedRange.end.line + 1 + // create a representation of both files with all spaces removed from each line + const oldFileAfterLastDiff = originalFileStr.split('\n').slice(oldLineAfterLastDiff).map(line => line.replace(/\s/g, '')).join('\n') + const newFileAfterLastDiff = newFileStr.split('\n').slice(newLineAfterLastDiff).map(line => line.replace(/\s/g, '')).join('\n') + + // find where the matchup starts in `oldLinesAfterLastDiff` + const targetStr = newFileAfterLastDiff.slice(-NUM_MATCHUP_TOKENS) + + // return if not enough tokens to match + if (targetStr.length < NUM_MATCHUP_TOKENS) return; + // return if no matchup found + const matchupIdx = oldFileAfterLastDiff.indexOf(targetStr) + if (matchupIdx === -1) return; + + // resolve the promise with the delta, up to first matchup + res({ + matchupLine: oldLineAfterLastDiff, + deltaStr: newFileStr.split('\n').splice(0, newLineAfterLastDiff).join('\n'), + }); + + // abort the LLM call + _abort() + + }, + onFinalMessage: (finalMessage) => { + + const newFileStr = unfinishedFileStr + finalMessage + + const workspaceEdit = new vscode.WorkspaceEdit() + workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), newFileStr) + vscode.workspace.applyEdit(workspaceEdit) + + + console.log('FINAL MESSAGE', finalMessage) + + + res({ deltaStr: finalMessage, matchupLine: undefined }); + }, + onError: (e) => { + res({ deltaStr: '', matchupLine: undefined }); + console.error('Error rewriting file with diff', e); + }, + voidConfig, + setAbort: (a) => { setAbort(a); _abort = a }, + }) return promise } -const shouldApplyDiffFn = ({ diff, fileStr, speculationStr, type, voidConfig, setAbort }: { diff: string, fileStr: string, speculationStr: string, type: 'line' | 'chunk', voidConfig: VoidConfig, setAbort: SetAbort }) => { - const promptContent = ( - // the speculation is a line - type === 'line' ? `DIFF -\`\`\` -${diff} -\`\`\` +const shouldApplyDiffFn = ({ diffStr, fileStr, speculationStr, voidConfig, setAbort }: { diffStr: string, fileStr: string, speculationStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { -FILES + const promptContent = `DIFF \`\`\` -${fileStr} -\`\`\` - -SELECTION -\`\`\`${speculationStr}\`\`\` - -Return \`true\` if this line should be modified, and \`false\` if it should not be modified. -` - // the speculation is a chunk - : `DIFF -\`\`\` -${diff} +${diffStr} \`\`\` FILES @@ -596,38 +126,35 @@ SELECTION ${speculationStr} \`\`\` -Return \`true\` if this any part of the chunk should be modified, and \`false\` if it should not be modified. -`) +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: 'assistant', - content: type === 'line' ? searchDiffLineInstructions : searchDiffChunkInstructions, - }, { - role: 'assistant', - content: promptContent, - }], - onText: () => { }, + 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 applying diff to line: ', e) + console.error('Error in shouldApplyDiff: ', e) }, + onText: () => { }, voidConfig, setAbort, }) + // return the promise return promise } @@ -636,62 +163,57 @@ Return \`true\` if this any part of the chunk should be modified, and \`false\` // 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, fileStr, diff, voidConfig, setAbort }: { fileUri: vscode.Uri, fileStr: string, diff: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { +const applyDiffLazily = async ({ fileUri, fileStr, diffStr, voidConfig, setAbort }: { fileUri: vscode.Uri, fileStr: string, diffStr: string, voidConfig: VoidConfig, setAbort: SetAbort }) => { - const CHUNK_SIZE = 20 // number of lines to search at a time + console.log('apply diff lazily') + + const LINES_PER_CHUNK = 20 // number of lines to search at a time // read file content const fileLines = fileStr.split('\n') const completedLines = [] // search the file chunk-by-chunk - for (let chunkIdx = 0; chunkIdx * CHUNK_SIZE < fileLines.length; chunkIdx++) { + let chunkStart: number | undefined = 0 + while (chunkStart !== undefined && chunkStart < fileLines.length) { + + console.log('chunkStart', chunkStart) // get the chunk - const chunkStart = chunkIdx * CHUNK_SIZE - const chunkEnd = (chunkIdx + 1) * CHUNK_SIZE - const chunkLines = fileLines.slice(chunkStart, chunkEnd) + const chunkLines = fileLines.slice(chunkStart, chunkStart + LINES_PER_CHUNK) const chunkStr = chunkLines.join('\n'); + console.log('AAAAAA') + // ask LLM if we should apply the diff to the chunk - let shouldApplyDiff = await shouldApplyDiffFn({ speculationStr: chunkStr, type: 'chunk', diff, fileStr, voidConfig, setAbort }) + let shouldApplyDiff = await shouldApplyDiffFn({ fileStr, speculationStr: chunkStr, diffStr, voidConfig, setAbort }) if (!shouldApplyDiff) { // should not change the chunk completedLines.push(chunkStr); + chunkStart += chunkLines.length // TODO update highlighting here continue; } - // search the chunk line-by-line - for (const lineStr of chunkLines) { + console.log('BBBBBB') - // ask LLM if we should apply the diff to the line - let shouldApplyDiff = await shouldApplyDiffFn({ speculationStr: lineStr, type: 'line', diff, fileStr, voidConfig, setAbort }) - if (!shouldApplyDiff) { // should not change the line - completedLines.push(lineStr); - // TODO update highlighting here - continue; - } + // ask LLM to rewrite file with diff (if there is significant matchup with the original file, we stop rewriting) + const { deltaStr, matchupLine } = await writeFileWithDiffUntilMatchup({ + originalFileStr: fileStr, + unfinishedFileStr: completedLines.join('\n'), + diffStr, + fileUri, + voidConfig, + // TODO! update highlighting here + setAbort, + }) - // ask LLM to apply the diff - const changeStr = await rewriteFileWithDiff({ // rewrite file with diff (if there is significant matchup with the original file, we stop rewriting) - originalFileStr: fileStr, - newFileStr: completedLines.join('\n'), - diff, - fileUri, - voidConfig, - onText: async (newText, fullText) => { - // TODO! update highlighting here - // also make edits here - - }, - setAbort, - }) - completedLines.push(changeStr) - - } + console.log('CCCCCC') + completedLines.push(deltaStr) + chunkStart = matchupLine } + } diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index 2a29369f..7111a6a2 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -12,9 +12,14 @@ export type OnFinalMessage = (input: string) => void export type SetAbort = (abort: () => void) => void -export type LLMMessage = { +export type LLMMessageAnthropic = { role: 'user' | 'assistant', - content: string + content: string, +} + +export type LLMMessage = { + role: 'system' | 'user' | 'assistant', + content: string, } type SendLLMMessageFnTypeInternal = (params: { @@ -45,10 +50,20 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi 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.default.maxTokens), - messages: messages, }); let did_abort = false @@ -79,8 +94,8 @@ 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 } setAbort(abort) 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.ts b/extensions/void/src/extension.ts index 9f1ff5ec..c26cea42 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -117,7 +117,7 @@ export function activate(context: vscode.ExtensionContext) { let abort = () => { } // TODO this is unused // apply the change - applyDiffLazily({ fileUri, fileStr, diff: m.code, voidConfig, setAbort: (a) => { abort = a } }) + applyDiffLazily({ fileUri, fileStr, diffStr: m.code, voidConfig, setAbort: (a) => { abort = a } }) // set the file equal to the change // await editor.edit(editBuilder => { diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index ad7597df..70e7f693 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/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; } @@ -81,7 +81,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/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index 12508cdd..842b03ee 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -73,7 +73,6 @@ const Sidebar = () => {
        - } diff --git a/extensions/void/src/sidebar/SidebarChat.tsx b/extensions/void/src/sidebar/SidebarChat.tsx index 3e9aaa1b..0273cca5 100644 --- a/extensions/void/src/sidebar/SidebarChat.tsx +++ b/extensions/void/src/sidebar/SidebarChat.tsx @@ -11,6 +11,7 @@ import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMess import { useThreads } from "./contextForThreads"; import { sendLLMMessage } from "../common/sendLLMMessage"; import { useVoidConfig } from "./contextForConfig"; +import { generateDiffInstructions } from "../common/systemPrompts"; @@ -109,11 +110,13 @@ export const SidebarChat = () => { const [latestError, setLatestError] = useState('') // higher level state - const { allThreads, currentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads() + const { getAllThreads, getCurrentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads() + const { voidConfig } = useVoidConfig() // 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) { @@ -156,15 +159,17 @@ export const SidebarChat = () => { getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files }) const relevantFiles = await awaitVSCodeResponse('files') - // add message to chat history + // add system message to chat history + const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions } + addMessageToHistory(systemPromptElt) + const userContent = userInstructionsStr(instructions, relevantFiles.files, selection) - // console.log('prompt:\n', content) const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files } addMessageToHistory(newHistoryElt) // send message to LLM sendLLMMessage({ - messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content: userContent }], + messages: [...(getCurrentThread()?.messages ?? []).map(m => ({ role: m.role, content: m.content })),], onText: (newText, fullText) => setMessageStream(fullText), onFinalMessage: (content) => { // add assistant's message to chat history, and clear selection @@ -215,7 +220,7 @@ export const SidebarChat = () => { return <>
        {/* previous messages */} - {currentThread !== null && currentThread.messages.map((message, i) => + {getCurrentThread() !== null && getCurrentThread()?.messages.map((message, i) => )} {/* message stream */} @@ -225,7 +230,6 @@ export const SidebarChat = () => {
        {/* selection */}
        -
        {/* selection */} diff --git a/extensions/void/src/sidebar/SidebarThreadSelector.tsx b/extensions/void/src/sidebar/SidebarThreadSelector.tsx index 323d66e5..85ffed06 100644 --- a/extensions/void/src/sidebar/SidebarThreadSelector.tsx +++ b/extensions/void/src/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].createdAt > allThreads![threadId2].createdAt ? -1 : 1) @@ -62,7 +64,7 @@ export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => { return (