From 631f7241382e199d2a3bce6d24eb24c1961cb88b Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 1 Oct 2024 22:25:16 -0700 Subject: [PATCH] merge ollama --- extensions/void/src/SidebarWebviewProvider.ts | 51 +- extensions/void/src/common/sendLLMMessage.ts | 117 ++-- extensions/void/src/extension.ts | 10 +- extensions/void/src/shared_types.ts | 3 - extensions/void/src/sidebar/Sidebar.tsx | 538 +++++++----------- extensions/void/src/sidebar/getVscodeApi.ts | 1 - extensions/void/src/sidebar/styles.css | 6 - extensions/void/tailwind.config.js | 1 - 8 files changed, 288 insertions(+), 439 deletions(-) diff --git a/extensions/void/src/SidebarWebviewProvider.ts b/extensions/void/src/SidebarWebviewProvider.ts index 0d85fd7f..d5a69992 100644 --- a/extensions/void/src/SidebarWebviewProvider.ts +++ b/extensions/void/src/SidebarWebviewProvider.ts @@ -14,15 +14,15 @@ function getNonce() { export class SidebarWebviewProvider implements vscode.WebviewViewProvider { public static readonly viewId = 'void.viewnumberone'; - public webview: Promise // used to send messages to the webview + public webview: Promise // 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 - private _res: (c: vscode.Webview) => void // used to resolve the webview - private allowed_urls: string[] = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com', 'http://localhost:11434']; - private _webviewView?: vscode.WebviewView; + + private _webviewView?: vscode.WebviewView; // only used inside onDidChangeConfiguration constructor(context: vscode.ExtensionContext) { - // const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later, not sure for what though... was included in webviewProvider code + // 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 @@ -32,23 +32,25 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { vscode.workspace.onDidChangeConfiguration(event => { if (event.affectsConfiguration('void.ollamaSettings.endpoint')) { - this.updateAllowedUrls(); - // Regenerate the webview's HTML content if (this._webviewView) { - this._webviewView.webview.html = this.getWebviewContent(this._webviewView.webview); + this.updateWebviewHTML(this._webviewView.webview); } } }); } - private getWebviewContent(webview: vscode.Webview): string { + private updateWebviewHTML(webview: vscode.Webview) { + const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com']; + const ollamaEndpoint: string | undefined = vscode.workspace.getConfiguration('void').get('ollamaSettings.endpoint'); + if (ollamaEndpoint) + allowed_urls.push(ollamaEndpoint); + 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')); const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri)); const nonce = getNonce(); - const allowed_urls = this.allowed_urls; - return ` + const webviewHTML = ` @@ -63,12 +65,10 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { `; + + webview.html = webviewHTML; } - private updateAllowedUrls() { - const ollamaEndpoint: string = vscode.workspace.getConfiguration('void').get('ollamaSettings.endpoint') || 'http://localhost:11434'; - this.allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com', ollamaEndpoint]; - } // called internally by vscode resolveWebviewView( @@ -76,31 +76,18 @@ export class SidebarWebviewProvider implements vscode.WebviewViewProvider { context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken, ) { - this._webviewView = webviewView; - const webview = webviewView.webview + const webview = webviewView.webview; webview.options = { enableScripts: true, localResourceRoots: [this._extensionUri] }; - // This allows us to use React in vscode - // when you run `npm run build`, we take the React code in the `sidebar` folder - // and compile it into `dist/sidebar/index.js` and `dist/sidebar/styles.css` - // we render that code here - const rootPath = this._extensionUri; - const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/index.js')); - const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/styles.css')); - const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath)); - - const nonce = getNonce(); // only scripts with the nonce are allowed to run, this is a recommended security measure - - // Regenerate the Sidebar html whenever the allowed_urls changes - this.updateAllowedUrls(); - const allowed_urls = this.allowed_urls; - webview.html = this.getWebviewContent(webview); + this.updateWebviewHTML(webview); + // resolve webview and _webviewView this._res(webview); + this._webviewView = webviewView; } } diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index 68955b34..1d9a9a78 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -105,11 +105,13 @@ const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal // OpenAI const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { - let did_abort = false + let didAbort = false let fullText = '' // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - let abort: () => void = () => { did_abort = true } + let abort: () => void = () => { + didAbort = true; + }; const openai = new OpenAI({ apiKey: apiConfig.openai.apikey, dangerouslyAllowBrowser: true }); @@ -120,13 +122,13 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal }) .then(async response => { abort = () => { - // response.controller.abort() // this isn't needed now, to keep consistency with claude will leave it commented - did_abort = true; + // response.controller.abort() + didAbort = true; } // when receive text try { for await (const chunk of response) { - if (did_abort) return; + if (didAbort) return; const newText = chunk.choices[0]?.delta?.content || ''; fullText += newText; onText(newText, fullText); @@ -138,8 +140,50 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal console.error('Error in OpenAI stream:', error); onFinalMessage(fullText); } - // when we get the final message on this stream - onFinalMessage(fullText) + }) + return { abort }; +}; + + + +// Ollama +export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { + + let didAbort = false + let fullText = "" + + // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either + let abort = () => { + didAbort = true; + }; + + const ollama = new Ollama({ host: apiConfig.ollama.endpoint }) + + ollama.chat({ + model: apiConfig.ollama.model, + messages: messages, + stream: true, + }) + .then(async stream => { + abort = () => { + // ollama.abort() + didAbort = true + } + // iterate through the stream + try { + for await (const chunk of stream) { + if (didAbort) return; + const newText = chunk.message.content; + fullText += newText; + onText(newText, fullText); + } + onFinalMessage(fullText); + } + // when error/fail + catch (error) { + console.error('Error:', error); + onFinalMessage(fullText); + } }) return { abort }; }; @@ -152,11 +196,11 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { - let did_abort = false + let didAbort = false let fullText = '' // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - let abort: () => void = () => { did_abort = true } + let abort: () => void = () => { didAbort = true } fetch('https://api.greptile.com/v2/query', { @@ -180,7 +224,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin }) // TODO make this actually stream, right now it just sends one message at the end .then(async responseArr => { - if (did_abort) + if (didAbort) return for (let response of responseArr) { @@ -215,28 +259,13 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin return { abort } - - } + export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => { if (!apiConfig) return { abort: () => { } } - if ( - 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.' }) - return { abort: () => { }} - } - - - switch (apiConfig.whichApi) { case 'anthropic': return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); @@ -249,41 +278,7 @@ export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, default: console.error(`Error: whichApi was ${apiConfig.whichApi}, which is not recognized!`); return { abort: () => { } } - //return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO + //return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO } } - -// Ollama -export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => { - const ollamaClient = new Ollama({ host: apiConfig.ollama.endpoint }) - - let didAbort = false; - let fullText = ""; - - // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either - const abort = () => { - didAbort = true; - }; - - ollamaClient.chat({ - model: apiConfig.ollama.model, - messages: messages, - stream: true, - }) - .then(async (stream) => { - for await (const chunk of stream) { - if (didAbort) return; - const newText = chunk.message.content; - fullText += newText; - onText(newText, fullText); - } - onFinalMessage(fullText); - }) - .catch((error) => { - console.error('Error:', error); - onFinalMessage(fullText); - }); - - return { abort }; -}; diff --git a/extensions/void/src/extension.ts b/extensions/void/src/extension.ts index a2547440..a06881d4 100644 --- a/extensions/void/src/extension.ts +++ b/extensions/void/src/extension.ts @@ -114,7 +114,8 @@ export function activate(context: vscode.ExtensionContext) { // send contents to webview webview.postMessage({ type: 'files', files, } satisfies WebviewMessage) - } else if (m.type === 'applyCode') { + } + else if (m.type === 'applyCode') { const editor = vscode.window.activeTextEditor if (!editor) { @@ -124,19 +125,16 @@ export function activate(context: vscode.ExtensionContext) { const oldContents = await readFileContentOfUri(editor.document.uri) const suggestedEdits = getDiffedLines(oldContents, m.code) await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits) - } else if (m.type === 'getApiConfig') { + } + else if (m.type === 'getApiConfig') { const apiConfig = getApiConfig() console.log('Api config:', apiConfig) webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage) - } else if (m.type === 'displayError') { - - vscode.window.showErrorMessage(m.message, { modal: true }); } else { - console.error('unrecognized command', m.type, m) } }) diff --git a/extensions/void/src/shared_types.ts b/extensions/void/src/shared_types.ts index 722c3218..4d7c0dc9 100644 --- a/extensions/void/src/shared_types.ts +++ b/extensions/void/src/shared_types.ts @@ -27,9 +27,6 @@ type WebviewMessage = ( // editor -> sidebar | { type: 'apiConfig', apiConfig: ApiConfig } - // Display native vscode error - | { type: 'displayError', message: string } // - ) type Command = WebviewMessage['type'] diff --git a/extensions/void/src/sidebar/Sidebar.tsx b/extensions/void/src/sidebar/Sidebar.tsx index caa9faf3..539b06be 100644 --- a/extensions/void/src/sidebar/Sidebar.tsx +++ b/extensions/void/src/sidebar/Sidebar.tsx @@ -1,57 +1,31 @@ -/* 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 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} @@ -62,303 +36,235 @@ If you make a change, rewrite the entire 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 */} - -
- )} -
- ); + 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) => -
- -
- )} -
- ); -}; + -{' '}{getBasename(filename.fsPath)} + + + )} + +} + const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => { - const role = chatMessage.role; - const children = chatMessage.displayContent; - if (!children) return null; + const role = chatMessage.role + const children = chatMessage.displayContent - let chatbubbleContents: React.ReactNode; + if (!children) + return null - if (role === "user") { - chatbubbleContents = ( - <> - - {chatMessage.selection?.selectionStr && ( - - )} - {children} - - ); - } else if (role === "assistant") { + let chatbubbleContents: React.ReactNode + + 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]; -}; + 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) +} -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 [isDisabled, setIsDisabled] = useState(false); - const [errorShown, setErrorShown] = useState(false); + const [chatMessageHistory, setChatHistory] = useState([]) + const [messageStream, setMessageStream] = useState('') + const [isLoading, setIsLoading] = useState(false) - const abortFnRef = useRef<(() => void) | null>(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); - } - } + const [apiConfig, setApiConfig] = useState(null) // 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 (isDisabled) { - getVSCodeAPI().postMessage({ - type: "displayError", - message: "Required API keys are not set.", - }); - return; - } - setSelection(m.selection); + if (m.type === 'ctrl+l') { - const filepath = m.selection.filePath; + setSelection(m.selection) + + 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); - checkApiConfig(m.apiConfig); + else if (m.type === 'apiConfig') { + setApiConfig(m.apiConfig) } - }; - window.addEventListener("message", listener); - return () => { - window.removeEventListener("message", listener); - }; - }, [files, selection, isDisabled]); - const formRef = useRef(null); + } + window.addEventListener('message', listener); + return () => { window.removeEventListener('message', listener) } + }, [files, selection]) + + + const formRef = useRef(null) const onSubmit = async (e: FormEvent) => { - e.preventDefault(); - if (isLoading || isDisabled) return; - setIsLoading(true); - setInstructions(""); + e.preventDefault() + if (isLoading) return + + 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, - }); + apiConfig: apiConfig + }) + abortFnRef.current = abort - 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 + : (
- +
)} -
-
{ - if (e.key === "Enter" && !e.shiftKey) onSubmit(e); - }} - onSubmit={(e) => { - console.log("submit!"); - e.preventDefault(); - onSubmit(e); - }} - > - {/* input */} -