big changes + react build pipeline (broken)

This commit is contained in:
Andrew Pareles 2024-11-09 19:57:57 -08:00
parent b52f0d8dd7
commit 8da76e72df
34 changed files with 25359 additions and 3340 deletions

View file

@ -1,3 +0,0 @@
{
"type": "module"
}

View file

@ -1,450 +1,450 @@
import * as vscode from 'vscode';
import { findDiffs } from './src/extension/findDiffs';
import { throttle } from 'lodash';
import { DiffArea, BaseDiff, Diff } from '../common/shared_types';
import { readFileContentOfUri } from './src/extension/extensionLib/readFileContentOfUri';
import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage';
import { writeFileWithDiffInstructions } from '../common/systemPrompts';
import { VoidConfig } from './src/webviews/common/contextForConfig';
// import * as vscode from 'vscode';
// import { findDiffs } from './src/extension/findDiffs';
// import { throttle } from 'lodash';
// import { DiffArea, BaseDiff, Diff } from '../common/shared_types';
// import { readFileContentOfUri } from './src/extension/extensionLib/readFileContentOfUri';
// import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage';
// import { writeFileWithDiffInstructions } from '../common/systemPrompts';
// import { VoidConfig } from './src/webviews/common/contextForConfig';
const THROTTLE_TIME = 100
// const THROTTLE_TIME = 100
// TODO in theory this should be disposed
const lightGrayDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(218 218 218 / .2)',
isWholeLine: true,
})
const darkGrayDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgb(148 148 148 / .2)',
isWholeLine: true,
})
// // TODO in theory this should be disposed
// const lightGrayDecoration = vscode.window.createTextEditorDecorationType({
// backgroundColor: 'rgba(218 218 218 / .2)',
// isWholeLine: true,
// })
// const darkGrayDecoration = vscode.window.createTextEditorDecorationType({
// backgroundColor: 'rgb(148 148 148 / .2)',
// isWholeLine: true,
// })
// responsible for displaying diffs and showing accept/reject buttons
export class DiffProvider implements vscode.CodeLensProvider {
// // responsible for displaying diffs and showing accept/reject buttons
// export class DiffProvider implements vscode.CodeLensProvider {
private _originalFileOfDocument: { [docUriStr: string]: string } = {}
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
// private _originalFileOfDocument: { [docUriStr: string]: string } = {}
// private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
// private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
private _diffareaidPool = 0
private _diffidPool = 0
// private _diffareaidPool = 0
// private _diffidPool = 0
private _extensionUri: vscode.Uri
// private _extensionUri: vscode.Uri
// used internally by vscode
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
// // used internally by vscode
// private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
// public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
// used internally by vscode
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
const docUriStr = document.uri.toString()
return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? []
}
// // used internally by vscode
// public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
// const docUriStr = document.uri.toString()
// return this._diffsOfDocument[docUriStr]?.flatMap(diff => diff.lenses) ?? []
// }
// declared by us, registered with vscode.languages.registerCodeLensProvider()
constructor(context: vscode.ExtensionContext) {
this._extensionUri = context.extensionUri
// // declared by us, registered with vscode.languages.registerCodeLensProvider()
// constructor(context: vscode.ExtensionContext) {
// this._extensionUri = context.extensionUri
console.log('Creating DisplayChangesProvider')
// console.log('Creating DisplayChangesProvider')
// this acts as a useEffect every time text changes
vscode.workspace.onDidChangeTextDocument((e) => {
// // this acts as a useEffect every time text changes
// vscode.workspace.onDidChangeTextDocument((e) => {
const editor = vscode.window.activeTextEditor
// const editor = vscode.window.activeTextEditor
if (!editor) return
// if (!editor) return
const docUriStr = editor.document.uri.toString()
const changes = e.contentChanges.map(c => ({ startLine: c.range.start.line, endLine: c.range.end.line, text: c.text, }))
// const docUriStr = editor.document.uri.toString()
// const changes = e.contentChanges.map(c => ({ startLine: c.range.start.line, endLine: c.range.end.line, text: c.text, }))
// on user change, grow/shrink/merge/delete diff areas
this.resizeDiffAreas(docUriStr, changes, 'currentFile')
// // on user change, grow/shrink/merge/delete diff areas
// this.resizeDiffAreas(docUriStr, changes, 'currentFile')
// refresh the diffAreas
this.refreshStylesAndDiffs(docUriStr)
// // refresh the diffAreas
// this.refreshStylesAndDiffs(docUriStr)
})
}
// })
// }
// used by us only
public createDiffArea(uri: vscode.Uri, partialDiffArea: Omit<DiffArea, 'diffareaid'>, originalFile: string) {
// // used by us only
// public createDiffArea(uri: vscode.Uri, partialDiffArea: Omit<DiffArea, 'diffareaid'>, originalFile: string) {
const uriStr = uri.toString()
// const uriStr = uri.toString()
this._originalFileOfDocument[uriStr] = originalFile
// this._originalFileOfDocument[uriStr] = originalFile
// make sure array is defined
if (!this._diffAreasOfDocument[uriStr]) this._diffAreasOfDocument[uriStr] = []
// // make sure array is defined
// if (!this._diffAreasOfDocument[uriStr]) this._diffAreasOfDocument[uriStr] = []
// remove all diffAreas that the new `diffArea` is overlapping with
this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => {
const noOverlap = da.startLine > partialDiffArea.endLine || da.endLine < partialDiffArea.startLine
if (!noOverlap) return false
return true
})
// // remove all diffAreas that the new `diffArea` is overlapping with
// this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => {
// const noOverlap = da.startLine > partialDiffArea.endLine || da.endLine < partialDiffArea.startLine
// if (!noOverlap) return false
// return true
// })
// add `diffArea` to storage
const diffArea = {
...partialDiffArea,
diffareaid: this._diffareaidPool
}
this._diffAreasOfDocument[uriStr].push(diffArea)
this._diffareaidPool += 1
// // add `diffArea` to storage
// const diffArea = {
// ...partialDiffArea,
// diffareaid: this._diffareaidPool
// }
// this._diffAreasOfDocument[uriStr].push(diffArea)
// this._diffareaidPool += 1
return diffArea
}
// return diffArea
// }
// used by us only
// changes the start/line locations based on the changes that were recently made. does not change any of the diffs in the diff areas
// changes tells us how many lines were inserted/deleted so we can grow/shrink the diffAreas accordingly
public resizeDiffAreas(docUriStr: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') {
// // used by us only
// // changes the start/line locations based on the changes that were recently made. does not change any of the diffs in the diff areas
// // changes tells us how many lines were inserted/deleted so we can grow/shrink the diffAreas accordingly
// public resizeDiffAreas(docUriStr: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') {
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
// const diffAreas = this._diffAreasOfDocument[docUriStr] || []
let endLine: 'originalEndLine' | 'endLine'
let startLine: 'originalStartLine' | 'startLine'
// let endLine: 'originalEndLine' | 'endLine'
// let startLine: 'originalStartLine' | 'startLine'
if (changesTo === 'originalFile') {
endLine = 'originalEndLine' as const
startLine = 'originalStartLine' as const
} else {
endLine = 'endLine' as const
startLine = 'startLine' as const
}
// if (changesTo === 'originalFile') {
// endLine = 'originalEndLine' as const
// startLine = 'originalStartLine' as const
// } else {
// endLine = 'endLine' as const
// startLine = 'startLine' as const
// }
for (const change of changes) {
// for (const change of changes) {
// here, `change.range` is the range of the original file that gets replaced with `change.text`
// // here, `change.range` is the range of the original file that gets replaced with `change.text`
// compute net number of newlines lines that were added/removed
const numNewLines = (change.text.match(/\n/g) || []).length
const numLineDeletions = change.endLine - change.startLine
const deltaNewlines = numNewLines - numLineDeletions
// // compute net number of newlines lines that were added/removed
// const numNewLines = (change.text.match(/\n/g) || []).length
// const numLineDeletions = change.endLine - change.startLine
// const deltaNewlines = numNewLines - numLineDeletions
// compute overlap with each diffArea and shrink/elongate the diffArea accordingly
for (const diffArea of diffAreas) {
// // compute overlap with each diffArea and shrink/elongate the diffArea accordingly
// for (const diffArea of diffAreas) {
// if the change is fully within the diffArea, elongate it by the delta amount of newlines
if (change.startLine >= diffArea[startLine] && change.endLine <= diffArea[endLine]) {
diffArea[endLine] += deltaNewlines
}
// check if the `diffArea` was fully deleted and remove it if so
if (diffArea[startLine] > diffArea[endLine]) {
//remove it
const index = diffAreas.findIndex(da => da === diffArea)
diffAreas.splice(index, 1)
}
// TODO handle other cases where eg. the change overlaps many diffAreas
}
// // if the change is fully within the diffArea, elongate it by the delta amount of newlines
// if (change.startLine >= diffArea[startLine] && change.endLine <= diffArea[endLine]) {
// diffArea[endLine] += deltaNewlines
// }
// // check if the `diffArea` was fully deleted and remove it if so
// if (diffArea[startLine] > diffArea[endLine]) {
// //remove it
// const index = diffAreas.findIndex(da => da === diffArea)
// diffAreas.splice(index, 1)
// }
// // TODO handle other cases where eg. the change overlaps many diffAreas
// }
// if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines
for (const diffArea of diffAreas) {
if (diffArea[startLine] > change.endLine) {
diffArea[startLine] += deltaNewlines
diffArea[endLine] += deltaNewlines
}
}
// // if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines
// for (const diffArea of diffAreas) {
// if (diffArea[startLine] > change.endLine) {
// diffArea[startLine] += deltaNewlines
// diffArea[endLine] += deltaNewlines
// }
// }
// TODO merge any diffAreas if they overlap with each other as a result from the shift
// // TODO merge any diffAreas if they overlap with each other as a result from the shift
}
}
// }
// }
// used by us only
// refreshes all the diffs inside each diff area, and refreshes the styles
public refreshStylesAndDiffs(docUriStr: string) {
// // used by us only
// // refreshes all the diffs inside each diff area, and refreshes the styles
// public refreshStylesAndDiffs(docUriStr: string) {
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
if (!editor) {
console.log('Error: No active editor!')
return;
}
const originalFile = this._originalFileOfDocument[docUriStr]
if (!originalFile) {
console.log('Error: No original file!')
return;
}
// const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
// if (!editor) {
// console.log('Error: No active editor!')
// return;
// }
// const originalFile = this._originalFileOfDocument[docUriStr]
// if (!originalFile) {
// console.log('Error: No original file!')
// return;
// }
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
// reset all diffs (we update them below)
this._diffsOfDocument[docUriStr] = []
// TODO!!!!
// vscode.languages.clearInlineDiffs(editor)
// for each diffArea
for (const diffArea of diffAreas) {
// get code inside of diffArea
const originalCode = originalFile.split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)).replace(/\r\n/g, '\n')
// compute the diffs
const diffs = findDiffs(originalCode, currentCode)
// add the diffs to `this._diffsOfDocument[docUriStr]`
// const diffAreas = this._diffAreasOfDocument[docUriStr] || []
// // reset all diffs (we update them below)
// this._diffsOfDocument[docUriStr] = []
// // TODO!!!!
// // vscode.languages.clearInlineDiffs(editor)
// // for each diffArea
// for (const diffArea of diffAreas) {
// // get code inside of diffArea
// const originalCode = originalFile.split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
// const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)).replace(/\r\n/g, '\n')
// // compute the diffs
// const diffs = findDiffs(originalCode, currentCode)
// // add the diffs to `this._diffsOfDocument[docUriStr]`
// if no diffs, set diffs to []
if (!this._diffsOfDocument[docUriStr])
this._diffsOfDocument[docUriStr] = []
// // if no diffs, set diffs to []
// if (!this._diffsOfDocument[docUriStr])
// this._diffsOfDocument[docUriStr] = []
// add each diff and its codelens to the document
for (let i = diffs.length - 1; i > -1; i -= 1) {
let suggestedDiff = diffs[i]
// // add each diff and its codelens to the document
// for (let i = diffs.length - 1; i > -1; i -= 1) {
// let suggestedDiff = diffs[i]
this._diffsOfDocument[docUriStr].push({
...suggestedDiff,
diffid: this._diffidPool,
// originalCode: suggestedDiff.deletedText,
lenses: [
new vscode.CodeLens(suggestedDiff.range, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
new vscode.CodeLens(suggestedDiff.range, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] })
]
});
vscode.languages.addInlineDiff(editor, suggestedDiff.originalCode, suggestedDiff.range)
this._diffidPool += 1
}
// this._diffsOfDocument[docUriStr].push({
// ...suggestedDiff,
// diffid: this._diffidPool,
// // originalCode: suggestedDiff.deletedText,
// lenses: [
// new vscode.CodeLens(suggestedDiff.range, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
// new vscode.CodeLens(suggestedDiff.range, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] })
// ]
// });
// vscode.languages.addInlineDiff(editor, suggestedDiff.originalCode, suggestedDiff.range)
// this._diffidPool += 1
// }
}
// }
// for each diffArea, highlight its sweepIndex in dark gray
editor.setDecorations(
darkGrayDecoration,
(this._diffAreasOfDocument[docUriStr]
.filter(diffArea => diffArea.sweepIndex !== null)
.map(diffArea => {
let s = diffArea.sweepIndex!
return new vscode.Range(s, 0, s, 0)
})
)
)
// for each diffArea, highlight sweepIndex+1...end in light gray
editor.setDecorations(
lightGrayDecoration,
(this._diffAreasOfDocument[docUriStr]
.filter(diffArea => diffArea.sweepIndex !== null)
.map(diffArea => {
return new vscode.Range(diffArea.sweepIndex! + 1, 0, diffArea.endLine, 0)
})
)
)
// // for each diffArea, highlight its sweepIndex in dark gray
// editor.setDecorations(
// darkGrayDecoration,
// (this._diffAreasOfDocument[docUriStr]
// .filter(diffArea => diffArea.sweepIndex !== null)
// .map(diffArea => {
// let s = diffArea.sweepIndex!
// return new vscode.Range(s, 0, s, 0)
// })
// )
// )
// // for each diffArea, highlight sweepIndex+1...end in light gray
// editor.setDecorations(
// lightGrayDecoration,
// (this._diffAreasOfDocument[docUriStr]
// .filter(diffArea => diffArea.sweepIndex !== null)
// .map(diffArea => {
// return new vscode.Range(diffArea.sweepIndex! + 1, 0, diffArea.endLine, 0)
// })
// )
// )
// update code lenses
this._onDidChangeCodeLenses.fire()
// // update code lenses
// this._onDidChangeCodeLenses.fire()
}
// }
// called on void.acceptDiff
public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
// // called on void.acceptDiff
// public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
// const editor = vscode.window.activeTextEditor
// if (!editor)
// return
const docUriStr = editor.document.uri.toString()
// const docUriStr = editor.document.uri.toString()
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
// const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
// if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
// const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
// if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diff = this._diffsOfDocument[docUriStr][diffIdx]
const originalFile = this._originalFileOfDocument[docUriStr]
const currentFile = await readFileContentOfUri(editor.document.uri)
// Fixed: Handle newlines properly by splitting into lines and joining with proper newlines
const originalLines = originalFile.split('\n');
const currentLines = currentFile.split('\n');
// Get the changed lines from current file
const changedLines = currentLines.slice(diff.range.start.line, diff.range.end.line + 1);
// Create new original file content by replacing the affected lines
const newOriginalLines = [
...originalLines.slice(0, diff.originalRange.start.line),
...changedLines,
...originalLines.slice(diff.originalRange.end.line + 1)
];
this._originalFileOfDocument[docUriStr] = newOriginalLines.join('\n');
// Update diff areas based on the change
this.resizeDiffAreas(docUriStr, [{
text: changedLines.join('\n'),
startLine: diff.originalRange.start.line,
endLine: diff.originalRange.end.line
}], 'originalFile')
// Check if diffArea should be removed
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
const originalArea = newOriginalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
if (originalArea === currentArea) {
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
this.refreshStylesAndDiffs(docUriStr)
}
// called on void.rejectDiff
public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
const docUriStr = editor.document.uri.toString()
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
const diff = this._diffsOfDocument[docUriStr][diffIdx]
// Apply the rejection by replacing with original code
// we don't have to edit the original or final file; just do a workspace edit so the code equals the original code
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.replace(editor.document.uri, diff.range, diff.originalCode)
await vscode.workspace.applyEdit(workspaceEdit)
// Check if diffArea should be removed
const originalFile = this._originalFileOfDocument[docUriStr]
const currentFile = await readFileContentOfUri(editor.document.uri)
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
const currentLines = currentFile.split('\n');
const originalLines = originalFile.split('\n');
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
const originalArea = originalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
if (originalArea === currentArea) {
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
this.refreshStylesAndDiffs(docUriStr)
}
async startStreamingInDiffArea({ docUri, oldFileStr, diffRepr, diffArea, voidConfig, abortRef }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) {
const promptContent = `\
ORIGINAL_FILE
\`\`\`
${oldFileStr}
\`\`\`
DIFF
\`\`\`
${diffRepr}
\`\`\`
INSTRUCTIONS
Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
`
// make LLM complete the file to include the diff
await new Promise<void>((resolve, reject) => {
sendLLMMessage({
logging: { loggingName: 'streamChunk' },
messages: [
{ role: 'system', content: writeFileWithDiffInstructions, },
// TODO include more context too
{ role: 'user', content: promptContent, }
],
onText: (newText, fullText) => {
this._updateStream(docUri.toString(), diffArea, fullText)
},
onFinalMessage: (fullText) => {
this._updateStream(docUri.toString(), diffArea, fullText)
resolve();
},
onError: (e) => {
console.error('Error rewriting file with diff', e);
resolve();
},
voidConfig,
abortRef,
})
})
}
// used by us only
private _updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => {
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
if (!editor) {
console.log('Error: No active editor!')
return;
}
// original code all diffs are based on in the code
const originalDiffAreaCode = (this._originalFileOfDocument[docUriStr] || '').split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
// figure out where to highlight based on where the AI is in the stream right now, use the last diff in findDiffs to figure that out
const diffs = findDiffs(originalDiffAreaCode, newDiffAreaCode)
const lastDiff = diffs?.[diffs.length - 1] ?? null
// these are two different coordinate systems - new and old line number
let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
let oldFileStartLine: number // get original[oldStartingPoint...]
if (!lastDiff) {
// if the writing is identical so far, display no changes
newFileEndLine = 0
oldFileStartLine = 0
}
else {
if (lastDiff.type === 'insertion') {
newFileEndLine = lastDiff.range.end.line
oldFileStartLine = lastDiff.originalRange.start.line
}
else if (lastDiff.type === 'deletion') {
newFileEndLine = lastDiff.range.start.line
oldFileStartLine = lastDiff.originalRange.start.line
}
else if (lastDiff.type === 'edit') {
newFileEndLine = lastDiff.range.end.line
oldFileStartLine = lastDiff.originalRange.start.line
}
else {
throw new Error(`updateStream: diff.type not recognized: ${lastDiff.type}`)
}
}
// display
const newFileTop = newDiffAreaCode.split('\n').slice(0, newFileEndLine + 1).join('\n')
const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n')
let newCode = `${newFileTop}\n${oldFileBottom}`
diffArea.sweepIndex = newFileEndLine
// replace oldDACode with newDACode with a vscode edit
const workspaceEdit = new vscode.WorkspaceEdit();
const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)
workspaceEdit.replace(editor.document.uri, diffareaRange, newCode)
await vscode.workspace.applyEdit(workspaceEdit)
}, THROTTLE_TIME)
}
// const diff = this._diffsOfDocument[docUriStr][diffIdx]
// const originalFile = this._originalFileOfDocument[docUriStr]
// const currentFile = await readFileContentOfUri(editor.document.uri)
// // Fixed: Handle newlines properly by splitting into lines and joining with proper newlines
// const originalLines = originalFile.split('\n');
// const currentLines = currentFile.split('\n');
// // Get the changed lines from current file
// const changedLines = currentLines.slice(diff.range.start.line, diff.range.end.line + 1);
// // Create new original file content by replacing the affected lines
// const newOriginalLines = [
// ...originalLines.slice(0, diff.originalRange.start.line),
// ...changedLines,
// ...originalLines.slice(diff.originalRange.end.line + 1)
// ];
// this._originalFileOfDocument[docUriStr] = newOriginalLines.join('\n');
// // Update diff areas based on the change
// this.resizeDiffAreas(docUriStr, [{
// text: changedLines.join('\n'),
// startLine: diff.originalRange.start.line,
// endLine: diff.originalRange.end.line
// }], 'originalFile')
// // Check if diffArea should be removed
// const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
// const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
// const originalArea = newOriginalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
// if (originalArea === currentArea) {
// const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
// this._diffAreasOfDocument[docUriStr].splice(index, 1)
// }
// this.refreshStylesAndDiffs(docUriStr)
// }
// // called on void.rejectDiff
// public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
// const editor = vscode.window.activeTextEditor
// if (!editor)
// return
// const docUriStr = editor.document.uri.toString()
// const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
// if (diffIdx === -1) { console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
// const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
// if (diffareaIdx === -1) { console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return; }
// const diff = this._diffsOfDocument[docUriStr][diffIdx]
// // Apply the rejection by replacing with original code
// // we don't have to edit the original or final file; just do a workspace edit so the code equals the original code
// const workspaceEdit = new vscode.WorkspaceEdit();
// workspaceEdit.replace(editor.document.uri, diff.range, diff.originalCode)
// await vscode.workspace.applyEdit(workspaceEdit)
// // Check if diffArea should be removed
// const originalFile = this._originalFileOfDocument[docUriStr]
// const currentFile = await readFileContentOfUri(editor.document.uri)
// const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
// const currentLines = currentFile.split('\n');
// const originalLines = originalFile.split('\n');
// const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
// const originalArea = originalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
// if (originalArea === currentArea) {
// const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
// this._diffAreasOfDocument[docUriStr].splice(index, 1)
// }
// this.refreshStylesAndDiffs(docUriStr)
// }
// async startStreamingInDiffArea({ docUri, oldFileStr, diffRepr, diffArea, voidConfig, abortRef }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) {
// const promptContent = `\
// ORIGINAL_FILE
// \`\`\`
// ${oldFileStr}
// \`\`\`
// DIFF
// \`\`\`
// ${diffRepr}
// \`\`\`
// INSTRUCTIONS
// Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
// `
// // make LLM complete the file to include the diff
// await new Promise<void>((resolve, reject) => {
// sendLLMMessage({
// logging: { loggingName: 'streamChunk' },
// messages: [
// { role: 'system', content: writeFileWithDiffInstructions, },
// // TODO include more context too
// { role: 'user', content: promptContent, }
// ],
// onText: (newText, fullText) => {
// this._updateStream(docUri.toString(), diffArea, fullText)
// },
// onFinalMessage: (fullText) => {
// this._updateStream(docUri.toString(), diffArea, fullText)
// resolve();
// },
// onError: (e) => {
// console.error('Error rewriting file with diff', e);
// resolve();
// },
// voidConfig,
// abortRef,
// })
// })
// }
// // used by us only
// private _updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => {
// const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
// if (!editor) {
// console.log('Error: No active editor!')
// return;
// }
// // original code all diffs are based on in the code
// const originalDiffAreaCode = (this._originalFileOfDocument[docUriStr] || '').split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
// // figure out where to highlight based on where the AI is in the stream right now, use the last diff in findDiffs to figure that out
// const diffs = findDiffs(originalDiffAreaCode, newDiffAreaCode)
// const lastDiff = diffs?.[diffs.length - 1] ?? null
// // these are two different coordinate systems - new and old line number
// let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
// let oldFileStartLine: number // get original[oldStartingPoint...]
// if (!lastDiff) {
// // if the writing is identical so far, display no changes
// newFileEndLine = 0
// oldFileStartLine = 0
// }
// else {
// if (lastDiff.type === 'insertion') {
// newFileEndLine = lastDiff.range.end.line
// oldFileStartLine = lastDiff.originalRange.start.line
// }
// else if (lastDiff.type === 'deletion') {
// newFileEndLine = lastDiff.range.start.line
// oldFileStartLine = lastDiff.originalRange.start.line
// }
// else if (lastDiff.type === 'edit') {
// newFileEndLine = lastDiff.range.end.line
// oldFileStartLine = lastDiff.originalRange.start.line
// }
// else {
// throw new Error(`updateStream: diff.type not recognized: ${lastDiff.type}`)
// }
// }
// // display
// const newFileTop = newDiffAreaCode.split('\n').slice(0, newFileEndLine + 1).join('\n')
// const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n')
// let newCode = `${newFileTop}\n${oldFileBottom}`
// diffArea.sweepIndex = newFileEndLine
// // replace oldDACode with newDACode with a vscode edit
// const workspaceEdit = new vscode.WorkspaceEdit();
// const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)
// workspaceEdit.replace(editor.document.uri, diffareaRange, newCode)
// await vscode.workspace.applyEdit(workspaceEdit)
// }, THROTTLE_TIME)
// }

