diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index 6ebc6476..d6a323be 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -9,9 +9,6 @@ "version": "0.0.1", "dependencies": { "@anthropic-ai/sdk": "^0.27.1", - "diff-match-patch": "^1.0.5", - "diff": "^7.0.0", - "ollama": "^0.5.9", "openai": "^4.57.0" }, "devDependencies": { @@ -23,7 +20,7 @@ "@types/node": "^22.5.1", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", - "@types/vscode": "1.89.0", + "@types/vscode": "1.92.0", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", "@vscode/test-cli": "^0.0.10", @@ -42,7 +39,8 @@ "rimraf": "^6.0.1", "tailwindcss": "^3.4.10", "typescript": "5.5.4", - "typescript-eslint": "^8.3.0" + "typescript-eslint": "^8.3.0", + "uuid": "^10.0.0" }, "engines": { "vscode": "^1.89.0" @@ -769,9 +767,9 @@ "license": "MIT" }, "node_modules/@types/vscode": { - "version": "1.89.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.89.0.tgz", - "integrity": "sha512-TMfGKLSVxfGfoO8JfIE/neZqv7QLwS4nwPwL/NwMvxtAY2230H2I4Z5xx6836pmJvMAzqooRQ4pmLm7RUicP3A==", + "version": "1.92.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.92.0.tgz", + "integrity": "sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==", "dev": true, "license": "MIT" }, @@ -2153,20 +2151,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-match-patch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", - "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -5573,15 +5557,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -7720,6 +7695,20 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -7780,12 +7769,6 @@ "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", diff --git a/extensions/void/package.json b/extensions/void/package.json index 17c44e1f..7b876946 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -363,7 +363,7 @@ "@types/node": "^22.5.1", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", - "@types/vscode": "1.89.0", + "@types/vscode": "1.92.0", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", "@vscode/test-cli": "^0.0.10", @@ -382,13 +382,11 @@ "rimraf": "^6.0.1", "tailwindcss": "^3.4.10", "typescript": "5.5.4", - "typescript-eslint": "^8.3.0" + "typescript-eslint": "^8.3.0", + "uuid": "^10.0.0" }, "dependencies": { "@anthropic-ai/sdk": "^0.27.1", - "diff-match-patch": "^1.0.5", - "ollama": "^0.5.9", - "openai": "^4.57.0", - "diff": "^7.0.0" + "openai": "^4.57.0" } -} \ No newline at end of file +} diff --git a/extensions/void/src/SidebarWebviewProvider.ts b/extensions/void/src/SidebarWebviewProvider.ts index a750a21c..a6cdf85c 100644 --- a/extensions/void/src/SidebarWebviewProvider.ts +++ b/extensions/void/src/SidebarWebviewProvider.ts @@ -55,7 +55,7 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { const openAICompatibleEndpoint: string | undefined = vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint'); this._webviewDeps.push('void.openAICompatible.endpoint'); if (openAICompatibleEndpoint) - allowed_urls.push(openAICompatibleEndpoint); + allowed_urls.push(openAICompatibleEndpoint+'/chat/completions'); const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js')); const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css')); diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index 0c0541be..55c53350 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { DisplayChangesProvider } from './DisplayChangesProvider'; -import { BaseDiffArea, ChatThreads, WebviewMessage } from './shared_types'; +import { BaseDiffArea, ChatThreads, MessageFromSidebar, MessageToSidebar } from './shared_types'; import { SidebarWebviewProvider } from './SidebarWebviewProvider'; import { ApiConfig } from './common/sendLLMMessage'; @@ -79,7 +79,7 @@ export function activate(context: vscode.ExtensionContext) { const filePath = editor.document.uri; // send message to the webview (Sidebar.tsx) - webviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies WebviewMessage)); + webviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar)); }) ); @@ -105,23 +105,23 @@ export function activate(context: vscode.ExtensionContext) { // top navigation bar commands context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => { - webview.postMessage({ type: 'startNewThread' } satisfies WebviewMessage) + webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar) })) context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => { - webview.postMessage({ type: 'toggleThreadSelector' } satisfies WebviewMessage) + webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar) })) // when config changes, send it to the sidebar vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('void')) { const apiConfig = getApiConfig() - webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage) + webview.postMessage({ type: 'apiConfig', apiConfig } satisfies MessageToSidebar) } }) // Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`) - webview.onDidReceiveMessage(async (m: WebviewMessage) => { + webview.onDidReceiveMessage(async (m: MessageFromSidebar) => { if (m.type === 'requestFiles') { @@ -131,7 +131,7 @@ export function activate(context: vscode.ExtensionContext) { ) // send contents to webview - webview.postMessage({ type: 'files', files, } satisfies WebviewMessage) + webview.postMessage({ type: 'files', files, } satisfies MessageToSidebar) } else if (m.type === 'applyChanges') { @@ -168,11 +168,11 @@ export function activate(context: vscode.ExtensionContext) { } else if (m.type === 'getApiConfig') { const apiConfig = getApiConfig() - webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage) + webview.postMessage({ type: 'apiConfig', apiConfig } satisfies MessageToSidebar) } else if (m.type === 'getAllThreads') { const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {} - webview.postMessage({ type: 'allThreads', threads } satisfies WebviewMessage) + webview.postMessage({ type: 'allThreads', threads } satisfies MessageToSidebar) } else if (m.type === 'persistThread') { const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {} @@ -180,7 +180,7 @@ export function activate(context: vscode.ExtensionContext) { context.workspaceState.update('allThreads', updatedThreads) } else { - console.error('unrecognized command', m.type, m) + console.error('unrecognized command', m) } }) } diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index ad2025f1..0fcbef42 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/shared_types.ts @@ -38,45 +38,25 @@ type Diff = { lenses: vscode.CodeLens[], } & BaseDiff -type WebviewMessage = ( - - // editor -> sidebar +// editor -> sidebar +type MessageToSidebar = ( | { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor - - // sidebar -> editor - | { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar - - // sidebar -> editor - | { type: 'requestFiles', filepaths: vscode.Uri[] } - - // editor -> sidebar | { type: 'files', files: { filepath: vscode.Uri, content: string }[] } - - // sidebar -> editor - | { type: 'getApiConfig' } - - // editor -> sidebar | { type: 'apiConfig', apiConfig: ApiConfig } - - // sidebar -> editor - | { type: 'getAllThreads' } - - // editor -> sidebar | { type: 'allThreads', threads: ChatThreads } - - // sidebar -> editor - | { type: 'persistThread', thread: ChatThreads[string] } - - // editor -> sidebar | { type: 'startNewThread' } - - // editor -> sidebar | { type: 'toggleThreadSelector' } - ) +// sidebar -> editor +type MessageFromSidebar = ( + | { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar + | { type: 'requestFiles', filepaths: vscode.Uri[] } + | { type: 'getApiConfig' } + | { type: 'getAllThreads' } + | { type: 'persistThread', thread: ChatThreads[string] } +) -type Command = WebviewMessage['type'] type ChatThreads = { [id: string]: { @@ -105,8 +85,8 @@ export { Diff, DiffArea, CodeSelection, File, - WebviewMessage, - Command, + MessageFromSidebar, + MessageToSidebar, ChatThreads, ChatMessage, } diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index 22091356..9f769dc1 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -1,330 +1,44 @@ import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react" import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage" -import { File, CodeSelection, WebviewMessage, ChatMessage } from "../shared_types" -import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi" +import { CodeSelection, ChatMessage, MessageToSidebar } from "../shared_types" +import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode } from "./getVscodeApi" -import { marked } from 'marked'; -import MarkdownRender from "./markdown/MarkdownRender"; -import BlockCode from "./markdown/BlockCode"; - -import * as vscode from 'vscode' -import { SelectedFiles } from "./components/SelectedFiles"; +import { SidebarThreadSelector } from "./SidebarThreadSelector"; import { useThreads } from "./threadsContext"; - - -const filesStr = (fullFiles: File[]) => { - return fullFiles.map(({ filepath, content }) => - ` -${filepath.fsPath} -\`\`\` -${content} -\`\`\``).join('\n') -} - -const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => { - return ` -${filesStr(files)} - -${!selection ? '' : ` -I am currently selecting this code: -\`\`\`${selection.selectionStr}\`\`\` -`} - -Please edit the code following these instructions (or, if appropriate, answer my question instead): -${instructions} - -If you make a change, rewrite the entire file. -`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply -} - - -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 = <> - - {chatMessage.selection?.selectionStr && } - {children} - - } - else if (role === 'assistant') { - - chatbubbleContents = // sectionsHTML - } - - - return
-
- {chatbubbleContents} -
-
-} - -const ThreadSelector = ({ onClose }: { onClose: () => void }) => { - const { allThreads, currentThread, switchToThread } = useThreads() - return ( -
-
- -
- {/* iterate through all past threads */} - {Object.keys(allThreads ?? {}).map((threadId) => { - const pastThread = (allThreads ?? {})[threadId]; - return ( - - ) - })} -
- ) -} +import { SidebarChat } from "./SidebarChat"; const Sidebar = () => { - const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads() - - // state of current message - const [selection, setSelection] = useState(null) // the code the user is selecting - const [files, setFiles] = useState([]) // 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 [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false) const [requestFailed, setRequestFailed] = useState(false) const [requestFailedReason, setRequestFailedReason] = useState('') - const abortFnRef = useRef<(() => void) | null>(null) - - const [apiConfig, setApiConfig] = useState(null) - // get Api Config on mount useEffect(() => { getVSCodeAPI().postMessage({ type: 'getApiConfig' }) }, []) - // Receive messages from the extension + // Receive messages from the VSCode extension useEffect(() => { const listener = (event: MessageEvent) => { - - const m = event.data as WebviewMessage; - // resolve any awaiting promises - // eg. it will resolve the promise below for `await VSCodeResponse('files')` - resolveAwaitingVSCodeResponse(m) - - // if user pressed ctrl+l, add their selection to the sidebar - if (m.type === 'ctrl+l') { - 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]) - - } - // when get apiConfig, set - else if (m.type === 'apiConfig') { - setApiConfig(m.apiConfig) - } - - // if they pressed the + to add a new chat - else if (m.type === 'startNewThread') { - setIsThreadSelectorOpen(false) - if (currentThread?.messages.length !== 0) - startNewThread() - } - - // if they opened thread selector - else if (m.type === 'toggleThreadSelector') { - setIsThreadSelectorOpen(v => !v) - } - + const m = event.data as MessageToSidebar; + onMessageFromVSCode(m) } window.addEventListener('message', listener); return () => { window.removeEventListener('message', listener) } - }, [files, selection, startNewThread, currentThread]) + }, []) - const formRef = useRef(null) - const onSubmit = async (e: FormEvent) => { - - e.preventDefault() - if (isLoading) return - - // Reset any error messages from previous submit - setRequestFailed(false) - setRequestFailedReason('') - - setIsLoading(true) - setInstructions(''); - formRef.current?.reset(); // reset the form's text - setSelection(null) - setFiles([]) - - // request file content from vscode and await response - getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files }) - const relevantFiles = await awaitVSCodeResponse('files') - - // add message to chat history - const content = userInstructionsStr(instructions, relevantFiles.files, selection) - // console.log('prompt:\n', content) - const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files } - addMessageToHistory(newHistoryElt) - - // send message to LLM - let { abort } = sendLLMMessage({ - messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', 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) - - // clear selection - setMessageStream('') - setIsLoading(false) - }, - onError: (message) => { onStop(); setRequestFailed(true); setRequestFailedReason(message)}, - apiConfig: apiConfig - }) - abortFnRef.current = abort - - } - - const onStop = useCallback(() => { - // abort claude - abortFnRef.current?.() - - // if messageStream was not empty, add it to the history - const llmContent = messageStream || '(canceled)' - const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent } - addMessageToHistory(newHistoryElt) - - setMessageStream('') - setIsLoading(false) - - }, [addMessageToHistory, messageStream]) - - //Clear code selection - const clearSelection = () => { - setSelection(null); - }; - return <>
{isThreadSelectorOpen && (
- setIsThreadSelectorOpen(false)} /> + setIsThreadSelectorOpen(false)} />
)} -
- {/* previous messages */} - {currentThread !== null && currentThread.messages.map((message, i) => - - )} - {/* message stream */} - -
- {/* chatbar */} -
- {/* selection */} -
-
-
- {/* selection */} - {(files.length || selection?.selectionStr) &&
- {/* selected files */} - - {/* selected code */} - {!!selection?.selectionStr && ( - - Remove - - )} /> - )} -
} - {/* error message */} - {requestFailed && ( -
-
{`${requestFailedReason}`}
-
- )} -
{ if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }} - - onSubmit={(e) => { - console.log('submit!') - e.preventDefault(); - onSubmit(e) - }}> - {/* input */} - -