diff --git a/src/vs/workbench/contrib/void/browser/prompt/stringifyFiles.ts b/src/vs/workbench/contrib/void/browser/prompt/stringifyFiles.ts index 7cde3999..98d566e7 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/stringifyFiles.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/stringifyFiles.ts @@ -1,55 +1,25 @@ -import { URI } from '../../../../../base/common/uri.js'; +import { CodeSelection } from '../registerThreads.js'; +export const filesStr = (selections: CodeSelection[]) => { -export type LLMCodeSelection = { selectionStr: string; filePath: URI } -export type LLMFile = { content: string, filepath: URI } - -export const filesStr = (fullFiles: LLMFile[]) => { - return fullFiles.map(({ filepath, content }) => - ` -${filepath.fsPath} + return selections.map(({ fileURI, content, selectionStr }) => + `\ +File: ${fileURI.fsPath} \`\`\` ${content} -\`\`\``).join('\n') +\`\`\`${selectionStr === null ? '' : ` +Selection: ${selectionStr}`} +`).join('\n') } -export const userInstructionsStr = (instructions: string, files: LLMFile[], selection: LLMCodeSelection | null) => { +export const userInstructionsStr = (instructions: string, selections: CodeSelection[]) => { let str = ''; - - if (files.length > 0) { - str += filesStr(files); - } - - if (selection) { - str += ` -I am currently selecting this code: -\t\`\`\`${selection.selectionStr}\`\`\` -`; - } - - if (files.length > 0 && selection) { - str += ` -Please edit the selected code or the entire file following these instructions: -`; - } else if (files.length > 0) { - str += ` -Please edit the file following these instructions: -`; - } else if (selection) { - str += ` -Please edit the selected code following these instructions: -`; - } - - str += ` -\t${instructions} -`; - if (files.length > 0) { - str += ` -\tIf you make a change, rewrite the entire file. -`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply + if (selections.length > 0) { + str += filesStr(selections); + str += `Please edit the selected code following these instructions:\n` } + str += `${instructions}`; return str; }; 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 eefa19ba..8137a1e0 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 @@ -1,12 +1,12 @@ -import React, { FormEvent, useCallback, useRef, useState } from 'react'; +import React, { FormEvent, Fragment, useCallback, useRef, useState } from 'react'; import { useConfigState, useService, useThreadsState } from '../util/services.js'; -import { URI } from '../../../../../../../base/common/uri.js'; import { VSReadFile } from '../../../registerInlineDiffs.js'; import { sendLLMMessage } from '../util/sendLLMMessage.js'; import { generateDiffInstructions } from '../../../prompt/systemPrompts.js'; -import { LLMCodeSelection, userInstructionsStr } from '../../../prompt/stringifyFiles.js'; +import { userInstructionsStr } from '../../../prompt/stringifyFiles.js'; +import { CodeSelection, CodeStagingSelection } from '../../../registerThreads.js'; import { BlockCode } from '../markdown/BlockCode.js'; import { MarkdownRender } from '../markdown/MarkdownRender.js'; @@ -17,8 +17,7 @@ export type ChatMessage = role: 'user'; content: string; // content sent to the llm displayContent: string; // content displayed to user - selection: LLMCodeSelection | null; // the user's selection - files: URI[]; // the files sent in the message + selections: CodeSelection[] | null; // the user's selection } | { role: 'assistant'; @@ -40,37 +39,51 @@ const getBasename = (pathStr: string) => { return parts[parts.length - 1] } -export const SelectedFiles = ({ files, setFiles, }: { files: URI[]; setFiles: null | ((files: URI[]) => void) }) => { +export const SelectedFiles = ({ type, selections, setStagingSelns, }: + | { type: 'past', selections: CodeSelection[]; setStagingSelns?: undefined } + | { type: 'staging', selections: CodeStagingSelection[]; setStagingSelns: ((files: CodeStagingSelection[]) => void) } +) => { return ( - files.length !== 0 && ( + selections.length !== 0 && (
- {files.map((filename, i) => ( - + + {selection.selectionStr && setStagingSelns?.([...selections.slice(0, i), { ...selection, selectionStr: null }, ...selections.slice(i + 1, Infinity)])} + className="btn btn-secondary btn-sm border border-vscode-input-border rounded" + > + Remove + + )} />} + ))}
) @@ -90,11 +103,7 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => { if (role === 'user') { chatbubbleContents = <> - - {chatMessage.selection?.selectionStr && } + {children} } @@ -133,8 +142,6 @@ export const SidebarChat = () => { // ----- SIDEBAR CHAT state (local) ----- // state of current message - const [selection, setSelection] = useState(null) // the code the user is selecting - const [files, setFiles] = useState([]) // the names of the files in the chat const [instructions, setInstructions] = useState('') // the user's instructions // state of chat @@ -146,8 +153,6 @@ export const SidebarChat = () => { - - const isDisabled = !instructions const formRef = useRef(null) @@ -160,16 +165,15 @@ export const SidebarChat = () => { setIsLoading(true) setInstructions(''); formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens - setSelection(null) - setFiles([]) + threadsStateService.setStaging([]) // clear staging setLatestError('') + const stagingSelections = threadsStateService.state._currentStagingSelections - - const relevantFiles = await Promise.all( - files.map(async (filepath) => ({ content: await VSReadFile(fileService, filepath), filepath })) + const selections = await Promise.all( + stagingSelections.map(async (sel) => ({ ...sel, content: await VSReadFile(fileService, sel.fileURI) })) ).then( - (files) => files.filter(file => file.content !== null) as {content:string, filepath:URI}[] + (files) => files.filter(file => file.content !== null) as CodeSelection[] ) // add system message to chat history @@ -177,8 +181,8 @@ export const SidebarChat = () => { threadsStateService.addMessageToCurrentThread(systemPromptElt) - const userContent = userInstructionsStr(instructions, relevantFiles, selection) - const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files } + const userContent = userInstructionsStr(instructions, selections) + const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selections } threadsStateService.addMessageToCurrentThread(newHistoryElt) const currentThread = threadsStateService.getCurrentThread(threadsStateService.state) // the the instant state right now, don't wait for the React state @@ -231,6 +235,9 @@ export const SidebarChat = () => { const currentThread = threadsStateService.getCurrentThread(threadsState) + + const selections = threadsState._currentStagingSelections ?? [] + return <>
{/* previous messages */} @@ -246,22 +253,9 @@ export const SidebarChat = () => {
- {/* selection */} - {(files.length || selection?.selectionStr) &&
- {/* selected files */} - - {/* selected code */} - {!!selection?.selectionStr && ( - setSelection(null)} - className="btn btn-secondary btn-sm border border-vscode-input-border rounded" - > - Remove - - )} /> - )} + {/* selections */} + {(selections.length || selections) &&
+
}
; fireFocusChat(): void; fireBlurChat(): void; + + openView(): void; } @@ -192,8 +196,9 @@ class VoidSidebarStateService extends Disposable implements IVoidSidebarStateSer setState(newState: Partial) { // make sure view is open if the tab changes - if ('currentTab' in newState) - this._viewsService.openView(SIDEBAR_VIEW_ID); + if ('currentTab' in newState) { + this.openView() + } this.state = { ...this.state, ...newState } this._onDidChangeState.fire() @@ -207,12 +212,18 @@ class VoidSidebarStateService extends Disposable implements IVoidSidebarStateSer this._onBlurChat.fire() } + openView() { + this._viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID); + this._viewsService.openView(SIDEBAR_VIEW_ID); + } + constructor( @IViewsService private readonly _viewsService: IViewsService, + // @IThreadHistoryService private readonly _threadHistoryService: IThreadHistoryService, ) { super() // auto open the view on mount (if it bothers you this is here, this is technically just initializing the state of the view) - this._viewsService.openView(SIDEBAR_VIEW_ID); + this.openView() // initial state this.state = { @@ -233,27 +244,41 @@ registerSingleton(IVoidSidebarStateService, VoidSidebarStateService, Instantiati // Action: when press ctrl+L, show the sidebar chat and add to the selection registerAction2(class extends Action2 { constructor() { - super({ id: 'void.ctrl+l', title: 'Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.WorkbenchContrib } }); + super({ id: 'void.ctrl+l', title: 'Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } }); } async run(accessor: ServicesAccessor): Promise { + + const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel() + if (!model) + return + + const stateService = accessor.get(IVoidSidebarStateService) stateService.setState({ isHistoryOpen: false, currentTab: 'chat' }) stateService.fireFocusChat() - // const selection = accessor.get(IEditorService).activeTextEditorControl?.getSelection() + // add selection + const threadHistoryService = accessor.get(IThreadHistoryService) + const currentStaging = threadHistoryService.state._currentStagingSelections + const currentStagingEltIdx = currentStaging?.findIndex(s => s.fileURI.fsPath === model.uri.fsPath) + // if there exists a selection with this URI, replace it + const selectionRange = accessor.get(IEditorService).activeTextEditorControl?.getSelection() - // chat state: - // // if user pressed ctrl+l, add their selection to the sidebar - // useOnVSCodeMessage('ctrl+l', (m) => { - // setSelection(m.selection) - // const filepath = m.selection.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]) - // }) + if (selectionRange) { + const selection: CodeStagingSelection = { selectionStr: model.getValueInRange(selectionRange), fileURI: model.uri } + if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) { + threadHistoryService.setStaging([ + ...currentStaging!.slice(0, currentStagingEltIdx), + selection, + ...currentStaging!.slice(currentStagingEltIdx + 1, Infinity) + ]) + } + else { + threadHistoryService.setStaging([...(currentStaging ?? []), selection]) + } + } } }); diff --git a/src/vs/workbench/contrib/void/browser/registerThreads.ts b/src/vs/workbench/contrib/void/browser/registerThreads.ts index 1e758d1f..5695aded 100644 --- a/src/vs/workbench/contrib/void/browser/registerThreads.ts +++ b/src/vs/workbench/contrib/void/browser/registerThreads.ts @@ -6,15 +6,24 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -export type CodeSelection = { selectionStr: string; filePath: URI } +// if selectionStr is null, it means just send the whole file +export type CodeSelection = { + selectionStr: string | null; + fileURI: URI, + content: string; +} + +export type CodeStagingSelection = { + selectionStr: string | null; + fileURI: URI; +} export type ChatMessage = | { role: 'user'; content: string; // content sent to the llm displayContent: string; // content displayed to user - selection: CodeSelection | null; // the user's selection - files: URI[]; // the files sent in the message + selections: CodeSelection[] | null; // the user's selection } | { role: 'assistant'; @@ -40,6 +49,7 @@ export type ChatThreads = { export type ThreadsState = { allThreads: ChatThreads; _currentThreadId: string | null; // intended for internal use only + _currentStagingSelections: CodeStagingSelection[] | null; } @@ -64,8 +74,10 @@ export interface IThreadHistoryService { getCurrentThread(state: ThreadsState): ChatThreads[string] | null; startNewThread(): void; switchToThread(threadId: string): void; - startNewThread(): void; addMessageToCurrentThread(message: ChatMessage): void; + + setStaging(stagingSelection: CodeStagingSelection[] | null): void; + } export const IThreadHistoryService = createDecorator('voidThreadHistoryService'); @@ -84,8 +96,9 @@ class ThreadHistoryService extends Disposable implements IThreadHistoryService { super() this.state = { + allThreads: this._readAllThreads(), _currentThreadId: null, - allThreads: this._readAllThreads() + _currentStagingSelections: null, } } @@ -105,7 +118,8 @@ class ThreadHistoryService extends Disposable implements IThreadHistoryService { ...this.state, ...state } - if (affectsCurrent) this._onDidChangeCurrentThread.fire() + if (affectsCurrent) + this._onDidChangeCurrentThread.fire() } // must "prove" that you have access to the current state by providing it @@ -166,6 +180,11 @@ class ThreadHistoryService extends Disposable implements IThreadHistoryService { this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it) } + + setStaging(stagingSelection: CodeStagingSelection[] | null): void { + this._setState({ _currentStagingSelections: stagingSelection }, true) // this is a hack for now + } + } registerSingleton(IThreadHistoryService, ThreadHistoryService, InstantiationType.Eager);