Merge remote-tracking branch 'origin/edit-chats' into model-selection

This commit is contained in:
Andrew Pareles 2025-02-18 16:12:11 -08:00
commit 9ab9196b38
11 changed files with 540 additions and 243 deletions

View file

@ -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<IMarkerCheckService>('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);

View file

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

@ -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[] = [

View file

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

View file

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

View file

@ -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 <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}
@ -694,7 +706,6 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
: role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
}
${role !== 'assistant' ? 'my-2' : ''}
`}
onMouseEnter={() => 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"
>

View file

@ -1,7 +1,6 @@
import { useEffect } from 'react';
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
useEffect(() => {
if (!containerRef.current) return;
@ -12,90 +11,118 @@ export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivE
'[class*="overflow-y-auto"]'
].join(',');
// 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))
];
// Function to initialize scrollbar styles for elements
const initializeScrollbarStyles = () => {
// 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]);
};

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

@ -22,7 +22,7 @@ import './chatThreadService.js'
import './autocompleteService.js'
// register Context services
import './contextGatheringService.js'
// import './contextGatheringService.js'
// import './contextUserChangesService.js'
// settings pane

View file

@ -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<T extends ToolName> = keyof typeof voidTools[T]['params']
export type ToolParamsObj<T extends ToolName> = { [paramName in ToolParamNames<T>]: 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) => {