Merge pull request #53 from anetaj/feature/persist-threads-history

Persist chat threads history
This commit is contained in:
Andrew Pareles 2024-10-11 19:19:13 -07:00 committed by GitHub
commit e04d839640
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 338 additions and 125 deletions

View file

@ -8,7 +8,7 @@
"**/.DS_Store": true, "**/.DS_Store": true,
"**/Thumbs.db": true, "**/Thumbs.db": true,
"out": false, "out": false,
"**/node_modules": false "**/node_modules": true
}, },
"search.exclude": { "search.exclude": {
"out": true // set this to false to include "out" folder in search results "out": true // set this to false to include "out" folder in search results

View file

@ -6,3 +6,6 @@ Here's an overview on how the extension works:
- The extension mounts in `extension.ts`. - The extension mounts in `extension.ts`.
- The Sidebar's HTML (everything in `sidebar/`) is built in React, and it's rendered by mounting a `<script>` tag - see `SidebarWebviewProvider.ts`. - The Sidebar's HTML (everything in `sidebar/`) is built in React, and it's rendered by mounting a `<script>` tag - see `SidebarWebviewProvider.ts`.
- Communication between the sidebar script and the extension takes place via API. You can search for "postMessage" to see where API calls happen.

View file

@ -228,6 +228,15 @@
"title": "Discard Diff" "title": "Discard Diff"
}, },
{ {
"command": "void.startNewThread",
"title": "Start a new chat",
"icon": "$(add)"
},
{
"command": "void.toggleThreadSelector",
"title": "View past chats",
"icon": "$(history)"
}, {
"command": "void.openSettings", "command": "void.openSettings",
"title": "Void settings", "title": "Void settings",
"icon": "$(settings-gear)" "icon": "$(settings-gear)"
@ -265,6 +274,16 @@
], ],
"menus": { "menus": {
"view/title": [ "view/title": [
{
"command": "void.startNewThread",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
},
{
"command": "void.toggleThreadSelector",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
},
{ {
"command": "void.openSettings", "command": "void.openSettings",
"when": "view == 'void.viewnumberone'", "when": "view == 'void.viewnumberone'",

View file

@ -1,7 +1,6 @@
import Anthropic from '@anthropic-ai/sdk'; import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai'; import OpenAI from 'openai';
import { Ollama } from 'ollama/browser' import { Ollama } from 'ollama/browser'
import { getVSCodeAPI } from '../sidebar/getVscodeApi';
// always compare these against package.json to make sure every setting in this type can actually be provided by the user // always compare these against package.json to make sure every setting in this type can actually be provided by the user

View file

@ -1,5 +1,5 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { WebviewMessage } from './shared_types'; import { ChatThreads, WebviewMessage } from './shared_types';
import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider'; import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider';
import { getDiffedLines } from './getDiffedLines'; import { getDiffedLines } from './getDiffedLines';
import { ApprovalCodeLensProvider } from './ApprovalCodeLensProvider'; import { ApprovalCodeLensProvider } from './ApprovalCodeLensProvider';
@ -100,6 +100,14 @@ export function activate(context: vscode.ExtensionContext) {
webviewProvider.webview.then( webviewProvider.webview.then(
webview => { webview => {
// top navigation bar commands
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
webview.postMessage({ type: 'startNewThread' } satisfies WebviewMessage)
}))
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
webview.postMessage({ type: 'toggleThreadSelector' } satisfies WebviewMessage)
}))
// when config changes, send it to the sidebar // when config changes, send it to the sidebar
vscode.workspace.onDidChangeConfiguration(e => { vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('void')) { if (e.affectsConfiguration('void')) {
@ -135,6 +143,7 @@ export function activate(context: vscode.ExtensionContext) {
await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits) await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits)
} }
else if (m.type === 'getApiConfig') { else if (m.type === 'getApiConfig') {
context.workspaceState.update('allThreads', {})
const apiConfig = getApiConfig() const apiConfig = getApiConfig()
console.log('Api config:', apiConfig) console.log('Api config:', apiConfig)
@ -142,6 +151,15 @@ export function activate(context: vscode.ExtensionContext) {
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage) webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
} }
else if (m.type === 'getAllThreads') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
webview.postMessage({ type: 'allThreads', threads } satisfies WebviewMessage)
}
else if (m.type === 'persistThread') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
const updatedThreads: ChatThreads = { ...threads, [m.thread.id]: m.thread }
context.workspaceState.update('allThreads', updatedThreads)
}
else { else {
console.error('unrecognized command', m.type, m) console.error('unrecognized command', m.type, m)
} }

View file

@ -27,13 +27,52 @@ type WebviewMessage = (
// editor -> sidebar // editor -> sidebar
| { type: 'apiConfig', apiConfig: ApiConfig } | { 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' }
) )
type Command = WebviewMessage['type'] type Command = WebviewMessage['type']
type ChatThreads = {
[id: string]: {
id: string; // store the id here too
createdAt: string;
messages: ChatMessage[];
}
}
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)
}
export { export {
Selection, Selection,
File, File,
WebviewMessage, WebviewMessage,
Command, Command,
ChatThreads,
ChatMessage,
} }

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react" import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage" import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage"
import { File, Selection, WebviewMessage } from "../shared_types" import { ChatMessage, File, Selection, WebviewMessage } from "../shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi" import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
import { marked } from 'marked'; import { marked } from 'marked';
@ -8,7 +8,8 @@ import MarkdownRender from "./markdown/MarkdownRender";
import BlockCode from "./markdown/BlockCode"; import BlockCode from "./markdown/BlockCode";
import * as vscode from 'vscode' import * as vscode from 'vscode'
import { FilesSelector, IncludedFiles } from "./components/Files"; import { FilesSelector, SelectedFiles } from "./components/SelectedFiles";
import { useChat } from "./chatContext";
const filesStr = (fullFiles: File[]) => { const filesStr = (fullFiles: File[]) => {
@ -49,7 +50,7 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
if (role === 'user') { if (role === 'user') {
chatbubbleContents = <> chatbubbleContents = <>
<IncludedFiles files={chatMessage.files} /> <SelectedFiles files={chatMessage.files} />
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />} {chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />}
{children} {children}
</> </>
@ -67,34 +68,48 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
</div> </div>
} }
type ChatMessage = { const ThreadSelector = ({ onClose }: { onClose: () => void }) => {
role: 'user' const { allThreads, currentThread, switchToThread } = useChat()
content: string, // content sent to the llm return (
displayContent: string, // content displayed to user <div className="flex flex-col space-y-1">
selection: Selection | null, // the user's selection <div className="text-right">
files: vscode.Uri[], // the files sent in the message <button className="btn btn-sm" onClick={onClose}>
} | { <svg
role: 'assistant', xmlns="http://www.w3.org/2000/svg"
content: string, // content received from LLM fill="none"
displayContent: string // content displayed to user (this is the same as content for now) viewBox="0 0 24 24"
} stroke="currentColor"
className="size-4"
>
// const [stateRef, setState] = useInstantState(initVal) <path
// setState instantly changes the value of stateRef instead of having to wait until the next render strokeLinecap="round"
const useInstantState = <T,>(initVal: T) => { strokeLinejoin="round"
const stateRef = useRef<T>(initVal) d="M6 18 18 6M6 6l12 12"
const [_, setS] = useState<T>(initVal) />
const setState = useCallback((newVal: T) => { </svg>
setS(newVal); </button>
stateRef.current = newVal; </div>
}, []) {/* iterate through all past threads */}
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes {Object.keys(allThreads ?? {}).map((threadId) => {
const pastThread = (allThreads ?? {})[threadId];
return (
<button
key={pastThread.id}
className={`btn btn-sm btn-secondary ${pastThread.id === currentThread?.id ? "btn-primary" : ""}`}
onClick={() => switchToThread(pastThread.id)}
>
{new Date(pastThread.createdAt).toLocaleString()}
</button>
)
})}
</div>
)
} }
const Sidebar = () => { const Sidebar = () => {
const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useChat()
// state of current message // state of current message
const [selection, setSelection] = useState<Selection | null>(null) // the code the user is selecting const [selection, setSelection] = useState<Selection | null>(null) // the code the user is selecting
@ -102,9 +117,9 @@ const Sidebar = () => {
const [instructions, setInstructions] = useState('') // the user's instructions const [instructions, setInstructions] = useState('') // the user's instructions
// state of chat // state of chat
const [chatMessageHistory, setChatMessageHistory] = useState<ChatMessage[]>([])
const [messageStream, setMessageStream] = useState('') const [messageStream, setMessageStream] = useState('')
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false)
const abortFnRef = useRef<(() => void) | null>(null) const abortFnRef = useRef<(() => void) | null>(null)
@ -126,13 +141,12 @@ const Sidebar = () => {
// if user pressed ctrl+l, add their selection to the sidebar // if user pressed ctrl+l, add their selection to the sidebar
if (m.type === 'ctrl+l') { if (m.type === 'ctrl+l') {
setSelection(m.selection) setSelection(m.selection)
const filepath = m.selection.filePath const filepath = m.selection.filePath
// add file if it's not a duplicate // 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]) if (!files.find(f => f.fsPath === filepath.fsPath))
setFiles(files => [...files, filepath])
} }
// when get apiConfig, set // when get apiConfig, set
@ -140,10 +154,21 @@ const Sidebar = () => {
setApiConfig(m.apiConfig) setApiConfig(m.apiConfig)
} }
// if they pressed the + to add a new chat
else if (m.type === 'startNewThread') {
setIsThreadSelectorOpen(false)
startNewThread()
}
// if they opened thread selector
else if (m.type === 'toggleThreadSelector') {
setIsThreadSelectorOpen(v => !v)
}
} }
window.addEventListener('message', listener); window.addEventListener('message', listener);
return () => { window.removeEventListener('message', listener) } return () => { window.removeEventListener('message', listener) }
}, [files, selection]) }, [files, selection, startNewThread])
const formRef = useRef<HTMLFormElement | null>(null) const formRef = useRef<HTMLFormElement | null>(null)
@ -158,15 +183,6 @@ const Sidebar = () => {
setSelection(null) setSelection(null)
setFiles([]) setFiles([])
// TODO this is just a hack, turn this into a button instead, and track all histories somewhere
if (instructions === 'clear') {
setChatMessageHistory([])
setMessageStream('')
setIsLoading(false)
return
}
// request file content from vscode and await response // request file content from vscode and await response
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files }) getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
const relevantFiles = await awaitVSCodeResponse('files') const relevantFiles = await awaitVSCodeResponse('files')
@ -175,16 +191,18 @@ const Sidebar = () => {
const content = userInstructionsStr(instructions, relevantFiles.files, selection) const content = userInstructionsStr(instructions, relevantFiles.files, selection)
// console.log('prompt:\n', content) // console.log('prompt:\n', content)
const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files } const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
setChatMessageHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) addMessageToHistory(newHistoryElt)
// send message to claude // send message to claude
let { abort } = sendLLMMessage({ let { abort } = sendLLMMessage({
messages: [...chatMessageHistory.map(m => ({ role: m.role, content: m.content })), { role: 'user', content }], messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content }],
onText: (newText, fullText) => setMessageStream(fullText), onText: (newText, fullText) => setMessageStream(fullText),
onFinalMessage: (content) => { onFinalMessage: (content) => {
// add assistant's message to chat history, and clear selection // add assistant's message to chat history, and clear selection
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, } const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
setChatMessageHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) addMessageToHistory(newHistoryElt)
// clear selection
setMessageStream('') setMessageStream('')
setIsLoading(false) setIsLoading(false)
}, },
@ -201,12 +219,12 @@ const Sidebar = () => {
// if messageStream was not empty, add it to the history // if messageStream was not empty, add it to the history
const llmContent = messageStream || '(canceled)' const llmContent = messageStream || '(canceled)'
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent } const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
setChatMessageHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt]) addMessageToHistory(newHistoryElt)
setMessageStream('') setMessageStream('')
setIsLoading(false) setIsLoading(false)
}, [messageStream]) }, [addMessageToHistory, messageStream])
//Clear code selection //Clear code selection
const clearSelection = () => { const clearSelection = () => {
@ -215,9 +233,14 @@ const Sidebar = () => {
return <> return <>
<div className="flex flex-col h-screen w-full"> <div className="flex flex-col h-screen w-full">
{isThreadSelectorOpen && (
<div className="mb-2 max-h-[30vh] overflow-y-auto">
<ThreadSelector onClose={() => setIsThreadSelectorOpen(false)} />
</div>
)}
<div className="overflow-y-auto overflow-x-hidden space-y-4"> <div className="overflow-y-auto overflow-x-hidden space-y-4">
{/* previous messages */} {/* previous messages */}
{chatMessageHistory.map((message, i) => {currentThread !== null && currentThread.messages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} /> <ChatBubble key={i} chatMessage={message} />
)} )}
{/* message stream */} {/* message stream */}
@ -225,61 +248,71 @@ const Sidebar = () => {
</div> </div>
{/* chatbar */} {/* chatbar */}
<div className="shrink-0 py-4"> <div className="shrink-0 py-4">
<div className="input"> {/* selection */}
{/* selection */} <div className="text-left">
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2"> {/* selected files */}
{/* selected files */} <FilesSelector files={files} setFiles={setFiles} />
<FilesSelector files={files} setFiles={setFiles} /> {/* selected code */}
{/* selected code */}
{!!selection?.selectionStr && (
<BlockCode className="rounded bg-vscode-sidebar-bg" text={selection.selectionStr} toolbar={(
<button
onClick={clearSelection}
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
>
Remove
</button>
)} />
)}
</div>}
<form
ref={formRef}
className="flex flex-row items-center rounded-md p-2"
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
onSubmit={(e) => {
console.log('submit!')
e.preventDefault();
onSubmit(e)
}}>
{/* input */}
<textarea <div className="relative">
onChange={(e) => { setInstructions(e.target.value) }} <div className="input">
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none" {/* selection */}
placeholder="Ctrl+L to select" {(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
rows={1} {/* selected files */}
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically <FilesSelector files={files} setFiles={setFiles} />
/> {/* selected code */}
{/* submit button */} {!!selection?.selectionStr && (
{isLoading ? <BlockCode className="rounded bg-vscode-sidebar-bg" text={selection.selectionStr} toolbar={(
<button <button
onClick={onStop} onClick={clearSelection}
className="btn btn-primary rounded-r-lg max-h-10 p-2" className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
type='button' >
>Stop</button> Remove
: <button </button>
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10" )} />
disabled={!instructions} )}
type='submit' </div>}
> <form
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> ref={formRef}
<line x1="12" y1="19" x2="12" y2="5"></line> className="flex flex-row items-center rounded-md p-2"
<polyline points="5 12 12 5 19 12"></polyline> onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
</svg>
</button> onSubmit={(e) => {
} console.log('submit!')
</form> e.preventDefault();
onSubmit(e)
}}>
{/* input */}
<textarea
onChange={(e) => { setInstructions(e.target.value) }}
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
placeholder="Ctrl+L to select"
rows={1}
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
/>
{/* submit button */}
{isLoading ?
<button
onClick={onStop}
className="btn btn-primary rounded-r-lg max-h-10 p-2"
type='button'
>Stop</button>
: <button
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
disabled={!instructions}
type='submit'
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</button>
}
</form>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -0,0 +1,102 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
import { ChatMessage, ChatThreads } from "../shared_types"
import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
type ChatContextValue = {
readonly allThreads: ChatThreads | null,
readonly currentThread: ChatThreads[string] | null;
addMessageToHistory: (message: ChatMessage) => void;
switchToThread: (threadId: string) => void;
startNewThread: () => void;
}
const ChatContext = createContext<ChatContextValue>({} as ChatContextValue)
const createNewThread = () => ({
id: new Date().getTime().toString(),
createdAt: new Date().toISOString(),
messages: [],
})
// const [stateRef, setState] = useInstantState(initVal)
// setState instantly changes the value of stateRef instead of having to wait until the next render
const useInstantState = <T,>(initVal: T) => {
const stateRef = useRef<T>(initVal)
const [_, setS] = useState<T>(initVal)
const setState = useCallback((newVal: T) => {
setS(newVal);
stateRef.current = newVal;
}, [])
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
}
function ChatProvider({ children }: { children: ReactNode }) {
const [allThreads, setAllThreads] = useInstantState<ChatThreads>({})
const [currentThreadId, setCurrentThreadId] = useInstantState<string | null>(null)
// this loads allThreads in on mount
useEffect(() => {
getVSCodeAPI().postMessage({ type: "getAllThreads" })
awaitVSCodeResponse('allThreads')
.then(response => { setAllThreads(response.threads) })
}, [setAllThreads])
return (
<ChatContext.Provider
value={{
allThreads: allThreads.current,
currentThread: currentThreadId.current === null || allThreads.current === null ? null : allThreads.current[currentThreadId.current],
addMessageToHistory: (message: ChatMessage) => {
let currentThread: ChatThreads[string]
if (!(currentThreadId.current === null || allThreads.current === null)) {
currentThread = allThreads.current[currentThreadId.current]
}
else {
currentThread = createNewThread()
setCurrentThreadId(currentThread.id)
}
console.log('adding message: ', currentThreadId, currentThread.id, message.displayContent)
console.log('allThreads', allThreads)
setAllThreads({
...allThreads.current,
[currentThread.id]: {
...currentThread,
messages: [...currentThread.messages, message],
}
})
getVSCodeAPI().postMessage({ type: "persistThread", thread: currentThread })
},
switchToThread: (threadId: string) => {
setCurrentThreadId(threadId);
},
startNewThread: () => {
const newThread = createNewThread()
setAllThreads({
...allThreads.current,
[newThread.id]: newThread
})
setCurrentThreadId(newThread.id)
},
}}
>
{children}
</ChatContext.Provider>
)
}
function useChat(): ChatContextValue {
const context = useContext<ChatContextValue>(ChatContext)
if (context === undefined) {
throw new Error("useChat must be used within a ChatProvider")
}
return context
}
export { ChatProvider, useChat }

View file

@ -51,7 +51,7 @@ export const FilesSelector = ({
) )
} }
export const IncludedFiles = ({ files }: { files: vscode.Uri[] }) => { export const SelectedFiles = ({ files }: { files: vscode.Uri[] }) => {
return ( return (
files.length !== 0 && ( files.length !== 0 && (
<div className="text-xs my-2"> <div className="text-xs my-2">

View file

@ -10,6 +10,11 @@ const awaiting: { [c in Command]: ((res: any) => void)[] } = {
"files": [], "files": [],
"apiConfig": [], "apiConfig": [],
"getApiConfig": [], "getApiConfig": [],
"startNewThread": [],
"getAllThreads": [],
"allThreads": [],
"persistThread": [],
"toggleThreadSelector": []
} }
// use this function to await responses // use this function to await responses

View file

@ -1,16 +1,20 @@
import * as React from 'react' import * as React from "react"
import * as ReactDOM from 'react-dom/client' import * as ReactDOM from "react-dom/client"
import Sidebar from './Sidebar' import Sidebar from "./Sidebar"
import { ChatProvider } from "./chatContext"
// mount the sidebar on the id="root" element // mount the sidebar on the id="root" element
if (typeof document === 'undefined') { if (typeof document === "undefined") {
console.log('index.tsx error: document was undefined') console.log("index.tsx error: document was undefined")
} }
const rootElement = document.getElementById('root')! const rootElement = document.getElementById("root")!
console.log('root Element', rootElement) console.log("root Element", rootElement)
const extension = (
<ChatProvider>
<Sidebar />
</ChatProvider>
)
const root = ReactDOM.createRoot(rootElement) const root = ReactDOM.createRoot(rootElement)
root.render(<Sidebar />) root.render(extension)

View file

@ -1,6 +1,5 @@
import React, { ReactNode, useCallback, useEffect, useState } from "react" import React, { ReactNode, useCallback, useEffect, useState } from "react"
import { getVSCodeAPI } from "../getVscodeApi" import { getVSCodeAPI } from "../getVscodeApi"
import { classNames } from "../utils"
enum CopyButtonState { enum CopyButtonState {
Copy = "Copy", Copy = "Copy",
@ -70,11 +69,7 @@ const BlockCode = ({
</div> </div>
)} )}
<div <div
className={classNames( className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${!hideToolbar ? "rounded-tl-none" : ""} ${className}`}
"overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg",
!hideToolbar && "rounded-tl-none",
className
)}
> >
<pre className="p-2">{text}</pre> <pre className="p-2">{text}</pre>
</div> </div>

View file

@ -1,3 +0,0 @@
export default function classNames(...classes: any[]) {
return classes.filter(Boolean).join(' ')
}

View file

@ -1 +0,0 @@
export { default as classNames } from "./classNames";