mirror of
https://github.com/voideditor/void
synced 2026-05-23 01:18:25 +00:00
Merge branch 'voideditor:main' into remote-ssh-extension
This commit is contained in:
commit
4950de2cb5
43 changed files with 1285 additions and 473 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -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 })
|
||||
// });
|
||||
|
||||
// }
|
||||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class SurroundingsRemover {
|
|||
|
||||
if (index === -1) {
|
||||
this.i = this.j + 1
|
||||
return false
|
||||
return null
|
||||
}
|
||||
// console.log('index', index, until.length)
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string | null> => {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<DiffZone, 'diffareaid'> = {
|
||||
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<DiffZone, 'diffareaid'> = {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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}</${midTag}>
|
||||
\`\`\`
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
${instructions}
|
||||
|
|
@ -319,8 +329,8 @@ ${instructions}
|
|||
<${preTag}>${prefix}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
|
||||
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</${midTag}>
|
||||
\`\`\`).`
|
||||
${tripleTick[1]}).`
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 <>
|
||||
<button
|
||||
|
|
@ -84,20 +97,30 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c
|
|||
</code>
|
||||
}
|
||||
|
||||
const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token | string, nested?: boolean, noSpace?: boolean }): JSX.Element => {
|
||||
const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocation: chatLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: number }): JSX.Element => {
|
||||
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
const t = token as MarkedToken
|
||||
console.log(t.raw)
|
||||
|
||||
if (t.type === "space") {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
const isCodeblockClosed = t.raw?.startsWith('```') && t.raw?.endsWith('```');
|
||||
|
||||
const applyBoxId = getApplyBoxId({
|
||||
threadId: chatLocation!.threadId,
|
||||
messageIdx: chatLocation!.messageIdx,
|
||||
tokenIdx: tokenIdx,
|
||||
})
|
||||
|
||||
return <BlockCode
|
||||
initValue={t.text}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]} // use vscode to detect language
|
||||
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
|
||||
buttonsOnHover={<ApplyButtonsOnHover applyStr={t.text} applyBoxId={applyBoxId} />}
|
||||
/>
|
||||
}
|
||||
|
||||
|
|
@ -183,21 +206,21 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token
|
|||
if (t.type === "paragraph") {
|
||||
const contents = <>
|
||||
{t.tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} />
|
||||
<RenderToken key={index} token={token} tokenIdx={index} /> // assign a unique tokenId to nested components
|
||||
))}
|
||||
</>
|
||||
if (nested)
|
||||
return contents
|
||||
return <p className={`${noSpace ? '' : 'my-4'} leading`}>{contents}</p>
|
||||
if (nested) return contents
|
||||
|
||||
return <p className={`${noSpace ? '' : 'my-4'}`}>
|
||||
{contents}
|
||||
</p>
|
||||
}
|
||||
|
||||
if (t.type === "html") {
|
||||
return (
|
||||
<pre className={`bg-4oid-bg-2 p-4 rounded-lg ${noSpace ? '' : 'my-4'} font-mono text-sm`}>
|
||||
{`<html>`}
|
||||
<p className={`${noSpace ? '' : 'my-4'}`}>
|
||||
{t.raw}
|
||||
{`</html>`}
|
||||
</pre>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -266,12 +289,12 @@ const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token
|
|||
)
|
||||
}
|
||||
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace }: { string: string, nested?: boolean, noSpace?: boolean }) => {
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation }) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
{tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} />
|
||||
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatMessageLocation={chatMessageLocation} tokenIdx={index} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
|
|||
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { useRefState } from '../util/helpers.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
import { isFeatureNameDisabled } from '../../../../../../../platform/void/common/voidSettingsTypes.js';
|
||||
import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
|
||||
export const QuickEditChat = ({
|
||||
diffareaid,
|
||||
|
|
@ -59,6 +59,7 @@ export const QuickEditChat = ({
|
|||
|
||||
const id = inlineDiffsService.startApplying({
|
||||
from: 'QuickEdit',
|
||||
type:'rewrite',
|
||||
diffareaid: diffareaid,
|
||||
})
|
||||
setCurrentlyStreamingDiffZone(id ?? null)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
|
||||
import { errorDetails } from '../../../../../../../platform/void/common/llmMessageTypes.js';
|
||||
import { errorDetails } from '../../../../../../../workbench/contrib/void/common/llmMessageTypes.js';
|
||||
import { useSettingsState } from '../util/services.js';
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K
|
|||
|
||||
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
|
||||
import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js';
|
||||
import { ChatMessage, StagingInfo, StagingSelectionItem } from '../../../chatThreadService.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
|
|
@ -21,9 +21,11 @@ import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
|||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
import { Pencil } from 'lucide-react';
|
||||
import { FeatureName, isFeatureNameDisabled } from '../../../../../../../platform/void/common/voidSettingsTypes.js';
|
||||
import { Pencil, X } from 'lucide-react';
|
||||
import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
import { ChatMessageLocation } from '../../../searchAndReplaceService.js';
|
||||
|
||||
|
||||
|
||||
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
|
|
@ -145,15 +147,19 @@ interface VoidChatAreaProps {
|
|||
onAbort: () => void;
|
||||
isStreaming: boolean;
|
||||
isDisabled?: boolean;
|
||||
divRef: React.RefObject<HTMLDivElement>;
|
||||
divRef?: React.RefObject<HTMLDivElement>;
|
||||
|
||||
// UI customization
|
||||
featureName: FeatureName;
|
||||
className?: string;
|
||||
showModelDropdown?: boolean;
|
||||
showSelections?: boolean;
|
||||
selections?: any[];
|
||||
onSelectionsChange?: (selections: any[]) => void;
|
||||
showProspectiveSelections?: boolean;
|
||||
|
||||
staging?: StagingInfo
|
||||
setStaging?: (s: StagingInfo) => void
|
||||
// selections?: any[];
|
||||
// onSelectionsChange?: (selections: any[]) => void;
|
||||
|
||||
onClickAnywhere?: () => void;
|
||||
// Optional close button
|
||||
|
|
@ -173,8 +179,9 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
|||
showModelDropdown = true,
|
||||
featureName,
|
||||
showSelections = false,
|
||||
selections = [],
|
||||
onSelectionsChange,
|
||||
showProspectiveSelections = true,
|
||||
staging,
|
||||
setStaging,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -192,11 +199,12 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
|||
}}
|
||||
>
|
||||
{/* Selections section */}
|
||||
{showSelections && onSelectionsChange && (
|
||||
{showSelections && staging && setStaging && (
|
||||
<SelectedFiles
|
||||
type='staging'
|
||||
selections={selections}
|
||||
setSelections={onSelectionsChange}
|
||||
selections={staging.selections || []}
|
||||
setSelections={(selections) => setStaging({ ...staging, selections })}
|
||||
showProspectiveSelections={showProspectiveSelections}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -535,19 +543,55 @@ export const SelectedFiles = (
|
|||
|
||||
|
||||
type ChatBubbleMode = 'display' | 'edit'
|
||||
const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLoading?: boolean, }) => {
|
||||
const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => {
|
||||
|
||||
const role = chatMessage.role
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
// edit mode state
|
||||
const [mode, setMode] = useState<ChatBubbleMode>('display')
|
||||
const [editText, setEditText] = useState(chatMessage.displayContent ?? '')
|
||||
const [staging, setStaging] = chatThreadsService._useFocusedStagingState(messageIdx)
|
||||
const mode: ChatBubbleMode = staging.isBeingEdited ? 'edit' : 'display'
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isDisabled, setIsDisabled] = useState(false)
|
||||
const [textAreaRefState, setTextAreaRef] = useState<HTMLTextAreaElement | null>(null)
|
||||
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
|
||||
// initialize on first render, and when edit was just enabled
|
||||
const _mustInitialize = useRef(true)
|
||||
const _justEnabledEdit = useRef(false)
|
||||
useEffect(() => {
|
||||
const canInitialize = role === 'user' && mode === 'edit' && textAreaRefState
|
||||
const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current
|
||||
if (canInitialize && shouldInitialize) {
|
||||
setStaging({
|
||||
...staging,
|
||||
selections: chatMessage.selections || [],
|
||||
})
|
||||
if (textAreaFnsRef.current)
|
||||
textAreaFnsRef.current.setValue(chatMessage.displayContent || '')
|
||||
|
||||
if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show)
|
||||
return null
|
||||
textAreaRefState.focus();
|
||||
|
||||
_justEnabledEdit.current = false
|
||||
_mustInitialize.current = false
|
||||
}
|
||||
|
||||
}, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current])
|
||||
const EditSymbol = mode === 'display' ? Pencil : X
|
||||
const onOpenEdit = () => {
|
||||
setStaging({ ...staging, isBeingEdited: true })
|
||||
chatThreadsService.setFocusedMessageIdx(messageIdx)
|
||||
_justEnabledEdit.current = true
|
||||
}
|
||||
const onCloseEdit = () => {
|
||||
setIsFocused(false)
|
||||
setIsHovered(false)
|
||||
setStaging({ ...staging, isBeingEdited: false })
|
||||
chatThreadsService.setFocusedMessageIdx(undefined)
|
||||
|
||||
}
|
||||
// set chat bubble contents
|
||||
let chatbubbleContents: React.ReactNode
|
||||
if (role === 'user') {
|
||||
|
|
@ -558,23 +602,85 @@ const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLo
|
|||
</>
|
||||
}
|
||||
else if (mode === 'edit') {
|
||||
|
||||
const onSubmit = async () => {
|
||||
|
||||
if (isDisabled) return;
|
||||
if (!textAreaRefState) return;
|
||||
if (messageIdx === undefined) return;
|
||||
|
||||
// cancel any streams on this thread
|
||||
const thread = chatThreadsService.getCurrentThread()
|
||||
chatThreadsService.cancelStreaming(thread.id)
|
||||
|
||||
// reset state
|
||||
setStaging({ ...staging, isBeingEdited: false })
|
||||
chatThreadsService.setFocusedMessageIdx(undefined)
|
||||
|
||||
// stream the edit
|
||||
const userMessage = textAreaRefState.value;
|
||||
await chatThreadsService.editUserMessageAndStreamResponse(userMessage, messageIdx)
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
const threadId = chatThreadsService.state.currentThreadId
|
||||
chatThreadsService.cancelStreaming(threadId)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Escape') {
|
||||
onCloseEdit()
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show)
|
||||
return null
|
||||
}
|
||||
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections || []} />
|
||||
<textarea
|
||||
value={editText}
|
||||
onChange={(e) => setEditText(e.target.value)}
|
||||
className={`
|
||||
w-full max-w-full
|
||||
h-auto min-h-[81px] max-h-[500px]
|
||||
bg-void-bg-1 resize-none
|
||||
`}
|
||||
style={{ marginTop: 0 }}
|
||||
/>
|
||||
<VoidChatArea
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onAbort}
|
||||
isStreaming={false}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
showProspectiveSelections={false}
|
||||
featureName="Ctrl+L"
|
||||
staging={staging}
|
||||
setStaging={setStaging}
|
||||
>
|
||||
<VoidInputBox2
|
||||
ref={setTextAreaRef}
|
||||
className='min-h-[81px] max-h-[500px] p-1'
|
||||
placeholder="Edit your message..."
|
||||
onChangeText={(text) => setIsDisabled(!text)}
|
||||
onFocus={() => {
|
||||
setIsFocused(true)
|
||||
chatThreadsService.setFocusedMessageIdx(messageIdx);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFocused(false)
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true}
|
||||
/>
|
||||
</VoidChatArea>
|
||||
</>
|
||||
}
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} />
|
||||
const thread = chatThreadsService.getCurrentThread()
|
||||
|
||||
const chatMessageLocation: ChatMessageLocation = {
|
||||
threadId: thread.id,
|
||||
messageIdx: messageIdx!,
|
||||
}
|
||||
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} chatMessageLocation={chatMessageLocation} />
|
||||
}
|
||||
|
||||
return <div
|
||||
|
|
@ -594,16 +700,19 @@ const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLo
|
|||
// style chatbubble according to role
|
||||
className={`
|
||||
text-left rounded-lg
|
||||
overflow-x-auto max-w-full
|
||||
${role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1' : 'px-2'}
|
||||
max-w-full
|
||||
${mode === 'edit' ? ''
|
||||
: role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1 overflow-x-auto'
|
||||
: role === 'assistant' ? 'px-2 overflow-x-auto' : ''
|
||||
}
|
||||
`}
|
||||
>
|
||||
{chatbubbleContents}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm px-2' />}
|
||||
</div>
|
||||
|
||||
{/* edit button */}
|
||||
{role === 'user' && <Pencil
|
||||
{role === 'user' && <EditSymbol
|
||||
size={18}
|
||||
className={`
|
||||
absolute -top-1 right-1
|
||||
|
|
@ -612,9 +721,15 @@ const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLo
|
|||
p-[2px]
|
||||
bg-void-bg-1 border border-void-border-1 rounded-md
|
||||
transition-opacity duration-200 ease-in-out
|
||||
${isHovered ? 'opacity-100' : 'opacity-0'}
|
||||
${isHovered || (isFocused && mode === 'edit') ? 'opacity-100' : 'opacity-0'}
|
||||
`}
|
||||
onClick={() => setMode(m => m === 'display' ? 'edit' : 'display')}
|
||||
onClick={() => {
|
||||
if (mode === 'display') {
|
||||
onOpenEdit()
|
||||
} else if (mode === 'edit') {
|
||||
onCloseEdit()
|
||||
}
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -628,6 +743,7 @@ export const SidebarChat = () => {
|
|||
const accessor = useAccessor()
|
||||
// const modelService = accessor.get('IModelService')
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
// ----- HIGHER STATE -----
|
||||
|
|
@ -636,8 +752,8 @@ export const SidebarChat = () => {
|
|||
useEffect(() => {
|
||||
const disposables: IDisposable[] = []
|
||||
disposables.push(
|
||||
sidebarStateService.onDidFocusChat(() => { textAreaRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { textAreaRef.current?.blur() })
|
||||
sidebarStateService.onDidFocusChat(() => { !chatThreadsService.isFocusingMessage() && textAreaRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { !chatThreadsService.isFocusingMessage() && textAreaRef.current?.blur() })
|
||||
)
|
||||
return () => disposables.forEach(d => d.dispose())
|
||||
}, [sidebarStateService, textAreaRef])
|
||||
|
|
@ -646,11 +762,10 @@ export const SidebarChat = () => {
|
|||
|
||||
// threads state
|
||||
const chatThreadsState = useChatThreadsState()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
const currentThread = chatThreadsService.getCurrentThread()
|
||||
const previousMessages = currentThread?.messages ?? []
|
||||
const selections = chatThreadsState.currentStagingSelections
|
||||
const [staging, setStaging] = chatThreadsService._useFocusedStagingState()
|
||||
|
||||
// stream state
|
||||
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
|
||||
|
|
@ -675,8 +790,6 @@ export const SidebarChat = () => {
|
|||
|
||||
const onSubmit = useCallback(async () => {
|
||||
|
||||
console.log('onSubmit')
|
||||
|
||||
if (isDisabled) return
|
||||
if (isStreaming) return
|
||||
|
||||
|
|
@ -684,11 +797,11 @@ export const SidebarChat = () => {
|
|||
const userMessage = textAreaRef.current?.value ?? ''
|
||||
await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
|
||||
|
||||
chatThreadsService.setStaging([]) // clear staging
|
||||
setStaging({ ...staging, selections: [], }) // clear staging
|
||||
textAreaFnsRef.current?.setValue('')
|
||||
textAreaRef.current?.focus() // focus input after submit
|
||||
|
||||
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef])
|
||||
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, staging, setStaging])
|
||||
|
||||
const onAbort = () => {
|
||||
const threadId = currentThread.id
|
||||
|
|
@ -709,7 +822,7 @@ export const SidebarChat = () => {
|
|||
|
||||
const prevMessagesHTML = useMemo(() => {
|
||||
return previousMessages.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
<ChatBubble key={`${message.displayContent}-${i}`} chatMessage={message} messageIdx={i} />
|
||||
)
|
||||
}, [previousMessages])
|
||||
|
||||
|
|
@ -773,8 +886,9 @@ export const SidebarChat = () => {
|
|||
isStreaming={isStreaming}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
selections={selections || []}
|
||||
onSelectionsChange={chatThreadsService.setStaging.bind(chatThreadsService)}
|
||||
showProspectiveSelections={prevMessagesHTML.length === 0}
|
||||
staging={staging}
|
||||
setStaging={setStaging}
|
||||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||
featureName="Ctrl+L"
|
||||
>
|
||||
|
|
@ -783,6 +897,7 @@ export const SidebarChat = () => {
|
|||
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
|
||||
onChangeText={onChangeText}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={() => { chatThreadsService.setFocusedMessageIdx(undefined) }}
|
||||
ref={textAreaRef}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true}
|
||||
|
|
|
|||
|
|
@ -58,9 +58,11 @@ type InputBox2Props = {
|
|||
className?: string;
|
||||
onChangeText?: (value: string) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onFocus?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||
onChangeHeight?: (newHeight: number) => void;
|
||||
}
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onChangeText }, ref) {
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
|
||||
|
||||
// mirrors whatever is in ref
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
|
|
@ -114,6 +116,9 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
adjustHeight()
|
||||
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
|
||||
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
|
||||
disabled={!isEnabled}
|
||||
|
||||
className={`w-full resize-none max-h-[500px] overflow-y-auto text-void-fg-1 placeholder:text-void-fg-3 ${className}`}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@
|
|||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSidebarState } from '../../../sidebarStateService.js'
|
||||
import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js'
|
||||
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
|
||||
import { VoidUriState } from '../../../voidUriStateService.js';
|
||||
import { VoidQuickEditState } from '../../../quickEditStateService.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js'
|
||||
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
|
||||
|
||||
|
||||
|
||||
|
|
@ -25,9 +25,9 @@ import { IContextViewService, IContextMenuService } from '../../../../../../../p
|
|||
import { IFileService } from '../../../../../../../platform/files/common/files.js';
|
||||
import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js';
|
||||
import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js';
|
||||
import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../platform/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../platform/void/common/voidSettingsService.js';
|
||||
import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
|
||||
import { IInlineDiffsService } from '../../../inlineDiffsService.js';
|
||||
import { IVoidUriStateService } from '../../../voidUriStateService.js';
|
||||
import { IQuickEditStateService } from '../../../quickEditStateService.js';
|
||||
|
|
@ -46,7 +46,7 @@ import { IKeybindingService } from '../../../../../../../platform/keybinding/com
|
|||
import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js'
|
||||
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'
|
||||
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
|
||||
import { IMetricsService } from '../../../../../../../platform/void/common/metricsService.js'
|
||||
import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js'
|
||||
|
||||
|
||||
|
||||
|
|
@ -288,6 +288,17 @@ export const useChatThreadsState = () => {
|
|||
return () => { chatThreadsStateListeners.delete(ss) }
|
||||
}, [ss])
|
||||
return s
|
||||
// allow user to set state natively in react
|
||||
// const ss: React.Dispatch<React.SetStateAction<ThreadsState>> = (action)=>{
|
||||
// _ss(action)
|
||||
// if (typeof action === 'function') {
|
||||
// const newState = action(chatThreadsState)
|
||||
// chatThreadsState = newState
|
||||
// } else {
|
||||
// chatThreadsState = action
|
||||
// }
|
||||
// }
|
||||
// return [s, ss] as const
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FeatureName, featureNames, isFeatureNameDisabled, ModelSelection, modelSelectionsEqual, ProviderName, providerNames, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { FeatureName, featureNames, isFeatureNameDisabled, ModelSelection, modelSelectionsEqual, ProviderName, providerNames, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
|
||||
import { _VoidSelectBox, VoidCustomDropdownBox } from '../util/inputs.js'
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
|
||||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
|
||||
import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js'
|
||||
import { ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
|
||||
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
|
||||
|
||||
export type ChatMessageLocation = {
|
||||
threadId: string;
|
||||
messageIdx: number;
|
||||
}
|
||||
|
||||
|
||||
export type SearchAndReplaceBlock = {
|
||||
search: string;
|
||||
replace: string;
|
||||
}
|
||||
|
||||
// service that manages state
|
||||
export type ApplyState = {
|
||||
[applyBoxId: string]: {
|
||||
searchAndReplaceBlocks: SearchAndReplaceBlock;
|
||||
}
|
||||
}
|
||||
|
||||
// the purpose of this service is to generate search and replace blocks for a given codeblock `codeblockId` and on a file `fileName` and version `fileVersion`
|
||||
|
||||
export interface IFastApplyService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
// readonly state: ApplyState; // readonly to the user
|
||||
// setState(newState: Partial<ApplyState>): void;
|
||||
// onDidChangeState: Event<void>;
|
||||
}
|
||||
|
||||
export const IVoidFastApplyService = createDecorator<IFastApplyService>('voidFastApplyService');
|
||||
class VoidFastApplyService extends Disposable implements IFastApplyService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
static readonly ID = 'voidFastApplyService';
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
|
||||
// state
|
||||
// state: ApplyState
|
||||
|
||||
constructor(
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
// this.state = { currentUri: undefined }
|
||||
}
|
||||
|
||||
setState(newState: Partial<ApplyState>) {
|
||||
|
||||
// this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidFastApplyService, VoidFastApplyService, InstantiationType.Eager);
|
||||
|
|
@ -17,7 +17,7 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit
|
|||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ISidebarStateService } from './sidebarStateService.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
|
|
@ -67,6 +67,13 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => {
|
|||
}
|
||||
|
||||
|
||||
const findMatchingStagingIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem) => {
|
||||
return currentSelections?.findIndex(s =>
|
||||
s.fileURI.fsPath === newSelection.fileURI.fsPath
|
||||
&& s.range?.startLineNumber === newSelection.range?.startLineNumber
|
||||
&& s.range?.endLineNumber === newSelection.range?.endLineNumber
|
||||
)
|
||||
}
|
||||
|
||||
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
|
||||
registerAction2(class extends Action2 {
|
||||
|
|
@ -124,26 +131,26 @@ registerAction2(class extends Action2 {
|
|||
range: selectionRange,
|
||||
}
|
||||
|
||||
// add selection to staging
|
||||
// update the staging selections
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
const currentStaging = chatThreadService.state.currentStagingSelections
|
||||
const currentStagingEltIdx = currentStaging?.findIndex(s =>
|
||||
s.fileURI.fsPath === model.uri.fsPath
|
||||
&& s.range?.startLineNumber === selection.range?.startLineNumber
|
||||
&& s.range?.endLineNumber === selection.range?.endLineNumber
|
||||
)
|
||||
|
||||
// if matches with existing selection, overwrite
|
||||
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
|
||||
chatThreadService.setStaging([
|
||||
...currentStaging!.slice(0, currentStagingEltIdx),
|
||||
const focusedMessageIdx = chatThreadService.getFocusedMessageIdx()
|
||||
const [staging, setStaging] = chatThreadService._useFocusedStagingState(focusedMessageIdx)
|
||||
const selections = staging.selections || []
|
||||
const setSelections = (s: StagingSelectionItem[]) => setStaging({ ...staging, selections: s })
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection)
|
||||
if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) {
|
||||
setSelections([
|
||||
...selections!.slice(0, matchingStagingEltIdx),
|
||||
selection,
|
||||
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
|
||||
...selections!.slice(matchingStagingEltIdx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add
|
||||
// if no match, add it
|
||||
else {
|
||||
chatThreadService.setStaging([...(currentStaging ?? []), selection])
|
||||
setSelections([...(selections ?? []), selection])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,3 +33,26 @@ import './media/void.css'
|
|||
|
||||
// update (frontend part, also see platform/)
|
||||
import './voidUpdateActions.js'
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
|
||||
|
||||
// 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'
|
||||
|
||||
// tools
|
||||
import '../common/toolsService.js'
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
|
|||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { IVoidUpdateService } from '../../../../platform/void/common/voidUpdateService.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
|
||||
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { displayInfoOfProviderName, isFeatureNameDisabled } from './voidSettingsTypes.js';
|
||||
// import { INotificationService } from '../../notification/common/notification.js';
|
||||
|
|
@ -3,14 +3,14 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { Action2, registerAction2 } from '../../actions/common/actions.js';
|
||||
import { localize2 } from '../../../nls.js';
|
||||
import { ServicesAccessor } from '../../../editor/browser/editorExtensions.js';
|
||||
import { INotificationService } from '../../notification/common/notification.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { registerAction2, Action2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
|
||||
export interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
|
@ -3,14 +3,14 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { ILLMMessageService } from './llmMessageService.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
|
||||
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
|
||||
|
||||
|
||||
182
src/vs/workbench/contrib/void/common/toolsService.ts
Normal file
182
src/vs/workbench/contrib/void/common/toolsService.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { CancellationToken } from '../../../../base/common/cancellation.js'
|
||||
import { URI } from '../../../../base/common/uri.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 { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js'
|
||||
import { ISearchService } from '../../../../workbench/services/search/common/search.js'
|
||||
|
||||
|
||||
// tool use for AI
|
||||
|
||||
|
||||
|
||||
// we do this using Anthropic's style and convert to OpenAI style later
|
||||
export type InternalToolInfo = {
|
||||
description: string,
|
||||
params: {
|
||||
[paramName: string]: { type: string, description: string | undefined } // name -> type
|
||||
},
|
||||
required: string[], // required paramNames
|
||||
}
|
||||
|
||||
// helper
|
||||
const pagination = {
|
||||
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
|
||||
param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, }
|
||||
} as const
|
||||
|
||||
export const contextTools = {
|
||||
read_file: {
|
||||
description: 'Returns file contents of a given URI.',
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
},
|
||||
required: ['uri'],
|
||||
},
|
||||
|
||||
list_dir: {
|
||||
description: `Returns all file names and folder names in a given URI. ${pagination.desc}`,
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
...pagination.param
|
||||
},
|
||||
required: ['uri'],
|
||||
},
|
||||
|
||||
pathname_search: {
|
||||
description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${pagination.desc}`,
|
||||
params: {
|
||||
query: { type: 'string', description: undefined },
|
||||
...pagination.param,
|
||||
},
|
||||
required: ['query']
|
||||
},
|
||||
|
||||
search: {
|
||||
description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${pagination.desc}`,
|
||||
params: {
|
||||
query: { type: 'string', description: undefined },
|
||||
...pagination.param,
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
|
||||
// semantic_search: {
|
||||
// description: 'Searches files semantically for the given string query.',
|
||||
// // RAG
|
||||
// },
|
||||
|
||||
} as const satisfies { [name: string]: InternalToolInfo }
|
||||
|
||||
export type ContextToolName = keyof typeof contextTools
|
||||
type ContextToolParamNames<T extends ContextToolName> = keyof typeof contextTools[T]['params']
|
||||
type ContextToolParams<T extends ContextToolName> = { [paramName in ContextToolParamNames<T>]: unknown }
|
||||
|
||||
type AllContextToolCallFns = {
|
||||
[ToolName in ContextToolName]: ((p: (ContextToolParams<ToolName>)) => Promise<string>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// TODO check to make sure in workspace
|
||||
// TODO check to make sure is not gitignored
|
||||
|
||||
|
||||
async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI): Promise<string> {
|
||||
let output = ''
|
||||
function traverseChildren(children: IFileStat[], depth: number) {
|
||||
const indentation = ' '.repeat(depth);
|
||||
for (const child of children) {
|
||||
output += `${indentation}- ${child.name}\n`;
|
||||
traverseChildren(child.children ?? [], depth + 1);
|
||||
}
|
||||
}
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
|
||||
// kickstart recursion
|
||||
output += `${stat.name}\n`;
|
||||
traverseChildren(stat.children ?? [], 1);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (typeof uriStr !== 'string') throw new Error('(uri was not a string)')
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
|
||||
export interface IToolService {
|
||||
readonly _serviceBrand: undefined;
|
||||
callContextTool: <T extends ContextToolName>(toolName: T, params: ContextToolParams<T>) => Promise<string>
|
||||
}
|
||||
|
||||
export const IToolService = createDecorator<IToolService>('ToolService');
|
||||
|
||||
export class ToolService implements IToolService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
contextToolCallFns: AllContextToolCallFns
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
|
||||
@ISearchService searchService: ISearchService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
) {
|
||||
|
||||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.contextToolCallFns = {
|
||||
read_file: async ({ uri: uriStr }) => {
|
||||
const uri = validateURI(uriStr)
|
||||
const fileContents = await VSReadFileRaw(fileService, uri)
|
||||
return fileContents ?? '(could not read file)'
|
||||
},
|
||||
list_dir: async ({ uri: uriStr }) => {
|
||||
const uri = validateURI(uriStr)
|
||||
const treeStr = await generateDirectoryTreeMd(fileService, uri)
|
||||
return treeStr
|
||||
},
|
||||
pathname_search: async ({ query: queryStr }) => {
|
||||
if (typeof queryStr !== 'string') return '(Error: query was not a string)'
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, });
|
||||
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None);
|
||||
const str = data.results.map(({ resource, results }) => resource.fsPath).join('\n')
|
||||
return str
|
||||
},
|
||||
search: async ({ query: queryStr }) => {
|
||||
if (typeof queryStr !== 'string') return '(Error: query was not a string)'
|
||||
const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri));
|
||||
|
||||
const data = await searchService.textSearch(query, CancellationToken.None);
|
||||
const str = data.results.map(({ resource, results }) => resource.fsPath).join('\n')
|
||||
return str
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
callContextTool: IToolService['callContextTool'] = (toolName, params) => {
|
||||
return this.contextToolCallFns[toolName](params)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IToolService, ToolService, InstantiationType.Eager);
|
||||
|
||||
|
|
@ -3,13 +3,13 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { deepClone } from '../../../base/common/objects.js';
|
||||
import { IEncryptionService } from '../../encryption/common/encryptionService.js';
|
||||
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { IEncryptionService } from '../../../../platform/encryption/common/encryptionService.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultModelNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings } from './voidSettingsTypes.js';
|
||||
|
||||
|
|
@ -227,7 +227,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
|||
}
|
||||
else if (providerName === 'deepseek') {
|
||||
return {
|
||||
title: 'DeepSeek',
|
||||
title: 'DeepSeek.com API',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
|
|
@ -248,17 +248,17 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
|||
}
|
||||
else if (providerName === 'gemini') {
|
||||
return {
|
||||
title: 'Gemini',
|
||||
title: 'Gemini API',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'groq') {
|
||||
return {
|
||||
title: 'Groq',
|
||||
title: 'Groq.com API',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
return {
|
||||
title: 'Mistral',
|
||||
title: 'Mistral API',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3,10 +3,10 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -6,6 +6,27 @@
|
|||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
|
||||
import { InternalToolInfo } from '../../common/toolsService.js';
|
||||
|
||||
|
||||
|
||||
|
||||
export const toAnthropicTool = (toolName: string, toolInfo: InternalToolInfo) => {
|
||||
const { description, params, required } = toolInfo
|
||||
return {
|
||||
name: toolName,
|
||||
description: description,
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
required: required,
|
||||
}
|
||||
} satisfies Anthropic.Messages.Tool
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
|
|
@ -22,8 +22,6 @@ export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages
|
|||
messages: messages,
|
||||
model: modelName,
|
||||
stream: true,
|
||||
// temperature: 0.7,
|
||||
// max_tokens: parseMaxTokensStr(thisConfig.maxTokens),
|
||||
})
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
|
|
@ -21,8 +21,6 @@ export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messa
|
|||
messages: messages,
|
||||
model: modelName,
|
||||
stream: true,
|
||||
// temperature: 0.7,
|
||||
// maxTokens: 2048,
|
||||
})
|
||||
.then(async response => {
|
||||
// Mistral has a really nonstandard API - no interrupt and weird stream types
|
||||
|
|
@ -6,43 +6,34 @@
|
|||
import OpenAI from 'openai';
|
||||
import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { Model } from 'openai/resources/models.js';
|
||||
import { InternalToolInfo } from '../../common/toolsService.js';
|
||||
// import { parseMaxTokensStr } from './util.js';
|
||||
|
||||
|
||||
// developer command - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
|
||||
// prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
|
||||
|
||||
export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
const onSuccess = ({ models }: { models: Model[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.openAICompatible
|
||||
const openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
|
||||
|
||||
openai.models.list()
|
||||
.then(async (response) => {
|
||||
const models: Model[] = []
|
||||
models.push(...response.data)
|
||||
while (response.hasNextPage()) {
|
||||
models.push(...(await response.getNextPage()).data)
|
||||
}
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
export const toOpenAITool = (toolName: string, toolInfo: InternalToolInfo) => {
|
||||
const { description, params, required } = toolInfo
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: toolName,
|
||||
description: description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
required: required,
|
||||
}
|
||||
}
|
||||
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type NewParams = Pick<Parameters<_InternalSendLLMChatMessageFnType>[0] & Parameters<_InternalSendLLMFIMMessageFnType>[0], 'settingsOfProvider' | 'providerName'>
|
||||
const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => {
|
||||
|
||||
|
|
@ -81,26 +72,46 @@ const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => {
|
|||
|
||||
|
||||
|
||||
// might not currently be used in the code
|
||||
export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
const onSuccess = ({ models }: { models: Model[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
try {
|
||||
const openai = newOpenAI({ providerName: 'openAICompatible', settingsOfProvider })
|
||||
|
||||
openai.models.list()
|
||||
.then(async (response) => {
|
||||
const models: Model[] = []
|
||||
models.push(...response.data)
|
||||
while (response.hasNextPage()) {
|
||||
models.push(...(await response.getNextPage()).data)
|
||||
}
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
|
||||
|
||||
const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider })
|
||||
// openai.completions has a FIM parameter called `suffix`, but it's deprecated and only works for ~GPT 3 era models
|
||||
|
||||
const options: OpenAI.Completions.CompletionCreateParamsStreaming = {
|
||||
prompt: messages.prefix,
|
||||
suffix: messages.suffix,
|
||||
model: modelName,
|
||||
stream: true,
|
||||
// max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)
|
||||
}
|
||||
|
||||
|
||||
openai.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
// TODO!!!
|
||||
console.log('RESPONSE', response)
|
||||
})
|
||||
onFinalMessage({ fullText: 'TODO' })
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +127,7 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, on
|
|||
model: modelName,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
// max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)
|
||||
// tools: Object.keys(contextTools).map(name => toOpenAITool(name, contextTools[name as ContextToolName])),
|
||||
}
|
||||
|
||||
openai.chat.completions
|
||||
|
|
@ -125,7 +136,11 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, on
|
|||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
|
||||
let newText = ''
|
||||
newText += chunk.choices[0]?.delta?.tool_calls?.[0]?.function?.name ?? ''
|
||||
newText += chunk.choices[0]?.delta?.tool_calls?.[0]?.function?.arguments ?? ''
|
||||
newText += chunk.choices[0]?.delta?.content ?? ''
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import { IMetricsService } from '../../common/metricsService.js';
|
|||
|
||||
import { sendAnthropicChat } from './anthropic.js';
|
||||
import { sendOllamaFIM, sendOllamaChat } from './ollama.js';
|
||||
import { sendOpenAIChat } from './openai.js';
|
||||
import { sendOpenAIChat, sendOpenAIFIM } from './openai.js';
|
||||
import { sendGeminiChat } from './gemini.js';
|
||||
import { sendGroqChat } from './groq.js';
|
||||
import { sendMistralChat } from './mistral.js';
|
||||
|
|
@ -65,9 +65,14 @@ export const sendLLMMessage = ({
|
|||
|
||||
metricsService: IMetricsService
|
||||
) => {
|
||||
// messages.unshift({ role: 'system', content: aiInstructions })
|
||||
|
||||
const messagesArr = messagesType === 'chatMessages' ? cleanChatMessages(messages_) : []
|
||||
let messagesArr: _InternalLLMChatMessage[] = []
|
||||
if (messagesType === 'chatMessages') {
|
||||
messagesArr = cleanChatMessages([
|
||||
{ role: 'system', content: aiInstructions },
|
||||
...messages_
|
||||
])
|
||||
}
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureLLMEvent = (eventId: string, extras?: object) => {
|
||||
|
|
@ -131,33 +136,32 @@ export const sendLLMMessage = ({
|
|||
|
||||
try {
|
||||
switch (providerName) {
|
||||
case 'anthropic':
|
||||
sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'deepseek':
|
||||
case 'openAICompatible':
|
||||
sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'gemini':
|
||||
sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
if (messagesType === 'FIMMessage') sendOpenAIFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'ollama':
|
||||
if ( // TODO @andrew in future we want to use our own templates instead of using ollamaFIM
|
||||
messagesType === 'FIMMessage'
|
||||
&& settingsOfProvider['ollama']._didFillInProviderSettings
|
||||
&& settingsOfProvider['ollama'].models.some(m => !m.isHidden)
|
||||
)
|
||||
sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName })
|
||||
else
|
||||
sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
if (messagesType === 'FIMMessage') sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName })
|
||||
else /* */ sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName })
|
||||
break;
|
||||
case 'anthropic':
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM' })
|
||||
else /* */ sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'gemini':
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM' })
|
||||
else /* */ sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'groq':
|
||||
sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Groq FIM' })
|
||||
else /* */ sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'mistral':
|
||||
sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Mistral FIM' })
|
||||
else /* */ sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
default:
|
||||
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
|
||||
|
|
@ -6,8 +6,8 @@
|
|||
// registered in app.ts
|
||||
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
|
||||
|
||||
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js';
|
||||
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
|
|
@ -66,7 +66,7 @@ export class LLMMessageChannel implements IServerChannel {
|
|||
}
|
||||
}
|
||||
|
||||
// browser uses this to call
|
||||
// browser uses this to call (see this.channel.call() in llmMessageService.ts for all usages)
|
||||
async call(_: unknown, command: string, params: any): Promise<any> {
|
||||
try {
|
||||
if (command === 'sendLLMMessage') {
|
||||
|
|
@ -3,14 +3,13 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
import { IApplicationStorageMainService } from '../../storage/electron-main/storageMainService.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { isLinux, isMacintosh, isWindows } from '../../../../base/common/platform.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { StorageTarget, StorageScope } from '../../../../platform/storage/common/storage.js';
|
||||
import { IApplicationStorageMainService } from '../../../../platform/storage/electron-main/storageMainService.js';
|
||||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
|
||||
modelName -> {
|
||||
system_message_type: 'system' | 'developer' (openai) | null // if null, we will just do a string of system message
|
||||
supports_tools: boolean // we will just do a string of tool use if it doesn't support
|
||||
supports_autocomplete_FIM (suffix) // we will just do a description of FIM if it doens't support <|fim_hole|>
|
||||
|
||||
supports_streaming: boolean // (o1 does NOT) we will just dump the final result if doesn't support it
|
||||
max_tokens: number // required, DEFAULT is Infinity
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
|
|
@ -3,10 +3,9 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
|
||||
import { IVoidUpdateService } from '../common/voidUpdateService.js';
|
||||
|
||||
|
|
@ -17,7 +17,6 @@ import './browser/workbench.contribution.js';
|
|||
//#region --- Void
|
||||
// Void added this:
|
||||
import './contrib/void/browser/void.contribution.js';
|
||||
import '../platform/void/browser/void.contribution.js';
|
||||
//#endregion
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue