Merge pull request #281 from voideditor/model-selection

Latest streaming changes
This commit is contained in:
Andrew Pareles 2025-02-12 18:51:55 -08:00 committed by GitHub
commit 7fbe8d905f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1285 additions and 473 deletions

View file

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

View file

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

View file

@ -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 })
// });
// }

View file

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

View file

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

View file

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

View file

@ -62,7 +62,7 @@ class SurroundingsRemover {
if (index === -1) {
this.i = this.j + 1
return false
return null
}
// console.log('index', index, until.length)

View file

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

View file

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

View file

@ -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]}).`
};

View file

@ -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 = {

View file

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

View file

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

View file

@ -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';

View file

@ -7,7 +7,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
import { ChatMessage, 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}

View file

@ -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}`}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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';

View file

@ -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';

View file

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

View file

@ -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';

View 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);

View file

@ -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';

View file

@ -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',
}
}

View file

@ -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';

View file

@ -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 }) => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
}
*/

View file

@ -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';

View file

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