View file

@ -1,184 +1,184 @@
import * as vscode from 'vscode';
// import * as vscode from 'vscode';
import { v4 as uuidv4 } from 'uuid'
import { AbortRef } from '../common/sendLLMMessage';
import { MessageToSidebar, MessageFromSidebar, DiffArea, ChatThreads } from '../common/shared_types';
import { getVoidConfigFromPartial } from '../webviews/common/contextForConfig';
import { DiffProvider } from '../../DiffProvider';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { SidebarWebviewProvider } from '../sidebar/SidebarWebviewProvider';
import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider';
// import { v4 as uuidv4 } from 'uuid'
// import { AbortRef } from '../common/sendLLMMessage';
// import { MessageToSidebar, MessageFromSidebar, DiffArea, ChatThreads } from '../common/shared_types';
// import { getVoidConfigFromPartial } from '../webviews/common/contextForConfig';
// import { DiffProvider } from '../../DiffProvider';
// import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
// import { SidebarWebviewProvider } from '../sidebar/SidebarWebviewProvider';
// import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider';
const roundRangeToLines = (selection: vscode.Selection) => {
let endLine = selection.end.character === 0 ? selection.end.line - 1 : selection.end.line // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
return new vscode.Range(selection.start.line, 0, endLine, Number.MAX_SAFE_INTEGER)
}
// const roundRangeToLines = (selection: vscode.Selection) => {
// let endLine = selection.end.character === 0 ? selection.end.line - 1 : selection.end.line // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
// return new vscode.Range(selection.start.line, 0, endLine, Number.MAX_SAFE_INTEGER)
// }
const getSelection = (editor: vscode.TextEditor) => {
// get the range of the selection and the file the user is in
const selectionRange = roundRangeToLines(editor.selection);
const selectionStr = editor.document.getText(selectionRange).trim();
const filePath = editor.document.uri;
return { selectionStr, filePath }
}
// const getSelection = (editor: vscode.TextEditor) => {
// // get the range of the selection and the file the user is in
// const selectionRange = roundRangeToLines(editor.selection);
// const selectionStr = editor.document.getText(selectionRange).trim();
// const filePath = editor.document.uri;
// return { selectionStr, filePath }
// }
export function activate(context: vscode.ExtensionContext) {
// export function activate(context: vscode.ExtensionContext) {
// 1. Mount the chat sidebar
const sidebarWebviewProvider = new SidebarWebviewProvider(context);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, sidebarWebviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
);
// // 1. Mount the chat sidebar
// const sidebarWebviewProvider = new SidebarWebviewProvider(context);
// context.subscriptions.push(
// vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, sidebarWebviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
// );
// 1.5
const ctrlKWebviewProvider = new CtrlKWebviewProvider(context)
// // 1.5
// const ctrlKWebviewProvider = new CtrlKWebviewProvider(context)
// 2. ctrl+l
context.subscriptions.push(
vscode.commands.registerCommand('void.ctrl+l', () => {
const editor = vscode.window.activeTextEditor
if (!editor) return
// // 2. ctrl+l
// context.subscriptions.push(
// vscode.commands.registerCommand('void.ctrl+l', () => {
// const editor = vscode.window.activeTextEditor
// if (!editor) return
// show the sidebar
vscode.commands.executeCommand('workbench.view.extension.voidViewContainer');
// vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar
// // show the sidebar
// vscode.commands.executeCommand('workbench.view.extension.voidViewContainer');
// // vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar
const { selectionStr, filePath } = getSelection(editor)
// const { selectionStr, filePath } = getSelection(editor)
// send message to the webview (Sidebar.tsx)
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
})
);
// // send message to the webview (Sidebar.tsx)
// sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
// })
// );
// 2.5: ctrl+k
context.subscriptions.push(
vscode.commands.registerCommand('void.ctrl+k', () => {
console.log('CTRLK PRESSED')
const editor = vscode.window.activeTextEditor
if (!editor) return
// // 2.5: ctrl+k
// context.subscriptions.push(
// vscode.commands.registerCommand('void.ctrl+k', () => {
// console.log('CTRLK PRESSED')
// const editor = vscode.window.activeTextEditor
// if (!editor) return
const { selectionStr, filePath } = getSelection(editor)
// const { selectionStr, filePath } = getSelection(editor)
// send message to the webview (Sidebar.tsx)
// ctrlKWebviewProvider.onPressCtrlK()
// sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
})
);
// // send message to the webview (Sidebar.tsx)
// // ctrlKWebviewProvider.onPressCtrlK()
// // sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
// })
// );
// 3. Show an approve/reject codelens above each change
const diffProvider = new DiffProvider(context);
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', diffProvider));
// // 3. Show an approve/reject codelens above each change
// const diffProvider = new DiffProvider(context);
// context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', diffProvider));
// 4. Add approve/reject commands
context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
diffProvider.acceptDiff(params)
}));
context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => {
diffProvider.rejectDiff(params)
}));
// // 4. Add approve/reject commands
// context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
// diffProvider.acceptDiff(params)
// }));
// context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => {
// diffProvider.rejectDiff(params)
// }));
// 5. Receive messages from sidebar
sidebarWebviewProvider.webview.then(
webview => {
// // 5. Receive messages from sidebar
// sidebarWebviewProvider.webview.then(
// webview => {
// top navigation bar commands
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar)
}))
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar)
}))
context.subscriptions.push(vscode.commands.registerCommand('void.toggleSettings', async () => {
webview.postMessage({ type: 'toggleSettings' } satisfies MessageToSidebar)
}));
// // top navigation bar commands
// context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
// webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar)
// }))
// context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
// webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar)
// }))
// context.subscriptions.push(vscode.commands.registerCommand('void.toggleSettings', async () => {
// webview.postMessage({ type: 'toggleSettings' } satisfies MessageToSidebar)
// }));
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
// // Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
// webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
const abortApplyRef: AbortRef = { current: null }
// const abortApplyRef: AbortRef = { current: null }
if (m.type === 'requestFiles') {
// if (m.type === 'requestFiles') {
// get contents of all file paths
const files = await Promise.all(
m.filepaths.map(async (filepath) => ({ filepath, content: await readFileContentOfUri(filepath) }))
)
// // get contents of all file paths
// const files = await Promise.all(
// m.filepaths.map(async (filepath) => ({ filepath, content: await readFileContentOfUri(filepath) }))
// )
// send contents to webview
webview.postMessage({ type: 'files', files, } satisfies MessageToSidebar)
// // send contents to webview
// webview.postMessage({ type: 'files', files, } satisfies MessageToSidebar)
}
else if (m.type === 'applyChanges') {
// }
// else if (m.type === 'applyChanges') {
const editor = vscode.window.activeTextEditor
if (!editor) {
vscode.window.showInformationMessage('No active editor!')
return
}
// create an area to show diffs
const partialDiffArea: Omit<DiffArea, 'diffareaid'> = {
startLine: 0, // in ctrl+L the start and end lines are the full document
endLine: editor.document.lineCount,
originalStartLine: 0,
originalEndLine: editor.document.lineCount,
sweepIndex: null,
}
const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri))
// const editor = vscode.window.activeTextEditor
// if (!editor) {
// vscode.window.showInformationMessage('No active editor!')
// return
// }
// // create an area to show diffs
// const partialDiffArea: Omit<DiffArea, 'diffareaid'> = {
// startLine: 0, // in ctrl+L the start and end lines are the full document
// endLine: editor.document.lineCount,
// originalStartLine: 0,
// originalEndLine: editor.document.lineCount,
// sweepIndex: null,
// }
// const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri))
const docUri = editor.document.uri
const fileStr = await readFileContentOfUri(docUri)
const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
// const docUri = editor.document.uri
// const fileStr = await readFileContentOfUri(docUri)
// const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
await diffProvider.startStreamingInDiffArea({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffArea, abortRef: abortApplyRef })
}
else if (m.type === 'getPartialVoidConfig') {
const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {}
webview.postMessage({ type: 'partialVoidConfig', partialVoidConfig } satisfies MessageToSidebar)
}
else if (m.type === 'persistPartialVoidConfig') {
const partialVoidConfig = m.partialVoidConfig
context.globalState.update('partialVoidConfig', partialVoidConfig)
}
else if (m.type === 'getAllThreads') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
webview.postMessage({ type: 'allThreads', threads } satisfies MessageToSidebar)
}
else if (m.type === 'persistThread') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
const updatedThreads: ChatThreads = { ...threads, [m.thread.id]: m.thread }
context.workspaceState.update('allThreads', updatedThreads)
}
else if (m.type === 'getDeviceId') {
let deviceId = context.globalState.get('void_deviceid')
if (!deviceId || typeof deviceId !== 'string') {
deviceId = uuidv4()
context.globalState.update('void_deviceid', deviceId)
}
webview.postMessage({ type: 'deviceId', deviceId: deviceId as string } satisfies MessageToSidebar)
}
else {
console.error('unrecognized command', m)
}
})
}
)
// await diffProvider.startStreamingInDiffArea({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffArea, abortRef: abortApplyRef })
// }
// else if (m.type === 'getPartialVoidConfig') {
// const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {}
// webview.postMessage({ type: 'partialVoidConfig', partialVoidConfig } satisfies MessageToSidebar)
// }
// else if (m.type === 'persistPartialVoidConfig') {
// const partialVoidConfig = m.partialVoidConfig
// context.globalState.update('partialVoidConfig', partialVoidConfig)
// }
// else if (m.type === 'getAllThreads') {
// const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
// webview.postMessage({ type: 'allThreads', threads } satisfies MessageToSidebar)
// }
// else if (m.type === 'persistThread') {
// const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
// const updatedThreads: ChatThreads = { ...threads, [m.thread.id]: m.thread }
// context.workspaceState.update('allThreads', updatedThreads)
// }
// else if (m.type === 'getDeviceId') {
// let deviceId = context.globalState.get('void_deviceid')
// if (!deviceId || typeof deviceId !== 'string') {
// deviceId = uuidv4()
// context.globalState.update('void_deviceid', deviceId)
// }
// webview.postMessage({ type: 'deviceId', deviceId: deviceId as string } satisfies MessageToSidebar)
// }
// else {
// console.error('unrecognized command', m)
// }
// })
// }
// )
// Gets called when user presses ctrl + k (mounts ctrl+k-style codelens)
// TODO need to build this
// const ctrlKCodeLensProvider = new CtrlKCodeLensProvider();
// context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', ctrlKCodeLensProvider));
// context.subscriptions.push(
// vscode.commands.registerCommand('void.ctrl+k', () => {
// const editor = vscode.window.activeTextEditor;
// if (!editor)
// return
// ctrlKCodeLensProvider.addNewCodeLens(editor.document, editor.selection);
// // vscode.commands.executeCommand('editor.action.showHover'); // apparently this refreshes the codelenses by having the internals call provideCodeLenses
// })
// )
// // Gets called when user presses ctrl + k (mounts ctrl+k-style codelens)
// // TODO need to build this
// // const ctrlKCodeLensProvider = new CtrlKCodeLensProvider();
// // context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', ctrlKCodeLensProvider));
// // context.subscriptions.push(
// // vscode.commands.registerCommand('void.ctrl+k', () => {
// // const editor = vscode.window.activeTextEditor;
// // if (!editor)
// // return
// // ctrlKCodeLensProvider.addNewCodeLens(editor.document, editor.selection);
// // // vscode.commands.executeCommand('editor.action.showHover'); // apparently this refreshes the codelenses by having the internals call provideCodeLenses
// // })
// // )
}
// }

View file

@ -0,0 +1,395 @@
// import Anthropic from '@anthropic-ai/sdk';
// import OpenAI from 'openai';
// import { Ollama } from 'ollama/browser'
// import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai';
// // import { VoidConfig } from '../webviews/common/contextForConfig'
// // import { captureEvent } from '../webviews/common/posthog';
// // import { ChatMessage } from './shared_types';
// type VoidConfig = any
// export type AbortRef = { current: (() => void) | null }
// export type OnText = (newText: string, fullText: string) => void
// export type OnFinalMessage = (input: string) => void
// export type LLMMessageAnthropic = {
// role: 'user' | 'assistant';
// content: string;
// }
// export type LLMMessage = {
// role: 'system' | 'user' | 'assistant';
// content: string;
// }
// type SendLLMMessageFnTypeInternal = (params: {
// messages: LLMMessage[];
// onText: OnText;
// onFinalMessage: OnFinalMessage;
// onError: (error: string) => void;
// voidConfig: VoidConfig;
// _setAborter: (aborter: () => void) => void;
// }) => void
// type SendLLMMessageFnTypeExternal = (params: {
// messages: LLMMessage[];
// onText: OnText;
// onFinalMessage: (fullText: string) => void;
// onError: (error: string) => void;
// voidConfig: VoidConfig | null;
// abortRef: AbortRef;
// logging: {
// loggingName: string,
// };
// }) => void
// const parseMaxTokensStr = (maxTokensStr: string) => {
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
// if (Number.isNaN(int))
// return undefined
// return int
// }
// // Anthropic
// const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
// const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
// // find system messages and concatenate them
// const systemMessage = messages
// .filter(msg => msg.role === 'system')
// .map(msg => msg.content)
// .join('\n');
// // remove system messages for Anthropic
// const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
// const stream = anthropic.messages.stream({
// system: systemMessage,
// messages: anthropicMessages,
// model: voidConfig.anthropic.model,
// max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
// });
// // when receive text
// stream.on('text', (newText, fullText) => {
// onText(newText, fullText)
// })
// // when we get the final message on this stream (or when error/fail)
// stream.on('finalMessage', (claude_response) => {
// // stringify the response's content
// const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
// onFinalMessage(content)
// })
// stream.on('error', (error) => {
// // the most common error will be invalid API key (401), so we handle this with a nice message
// if (error instanceof Anthropic.APIError && error.status === 401) {
// onError('Invalid API key.')
// }
// else {
// onError(error.message)
// }
// })
// // TODO need to test this to make sure it works, it might throw an error
// _setAborter(() => stream.controller.abort())
// };
// // Gemini
// const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
// let fullText = ''
// const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
// const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
// // remove system messages that get sent to Gemini
// // str of all system messages
// const systemMessage = messages
// .filter(msg => msg.role === 'system')
// .map(msg => msg.content)
// .join('\n');
// // Convert messages to Gemini format
// const geminiMessages: Content[] = messages
// .filter(msg => msg.role !== 'system')
// .map((msg, i) => ({
// parts: [{ text: msg.content }],
// role: msg.role === 'assistant' ? 'model' : 'user'
// }))
// model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
// .then(async response => {
// _setAborter(() => response.stream.return(fullText))
// for await (const chunk of response.stream) {
// const newText = chunk.text();
// fullText += newText;
// onText(newText, fullText);
// }
// onFinalMessage(fullText);
// })
// .catch((error) => {
// if (error instanceof GoogleGenerativeAIFetchError) {
// if (error.status === 400) {
// onError('Invalid API key.');
// }
// else {
// onError(`${error.name}:\n${error.message}`);
// }
// }
// else {
// onError(error);
// }
// })
// }
// // OpenAI, OpenRouter, OpenAICompatible
// const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
// let fullText = ''
// let openai: OpenAI
// let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
// const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
// if (voidConfig.default.whichApi === 'openAI') {
// openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
// options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
// }
// else if (voidConfig.default.whichApi === 'openRouter') {
// openai = new OpenAI({
// baseURL: 'https://openrouter.ai/api/v1', apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true,
// defaultHeaders: {
// 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
// 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
// },
// });
// options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
// }
// else if (voidConfig.default.whichApi === 'openAICompatible') {
// openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
// options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
// }
// else {
// console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
// throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
// }
// openai.chat.completions
// .create(options)
// .then(async response => {
// _setAborter(() => response.controller.abort())
// // when receive text
// for await (const chunk of response) {
// const newText = chunk.choices[0]?.delta?.content || '';
// fullText += newText;
// onText(newText, fullText);
// }
// onFinalMessage(fullText);
// })
// // when error/fail - this catches errors of both .create() and .then(for await)
// .catch(error => {
// if (error instanceof OpenAI.APIError) {
// if (error.status === 401) {
// onError('Invalid API key.');
// }
// else {
// onError(`${error.name}:\n${error.message}`);
// }
// }
// else {
// onError(error);
// }
// })
// };
// // Ollama
// export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
// let fullText = ''
// const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
// ollama.chat({
// model: voidConfig.ollama.model,
// messages: messages,
// stream: true,
// options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
// })
// .then(async stream => {
// _setAborter(() => stream.abort())
// // iterate through the stream
// for await (const chunk of stream) {
// const newText = chunk.message.content;
// fullText += newText;
// onText(newText, fullText);
// }
// onFinalMessage(fullText);
// })
// // when error/fail
// .catch(error => {
// onError(error)
// })
// };
// // Greptile
// // https://docs.greptile.com/api-reference/query
// // https://docs.greptile.com/quickstart#sample-response-streamed
// const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
// let fullText = ''
// fetch('https://api.greptile.com/v2/query', {
// method: 'POST',
// headers: {
// 'Authorization': `Bearer ${voidConfig.greptile.apikey}`,
// 'X-Github-Token': `${voidConfig.greptile.githubPAT}`,
// 'Content-Type': `application/json`,
// },
// body: JSON.stringify({
// messages,
// stream: true,
// repositories: [voidConfig.greptile.repoinfo],
// }),
// })
// // this is {message}\n{message}\n{message}...\n
// .then(async response => {
// const text = await response.text()
// console.log('got greptile', text)
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
// })
// // TODO make this actually stream, right now it just sends one message at the end
// // TODO add _setAborter() when add streaming
// .then(async responseArr => {
// for (const response of responseArr) {
// const type: string = response['type']
// const message = response['message']
// // when receive text
// if (type === 'message') {
// fullText += message
// onText(message, fullText)
// }
// else if (type === 'sources') {
// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
// fullText += filepath
// onText(filepath, fullText)
// }
// // type: 'status' with an empty 'message' means last message
// else if (type === 'status') {
// if (!message) {
// onFinalMessage(fullText)
// }
// }
// }
// })
// .catch(e => {
// onError(e)
// });
// }
// export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({
// messages,
// onText: onText_,
// onFinalMessage: onFinalMessage_,
// onError: onError_,
// abortRef: abortRef_,
// voidConfig,
// logging: { loggingName }
// }) => {
// 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() }))
// // only captures number of messages and message "shape", no actual code, instructions, prompts, etc
// const captureChatEvent = (eventId: string, extras?: object) => {
// // captureEvent(eventId, {
// // whichApi: voidConfig.default['whichApi'],
// // numMessages: messages?.length,
// // messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
// // version: '2024-11-02',
// // ...extras,
// // })
// }
// const submit_time = new Date()
// let _fullTextSoFar = ''
// let _aborter: (() => void) | null = null
// let _setAborter = (fn: () => void) => { _aborter = fn }
// let _didAbort = false
// const onText = (newText: string, fullText: string) => {
// if (_didAbort) return
// onText_(newText, fullText)
// _fullTextSoFar = fullText
// }
// const onFinalMessage = (fullText: string) => {
// if (_didAbort) return
// captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
// onFinalMessage_(fullText)
// }
// const onError = (error: string) => {
// if (_didAbort) return
// captureChatEvent(`${loggingName} - Error`, { error })
// onError_(error)
// }
// const onAbort = () => {
// captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
// _aborter?.()
// _didAbort = true
// }
// abortRef_.current = onAbort
// captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
// switch (voidConfig.default.whichApi) {
// case 'anthropic':
// sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
// break;
// case 'openAI':
// case 'openRouter':
// case 'openAICompatible':
// sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
// break;
// case 'gemini':
// sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
// break;
// case 'ollama':
// sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
// break;
// case 'greptile':
// sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
// break;
// default:
// onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
// break;
// }
// }

View file

@ -0,0 +1,4 @@
// tsup to build all react
// build tailwind -> styles.css

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,74 @@
import React, { useState } from 'react'
import { mountFnGenerator } from '../util/mountFnGenerator'
import { VIEWPANE_FILTER_ACTION } from '../../../../../browser/parts/views/viewPane'
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
// import { SidebarChat } from './SidebarChat.js';
// import { SidebarSettings } from './SidebarSettings.js';
console.log('!!filteraction', VIEWPANE_FILTER_ACTION)
const Sidebar = () => {
// const chatInputRef = useRef<HTMLTextAreaElement | null>(null)
const [tab, setTab] = useState<'threadSelector' | 'chat' | 'settings'>('chat')
// // if they pressed the + to add a new chat
// useOnVSCodeMessage('startNewThread', (m) => {
// setTab('chat');
// chatInputRef.current?.focus();
// })
// // ctrl+l should switch back to chat
// useOnVSCodeMessage('ctrl+l', (m) => {
// setTab('chat');
// chatInputRef.current?.focus();
// })
// // if they toggled thread selector
// useOnVSCodeMessage('toggleThreadSelector', (m) => {
// if (tab === 'threadSelector') {
// setTab('chat')
// chatInputRef.current?.blur();
// } else
// setTab('threadSelector')
// })
// // if they toggled settings
// useOnVSCodeMessage('toggleSettings', (m) => {
// if (tab === 'settings') {
// setTab('chat')
// chatInputRef.current?.blur();
// } else
// setTab('settings')
// })
return <>
<div className={`flex flex-col h-screen w-full`}>
<span onClick={() => {
const tabs = ['chat', 'settings', 'threadSelector']
let index = tabs.indexOf(tab)
setTab(tabs[(index + 1) % tabs.length] as any)
}}>clickme {tab}</span>
<div className={`mb-2 h-[30vh] ${tab !== 'threadSelector' ? 'hidden' : ''}`}>
hi
{/* <SidebarThreadSelector onClose={() => setTab('chat')} /> */}
</div>
<div className={`${tab !== 'chat' && tab !== 'threadSelector' ? 'hidden' : ''}`}>
{/* <SidebarChat chatInputRef={chatInputRef} /> */}
</div>
<div className={`${tab !== 'settings' ? 'hidden' : ''}`}>
{/* <SidebarSettings /> */}
</div>
</div>
</>
}
const mountFn = mountFnGenerator(Sidebar)
export default mountFn

View file

@ -0,0 +1,350 @@
// import React, { FormEvent, useCallback, useRef, useState } from "react";
// import MarkdownRender from "../../sidebar/markdown/!MarkdownRender";
// import BlockCode from "../../sidebar/markdown/!BlockCode";
// import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation';
// const filesStr = (fullFiles: File[]) => {
// return fullFiles.map(({ filepath, content }) =>
// `
// ${filepath.fsPath}
// \`\`\`
// ${content}
// \`\`\``).join('\n')
// }
// const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
// let str = '';
// if (files.length > 0) {
// str += filesStr(files);
// }
// if (selection) {
// str += `
// I am currently selecting this code:
// \t\`\`\`${selection.selectionStr}\`\`\`
// `;
// }
// if (files.length > 0 && selection) {
// str += `
// Please edit the selected code or the entire file following these instructions:
// `;
// } else if (files.length > 0) {
// str += `
// Please edit the file following these instructions:
// `;
// } else if (selection) {
// str += `
// Please edit the selected code following these instructions:
// `;
// }
// str += `
// \t${instructions}
// `;
// if (files.length > 0) {
// str += `
// \tIf you make a change, rewrite the entire file.
// `; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
// }
// return str;
// };
// const getBasename = (pathStr: string) => {
// // "unixify" path
// pathStr = pathStr.replace(/[/\\]+/g, "/") // replace any / or \ or \\ with /
// const parts = pathStr.split("/") // split on /
// return parts[parts.length - 1]
// }
// export const SelectedFiles = ({ files, setFiles, }: { files: vscode.Uri[], setFiles: null | ((files: vscode.Uri[]) => void) }) => {
// return (
// files.length !== 0 && (
// <div className="flex flex-wrap -mx-1 -mb-1">
// {files.map((filename, i) => (
// <button
// key={filename.path}
// disabled={!setFiles}
// className={`btn btn-secondary btn-sm border border-vscode-input-border rounded flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default`}
// type="button"
// onClick={() => setFiles?.([...files.slice(0, i), ...files.slice(i + 1, Infinity)])}
// >
// <span>{getBasename(filename.fsPath)}</span>
// {/* X button */}
// {!!setFiles && <span className="">
// <svg
// xmlns="http://www.w3.org/2000/svg"
// fill="none"
// viewBox="0 0 24 24"
// stroke="currentColor"
// className="size-4"
// >
// <path
// strokeLinecap="round"
// strokeLinejoin="round"
// d="M6 18 18 6M6 6l12 12"
// />
// </svg>
// </span>}
// </button>
// ))}
// </div>
// )
// )
// }
// const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
// const role = chatMessage.role
// const children = chatMessage.displayContent
// if (!children)
// return null
// let chatbubbleContents: React.ReactNode
// if (role === 'user') {
// chatbubbleContents = <>
// <SelectedFiles files={chatMessage.files} setFiles={null} />
// {chatMessage.selection?.selectionStr && <BlockCode
// text={chatMessage.selection.selectionStr}
// buttonsOnHover={null}
// />}
// {children}
// </>
// }
// else if (role === 'assistant') {
// chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
// }
// return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
// <div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
// {chatbubbleContents}
// </div>
// </div>
// }
// export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HTMLTextAreaElement> }) => {
// // // 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) {
// // switchToThread(threadId)
// // return
// // }
// // }
// // // start a new thread
// // startNewThread()
// // })
// // // if user pressed ctrl+l, add their selection to the sidebar
// // useOnVSCodeMessage('ctrl+l', (m) => {
// // setSelection(m.selection)
// // const filepath = m.selection.filePath
// // // add current file to the context if it's not already in the files array
// // if (!files.find(f => f.fsPath === filepath.fsPath))
// // setFiles(files => [...files, filepath])
// // })
// // state of current message
// const [selection, setSelection] = useState<CodeSelection | null>(null) // the code the user is selecting
// const [files, setFiles] = useState<vscode.Uri[]>([]) // the names of the files in the chat
// const [instructions, setInstructions] = useState('') // the user's instructions
// // state of chat
// const [messageStream, setMessageStream] = useState('')
// const [isLoading, setIsLoading] = useState(false)
// const abortFnRef = useRef<(() => void) | null>(null)
// const [latestError, setLatestError] = useState('')
// // higher level state
// const { getAllThreads, getCurrentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads()
// const { voidConfig } = useVoidConfig()
// const isDisabled = !instructions
// const formRef = useRef<HTMLFormElement | null>(null)
// const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
// e.preventDefault()
// if (isDisabled) return
// if (isLoading) return
// setIsLoading(true)
// setInstructions('');
// formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens
// setSelection(null)
// setFiles([])
// setLatestError('')
// // request file content from vscode and await response
// getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
// const relevantFiles = await awaitVSCodeResponse('files')
// // add system message to chat history
// const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
// addMessageToHistory(systemPromptElt)
// const userContent = userInstructionsStr(instructions, relevantFiles.files, selection)
// const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files }
// addMessageToHistory(newHistoryElt)
// // send message to LLM
// sendLLMMessage({
// logging: { loggingName: 'Chat' },
// 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
// const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content }
// addMessageToHistory(newHistoryElt)
// setMessageStream('')
// setIsLoading(false)
// },
// onError: (error) => {
// // add assistant's message to chat history, and clear selection
// let content = messageStream; // just use the current content
// const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
// addMessageToHistory(newHistoryElt)
// setMessageStream('')
// setIsLoading(false)
// setLatestError(error)
// },
// voidConfig,
// abortRef: abortFnRef,
// })
// }
// const onAbort = useCallback(() => {
// // abort claude
// abortFnRef.current?.()
// // if messageStream was not empty, add it to the history
// const llmContent = messageStream || '(null)'
// const newHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream, }
// addMessageToHistory(newHistoryElt)
// setMessageStream('')
// setIsLoading(false)
// }, [messageStream, addMessageToHistory])
// return <>
// <div className="overflow-x-hidden space-y-4">
// {/* previous messages */}
// {getCurrentThread() !== null && getCurrentThread()?.messages.map((message, i) =>
// <ChatBubble key={i} chatMessage={message} />
// )}
// {/* message stream */}
// <ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
// </div>
// {/* chatbar */}
// <div className="shrink-0 py-4">
// {/* selection */}
// <div className="text-left">
// <div className="relative">
// <div className="input">
// {/* selection */}
// {(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
// {/* selected files */}
// <SelectedFiles files={files} setFiles={setFiles} />
// {/* selected code */}
// {!!selection?.selectionStr && (
// <BlockCode text={selection.selectionStr}
// buttonsOnHover={(
// <button
// onClick={() => setSelection(null)}
// className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
// >
// Remove
// </button>
// )} />
// )}
// </div>}
// <form
// ref={formRef}
// className="flex flex-row items-center rounded-md p-2"
// onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
// onSubmit={(e) => {
// console.log('submit!')
// onSubmit(e)
// }}>
// {/* input */}
// <textarea
// ref={chatInputRef}
// onChange={(e) => { setInstructions(e.target.value) }}
// className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
// placeholder="Ctrl+L to select"
// rows={1}
// onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
// />
// {isLoading ?
// // stop button
// <button
// onClick={onAbort}
// type='button'
// className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
// >
// <svg
// className='scale-50'
// stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
// <path d="M24 24H0V0h24v24z"></path>
// </svg>
// </button>
// :
// // submit button (up arrow)
// <button
// className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
// disabled={isDisabled}
// type='submit'
// >
// <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
// <line x1="12" y1="19" x2="12" y2="5"></line>
// <polyline points="5 12 12 5 19 12"></polyline>
// </svg>
// </button>
// }
// </form>
// </div>
// </div>
// </div>
// {/* error message */}
// {!latestError ? null : <div>
// {latestError}
// </div>}
// </div>
// </>
// }

View file

@ -0,0 +1,105 @@
// import React, { useState } from "react";
// import { configFields, useVoidConfig, VoidConfigField } from "../util/contextForConfig";
// const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => {
// const { voidConfig, partialVoidConfig, voidConfigInfo, setConfigParam } = useVoidConfig()
// const { enumArr, defaultVal, description } = voidConfigInfo[field][param]
// const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item
// const updateState = (newValue: string) => { setConfigParam(field, param, newValue) }
// const resetButton = <button
// disabled={val === defaultVal}
// title={val === defaultVal ? 'This is the default value.' : `Revert value to '${defaultVal}'?`}
// className='group btn btn-sm disabled:opacity-75 disabled:cursor-default'
// onClick={() => updateState(defaultVal)}
// >
// <svg
// className='size-5 group-disabled:stroke-current group-disabled:fill-current group-hover:stroke-red-600 group-hover:fill-red-600 duration-200'
// fill="currentColor" strokeWidth="0" viewBox="0 0 16 16" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M3.5 2v3.5L4 6h3.5V5H4.979l.941-.941a3.552 3.552 0 1 1 5.023 5.023L5.746 14.28l.72.72 5.198-5.198A4.57 4.57 0 0 0 5.2 3.339l-.7.7V2h-1z"></path>
// </svg>
// </button>
// const inputElement = enumArr === undefined ?
// // string
// (<input
// className='input p-1 w-full'
// type="text"
// value={val}
// onChange={(e) => updateState(e.target.value)}
// />)
// :
// // enum
// (<select
// className='dropdown p-1 w-full'
// value={val}
// onChange={(e) => updateState(e.target.value)}
// >
// {enumArr.map((option) => (
// <option key={option} value={option}>
// {option}
// </option>
// ))}
// </select>)
// return <div>
// <label className='hidden'>{param}</label>
// <span>{description}</span>
// <div className='flex items-center'>
// {inputElement}
// {resetButton}
// </div>
// </div>
// }
// export const SidebarSettings = () => {
// const { voidConfig, voidConfigInfo } = useVoidConfig()
// const current_field = voidConfig.default['whichApi'] as VoidConfigField
// return (
// <div className='space-y-4 py-2 overflow-y-auto'>
// {/* choose the field */}
// <div className='outline-vscode-input-bg'>
// <SettingOfFieldAndParam
// field='default'
// param='whichApi'
// />
// <SettingOfFieldAndParam
// field='default'
// param='maxTokens'
// />
// </div>
// <hr />
// {/* render all fields, but hide the ones not visible for fast tab switching */}
// {configFields.map(field => {
// return <div
// key={field}
// className={`flex flex-col gap-y-2 ${field !== current_field ? 'hidden' : ''}`}
// >
// {Object.keys(voidConfigInfo[field]).map((param) => (
// <SettingOfFieldAndParam
// key={param}
// field={field}
// param={param}
// />
// ))}
// </div>
// })}
// {/* Remove this after 10/21/24, this is just to give developers a heads up about the recent change */}
// <div className='pt-20'>
// {`We recently updated Settings. To copy your old Void settings over, press Ctrl+Shift+P, `}
// {`type 'Open User Settings (JSON)',`}
// {` and look for 'void.'. `}
// </div>
// </div>
// )
// }

View file

@ -0,0 +1,79 @@
// import React from "react";
// import { ThreadsProvider, useThreads } from "../util/contextForThreads";
// const truncate = (s: string) => {
// let len = s.length
// const TRUNC_AFTER = 16
// if (len >= TRUNC_AFTER)
// s = s.substring(0, TRUNC_AFTER) + '...'
// return s
// }
// export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
// 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)
// return (
// <div className="flex flex-col gap-y-1">
// {/* X button at top right */}
// <div className="text-right">
// <button className="btn btn-sm" onClick={onClose}>
// <svg
// xmlns="http://www.w3.org/2000/svg"
// fill="none"
// viewBox="0 0 24 24"
// stroke="currentColor"
// className="size-4"
// >
// <path
// strokeLinecap="round"
// strokeLinejoin="round"
// d="M6 18 18 6M6 6l12 12"
// />
// </svg>
// </button>
// </div>
// {/* a list of all the past threads */}
// <div className='flex flex-col gap-y-1 max-h-80 overflow-y-auto'>
// {sortedThreadIds.map((threadId) => {
// if (!allThreads)
// return <>Error: Threads not found.</>
// const pastThread = allThreads[threadId]
// let btnStringArr = []
// let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)')
// btnStringArr.push(msg1)
// let msg2 = truncate(allThreads[threadId].messages[1]?.displayContent ?? '')
// if (msg2)
// btnStringArr.push(msg2)
// btnStringArr.push(allThreads[threadId].messages.length)
// const btnString = btnStringArr.join(' / ')
// return (
// <button
// key={pastThread.id}
// className={`btn btn-sm rounded-sm ${pastThread.id === getCurrentThread()?.id ? "btn-primary" : "btn-secondary"}`}
// onClick={() => switchToThread(pastThread.id)}
// title={new Date(pastThread.createdAt).toLocaleString()}
// >
// {btnString}
// </button>
// )
// })}
// </div>
// </div>
// )
// }

View file

@ -0,0 +1,97 @@
// import * as vscode from 'vscode';
// import { PartialVoidConfig } from '../webviews/common/contextForConfig'
// // type CodeSelection = { selectionStr: string, filePath: vscode.Uri }
// // type File = { filepath: vscode.Uri, content: string }
// // an area that is currently being diffed
// type DiffArea = {
// diffareaid: number,
// startLine: number,
// endLine: number,
// originalStartLine: number,
// originalEndLine: number,
// sweepIndex: number | null // null iff not sweeping
// }
// // the return type of diff creator
// type BaseDiff = {
// type: 'edit' | 'insertion' | 'deletion';
// // repr: string; // representation of the diff in text
// originalRange: vscode.Range;
// originalCode: string;
// range: vscode.Range;
// code: string;
// }
// // each diff on the user's screen
// type Diff = {
// diffid: number,
// lenses: vscode.CodeLens[],
// } & BaseDiff
// // editor -> sidebar
// type MessageToSidebar = (
// | { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor. selection and path are frozen snapshots
// | { type: 'ctrl+k', selection: CodeSelection }
// | { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
// | { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig }
// | { type: 'allThreads', threads: ChatThreads }
// | { type: 'startNewThread' }
// | { type: 'toggleThreadSelector' }
// | { type: 'toggleSettings' }
// | { type: 'deviceId', deviceId: string }
// )
// // sidebar -> editor
// type MessageFromSidebar = (
// | { type: 'applyChanges', diffRepr: string } // user clicks "apply" in the sidebar
// | { type: 'requestFiles', filepaths: vscode.Uri[] }
// | { type: 'getPartialVoidConfig' }
// | { type: 'persistPartialVoidConfig', partialVoidConfig: PartialVoidConfig }
// | { type: 'getAllThreads' }
// | { type: 'persistThread', thread: ChatThreads[string] }
// | { type: 'getDeviceId' }
// )
// // type ChatThreads = {
// // [id: string]: {
// // id: string; // store the id here too
// // createdAt: string; // ISO string
// // lastModified: string; // ISO string
// // messages: ChatMessage[];
// // }
// // }
// // type ChatMessage =
// // | {
// // role: "user";
// // content: string; // content sent to the llm
// // displayContent: string; // content displayed to user
// // selection: CodeSelection | null; // the user's selection
// // files: vscode.Uri[]; // the files sent in the message
// // }
// // | {
// // role: "assistant";
// // content: string; // content received from LLM
// // displayContent: string | undefined; // content displayed to user (this is the same as content for now)
// // }
// // | {
// // role: "system";
// // content: string;
// // displayContent?: undefined;
// // }
// export {
// BaseDiff, Diff,
// DiffArea,
// CodeSelection,
// File,
// MessageFromSidebar,
// MessageToSidebar,
// ChatThreads,
// ChatMessage,
// }

View file

@ -0,0 +1,17 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['./sidebar-tsx/Sidebar.tsx'], // You'll need to create this index file
outDir: './out',
format: ['esm'],
// dts: true,
splitting: false,
sourcemap: true,
clean: true,
platform: 'browser',
target: 'esnext',
outExtension: () => ({ js: '.js' }),
external: [/\.\.\/\.\.\/.*/],
noExternal: ['react', 'react-dom'],
treeshake: true,
})

View file

@ -1,5 +1,6 @@
// import React, { useEffect } from "react";
// import * as ReactDOM from "react-dom/client"
import React from "react";
import * as ReactDOM from "react-dom/client"
import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation';
// import { initPosthog, identifyUser } from "./posthog";
// const ListenersAndTracking = () => {
@ -32,30 +33,11 @@
// export const mount = (children: React.ReactNode) => {
// if (typeof document === "undefined") {
// console.error("index.tsx error: document was undefined")
// return
// }
// // mount the sidebar on the id="root" element
// const rootElement = document.getElementById("root")!
// // console.log("Void root Element:", rootElement)
// const content = (<>
// <ListenersAndTracking />
// <PropsProvider rootElement={rootElement}>
// <ThreadsProvider>
// <ConfigProvider>
// {children}
// </ConfigProvider>
// </ThreadsProvider>
// </PropsProvider>
// </>)
// const root = ReactDOM.createRoot(rootElement)
// root.render(content);
// }
export const mountFnGenerator = (Component: React.FC<{ accessor: ServicesAccessor }>) => (rootElement: HTMLElement, accessor: ServicesAccessor) => {
if (typeof document === "undefined") {
console.error("index.tsx error: document was undefined")
return
}
const root = ReactDOM.createRoot(rootElement)
root.render(<Component accessor={accessor} />);
}

View file

@ -1,43 +1,43 @@
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
// import { Disposable } from '../../../../base/common/lifecycle.js';
// import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions';
// import { createDecorator } from '../../../../platform/instantiation/common/instantiation';
// import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
interface IMetricsService {
readonly _serviceBrand: undefined;
}
// interface IMetricsService {
// readonly _serviceBrand: undefined;
// }
const IMetricsService = createDecorator<IMetricsService>('inlineDiffService');
class MetricsService extends Disposable implements IMetricsService {
_serviceBrand: undefined;
// const IMetricsService = createDecorator<IMetricsService>('inlineDiffService');
// class MetricsService extends Disposable implements IMetricsService {
// _serviceBrand: undefined;
constructor(
@ITelemetryService private readonly _telemetryService: ITelemetryService
) {
super()
}
// constructor(
// @ITelemetryService private readonly _telemetryService: ITelemetryService
// ) {
// super()
// }
init() {
// init() {
posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2',
{
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only' // we only track events from identified users. We identify them in Sidebar
}
)
// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2',
// {
// api_host: 'https://us.i.posthog.com',
// person_profiles: 'identified_only' // we only track events from identified users. We identify them in Sidebar
// }
// )
const deviceId = this._telemetryService.devDeviceId
console.debug('deviceId', deviceId)
// const deviceId = this._telemetryService.devDeviceId
// console.debug('deviceId', deviceId)
posthog.identify(deviceId)
// posthog.identify(deviceId)
// export const captureEvent = (eventId: string, properties: object) => {
// posthog.capture(eventId, properties)
// }
// // export const captureEvent = (eventId: string, properties: object) => {
// // posthog.capture(eventId, properties)
// // }
}
// }
}
// }
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
// registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);

View file

@ -37,9 +37,11 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
// import { IVoidSettingsService } from './registerSettings.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
// import { IEditorService } from '../../../services/editor/common/editorService.js';
// import mountFn from './react/out/Sidebar.js';
// import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
const mountFn = (...params: any) => { }
// compare against search.contribution.ts and https://app.greptile.com/chat/w1nsmt3lauwzculipycpn?repo=github%3Amain%3Amicrosoft%2Fvscode
@ -68,8 +70,8 @@ class VoidSidebarViewPane extends ViewPane {
@ITelemetryService telemetryService: ITelemetryService,
@IHoverService hoverService: IHoverService,
// Void:
@IVoidSidebarStateService private readonly _voidSidebarStateService: IVoidSidebarStateService,
@IThreadHistoryService private readonly _threadHistoryService: IThreadHistoryService,
// @IVoidSidebarStateService private readonly _voidSidebarStateService: IVoidSidebarStateService,
// @IThreadHistoryService private readonly _threadHistoryService: IThreadHistoryService,
// TODO chat service
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService)
@ -83,61 +85,54 @@ class VoidSidebarViewPane extends ViewPane {
// <div className={`flex flex-col h-screen w-full`}>
const { root, chat, history, settings } = dom.h('div@root', [
dom.h('div@chat', []),
dom.h('div@history', []),
dom.h('div@settings', []),
])
root.style.display = 'flex';
root.style.flexDirection = 'column';
root.style.height = '100vh';
root.style.width = '100%';
const { root } = dom.h('div@root')
dom.append(parent, root);
this._renderChat(chat);
this._renderHistory(history);
this._renderSettings(settings);
// gets set immediately
let accessor_: ServicesAccessor = null as unknown as ServicesAccessor
this.instantiationService.invokeFunction(accessor => { accessor_ = accessor });
mountFn(root, accessor_);
}
private _renderChat(element: HTMLElement) {
// <div className={`${tab !== 'chat' && tab !== 'threadSelector' ? 'hidden' : ''}`}>
// <SidebarChat chatInputRef={chatInputRef} />
// </div>
// private _renderChat(element: HTMLElement) {
// // useEffect(() => {
// // this._voidSidebarStateService.onDidChange(() => {
// // })
// // this._voidSidebarStateService.onFocusChat(() => {
// // })
// // this._voidSidebarStateService.onBlurChat(() => {
// // })
// // })
this._voidSidebarStateService.onDidChange(() => {
})
// }
this._voidSidebarStateService.onFocusChat(() => {
})
this._voidSidebarStateService.onBlurChat(() => {
})
}
// private _renderHistory(element: HTMLElement) {
// // <div className={`mb-2 h-[30vh] ${tab !== 'threadSelector' ? 'hidden' : ''}`}>
// // <SidebarThreadSelector onClose={() => setTab('chat')} />
// // </div>
private _renderHistory(element: HTMLElement) {
// <div className={`mb-2 h-[30vh] ${tab !== 'threadSelector' ? 'hidden' : ''}`}>
// <SidebarThreadSelector onClose={() => setTab('chat')} />
// </div>
this._voidSidebarStateService.onDidChange(() => {
})
// this._voidSidebarStateService.onDidChange(() => {
// })
this._threadHistoryService.onDidChangeCurrentThread(() => {
// this._threadHistoryService.onDidChangeCurrentThread(() => {
})
// })
}
// }
private _renderSettings(element: HTMLElement) {
// <div className={`${tab !== 'settings' ? 'hidden' : ''}`}>
// <SidebarSettings />
// </div>
// private _renderSettings(element: HTMLElement) {
// // <div className={`${tab !== 'settings' ? 'hidden' : ''}`}>
// // <SidebarSettings />
// // </div>
}
// }
}
@ -146,7 +141,7 @@ class VoidSidebarViewPane extends ViewPane {
// ---------- Register viewpane inside the void container ----------
const voidThemeIcon = Codicon.search;
const voidThemeIcon = Codicon.array;
const voidViewIcon = registerIcon('void-view-icon', voidThemeIcon, localize('voidViewIcon', 'View icon of the Void chat view.'));
// called VIEWLET_ID in other places for some reason
@ -265,7 +260,7 @@ registerAction2(class extends Action2 {
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.focusChat()
const selection = accessor.get(IEditorService).activeTextEditorControl?.getSelection()
// const selection = accessor.get(IEditorService).activeTextEditorControl?.getSelection()
// chat state:

View file

@ -1,359 +0,0 @@
import React, { FormEvent, useCallback, useEffect, useRef, useState } from "react";
import { marked } from 'marked';
import MarkdownRender from "./markdown/MarkdownRender";
import BlockCode from "./markdown/BlockCode";
import { File, ChatMessage, CodeSelection } from "../../common/shared_types";
import * as vscode from 'vscode'
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "../util/getVscodeApi";
import { useThreads } from "../util/contextForThreads";
import { sendLLMMessage } from "../../common/sendLLMMessage";
import { useVoidConfig } from "../util/contextForConfig";
import { captureEvent } from "../util/posthog";
import { generateDiffInstructions } from "../../common/systemPrompts";
const filesStr = (fullFiles: File[]) => {
return fullFiles.map(({ filepath, content }) =>
`
${filepath.fsPath}
\`\`\`
${content}
\`\`\``).join('\n')
}
const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
let str = '';
if (files.length > 0) {
str += filesStr(files);
}
if (selection) {
str += `
I am currently selecting this code:
\t\`\`\`${selection.selectionStr}\`\`\`
`;
}
if (files.length > 0 && selection) {
str += `
Please edit the selected code or the entire file following these instructions:
`;
} else if (files.length > 0) {
str += `
Please edit the file following these instructions:
`;
} else if (selection) {
str += `
Please edit the selected code following these instructions:
`;
}
str += `
\t${instructions}
`;
if (files.length > 0) {
str += `
\tIf you make a change, rewrite the entire file.
`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
}
return str;
};
const getBasename = (pathStr: string) => {
// "unixify" path
pathStr = pathStr.replace(/[/\\]+/g, "/") // replace any / or \ or \\ with /
const parts = pathStr.split("/") // split on /
return parts[parts.length - 1]
}
export const SelectedFiles = ({ files, setFiles, }: { files: vscode.Uri[], setFiles: null | ((files: vscode.Uri[]) => void) }) => {
return (
files.length !== 0 && (
<div className="flex flex-wrap -mx-1 -mb-1">
{files.map((filename, i) => (
<button
key={filename.path}
disabled={!setFiles}
className={`btn btn-secondary btn-sm border border-vscode-input-border rounded flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default`}
type="button"
onClick={() => setFiles?.([...files.slice(0, i), ...files.slice(i + 1, Infinity)])}
>
<span>{getBasename(filename.fsPath)}</span>
{/* X button */}
{!!setFiles && <span className="">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</span>}
</button>
))}
</div>
)
)
}
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
const role = chatMessage.role
const children = chatMessage.displayContent
if (!children)
return null
let chatbubbleContents: React.ReactNode
if (role === 'user') {
chatbubbleContents = <>
<SelectedFiles files={chatMessage.files} setFiles={null} />
{chatMessage.selection?.selectionStr && <BlockCode
text={chatMessage.selection.selectionStr}
buttonsOnHover={null}
/>}
{children}
</>
}
else if (role === 'assistant') {
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
}
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
{chatbubbleContents}
</div>
</div>
}
export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HTMLTextAreaElement> }) => {
// // 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) {
// switchToThread(threadId)
// return
// }
// }
// // start a new thread
// startNewThread()
// })
// // if user pressed ctrl+l, add their selection to the sidebar
// useOnVSCodeMessage('ctrl+l', (m) => {
// setSelection(m.selection)
// const filepath = m.selection.filePath
// // add current file to the context if it's not already in the files array
// if (!files.find(f => f.fsPath === filepath.fsPath))
// setFiles(files => [...files, filepath])
// })
// state of current message
const [selection, setSelection] = useState<CodeSelection | null>(null) // the code the user is selecting
const [files, setFiles] = useState<vscode.Uri[]>([]) // the names of the files in the chat
const [instructions, setInstructions] = useState('') // the user's instructions
// state of chat
const [messageStream, setMessageStream] = useState('')
const [isLoading, setIsLoading] = useState(false)
const abortFnRef = useRef<(() => void) | null>(null)
const [latestError, setLatestError] = useState('')
// higher level state
const { getAllThreads, getCurrentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads()
const { voidConfig } = useVoidConfig()
const isDisabled = !instructions
const formRef = useRef<HTMLFormElement | null>(null)
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (isDisabled) return
if (isLoading) return
setIsLoading(true)
setInstructions('');
formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens
setSelection(null)
setFiles([])
setLatestError('')
// request file content from vscode and await response
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
const relevantFiles = await awaitVSCodeResponse('files')
// add system message to chat history
const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
addMessageToHistory(systemPromptElt)
const userContent = userInstructionsStr(instructions, relevantFiles.files, selection)
const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files }
addMessageToHistory(newHistoryElt)
// send message to LLM
sendLLMMessage({
logging: { loggingName: 'Chat' },
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
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content }
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
},
onError: (error) => {
// add assistant's message to chat history, and clear selection
let content = messageStream; // just use the current content
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
setLatestError(error)
},
voidConfig,
abortRef: abortFnRef,
})
}
const onAbort = useCallback(() => {
// abort claude
abortFnRef.current?.()
// if messageStream was not empty, add it to the history
const llmContent = messageStream || '(null)'
const newHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream, }
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
}, [messageStream, addMessageToHistory])
return <>
<div className="overflow-x-hidden space-y-4">
{/* previous messages */}
{getCurrentThread() !== null && getCurrentThread()?.messages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
)}
{/* message stream */}
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
</div>
{/* chatbar */}
<div className="shrink-0 py-4">
{/* selection */}
<div className="text-left">
<div className="relative">
<div className="input">
{/* selection */}
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
{/* selected files */}
<SelectedFiles files={files} setFiles={setFiles} />
{/* selected code */}
{!!selection?.selectionStr && (
<BlockCode text={selection.selectionStr}
buttonsOnHover={(
<button
onClick={() => setSelection(null)}
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
>
Remove
</button>
)} />
)}
</div>}
<form
ref={formRef}
className="flex flex-row items-center rounded-md p-2"
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
onSubmit={(e) => {
console.log('submit!')
onSubmit(e)
}}>
{/* input */}
<textarea
ref={chatInputRef}
onChange={(e) => { setInstructions(e.target.value) }}
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
placeholder="Ctrl+L to select"
rows={1}
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
/>
{isLoading ?
// stop button
<button
onClick={onAbort}
type='button'
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
>
<svg
className='scale-50'
stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M24 24H0V0h24v24z"></path>
</svg>
</button>
:
// submit button (up arrow)
<button
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
disabled={isDisabled}
type='submit'
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</button>
}
</form>
</div>
</div>
</div>
{/* error message */}
{!latestError ? null : <div>
{latestError}
</div>}
</div>
</>
}

