diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index 6ebc6476..a23620fe 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.1", "dependencies": { "@anthropic-ai/sdk": "^0.27.1", - "diff-match-patch": "^1.0.5", "diff": "^7.0.0", + "diff-match-patch": "^1.0.5", "ollama": "^0.5.9", "openai": "^4.57.0" }, @@ -23,6 +23,7 @@ "@types/node": "^22.5.1", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", + "@types/uuid": "^10.0.0", "@types/vscode": "1.89.0", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", @@ -42,7 +43,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" @@ -768,6 +770,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/vscode": { "version": "1.89.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.89.0.tgz", @@ -7720,6 +7729,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", diff --git a/extensions/void/package.json b/extensions/void/package.json index 17c44e1f..72bfb32a 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -363,6 +363,7 @@ "@types/node": "^22.5.1", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", + "@types/uuid": "^10.0.0", "@types/vscode": "1.89.0", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", @@ -382,13 +383,14 @@ "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": "^7.0.0", "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/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 8521d83d..e41e87e4 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -1,317 +1,42 @@ 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 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 - - 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) - }, - 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 - - )} /> - )} -
} -
{ if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }} - - onSubmit={(e) => { - console.log('submit!') - e.preventDefault(); - onSubmit(e) - }}> - {/* input */} - -