mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
finish merge
This commit is contained in:
parent
b72da01632
commit
251b65d04d
11 changed files with 200 additions and 254 deletions
|
|
@ -6,3 +6,6 @@ Here's an overview on how the extension works:
|
|||
- 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`.
|
||||
|
||||
- Communication between the sidebar script and the extension takes place via API. You can search for "postMessage" to see where API calls happen.
|
||||
|
||||
|
|
|
|||
|
|
@ -228,13 +228,13 @@
|
|||
"title": "Discard Diff"
|
||||
},
|
||||
{
|
||||
"command": "void.newChat",
|
||||
"title": "New chat",
|
||||
"command": "void.startNewThread",
|
||||
"title": "Start a new chat",
|
||||
"icon": "$(add)"
|
||||
},
|
||||
{
|
||||
"command": "void.prevChats",
|
||||
"title": "Previous chats",
|
||||
"command": "void.openThreadSelector",
|
||||
"title": "View all your past chats",
|
||||
"icon": "$(history)"
|
||||
}, {
|
||||
"command": "void.openSettings",
|
||||
|
|
@ -275,12 +275,12 @@
|
|||
"menus": {
|
||||
"view/title": [
|
||||
{
|
||||
"command": "void.newChat",
|
||||
"command": "void.startNewThread",
|
||||
"when": "view == 'void.viewnumberone'",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "void.prevChats",
|
||||
"command": "void.openThreadSelector",
|
||||
"when": "view == 'void.viewnumberone'",
|
||||
"group": "navigation"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as vscode from 'vscode';
|
||||
import { ChatThread, WebviewMessage } from './shared_types';
|
||||
import { ChatThreads, WebviewMessage } from './shared_types';
|
||||
import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider';
|
||||
import { getDiffedLines } from './getDiffedLines';
|
||||
import { ApprovalCodeLensProvider } from './ApprovalCodeLensProvider';
|
||||
|
|
@ -101,11 +101,11 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
webview => {
|
||||
|
||||
// top navigation bar commands
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.newChat', async () => {
|
||||
webview.postMessage({ type: 'startNewChat' } satisfies WebviewMessage)
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
|
||||
webview.postMessage({ type: 'startNewThread' } satisfies WebviewMessage)
|
||||
}))
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.prevChats', async () => {
|
||||
webview.postMessage({ type: 'showPreviousChats' } satisfies WebviewMessage)
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.openThreadSelector', async () => {
|
||||
webview.postMessage({ type: 'openThreadSelector' } satisfies WebviewMessage)
|
||||
}))
|
||||
|
||||
// when config changes, send it to the sidebar
|
||||
|
|
@ -150,19 +150,14 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
|
||||
|
||||
}
|
||||
else if (m.type === 'getThreadHistory') {
|
||||
|
||||
const threads: ChatThread[] = context.workspaceState.get('threadHistory') ?? []
|
||||
webview.postMessage({ type: 'threadHistory', threads } 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 === 'updateThread') {
|
||||
|
||||
const threads: ChatThread[] = context.workspaceState.get('threadHistory') as [] ?? []
|
||||
const updatedThreads = threads.find((t: ChatThread) => t.id === m.thread.id)
|
||||
? threads.map((t: ChatThread) => t.id === m.thread.id ? m.thread : t)
|
||||
: [...threads, m.thread]
|
||||
context.workspaceState.update('threadHistory', updatedThreads)
|
||||
webview.postMessage({ type: 'threadHistory', threads: updatedThreads } 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 {
|
||||
console.error('unrecognized command', m.type, m)
|
||||
|
|
|
|||
|
|
@ -28,42 +28,44 @@ type WebviewMessage = (
|
|||
| { type: 'apiConfig', apiConfig: ApiConfig }
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'getThreadHistory' }
|
||||
| { type: 'getAllThreads' }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'threadHistory', threads: ChatThread[] }
|
||||
| { type: 'allThreads', threads: ChatThreads }
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'updateThread', thread: ChatThread }
|
||||
| { type: 'persistThread', thread: ChatThreads[string] }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'startNewChat' }
|
||||
| { type: 'startNewThread' }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'showPreviousChats' }
|
||||
| { type: 'openThreadSelector' }
|
||||
|
||||
)
|
||||
|
||||
type Command = WebviewMessage['type']
|
||||
|
||||
type ChatThread = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
messages: ChatMessage[];
|
||||
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: "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)
|
||||
role: "assistant";
|
||||
content: string; // content received from LLM
|
||||
displayContent: string; // content displayed to user (this is the same as content for now)
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
@ -71,6 +73,6 @@ export {
|
|||
File,
|
||||
WebviewMessage,
|
||||
Command,
|
||||
ChatThread,
|
||||
ChatThreads,
|
||||
ChatMessage,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { ReactNode, useCallback, useEffect, useState } from "react"
|
||||
import { getVSCodeAPI } from "../getVscodeApi"
|
||||
import { classNames } from "../utils"
|
||||
|
||||
enum CopyButtonState {
|
||||
Copy = "Copy",
|
||||
|
|
@ -70,11 +69,7 @@ const BlockCode = ({
|
|||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
"overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg",
|
||||
!hideToolbar && "rounded-tl-none",
|
||||
className
|
||||
)}
|
||||
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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { MarkdownRender, BlockCode } from "./MarkdownRender";
|
|||
import * as vscode from 'vscode'
|
||||
import { FilesSelector, IncludedFiles } from "./components/Files";
|
||||
import { useChat } from "./context";
|
||||
import ThreadHistory from "./components/ThreadHistory";
|
||||
|
||||
|
||||
const filesStr = (fullFiles: File[]) => {
|
||||
|
|
@ -83,12 +82,51 @@ const useInstantState = <T,>(initVal: T) => {
|
|||
|
||||
|
||||
|
||||
const ThreadSelector = ({ onClose }: { onClose: () => void }) => {
|
||||
const { allThreads, currentThread, switchToThread } = useChat()
|
||||
return (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="text-right">
|
||||
<button className="btn btn-sm" onClick={onClose}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/* iterate through all past threads */}
|
||||
{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 {
|
||||
thread,
|
||||
currentThread,
|
||||
addMessageToHistory,
|
||||
setPreviousThreads,
|
||||
startNewChat,
|
||||
startNewThread,
|
||||
} = useChat()
|
||||
|
||||
// state of current message
|
||||
|
|
@ -97,10 +135,9 @@ const Sidebar = () => {
|
|||
const [instructions, setInstructions] = useState('') // the user's instructions
|
||||
|
||||
// state of chat
|
||||
const [chatMessageHistory, setChatMessageHistory] = useState<ChatMessage[]>([])
|
||||
const [messageStream, setMessageStream] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [showThreadsHistory, setShowThreadsHistory] = useState(false)
|
||||
const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false)
|
||||
|
||||
const abortFnRef = useRef<(() => void) | null>(null)
|
||||
|
||||
|
|
@ -122,13 +159,12 @@ const Sidebar = () => {
|
|||
|
||||
// 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 file if it's not a duplicate
|
||||
if (!files.find(f => f.fsPath === filepath.fsPath)) setFiles(files => [...files, 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
|
||||
|
|
@ -136,26 +172,22 @@ const Sidebar = () => {
|
|||
setApiConfig(m.apiConfig)
|
||||
}
|
||||
|
||||
// incoming thread history
|
||||
else if (m.type === 'threadHistory') {
|
||||
setPreviousThreads(m.threads)
|
||||
|
||||
// top navigation bar command - new chat
|
||||
else if (m.type === 'startNewThread') {
|
||||
setIsThreadSelectorOpen(false)
|
||||
startNewThread()
|
||||
}
|
||||
|
||||
// top navigation bar command - new chat
|
||||
else if (m.type === 'startNewChat') {
|
||||
setShowThreadsHistory(false)
|
||||
startNewChat()
|
||||
}
|
||||
|
||||
// top navigation bar command - new chat
|
||||
else if (m.type === 'showPreviousChats') {
|
||||
setShowThreadsHistory(true)
|
||||
else if (m.type === 'openThreadSelector') {
|
||||
setIsThreadSelectorOpen(true)
|
||||
}
|
||||
|
||||
}
|
||||
window.addEventListener('message', listener);
|
||||
return () => { window.removeEventListener('message', listener) }
|
||||
}, [files, selection, setPreviousThreads, startNewChat])
|
||||
}, [files, selection, startNewThread])
|
||||
|
||||
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
|
|
@ -170,15 +202,6 @@ const Sidebar = () => {
|
|||
setSelection(null)
|
||||
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
|
||||
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
|
||||
const relevantFiles = await awaitVSCodeResponse('files')
|
||||
|
|
@ -191,7 +214,7 @@ const Sidebar = () => {
|
|||
|
||||
// send message to claude
|
||||
let { abort } = sendLLMMessage({
|
||||
messages: [...thread.messages.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),
|
||||
onFinalMessage: (content) => {
|
||||
// add assistant's message to chat history, and clear selection
|
||||
|
|
@ -216,7 +239,6 @@ const Sidebar = () => {
|
|||
const llmContent = messageStream || '(canceled)'
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
|
||||
addMessageToHistory(newHistoryElt)
|
||||
setChatMessageHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
|
@ -230,14 +252,14 @@ const Sidebar = () => {
|
|||
|
||||
return <>
|
||||
<div className="flex flex-col h-screen w-full">
|
||||
{showThreadsHistory && (
|
||||
{isThreadSelectorOpen && (
|
||||
<div className="mb-2 max-h-[30vh] overflow-y-auto">
|
||||
<ThreadHistory onClose={() => setShowThreadsHistory(false)} />
|
||||
<ThreadSelector onClose={() => setIsThreadSelectorOpen(false)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-y-auto overflow-x-hidden space-y-4">
|
||||
{/* previous messages */}
|
||||
{thread.messages.map((message, i) =>
|
||||
{currentThread !== null && currentThread.messages.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
)}
|
||||
{/* message stream */}
|
||||
|
|
@ -253,62 +275,64 @@ const Sidebar = () => {
|
|||
{!selection?.selectionStr ? null
|
||||
: (
|
||||
<div className="relative">
|
||||
<button
|
||||
<div className="input">
|
||||
{/* selection */}
|
||||
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
|
||||
{/* selected files */}
|
||||
<FilesSelector files={files} setFiles={setFiles} />
|
||||
{/* 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 className="input">
|
||||
{/* selection */}
|
||||
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
|
||||
{/* selected files */}
|
||||
<FilesSelector files={files} setFiles={setFiles} />
|
||||
{/* 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
|
||||
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>}
|
||||
<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
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
import React from "react"
|
||||
import { useChat } from "../context"
|
||||
import { classNames } from "../utils"
|
||||
|
||||
const ThreadHistory = ({ onClose }: { onClose: () => void }) => {
|
||||
const { selectThread, previousThreads, thread } = useChat()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="text-right">
|
||||
<button className="btn btn-sm" onClick={onClose}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{previousThreads.map((prevThread) => (
|
||||
<button
|
||||
key={prevThread.id}
|
||||
className={classNames(
|
||||
"btn btn-sm btn-secondary",
|
||||
prevThread.id === thread.id && "btn-primary"
|
||||
)}
|
||||
onClick={() => selectThread(prevThread)}
|
||||
>
|
||||
{new Date(prevThread.createdAt).toLocaleString()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThreadHistory
|
||||
|
|
@ -1,18 +1,17 @@
|
|||
import React, {
|
||||
ReactNode,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react"
|
||||
import { ChatMessage, ChatThread } from "../shared_types"
|
||||
import { getVSCodeAPI } from "./getVscodeApi"
|
||||
import React, { ReactNode, createContext, useContext, useEffect, useState, } from "react"
|
||||
import { ChatMessage, ChatThreads } from "../shared_types"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
|
||||
|
||||
const createEmptyThread = () => ({
|
||||
id: "",
|
||||
createdAt: "",
|
||||
messages: [],
|
||||
})
|
||||
|
||||
type ChatContextValue = {
|
||||
allThreads: ChatThreads | null,
|
||||
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(),
|
||||
|
|
@ -20,69 +19,44 @@ const createNewThread = () => ({
|
|||
messages: [],
|
||||
})
|
||||
|
||||
interface IChatProviderProps {
|
||||
thread: ChatThread
|
||||
addMessageToHistory: (message: ChatMessage) => void
|
||||
setPreviousThreads: (threads: any) => void
|
||||
previousThreads: ChatThread[]
|
||||
selectThread: (thread: ChatThread) => void
|
||||
startNewChat: () => void
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
addMessageToHistory: () => {},
|
||||
setPreviousThreads: () => {},
|
||||
// placeholder for thread until first message is sent so that createdAt date is accurate
|
||||
thread: createEmptyThread(),
|
||||
previousThreads: [],
|
||||
selectThread: () => {},
|
||||
startNewChat: () => {},
|
||||
}
|
||||
|
||||
const ChatContext = createContext<IChatProviderProps>(defaults)
|
||||
|
||||
function ChatProvider({ children }: { children: ReactNode }) {
|
||||
const [previousThreads, setPreviousThreads] = useState<ChatThread[]>(
|
||||
defaults.previousThreads
|
||||
)
|
||||
const [thread, setThread] = useState<ChatThread>(defaults.thread)
|
||||
const [allThreads, setAllThreads] = useState<ChatThreads>({})
|
||||
const [currentThreadId, setCurrentThreadId] = useState<string | null>(null)
|
||||
|
||||
// this loads allThreads in on mount
|
||||
useEffect(() => {
|
||||
getVSCodeAPI().postMessage({ type: "getThreadHistory" })
|
||||
getVSCodeAPI().postMessage({ type: "getAllThreads" })
|
||||
awaitVSCodeResponse('allThreads')
|
||||
.then(response => { setAllThreads(response.threads) })
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (thread.messages.length) {
|
||||
getVSCodeAPI().postMessage({ type: "updateThread", thread })
|
||||
}
|
||||
}, [thread])
|
||||
|
||||
const addMessageToHistory = (message: ChatMessage) => {
|
||||
setThread((prev) => ({
|
||||
...prev,
|
||||
// replace placeholder thread with new thread if it's the first message
|
||||
...(!thread.id && createNewThread()),
|
||||
messages: [...prev.messages, message],
|
||||
let currentThread = !currentThreadId ? createNewThread() : allThreads[currentThreadId]
|
||||
setAllThreads((threads) => ({
|
||||
...threads,
|
||||
[currentThread.id]: {
|
||||
...currentThread,
|
||||
messages: [...currentThread.messages, message],
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const handleReceiveThreadHistory = (threads: ChatThread[]) =>
|
||||
setPreviousThreads(
|
||||
threads.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<ChatContext.Provider
|
||||
value={{
|
||||
thread,
|
||||
allThreads,
|
||||
addMessageToHistory,
|
||||
setPreviousThreads: handleReceiveThreadHistory,
|
||||
previousThreads,
|
||||
selectThread: setThread,
|
||||
startNewChat: () => setThread(createNewThread()),
|
||||
currentThread: currentThreadId !== null ? allThreads[currentThreadId] : null,
|
||||
switchToThread: (threadId: string) => { setCurrentThreadId(threadId); },
|
||||
startNewThread: () => {
|
||||
const newThread = createNewThread()
|
||||
setAllThreads(threads => ({
|
||||
...threads,
|
||||
[newThread.id]: newThread
|
||||
}))
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
@ -90,8 +64,8 @@ function ChatProvider({ children }: { children: ReactNode }) {
|
|||
)
|
||||
}
|
||||
|
||||
function useChat(): IChatProviderProps {
|
||||
const context = useContext<IChatProviderProps>(ChatContext)
|
||||
function useChat(): ChatContextValue {
|
||||
const context = useContext<ChatContextValue>(ChatContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useChat must be used within a ChatProvider")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ const awaiting: { [c in Command]: ((res: any) => void)[] } = {
|
|||
"files": [],
|
||||
"apiConfig": [],
|
||||
"getApiConfig": [],
|
||||
"getThreadHistory": [],
|
||||
"threadHistory": [],
|
||||
"updateThread": [],
|
||||
"startNewChat": [],
|
||||
"showPreviousChats": [],
|
||||
"startNewThread": [],
|
||||
"getAllThreads": [],
|
||||
"allThreads": [],
|
||||
"persistThread": [],
|
||||
"openThreadSelector": []
|
||||
}
|
||||
|
||||
// use this function to await responses
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export default function classNames(...classes: any[]) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default as classNames } from "./classNames";
|
||||
Loading…
Reference in a new issue