View file

@ -1,105 +0,0 @@
import React, { useState } from "react";
import { configFields, useVoidConfig, VoidConfigField } from "../util/contextForConfig";
const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => {
const { voidConfig, partialVoidConfig, voidConfigInfo, setConfigParam } = useVoidConfig()
const { enumArr, defaultVal, description } = voidConfigInfo[field][param]
const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item
const updateState = (newValue: string) => { setConfigParam(field, param, newValue) }
const resetButton = <button
disabled={val === defaultVal}
title={val === defaultVal ? 'This is the default value.' : `Revert value to '${defaultVal}'?`}
className='group btn btn-sm disabled:opacity-75 disabled:cursor-default'
onClick={() => updateState(defaultVal)}
>
<svg
className='size-5 group-disabled:stroke-current group-disabled:fill-current group-hover:stroke-red-600 group-hover:fill-red-600 duration-200'
fill="currentColor" strokeWidth="0" viewBox="0 0 16 16" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M3.5 2v3.5L4 6h3.5V5H4.979l.941-.941a3.552 3.552 0 1 1 5.023 5.023L5.746 14.28l.72.72 5.198-5.198A4.57 4.57 0 0 0 5.2 3.339l-.7.7V2h-1z"></path>
</svg>
</button>
const inputElement = enumArr === undefined ?
// string
(<input
className='input p-1 w-full'
type="text"
value={val}
onChange={(e) => updateState(e.target.value)}
/>)
:
// enum
(<select
className='dropdown p-1 w-full'
value={val}
onChange={(e) => updateState(e.target.value)}
>
{enumArr.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>)
return <div>
<label className='hidden'>{param}</label>
<span>{description}</span>
<div className='flex items-center'>
{inputElement}
{resetButton}
</div>
</div>
}
export const SidebarSettings = () => {
const { voidConfig, voidConfigInfo } = useVoidConfig()
const current_field = voidConfig.default['whichApi'] as VoidConfigField
return (
<div className='space-y-4 py-2 overflow-y-auto'>
{/* choose the field */}
<div className='outline-vscode-input-bg'>
<SettingOfFieldAndParam
field='default'
param='whichApi'
/>
<SettingOfFieldAndParam
field='default'
param='maxTokens'
/>
</div>
<hr />
{/* render all fields, but hide the ones not visible for fast tab switching */}
{configFields.map(field => {
return <div
key={field}
className={`flex flex-col gap-y-2 ${field !== current_field ? 'hidden' : ''}`}
>
{Object.keys(voidConfigInfo[field]).map((param) => (
<SettingOfFieldAndParam
key={param}
field={field}
param={param}
/>
))}
</div>
})}
{/* Remove this after 10/21/24, this is just to give developers a heads up about the recent change */}
<div className='pt-20'>
{`We recently updated Settings. To copy your old Void settings over, press Ctrl+Shift+P, `}
{`type 'Open User Settings (JSON)',`}
{` and look for 'void.'. `}
</div>
</div>
)
}

View file

@ -1,79 +0,0 @@
import React from "react";
import { ThreadsProvider, useThreads } from "../util/contextForThreads";
const truncate = (s: string) => {
let len = s.length
const TRUNC_AFTER = 16
if (len >= TRUNC_AFTER)
s = s.substring(0, TRUNC_AFTER) + '...'
return s
}
export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
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)
return (
<div className="flex flex-col gap-y-1">
{/* X button at top right */}
<div className="text-right">
<button className="btn btn-sm" onClick={onClose}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* a list of all the past threads */}
<div className='flex flex-col gap-y-1 max-h-80 overflow-y-auto'>
{sortedThreadIds.map((threadId) => {
if (!allThreads)
return <>Error: Threads not found.</>
const pastThread = allThreads[threadId]
let btnStringArr = []
let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)')
btnStringArr.push(msg1)
let msg2 = truncate(allThreads[threadId].messages[1]?.displayContent ?? '')
if (msg2)
btnStringArr.push(msg2)
btnStringArr.push(allThreads[threadId].messages.length)
const btnString = btnStringArr.join(' / ')
return (
<button
key={pastThread.id}
className={`btn btn-sm rounded-sm ${pastThread.id === getCurrentThread()?.id ? "btn-primary" : "btn-secondary"}`}
onClick={() => switchToThread(pastThread.id)}
title={new Date(pastThread.createdAt).toLocaleString()}
>
{btnString}
</button>
)
})}
</div>
</div>
)
}

View file

@ -1,63 +0,0 @@
// import React, { useState, useRef } from '../void-imports/react.js'
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
// import { SidebarChat } from './SidebarChat.js';
// import { SidebarSettings } from './SidebarSettings.js';
// const Sidebar = () => {
// const chatInputRef = useRef<HTMLTextAreaElement | null>(null)
// const [tab, setTab] = useState<'threadSelector' | 'chat' | 'settings'>('chat')
// // // if they pressed the + to add a new chat
// // useOnVSCodeMessage('startNewThread', (m) => {
// // setTab('chat');
// // chatInputRef.current?.focus();
// // })
// // // ctrl+l should switch back to chat
// // useOnVSCodeMessage('ctrl+l', (m) => {
// // setTab('chat');
// // chatInputRef.current?.focus();
// // })
// // // if they toggled thread selector
// // useOnVSCodeMessage('toggleThreadSelector', (m) => {
// // if (tab === 'threadSelector') {
// // setTab('chat')
// // chatInputRef.current?.blur();
// // } else
// // setTab('threadSelector')
// // })
// // // if they toggled settings
// // useOnVSCodeMessage('toggleSettings', (m) => {
// // if (tab === 'settings') {
// // setTab('chat')
// // chatInputRef.current?.blur();
// // } else
// // setTab('settings')
// // })
// return <>
// <div className={`flex flex-col h-screen w-full`}>
// <div className={`mb-2 h-[30vh] ${tab !== 'threadSelector' ? 'hidden' : ''}`}>
// <SidebarThreadSelector onClose={() => setTab('chat')} />
// </div>
// <div className={`${tab !== 'chat' && tab !== 'threadSelector' ? 'hidden' : ''}`}>
// <SidebarChat chatInputRef={chatInputRef} />
// </div>
// <div className={`${tab !== 'settings' ? 'hidden' : ''}`}>
// <SidebarSettings />
// </div>
// </div>
// </>
// }
// export default Sidebar

View file

@ -1,30 +0,0 @@
// // renders the code from `src/sidebar`
// import * as vscode from 'vscode';
// import { updateWebviewHTML as _updateWebviewHTML } from './src/extension/extensionLib/updateWebviewHTML';
// export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
// public static readonly viewId = 'void.viewnumberone';
// public webview: Promise<vscode.Webview> // used to send messages to the webview, resolved by _res in resolveWebviewView
// private _res: (c: vscode.Webview) => void // used to resolve the webview
// private readonly _extensionUri: vscode.Uri
// constructor(context: vscode.ExtensionContext) {
// // const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code
// this._extensionUri = context.extensionUri
// let temp_res: typeof this._res | undefined = undefined
// this.webview = new Promise((res, rej) => { temp_res = res })
// if (!temp_res) throw new Error("Void sidebar provider: resolver was undefined")
// this._res = temp_res
// }
// // called internally by vscode
// resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken,) {
// const webview = webviewView.webview;
// _updateWebviewHTML(webview, this._extensionUri, { jsOutLocation: 'dist/webviews/sidebar/index.js', cssOutLocation: 'dist/webviews/styles.css' })
// this._res(webview); // resolve webview and _webviewView
// }
// }

View file

