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}`}
-
- )}
-
-
-
-
-
+
>
diff --git a/extensions/void/src/sidebar/SidebarChat.tsx b/extensions/void/src/sidebar/SidebarChat.tsx
new file mode 100644
index 00000000..645afb46
--- /dev/null
+++ b/extensions/void/src/sidebar/SidebarChat.tsx
@@ -0,0 +1,280 @@
+import React, { FormEvent, useCallback, useEffect, useRef, useState } from "react";
+
+
+import { marked } from 'marked';
+import MarkdownRender from "./markdown/MarkdownRender";
+import BlockCode from "./markdown/BlockCode";
+import { SelectedFiles } from "./components/SelectedFiles";
+import { File, ChatMessage, CodeSelection } from "../shared_types";
+import * as vscode from 'vscode'
+import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "./getVscodeApi";
+import { useThreads } from "./threadsContext";
+import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage";
+
+
+
+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 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}
+
+
+}
+
+
+export const SidebarChat = ({ setIsThreadSelectorOpen }: { setIsThreadSelectorOpen: (v: boolean | ((v: boolean) => boolean)) => void }) => {
+
+
+ // 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 abortFnRef = useRef<(() => void) | null>(null)
+
+ // higher level state
+ const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads()
+ const [apiConfig, setApiConfig] = useState(null)
+
+
+ // 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])
+ })
+
+ // when get apiConfig, set
+ useOnVSCodeMessage('apiConfig', (m) => {
+ setApiConfig(m.apiConfig)
+ })
+
+ // if they pressed the + to add a new chat
+ useOnVSCodeMessage('startNewThread', (m) => {
+ setIsThreadSelectorOpen(false)
+ if (currentThread?.messages.length !== 0)
+ startNewThread()
+
+ })
+
+ // if they opened thread selector
+ useOnVSCodeMessage('toggleThreadSelector', (m) => {
+ setIsThreadSelectorOpen(v => !v)
+ })
+
+
+ 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 <>
+
+ {/* 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
+
+ )} />
+ )}
+
}
+
+
+
+
+
+ >
+}
+
+
diff --git a/extensions/void/src/sidebar/SidebarThreadSelector.tsx b/extensions/void/src/sidebar/SidebarThreadSelector.tsx
new file mode 100644
index 00000000..287f0420
--- /dev/null
+++ b/extensions/void/src/sidebar/SidebarThreadSelector.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+import { ThreadsProvider, useThreads } from "./threadsContext";
+
+export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
+ const { allThreads, currentThread, switchToThread } = useThreads()
+ return (
+
+
+ {/* iterate through all past threads */}
+ {Object.keys(allThreads ?? {}).map((threadId) => {
+ const pastThread = (allThreads ?? {})[threadId];
+ return (
+
+ )
+ })}
+
+ )
+}
\ No newline at end of file
diff --git a/extensions/void/src/sidebar/getVscodeApi.ts b/extensions/void/src/sidebar/getVscodeApi.ts
index 9e5172d7..bdc5b2ed 100644
--- a/extensions/void/src/sidebar/getVscodeApi.ts
+++ b/extensions/void/src/sidebar/getVscodeApi.ts
@@ -1,48 +1,77 @@
-import { Command, WebviewMessage } from "../shared_types";
+import { useEffect } from "react";
+import { MessageFromSidebar, MessageToSidebar, } from "../shared_types";
+import { v4 as uuidv4 } from 'uuid';
+type Command = MessageToSidebar['type']
-// message -> res[]
-const awaiting: { [c in Command]: ((res: any) => void)[] } = {
+// messageType -> res[]
+const onetimeCallbacks: { [C in Command]: ((res: any) => void)[] } = {
"ctrl+l": [],
- "applyChanges": [],
- "requestFiles": [],
"files": [],
"apiConfig": [],
- "getApiConfig": [],
"startNewThread": [],
- "getAllThreads": [],
"allThreads": [],
- "persistThread": [],
"toggleThreadSelector": []
}
+// messageType -> id -> res
+const callbacks: { [C in Command]: { [id: string]: ((res: any) => void) } } = {
+ "ctrl+l": {},
+ "files": {},
+ "apiConfig": {},
+ "startNewThread": {},
+ "allThreads": {},
+ "toggleThreadSelector": {}
+}
+
+
// use this function to await responses
export const awaitVSCodeResponse = (c: C) => {
- let result: Promise = new Promise((res, rej) => {
- awaiting[c].push(res)
+ let result: Promise = new Promise((res, rej) => {
+ onetimeCallbacks[c].push(res)
})
return result
}
-export const resolveAwaitingVSCodeResponse = (m: WebviewMessage) => {
- // resolve all promises for this message
- for (let res of awaiting[m.type]) {
+
+// use this function to add a listener to a certain type of message
+export const useOnVSCodeMessage = (messageType: C, fn: (e: MessageToSidebar & { type: C }) => void) => {
+ useEffect(() => {
+ const mType = messageType
+ const callbackId: string = uuidv4();
+ // @ts-ignore
+ callbacks[mType][callbackId] = fn;
+ return () => { delete callbacks[mType][callbackId] }
+ }, [messageType, fn])
+}
+
+
+
+// this function gets called whenever sidebar receives a message - it should only mount once
+export const onMessageFromVSCode = (m: MessageToSidebar) => {
+ // resolve all promises for this message type
+ for (let res of onetimeCallbacks[m.type]) {
+ res(m)
+ onetimeCallbacks[m.type].splice(0) // clear the array
+ }
+ // call the listener for this message type
+ for (let res of Object.values(callbacks[m.type])) {
res(m)
- awaiting[m.type].splice(0) // clear the array
}
}
-// VS Code exposes the function acquireVsCodeApi() to us, it should only get called once
-let vsCodeApi: ReturnType | undefined;
type AcquireVsCodeApiType = () => {
- postMessage(message: WebviewMessage): void;
+ postMessage(message: MessageFromSidebar): void;
// setState(state: any): void; // getState and setState are made obsolete by us using { retainContextWhenHidden: true }
// getState(): any;
};
+// VS Code exposes the function acquireVsCodeApi() to us, this variable makes sure it only gets called once
+let vsCodeApi: ReturnType | undefined;
+
export function getVSCodeAPI(): ReturnType {
if (vsCodeApi)
return vsCodeApi;
diff --git a/extensions/void/src/sidebar/markdown/BlockCode.tsx b/extensions/void/src/sidebar/markdown/BlockCode.tsx
index f397c580..5da01e10 100644
--- a/extensions/void/src/sidebar/markdown/BlockCode.tsx
+++ b/extensions/void/src/sidebar/markdown/BlockCode.tsx
@@ -1,6 +1,9 @@
import React, { ReactNode, useCallback, useEffect, useState } from "react"
import { getVSCodeAPI } from "../getVscodeApi"
+import SyntaxHighlighter from "react-syntax-highlighter";
+import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
+
enum CopyButtonState {
Copy = "Copy",
Copied = "Copied!",
@@ -12,17 +15,27 @@ const COPY_FEEDBACK_TIMEOUT = 1000
// code block with toolbar (Apply, Copy, etc) at top
const BlockCode = ({
text,
+ language,
toolbar,
hideToolbar = false,
className,
}: {
text: string
+ language?: string
toolbar?: ReactNode
hideToolbar?: boolean
className?: string
}) => {
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
+ const customStyle = {
+ ...atomOneDarkReasonable,
+ 'code[class*="language-"]': {
+ ...atomOneDarkReasonable['code[class*="language-"]'],
+ background: "none",
+ },
+ }
+
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
@@ -71,7 +84,14 @@ const BlockCode = ({
)
diff --git a/extensions/void/src/sidebar/markdown/MarkdownRender.tsx b/extensions/void/src/sidebar/markdown/MarkdownRender.tsx
index 999cdd80..79e6e91d 100644
--- a/extensions/void/src/sidebar/markdown/MarkdownRender.tsx
+++ b/extensions/void/src/sidebar/markdown/MarkdownRender.tsx
@@ -12,7 +12,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
}
if (t.type === "code") {
- return
+ return
}
if (t.type === "heading") {
@@ -165,4 +165,4 @@ const MarkdownRender = ({ string, nested = false }: { string: string, nested?: b
)
}
-export default MarkdownRender
\ No newline at end of file
+export default MarkdownRender