diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index f9c50efa..faadd1cd 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -121,11 +121,11 @@ import { normalizeNFC } from '../../base/common/normalization.js'; import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js'; import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js'; -import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js'; -import { IMetricsService } from '../../platform/void/common/metricsService.js'; -import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js'; -import { VoidMainUpdateService } from '../../platform/void/electron-main/voidUpdateMainService.js'; -import { IVoidUpdateService } from '../../platform/void/common/voidUpdateService.js'; +import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/llmMessageChannel.js'; +import { IMetricsService } from '../../workbench/contrib/void/common/metricsService.js'; +import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js'; +import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js'; +import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpdateService.js'; /** * The main VS Code application. There will only ever be one instance, * even if the user starts many instances (e.g. from the command line). diff --git a/src/vs/platform/void/browser/void.contribution.ts b/src/vs/platform/void/browser/void.contribution.ts deleted file mode 100644 index 276d6e72..00000000 --- a/src/vs/platform/void/browser/void.contribution.ts +++ /dev/null @@ -1,21 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -// ---------- common ---------- - -// llmMessage -import '../common/llmMessageService.js' - -// voidSettings -import '../common/voidSettingsService.js' - -// refreshModel -import '../common/refreshModelService.js' - -// metrics -import '../common/metricsService.js' - -// updates -import '../common/voidUpdateService.js' diff --git a/src/vs/platform/void/electron-main/llmMessage/greptile.ts b/src/vs/platform/void/electron-main/llmMessage/greptile.ts deleted file mode 100644 index f61f87af..00000000 --- a/src/vs/platform/void/electron-main/llmMessage/greptile.ts +++ /dev/null @@ -1,68 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -// // Greptile -// // https://docs.greptile.com/api-reference/query -// // https://docs.greptile.com/quickstart#sample-response-streamed - -// import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js'; - -// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, _setAborter }) => { - -// let fullText = '' - -// const thisConfig = settingsOfProvider.greptile - -// fetch('https://api.greptile.com/v2/query', { -// method: 'POST', -// headers: { -// 'Authorization': `Bearer ${thisConfig.apikey}`, -// 'X-Github-Token': `${thisConfig.githubPAT}`, -// 'Content-Type': `application/json`, -// }, -// body: JSON.stringify({ -// messages, -// stream: true, -// repositories: [thisConfig.repoinfo], -// }), -// }) -// // this is {message}\n{message}\n{message}...\n -// .then(async response => { -// const text = await response.text() -// console.log('got greptile', text) -// return JSON.parse(`[${text.trim().split('\n').join(',')}]`) -// }) -// // TODO make this actually stream, right now it just sends one message at the end -// // TODO add _setAborter() when add streaming -// .then(async responseArr => { - -// for (const response of responseArr) { -// const type: string = response['type'] -// const message = response['message'] - -// // when receive text -// if (type === 'message') { -// fullText += message -// onText({ newText: message, fullText }) -// } -// else if (type === 'sources') { -// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null } -// fullText += filepath -// onText({ newText: filepath, fullText }) -// } -// // type: 'status' with an empty 'message' means last message -// else if (type === 'status') { -// if (!message) { -// onFinalMessage({ fullText }) -// } -// } -// } - -// }) -// .catch(error => { -// onError({ error }) -// }); - -// } diff --git a/src/vs/workbench/api/browser/mainThreadInlineDiff.ts b/src/vs/workbench/api/browser/mainThreadInlineDiff.ts index 344458f9..d43fc633 100644 --- a/src/vs/workbench/api/browser/mainThreadInlineDiff.ts +++ b/src/vs/workbench/api/browser/mainThreadInlineDiff.ts @@ -14,6 +14,8 @@ import { WorkspaceEdit } from '../../../editor/common/languages.js'; // import { IHistoryService } from '../../services/history/common/history.js'; +// VOID: THIS FILE IS OUTDATED!!!!!! No longer used anywhere. + @extHostNamedCustomer(MainContext.MainThreadInlineDiff) export class MainThreadInlineDiff extends Disposable implements MainThreadInlineDiffShape { diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 6b67c6be..aa8902f3 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -11,7 +11,6 @@ import { Position } from '../../../../editor/common/core/position.js'; import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Range } from '../../../../editor/common/core/range.js'; -import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; @@ -19,6 +18,7 @@ import { IModelService } from '../../../../editor/common/services/model.js'; import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; import { isWindows } from '../../../../base/common/platform.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { ILLMMessageService } from '../common/llmMessageService.js'; // import { IContextGatheringService } from './contextGatheringService.js'; // The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts @@ -784,8 +784,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ _newlineCount: 0, } - console.log('BB') - console.log('type', predictionType) + console.log('starting autocomplete...', predictionType) // set parameters of `newAutocompletion` appropriately newAutocompletion.llmPromise = new Promise((resolve, reject) => { diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index d0ca28b2..a3452eb2 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -11,7 +11,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; +import { ILLMMessageService } from '../common/llmMessageService.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js'; @@ -33,6 +33,14 @@ export type FileSelection = { export type StagingSelectionItem = CodeSelection | FileSelection +export type StagingInfo = { + isBeingEdited: boolean; + selections: StagingSelectionItem[] | null; // staging selections in edit mode +} + +const defaultStaging: StagingInfo = { isBeingEdited: false, selections: [] } + + // WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. export type ChatMessage = | { @@ -40,6 +48,7 @@ export type ChatMessage = content: string | null; // content sent to the llm - 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 } | { role: 'assistant'; @@ -59,13 +68,14 @@ 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) }; } export type ThreadsState = { allThreads: ChatThreads; currentThreadId: string; // intended for internal use only - currentStagingSelections: StagingSelectionItem[] | null; } export type ThreadStreamState = { @@ -84,11 +94,16 @@ const newThreadObject = () => { createdAt: now, lastModified: now, messages: [], + focusedMessageIdx: undefined, + staging: { + isBeingEdited: true, + selections: [], + } } satisfies ChatThreads[string] } const THREAD_VERSION_KEY = 'void.chatThreadVersion' -const THREAD_VERSION = 'v1' +const THREAD_VERSION = 'v2' const THREAD_STORAGE_KEY = 'void.chatThreadStorage' @@ -105,8 +120,13 @@ export interface IChatThreadService { openNewThread(): void; switchToThread(threadId: string): void; - setStaging(stagingSelection: StagingSelectionItem[] | null): void; + getFocusedMessageIdx(): number | undefined; + isFocusingMessage(): boolean; + setFocusedMessageIdx(messageIdx: number | undefined): void; + _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; + + editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise; addUserMessageAndStreamResponse(userMessage: string): Promise; cancelStreaming(threadId: string): void; dismissStreamError(threadId: string): void; @@ -137,7 +157,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { this.state = { allThreads: this._readAllThreads(), currentThreadId: null as unknown as string, // gets set in startNewThread() - currentStagingSelections: null, } // always be in a thread @@ -145,14 +164,71 @@ class ChatThreadService extends Disposable implements IChatThreadService { // for now just write the version, anticipating bigger changes in the future where we'll want to access this this._storageService.store(THREAD_VERSION_KEY, THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER) + } private _readAllThreads(): ChatThreads { // PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE // CAN ADD "v0" TAG IN STORAGE AND CONVERT - const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION) - return threads ? JSON.parse(threads) : {} + + + const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION) + + const threads: ChatThreads = threadsStr ? JSON.parse(threadsStr) : {} + + this._updateThreadsToVersion(threads, THREAD_VERSION) + + return threads + } + + + private _updateThreadsToVersion(oldThreadsObject: any, toVersion: string) { + + if (toVersion === 'v2') { + + const threads: ChatThreads = oldThreadsObject + + /** v1 -> v2 + - threadsState.currentStagingSelections: CodeStagingSelection[] | null; + + thread.staging: StagingInfo + + thread.focusedMessageIdx?: number | undefined; + + + chatMessage.staging: StagingInfo | null + */ + + // check if we need to update + let shouldUpdate = false + for (const thread of Object.values(threads)) { + if (!thread.staging) { + shouldUpdate = true + } + for (const chatMessage of Object.values(thread.messages)) { + if (chatMessage.role === 'user' && !chatMessage.staging) { + shouldUpdate = true + } + } + } + + if (!shouldUpdate) return; + + // update the threads + for (const thread of Object.values(threads)) { + if (!thread.staging) { + thread.staging = defaultStaging + thread.focusedMessageIdx = undefined + } + for (const chatMessage of Object.values(thread.messages)) { + if (chatMessage.role === 'user' && !chatMessage.staging) { + chatMessage.staging = defaultStaging + } + } + } + + // push the update + this._storeAllThreads(threads) + } + } private _storeAllThreads(threads: ChatThreads) { @@ -187,18 +263,51 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error }) } - async addUserMessageAndStreamResponse(userMessage: string) { - const threadId = this.getCurrentThread().id - const currSelns = this.state.currentStagingSelections ?? [] + async editUserMessageAndStreamResponse(userMessage: string, 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) + + // stream the edit + this.addUserMessageAndStreamResponse(userMessage, messageToReplace.staging) + + } + + async addUserMessageAndStreamResponse(userMessage: string, stagingOverride?: StagingInfo | null) { + + + const thread = this.getCurrentThread() + const threadId = thread.id + + let threadStaging = thread.staging + + const currStaging = stagingOverride ?? threadStaging ?? defaultStaging // don't use _useFocusedStagingState to avoid race conditions with focusing + const { selections: currSelns, } = currStaging // add user's message to chat history const instructions = userMessage const content = await chat_userMessage(instructions, currSelns, this._modelService) - const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns } + const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns, staging: null, } this._addMessageToThread(threadId, userHistoryElt) - this._setStreamState(threadId, { error: undefined }) const llmCancelToken = this._llmMessageService.sendLLMMessage({ @@ -241,12 +350,29 @@ class ChatThreadService extends Disposable implements IChatThreadService { getCurrentThread(): ChatThreads[string] { const state = this.state - return state.allThreads[state.currentThreadId]; + return state.allThreads[state.currentThreadId] + } + + getFocusedMessageIdx() { + const thread = this.getCurrentThread() + + // get the focusedMessageIdx + const focusedMessageIdx = thread.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; + + return focusedMessageIdx + } + + isFocusingMessage() { + return this.getFocusedMessageIdx() !== undefined } switchToThread(threadId: string) { - // console.log('threadId', threadId) - // console.log('messages', this.state.allThreads[threadId].messages) this._setState({ currentThreadId: threadId }, true) } @@ -291,11 +417,93 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it) } + // sets the currently selected message (must be undefined if no message is selected) + setFocusedMessageIdx(messageIdx: number | undefined) { - setStaging(stagingSelection: StagingSelectionItem[] | null): void { - this._setState({ currentStagingSelections: stagingSelection }, true) // this is a hack for now + const threadId = this.state.currentThreadId + const thread = this.state.allThreads[threadId] + if (!thread) return + + this._setState({ + allThreads: { + ...this.state.allThreads, + [threadId]: { + ...thread, + focusedMessageIdx: messageIdx + } + } + }, true) } + // set thread.messages[messageIdx].stagingSelections + private setEditMessageStaging(staging: StagingInfo, messageIdx: number): void { + + const thread = this.getCurrentThread() + const message = thread.messages[messageIdx] + if (message.role !== 'user') return; + + this._setState({ + allThreads: { + ...this.state.allThreads, + [thread.id]: { + ...thread, + messages: thread.messages.map((m, i) => + i === messageIdx ? { + ...m, + staging, + } : m + ) + } + } + }, true) + + } + + // set thread.stagingSelections + private setDefaultStaging(staging: StagingInfo): void { + + const thread = this.getCurrentThread() + + this._setState({ + allThreads: { + ...this.state.allThreads, + [thread.id]: { + ...thread, + staging, + } + } + }, true) + + } + + // gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected) + _useFocusedStagingState(messageIdx?: number | undefined) { + + const defaultStaging = { isBeingEdited: false, selections: [], text: '' } + + let staging: StagingInfo = defaultStaging + let setStaging: (selections: StagingInfo) => void = () => { } + + const thread = this.getCurrentThread() + const isFocusingMessage = messageIdx !== undefined + if (isFocusingMessage) { // is editing message + + 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) + } + + return [staging, setStaging] as const + } + + } registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index 768f4cfc..1cb53e5c 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -62,7 +62,7 @@ class SurroundingsRemover { if (index === -1) { this.i = this.j + 1 - return false + return null } // console.log('index', index, until.length) diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts index 60e5dc5c..b0f154d1 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts @@ -1,10 +1,17 @@ import { URI } from '../../../../../base/common/uri' 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 +// 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) } + +export const VSReadFileRaw = async (fileService: IFileService, uri: URI) => { + const res = await fileService.readFile(uri) + const str = res.value.toString() + return str +} diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index d4d7d065..ed435a26 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -25,15 +25,12 @@ 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, fastApply_userMessage, fastApply_systemMessage, defaultFimTags } from './prompt/prompts.js'; -import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, fastApply_rewritewholething_userMessage, fastApply_rewritewholething_systemMessage, defaultQuickEditFimTags, tripleTick } from './prompt/prompts.js'; import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; -import { errorDetails, LLMChatMessage } from '../../../../platform/void/common/llmMessageTypes.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; import { extractCodeFromFIM, extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; -import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; import { filenameToVscodeLanguage } from './helpers/detectLanguage.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { isMacintosh } from '../../../../base/common/platform.js'; @@ -41,6 +38,10 @@ import { EditorOption } from '../../../../editor/common/config/editorOptions.js' import { Emitter } from '../../../../base/common/event.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ILLMMessageService } from '../common/llmMessageService.js'; +import { LLMChatMessage, _InternalLLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; +import { IMetricsService } from '../common/metricsService.js'; +import { VSReadFile } from './helpers/readFile.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -100,19 +101,19 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number return paddingLeft; }; -// similar to ServiceLLM + + export type StartApplyingOpts = { from: 'QuickEdit'; + type: 'rewrite'; diffareaid: number; // id of the CtrlK area (contains text selection) } | { - from: 'Chat'; + from: 'ClickApply'; + type: 'searchReplace' | 'rewrite'; applyStr: string; -} | { - from: 'Autocomplete'; - range: IRange; - userMessage: string; } + export type AddCtrlKOpts = { startLine: number, endLine: number, @@ -137,6 +138,11 @@ export type Diff = { +type ExtractedCodeBlock = { + state: 'writingOriginal' | 'writingFinal' | 'done', + orig: string, + final: string, +} // _ means anything we don't include if we clone it // DiffArea.originalStartLine is the line in originalCode (not the file) @@ -996,7 +1002,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // @throttle(100) - private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latest: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }) { + private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latestMutable: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }) { // ----------- 1. Write the new code to the document ----------- // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out @@ -1035,39 +1041,39 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // at the start, add a newline between the stream and originalCode to make reasoning easier - if (!latest.addedSplitYet) { + if (!latestMutable.addedSplitYet) { this._writeText(uri, '\n', - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col, }, + { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col, }, { shouldRealignDiffAreas: true } ) - latest.addedSplitYet = true + latestMutable.addedSplitYet = true } // insert deltaText at latest line and col this._writeText(uri, deltaText, - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col }, + { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) - latest.line += deltaText.split('\n').length - 1 + latestMutable.line += deltaText.split('\n').length - 1 const lastNewlineIdx = deltaText.lastIndexOf('\n') - latest.col = lastNewlineIdx === -1 ? latest.col + deltaText.length : deltaText.length - lastNewlineIdx + latestMutable.col = lastNewlineIdx === -1 ? latestMutable.col + deltaText.length : deltaText.length - lastNewlineIdx // delete or insert to get original up to speed - if (latest.originalCodeStartLine < originalCodeStartLine) { + if (latestMutable.originalCodeStartLine < originalCodeStartLine) { // moved up, delete - const numLinesDeleted = originalCodeStartLine - latest.originalCodeStartLine + const numLinesDeleted = originalCodeStartLine - latestMutable.originalCodeStartLine this._writeText(uri, '', - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, + { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, { shouldRealignDiffAreas: true } ) } - else if (latest.originalCodeStartLine > originalCodeStartLine) { - this._writeText(uri, '\n' + diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), (latest.originalCodeStartLine - 1) - 1 + 1).join('\n'), - { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col }, + else if (latestMutable.originalCodeStartLine > originalCodeStartLine) { + this._writeText(uri, '\n' + diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n'), + { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) } - latest.originalCodeStartLine = originalCodeStartLine + latestMutable.originalCodeStartLine = originalCodeStartLine // add diffZone.startLine to convert to right coordinate system (line in file, not in diffarea) diffZone._streamState.line = (diffZone.startLine - 1) + newCodeEndLine @@ -1078,56 +1084,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { - // // if streaming, use diffs to figure out where to write new code - // // these are two different coordinate systems - new and old line number - // let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted - // let originalCodeStartLine: number // get original[oldStartingPoint...] - - // const lastDiff = computedDiffs.pop() - - // if (!lastDiff) { - // // if the writing is identical so far, display no changes - // newFileEndLine = diffZone.startLine - // originalCodeStartLine = 1 - // } - // else { - // if (lastDiff.type === 'insertion') { - // newFileEndLine = lastDiff.endLine - // originalCodeStartLine = lastDiff.originalStartLine - // } - // else if (lastDiff.type === 'deletion') { - // newFileEndLine = lastDiff.startLine - // originalCodeStartLine = lastDiff.originalStartLine - // } - // else if (lastDiff.type === 'edit') { - // newFileEndLine = lastDiff.endLine - // originalCodeStartLine = lastDiff.originalStartLine - // } - // else { - // throw new Error(`Void: diff.type not recognized on: ${lastDiff}`) - // } - // } - - // diffZone._streamState.line = newFileEndLine - - // // lines are 1-indexed - // const newFileTop = llmText.split('\n').slice(diffZone.startLine, (newFileEndLine - 1)).join('\n') - // const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), Infinity).join('\n') - - // const newCode = `${newFileTop}\n${oldFileBottom}` - - // this._writeText(uri, newCode, - // { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, // 1-indexed - // { shouldRealignDiffAreas: true } - // ) - - - // return computedDiffs - - - - - // called first, then call startApplying public addCtrlKZone({ startLine, endLine, editor }: AddCtrlKOpts) { @@ -1185,8 +1141,19 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { public startApplying(opts: StartApplyingOpts) { - const addedDiffZone = this._initializeStartApplying(opts) - return addedDiffZone?.diffareaid + + if (opts.type === 'rewrite') { + const addedDiffZone = this._initializeRewriteStream(opts) + return addedDiffZone?.diffareaid + } + + else if (opts.type === 'searchReplace') { + this._initializeSearchAndReplaceStream(opts) + return undefined + } + + else return undefined + } @@ -1207,7 +1174,243 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } - private _initializeStartApplying(opts: StartApplyingOpts): DiffZone | undefined { + private async _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { + const ORIGINAL = `<<<<<<< ORIGINAL` + const DIVIDER = `=======` + const FINAL = `>>>>>>> UPDATED` + + const searchReplaceSysMessage = `\ +You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file. + +A SEARCH/REPLACE block describes the code before and after a change. Here is the format: +${ORIGINAL} +// ... original code goes here +${DIVIDER} +// ... final code goes here +${FINAL} + +You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make. +Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks. + +Directions: +1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. +2. The "original" code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. +3. The "original" code in each SEARCH/REPLACE block should include enough text to uniquely identify the change in the file. +4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. + - Make sure you add all necessary imports. + - Make sure the "final" code is complete and will not result in syntax/lint errors. +5. Follow coding convention (spaces, semilcolons, comments, etc). + +## EXAMPLE 1 +ORIGINAL_FILE +${tripleTick[0]} +let w = 5 +let x = 6 +let y = 7 +let z = 8 +${tripleTick[1]} + +CHANGE +Make x equal to 6.5, not 6. +${tripleTick[0]} +// ... existing code +let x = 6.5 +// ... existing code +${tripleTick[1]} + + +## ACCEPTED OUTPUT +${tripleTick[0]} +${ORIGINAL} +let x = 6 +${DIVIDER} +let x = 6.5 +${FINAL} +${tripleTick[1]} +` + + const uri_ = this._getActiveEditorURI() + if (!uri_) return + const uri = uri_ + + // generate search/replace block text + const fileContents = await VSReadFile(this._modelService, uri) + if (fileContents === null) return + + + const searchReplaceUserMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\ +ORIGINAL_FILE +${originalCode} + +CHANGE +${applyStr} + +INSTRUCTIONS +Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation. +` + + const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => { + // for each prefix + for (let i = anyPrefix.length; i >= 0; i--) { + const prefix = anyPrefix.slice(0, i) + if (str.endsWith(prefix)) return prefix + } + return null + } + + const extractBlocks = (str: string) => { + + const ORIGINAL_ = ORIGINAL + `\n` + const DIVIDER_ = '\n' + DIVIDER + `\n` + const FINAL_ = '\n' + FINAL + + + const blocks: ExtractedCodeBlock[] = [] + + let i = 0 // search i and beyond (this is done by plain index, not by line number. much simpler this way) + while (true) { + let origStart = str.indexOf(ORIGINAL_, i) + if (origStart === -1) { return blocks } + origStart += ORIGINAL_.length + i = origStart + // wrote <<<< ORIGINAL + + let dividerStart = str.indexOf(DIVIDER_, i) + if (dividerStart === -1) { // if didnt find DIVIDER_, either writing originalStr or DIVIDER_ right now + const isWritingDIVIDER = endsWithAnyPrefixOf(str, DIVIDER_) + blocks.push({ + orig: str.substring(origStart, str.length - (isWritingDIVIDER?.length ?? 0)), + final: '', + state: 'writingOriginal' + }) + return blocks + } + const origStrDone = str.substring(origStart, dividerStart) + dividerStart += DIVIDER_.length + i = dividerStart + // wrote ===== + + let finalStart = str.indexOf(FINAL_, i) + if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL_ right now + const isWritingFINAL = endsWithAnyPrefixOf(str, FINAL_) + blocks.push({ + orig: origStrDone, + final: str.substring(origStart, str.length - (isWritingFINAL?.length ?? 0)), + state: 'writingFinal' + }) + return blocks + } + const finalStrDone = str.substring(dividerStart, finalStart) + finalStart += FINAL_.length + i = finalStart + // wrote >>>>> FINAL + + blocks.push({ + orig: origStrDone, + final: finalStrDone, + state: 'done' + }) + } + } + + + // 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 = searchReplaceUserMessage({ originalCode: fileContents, applyStr: applyStr }) + const messages: LLMChatMessage[] = [ + { role: 'system', content: searchReplaceSysMessage }, + { role: 'user', content: userMessageContent } + ] + let streamRequestIdRef: { current: string | null } = { current: null } + + const diffareaidOfBlockNum: number[] = [] + + const onText = ({ newText, fullText }: { newText: string, fullText: string }) => { + const blocks = extractBlocks(fullText) + + // find block.orig in fileContents and return its range in file + const findTextInCode = (text: string, fileContents: string) => { + const idx = fileContents.indexOf(text) + if (idx === -1) return 'Not found' as const + const lastIdx = fileContents.lastIndexOf(text) + if (lastIdx !== idx) return 'Not unique' as const + const startLine = fileContents.substring(0, idx).split('\n').length + const numLines = text.split('\n').length + const endLine = startLine + numLines - 1 + return [startLine, endLine] + } + + let latestStreamInfoMutable: any = {} + + for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { + const block = blocks[blockNum] + if (block.state === 'writingOriginal') continue + + const foundInCode = findTextInCode(block.orig, fileContents) + if (typeof foundInCode === 'string') { + console.log('ERROR!!!!', foundInCode) + continue + } + + const [startLine, endLine] = foundInCode + + // if should add new diffarea + if (blockNum > diffareaidOfBlockNum.length) { + const adding: Omit = { + type: 'DiffZone', + originalCode: block.orig, + startLine, + endLine, + _URI: uri, + _streamState: { + isStreaming: true, + streamRequestIdRef, + line: startLine, + }, + _diffOfId: {}, // added later + _removeStylesFns: new Set(), + } + const diffZone = this._addDiffArea(adding) + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) + + diffareaidOfBlockNum.push(diffZone.diffareaid) + + latestStreamInfoMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + } + + const diffareaid = diffareaidOfBlockNum[blockNum] + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone.type !== 'DiffZone') continue + + this._writeStreamedDiffZoneLLMText(diffZone, fullText, newText, latestStreamInfoMutable) + this._refreshStylesAndDiffsInURI(uri) + } + + } + + + + + // TODO turn this into a service and provide it + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + useProviderFor: 'FastApply', + logging: { loggingName: `generateSearchAndReplace` }, + messages, + onText: ({ newText, fullText }) => { onText({ newText, fullText }) }, + onFinalMessage: ({ fullText }) => { }, + onError: (e) => { console.log('ERROR', e) }, + + }) + + } + + + + + private _initializeRewriteStream(opts: StartApplyingOpts): DiffZone | undefined { const { from } = opts @@ -1215,7 +1418,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { let endLine: number let uri: URI - if (from === 'Chat') { + if (from === 'ClickApply') { const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1257,8 +1460,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const { onFinishEdit } = this._addToHistory(uri) // __TODO__ let users customize modelFimTags - const isOllamaFIM = false // this._voidSettingsService.state.modelSelectionOfFeature['Ctrl+K']?.providerName === 'ollama' - const modelFimTags = defaultFimTags + const quickEditFIMTags = defaultQuickEditFimTags const adding: Omit = { type: 'DiffZone', @@ -1289,10 +1491,10 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // now handle messages let messages: LLMChatMessage[] - if (from === 'Chat') { - const userContent = fastApply_userMessage({ originalCode, applyStr: opts.applyStr, uri }) + if (from === 'ClickApply') { + const userContent = fastApply_rewritewholething_userMessage({ originalCode, applyStr: opts.applyStr, uri }) messages = [ - { role: 'system', content: fastApply_systemMessage, }, + { role: 'system', content: fastApply_rewritewholething_systemMessage, }, { role: 'user', content: userContent, } ] } @@ -1303,25 +1505,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const { _mountInfo } = ctrlKZone const instructions = _mountInfo?.textAreaRef.current?.value ?? '' - // __TODO__ use Ollama's FIM api, if (isOllamaFIM) {...} else: const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) - // if (isOllamaFIM) { - // messages = { - // type: 'ollamaFIM', - // prefix, - // suffix, - // } - - // } - // else { const language = filenameToVscodeLanguage(uri.fsPath) ?? '' - const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language }) + const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language }) // type: 'messages', messages = [ - { role: 'system', content: ctrlKStream_systemMessage({ fimTags: modelFimTags }), }, + { role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), }, { role: 'user', content: userContent, } ] - // } } else { throw new Error(`featureName ${from} is invalid`) } @@ -1351,16 +1542,15 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const extractText = (fullText: string, recentlyAddedTextLen: number) => { if (from === 'QuickEdit') { - if (isOllamaFIM) return fullText - return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag }) + return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: quickEditFIMTags.midTag }) } - else if (from === 'Chat') { + else if (from === 'ClickApply') { return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) } throw 1 } - const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + const latestStreamInfoMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } // state used in onText: let fullText = '' @@ -1368,7 +1558,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', - useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K', + useProviderFor: opts.from === 'ClickApply' ? 'FastApply' : 'Ctrl+K', logging: { loggingName: `startApplying - ${from}` }, messages, onText: ({ newText: newText_ }) => { @@ -1377,7 +1567,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { fullText += prevIgnoredSuffix + newText const [text, deltaText, ignoredSuffix] = extractText(fullText, newText.length) - this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo) + this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfoMutable) this._refreshStylesAndDiffsInURI(uri) prevIgnoredSuffix = ignoredSuffix diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 7dc23d53..428625fd 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -10,6 +10,10 @@ import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThrea import { VSReadFile } from '../helpers/readFile.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; + +// this is just for ease of readability +export const tripleTick = ['```', '```'] + export const chat_systemMessage = `\ You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`. @@ -29,7 +33,7 @@ Do not tell the user anything about the examples below. ## EXAMPLE 1 FILES math.ts -\`\`\`typescript +${tripleTick[0]}typescript const addNumbers = (a, b) => a + b const multiplyNumbers = (a, b) => a * b const subtractNumbers = (a, b) => a - b @@ -58,21 +62,21 @@ const normalized = (vector: number[]) => { const v2 = [...vector] // clone vector return normalize(v2) } -\`\`\` +${tripleTick[1]} SELECTIONS math.ts (lines 3:3) -\`\`\`typescript +${tripleTick[0]}typescript const subtractNumbers = (a, b) => a - b -\`\`\` +${tripleTick[1]} INSTRUCTIONS add a function that exponentiates a number below this, and use it to make a power function that raises all entries of a vector to a power -ACCEPTED OUTPUT +## ACCEPTED OUTPUT We can add the following code to the file: -\`\`\`typescript +${tripleTick[0]}typescript // existing code... const subtractNumbers = (a, b) => a - b const exponentiateNumbers = (a, b) => Math.pow(a, b) @@ -84,13 +88,13 @@ const raiseAll = (vector: number[], power: number) => { vector[i] = exponentiateNumbers(vector[i], power) return vector } -\`\`\` +${tripleTick[1]} ## EXAMPLE 2 FILES fib.ts -\`\`\`typescript +${tripleTick[0]}typescript const dfs = (root) => { if (!root) return; @@ -102,20 +106,20 @@ const fib = (n) => { if (n < 1) return 1 return fib(n - 1) + fib(n - 2) } -\`\`\` +${tripleTick[1]} SELECTIONS fib.ts (lines 10:10) -\`\`\`typescript +${tripleTick[0]}typescript return fib(n - 1) + fib(n - 2) -\`\`\` +${tripleTick[1]} INSTRUCTIONS memoize results -ACCEPTED OUTPUT +## ACCEPTED OUTPUT To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function: -\`\`\`typescript +${tripleTick[0]}typescript // existing code... const fib = (n, memo = {}) => { if (n < 1) return 1; @@ -123,7 +127,7 @@ const fib = (n, memo = {}) => { memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo return memo[n]; } -\`\`\` +${tripleTick[1]} Explanation: Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n. Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result. @@ -133,21 +137,21 @@ Store Result: After computing fib(n), the result is stored in memo for future re ` -type FileSelnLocal = FileSelection & { content: string } -const stringifyFileSelection = ({ fileURI, selectionStr, range, content }: FileSelnLocal) => { +type FileSelnLocal = { fileURI: URI, content: string } +const stringifyFileSelection = ({ fileURI, content }: FileSelnLocal) => { return `\ ${fileURI.fsPath} -\`\`\`${filenameToVscodeLanguage(fileURI.fsPath) ?? ''} +${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''} ${content} -\`\`\` +${tripleTick[1]} ` } const stringifyCodeSelection = ({ fileURI, selectionStr, range }: CodeSelection) => { return `\ ${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber}) -\`\`\`${filenameToVscodeLanguage(fileURI.fsPath) ?? ''} +${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''} ${selectionStr} -\`\`\` +${tripleTick[1]} ` } @@ -183,7 +187,7 @@ export const chat_userMessage = async (instructions: string, selections: Staging -export const fastApply_systemMessage = `\ +export const fastApply_rewritewholething_systemMessage = `\ You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`. Directions: @@ -195,20 +199,20 @@ Directions: -export const fastApply_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { +export const fastApply_rewritewholething_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { const language = filenameToVscodeLanguage(uri.fsPath) ?? '' return `\ ORIGINAL_FILE -\`\`\`${language} +${tripleTick[0]}${language} ${originalCode} -\`\`\` +${tripleTick[1]} CHANGE -\`\`\` +${tripleTick[0]} ${applyStr} -\`\`\` +${tripleTick[1]} INSTRUCTIONS Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation. @@ -220,6 +224,12 @@ Please finish writing the new file by applying the change to the original file. + + + + + + export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => { const fullFileLines = fullFileStr.split('\n') @@ -269,19 +279,19 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF } -export type FimTagsType = { +export type QuickEditFimTagsType = { preTag: string, sufTag: string, midTag: string } -export const defaultFimTags: FimTagsType = { +export const defaultQuickEditFimTags: QuickEditFimTagsType = { preTag: 'ABOVE', sufTag: 'BELOW', midTag: 'SELECTION', } // this should probably be longer -export const ctrlKStream_systemMessage = ({ fimTags: { preTag, midTag, sufTag } }: { fimTags: FimTagsType }) => { +export const ctrlKStream_systemMessage = ({ quickEditFIMTags: { preTag, midTag, sufTag } }: { quickEditFIMTags: QuickEditFimTagsType }) => { return `\ You are a FIM (fill-in-the-middle) coding assistant. Your task is to fill in the middle SELECTION marked by <${midTag}> tags. @@ -297,7 +307,7 @@ Instructions: } export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: { - selection: string, prefix: string, suffix: string, instructions: string, fimTags: FimTagsType, language: string, + selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string, isOllamaFIM: false, // we require this be false for clarity }) => { const { preTag, sufTag, midTag } = fimTags @@ -309,9 +319,9 @@ export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instruction return `\ CURRENT SELECTION -\`\`\`${language} +${tripleTick[0]}${language} <${midTag}>${selection} -\`\`\` +${tripleTick[1]} INSTRUCTIONS ${instructions} @@ -319,8 +329,8 @@ ${instructions} <${preTag}>${prefix} <${sufTag}>${suffix} -Return only the completion block of code (of the form \`\`\`${language} +Return only the completion block of code (of the form ${tripleTick[0]}${language} <${midTag}>...new code -\`\`\`).` +${tripleTick[1]}).` }; diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index 1498c8f6..1a6e0deb 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -7,12 +7,12 @@ import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IInlineDiffsService } from './inlineDiffsService.js'; import { roundRangeToLines } from './sidebarActions.js'; import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js'; import { localize2 } from '../../../../nls.js'; +import { IMetricsService } from '../common/metricsService.js'; export type QuickEditPropsType = { 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 28cd2539..7b03f068 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 @@ -6,7 +6,8 @@ import React, { JSX, useCallback, useEffect, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode } from './BlockCode.js' -import { useAccessor } from '../util/services.js' +import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js' +import { ChatMessageLocation, } from '../../../searchAndReplaceService.js' import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' @@ -18,7 +19,17 @@ enum CopyButtonState { const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' -const CodeButtonsOnHover = ({ text }: { text: string }) => { + + +type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: number } + +const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { + return `${threadId}-${messageIdx}-${tokenIdx}` +} + + + +const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, applyBoxId: string }) => { const accessor = useAccessor() const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy) @@ -36,22 +47,24 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => { }, [copyButtonState]) const onCopy = useCallback(() => { - clipboardService.writeText(text) + clipboardService.writeText(applyStr) .then(() => { setCopyButtonState(CopyButtonState.Copied) }) .catch(() => { setCopyButtonState(CopyButtonState.Error) }) - metricsService.capture('Copy Code', { length: text.length }) // capture the length only + metricsService.capture('Copy Code', { length: applyStr.length }) // capture the length only - }, [metricsService, clipboardService, text]) + }, [metricsService, clipboardService, applyStr]) const onApply = useCallback(() => { - inlineDiffService.startApplying({ - from: 'Chat', - applyStr: text, - }) - metricsService.capture('Apply Code', { length: text.length }) // capture the length only - }, [metricsService, inlineDiffService, text]) - const isSingleLine = !text.includes('\n') + inlineDiffService.startApplying({ + from: 'ClickApply', + type: 'searchReplace', + applyStr, + }) + metricsService.capture('Apply Code', { length: applyStr.length }) // capture the length only + }, [metricsService, inlineDiffService, applyStr]) + + const isSingleLine = !applyStr.includes('\n') return <>