@ -1,289 +0,0 @@
// import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
// import { awaitVSCodeResponse, getVSCodeAPI, useOnVSCodeMessage } from "./getVscodeApi"
// const configEnum = <EnumArr extends readonly string[]>(description: string, defaultVal: EnumArr[number], enumArr: EnumArr) => {
// return {
// description,
// defaultVal,
// enumArr,
// }
// }
// const configString = (description: string, defaultVal: string) => {
// return {
// description,
// defaultVal,
// enumArr: undefined,
// }
// }
// // fields you can customize (don't forget 'default' - it isn't included here!)
// export const configFields = [
// 'anthropic',
// 'openAI',
// 'gemini',
// 'greptile',
// 'ollama',
// 'openRouter',
// 'openAICompatible',
// 'azure',
// ] as const
// const voidConfigInfo: Record<
// typeof configFields[number] | 'default', {
// [prop: string]: {
// description: string,
// enumArr?: readonly string[] | undefined,
// defaultVal: string,
// },
// }
// > = {
// default: {
// whichApi: configEnum(
// "API Provider.",
// 'anthropic',
// configFields,
// ),
// maxTokens: configEnum(
// "Max number of tokens to output.",
// '1024',
// [
// "default", // this will be parseInt'd into NaN and ignored by the API. Anything that's not a number has this behavior.
// "1024",
// "2048",
// "4096",
// "8192"
// ] as const,
// ),
// },
// anthropic: {
// apikey: configString('Anthropic API key.', ''),
// model: configEnum(
// "Anthropic model to use.",
// 'claude-3-5-sonnet-20240620',
// [
// "claude-3-5-sonnet-20240620",
// "claude-3-opus-20240229",
// "claude-3-sonnet-20240229",
// "claude-3-haiku-20240307"
// ] as const,
// ),
// },
// openAI: {
// apikey: configString('OpenAI API key.', ''),
// model: configEnum(
// 'OpenAI model to use.',
// 'gpt-4o',
// [
// "o1-preview",
// "o1-mini",
// "gpt-4o",
// "gpt-4o-2024-05-13",
// "gpt-4o-2024-08-06",
// "gpt-4o-mini",
// "gpt-4o-mini-2024-07-18",
// "gpt-4-turbo",
// "gpt-4-turbo-2024-04-09",
// "gpt-4-turbo-preview",
// "gpt-4-0125-preview",
// "gpt-4-1106-preview",
// "gpt-4",
// "gpt-4-0613",
// "gpt-3.5-turbo-0125",
// "gpt-3.5-turbo",
// "gpt-3.5-turbo-1106"
// ] as const
// ),
// },
// greptile: {
// apikey: configString('Greptile API key.', ''),
// githubPAT: configString('Github PAT that Greptile uses to access your repository', ''),
// remote: configEnum(
// 'Repo location',
// 'github',
// [
// 'github',
// 'gitlab'
// ] as const
// ),
// repository: configString('Repository identifier in "owner/repository" format.', ''),
// branch: configString('Name of the branch to use.', 'main'),
// },
// ollama: {
// endpoint: configString(
// 'The endpoint of your Ollama instance. Start Ollama by running `OLLAMA_ORIGINS="vscode-webview://*" ollama serve`.',
// 'http://127.0.0.1:11434'
// ),
// // TODO we should allow user to select model inside Void, but for now we'll just let them handle the Ollama setup on their own
// // model: configEnum(
// // 'Ollama model to use.',
// // 'llama3.1',
// // ["codegemma", "codegemma:2b", "codegemma:7b", "codellama", "codellama:7b", "codellama:13b", "codellama:34b", "codellama:70b", "codellama:code", "codellama:python", "command-r", "command-r:35b", "command-r-plus", "command-r-plus:104b", "deepseek-coder-v2", "deepseek-coder-v2:16b", "deepseek-coder-v2:236b", "falcon2", "falcon2:11b", "firefunction-v2", "firefunction-v2:70b", "gemma", "gemma:2b", "gemma:7b", "gemma2", "gemma2:2b", "gemma2:9b", "gemma2:27b", "llama2", "llama2:7b", "llama2:13b", "llama2:70b", "llama3", "llama3:8b", "llama3:70b", "llama3-chatqa", "llama3-chatqa:8b", "llama3-chatqa:70b", "llama3-gradient", "llama3-gradient:8b", "llama3-gradient:70b", "llama3.1", "llama3.1:8b", "llama3.1:70b", "llama3.1:405b", "llava", "llava:7b", "llava:13b", "llava:34b", "llava-llama3", "llava-llama3:8b", "llava-phi3", "llava-phi3:3.8b", "mistral", "mistral:7b", "mistral-large", "mistral-large:123b", "mistral-nemo", "mistral-nemo:12b", "mixtral", "mixtral:8x7b", "mixtral:8x22b", "moondream", "moondream:1.8b", "openhermes", "openhermes:v2.5", "phi3", "phi3:3.8b", "phi3:14b", "phi3.5", "phi3.5:3.8b", "qwen", "qwen:7b", "qwen:14b", "qwen:32b", "qwen:72b", "qwen:110b", "qwen2", "qwen2:0.5b", "qwen2:1.5b", "qwen2:7b", "qwen2:72b", "smollm", "smollm:135m", "smollm:360m", "smollm:1.7b"] as const
// // ),
// },
// openRouter: {
// model: configString(
// 'OpenRouter model to use.',
// 'openai/gpt-4o'
// ),
// apikey: configString('OpenRouter API key.', ''),
// },
// openAICompatible: {
// endpoint: configString('The baseUrl (exluding /chat/completions).', 'http://127.0.0.1:11434/v1'),
// model: configString('The name of the model to use.', 'gpt-4o'),
// apikey: configString('Your API key.', ''),
// },
// azure: {
// // "void.azure.apiKey": {
// // "type": "string",
// // "description": "Azure API key."
// // },
// // "void.azure.deploymentId": {
// // "type": "string",
// // "description": "Azure API deployment ID."
// // },
// // "void.azure.resourceName": {
// // "type": "string",
// // "description": "Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`"
// // },
// // "void.azure.providerSettings": {
// // "type": "object",
// // "properties": {
// // "baseURL": {
// // "type": "string",
// // "default": "https://${resourceName}.openai.azure.com/openai/deployments",
// // "description": "Azure API base URL."
// // },
// // "headers": {
// // "type": "object",
// // "description": "Custom headers to include in the requests."
// // }
// // }
// // },
// },
// gemini: {
// apikey: configString('Google API key.', ''),
// model: configEnum(
// 'Gemini model to use.',
// 'gemini-1.5-flash',
// [
// "gemini-1.5-flash",
// "gemini-1.5-pro",
// "gemini-1.5-flash-8b",
// "gemini-1.0-pro"
// ] as const
// ),
// },
// }
// // this is the type that comes with metadata like desc, default val, etc
// type VoidConfigInfo = typeof voidConfigInfo
// export type VoidConfigField = keyof typeof voidConfigInfo // typeof configFields[number]
// // this is the type that specifies the user's actual config
// export type PartialVoidConfig = {
// [K in keyof typeof voidConfigInfo]?: {
// [P in keyof typeof voidConfigInfo[K]]?: typeof voidConfigInfo[K][P]['defaultVal']
// }
// }
// export type VoidConfig = {
// [K in keyof typeof voidConfigInfo]: {
// [P in keyof typeof voidConfigInfo[K]]: typeof voidConfigInfo[K][P]['defaultVal']
// }
// }
// export const getVoidConfigFromPartial = (partialVoidConfig: PartialVoidConfig): VoidConfig => {
// const config = {} as PartialVoidConfig
// for (let field of [...configFields, 'default'] as const) {
// config[field] = {}
// for (let prop in voidConfigInfo[field]) {
// config[field][prop] = partialVoidConfig[field]?.[prop]?.trim() || voidConfigInfo[field][prop].defaultVal
// }
// }
// return config as VoidConfig
// }
// const defaultVoidConfig: VoidConfig = getVoidConfigFromPartial({})
// // const [stateRef, setState] = useInstantState(initVal)
// // setState instantly changes the value of stateRef instead of having to wait until the next render
// const useInstantState = <T,>(initVal: T) => {
// const stateRef = useRef<T>(initVal)
// const [_, setS] = useState<T>(initVal)
// const setState = useCallback((newVal: T) => {
// setS(newVal);
// stateRef.current = newVal;
// }, [])
// return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
// }
// type SetConfigParamType = <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => void
// type ConfigValueType = {
// voidConfig: VoidConfig,
// voidConfigInfo: VoidConfigInfo,
// partialVoidConfig: PartialVoidConfig,
// setConfigParam: SetConfigParamType
// }
// const ConfigContext = createContext<ConfigValueType>(undefined as unknown as ConfigValueType)
// export function ConfigProvider({ children }: { children: ReactNode }) {
// const [partialVoidConfig, setPartialVoidConfig] = useInstantState<PartialVoidConfig>({}) // the user's selections
// const [voidConfig, setVoidConfig] = useState<VoidConfig>(defaultVoidConfig)
// // get the config on mount
// useEffect(() => {
// getVSCodeAPI().postMessage({ type: 'getPartialVoidConfig' })
// awaitVSCodeResponse('partialVoidConfig').then((m) => {
// setPartialVoidConfig(m.partialVoidConfig)
// const newFullConfig = getVoidConfigFromPartial(m.partialVoidConfig)
// setVoidConfig(newFullConfig)
// })
// }, [setPartialVoidConfig])
// // return the provider
// return (<ConfigContext.Provider
// value={{
// voidConfig,
// voidConfigInfo,
// partialVoidConfig: partialVoidConfig.current ?? {},
// setConfigParam: (field, param, newVal) => {
// const newPartialConfig: PartialVoidConfig = {
// ...partialVoidConfig.current,
// [field]: {
// ...partialVoidConfig.current?.[field],
// [param]: newVal
// }
// }
// setPartialVoidConfig(newPartialConfig)
// const newFullConfig = getVoidConfigFromPartial(newPartialConfig)
// setVoidConfig(newFullConfig)
// getVSCodeAPI().postMessage({ type: 'persistPartialVoidConfig', partialVoidConfig: newPartialConfig })
// }
// }}
// >
// {children}
// </ConfigContext.Provider>
// )
// }
// export function useVoidConfig(): ConfigValueType {
// const context = useContext<ConfigValueType>(ConfigContext)
// if (context === undefined) {
// throw new Error("useVoidConfig missing Provider")
// }
// return context
// }

View file

@ -1,106 +0,0 @@
// import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
// import { ChatMessage, ChatThreads } from "../../common/shared_types"
// import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
// // a "thread" means a chat message history
// type ConfigForThreadsValueType = {
// readonly getAllThreads: () => ChatThreads;
// readonly getCurrentThread: () => ChatThreads[string] | null;
// addMessageToHistory: (message: ChatMessage) => void;
// switchToThread: (threadId: string) => void;
// startNewThread: () => void;
// }
// const ThreadsContext = createContext<ConfigForThreadsValueType>(undefined as unknown as ConfigForThreadsValueType)
// const createNewThread = () => {
// const now = new Date().toISOString()
// return {
// id: new Date().getTime().toString(),
// createdAt: now,
// lastModified: now,
// messages: [],
// }
// }
// // const [stateRef, setState] = useInstantState(initVal)
// // setState instantly changes the value of stateRef instead of having to wait until the next render
// const useInstantState = <T,>(initVal: T) => {
// const stateRef = useRef<T>(initVal)
// const [_, setS] = useState<T>(initVal)
// const setState = useCallback((newVal: T) => {
// setS(newVal);
// stateRef.current = newVal;
// }, [])
// return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
// }
// export function ThreadsProvider({ children }: { children: ReactNode }) {
// const [allThreadsRef, setAllThreads] = useInstantState<ChatThreads>({})
// const [currentThreadIdRef, setCurrentThreadId] = useInstantState<string | null>(null)
// // this loads allThreads in on mount
// useEffect(() => {
// getVSCodeAPI().postMessage({ type: 'getAllThreads' })
// awaitVSCodeResponse('allThreads')
// .then(response => {
// setAllThreads(response.threads)
// })
// }, [setAllThreads])
// return (
// <ThreadsContext.Provider
// value={{
// getAllThreads: () => allThreadsRef.current ?? {},
// getCurrentThread: () => currentThreadIdRef.current ? allThreadsRef.current?.[currentThreadIdRef.current] ?? null : null,
// addMessageToHistory: (message: ChatMessage) => {
// let currentThread: ChatThreads[string]
// if (!(currentThreadIdRef.current === null || allThreadsRef.current === null)) {
// currentThread = allThreadsRef.current[currentThreadIdRef.current]
// }
// else {
// currentThread = createNewThread()
// setCurrentThreadId(currentThread.id)
// }
// setAllThreads({
// ...allThreadsRef.current,
// [currentThread.id]: {
// ...currentThread,
// lastModified: new Date().toISOString(),
// messages: [...currentThread.messages, message],
// }
// })
// getVSCodeAPI().postMessage({ type: "persistThread", thread: currentThread })
// },
// switchToThread: (threadId: string) => {
// setCurrentThreadId(threadId);
// },
// startNewThread: () => {
// const newThread = createNewThread()
// setAllThreads({
// ...allThreadsRef.current,
// [newThread.id]: newThread
// })
// setCurrentThreadId(newThread.id)
// },
// }}
// >
// {children}
// </ThreadsContext.Provider>
// )
// }
// export function useThreads(): ConfigForThreadsValueType {
// const context = useContext<ConfigForThreadsValueType>(ThreadsContext)
// if (context === undefined) {
// throw new Error("useThreads missing Provider")
// }
// return context
// }

View file

