Merge branch 'speculative-edits' into multiple-webviews

This commit is contained in:
Andrew 2024-10-25 22:14:11 -07:00
commit 64468b6f4d
15 changed files with 944 additions and 314 deletions

View file

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

View file

@ -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",

View file

@ -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<T> = ((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<CompetedReturn> = () => { }
const promise = new Promise<CompetedReturn>((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<boolean> = () => { }
const promise = new Promise<boolean>((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 }

View file

@ -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: () => { } }
}
}

View file

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

View file

@ -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<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
{{selection}}
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
SELECTION
\`\`\` <button\`\`\`
INSTRUCTIONS
\`\`\`make all the buttons like this into divs\`\`\`
EXPECTED OUTPUT
We should change all the buttons like the one selected into a div component. Here is the change:
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
`;
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<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<button
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
DIFF
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
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<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
\`\`\`
RESULT
The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` 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<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<button
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
DIFF
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
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<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
\`\`\`
COMPLETION
\`\`\`
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<div
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</div>
</li>
))}
</ul>
<div className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</div>
</div>
);
};
export default Sidebar;\`\`\`
`
export {
generateDiffInstructions,
searchDiffChunkInstructions,
writeFileWithDiffInstructions,
};

View file

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

View file

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

View file

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

View file

@ -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] = {}

View file

@ -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 = <T,>(initVal: T) => {
export function ThreadsProvider({ children }: { children: ReactNode }) {
const [allThreads, setAllThreads] = useInstantState<ChatThreads>({})
const [currentThreadId, setCurrentThreadId] = useInstantState<string | null>(null)
const [allThreadsRef, setAllThreads] = useInstantState<ChatThreads>({})
const [currentThreadIdRef, setCurrentThreadId] = useInstantState<string | null>(null)
// this loads allThreads in on mount
useEffect(() => {
@ -55,12 +55,12 @@ export function ThreadsProvider({ children }: { children: ReactNode }) {
return (
<ThreadsContext.Provider
value={{
allThreads: allThreads.current,
currentThread: currentThreadId.current === null || allThreads.current === null ? null : allThreads.current[currentThreadId.current],
getAllThreads: () => 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)

View file

@ -60,7 +60,6 @@ const Sidebar = () => {
</div>
</div>
</>
}

View file

@ -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<HT
const [latestError, setLatestError] = useState('')
// higher level state
const { allThreads, currentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads()
const { getAllThreads, getCurrentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads()
const { voidConfig } = useVoidConfig()
@ -168,20 +170,21 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = useCallback((eventId: string, extras?: object) => {
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<HT
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
captureChatEvent('Chat - Sending Message', { messageLength: instructions.length })
const submit_time = new Date()
let { abort } = sendLLMMessage({
messages: [...(currentThread?.messages ?? []).map(m => ({ 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<HT
setLatestError(error)
},
voidConfig: voidConfig
setAbort: (abort) => {
abortFnRef.current = abort
},
voidConfig,
})
abortFnRef.current = abort
}
@ -286,7 +293,7 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
return <>
<div className="overflow-x-hidden space-y-4">
{/* previous messages */}
{currentThread !== null && currentThread.messages.map((message, i) =>
{getCurrentThread() !== null && getCurrentThread()?.messages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
)}
{/* message stream */}
@ -296,7 +303,6 @@ export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HT
<div className="shrink-0 py-4">
{/* selection */}
<div className="text-left">
<div className="relative">
<div className="input">
{/* selection */}

View file

@ -69,6 +69,10 @@ export const SidebarSettings = () => {
field='default'
param='whichApi'
/>
<SettingOfFieldAndParam
field='default'
param='maxTokens'
/>
</div>
<hr />

View file

@ -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 (
<button
key={pastThread.id}
className={`btn btn-sm rounded-sm ${pastThread.id === currentThread?.id ? "btn-primary" : "btn-secondary"}`}
className={`btn btn-sm rounded-sm ${pastThread.id === getCurrentThread()?.id ? "btn-primary" : "btn-secondary"}`}
onClick={() => switchToThread(pastThread.id)}
title={new Date(pastThread.createdAt).toLocaleString()}
>