diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index a3452eb2..248ab441 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -13,7 +13,9 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; import { IModelService } from '../../../../editor/common/services/model.js'; -import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js'; +import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles } from './prompt/prompts.js'; +import { LLMChatMessage } from '../common/llmMessageTypes.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) export type CodeSelection = { @@ -32,23 +34,17 @@ export type FileSelection = { export type StagingSelectionItem = CodeSelection | FileSelection - -export type StagingInfo = { - isBeingEdited: boolean; - selections: StagingSelectionItem[] | null; // staging selections in edit mode -} - -const defaultStaging: StagingInfo = { isBeingEdited: false, selections: [] } - - // WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. export type ChatMessage = | { role: 'user'; - content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty) + content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user - allowed to be '', will be ignored selections: StagingSelectionItem[] | null; // the user's selection - staging: StagingInfo | null + state: { + stagingSelections: StagingSelectionItem[]; + isBeingEdited: boolean; + } } | { role: 'assistant'; @@ -61,6 +57,11 @@ export type ChatMessage = displayContent?: undefined; } +type UserMessageType = ChatMessage & { role: 'user' } +type UserMessageState = UserMessageType['state'] + +export const defaultMessageState: UserMessageState = { stagingSelections: [], isBeingEdited: false } + // a 'thread' means a chat message history export type ChatThreads = { [id: string]: { @@ -68,11 +69,18 @@ export type ChatThreads = { createdAt: string; // ISO string lastModified: string; // ISO string messages: ChatMessage[]; - staging: StagingInfo | null; - focusedMessageIdx?: number | undefined; // index of the message that is being edited (undefined if none) + state: { + stagingSelections: StagingSelectionItem[]; + focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none) + isCheckedOfSelectionId: { [selectionId: string]: boolean }; + } }; } +type ThreadType = ChatThreads[string] + +const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {} } + export type ThreadsState = { allThreads: ChatThreads; currentThreadId: string; // intended for internal use only @@ -94,11 +102,12 @@ const newThreadObject = () => { createdAt: now, lastModified: now, messages: [], - focusedMessageIdx: undefined, - staging: { - isBeingEdited: true, - selections: [], - } + state: { + stagingSelections: [], + focusedMessageIdx: undefined, + isCheckedOfSelectionId: {} + }, + } satisfies ChatThreads[string] } @@ -124,7 +133,9 @@ export interface IChatThreadService { isFocusingMessage(): boolean; setFocusedMessageIdx(messageIdx: number | undefined): void; - _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; + // _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; + _useCurrentThreadState(): readonly [ThreadType['state'], (newState: Partial) => void]; + _useCurrentMessageState(messageIdx: number): readonly [UserMessageState, (newState: Partial) => void]; editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise; addUserMessageAndStreamResponse(userMessage: string): Promise; @@ -150,6 +161,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { constructor( @IStorageService private readonly _storageService: IStorageService, @IModelService private readonly _modelService: IModelService, + @IFileService private readonly _fileService: IFileService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, ) { super() @@ -190,21 +202,19 @@ class ChatThreadService extends Disposable implements IChatThreadService { const threads: ChatThreads = oldThreadsObject /** v1 -> v2 - - threadsState.currentStagingSelections: CodeStagingSelection[] | null; - + thread.staging: StagingInfo - + thread.focusedMessageIdx?: number | undefined; - - + chatMessage.staging: StagingInfo | null - */ + - threads.state.currentStagingSelections: CodeStagingSelection[] | null; + + thread[threadIdx].state + + message.state +*/ // check if we need to update let shouldUpdate = false for (const thread of Object.values(threads)) { - if (!thread.staging) { + if (!thread.state) { shouldUpdate = true } for (const chatMessage of Object.values(thread.messages)) { - if (chatMessage.role === 'user' && !chatMessage.staging) { + if (chatMessage.role === 'user' && !chatMessage.state) { shouldUpdate = true } } @@ -214,13 +224,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { // update the threads for (const thread of Object.values(threads)) { - if (!thread.staging) { - thread.staging = defaultStaging - thread.focusedMessageIdx = undefined + if (!thread.state) { + thread.state = defaultThreadState } for (const chatMessage of Object.values(thread.messages)) { - if (chatMessage.role === 'user' && !chatMessage.staging) { - chatMessage.staging = defaultStaging + if (chatMessage.role === 'user' && !chatMessage.state) { + chatMessage.state = defaultMessageState } } } @@ -245,6 +254,17 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._onDidChangeCurrentThread.fire() } + private _getAllSelections() { + const thread = this.getCurrentThread() + return thread.messages.flatMap(m => m.role === 'user' && m.selections || []) + } + + private _getSelectionsUpToMessageIdx(messageIdx: number) { + const thread = this.getCurrentThread() + const prevMessages = thread.messages.slice(0, messageIdx) + return prevMessages.flatMap(m => m.role === 'user' && m.selections || []) + } + private _setStreamState(threadId: string, state: Partial>) { this.streamState[threadId] = { ...this.streamState[threadId], @@ -268,12 +288,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { const thread = this.getCurrentThread() - const messageToReplace = thread.messages[messageIdx] - if (messageToReplace?.role !== 'user') { - console.log(`Error: tried to edit non-user message. messageIdx=${messageIdx}, numMessages=${thread.messages.length}`) - return + if (thread.messages?.[messageIdx]?.role !== 'user') { + throw new Error("Error: editing a message with role !=='user'") } + // get prev and curr selections before clearing the message + const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx) + const currSelns = thread.messages[messageIdx].selections || [] + // clear messages up to the index const slicedMessages = thread.messages.slice(0, messageIdx) this._setState({ @@ -287,36 +309,45 @@ class ChatThreadService extends Disposable implements IChatThreadService { }, true) // stream the edit - this.addUserMessageAndStreamResponse(userMessage, messageToReplace.staging) + this.addUserMessageAndStreamResponse(userMessage, { prevSelns, currSelns }) } - async addUserMessageAndStreamResponse(userMessage: string, stagingOverride?: StagingInfo | null) { - + async addUserMessageAndStreamResponse(userMessage: string, options?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] }) { const thread = this.getCurrentThread() const threadId = thread.id - let threadStaging = thread.staging - - const currStaging = stagingOverride ?? threadStaging ?? defaultStaging // don't use _useFocusedStagingState to avoid race conditions with focusing - const { selections: currSelns, } = currStaging - // add user's message to chat history const instructions = userMessage - const content = await chat_userMessage(instructions, currSelns, this._modelService) - const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns, staging: null, } + + const prevSelns: StagingSelectionItem[] = options?.prevSelns ?? this._getAllSelections() + const currSelns: StagingSelectionItem[] = options?.currSelns ?? thread.state.stagingSelections + + // read all curr+previous files on demand instead of adding them to the history + const messageContent = await chat_userMessageContent(instructions, prevSelns, currSelns) + const messageContentWithAllFiles = await chat_userMessageContentWithAllFiles(instructions, prevSelns, currSelns, this._modelService, this._fileService) + const prevLLMMessages = this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })) + const currLLMMessage: LLMChatMessage = { role: 'user', content: messageContentWithAllFiles } + + const userHistoryElt: ChatMessage = { role: 'user', content: messageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) this._setStreamState(threadId, { error: undefined }) + console.log(`messageContent`) + console.log([{ role: 'system', content: chat_systemMessage }, + ...prevLLMMessages, + currLLMMessage,]) + const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', logging: { loggingName: 'Chat' }, useProviderFor: 'Ctrl+L', messages: [ { role: 'system', content: chat_systemMessage }, - ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), + ...prevLLMMessages, + currLLMMessage, ], onText: ({ newText, fullText }) => { this._setStreamState(threadId, { messageSoFar: fullText }) @@ -357,13 +388,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { const thread = this.getCurrentThread() // get the focusedMessageIdx - const focusedMessageIdx = thread.focusedMessageIdx + const focusedMessageIdx = thread.state.focusedMessageIdx if (focusedMessageIdx === undefined) return; // check that the message is actually being edited const focusedMessage = thread.messages[focusedMessageIdx] if (focusedMessage.role !== 'user') return; - if (!focusedMessage.staging?.isBeingEdited) return; + if (!focusedMessage.state) return; return focusedMessageIdx } @@ -429,28 +460,34 @@ class ChatThreadService extends Disposable implements IChatThreadService { ...this.state.allThreads, [threadId]: { ...thread, - focusedMessageIdx: messageIdx + state: { + ...thread.state, + focusedMessageIdx: messageIdx, + } } } }, true) } - // set thread.messages[messageIdx].stagingSelections - private setEditMessageStaging(staging: StagingInfo, messageIdx: number): void { + // set message.state + private _setCurrentMessageState(state: Partial, messageIdx: number): void { - const thread = this.getCurrentThread() - const message = thread.messages[messageIdx] - if (message.role !== 'user') return; + const threadId = this.state.currentThreadId + const thread = this.state.allThreads[threadId] + if (!thread) return this._setState({ allThreads: { ...this.state.allThreads, - [thread.id]: { + [threadId]: { ...thread, messages: thread.messages.map((m, i) => - i === messageIdx ? { + i === messageIdx && m.role === 'user' ? { ...m, - staging, + state: { + ...m.state, + ...state + }, } : m ) } @@ -459,48 +496,53 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - // set thread.stagingSelections - private setDefaultStaging(staging: StagingInfo): void { + // set thread.state + private _setCurrentThreadState(state: Partial): void { - const thread = this.getCurrentThread() + const threadId = this.state.currentThreadId + const thread = this.state.allThreads[threadId] + if (!thread) return this._setState({ allThreads: { ...this.state.allThreads, [thread.id]: { ...thread, - staging, + state: { + ...thread.state, + ...state + } } } }, true) } - // gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected) - _useFocusedStagingState(messageIdx?: number | undefined) { - const defaultStaging = { isBeingEdited: false, selections: [], text: '' } - - let staging: StagingInfo = defaultStaging - let setStaging: (selections: StagingInfo) => void = () => { } + _useCurrentMessageState(messageIdx: number) { const thread = this.getCurrentThread() - const isFocusingMessage = messageIdx !== undefined - if (isFocusingMessage) { // is editing message + const messages = thread.messages + const currMessage = messages[messageIdx] - const message = thread.messages[messageIdx!] - if (message.role === 'user') { - staging = message.staging || defaultStaging - setStaging = (s) => this.setEditMessageStaging(s, messageIdx) - } - - } - else { // is editing the default input box - staging = thread.staging || defaultStaging - setStaging = this.setDefaultStaging.bind(this) + if (currMessage.role !== 'user') { + return [defaultMessageState, (s: any) => { }] as const } - return [staging, setStaging] as const + const state = currMessage.state + const setState = (newState: Partial) => this._setCurrentMessageState(newState, messageIdx) + + return [state, setState] as const + + } + + _useCurrentThreadState() { + const thread = this.getCurrentThread() + + const state = thread.state + const setState = this._setCurrentThreadState.bind(this) + + return [state, setState] as const } diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts index b0f154d1..f7752b84 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts @@ -3,14 +3,41 @@ import { EndOfLinePreference } from '../../../../../editor/common/model' import { IModelService } from '../../../../../editor/common/services/model.js' import { IFileService } from '../../../../../platform/files/common/files' -// read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.) -export const VSReadFile = async (modelService: IModelService, uri: URI): Promise => { - const model = modelService.getModel(uri) - if (!model) return null - return model.getValue(EndOfLinePreference.LF) + +// attempts to read URI of currently opened model, then of raw file +export const VSReadFile = async (modelService: IModelService, fileService: IFileService, uri: URI) => { + + const modelResult = await _VSReadModel(modelService, uri) + if (modelResult) return modelResult + + const fileResult = await _VSReadFileRaw(fileService, uri) + if (fileResult) return fileResult + + return '' + } -export const VSReadFileRaw = async (fileService: IFileService, uri: URI) => { +// read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.) +export const _VSReadModel = async (modelService: IModelService, uri: URI): Promise => { + + // attempt to read saved model (sometimes doesn't work if page is reloaded) + const model = modelService.getModel(uri) + if (model) { + return model.getValue(EndOfLinePreference.LF) + } + + // look at all opened models and check if they have the same `fsPath` + const models = modelService.getModels(); + for (const model of models) { + if (model.uri.fsPath.toString() === uri.fsPath.toString()) { + return model.getValue(EndOfLinePreference.LF); + } + } + + return null +} + +export const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => { const res = await fileService.readFile(uri) const str = res.value.toString() return str diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index b3fb4482..5ecb924a 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -7,8 +7,9 @@ import { URI } from '../../../../../base/common/uri.js'; import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js'; -import { VSReadFile } from '../helpers/readFile.js'; +import { _VSReadModel, VSReadFile } from '../helpers/readFile.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; // this is just for ease of readability @@ -156,10 +157,10 @@ ${tripleTick[1]} } const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.' -const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService) => { +const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService, fileService: IFileService) => { if (fileSelections.length === 0) return null const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => { - const content = await VSReadFile(modelService, sel.fileURI) ?? failToReadStr + const content = await VSReadFile(modelService, fileService, sel.fileURI) ?? failToReadStr return { ...sel, content } })) return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n') @@ -167,23 +168,60 @@ const stringifyFileSelections = async (fileSelections: FileSelection[], modelSer const stringifyCodeSelections = (codeSelections: CodeSelection[]) => { return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n') } +const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => { + if (!currSelns) return '' + return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n') +} +export const chat_userMessageContent = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null) => { - -export const chat_userMessage = async (instructions: string, selections: StagingSelectionItem[] | null, modelService: IModelService) => { - const fileSelections = selections?.filter(s => s.type === 'File') as FileSelection[] - const codeSelections = selections?.filter(s => s.type === 'Selection') as CodeSelection[] - - const filesStr = await stringifyFileSelections(fileSelections, modelService) - const codeStr = stringifyCodeSelections(codeSelections) + const selnsStr = stringifySelectionNames(currSelns) let str = '' - if (filesStr) str += `FILES\n${filesStr}\n` - if (codeStr) str += `SELECTIONS\n${codeStr}\n` - str += `INSTRUCTIONS\n${instructions}` + if (selnsStr) { str += `SELECTIONS\n${selnsStr}\n` } + str += `\nINSTRUCTIONS\n${instructions}` return str; }; +export const chat_userMessageContentWithAllFilesToo = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, modelService: IModelService, fileService: IFileService) => { + + // ADD IN FILES AT TOP + const allSelections = [...currSelns || [], ...prevSelns || []] + + const codeSelections: CodeSelection[] = [] + const fileSelections: FileSelection[] = [] + const filesURIs = new Set() + + for (const selection of allSelections) { + if (selection.type === 'Selection') { + codeSelections.push(selection) + } + else if (selection.type === 'File') { + const fileSelection = selection + const path = fileSelection.fileURI.fsPath + if (!filesURIs.has(path)) { + filesURIs.add(path) + fileSelections.push(fileSelection) + } + } + } + + const filesStr = await stringifyFileSelections(fileSelections, modelService, fileService) + const selnsStr = stringifyCodeSelections(codeSelections) + + // ACTUAL MESSAGE CONTENT + const messageContent = await chat_userMessageContent(instructions, prevSelns, currSelns) + + + let str = '' + + str += 'ALL FILE CONTENTS\n' + if (filesStr) str += `${filesStr}\n` + if (selnsStr) str += `${selnsStr}\n` + if (messageContent) str += `\n${messageContent}\n` + + return str; +}; diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 86afcc33..351a399a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -92,7 +92,6 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatLocation, tok // deal with built-in tokens first (assume marked token) const t = token as MarkedToken - console.log(t.raw) if (t.type === "space") { return {t.raw} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 2aaf9dd2..52944476 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -7,7 +7,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js'; -import { ChatMessage, StagingInfo, StagingSelectionItem } from '../../../chatThreadService.js'; +import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js'; import { BlockCode } from '../markdown/BlockCode.js'; import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; @@ -156,8 +156,8 @@ interface VoidChatAreaProps { showSelections?: boolean; showProspectiveSelections?: boolean; - staging?: StagingInfo - setStaging?: (s: StagingInfo) => void + selections?: StagingSelectionItem[] + setSelections?: (s: StagingSelectionItem[]) => void // selections?: any[]; // onSelectionsChange?: (selections: any[]) => void; @@ -180,8 +180,8 @@ export const VoidChatArea: React.FC = ({ featureName, showSelections = false, showProspectiveSelections = true, - staging, - setStaging, + selections, + setSelections, }) => { return (
= ({ }} > {/* Selections section */} - {showSelections && staging && setStaging && ( + {showSelections && selections && setSelections && ( setStaging({ ...staging, selections })} + selections={selections} + setSelections={setSelections} showProspectiveSelections={showProspectiveSelections} /> )} @@ -550,9 +550,23 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') - // edit mode state - const [staging, setStaging] = chatThreadsService._useFocusedStagingState(messageIdx) - const mode: ChatBubbleMode = staging.isBeingEdited ? 'edit' : 'display' + // global state + let isBeingEdited = false + let setIsBeingEdited = (v: boolean) => { } + let stagingSelections: StagingSelectionItem[] = [] + let setStagingSelections = (s: StagingSelectionItem[]) => { } + + if (messageIdx !== undefined) { + const [_state, _setState] = chatThreadsService._useCurrentMessageState(messageIdx) + isBeingEdited = _state.isBeingEdited + setIsBeingEdited = (v) => _setState({ isBeingEdited: v }) + stagingSelections = _state.stagingSelections + setStagingSelections = (s) => { _setState({ stagingSelections: s }) } + } + + + // local state + const mode: ChatBubbleMode = isBeingEdited ? 'edit' : 'display' const [isFocused, setIsFocused] = useState(false) const [isHovered, setIsHovered] = useState(false) const [isDisabled, setIsDisabled] = useState(false) @@ -565,10 +579,8 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM const canInitialize = role === 'user' && mode === 'edit' && textAreaRefState const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current if (canInitialize && shouldInitialize) { - setStaging({ - ...staging, - selections: chatMessage.selections || [], - }) + setStagingSelections(chatMessage.selections || []) + if (textAreaFnsRef.current) textAreaFnsRef.current.setValue(chatMessage.displayContent || '') @@ -581,14 +593,14 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM }, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current]) const EditSymbol = mode === 'display' ? Pencil : X const onOpenEdit = () => { - setStaging({ ...staging, isBeingEdited: true }) + setIsBeingEdited(true) chatThreadsService.setFocusedMessageIdx(messageIdx) _justEnabledEdit.current = true } const onCloseEdit = () => { setIsFocused(false) setIsHovered(false) - setStaging({ ...staging, isBeingEdited: false }) + setIsBeingEdited(false) chatThreadsService.setFocusedMessageIdx(undefined) } @@ -614,7 +626,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM chatThreadsService.cancelStreaming(thread.id) // reset state - setStaging({ ...staging, isBeingEdited: false }) + setIsBeingEdited(false) chatThreadsService.setFocusedMessageIdx(undefined) // stream the edit @@ -649,8 +661,8 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM showSelections={true} showProspectiveSelections={false} featureName="Ctrl+L" - staging={staging} - setStaging={setStaging} + selections={stagingSelections} + setSelections={setStagingSelections} > { const currentThread = chatThreadsService.getCurrentThread() const previousMessages = currentThread?.messages ?? [] - const [staging, setStaging] = chatThreadsService._useFocusedStagingState() + + const [_state, _setState] = chatThreadsService._useCurrentThreadState() + const selections = _state.stagingSelections + const setSelections = (s: StagingSelectionItem[]) => { _setState({ stagingSelections: s }) } // stream state const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) @@ -797,11 +812,11 @@ export const SidebarChat = () => { const userMessage = textAreaRef.current?.value ?? '' await chatThreadsService.addUserMessageAndStreamResponse(userMessage) - setStaging({ ...staging, selections: [], }) // clear staging + setSelections([]) // clear staging textAreaFnsRef.current?.setValue('') textAreaRef.current?.focus() // focus input after submit - }, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, staging, setStaging]) + }, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, selections, setSelections]) const onAbort = () => { const threadId = currentThread.id @@ -887,8 +902,8 @@ export const SidebarChat = () => { isDisabled={isDisabled} showSelections={true} showProspectiveSelections={prevMessagesHTML.length === 0} - staging={staging} - setStaging={setStaging} + selections={selections} + setSelections={setSelections} onClickAnywhere={() => { textAreaRef.current?.focus() }} featureName="Ctrl+L" > diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/delete.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/delete.tsx new file mode 100644 index 00000000..07a09338 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/delete.tsx @@ -0,0 +1 @@ +A B C diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index d65c51a7..2e64c53f 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -135,9 +135,20 @@ registerAction2(class extends Action2 { const chatThreadService = accessor.get(IChatThreadService) const focusedMessageIdx = chatThreadService.getFocusedMessageIdx() - const [staging, setStaging] = chatThreadService._useFocusedStagingState(focusedMessageIdx) - const selections = staging.selections || [] - const setSelections = (s: StagingSelectionItem[]) => setStaging({ ...staging, selections: s }) + + // set the selections to the proper value + let selections: StagingSelectionItem[] = [] + let setSelections = (s: StagingSelectionItem[]) => { } + + if (focusedMessageIdx === undefined) { + const [state, setState] = chatThreadService._useCurrentThreadState() + selections = state.stagingSelections + setSelections = (s) => setState({ stagingSelections: s }) + } else { + const [state, setState] = chatThreadService._useCurrentMessageState(focusedMessageIdx) + selections = state.stagingSelections + setSelections = (s) => setState({ stagingSelections: s }) + } // if matches with existing selection, overwrite (since text may change) const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection) diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 8ffd6b9b..e96186c9 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -4,7 +4,7 @@ import { IFileService, IFileStat } from '../../../../platform/files/common/files import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js' import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js' import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js' -import { VSReadFileRaw } from '../../../../workbench/contrib/void/browser/helpers/readFile.js' +import { _VSReadFileRaw } from '../../../../workbench/contrib/void/browser/helpers/readFile.js' import { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js' import { ISearchService } from '../../../../workbench/services/search/common/search.js' @@ -140,7 +140,7 @@ export class ToolService implements IToolService { this.contextToolCallFns = { read_file: async ({ uri: uriStr }) => { const uri = validateURI(uriStr) - const fileContents = await VSReadFileRaw(fileService, uri) + const fileContents = await _VSReadFileRaw(fileService, uri) return fileContents ?? '(could not read file)' }, list_dir: async ({ uri: uriStr }) => {