diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts
index de39feaa..3bcfa6db 100644
--- a/extensions/void/src/common/sendLLMMessage.ts
+++ b/extensions/void/src/common/sendLLMMessage.ts
@@ -223,14 +223,14 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText,
if (!apiConfig) return { abort: () => { } }
if (
- apiConfig.anthropic.apikey === "" &&
- apiConfig.greptile.apikey === "" &&
- apiConfig.openai.apikey === "" &&
- apiConfig.ollama.endpoint === "" &&
- apiConfig.ollama.model === ""
+ apiConfig.anthropic.apikey === '' &&
+ apiConfig.greptile.apikey === '' &&
+ apiConfig.openai.apikey === '' &&
+ apiConfig.ollama.endpoint === '' &&
+ apiConfig.ollama.model === '' &&
+ apiConfig.whichApi === ''
) {
getVSCodeAPI().postMessage({ type: 'displayError', message: 'Required API keys are not set.' })
- onFinalMessage("Required API keys are not set.");
return { abort: () => { }}
}
diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts
index 835dcade..3eedae28 100644
--- a/extensions/void/src/extension.ts
+++ b/extensions/void/src/extension.ts
@@ -126,7 +126,7 @@ export function activate(context: vscode.ExtensionContext) {
} else if (m.type === 'displayError') {
- vscode.window.showWarningMessage(m.message, { modal: true });
+ vscode.window.showErrorMessage(m.message, { modal: true });
}
else {
diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx
index 61426869..d4c35d1b 100644
--- a/extensions/void/src/sidebar/Sidebar.tsx
+++ b/extensions/void/src/sidebar/Sidebar.tsx
@@ -1,273 +1,390 @@
-import React, { useState, ChangeEvent, useEffect, useRef, useCallback, FormEvent } from "react"
-import { ApiConfig, LLMMessage, sendLLMMessage } from "../common/sendLLMMessage"
-import { Command, File, Selection, WebviewMessage } from "../shared_types"
-import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
+/* eslint-disable no-mixed-spaces-and-tabs */
+import React, {
+ useState,
+ ChangeEvent,
+ useEffect,
+ useRef,
+ useCallback,
+ FormEvent,
+} from "react";
+import {
+ ApiConfig,
+ LLMMessage,
+ sendLLMMessage,
+} from "../common/sendLLMMessage";
+import { Command, File, Selection, WebviewMessage } from "../shared_types";
+import {
+ awaitVSCodeResponse,
+ getVSCodeAPI,
+ resolveAwaitingVSCodeResponse,
+} from "./getVscodeApi";
-import { marked } from 'marked';
+import { marked } from "marked";
import MarkdownRender, { BlockCode } from "./MarkdownRender";
-import * as vscode from 'vscode'
-
+import * as vscode from "vscode";
const filesStr = (fullFiles: File[]) => {
- return fullFiles.map(({ filepath, content }) =>
- `
+ return fullFiles
+ .map(
+ ({ filepath, content }) =>
+ `
${filepath.fsPath}
\`\`\`
${content}
-\`\`\``).join('\n')
-}
+\`\`\``
+ )
+ .join("\n");
+};
-const userInstructionsStr = (instructions: string, files: File[], selection: Selection | null) => {
+const userInstructionsStr = (
+ instructions: string,
+ files: File[],
+ selection: Selection | null
+) => {
return `
${filesStr(files)}
-${!selection ? '' : `
+${!selection
+ ? ""
+ : `
I am currently selecting this code:
\`\`\`${selection.selectionStr}\`\`\`
-`}
+`
+ }
Please edit the code following these instructions:
${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 FilesSelector = ({ files, setFiles }: { files: vscode.Uri[], setFiles: (files: vscode.Uri[]) => void }) => {
- return files.length !== 0 &&
- Include files:
- {files.map((filename, i) =>
-
- {/* X button on a file */}
-
+const FilesSelector = ({
+ files,
+ setFiles,
+}: {
+ files: vscode.Uri[];
+ setFiles: (files: vscode.Uri[]) => void;
+}) => {
+ return (
+ files.length !== 0 && (
+
+ Include files:
+ {files.map((filename, i) => (
+
+ {/* X button on a file */}
+
+
+ ))}
- )}
-
-}
+ )
+ );
+};
const IncludedFiles = ({ files }: { files: vscode.Uri[] }) => {
- return files.length !== 0 &&
- {files.map((filename, i) =>
-
-
+ return (
+ files.length !== 0 && (
+
+ {files.map((filename, i) => (
+
+
+
+ ))}
- )}
-
-}
-
+ )
+ );
+};
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
+ const role = chatMessage.role;
+ const children = chatMessage.displayContent;
- const role = chatMessage.role
- const children = chatMessage.displayContent
+ if (!children) return null;
- if (!children)
- return null
+ let chatbubbleContents: React.ReactNode;
- let chatbubbleContents: React.ReactNode
-
- if (role === 'user') {
- chatbubbleContents = <>
-
- {chatMessage.selection?.selectionStr &&
}
- {children}
- >
- }
- else if (role === 'assistant') {
+ if (role === "user") {
+ chatbubbleContents = (
+ <>
+
+ {chatMessage.selection?.selectionStr && (
+
+ )}
+ {children}
+ >
+ );
+ } else if (role === "assistant") {
const tokens = marked.lexer(children); // https://marked.js.org/using_pro#renderer
- chatbubbleContents =
// sectionsHTML
+ chatbubbleContents =
; // sectionsHTML
}
-
- return
-
- {chatbubbleContents}
+ return (
+
+
+ {chatbubbleContents}
+
-
-}
+ );
+};
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]
-}
-
-type ChatMessage = {
- role: 'user'
- content: string, // content sent to the llm
- displayContent: string, // content displayed to user
- selection: Selection | null, // the user's selection
- files: vscode.Uri[], // the files sent in the message
-} | {
- role: 'assistant',
- content: string, // content received from LLM
- displayContent: string // content displayed to user (this is the same as content for now)
-}
+ pathStr = pathStr.replace(/[/\\]+/g, "/"); // replace any / or \ or \\ with /
+ const parts = pathStr.split("/"); // split on /
+ return parts[parts.length - 1];
+};
+type ChatMessage =
+ | {
+ role: "user";
+ content: string; // content sent to the llm
+ displayContent: string; // content displayed to user
+ selection: Selection | null; // the user's selection
+ files: vscode.Uri[]; // the files sent in the message
+ }
+ | {
+ role: "assistant";
+ content: string; // content received from LLM
+ displayContent: string; // content displayed to user (this is the same as content for now)
+ };
// const [stateRef, setState] = useInstantState(initVal)
// setState instantly changes the value of stateRef instead of having to wait until the next render
+
const useInstantState =
(initVal: T) => {
- const stateRef = useRef(initVal)
- const [_, setS] = useState(initVal)
+ const stateRef = useRef(initVal);
+ const [_, setS] = useState(initVal);
const setState = useCallback((newVal: T) => {
setS(newVal);
stateRef.current = newVal;
- }, [])
- return [stateRef as React.RefObject, setState] as const // make s.current readonly - setState handles all changes
-}
-
-
+ }, []);
+ return [stateRef as React.RefObject, setState] as const; // make s.current readonly - setState handles all changes
+};
const Sidebar = () => {
-
// 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
+ 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 [chatMessageHistory, setChatHistory] = useState([])
- const [messageStream, setMessageStream] = useState('')
- const [isLoading, setIsLoading] = useState(false)
+ const [chatMessageHistory, setChatHistory] = useState([]);
+ const [messageStream, setMessageStream] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [isDisabled, setIsDisabled] = useState(false);
+ const [errorShown, setErrorShown] = useState(false);
- const abortFnRef = useRef<(() => void) | null>(null)
- const [apiConfig, setApiConfig] = useState(null)
+ const abortFnRef = useRef<(() => void) | null>(null);
+
+ const [apiConfig, setApiConfig] = useState(null);
+
+ const checkApiConfig = (apiConfig: ApiConfig) => {
+ if (
+ (apiConfig.anthropic.apikey === "" &&
+ apiConfig.greptile.apikey === "" &&
+ apiConfig.openai.apikey === "" &&
+ (apiConfig.ollama.endpoint === "" ||
+ apiConfig.ollama.model === "")) ||
+ apiConfig.whichApi === ""
+ ) {
+ setIsDisabled(true);
+ } else {
+ setIsDisabled(false);
+ }
+
+ }
// get Api Config on mount
useEffect(() => {
- getVSCodeAPI().postMessage({ type: 'getApiConfig' })
- }, [])
+ getVSCodeAPI().postMessage({ type: "getApiConfig" });
+ }, []);
// Receive messages from the 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)
+ resolveAwaitingVSCodeResponse(m);
// if user pressed ctrl+l, add their selection to the sidebar
- if (m.type === 'ctrl+l') {
+ if (m.type === "ctrl+l") {
+ if (isDisabled) {
+ getVSCodeAPI().postMessage({
+ type: "displayError",
+ message: "Required API keys are not set.",
+ });
+ return;
+ }
+ setSelection(m.selection);
-
- setSelection(m.selection)
-
- const filepath = m.selection.filePath
+ const filepath = m.selection.filePath;
// add file if it's not a duplicate
- if (!files.find(f => f.fsPath === filepath.fsPath)) setFiles(files => [...files, filepath])
-
+ if (!files.find((f) => f.fsPath === filepath.fsPath))
+ setFiles((files) => [...files, filepath]);
}
// when get apiConfig, set
- else if (m.type === 'apiConfig') {
- setApiConfig(m.apiConfig)
+ else if (m.type === "apiConfig") {
+ setApiConfig(m.apiConfig);
+ checkApiConfig(m.apiConfig);
}
+ };
+ window.addEventListener("message", listener);
+ return () => {
+ window.removeEventListener("message", listener);
+ };
+ }, [files, selection, isDisabled]);
- }
- window.addEventListener('message', listener);
- return () => { window.removeEventListener('message', listener) }
- }, [files, selection])
-
-
- const formRef = useRef(null)
+ const formRef = useRef(null);
const onSubmit = async (e: FormEvent) => {
+ e.preventDefault();
+ if (isLoading || isDisabled) return;
- e.preventDefault()
- if (isLoading) return
-
- setIsLoading(true)
- setInstructions('');
+ setIsLoading(true);
+ setInstructions("");
formRef.current?.reset(); // reset the form's text
- setSelection(null)
- setFiles([])
+ setSelection(null);
+ setFiles([]);
// request file content from vscode and await response
- getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
- const relevantFiles = await awaitVSCodeResponse('files')
+ getVSCodeAPI().postMessage({ type: "requestFiles", filepaths: files });
+ const relevantFiles = await awaitVSCodeResponse("files");
// add message to chat history
- const content = userInstructionsStr(instructions, relevantFiles.files, selection)
+ const content = userInstructionsStr(
+ instructions,
+ relevantFiles.files,
+ selection
+ );
// console.log('prompt:\n', content)
- const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
- setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
+ const newHistoryElt: ChatMessage = {
+ role: "user",
+ content,
+ displayContent: instructions,
+ selection,
+ files,
+ };
+ setChatHistory((chatMessageHistory) => [
+ ...chatMessageHistory,
+ newHistoryElt,
+ ]);
// send message to claude
let { abort } = sendLLMMessage({
- messages: [...chatMessageHistory.map(m => ({ role: m.role, content: m.content })), { role: 'user', content }],
+ messages: [
+ ...chatMessageHistory.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
- const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
- setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
+ const newHistoryElt: ChatMessage = {
+ role: "assistant",
+ content,
+ displayContent: content,
+ };
+ setChatHistory((chatMessageHistory) => [
+ ...chatMessageHistory,
+ newHistoryElt,
+ ]);
// clear selection
- setMessageStream('')
- setIsLoading(false)
+ setMessageStream("");
+ setIsLoading(false);
},
- apiConfig: apiConfig
- })
-
-
- abortFnRef.current = abort
- }
+ apiConfig: apiConfig,
+ });
+ abortFnRef.current = abort;
+ };
const onStop = useCallback(() => {
// abort claude
- abortFnRef.current?.()
+ 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 }
- setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
+ const llmContent = messageStream || "(canceled)";
+ const newHistoryElt: ChatMessage = {
+ role: "assistant",
+ displayContent: messageStream,
+ content: llmContent,
+ };
+ setChatHistory((chatMessageHistory) => [
+ ...chatMessageHistory,
+ newHistoryElt,
+ ]);
- setMessageStream('')
- setIsLoading(false)
-
- }, [messageStream])
+ setMessageStream("");
+ setIsLoading(false);
+ }, [messageStream]);
//Clear code selection
const clearSelection = () => {
setSelection(null);
};
- return <>
-
-
- {/* previous messages */}
- {chatMessageHistory.map((message, i) =>
-
- )}
- {/* message stream */}
-
-
- {/* chatbar */}
-
- {/* selection */}
-
- {/* selected files */}
-
- {/* selected code */}
- {!selection?.selectionStr ? null
- : (
+ return (
+ <>
+
+
+ {/* previous messages */}
+ {chatMessageHistory.map((message, i) => (
+
+ ))}
+ {/* message stream */}
+
+
+ {/* chatbar */}
+
+ {/* selection */}
+
+ {/* selected files */}
+
+ {/* selected code */}
+ {!selection?.selectionStr ? null : (
-
+
- )}
+ )}
+
+
-
+ {/* Red overlay when isDisabled is true */}
+ {isDisabled && (
+
+ )}
-
+ >
+ );
+};
- >
-
-}
-
-export default Sidebar
+export default Sidebar;
diff --git a/extensions/void/src/sidebar/styles.css b/extensions/void/src/sidebar/styles.css
index b5c61c95..d5fc7bc9 100644
--- a/extensions/void/src/sidebar/styles.css
+++ b/extensions/void/src/sidebar/styles.css
@@ -1,3 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+
+.no-select {
+ user-select: none;
+ pointer-events: none;
+ filter: blur(3px)
+}