@ -1,42 +0,0 @@
import React, { ReactNode, useCallback, useEffect, useState } from "react"
import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
const customStyle = {
...atomOneDarkReasonable,
'code[class*="language-"]': {
...atomOneDarkReasonable['code[class*="language-"]'],
background: "none",
},
}
return (<>
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
{!toolbar ? null : (
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
<div className="flex space-x-2 p-2">{buttonsOnHover === null ? null : buttonsOnHover}</div>
</div>
)}
<div
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg`}
>
<SyntaxHighlighter
language={language ?? 'plaintext'} // TODO must auto detect language
style={customStyle}
className={"rounded-sm"}
>
{text}
</SyntaxHighlighter>
</div>
</div>
</>
)
}
export default BlockCode

View file

@ -1,223 +0,0 @@
import React, { JSX, useCallback, useEffect, useState } from "react"
import { marked, MarkedToken, Token, TokensList } from "marked"
import BlockCode from "./BlockCode"
import { getVSCodeAPI } from "../../util/getVscodeApi"
enum CopyButtonState {
Copy = "Copy",
Copied = "Copied!",
Error = "Could not copy",
}
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
setCopyButtonState(CopyButtonState.Copy)
}, COPY_FEEDBACK_TIMEOUT)
}
}, [copyButtonState])
const onCopy = useCallback(() => {
navigator.clipboard.writeText(text).then(
() => {
setCopyButtonState(CopyButtonState.Copied)
},
() => {
setCopyButtonState(CopyButtonState.Error)
}
)
}, [text])
return <>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={onCopy}
>
{copyButtonState}
</button>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={async () => {
getVSCodeAPI().postMessage({ type: "applyChanges", diffRepr: text })
}}
>
Apply
</button>
</>
}
const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
if (t.type === "space") {
return <span>{t.raw}</span>
}
if (t.type === "code") {
return <BlockCode
text={t.text}
language={t.lang}
buttonsOnHover={<CodeButtonsOnHover diffRepr={t.text} />}
/>
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
return <HeadingTag>{t.text}</HeadingTag>
}
if (t.type === "table") {
return (
<table>
<thead>
<tr>
{t.header.map((cell: any, index: number) => (
<th key={index} style={{ textAlign: t.align[index] || "left" }}>
{cell.raw}
</th>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td
key={cellIndex}
style={{ textAlign: t.align[cellIndex] || "left" }}
>
{cell.raw}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
if (t.type === "hr") {
return <hr />
}
if (t.type === "blockquote") {
return <blockquote>{t.text}</blockquote>
}
if (t.type === "list") {
const ListTag = t.ordered ? "ol" : "ul"
return (
<ListTag
start={t.start ? t.start : undefined}
className={`list-inside ${t.ordered ? "list-decimal" : "list-disc"}`}
>
{t.items.map((item, index) => (
<li key={index}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly />
)}
<MarkdownRender string={item.text} nested={true} />
</li>
))}
</ListTag>
)
}
if (t.type === "paragraph") {
const contents = <>
{t.tokens.map((token, index) => (
<RenderToken key={index} token={token} />
))}
</>
if (nested)
return contents
return <p>{contents}</p>
}
// don't actually render <html> tags, just render strings of them
if (t.type === "html") {
return (
<pre>
{`<html>`}
{t.raw}
{`</html>`}
</pre>
)
}
if (t.type === "text" || t.type === "escape") {
return <span>{t.raw}</span>
}
if (t.type === "def") {
return <></> // Definitions are typically not rendered
}
if (t.type === "link") {
return (
<a href={t.href} title={t.title ?? undefined}>
{t.text}
</a>
)
}
if (t.type === "image") {
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />
}
if (t.type === "strong") {
return <strong>{t.text}</strong>
}
if (t.type === "em") {
return <em>{t.text}</em>
}
// inline code
if (t.type === "codespan") {
return (
<code className="text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono">
{t.text}
</code>
)
}
if (t.type === "br") {
return <br />
}
// strikethrough
if (t.type === "del") {
return <del>{t.text}</del>
}
// default
return (
<div className="bg-orange-50 rounded-sm overflow-hidden">
<span className="text-xs text-orange-500">Unknown type:</span>
{t.raw}
</div>
)
}
const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>
{tokens.map((token, index) => (
<RenderToken key={index} token={token} nested={nested} />
))}
</>
)
}
export default MarkdownRender

View file

@ -1,97 +0,0 @@
import * as vscode from 'vscode';
import { PartialVoidConfig } from '../webviews/common/contextForConfig'
// type CodeSelection = { selectionStr: string, filePath: vscode.Uri }
// type File = { filepath: vscode.Uri, content: string }
// an area that is currently being diffed
type DiffArea = {
diffareaid: number,
startLine: number,
endLine: number,
originalStartLine: number,
originalEndLine: number,
sweepIndex: number | null // null iff not sweeping
}
// the return type of diff creator
type BaseDiff = {
type: 'edit' | 'insertion' | 'deletion';
// repr: string; // representation of the diff in text
originalRange: vscode.Range;
originalCode: string;
range: vscode.Range;
code: string;
}
// each diff on the user's screen
type Diff = {
diffid: number,
lenses: vscode.CodeLens[],
} & BaseDiff
// editor -> sidebar
type MessageToSidebar = (
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor. selection and path are frozen snapshots
| { type: 'ctrl+k', selection: CodeSelection }
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
| { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig }
| { type: 'allThreads', threads: ChatThreads }
| { type: 'startNewThread' }
| { type: 'toggleThreadSelector' }
| { type: 'toggleSettings' }
| { type: 'deviceId', deviceId: string }
)
// sidebar -> editor
type MessageFromSidebar = (
| { type: 'applyChanges', diffRepr: string } // user clicks "apply" in the sidebar
| { type: 'requestFiles', filepaths: vscode.Uri[] }
| { type: 'getPartialVoidConfig' }
| { type: 'persistPartialVoidConfig', partialVoidConfig: PartialVoidConfig }
| { type: 'getAllThreads' }
| { type: 'persistThread', thread: ChatThreads[string] }
| { type: 'getDeviceId' }
)
// type ChatThreads = {
// [id: string]: {
// id: string; // store the id here too
// createdAt: string; // ISO string
// lastModified: string; // ISO string
// messages: ChatMessage[];
// }
// }
// type ChatMessage =
// | {
// role: "user";
// content: string; // content sent to the llm
// displayContent: string; // content displayed to user
// selection: CodeSelection | null; // the user's selection
// files: vscode.Uri[]; // the files sent in the message
// }
// | {
// role: "assistant";
// content: string; // content received from LLM
// displayContent: string | undefined; // content displayed to user (this is the same as content for now)
// }
// | {
// role: "system";
// content: string;
// displayContent?: undefined;
// }
export {
BaseDiff, Diff,
DiffArea,
CodeSelection,
File,
MessageFromSidebar,
MessageToSidebar,
ChatThreads,
ChatMessage,
}

View file

@ -1 +0,0 @@
to_be_built/

View file

@ -1 +0,0 @@
Use the command 'tsx' to run the main file here

View file

@ -1,54 +0,0 @@
import * as fs from 'fs'
import * as path from 'path'
import * as tsup from 'tsup'
const createFiles = (imports: string[], to_be_built_folder: string) => {
for (const importName of imports) {
const content = `\
export * from '${importName}';
`;
const dir = path.dirname(importName);
const file = path.basename(importName);
const fullPath = path.join(to_be_built_folder, dir, `${file}.ts`);
// Create all necessary directories before writing the file
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content, 'utf8');
}
}
const compileFiles = async (imports: string[], to_be_built_folder: string, outDir: string) => {
const fileEntries = imports.map((importName) => path.join(to_be_built_folder, `${importName}.ts`))
await tsup.build({
entry: fileEntries,
format: ['esm'],
sourcemap: false,
bundle: true,
clean: true,
// minify: true, // no need to minify since it all gets bundled later
outDir: path.join(outDir),
dts: true,
noExternal: [/.*/], // This bundles everything
platform: 'browser', // Important for browser compatibility
target: 'es2020',
outExtension: () => ({ js: '.js' })
})
}
const to_be_built_folder = 'to_be_built'
// const imports = ['openai', '@anthropic-ai/sdk', 'react', 'react-dom']
const imports = ['sendLLMMessage']
// fs.rmSync(to_be_built_folder, { recursive: true, force: true });
// createFiles(imports, to_be_built_folder)
const OUT_DIR = '../src/vs/workbench/contrib/void/browser/void-imports'
compileFiles(imports, to_be_built_folder, OUT_DIR)

View file

@ -1,817 +0,0 @@
{
"name": "void-imports",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "void-imports",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@google/generative-ai": "^0.21.0",
"ollama": "^0.5.9",
"openai": "^4.71.1"
},
"devDependencies": {
"tsx": "^4.19.2"
}
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.32.1",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.32.1.tgz",
"integrity": "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==",
"license": "MIT",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
"integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@google/generative-ai": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz",
"integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@types/node": {
"version": "18.19.64",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz",
"integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
"integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/agentkeepalive": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz",
"integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==",
"license": "MIT",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/esbuild": {
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
"integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.23.1",
"@esbuild/android-arm": "0.23.1",
"@esbuild/android-arm64": "0.23.1",
"@esbuild/android-x64": "0.23.1",
"@esbuild/darwin-arm64": "0.23.1",
"@esbuild/darwin-x64": "0.23.1",
"@esbuild/freebsd-arm64": "0.23.1",
"@esbuild/freebsd-x64": "0.23.1",
"@esbuild/linux-arm": "0.23.1",
"@esbuild/linux-arm64": "0.23.1",
"@esbuild/linux-ia32": "0.23.1",
"@esbuild/linux-loong64": "0.23.1",
"@esbuild/linux-mips64el": "0.23.1",
"@esbuild/linux-ppc64": "0.23.1",
"@esbuild/linux-riscv64": "0.23.1",
"@esbuild/linux-s390x": "0.23.1",
"@esbuild/linux-x64": "0.23.1",
"@esbuild/netbsd-x64": "0.23.1",
"@esbuild/openbsd-arm64": "0.23.1",
"@esbuild/openbsd-x64": "0.23.1",
"@esbuild/sunos-x64": "0.23.1",
"@esbuild/win32-arm64": "0.23.1",
"@esbuild/win32-ia32": "0.23.1",
"@esbuild/win32-x64": "0.23.1"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data-encoder": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
"license": "MIT"
},
"node_modules/formdata-node": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
"license": "MIT",
"dependencies": {
"node-domexception": "1.0.0",
"web-streams-polyfill": "4.0.0-beta.3"
},
"engines": {
"node": ">= 12.20"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-tsconfig": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz",
"integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/ollama": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.9.tgz",
"integrity": "sha512-F/KZuDRC+ZsVCuMvcOYuQ6zj42/idzCkkuknGyyGVmNStMZ/sU3jQpvhnl4SyC0+zBzLiKNZJnJeuPFuieWZvQ==",
"license": "MIT",
"dependencies": {
"whatwg-fetch": "^3.6.20"
}
},
"node_modules/openai": {
"version": "4.71.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-4.71.1.tgz",
"integrity": "sha512-C6JNMaQ1eijM0lrjiRUL3MgThVP5RdwNAghpbJFdW0t11LzmyqON8Eh8MuUuEZ+CeD6bgYl2Fkn2BoptVxv9Ug==",
"license": "Apache-2.0",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
"node-fetch": "^2.6.7"
},
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"zod": "^3.23.8"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tsx": {
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz",
"integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.23.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-fetch": {
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
}
}
}

View file

@ -1,19 +0,0 @@
{
"name": "void-imports",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "tdx index.ts"
},
"author": "",
"description": "",
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@google/generative-ai": "^0.21.0",
"ollama": "^0.5.9",
"openai": "^4.71.1"
},
"devDependencies": {
"tsx": "^4.19.2"
}
}

View file

@ -1,395 +0,0 @@
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai';
// import { VoidConfig } from '../webviews/common/contextForConfig'
// import { captureEvent } from '../webviews/common/posthog';
// import { ChatMessage } from './shared_types';
type VoidConfig = any
export type AbortRef = { current: (() => void) | null }
export type OnText = (newText: string, fullText: string) => void
export type OnFinalMessage = (input: string) => void
export type LLMMessageAnthropic = {
role: 'user' | 'assistant';
content: string;
}
export type LLMMessage = {
role: 'system' | 'user' | 'assistant';
content: string;
}
type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: (error: string) => void;
voidConfig: VoidConfig;
_setAborter: (aborter: () => void) => void;
}) => void
type SendLLMMessageFnTypeExternal = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: (fullText: string) => void;
onError: (error: string) => void;
voidConfig: VoidConfig | null;
abortRef: AbortRef;
logging: {
loggingName: string,
};
}) => void
const parseMaxTokensStr = (maxTokensStr: string) => {
// parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
if (Number.isNaN(int))
return undefined
return int
}
// Anthropic
const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
// find system messages and concatenate them
const systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// remove system messages for Anthropic
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
const stream = anthropic.messages.stream({
system: systemMessage,
messages: anthropicMessages,
model: voidConfig.anthropic.model,
max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
});
// when receive text
stream.on('text', (newText, fullText) => {
onText(newText, fullText)
})
// when we get the final message on this stream (or when error/fail)
stream.on('finalMessage', (claude_response) => {
// stringify the response's content
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
onFinalMessage(content)
})
stream.on('error', (error) => {
// the most common error will be invalid API key (401), so we handle this with a nice message
if (error instanceof Anthropic.APIError && error.status === 401) {
onError('Invalid API key.')
}
else {
onError(error.message)
}
})
// TODO need to test this to make sure it works, it might throw an error
_setAborter(() => stream.controller.abort())
};
// Gemini
const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = ''
const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
// remove system messages that get sent to Gemini
// str of all system messages
const systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// Convert messages to Gemini format
const geminiMessages: Content[] = messages
.filter(msg => msg.role !== 'system')
.map((msg, i) => ({
parts: [{ text: msg.content }],
role: msg.role === 'assistant' ? 'model' : 'user'
}))
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
.then(async response => {
_setAborter(() => response.stream.return(fullText))
for await (const chunk of response.stream) {
const newText = chunk.text();
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
})
.catch((error) => {
if (error instanceof GoogleGenerativeAIFetchError) {
if (error.status === 400) {
onError('Invalid API key.');
}
else {
onError(`${error.name}:\n${error.message}`);
}
}
else {
onError(error);
}
})
}
// OpenAI, OpenRouter, OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = ''
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
if (voidConfig.default.whichApi === 'openAI') {
openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
}
else if (voidConfig.default.whichApi === 'openRouter') {
openai = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1', apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true,
defaultHeaders: {
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
},
});
options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
}
else if (voidConfig.default.whichApi === 'openAICompatible') {
openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
}
else {
console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
}
openai.chat.completions
.create(options)
.then(async response => {
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
const newText = chunk.choices[0]?.delta?.content || '';
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
})
// when error/fail - this catches errors of both .create() and .then(for await)
.catch(error => {
if (error instanceof OpenAI.APIError) {
if (error.status === 401) {
onError('Invalid API key.');
}
else {
onError(`${error.name}:\n${error.message}`);
}
}
else {
onError(error);
}
})
};
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = ''
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
ollama.chat({
model: voidConfig.ollama.model,
messages: messages,
stream: true,
options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
})
.then(async stream => {
_setAborter(() => stream.abort())
// iterate through the stream
for await (const chunk of stream) {
const newText = chunk.message.content;
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
})
// when error/fail
.catch(error => {
onError(error)
})
};
// Greptile
// https://docs.greptile.com/api-reference/query
// https://docs.greptile.com/quickstart#sample-response-streamed
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = ''
fetch('https://api.greptile.com/v2/query', {
method: 'POST',
headers: {
'Authorization': `Bearer ${voidConfig.greptile.apikey}`,
'X-Github-Token': `${voidConfig.greptile.githubPAT}`,
'Content-Type': `application/json`,
},
body: JSON.stringify({
messages,
stream: true,
repositories: [voidConfig.greptile.repoinfo],
}),
})
// this is {message}\n{message}\n{message}...\n
.then(async response => {
const text = await response.text()
console.log('got greptile', text)
return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
})
// TODO make this actually stream, right now it just sends one message at the end
// TODO add _setAborter() when add streaming
.then(async responseArr => {
for (const response of responseArr) {
const type: string = response['type']
const message = response['message']
// when receive text
if (type === 'message') {
fullText += message
onText(message, fullText)
}
else if (type === 'sources') {
const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
fullText += filepath
onText(filepath, fullText)
}
// type: 'status' with an empty 'message' means last message
else if (type === 'status') {
if (!message) {
onFinalMessage(fullText)
}
}
}
})
.catch(e => {
onError(e)
});
}
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({
messages,
onText: onText_,
onFinalMessage: onFinalMessage_,
onError: onError_,
abortRef: abortRef_,
voidConfig,
logging: { loggingName }
}) => {
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() }))
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = (eventId: string, extras?: object) => {
// captureEvent(eventId, {
// whichApi: voidConfig.default['whichApi'],
// numMessages: messages?.length,
// messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
// version: '2024-11-02',
// ...extras,
// })
}
const submit_time = new Date()
let _fullTextSoFar = ''
let _aborter: (() => void) | null = null
let _setAborter = (fn: () => void) => { _aborter = fn }
let _didAbort = false
const onText = (newText: string, fullText: string) => {
if (_didAbort) return
onText_(newText, fullText)
_fullTextSoFar = fullText
}
const onFinalMessage = (fullText: string) => {
if (_didAbort) return
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_(fullText)
}
const onError = (error: string) => {
if (_didAbort) return
captureChatEvent(`${loggingName} - Error`, { error })
onError_(error)
}
const onAbort = () => {
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
_aborter?.()
_didAbort = true
}
abortRef_.current = onAbort
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
switch (voidConfig.default.whichApi) {
case 'anthropic':
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
case 'openAI':
case 'openRouter':
case 'openAICompatible':
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
case 'gemini':
sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
case 'ollama':
sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
case 'greptile':
sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
default:
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
break;
}
}

View file

@ -1,12 +0,0 @@
{
"compilerOptions": {
"target": "ES6",
"module": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["to_be_built/**/*"],
"exclude": ["node_modules"]
}