mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
Merge pull request #303 from voideditor/model-selection
Add reasoning parsing, marketplace URL, chat selection state, misc UI
This commit is contained in:
commit
dbc14a0713
21 changed files with 665 additions and 619 deletions
|
|
@ -31,8 +31,8 @@
|
|||
"nodejsRepository": "https://nodejs.org",
|
||||
"urlProtocol": "void",
|
||||
"extensionsGallery": {
|
||||
"serviceUrl": "https://open-vsx.org/vscode/gallery",
|
||||
"itemUrl": "https://open-vsx.org/vscode/item"
|
||||
"serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery",
|
||||
"itemUrl": "https://marketplace.visualstudio.com/items"
|
||||
},
|
||||
"builtInExtensions": []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,185 +3,106 @@
|
|||
* 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 { URI } from '../../../../base/common/uri.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
// import { IToolService, ToolService } from '../common/toolsService.js';
|
||||
// 1. search(ai)
|
||||
// - tool use to find all possible changes
|
||||
// - if search only: is this file related to the search?
|
||||
// - if search + replace: should I modify this file?
|
||||
// 2. replace(ai)
|
||||
// - what changes to make?
|
||||
// 3. postprocess errors
|
||||
// -fastapply changes simultaneously
|
||||
// -iterate on syntax errors (all files can be changed from a syntax error, not just the one with the error)
|
||||
|
||||
|
||||
// private async _searchUsingAI({ searchClause }: { searchClause: string }) {
|
||||
|
||||
export type ChatMessageLocation = {
|
||||
threadId: string;
|
||||
messageIdx: number;
|
||||
}
|
||||
// // const relevantURIs: URI[] = []
|
||||
// // const gatherPrompt = `\
|
||||
// // asdasdas
|
||||
// // `
|
||||
// // const filterPrompt = `\
|
||||
// // Is this file relevant?
|
||||
// // `
|
||||
|
||||
|
||||
export type SearchAndReplaceBlock = {
|
||||
search: string;
|
||||
replace: string;
|
||||
}
|
||||
// // // optimizations (DO THESE LATER!!!!!!)
|
||||
// // // if tool includes a uri in uriSet, skip it obviously
|
||||
// // let uriSet = new Set<URI>()
|
||||
// // // gather
|
||||
// // let messages = []
|
||||
// // while (true) {
|
||||
// // const result = await new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['search'],
|
||||
// // onFinalMessage: ({ result: r, }) => {
|
||||
// // res(r)
|
||||
// // },
|
||||
// // onError: (error) => {
|
||||
// // rej(error)
|
||||
// // }
|
||||
// // })
|
||||
// // })
|
||||
|
||||
// service that manages state
|
||||
export type ApplyState = {
|
||||
[applyBoxId: string]: {
|
||||
searchAndReplaceBlocks: SearchAndReplaceBlock;
|
||||
}
|
||||
}
|
||||
// // messages.push({ role: 'tool', content: turnToString(result) })
|
||||
|
||||
// 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`
|
||||
// // sendLLMMessage({
|
||||
// // messages: { 'Output ': result },
|
||||
// // onFinalMessage: (r) => {
|
||||
// // // output is file1\nfile2\nfile3\n...
|
||||
// // }
|
||||
// // })
|
||||
|
||||
export interface IFastApplyService {
|
||||
readonly _serviceBrand: undefined;
|
||||
// // uriSet.add(...)
|
||||
// // }
|
||||
|
||||
// readonly state: ApplyState; // readonly to the user
|
||||
// setState(newState: Partial<ApplyState>): void;
|
||||
// onDidChangeState: Event<void>;
|
||||
}
|
||||
// // // writes
|
||||
// // if (!replaceClause) return
|
||||
|
||||
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(
|
||||
// @IToolService private readonly toolService: ToolService
|
||||
) {
|
||||
super()
|
||||
|
||||
// initial state
|
||||
// this.state = { currentUri: undefined }
|
||||
}
|
||||
|
||||
setState(newState: Partial<ApplyState>) {
|
||||
|
||||
// this.state = { ...this.state, ...newState }
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
aiSearch(searchStr: string) {
|
||||
|
||||
}
|
||||
|
||||
aiReplace(searchStr: string, replaceStr: string) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 1. search(ai)
|
||||
// - tool use to find all possible changes
|
||||
// - if search only: is this file related to the search?
|
||||
// - if search + replace: should I modify this file?
|
||||
// 2. replace(ai)
|
||||
// - what changes to make?
|
||||
// 3. postprocess errors
|
||||
// -fastapply changes simultaneously
|
||||
// -iterate on syntax errors (all files can be changed from a syntax error, not just the one with the error)
|
||||
|
||||
|
||||
// private async _searchUsingAI({ searchClause }: { searchClause: string }) {
|
||||
|
||||
// // const relevantURIs: URI[] = []
|
||||
// // const gatherPrompt = `\
|
||||
// // asdasdas
|
||||
// // `
|
||||
// // const filterPrompt = `\
|
||||
// // Is this file relevant?
|
||||
// // `
|
||||
|
||||
|
||||
// // // optimizations (DO THESE LATER!!!!!!)
|
||||
// // // if tool includes a uri in uriSet, skip it obviously
|
||||
// // let uriSet = new Set<URI>()
|
||||
// // // gather
|
||||
// // let messages = []
|
||||
// // while (true) {
|
||||
// // const result = await new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['search'],
|
||||
// // onFinalMessage: ({ result: r, }) => {
|
||||
// // res(r)
|
||||
// // },
|
||||
// // onError: (error) => {
|
||||
// // rej(error)
|
||||
// // }
|
||||
// // })
|
||||
// // })
|
||||
|
||||
// // messages.push({ role: 'tool', content: turnToString(result) })
|
||||
|
||||
// // sendLLMMessage({
|
||||
// // messages: { 'Output ': result },
|
||||
// // onFinalMessage: (r) => {
|
||||
// // // output is file1\nfile2\nfile3\n...
|
||||
// // }
|
||||
// // })
|
||||
|
||||
// // uriSet.add(...)
|
||||
// // }
|
||||
|
||||
// // // writes
|
||||
// // if (!replaceClause) return
|
||||
|
||||
// // for (const uri of uriSet) {
|
||||
// // // in future, batch these
|
||||
// // applyWorkflow({ uri, applyStr: replaceClause })
|
||||
// // }
|
||||
// // for (const uri of uriSet) {
|
||||
// // // in future, batch these
|
||||
// // applyWorkflow({ uri, applyStr: replaceClause })
|
||||
// // }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// // while (true) {
|
||||
// // const result = new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['search'],
|
||||
// // onResult: (r) => {
|
||||
// // res(r)
|
||||
// // }
|
||||
// // })
|
||||
// // })
|
||||
// // while (true) {
|
||||
// // const result = new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['search'],
|
||||
// // onResult: (r) => {
|
||||
// // res(r)
|
||||
// // }
|
||||
// // })
|
||||
// // })
|
||||
|
||||
// // messages.push(result)
|
||||
// // messages.push(result)
|
||||
|
||||
// // }
|
||||
// // }
|
||||
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) {
|
||||
// private async _replaceUsingAI({ searchClause, replaceClause, relevantURIs }: { searchClause: string, replaceClause: string, relevantURIs: URI[] }) {
|
||||
|
||||
// for (const uri of relevantURIs) {
|
||||
// for (const uri of relevantURIs) {
|
||||
|
||||
// uri
|
||||
// uri
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// // should I change this file?
|
||||
// // if so what changes to make?
|
||||
// // should I change this file?
|
||||
// // if so what changes to make?
|
||||
|
||||
|
||||
|
||||
// // fast apply the changes
|
||||
// }
|
||||
// // fast apply the changes
|
||||
// }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IVoidFastApplyService, VoidFastApplyService, InstantiationType.Eager);
|
||||
|
|
|
|||
|
|
@ -1400,7 +1400,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
const latestStreamInfoMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
|
||||
// state used in onText:
|
||||
let fullText = ''
|
||||
let fullTextSoFar = '' // so far (INCLUDING ignored suffix)
|
||||
let prevIgnoredSuffix = ''
|
||||
|
||||
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
|
||||
|
|
@ -1408,12 +1408,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K',
|
||||
logging: { loggingName: `startApplying - ${from}` },
|
||||
messages,
|
||||
onText: ({ newText: newText_ }) => {
|
||||
onText: ({ fullText: fullText_ }) => {
|
||||
const newText_ = fullText_.substring(fullTextSoFar.length, Infinity)
|
||||
|
||||
const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix!
|
||||
fullText += prevIgnoredSuffix + newText // full text, including ```, etc
|
||||
fullTextSoFar += newText // full text, including ```, etc
|
||||
|
||||
const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length)
|
||||
const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullTextSoFar, newText.length)
|
||||
const { endLineInLlmTextSoFar } = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable)
|
||||
diffZone._streamState.line = (diffZone.startLine - 1) + endLineInLlmTextSoFar // change coordinate systems from originalCode to full file
|
||||
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export type ExtractedSearchReplaceBlock = {
|
|||
|
||||
const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
|
||||
// for each prefix
|
||||
for (let i = anyPrefix.length; i >= 0; i--) {
|
||||
for (let i = anyPrefix.length; i >= 1; i--) { // i >= 1 because must not be empty string
|
||||
const prefix = anyPrefix.slice(0, i)
|
||||
if (str.endsWith(prefix)) return prefix
|
||||
}
|
||||
|
|
@ -251,86 +251,109 @@ export const extractSearchReplaceBlocks = (str: string) => {
|
|||
|
||||
|
||||
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
|
||||
export const extractReasoningFromText = (
|
||||
onText_: OnText,
|
||||
thinkTags: [string, string],
|
||||
): OnText => {
|
||||
|
||||
let latestAddIdx = 0 // exclusive
|
||||
export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string, string]): OnText => {
|
||||
let latestAddIdx = 0 // exclusive index in fullText_
|
||||
let foundTag1 = false
|
||||
let foundTag2 = false
|
||||
|
||||
let fullText = ''
|
||||
let fullReasoning = ''
|
||||
let fullTextSoFar = ''
|
||||
let fullReasoningSoFar = ''
|
||||
|
||||
const onText: OnText = ({ newText: newText_, fullText: fullText_ }) => {
|
||||
// abcdef<t|hin|k>ghi
|
||||
// |
|
||||
let onText_ = onText
|
||||
onText = (params) => {
|
||||
onText_(params)
|
||||
}
|
||||
|
||||
const newOnText: OnText = ({ fullText: fullText_ }) => {
|
||||
// until found the first think tag, keep adding to fullText
|
||||
if (!foundTag1) {
|
||||
const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0])
|
||||
if (endsWithTag1) {
|
||||
// console.log('endswith1', { fullTextSoFar, fullReasoningSoFar, fullText_ })
|
||||
// wait until we get the full tag or know more
|
||||
return
|
||||
}
|
||||
// if found the first tag
|
||||
const tag1Index = fullText_.lastIndexOf(thinkTags[0])
|
||||
const tag1Index = fullText_.indexOf(thinkTags[0])
|
||||
if (tag1Index !== -1) {
|
||||
// console.log('tag1Index !==1', { tag1Index, fullTextSoFar, fullReasoningSoFar, thinkTags, fullText_ })
|
||||
foundTag1 = true
|
||||
const newText = fullText.substring(latestAddIdx, tag1Index)
|
||||
const newReasoning = fullText.substring(tag1Index + thinkTags[0].length, Infinity)
|
||||
|
||||
fullText += newText
|
||||
fullReasoning += newReasoning
|
||||
latestAddIdx += newText.length + newReasoning.length
|
||||
onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning })
|
||||
// Add text before the tag to fullTextSoFar
|
||||
fullTextSoFar += fullText_.substring(0, tag1Index)
|
||||
// Update latestAddIdx to after the first tag
|
||||
latestAddIdx = tag1Index + thinkTags[0].length
|
||||
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
|
||||
// console.log('adding to text A', { fullTextSoFar, fullReasoningSoFar })
|
||||
// add the text to fullText
|
||||
const newText = fullText.substring(latestAddIdx, Infinity)
|
||||
fullText += newText
|
||||
latestAddIdx += newText.length
|
||||
onText_({ newText, fullText, newReasoning: '', fullReasoning })
|
||||
fullTextSoFar = fullText_
|
||||
latestAddIdx = fullText_.length
|
||||
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
|
||||
// at this point, we found <tag1>
|
||||
|
||||
// until found the second think tag, keep adding to fullReasoning
|
||||
if (!foundTag2) {
|
||||
const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1])
|
||||
if (endsWithTag2) {
|
||||
// console.log('endsWith2', { fullTextSoFar, fullReasoningSoFar })
|
||||
// wait until we get the full tag or know more
|
||||
return
|
||||
}
|
||||
// if found the second tag
|
||||
const tag2Index = fullText_.lastIndexOf(thinkTags[1])
|
||||
if (tag2Index !== -1) {
|
||||
foundTag2 = true
|
||||
const newReasoning = fullText.substring(latestAddIdx, tag2Index)
|
||||
const newText = fullText.substring(tag2Index + thinkTags[1].length, Infinity)
|
||||
|
||||
fullText += newText
|
||||
fullReasoning += newReasoning
|
||||
latestAddIdx += newText.length + newReasoning.length
|
||||
onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning })
|
||||
// if found the second tag
|
||||
const tag2Index = fullText_.indexOf(thinkTags[1], latestAddIdx)
|
||||
if (tag2Index !== -1) {
|
||||
// console.log('tag2Index !== -1', { fullTextSoFar, fullReasoningSoFar })
|
||||
foundTag2 = true
|
||||
// Add everything between first and second tag to reasoning
|
||||
fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index)
|
||||
// Update latestAddIdx to after the second tag
|
||||
latestAddIdx = tag2Index + thinkTags[1].length
|
||||
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
|
||||
// add the text to fullReasoning
|
||||
const newReasoning = fullText.substring(latestAddIdx, Infinity)
|
||||
fullReasoning += newReasoning
|
||||
latestAddIdx += newReasoning.length
|
||||
onText_({ newText: '', fullText, newReasoning, fullReasoning })
|
||||
// add the text to fullReasoning (content after first tag but before second tag)
|
||||
// console.log('adding to text B', { fullTextSoFar, fullReasoningSoFar })
|
||||
|
||||
// If we have more text than we've processed, add it to reasoning
|
||||
if (fullText_.length > latestAddIdx) {
|
||||
fullReasoningSoFar += fullText_.substring(latestAddIdx)
|
||||
latestAddIdx = fullText_.length
|
||||
}
|
||||
|
||||
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
return
|
||||
}
|
||||
// at this point, we found <tag2>
|
||||
|
||||
fullText += newText_
|
||||
const newText = fullText.substring(latestAddIdx, Infinity)
|
||||
latestAddIdx += newText.length
|
||||
onText_({ newText, fullText, newReasoning: '', fullReasoning })
|
||||
// at this point, we found <tag2> - content after the second tag is normal text
|
||||
// console.log('adding to text C', { fullTextSoFar, fullReasoningSoFar })
|
||||
|
||||
// Add any new text after the closing tag to fullTextSoFar
|
||||
if (fullText_.length > latestAddIdx) {
|
||||
fullTextSoFar += fullText_.substring(latestAddIdx)
|
||||
latestAddIdx = fullText_.length
|
||||
}
|
||||
|
||||
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
}
|
||||
|
||||
return onText
|
||||
return newOnText
|
||||
}
|
||||
|
||||
|
||||
export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [string, string]): { fullText: string, fullReasoning: string } => {
|
||||
const tag1Idx = fullText_.indexOf(thinkTags[0])
|
||||
const tag2Idx = fullText_.indexOf(thinkTags[1])
|
||||
if (tag1Idx === -1) return { fullText: fullText_, fullReasoning: '' } // never started reasoning
|
||||
if (tag2Idx === -1) return { fullText: '', fullReasoning: fullText_ } // never stopped reasoning
|
||||
|
||||
const fullReasoning = fullText_.substring(tag1Idx + thinkTags[0].length, tag2Idx)
|
||||
const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity)
|
||||
return { fullText, fullReasoning }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
|
||||
import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js';
|
||||
import { CodeSelection, StagingSelectionItem, FileSelection } from '../../common/chatThreadService.js';
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { IVoidFileService } from '../../common/voidFileService.js';
|
||||
|
|
@ -299,7 +299,7 @@ For example, if the user is asking you to "make this variable a better name", ma
|
|||
export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, voidFileService: IVoidFileService }) => {
|
||||
|
||||
// we may want to do this in batches
|
||||
const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null }
|
||||
const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } }
|
||||
|
||||
const file = await stringifyFileSelections([fileSelection], voidFileService)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="relative group w-full overflow-hidden my-4">
|
||||
<div className="relative group w-full overflow-hidden">
|
||||
{buttonsOnHover === null ? null : (
|
||||
<div className={`z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200 ${isSingleLine ? 'h-full flex items-center' : ''}`}>
|
||||
<div className={`flex space-x-1 ${isSingleLine ? 'pr-2' : 'p-2'}`}>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@
|
|||
import React, { JSX } from 'react'
|
||||
import { marked, MarkedToken, Token } from 'marked'
|
||||
import { BlockCode } from './BlockCode.js'
|
||||
import { ChatMessageLocation, } from '../../../aiRegexService.js'
|
||||
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
|
||||
import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js'
|
||||
|
||||
export type ChatMessageLocation = {
|
||||
threadId: string;
|
||||
messageIdx: number;
|
||||
}
|
||||
|
||||
|
||||
type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string }
|
||||
|
||||
|
|
@ -33,7 +37,7 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c
|
|||
</code>
|
||||
}
|
||||
|
||||
const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
|
||||
const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
|
||||
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
|
|
@ -45,17 +49,19 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }:
|
|||
|
||||
if (t.type === "code") {
|
||||
|
||||
const applyBoxId = chatMessageLocation ? getApplyBoxId({
|
||||
threadId: chatMessageLocation.threadId,
|
||||
messageIdx: chatMessageLocation.messageIdx,
|
||||
const applyBoxId = chatMessageLocationForApply ? getApplyBoxId({
|
||||
threadId: chatMessageLocationForApply.threadId,
|
||||
messageIdx: chatMessageLocationForApply.messageIdx,
|
||||
tokenIdx: tokenIdx,
|
||||
}) : null
|
||||
|
||||
return <BlockCode
|
||||
return <div className='my-4'>
|
||||
<BlockCode
|
||||
initValue={t.text}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
|
||||
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
|
|
@ -129,7 +135,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }:
|
|||
<input type="checkbox" checked={item.checked} readOnly className="mr-2 form-checkbox" />
|
||||
)}
|
||||
<span className="ml-1">
|
||||
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} />
|
||||
<ChatMarkdownRender chatMessageLocationForApply={chatMessageLocationForApply} string={item.text} nested={true} />
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -241,12 +247,12 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }:
|
|||
)
|
||||
}
|
||||
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocation }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation }) => {
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocationForApply }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: 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} chatMessageLocation={chatMessageLocation} tokenIdx={index + ''} />
|
||||
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatMessageLocationForApply={chatMessageLocationForApply} tokenIdx={index + ''} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K
|
|||
|
||||
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
|
||||
import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js';
|
||||
import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadService.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
|
||||
import { ChatMarkdownRender, ChatMessageLocation } from '../markdown/ChatMarkdownRender.js';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
|
|
@ -24,7 +24,7 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
|||
import { ChevronRight, 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 '../../../aiRegexService.js';
|
||||
|
||||
import { ToolCallReturnType, ToolName } from '../../../../common/toolsService.js';
|
||||
|
||||
|
||||
|
|
@ -139,6 +139,9 @@ export const IconLoading = ({ className = '' }: { className?: string }) => {
|
|||
}
|
||||
|
||||
|
||||
const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-${messageIdx}`;
|
||||
|
||||
|
||||
interface VoidChatAreaProps {
|
||||
// Required
|
||||
children: React.ReactNode; // This will be the input component
|
||||
|
|
@ -187,12 +190,15 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
|||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
// border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
className={`
|
||||
flex flex-col gap-1 p-2 relative input text-left shrink-0
|
||||
gap-1
|
||||
flex flex-col p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
outline-1 outline-void-border-3 focus-within:outline-void-border-1 hover:outline-void-border-1
|
||||
max-h-[80vh] overflow-y-auto
|
||||
${className}
|
||||
`}
|
||||
onClick={(e) => {
|
||||
|
|
@ -370,12 +376,6 @@ export const SelectedFiles = (
|
|||
| { type: 'staging', selections: StagingSelectionItem[]; setSelections: ((newSelections: StagingSelectionItem[]) => void), showProspectiveSelections?: boolean }
|
||||
) => {
|
||||
|
||||
// index -> isOpened
|
||||
const [selectionIsOpened, setSelectionIsOpened] = useState<(boolean)[]>(selections?.map(() => false) ?? [])
|
||||
|
||||
// state for tracking hover on clear all button
|
||||
const [isClearHovered, setIsClearHovered] = useState(false)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
|
|
@ -403,6 +403,7 @@ export const SelectedFiles = (
|
|||
fileURI: uri,
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
state: { isOpened: false },
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -413,106 +414,96 @@ export const SelectedFiles = (
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center flex-wrap text-left relative'>
|
||||
<div className='flex items-center flex-wrap text-left relative gap-x-0.5 gap-y-1'>
|
||||
|
||||
{allSelections.map((selection, i) => {
|
||||
|
||||
const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
|
||||
const isThisSelectionOpened = (!!selection.selectionStr && selection.state.isOpened) //!!(selection.selectionStr && selectionIsOpened[i])
|
||||
const isThisSelectionAFile = selection.selectionStr === null
|
||||
const isThisSelectionProspective = i > selections.length - 1
|
||||
|
||||
const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}`
|
||||
|
||||
const selectionHTML = (<div key={thisKey} // container for `selectionSummary` and `selectionText`
|
||||
return <div // container for summarybox and code
|
||||
key={thisKey}
|
||||
className={`
|
||||
flex flex-col space-y-[1px]
|
||||
${isThisSelectionOpened ? 'w-full' : ''}
|
||||
`}
|
||||
>
|
||||
{/* selection summary */}
|
||||
<div // container for item and its delete button (if it's last)
|
||||
className='flex items-center gap-1 mr-0.5 my-0.5'
|
||||
{/* summarybox */}
|
||||
<div
|
||||
className={`
|
||||
flex items-center gap-0.5 relative
|
||||
px-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'}
|
||||
text-xs text-nowrap
|
||||
border rounded-sm ${isThisSelectionProspective
|
||||
? 'border-void-border-2'
|
||||
: isThisSelectionOpened
|
||||
? 'border-void-border-1 ring-1 ring-[#007FD4]'
|
||||
: 'border-void-border-1'
|
||||
}
|
||||
hover:border-void-border-1
|
||||
transition-all duration-150
|
||||
`}
|
||||
onClick={() => {
|
||||
if (type !== 'staging') return; // (never)
|
||||
if (isThisSelectionProspective) { // add prospective selection to selections
|
||||
setSelections([...selections, selection])
|
||||
} else if (isThisSelectionAFile) { // open files
|
||||
commandService.executeCommand('vscode.open', selection.fileURI, {
|
||||
preview: true,
|
||||
// preserveFocus: false,
|
||||
});
|
||||
} else { // show text
|
||||
|
||||
const selection = selections[i]
|
||||
const newSelection = { ...selection, state: { isOpened: !selection.state.isOpened } }
|
||||
const newSelections = [
|
||||
...selections.slice(0, i),
|
||||
newSelection,
|
||||
...selections.slice(i + 1)
|
||||
]
|
||||
setSelections(newSelections)
|
||||
|
||||
// setSelectionIsOpened(s => {
|
||||
// const newS = [...s]
|
||||
// newS[i] = !newS[i]
|
||||
// return newS
|
||||
// });
|
||||
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div // styled summary box
|
||||
className={`flex items-center gap-0.5 relative
|
||||
px-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
${isThisSelectionProspective ? 'bg-void-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'}
|
||||
text-xs text-nowrap
|
||||
border rounded-sm ${isClearHovered && !isThisSelectionProspective ? 'border-void-border-1' : 'border-void-border-2'} hover:border-void-border-1
|
||||
transition-all duration-150`}
|
||||
onClick={() => {
|
||||
if (isThisSelectionProspective) { // add prospective selection to selections
|
||||
if (type !== 'staging') return; // (never)
|
||||
setSelections([...selections, selection])
|
||||
{ // file name and range
|
||||
getBasename(selection.fileURI.fsPath)
|
||||
+ (isThisSelectionAFile ? '' : ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})`)
|
||||
}
|
||||
|
||||
} else if (isThisSelectionAFile) { // open files
|
||||
commandService.executeCommand('vscode.open', selection.fileURI, {
|
||||
preview: true,
|
||||
// preserveFocus: false,
|
||||
});
|
||||
} else { // show text
|
||||
setSelectionIsOpened(s => {
|
||||
const newS = [...s]
|
||||
newS[i] = !newS[i]
|
||||
return newS
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{/* file name */}
|
||||
{getBasename(selection.fileURI.fsPath)}
|
||||
{/* selection range */}
|
||||
{!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
|
||||
</span>
|
||||
|
||||
{/* X button */}
|
||||
{type === 'staging' && !isThisSelectionProspective &&
|
||||
<span
|
||||
className='cursor-pointer z-1'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't open/close selection
|
||||
if (type !== 'staging') return;
|
||||
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={10} className="stroke-[2]" />
|
||||
</span>}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* clear all selections button */}
|
||||
{/* {type !== 'staging' || selections.length === 0 || i !== selections.length - 1
|
||||
? null
|
||||
: <div className={`flex items-center ${isThisSelectionOpened ? 'w-full' : ''}`}>
|
||||
<div
|
||||
className='rounded-md'
|
||||
onMouseEnter={() => setIsClearHovered(true)}
|
||||
onMouseLeave={() => setIsClearHovered(false)}
|
||||
>
|
||||
<Delete
|
||||
size={16}
|
||||
className={`stroke-[1]
|
||||
stroke-void-fg-1
|
||||
fill-void-bg-3
|
||||
opacity-40
|
||||
hover:opacity-60
|
||||
transition-all duration-150
|
||||
cursor-pointer
|
||||
`}
|
||||
onClick={() => { setSelections([]) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
} */}
|
||||
{type === 'staging' && !isThisSelectionProspective ? // X button
|
||||
<IconX
|
||||
className='cursor-pointer z-1 stroke-[2]'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't open/close selection
|
||||
if (type !== 'staging') return;
|
||||
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
}}
|
||||
size={10}
|
||||
/>
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
{/* selection text */}
|
||||
{isThisSelectionOpened &&
|
||||
|
||||
{/* code box */}
|
||||
{isThisSelectionOpened ?
|
||||
<div
|
||||
className='w-full px-1 rounded-sm border-vscode-editor-border'
|
||||
className={`
|
||||
w-full px-1 rounded-sm border-vscode-editor-border
|
||||
${isThisSelectionOpened ? 'ring-1 ring-[#007FD4]' : ''}
|
||||
`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't focus input box
|
||||
}}
|
||||
|
|
@ -524,14 +515,9 @@ export const SelectedFiles = (
|
|||
showScrollbars={true}
|
||||
/>
|
||||
</div>
|
||||
: <></>
|
||||
}
|
||||
</div>)
|
||||
|
||||
return <Fragment key={thisKey}>
|
||||
{/* divider between `selections` and `prospectiveSelections` */}
|
||||
{/* {selections.length > 0 && i === selections.length && <div className='w-full'></div>} */}
|
||||
{selectionHTML}
|
||||
</Fragment>
|
||||
</div>
|
||||
|
||||
})}
|
||||
|
||||
|
|
@ -542,12 +528,13 @@ export const SelectedFiles = (
|
|||
}
|
||||
|
||||
|
||||
type ToolReusltToComponent = { [T in ToolName]: (props: { message: ToolMessage<T> }) => React.ReactNode }
|
||||
type ToolResultToComponent = { [T in ToolName]: (props: { message: ToolMessage<T> }) => React.ReactNode }
|
||||
interface ToolResultProps {
|
||||
actionTitle: string;
|
||||
actionParam: string;
|
||||
actionNumResults?: number;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const ToolResult = ({
|
||||
|
|
@ -555,26 +542,31 @@ const ToolResult = ({
|
|||
actionParam,
|
||||
actionNumResults,
|
||||
children,
|
||||
onClick,
|
||||
}: ToolResultProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const isDropdown = !!children
|
||||
const isClickable = !!isDropdown || !!onClick
|
||||
|
||||
return (
|
||||
<div className="mx-4 select-none">
|
||||
<div className="border border-void-border-3 rounded px-1 py-0.5 bg-void-bg-tool">
|
||||
<div className="border border-void-border-3 rounded px-2 py-1 bg-void-bg-2-alt overflow-hidden">
|
||||
<div
|
||||
className={`flex items-center min-h-[24px] ${isDropdown ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : 'mx-1'}`}
|
||||
onClick={() => children && setIsExpanded(!isExpanded)}
|
||||
className={`flex items-center min-h-[24px] ${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''} ${!isDropdown ? 'mx-1' : ''}`}
|
||||
onClick={() => {
|
||||
if (children) { setIsExpanded(v => !v); }
|
||||
if (onClick) { onClick(); }
|
||||
}}
|
||||
>
|
||||
{isDropdown && (
|
||||
<ChevronRight
|
||||
className={`text-void-fg-3 mr-0.5 h-5 w-5 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center flex-wrap gap-x-2 gap-y-0.5">
|
||||
<div className="flex items-center flex-nowrap whitespace-nowrap gap-x-2">
|
||||
<span className="text-void-fg-3">{actionTitle}</span>
|
||||
<span className="text-void-fg-4 text-xs italic">{`"`}{actionParam}{`"`}</span>
|
||||
<span className="text-void-fg-4 text-xs italic">{actionParam}</span>
|
||||
{actionNumResults !== undefined && (
|
||||
<span className="text-void-fg-4 text-xs">
|
||||
{`(`}{actionNumResults}{` result`}{actionNumResults !== 1 ? 's' : ''}{`)`}
|
||||
|
|
@ -583,7 +575,8 @@ const ToolResult = ({
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`mt-1 overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`}
|
||||
// the py-1 here makes sure all elements in the container have py-2 total. this makes a nice animation effect during transition.
|
||||
className={`overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'opacity-100 py-1' : 'max-h-0 opacity-0'}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
|
@ -594,98 +587,140 @@ const ToolResult = ({
|
|||
|
||||
|
||||
|
||||
const toolResultToComponent: ToolReusltToComponent = {
|
||||
'read_file': ({ message }) => (
|
||||
<ToolResult
|
||||
actionTitle="Read file"
|
||||
actionParam={getBasename(message.result.uri.fsPath)}
|
||||
/>
|
||||
),
|
||||
'list_dir': ({ message }) => (
|
||||
<ToolResult
|
||||
actionTitle="Inspected folder"
|
||||
actionParam={`${getBasename(message.result.rootURI.fsPath)}/`}
|
||||
actionNumResults={message.result.children?.length}
|
||||
>
|
||||
<div className="text-void-fg-2">
|
||||
{message.result.children?.map((item, i) => (
|
||||
<div key={i} className="pl-2 py-0.5 mb-1 bg-void-bg-1 rounded">
|
||||
{item.name}
|
||||
{item.isDirectory && '/'}
|
||||
</div>
|
||||
))}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="pl-2 text-void-fg-3 italic">
|
||||
{message.result.itemsRemaining} more items...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
),
|
||||
'pathname_search': ({ message }) => (
|
||||
<ToolResult
|
||||
actionTitle="Searched filename"
|
||||
actionParam={message.result.queryStr}
|
||||
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
|
||||
>
|
||||
<div className="text-void-fg-2">
|
||||
{Array.isArray(message.result.uris) ?
|
||||
message.result.uris.map((uri, i) => (
|
||||
<div key={i} className="pl-2 py-0.5 mb-1 bg-void-bg-1 rounded">
|
||||
<a
|
||||
href={uri.toString()}
|
||||
className="text-void-accent hover:underline"
|
||||
const toolResultToComponent: ToolResultToComponent = {
|
||||
'read_file': ({ message }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
return (
|
||||
<ToolResult
|
||||
actionTitle="Read file"
|
||||
actionParam={getBasename(message.result.uri.fsPath)}
|
||||
onClick={() => { commandService.executeCommand('vscode.open', message.result.uri, { preview: true }) }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
'list_dir': ({ message }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const explorerService = accessor.get('IExplorerService')
|
||||
// message.result.hasNextPage = true
|
||||
// message.result.itemsRemaining = 400
|
||||
return (
|
||||
<ToolResult
|
||||
actionTitle="Inspected folder"
|
||||
actionParam={`${getBasename(message.result.rootURI.fsPath)}/`}
|
||||
actionNumResults={message.result.children?.length}
|
||||
>
|
||||
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
|
||||
{message.result.children?.map((child, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
|
||||
onClick={() => {
|
||||
commandService.executeCommand('workbench.view.explorer');
|
||||
explorerService.select(child.uri, true);
|
||||
}}
|
||||
>
|
||||
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
|
||||
{`${child.name}${child.isDirectory ? '/' : ''}`}
|
||||
</div>
|
||||
))}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="italic">
|
||||
{message.result.itemsRemaining} more items...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
)
|
||||
},
|
||||
'pathname_search': ({ message }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
return (
|
||||
<ToolResult
|
||||
actionTitle="Searched filename"
|
||||
actionParam={`"${message.result.queryStr}"`}
|
||||
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
|
||||
>
|
||||
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
|
||||
{Array.isArray(message.result.uris) ?
|
||||
message.result.uris.map((uri, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
|
||||
onClick={() => {
|
||||
commandService.executeCommand('vscode.open', uri, { preview: true })
|
||||
}}
|
||||
>
|
||||
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
|
||||
{uri.fsPath.split('/').pop()}
|
||||
</a>
|
||||
</div>
|
||||
)) :
|
||||
<div className="">{message.result.uris}</div>
|
||||
}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="italic">
|
||||
More results available...
|
||||
</div>
|
||||
)) :
|
||||
<div className="pl-2">{message.result.uris}</div>
|
||||
}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="pl-2 text-void-fg-3 italic">
|
||||
More results available...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
),
|
||||
'search': ({ message }) => (
|
||||
<ToolResult
|
||||
actionTitle="Searched"
|
||||
actionParam={message.result.queryStr}
|
||||
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
|
||||
>
|
||||
<div className="text-void-fg-2">
|
||||
{typeof message.result.uris === 'string' ?
|
||||
message.result.uris :
|
||||
message.result.uris.map((uri, i) => (
|
||||
<div key={i} className="pl-2 py-0.5 mb-1 bg-void-bg-1 rounded">
|
||||
<a
|
||||
href={uri.toString()}
|
||||
className="text-void-accent hover:underline"
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
)
|
||||
},
|
||||
'search': ({ message }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
return (
|
||||
<ToolResult
|
||||
actionTitle="Searched"
|
||||
actionParam={`"${message.result.queryStr}"`}
|
||||
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
|
||||
>
|
||||
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
|
||||
{Array.isArray(message.result.uris) ?
|
||||
message.result.uris.map((uri, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
|
||||
onClick={() => {
|
||||
commandService.executeCommand('vscode.open', uri, { preview: true })
|
||||
}}
|
||||
>
|
||||
{uri.fsPath}
|
||||
</a>
|
||||
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
|
||||
{uri.fsPath.split('/').pop()}
|
||||
</div>
|
||||
)) :
|
||||
<div className="">{message.result.uris}</div>
|
||||
}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="italic">
|
||||
More results available...
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{message.result.hasNextPage && (
|
||||
<div className="pl-2 text-void-fg-3 italic">
|
||||
More results available...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</ToolResult>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
type ChatBubbleMode = 'display' | 'edit'
|
||||
const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => {
|
||||
const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx: number, isLoading?: boolean, }) => {
|
||||
|
||||
const role = chatMessage.role
|
||||
// Only show reasoning dropdown when there's actual content
|
||||
const reasoningStr = (chatMessage.role === 'assistant' && chatMessage.reasoning?.trim()) || null
|
||||
const hasReasoning = !!reasoningStr
|
||||
|
||||
const [isReasoningOpen, setIsReasoningOpen] = useState(false)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
|
@ -720,7 +755,6 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current
|
||||
if (canInitialize && shouldInitialize) {
|
||||
setStagingSelections(chatMessage.selections || [])
|
||||
|
||||
if (textAreaFnsRef.current)
|
||||
textAreaFnsRef.current.setValue(chatMessage.displayContent || '')
|
||||
|
||||
|
|
@ -750,7 +784,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
if (mode === 'display') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections || []} />
|
||||
{chatMessage.displayContent}
|
||||
<span className='px-0.5'>{chatMessage.displayContent}</span>
|
||||
</>
|
||||
}
|
||||
else if (mode === 'edit') {
|
||||
|
|
@ -806,7 +840,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
>
|
||||
<VoidInputBox2
|
||||
ref={setTextAreaRef}
|
||||
className='min-h-[81px] max-h-[500px] p-1'
|
||||
className='min-h-[81px] max-h-[500px] px-0.5'
|
||||
placeholder="Edit your message..."
|
||||
onChangeText={(text) => setIsDisabled(!text)}
|
||||
onFocus={() => {
|
||||
|
|
@ -829,10 +863,42 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
|
||||
const chatMessageLocation: ChatMessageLocation = {
|
||||
threadId: thread.id,
|
||||
messageIdx: messageIdx!,
|
||||
messageIdx: messageIdx,
|
||||
}
|
||||
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} chatMessageLocation={chatMessageLocation} />
|
||||
|
||||
const reasoningDropdown = hasReasoning ? (
|
||||
<div className="mx-4 select-none mt-2">
|
||||
<div className="border border-void-border-3 rounded px-1 py-0.5 bg-void-bg-tool">
|
||||
<div
|
||||
className="flex items-center min-h-[24px] cursor-pointer hover:brightness-125 transition-all duration-150"
|
||||
onClick={() => setIsReasoningOpen(!isReasoningOpen)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={`text-void-fg-3 mr-0.5 h-5 w-5 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isReasoningOpen ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
<div className="flex items-center flex-wrap gap-x-2 gap-y-0.5">
|
||||
<span className="text-void-fg-3">Reasoning</span>
|
||||
<span className="text-void-fg-4 text-xs italic">Model's step-by-step thinking</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`mt-1 overflow-hidden transition-all duration-200 ease-in-out ${isReasoningOpen ? 'max-h-[500px] opacity-100' : 'max-h-0 opacity-0'}`}
|
||||
>
|
||||
<div className="text-void-fg-2 p-2 bg-void-bg-1 rounded">
|
||||
<ChatMarkdownRender string={reasoningStr} chatMessageLocationForApply={chatMessageLocation} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
chatbubbleContents = (<>
|
||||
{/* Reasoning dropdown (conditional) */}
|
||||
{reasoningDropdown}
|
||||
{/* Main content */}
|
||||
<ChatMarkdownRender string={chatMessage.content ?? ''} chatMessageLocationForApply={chatMessageLocation} />
|
||||
</>)
|
||||
}
|
||||
else if (role === 'tool') {
|
||||
|
||||
|
|
@ -849,7 +915,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
className={`
|
||||
relative
|
||||
${mode === 'edit' ? 'px-2 w-full max-w-full'
|
||||
: role === 'user' ? `my-0.5 px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
|
||||
: role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
|
||||
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
|
||||
}
|
||||
`}
|
||||
|
|
@ -862,7 +928,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM
|
|||
text-left rounded-lg
|
||||
max-w-full
|
||||
${mode === 'edit' ? ''
|
||||
: role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1 overflow-x-auto'
|
||||
: role === 'user' ? 'p-2 flex flex-col gap-1 bg-void-bg-1 text-void-fg-1 overflow-x-auto'
|
||||
: role === 'assistant' ? 'px-2 overflow-x-auto' : ''
|
||||
}
|
||||
`}
|
||||
|
|
@ -926,14 +992,15 @@ export const SidebarChat = () => {
|
|||
const currentThread = chatThreadsService.getCurrentThread()
|
||||
const previousMessages = currentThread?.messages ?? []
|
||||
|
||||
const selections = chatThreadsService.getCurrentThread().state.stagingSelections
|
||||
const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadStagingSelections(s) }
|
||||
const selections = currentThread.state.stagingSelections
|
||||
const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadState({ stagingSelections: s }) }
|
||||
|
||||
// stream state
|
||||
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
|
||||
const isStreaming = !!currThreadStreamState?.streamingToken
|
||||
const latestError = currThreadStreamState?.error
|
||||
const messageSoFar = currThreadStreamState?.messageSoFar
|
||||
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
|
||||
|
||||
// ----- SIDEBAR CHAT state (local) -----
|
||||
|
||||
|
|
@ -982,13 +1049,27 @@ export const SidebarChat = () => {
|
|||
}, [isHistoryOpen, currentThread.id])
|
||||
|
||||
|
||||
const prevMessagesHTML = useMemo(() => {
|
||||
const pastMessagesHTML = useMemo(() => {
|
||||
return previousMessages.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} messageIdx={i} />
|
||||
<ChatBubble key={getChatBubbleId(currentThread.id, i)} chatMessage={message} messageIdx={i} />
|
||||
)
|
||||
}, [previousMessages])
|
||||
|
||||
|
||||
const streamingChatIdx = pastMessagesHTML.length
|
||||
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ?
|
||||
<ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)}
|
||||
messageIdx={streamingChatIdx} chatMessage={{
|
||||
role: 'assistant',
|
||||
content: messageSoFar ?? null,
|
||||
reasoning: reasoningSoFar ?? null,
|
||||
}}
|
||||
isLoading={isStreaming}
|
||||
/> : null
|
||||
|
||||
const allMessagesHTML = [...pastMessagesHTML, currStreamingMessageHTML]
|
||||
|
||||
|
||||
const threadSelector = <div ref={historyRef}
|
||||
className={`w-full h-auto ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}
|
||||
>
|
||||
|
|
@ -1006,15 +1087,12 @@ export const SidebarChat = () => {
|
|||
overflow-x-hidden
|
||||
overflow-y-auto
|
||||
py-4
|
||||
${prevMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''}
|
||||
${pastMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''}
|
||||
`}
|
||||
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights
|
||||
>
|
||||
{/* previous messages */}
|
||||
{prevMessagesHTML}
|
||||
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageSoFar ?? '', displayContent: messageSoFar || null }} isLoading={isStreaming} />
|
||||
{allMessagesHTML}
|
||||
|
||||
|
||||
{/* error message */}
|
||||
|
|
@ -1049,14 +1127,14 @@ export const SidebarChat = () => {
|
|||
isStreaming={isStreaming}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
showProspectiveSelections={prevMessagesHTML.length === 0}
|
||||
showProspectiveSelections={pastMessagesHTML.length === 0}
|
||||
selections={selections}
|
||||
setSelections={setSelections}
|
||||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||
featureName="Ctrl+L"
|
||||
>
|
||||
<VoidInputBox2
|
||||
className='min-h-[81px] p-1'
|
||||
className='min-h-[81px] px-0.5'
|
||||
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
|
||||
onChangeText={onChangeText}
|
||||
onKeyDown={onKeyDown}
|
||||
|
|
|
|||
|
|
@ -310,6 +310,7 @@ export const VoidCustomDropdownBox = <T extends any>({
|
|||
selectedOption,
|
||||
onChangeOption,
|
||||
getOptionDropdownName,
|
||||
getOptionDropdownDetail,
|
||||
getOptionDisplayName,
|
||||
getOptionsEqual,
|
||||
className,
|
||||
|
|
@ -321,6 +322,7 @@ export const VoidCustomDropdownBox = <T extends any>({
|
|||
selectedOption: T | undefined;
|
||||
onChangeOption: (newValue: T) => void;
|
||||
getOptionDropdownName: (option: T) => string;
|
||||
getOptionDropdownDetail?: (option: T) => string;
|
||||
getOptionDisplayName: (option: T) => string;
|
||||
getOptionsEqual: (a: T, b: T) => boolean;
|
||||
className?: string;
|
||||
|
|
@ -420,12 +422,21 @@ export const VoidCustomDropdownBox = <T extends any>({
|
|||
className="opacity-0 pointer-events-none absolute -left-[999999px] -top-[999999px] flex flex-col"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div key={getOptionDropdownName(option)} className="flex items-center whitespace-nowrap">
|
||||
<div className="w-4" />
|
||||
<span className="px-2">{getOptionDropdownName(option)}</span>
|
||||
</div>
|
||||
))}
|
||||
{options.map((option) => {
|
||||
const optionName = getOptionDropdownName(option);
|
||||
const optionDetail = getOptionDropdownDetail?.(option) || '';
|
||||
|
||||
return (
|
||||
<div key={optionName + optionDetail} className="flex items-center whitespace-nowrap">
|
||||
<div className="w-4" />
|
||||
<span className="flex justify-between w-full">
|
||||
<span>{optionName}</span>
|
||||
<span>{optionDetail}</span>
|
||||
<span>______</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Select Button */}
|
||||
|
|
@ -473,6 +484,7 @@ export const VoidCustomDropdownBox = <T extends any>({
|
|||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionDropdownName(option);
|
||||
const optionDetail = getOptionDropdownDetail?.(option) || '';
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -500,7 +512,10 @@ export const VoidCustomDropdownBox = <T extends any>({
|
|||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span>{optionName}</span>
|
||||
<span className="flex justify-between w-full">
|
||||
<span>{optionName}</span>
|
||||
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
|
||||
import { ThreadStreamState,IChatThreadService, ThreadsState } from '../../../../common/chatThreadService.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSidebarState } from '../../../sidebarStateService.js'
|
||||
|
|
@ -15,6 +15,7 @@ import { VoidQuickEditState } from '../../../quickEditStateService.js'
|
|||
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
|
||||
|
||||
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
|
||||
import { IExplorerService } from '../../../../../../../workbench/contrib/files/browser/files.js'
|
||||
import { IModelService } from '../../../../../../../editor/common/services/model.js';
|
||||
import { IClipboardService } from '../../../../../../../platform/clipboard/common/clipboardService.js';
|
||||
import { IContextViewService, IContextMenuService } from '../../../../../../../platform/contextview/browser/contextView.js';
|
||||
|
|
@ -28,7 +29,6 @@ import { IEditCodeService, URIStreamState } from '../../../editCodeService.js';
|
|||
import { IVoidUriStateService } from '../../../voidUriStateService.js';
|
||||
import { IQuickEditStateService } from '../../../quickEditStateService.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IChatThreadService } from '../../../chatThreadService.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
|
||||
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
|
||||
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
|
||||
|
|
@ -226,6 +226,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
|||
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
|
||||
IKeybindingService: accessor.get(IKeybindingService),
|
||||
|
||||
IExplorerService: accessor.get(IExplorerService),
|
||||
IEnvironmentService: accessor.get(IEnvironmentService),
|
||||
IConfigurationService: accessor.get(IConfigurationService),
|
||||
IPathService: accessor.get(IPathService),
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
|
|||
selectedOption={selectedOption}
|
||||
onChangeOption={onChangeOption}
|
||||
getOptionDisplayName={(option) => option.selection.modelName}
|
||||
getOptionDropdownName={(option) => option.name}
|
||||
getOptionDropdownName={(option) => option.selection.modelName}
|
||||
getOptionDropdownDetail={(option) => option.selection.providerName }
|
||||
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
|
||||
className='text-xs text-void-fg-3 px-1'
|
||||
matchInputWidth={false}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ module.exports = {
|
|||
"void-border-1": "var(--vscode-commandCenter-activeBorder)",
|
||||
"void-border-2": "var(--vscode-commandCenter-border)",
|
||||
"void-border-3": "var(--vscode-commandCenter-inactiveBorder)",
|
||||
"void-border-3": "var(--vscode-settings-sashBorder)",
|
||||
"void-border-4": "var(--vscode-editorGroup-border)",
|
||||
|
||||
|
||||
vscode: {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ 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';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js';
|
||||
// import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
// import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -24,22 +24,22 @@ class SearchReplaceService extends Disposable implements ISearchReplaceService {
|
|||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
|
||||
|
||||
constructor(
|
||||
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
// @ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
send(params: Omit<ServiceSendLLMMessageParams, 'onText'> & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) {
|
||||
this.llmMessageService.sendLLMMessage({
|
||||
...params as ServiceSendLLMMessageParams,
|
||||
onText: (p) => {
|
||||
const { retry } = params.onText(p)
|
||||
if (retry) {
|
||||
// send(params: ServiceSendLLMMessageParams & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) {
|
||||
// this.llmMessageService.sendLLMMessage({
|
||||
// ...params as ServiceSendLLMMessageParams,
|
||||
// onText: (p) => {
|
||||
// const { retry } = params.onText(p)
|
||||
// if (retry) {
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
|
|||
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { StagingSelectionItem, IChatThreadService } from './chatThreadService.js';
|
||||
import { StagingSelectionItem, IChatThreadService } from '../common/chatThreadService.js';
|
||||
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
|
|
@ -124,11 +124,13 @@ registerAction2(class extends Action2 {
|
|||
fileURI: model.uri,
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
state: { isOpened: false, }
|
||||
} : {
|
||||
type: 'Selection',
|
||||
fileURI: model.uri,
|
||||
selectionStr: selectionStr,
|
||||
range: selectionRange,
|
||||
state: { isOpened: true, }
|
||||
}
|
||||
|
||||
// update the staging selections
|
||||
|
|
@ -141,13 +143,16 @@ registerAction2(class extends Action2 {
|
|||
let setSelections = (s: StagingSelectionItem[]) => { }
|
||||
|
||||
if (focusedMessageIdx === undefined) {
|
||||
selections = chatThreadService.getCurrentThreadStagingSelections()
|
||||
setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadStagingSelections(s)
|
||||
selections = chatThreadService.getCurrentThreadState().stagingSelections
|
||||
setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadState({ stagingSelections: s })
|
||||
} else {
|
||||
selections = chatThreadService.getCurrentMessageState(focusedMessageIdx).stagingSelections
|
||||
setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
|
||||
}
|
||||
|
||||
// close all selections besides the new one
|
||||
selections = selections.map(s => ({ ...s, state: { ...s.state, isOpened: false } }))
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection)
|
||||
if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ import './sidebarStateService.js'
|
|||
// register quick edit (Ctrl+K)
|
||||
import './quickEditActions.js'
|
||||
|
||||
// register Thread History
|
||||
import './chatThreadService.js'
|
||||
|
||||
// register Autocomplete
|
||||
import './autocompleteService.js'
|
||||
|
|
@ -56,3 +54,7 @@ import '../common/voidUpdateService.js'
|
|||
|
||||
// tools
|
||||
import '../common/toolsService.js'
|
||||
|
||||
// register Thread History
|
||||
import '../common/chatThreadService.js'
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ 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 '../common/llmMessageService.js';
|
||||
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles, chat_selectionsString } from './prompt/prompts.js';
|
||||
import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from '../common/toolsService.js';
|
||||
import { toLLMChatMessage } from '../common/llmMessageTypes.js';
|
||||
import { ILLMMessageService } from './llmMessageService.js';
|
||||
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles, chat_selectionsString } from '../browser/prompt/prompts.js';
|
||||
import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from './toolsService.js';
|
||||
import { toLLMChatMessage } from './llmMessageTypes.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IVoidFileService } from '../common/voidFileService.js';
|
||||
import { IVoidFileService } from './voidFileService.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
|
||||
|
||||
|
|
@ -36,6 +36,9 @@ export type CodeSelection = {
|
|||
fileURI: URI;
|
||||
selectionStr: string;
|
||||
range: IRange;
|
||||
state: {
|
||||
isOpened: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type FileSelection = {
|
||||
|
|
@ -43,6 +46,9 @@ export type FileSelection = {
|
|||
fileURI: URI;
|
||||
selectionStr: null;
|
||||
range: null;
|
||||
state: {
|
||||
isOpened: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type StagingSelectionItem = CodeSelection | FileSelection
|
||||
|
|
@ -60,11 +66,7 @@ export type ToolMessage<T extends ToolName> = {
|
|||
|
||||
// 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 =
|
||||
| {
|
||||
role: 'system';
|
||||
content: string;
|
||||
displayContent?: undefined;
|
||||
} | {
|
||||
{
|
||||
role: 'user';
|
||||
content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
|
||||
|
|
@ -76,7 +78,7 @@ export type ChatMessage =
|
|||
} | {
|
||||
role: 'assistant';
|
||||
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
|
||||
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
|
||||
reasoning: string | null; // reasoning from the LLM, used for step-by-step thinking
|
||||
}
|
||||
| ToolMessage<ToolName>
|
||||
|
||||
|
|
@ -98,14 +100,18 @@ export type ChatThreads = {
|
|||
state: {
|
||||
stagingSelections: StagingSelectionItem[];
|
||||
focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none)
|
||||
isCheckedOfSelectionId: { [selectionId: string]: boolean };
|
||||
isCheckedOfSelectionId: { [selectionId: string]: boolean }; // TODO
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
type ThreadType = ChatThreads[string]
|
||||
|
||||
const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {} }
|
||||
const defaultThreadState: ThreadType['state'] = {
|
||||
stagingSelections: [],
|
||||
focusedMessageIdx: undefined,
|
||||
isCheckedOfSelectionId: {}
|
||||
}
|
||||
|
||||
export type ThreadsState = {
|
||||
allThreads: ChatThreads;
|
||||
|
|
@ -116,6 +122,7 @@ export type ThreadStreamState = {
|
|||
[threadId: string]: undefined | {
|
||||
error?: { message: string, fullError: Error | null, };
|
||||
messageSoFar?: string;
|
||||
reasoningSoFar?: string;
|
||||
streamingToken?: string;
|
||||
}
|
||||
}
|
||||
|
|
@ -128,19 +135,12 @@ const newThreadObject = () => {
|
|||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
state: {
|
||||
stagingSelections: [],
|
||||
focusedMessageIdx: undefined,
|
||||
isCheckedOfSelectionId: {}
|
||||
},
|
||||
state: defaultThreadState,
|
||||
|
||||
} satisfies ChatThreads[string]
|
||||
}
|
||||
|
||||
const THREAD_VERSION_KEY = 'void.chatThreadVersion'
|
||||
const LATEST_THREAD_VERSION = 'v2'
|
||||
|
||||
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
|
||||
export const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
|
||||
|
||||
|
||||
type ChatMode = 'agent' | 'chat'
|
||||
|
|
@ -166,8 +166,8 @@ export interface IChatThreadService {
|
|||
// exposed getters/setters
|
||||
getCurrentMessageState: (messageIdx: number) => UserMessageState
|
||||
setCurrentMessageState: (messageIdx: number, newState: Partial<UserMessageState>) => void
|
||||
getCurrentThreadStagingSelections: () => StagingSelectionItem[]
|
||||
setCurrentThreadStagingSelections: (stagingSelections: StagingSelectionItem[]) => void
|
||||
getCurrentThreadState: () => ThreadType['state']
|
||||
setCurrentThreadState: (newState: Partial<ThreadType['state']>) => void
|
||||
|
||||
|
||||
// call to edit a message
|
||||
|
|
@ -203,18 +203,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
) {
|
||||
super()
|
||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
|
||||
|
||||
const oldVersionNum = this._storageService.get(THREAD_VERSION_KEY, StorageScope.APPLICATION)
|
||||
const readThreads = this._readAllThreads() || {}
|
||||
|
||||
|
||||
const readThreads = this._readAllThreads()
|
||||
const updatedThreads = this._updatedThreadsToVersion(readThreads, oldVersionNum)
|
||||
|
||||
if (updatedThreads !== null) {
|
||||
this._storeAllThreads(updatedThreads)
|
||||
}
|
||||
|
||||
const allThreads = updatedThreads ?? readThreads
|
||||
const allThreads = readThreads
|
||||
this.state = {
|
||||
allThreads: allThreads,
|
||||
currentThreadId: null as unknown as string, // gets set in startNewThread()
|
||||
|
|
@ -222,65 +215,37 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// always be in a thread
|
||||
this.openNewThread()
|
||||
|
||||
this._storageService.store(THREAD_VERSION_KEY, LATEST_THREAD_VERSION, StorageScope.APPLICATION, StorageTarget.USER)
|
||||
|
||||
}
|
||||
|
||||
|
||||
private _readAllThreads(): ChatThreads {
|
||||
const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
|
||||
const threads: ChatThreads = threadsStr ? JSON.parse(threadsStr) : {}
|
||||
|
||||
return threads
|
||||
}
|
||||
|
||||
|
||||
// returns if should update
|
||||
private _updatedThreadsToVersion(oldThreadsObject: any, oldVersion: string | undefined): ChatThreads | null {
|
||||
|
||||
if (!oldVersion) {
|
||||
|
||||
// unknown, just reset chat?
|
||||
return null
|
||||
}
|
||||
|
||||
/** v1 -> v2
|
||||
- threads.state.currentStagingSelections: CodeStagingSelection[] | null;
|
||||
+ thread[threadIdx].state
|
||||
+ message.state
|
||||
+ chatMessage.staging: StagingInfo | null
|
||||
*/
|
||||
else if (oldVersion === 'v1') {
|
||||
const threads = oldThreadsObject as Omit<ChatThreads, 'staging' | 'focusedMessageIdx'>
|
||||
// update the threads
|
||||
for (const thread of Object.values(threads)) {
|
||||
if (!thread.state) {
|
||||
thread.state = defaultThreadState
|
||||
}
|
||||
for (const chatMessage of Object.values(thread.messages)) {
|
||||
if (chatMessage.role === 'user' && !chatMessage.state) {
|
||||
chatMessage.state = defaultMessageState
|
||||
}
|
||||
}
|
||||
// !!! this is important for properly restoring URIs from storage
|
||||
private _convertThreadDataFromStorage(threadsStr: string): ChatThreads {
|
||||
return JSON.parse(threadsStr, (key, value) => {
|
||||
if (value && typeof value === 'object' && value.$mid === 1) { //$mid is the MarshalledId. $mid === 1 means it is a URI
|
||||
return URI.from(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
// push the update
|
||||
return threads
|
||||
}
|
||||
else if (oldVersion === 'v2') {
|
||||
private _readAllThreads(): ChatThreads | null {
|
||||
const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION);
|
||||
if (!threadsStr) {
|
||||
return null
|
||||
}
|
||||
|
||||
// up to date
|
||||
return null
|
||||
|
||||
return this._convertThreadDataFromStorage(threadsStr);
|
||||
}
|
||||
|
||||
private _storeAllThreads(threads: ChatThreads) {
|
||||
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
|
||||
const serializedThreads = JSON.stringify(threads);
|
||||
this._storageService.store(
|
||||
THREAD_STORAGE_KEY,
|
||||
serializedThreads,
|
||||
StorageScope.APPLICATION,
|
||||
StorageTarget.USER
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// this should be the only place this.state = ... appears besides constructor
|
||||
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
|
||||
this.state = {
|
||||
|
|
@ -313,10 +278,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// ---------- streaming ----------
|
||||
|
||||
private _finishStreamingTextMessage = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => {
|
||||
private _finishStreamingTextMessage = (threadId: string, options: { content: string, reasoning?: string }, error?: { message: string, fullError: Error | null }) => {
|
||||
// add assistant's message to chat history, and clear selection
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content, displayContent: content || null })
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: options.content, reasoning: options.reasoning || null })
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error })
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -331,8 +296,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
|
||||
// get prev and curr selections before clearing the message
|
||||
const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx)
|
||||
const currSelns = thread.messages[messageIdx].selections || []
|
||||
const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx) // selections for previous messages
|
||||
const currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message
|
||||
|
||||
// clear messages up to the index
|
||||
const slicedMessages = thread.messages.slice(0, messageIdx)
|
||||
|
|
@ -414,17 +379,17 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
tools: tools,
|
||||
|
||||
onText: ({ fullText }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText })
|
||||
onText: ({ fullText, fullReasoning }) => {
|
||||
this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning })
|
||||
},
|
||||
onFinalMessage: async ({ fullText, toolCalls }) => {
|
||||
onFinalMessage: async ({ fullText, toolCalls, fullReasoning }) => {
|
||||
|
||||
if ((toolCalls?.length ?? 0) === 0) {
|
||||
this._finishStreamingTextMessage(threadId, fullText)
|
||||
this._finishStreamingTextMessage(threadId, { content: fullText, reasoning: fullReasoning })
|
||||
}
|
||||
else {
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, displayContent: fullText })
|
||||
this._setStreamState(threadId, { messageSoFar: undefined }) // clear streaming message
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning || null })
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) // clear streaming message
|
||||
for (const tool of toolCalls ?? []) {
|
||||
const toolName = tool.name as ToolName
|
||||
|
||||
|
|
@ -458,7 +423,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
res_()
|
||||
},
|
||||
onError: (error) => {
|
||||
this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '', error)
|
||||
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar }, error)
|
||||
res_()
|
||||
},
|
||||
})
|
||||
|
|
@ -476,7 +443,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
cancelStreaming(threadId: string) {
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '')
|
||||
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar })
|
||||
}
|
||||
|
||||
dismissStreamError(threadId: string): void {
|
||||
|
|
@ -489,7 +458,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
getCurrentThread(): ChatThreads[string] {
|
||||
const state = this.state
|
||||
return state.allThreads[state.currentThreadId]
|
||||
const thread = state.allThreads[state.currentThreadId]
|
||||
return thread
|
||||
}
|
||||
|
||||
getFocusedMessageIdx() {
|
||||
|
|
@ -626,12 +596,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
}
|
||||
|
||||
getCurrentThreadStagingSelections = () => {
|
||||
return this.getCurrentThread().state.stagingSelections
|
||||
getCurrentThreadState = () => {
|
||||
const currentThread = this.getCurrentThread()
|
||||
return currentThread.state
|
||||
}
|
||||
|
||||
setCurrentThreadStagingSelections = (stagingSelections: StagingSelectionItem[]) => {
|
||||
this._setCurrentThreadState({ stagingSelections })
|
||||
setCurrentThreadState = (newState: Partial<ThreadType['state']>) => {
|
||||
this._setCurrentThreadState(newState)
|
||||
}
|
||||
|
||||
// gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected)
|
||||
|
|
@ -652,4 +623,3 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
|
||||
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);
|
||||
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChatMessage } from '../browser/chatThreadService.js'
|
||||
import { ChatMessage } from './chatThreadService.js'
|
||||
import { InternalToolInfo, ToolName } from './toolsService.js'
|
||||
import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
|
@ -45,14 +45,14 @@ export type ToolCallType = {
|
|||
}
|
||||
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string; newReasoning: string; fullReasoning: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[] }) => void // id is tool_use_id
|
||||
export type OnText = (p: { fullText: string; fullReasoning: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[], fullReasoning?: string }) => void // id is tool_use_id
|
||||
export type OnError = (p: { message: string, fullError: Error | null }) => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
|
||||
export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => {
|
||||
if (c.role === 'system' || c.role === 'user') {
|
||||
if (c.role === 'user') {
|
||||
return { role: c.role, content: c.content || '(empty message)' }
|
||||
}
|
||||
else if (c.role === 'assistant')
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ export type ToolCallReturnType = {
|
|||
}
|
||||
|
||||
type DirectoryItem = {
|
||||
uri: URI;
|
||||
name: string;
|
||||
isDirectory: boolean;
|
||||
isSymbolicLink: boolean;
|
||||
|
|
@ -142,8 +143,9 @@ const computeDirectoryResult = async (
|
|||
|
||||
const children: DirectoryItem[] = listChildren.map(child => ({
|
||||
name: child.name,
|
||||
uri: child.resource,
|
||||
isDirectory: child.isDirectory,
|
||||
isSymbolicLink: child.isSymbolicLink || false
|
||||
isSymbolicLink: child.isSymbolicLink
|
||||
}));
|
||||
|
||||
const hasNextPage = (originalChildrenLength - 1) > toChildIdx;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
|
|||
options?: { doNotApplyEffects?: true }
|
||||
) => Promise<void>;
|
||||
|
||||
type SetGlobalSettingFn = <T extends GlobalSettingName, >(settingName: T, newVal: GlobalSettings[T]) => void;
|
||||
type SetGlobalSettingFn = <T extends GlobalSettingName>(settingName: T, newVal: GlobalSettings[T]) => void;
|
||||
|
||||
export type ModelOption = { name: string, selection: ModelSelection }
|
||||
|
||||
|
|
@ -49,6 +49,8 @@ export interface IVoidSettingsService {
|
|||
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
|
||||
readonly waitForInitState: Promise<void>;
|
||||
|
||||
readAndInitializeState: (providedState?: VoidSettingsState) => Promise<void>;
|
||||
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
|
|
@ -168,6 +170,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
state: VoidSettingsState;
|
||||
|
||||
private readonly _resolver: () => void
|
||||
waitForInitState: Promise<void> // await this if you need a valid state initially
|
||||
|
||||
constructor(
|
||||
|
|
@ -181,56 +185,57 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
// at the start, we haven't read the partial config yet, but we need to set state to something
|
||||
this.state = defaultState()
|
||||
|
||||
let resolver: () => void = () => { }
|
||||
this.waitForInitState = new Promise((res, rej) => resolver = res)
|
||||
this._resolver = resolver
|
||||
|
||||
// read and update the actual state immediately
|
||||
this._readState().then(readS => {
|
||||
this.readAndInitializeState()
|
||||
}
|
||||
|
||||
// the stored data structure might be outdated, so we need to update it here (can do a more general solution later when we need to)
|
||||
const newSettingsOfProvider = {
|
||||
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
|
||||
...{ deepseek: defaultSettingsOfProvider.deepseek },
|
||||
async readAndInitializeState(providedState?: VoidSettingsState) {
|
||||
// If providedState is given, use it instead of reading from storage
|
||||
const readS = providedState || await this._readState();
|
||||
|
||||
// A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS)
|
||||
...{ xAI: defaultSettingsOfProvider.xAI },
|
||||
// the stored data structure might be outdated, so we need to update it here
|
||||
const newSettingsOfProvider = {
|
||||
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
|
||||
...{ deepseek: defaultSettingsOfProvider.deepseek },
|
||||
|
||||
// A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS)
|
||||
...{ vLLM: defaultSettingsOfProvider.vLLM },
|
||||
// A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS)
|
||||
...{ xAI: defaultSettingsOfProvider.xAI },
|
||||
|
||||
// A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS)
|
||||
...{ vLLM: defaultSettingsOfProvider.vLLM },
|
||||
|
||||
...readS.settingsOfProvider,
|
||||
...readS.settingsOfProvider,
|
||||
|
||||
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
|
||||
gemini: {
|
||||
...readS.settingsOfProvider.gemini,
|
||||
models: [
|
||||
...readS.settingsOfProvider.gemini.models,
|
||||
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
|
||||
]
|
||||
}
|
||||
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
|
||||
gemini: {
|
||||
...readS.settingsOfProvider.gemini,
|
||||
models: [
|
||||
...readS.settingsOfProvider.gemini.models,
|
||||
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const newModelSelectionOfFeature = {
|
||||
// A HACK BECAUSE WE ADDED FastApply
|
||||
...{ 'Apply': null },
|
||||
...readS.modelSelectionOfFeature,
|
||||
}
|
||||
const newModelSelectionOfFeature = {
|
||||
// A HACK BECAUSE WE ADDED FastApply
|
||||
...{ 'Apply': null },
|
||||
...readS.modelSelectionOfFeature,
|
||||
};
|
||||
|
||||
readS = {
|
||||
...readS,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
}
|
||||
const finalState = {
|
||||
...readS,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
};
|
||||
|
||||
this.state = _validatedState(readS)
|
||||
|
||||
resolver()
|
||||
this._onDidChangeState.fire()
|
||||
})
|
||||
this.state = _validatedState(finalState);
|
||||
|
||||
|
||||
this._resolver();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI, { ClientOptions } from 'openai';
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { Ollama } from 'ollama';
|
||||
import OpenAI, { ClientOptions } from 'openai';
|
||||
|
||||
import { Model as OpenAIModel } from 'openai/resources/models.js';
|
||||
import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js';
|
||||
import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js';
|
||||
import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/llmMessageTypes.js';
|
||||
import { InternalToolInfo, isAToolName } from '../../common/toolsService.js';
|
||||
import { defaultProviderSettings, displayInfoOfProviderName, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
|
||||
import { extractReasoningFromText } from '../../browser/helpers/extractCodeFromResult.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ type ModelOptions = {
|
|||
supportsReasoningOutput: false | {
|
||||
// you are allowed to not include openSourceThinkTags if it's not open source (no such cases as of writing)
|
||||
// if it's open source, put the think tags here so we parse them out in e.g. ollama
|
||||
openSourceThinkTags?: [string, string]
|
||||
readonly openSourceThinkTags?: [string, string]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -651,9 +651,9 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError
|
|||
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
|
||||
if (!supportsFIM) {
|
||||
if (modelName === modelName_)
|
||||
onFinalMessage({ fullText: `Model ${modelName} does not support FIM.` })
|
||||
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
|
||||
else
|
||||
onFinalMessage({ fullText: `Model ${modelName_} (${modelName}) does not support FIM.` })
|
||||
onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null })
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -687,7 +687,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
|||
supportsReasoningOutput,
|
||||
supportsSystemMessage,
|
||||
supportsTools,
|
||||
maxOutputTokens,
|
||||
// maxOutputTokens, right now we are ignoring this
|
||||
} = getModelCapabilities(providerName, modelName_)
|
||||
|
||||
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, })
|
||||
|
|
@ -696,16 +696,19 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
|||
const includeInPayload = supportsReasoningOutput ? modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.input?.includeInPayload || {} : {}
|
||||
|
||||
const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {}
|
||||
const maxTokensObj = maxOutputTokens ? { max_tokens: maxOutputTokens } : {}
|
||||
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj, ...maxTokensObj }
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj, }
|
||||
|
||||
const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.output ?? {}
|
||||
if (needsManualReasoningParse && supportsReasoningOutput && supportsReasoningOutput.openSourceThinkTags)
|
||||
onText = extractReasoningFromText(onText, supportsReasoningOutput.openSourceThinkTags)
|
||||
|
||||
let fullReasoning = ''
|
||||
let fullText = ''
|
||||
const manuallyParseReasoning = needsManualReasoningParse && supportsReasoningOutput && supportsReasoningOutput.openSourceThinkTags
|
||||
if (manuallyParseReasoning) {
|
||||
onText = extractReasoningOnTextWrapper(onText, supportsReasoningOutput.openSourceThinkTags)
|
||||
}
|
||||
|
||||
|
||||
let fullReasoningSoFar = ''
|
||||
let fullTextSoFar = ''
|
||||
const toolCallOfIndex: ToolCallOfIndex = {}
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
|
|
@ -723,19 +726,31 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
|||
}
|
||||
// message
|
||||
const newText = chunk.choices[0]?.delta?.content ?? ''
|
||||
fullText += newText
|
||||
fullTextSoFar += newText
|
||||
|
||||
// reasoning
|
||||
let newReasoning = ''
|
||||
if (nameOfReasoningFieldInDelta) {
|
||||
// @ts-ignore
|
||||
newReasoning = (chunk.choices[0]?.delta?.[nameOfReasoningFieldInDelta] || '') + ''
|
||||
fullReasoning += newReasoning
|
||||
fullReasoningSoFar += newReasoning
|
||||
}
|
||||
|
||||
onText({ newText, fullText, newReasoning, fullReasoning })
|
||||
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
}
|
||||
// on final
|
||||
const toolCalls = toolCallsFrom_OpenAICompat(toolCallOfIndex)
|
||||
if (!fullTextSoFar && !fullReasoningSoFar && toolCalls.length === 0) {
|
||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
}
|
||||
else {
|
||||
if (manuallyParseReasoning) {
|
||||
const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, supportsReasoningOutput.openSourceThinkTags)
|
||||
onFinalMessage({ fullText, fullReasoning, toolCalls });
|
||||
} else {
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls });
|
||||
}
|
||||
}
|
||||
onFinalMessage({ fullText, toolCalls: toolCallsFrom_OpenAICompat(toolCallOfIndex) });
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
|
|
@ -797,7 +812,7 @@ const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[
|
|||
}).filter(t => !!t)
|
||||
}
|
||||
|
||||
const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
|
||||
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
|
||||
const {
|
||||
// supportsReasoning: modelSupportsReasoning,
|
||||
modelName,
|
||||
|
|
@ -822,7 +837,7 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM
|
|||
})
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
onText({ newText, fullText, newReasoning: '', fullReasoning: '' })
|
||||
onText({ fullText, fullReasoning: '' })
|
||||
})
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (response) => {
|
||||
|
|
|
|||
|
|
@ -63,22 +63,23 @@ export const sendLLMMessage = ({
|
|||
_fullTextSoFar = fullText
|
||||
}
|
||||
|
||||
const onFinalMessage: OnFinalMessage = ({ fullText, toolCalls }) => {
|
||||
const onFinalMessage: OnFinalMessage = (params) => {
|
||||
const { fullText, fullReasoning } = params
|
||||
if (_didAbort) return
|
||||
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
onFinalMessage_({ fullText, toolCalls })
|
||||
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, reasoningLength: fullReasoning?.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
onFinalMessage_(params)
|
||||
}
|
||||
|
||||
const onError: OnError = ({ message: error, fullError }) => {
|
||||
const onError: OnError = ({ message: errorMessage, fullError }) => {
|
||||
if (_didAbort) return
|
||||
console.error('sendLLMMessage onError:', error)
|
||||
console.error('sendLLMMessage onError:', errorMessage)
|
||||
|
||||
// handle failed to fetch errors, which give 0 information by design
|
||||
if (error === 'TypeError: fetch failed')
|
||||
error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void's Settings, or your local model provider like Ollama is powered off.`
|
||||
if (errorMessage === 'TypeError: fetch failed')
|
||||
errorMessage = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void's Settings, or your local model provider like Ollama is powered off.`
|
||||
|
||||
captureLLMEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ message: error, fullError })
|
||||
captureLLMEvent(`${loggingName} - Error`, { error: errorMessage })
|
||||
onError_({ message: errorMessage, fullError })
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue