diff --git a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts new file mode 100644 index 00000000..f41c0513 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts @@ -0,0 +1,119 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; +import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; +import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { CodeActionContext, CodeActionTriggerType } from '../../../../editor/common/languages.js'; + +export interface IMarkerCheckService { + readonly _serviceBrand: undefined; +} + +export const IMarkerCheckService = createDecorator('markerCheckService'); + +class MarkerCheckService extends Disposable implements IMarkerCheckService { + _serviceBrand: undefined; + + constructor( + @IMarkerService private readonly _markerService: IMarkerService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @ITextModelService private readonly _textModelService: ITextModelService, + ) { + super(); + + setInterval(async () => { + const allMarkers = this._markerService.read(); + const errors = allMarkers.filter(marker => marker.severity === MarkerSeverity.Error); + + if (errors.length > 0) { + for (const error of errors) { + + console.log(`----------------------------------------------`); + + console.log(`${error.resource.toString()}: ${error.startLineNumber} ${error.message} ${error.severity}`); // ! all errors in the file + + try { + // Get the text model for the file + const modelReference = await this._textModelService.createModelReference(error.resource); + const model = modelReference.object.textEditorModel; + + // Create a range from the marker + const range = new Range( + error.startLineNumber, + error.startColumn, + error.endLineNumber, + error.endColumn + ); + + // Get code action providers for this model + const codeActionProvider = this._languageFeaturesService.codeActionProvider; + const providers = codeActionProvider.ordered(model); + + if (providers.length > 0) { + // Request code actions from each provider + for (const provider of providers) { + const context: CodeActionContext = { + trigger: CodeActionTriggerType.Invoke, // keeping 'trigger' since it works + only: 'quickfix' // adding this to filter for quick fixes + }; + + const actions = await provider.provideCodeActions( + model, + range, + context, + CancellationToken.None + ); + + if (actions?.actions?.length) { + + const quickFixes = actions.actions.filter(action => action.isPreferred); // ! all quickFixes for the error + const quickFixesForImports = actions.actions.filter(action => action.isPreferred && action.title.includes('import')); // ! all possible imports + quickFixesForImports + + if (quickFixes.length > 0) { + console.log('Available Quick Fixes:'); + quickFixes.forEach(action => { + console.log(`- ${action.title}`); + }); + } + } + } + } + + // Dispose the model reference + modelReference.dispose(); + } catch (e) { + console.error('Error getting quick fixes:', e); + } + } + } + }, 5000); + } + + + // private _onMarkersChanged = (changedResources: readonly URI[]): void => { + // for (const resource of changedResources) { + // const markers = this._markerService.read({ resource }); + + // if (markers.length === 0) { + // console.log(`${resource.toString()}: No diagnostics`); + // continue; + // } + + // console.log(`Diagnostics for ${resource.toString()}:`); + // markers.forEach(marker => this._logMarker(marker)); + // } + // }; + + +} + +registerSingleton(IMarkerCheckService, MarkerCheckService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 244256a6..7f53a043 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -13,11 +13,23 @@ 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, chat_selectionsString } from './prompt/prompts.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from '../common/toolsService.js'; import { toLLMChatMessage } from '../common/llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; + +const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { + for (let i = arr.length - 1; i >= 0; i--) { + if (condition(arr[i])) { + return i; + } + } + return -1; +} + + // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) export type CodeSelection = { type: 'Selection'; @@ -36,13 +48,6 @@ 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: [] } - type ToolMessage = { role: 'tool'; name: T; // internal use @@ -61,16 +66,28 @@ export type ChatMessage = displayContent?: undefined; } | { 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'; content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored } | ToolMessage +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]: { @@ -78,11 +95,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 @@ -104,11 +128,12 @@ const newThreadObject = () => { createdAt: now, lastModified: now, messages: [], - focusedMessageIdx: undefined, - staging: { - isBeingEdited: true, - selections: [], - } + state: { + stagingSelections: [], + focusedMessageIdx: undefined, + isCheckedOfSelectionId: {} + }, + } satisfies ChatThreads[string] } @@ -136,7 +161,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, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise; addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise; @@ -162,6 +189,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, @IToolsService private readonly _toolsService: IToolsService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @@ -210,23 +238,21 @@ class ChatThreadService extends Disposable implements IChatThreadService { } /** 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 + + chatMessage.staging: StagingInfo | null + */ else if (oldVersion === 'v1') { const threads = oldThreadsObject as Omit // 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 } } } @@ -257,6 +283,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], @@ -277,20 +314,53 @@ class ChatThreadService extends Disposable implements IChatThreadService { - async addUserMessageAndStreamResponse({ userMessage, chatMode, stagingOverride }: { userMessage: string, chatMode: ChatMode, stagingOverride?: StagingInfo | null }) { + async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) { + + const thread = this.getCurrentThread() + + 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({ + allThreads: { + ...this.state.allThreads, + [thread.id]: { + ...thread, + messages: slicedMessages + } + } + }, true) + + // re-add the message and stream it + this.addUserMessageAndStreamResponse({ userMessage, chatMode, chatSelections: { prevSelns, currSelns } }) + + } + + + + async addUserMessageAndStreamResponse({ userMessage, chatMode, chatSelections }: { userMessage: string, chatMode: ChatMode, chatSelections?: { 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 + // selections in all past chats, then in current chat (can have many duplicates here) + const prevSelns: StagingSelectionItem[] = chatSelections?.prevSelns ?? this._getAllSelections() + const currSelns: StagingSelectionItem[] = chatSelections?.currSelns ?? thread.state.stagingSelections // 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 userMessageContent = await chat_userMessageContent(instructions, currSelns, currSelns) + const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._modelService, this._fileService) + const userMessageFullContent = chat_userMessageContentWithAllFiles(userMessageContent, selectionsStr) + + const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) this._setStreamState(threadId, { error: undefined }) @@ -314,13 +384,24 @@ class ChatThreadService extends Disposable implements IChatThreadService { let res_: () => void const awaitable = new Promise((res, rej) => { res_ = res }) + // replace last userMessage with userMessageFullContent (which contains all the files too) + const messages_ = this.getCurrentThread().messages.map(m => (toLLMChatMessage(m))) + const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user') + let messages = messages_ + if (lastUserMsgIdx !== -1) { // should never be -1 + messages = [ + ...messages.slice(0, lastUserMsgIdx), + { role: 'user', content: userMessageFullContent }, + ...messages.slice(lastUserMsgIdx + 1, Infinity)] + } + const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', useProviderFor: 'Ctrl+L', logging: { loggingName: `Agent` }, messages: [ { role: 'system', content: chat_systemMessage(this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)) }, - ...this.getCurrentThread().messages.map(m => (toLLMChatMessage(m))), + ...messages, ], tools: tools, @@ -328,9 +409,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { onText: ({ fullText }) => { this._setStreamState(threadId, { messageSoFar: fullText }) }, - onFinalMessage: async ({ fullText, toolCalls: toolCalls_ }) => { - // make sure all tool names are valid so we can cast to ToolName below - const toolCalls = toolCalls_?.filter(tool => tool.name in this._toolsService.toolFns) + onFinalMessage: async ({ fullText, toolCalls }) => { if ((toolCalls?.length ?? 0) === 0) { this._finishStreamingTextMessage(threadId, fullText) @@ -384,37 +463,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - - async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) { - - 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 - } - - // clear messages up to the index - const slicedMessages = thread.messages.slice(0, messageIdx) - this._setState({ - allThreads: { - ...this.state.allThreads, - [thread.id]: { - ...thread, - messages: slicedMessages - } - } - }, true) - - // re-add the message and stream it - this.addUserMessageAndStreamResponse({ userMessage, chatMode, stagingOverride: messageToReplace.staging }) - - } - - - - cancelStreaming(threadId: string) { const llmCancelToken = this.streamState[threadId]?.streamingToken if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) @@ -438,13 +486,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 } @@ -510,28 +558,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 ) } @@ -540,17 +594,22 @@ 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) @@ -558,30 +617,31 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // 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/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index b0a0d197..ad3eb81d 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, rewriteCode_userMessage, rewriteCode_systemMessage, defaultQuickEditFimTags, searchReplace_userMessage, searchReplace_systemMessage } from './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplace_systemMessage, searchReplace_userMessage, } from './prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -1182,12 +1182,12 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri = uri_ // generate search/replace block text - const origFileContents = await VSReadFile(this._modelService, uri) + const origFileContents = await VSReadFile(uri, this._modelService, this._fileService) if (origFileContents === null) return - // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) - this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + // // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) + // this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) const userMessageContent = searchReplace_userMessage({ originalCode: origFileContents, applyStr: applyStr }) const messages: LLMChatMessage[] = [ diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts index 4b9e05da..2b134b7f 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts @@ -8,14 +8,40 @@ 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 (uri: URI, modelService: IModelService, fileService: IFileService) => { + + 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.) +const _VSReadModel = async (modelService: IModelService, uri: URI): Promise => { + + // attempt to read saved model (doesn't work if application was reloaded...) + const model = modelService.getModel(uri) + if (model) { + return model.getValue(EndOfLinePreference.LF) + } + + // backup logic - 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 === uri.fsPath) + return model.getValue(EndOfLinePreference.LF); + } + + return null +} + +const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => { try { const res = await fileService.readFile(uri) const str = res.value.toString() diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index a86e82b1..1e9dd2a9 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -10,6 +10,7 @@ import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThrea import { VSReadFile } from '../helpers/readFile.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { os } from '../helpers/systemInfo.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; // this is just for ease of readability @@ -166,10 +167,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(sel.fileURI, modelService, fileService) ?? failToReadStr return { ...sel, content } })) return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n') @@ -177,24 +178,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_selectionsString = async (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) + + let str = '' + + str += 'ALL FILE CONTENTS\n' + if (filesStr) str += `${filesStr}\n` + if (selnsStr) str += `${selnsStr}\n` + + return str; +} + +export const chat_userMessageContentWithAllFilesToo = (userMessage: string, selectionsString: string | undefined) => { + if (userMessage) return `${userMessage}\n${selectionsString}\n` + else return userMessage +} export const rewriteCode_systemMessage = `\ @@ -256,12 +293,12 @@ For example, if the user is asking you to "make this variable a better name", ma - Make sure you give enough context in the code block to apply the changes to the correct location in the code` -export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, modelService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService }) => { +export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, modelService, fileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, fileService: IFileService }) => { // we may want to do this in batches const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null } - const file = await stringifyFileSelections([fileSelection], modelService) + const file = await stringifyFileSelections([fileSelection], modelService, fileService) return `\ ## FILE 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 f8184db1..5de08900 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 @@ -102,7 +102,6 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati // deal with built-in tokens first (assume marked token) const t = token as MarkedToken - // console.log('render:', 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 4f337a60..e9169280 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} > setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} @@ -768,7 +779,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) @@ -800,11 +814,11 @@ export const SidebarChat = () => { const userMessage = textAreaRef.current?.value ?? '' await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode: 'agent' }) - 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 @@ -891,8 +905,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/util/useScrollbarStyles.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx index 7f8ceb34..1ba69c24 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/useScrollbarStyles.tsx @@ -1,7 +1,6 @@ import { useEffect } from 'react'; export const useScrollbarStyles = (containerRef: React.MutableRefObject) => { - useEffect(() => { if (!containerRef.current) return; @@ -12,90 +11,118 @@ export const useScrollbarStyles = (containerRef: React.MutableRefObject { + // Get all matching elements within the container, including the container itself + const scrollElements = [ + ...(containerRef.current?.matches(overflowSelector) ? [containerRef.current] : []), + ...Array.from(containerRef.current?.querySelectorAll(overflowSelector) || []) + ]; - // Apply styles and listeners to each scroll element - scrollElements.forEach(element => { - // Add the scrollable class directly to the overflow element - element.classList.add('void-scrollable-element'); - - let fadeTimeout: NodeJS.Timeout | null = null; - let fadeInterval: NodeJS.Timeout | null = null; - - const fadeIn = () => { - if (fadeInterval) clearInterval(fadeInterval); - - let step = 0; - fadeInterval = setInterval(() => { - if (step <= 10) { - element.classList.remove(`show-scrollbar-${step - 1}`); - element.classList.add(`show-scrollbar-${step}`); - step++; - } else { - clearInterval(fadeInterval!); - } - }, 10); - }; - - const fadeOut = () => { - if (fadeInterval) clearInterval(fadeInterval); - - let step = 10; - fadeInterval = setInterval(() => { - if (step >= 0) { - element.classList.remove(`show-scrollbar-${step + 1}`); - element.classList.add(`show-scrollbar-${step}`); - step--; - } else { - clearInterval(fadeInterval!); - } - }, 60); - }; - - const onMouseEnter = () => { - if (fadeTimeout) clearTimeout(fadeTimeout); - if (fadeInterval) clearInterval(fadeInterval); - fadeIn(); - }; - - const onMouseLeave = () => { - if (fadeTimeout) clearTimeout(fadeTimeout); - fadeTimeout = setTimeout(() => { - fadeOut(); - }, 10); - }; - - element.addEventListener('mouseenter', onMouseEnter); - element.addEventListener('mouseleave', onMouseLeave); - - // Store cleanup function - const cleanup = () => { - element.removeEventListener('mouseenter', onMouseEnter); - element.removeEventListener('mouseleave', onMouseLeave); - if (fadeTimeout) clearTimeout(fadeTimeout); - if (fadeInterval) clearInterval(fadeInterval); - element.classList.remove('void-scrollable-element'); - // Remove any remaining show-scrollbar classes - for (let i = 0; i <= 10; i++) { - element.classList.remove(`show-scrollbar-${i}`); - } - }; - - // Store the cleanup function on the element for later use - (element as any).__scrollbarCleanup = cleanup; - }); - - return () => { - // Clean up all scroll elements + // Apply basic styling to all elements scrollElements.forEach(element => { - if ((element as any).__scrollbarCleanup) { - (element as any).__scrollbarCleanup(); + element.classList.add('void-scrollable-element'); + }); + + // Only initialize fade effects for elements that haven't been initialized yet + scrollElements.forEach(element => { + if (!(element as any).__scrollbarCleanup) { + let fadeTimeout: NodeJS.Timeout | null = null; + let fadeInterval: NodeJS.Timeout | null = null; + + const fadeIn = () => { + if (fadeInterval) clearInterval(fadeInterval); + + let step = 0; + fadeInterval = setInterval(() => { + if (step <= 10) { + element.classList.remove(`show-scrollbar-${step - 1}`); + element.classList.add(`show-scrollbar-${step}`); + step++; + } else { + clearInterval(fadeInterval!); + } + }, 10); + }; + + const fadeOut = () => { + if (fadeInterval) clearInterval(fadeInterval); + + let step = 10; + fadeInterval = setInterval(() => { + if (step >= 0) { + element.classList.remove(`show-scrollbar-${step + 1}`); + element.classList.add(`show-scrollbar-${step}`); + step--; + } else { + clearInterval(fadeInterval!); + } + }, 60); + }; + + const onMouseEnter = () => { + if (fadeTimeout) clearTimeout(fadeTimeout); + if (fadeInterval) clearInterval(fadeInterval); + fadeIn(); + }; + + const onMouseLeave = () => { + if (fadeTimeout) clearTimeout(fadeTimeout); + fadeTimeout = setTimeout(() => { + fadeOut(); + }, 10); + }; + + element.addEventListener('mouseenter', onMouseEnter); + element.addEventListener('mouseleave', onMouseLeave); + + // Store cleanup function + const cleanup = () => { + element.removeEventListener('mouseenter', onMouseEnter); + element.removeEventListener('mouseleave', onMouseLeave); + if (fadeTimeout) clearTimeout(fadeTimeout); + if (fadeInterval) clearInterval(fadeInterval); + element.classList.remove('void-scrollable-element'); + // Remove any remaining show-scrollbar classes + for (let i = 0; i <= 10; i++) { + element.classList.remove(`show-scrollbar-${i}`); + } + }; + + // Store the cleanup function on the element for later use + (element as any).__scrollbarCleanup = cleanup; } }); }; + + // Initialize for the first time + initializeScrollbarStyles(); + + // Set up mutation observer to do the same + const observer = new MutationObserver(() => { + initializeScrollbarStyles(); + }); + + // Start observing the container for child changes + observer.observe(containerRef.current, { + childList: true, + subtree: true + }); + + return () => { + observer.disconnect(); + // Your existing cleanup code... + if (containerRef.current) { + const scrollElements = [ + ...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []), + ...Array.from(containerRef.current.querySelectorAll(overflowSelector)) + ]; + scrollElements.forEach(element => { + if ((element as any).__scrollbarCleanup) { + (element as any).__scrollbarCleanup(); + } + }); + } + }; }, [containerRef]); }; diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 5af4659d..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/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 18fa1949..b179e603 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -22,7 +22,7 @@ import './chatThreadService.js' import './autocompleteService.js' // register Context services -import './contextGatheringService.js' +// import './contextGatheringService.js' // import './contextUserChangesService.js' // settings pane diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 07733c86..7620b2cd 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -1,10 +1,11 @@ import { CancellationToken } from '../../../../base/common/cancellation.js' import { URI } from '../../../../base/common/uri.js' +import { IModelService } from '../../../../editor/common/services/model.js' import { IFileService, IFileStat } from '../../../../platform/files/common/files.js' 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 { VSReadFile } 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' @@ -76,6 +77,8 @@ export const voidTools = { } satisfies { [name: string]: InternalToolInfo } export type ToolName = keyof typeof voidTools +export const toolNames = Object.keys(voidTools) as ToolName[] + export type ToolParamNames = keyof typeof voidTools[T]['params'] export type ToolParamsObj = { [paramName in ToolParamNames]: unknown } @@ -134,6 +137,7 @@ export class ToolsService implements IToolsService { constructor( @IFileService fileService: IFileService, + @IModelService modelService: IModelService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @ISearchService searchService: ISearchService, @IInstantiationService instantiationService: IInstantiationService, @@ -160,7 +164,7 @@ export class ToolsService implements IToolsService { const { uri: uriStr } = o const uri = validateURI(uriStr) - const fileContents = await VSReadFileRaw(fileService, uri) + const fileContents = await VSReadFile(uri, modelService, fileService) return fileContents ?? invalidToolParamMsg }, list_dir: async (s: string) => {