better context and file reading

This commit is contained in:
Mathew Pareles 2025-02-16 00:13:26 -08:00
parent 198a948f6c
commit 249eee341d
8 changed files with 265 additions and 132 deletions

View file

@ -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<ThreadType['state']>) => void];
_useCurrentMessageState(messageIdx: number): readonly [UserMessageState, (newState: Partial<UserMessageState>) => void];
editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise<void>;
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
@ -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<NonNullable<ThreadStreamState[string]>>) {
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<UserMessageState>, 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<ThreadType['state']>): 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<UserMessageState>) => 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
}

View file

@ -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<string | null> => {
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<string | null> => {
// 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

View file

@ -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<string>()
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;
};

View file

@ -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 <span>{t.raw}</span>

View file

@ -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<VoidChatAreaProps> = ({
featureName,
showSelections = false,
showProspectiveSelections = true,
staging,
setStaging,
selections,
setSelections,
}) => {
return (
<div
@ -199,11 +199,11 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
}}
>
{/* Selections section */}
{showSelections && staging && setStaging && (
{showSelections && selections && setSelections && (
<SelectedFiles
type='staging'
selections={staging.selections || []}
setSelections={(selections) => 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}
>
<VoidInputBox2
ref={setTextAreaRef}
@ -765,7 +777,10 @@ export const SidebarChat = () => {
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"
>

View file

@ -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)

View file

@ -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 }) => {