diff --git a/product.json b/product.json index 6939af1d..b7b9c269 100644 --- a/product.json +++ b/product.json @@ -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": [] } diff --git a/src/vs/workbench/contrib/void/browser/aiRegexService.ts b/src/vs/workbench/contrib/void/browser/aiRegexService.ts index f38236a9..4b2a919a 100644 --- a/src/vs/workbench/contrib/void/browser/aiRegexService.ts +++ b/src/vs/workbench/contrib/void/browser/aiRegexService.ts @@ -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() +// // // 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): void; - // onDidChangeState: Event; -} +// // // writes +// // if (!replaceClause) return -export const IVoidFastApplyService = createDecorator('voidFastApplyService'); -class VoidFastApplyService extends Disposable implements IFastApplyService { - _serviceBrand: undefined; - - // static readonly ID = 'voidFastApplyService'; - - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; - - - // state - // state: ApplyState - - constructor( - // @IToolService private readonly toolService: ToolService - ) { - super() - - // initial state - // this.state = { currentUri: undefined } - } - - setState(newState: Partial) { - - // 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() - // // // 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); diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index f7fcd7d0..c4b127a1 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -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 diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index 00eb2ef1..e24bc232 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -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_ }) => { - // abcdefghi - // | + 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 // 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 - fullText += newText_ - const newText = fullText.substring(latestAddIdx, Infinity) - latestAddIdx += newText.length - onText_({ newText, fullText, newReasoning: '', fullReasoning }) + // at this point, we found - 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 } } diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 90e01d50..a7259020 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -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) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx index 4c2cbea4..f7954b82 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx @@ -13,7 +13,7 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov return ( <> -
+
{buttonsOnHover === null ? null : (
diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 8168bed3..c8571403 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -6,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 } -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 + } /> +
} if (t.type === "heading") { @@ -129,7 +135,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: )} - + ))} @@ -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) => ( - + ))} ) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 9a4969fa..a77aba9a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -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 = ({ return (
{ @@ -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 ( -
+
{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 = (
- {/* selection summary */} -
{ + 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 + // }); + + } + }} > -
{ - 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 - }); - } - }} - > - - {/* file name */} - {getBasename(selection.fileURI.fsPath)} - {/* selection range */} - {!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''} - - - {/* X button */} - {type === 'staging' && !isThisSelectionProspective && - { - 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)]) - }} - > - - } - - -
- - {/* clear all selections button */} - {/* {type !== 'staging' || selections.length === 0 || i !== selections.length - 1 - ? null - :
-
setIsClearHovered(true)} - onMouseLeave={() => setIsClearHovered(false)} - > - { setSelections([]) }} - /> -
-
- } */} + {type === 'staging' && !isThisSelectionProspective ? // X button + { + e.stopPropagation(); // don't open/close selection + if (type !== 'staging') return; + setSelections([...selections.slice(0, i), ...selections.slice(i + 1)]) + }} + size={10} + /> + : <> + }
- {/* selection text */} - {isThisSelectionOpened && + + {/* code box */} + {isThisSelectionOpened ?
{ e.stopPropagation(); // don't focus input box }} @@ -524,14 +515,9 @@ export const SelectedFiles = ( showScrollbars={true} />
+ : <> } -
) - - return - {/* divider between `selections` and `prospectiveSelections` */} - {/* {selections.length > 0 && i === selections.length &&
} */} - {selectionHTML} -
+
})} @@ -542,12 +528,13 @@ export const SelectedFiles = ( } -type ToolReusltToComponent = { [T in ToolName]: (props: { message: ToolMessage }) => React.ReactNode } +type ToolResultToComponent = { [T in ToolName]: (props: { message: ToolMessage }) => 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 (
-
+
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 && ( )} -
+
{actionTitle} - {`"`}{actionParam}{`"`} + {actionParam} {actionNumResults !== undefined && ( {`(`}{actionNumResults}{` result`}{actionNumResults !== 1 ? 's' : ''}{`)`} @@ -583,7 +575,8 @@ const ToolResult = ({
{children}
@@ -594,98 +587,140 @@ const ToolResult = ({ -const toolResultToComponent: ToolReusltToComponent = { - 'read_file': ({ message }) => ( - - ), - 'list_dir': ({ message }) => ( - -
- {message.result.children?.map((item, i) => ( -
- {item.name} - {item.isDirectory && '/'} -
- ))} - {message.result.hasNextPage && ( -
- {message.result.itemsRemaining} more items... -
- )} -
-
- ), - 'pathname_search': ({ message }) => ( - -
- {Array.isArray(message.result.uris) ? - message.result.uris.map((uri, i) => ( -
- { + + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + return ( + { 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 ( + +
+ {message.result.children?.map((child, i) => ( +
{ + commandService.executeCommand('workbench.view.explorer'); + explorerService.select(child.uri, true); + }} + > + + {`${child.name}${child.isDirectory ? '/' : ''}`} +
+ ))} + {message.result.hasNextPage && ( +
+ {message.result.itemsRemaining} more items... +
+ )} +
+
+ ) + }, + 'pathname_search': ({ message }) => { + + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + return ( + +
+ {Array.isArray(message.result.uris) ? + message.result.uris.map((uri, i) => ( + + )) : +
{message.result.uris}
+ } + {message.result.hasNextPage && ( +
+ More results available...
- )) : -
{message.result.uris}
- } - {message.result.hasNextPage && ( -
- More results available... -
- )} -
- - ), - 'search': ({ message }) => ( - -
- {typeof message.result.uris === 'string' ? - message.result.uris : - message.result.uris.map((uri, i) => ( -
- + + ) + }, + 'search': ({ message }) => { + + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + return ( + +
+ {Array.isArray(message.result.uris) ? + message.result.uris.map((uri, i) => ( + + )) : +
{message.result.uris}
+ } + {message.result.hasNextPage && ( +
+ More results available...
- )) - } - {message.result.hasNextPage && ( -
- More results available... -
- )} -
- - ) + )} +
+ + ) + } }; 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 = <> - {chatMessage.displayContent} + {chatMessage.displayContent} } else if (mode === 'edit') { @@ -806,7 +840,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM > 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 = + + const reasoningDropdown = hasReasoning ? ( +
+
+
setIsReasoningOpen(!isReasoningOpen)} + > + +
+ Reasoning + Model's step-by-step thinking +
+
+
+
+ +
+
+
+
+ ) : null + + chatbubbleContents = (<> + {/* Reasoning dropdown (conditional) */} + {reasoningDropdown} + {/* Main content */} + + ) } 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) => - + ) }, [previousMessages]) + const streamingChatIdx = pastMessagesHTML.length + const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ? + : null + + const allMessagesHTML = [...pastMessagesHTML, currStreamingMessageHTML] + + const threadSelector =
@@ -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 */} - + {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" > ({ selectedOption, onChangeOption, getOptionDropdownName, + getOptionDropdownDetail, getOptionDisplayName, getOptionsEqual, className, @@ -321,6 +322,7 @@ export const VoidCustomDropdownBox = ({ 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 = ({ className="opacity-0 pointer-events-none absolute -left-[999999px] -top-[999999px] flex flex-col" aria-hidden="true" > - {options.map((option) => ( -
-
- {getOptionDropdownName(option)} -
- ))} + {options.map((option) => { + const optionName = getOptionDropdownName(option); + const optionDetail = getOptionDropdownDetail?.(option) || ''; + + return ( +
+
+ + {optionName} + {optionDetail} + ______ + +
+ ) + })}
{/* Select Button */} @@ -473,6 +484,7 @@ export const VoidCustomDropdownBox = ({ {options.map((option) => { const thisOptionIsSelected = getOptionsEqual(option, selectedOption); const optionName = getOptionDropdownName(option); + const optionDetail = getOptionDropdownDetail?.(option) || ''; return (
({ )}
- {optionName} + + {optionName} + {optionDetail} +
); })} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 5e164428..9898ef52 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -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), diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx index 2931b671..7ff66b32 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx @@ -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} diff --git a/src/vs/workbench/contrib/void/browser/react/tailwind.config.js b/src/vs/workbench/contrib/void/browser/react/tailwind.config.js index bc57116b..d7c0f7e0 100644 --- a/src/vs/workbench/contrib/void/browser/react/tailwind.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tailwind.config.js @@ -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: { diff --git a/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts b/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts index e7a9448e..e50a6d12 100644 --- a/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts +++ b/src/vs/workbench/contrib/void/browser/searchReplaceCacheService.ts @@ -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 = this._onDidChangeState.event; constructor( - @ILLMMessageService private readonly llmMessageService: ILLMMessageService, + // @ILLMMessageService private readonly llmMessageService: ILLMMessageService, ) { super() } - send(params: Omit & { 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) { - } - } - }) - } + // } + // } + // }) + // } } diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 722eaa57..fff6dda4 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -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) { diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index b179e603..f9f6a29b 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -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' + diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/common/chatThreadService.ts similarity index 80% rename from src/vs/workbench/contrib/void/browser/chatThreadService.ts rename to src/vs/workbench/contrib/void/common/chatThreadService.ts index ad4ef8a6..adfd3f29 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadService.ts @@ -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 = { // 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 @@ -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) => void - getCurrentThreadStagingSelections: () => StagingSelectionItem[] - setCurrentThreadStagingSelections: (stagingSelections: StagingSelectionItem[]) => void + getCurrentThreadState: () => ThreadType['state'] + setCurrentThreadState: (newState: Partial) => 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 - // 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, 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) => { + 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); - diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index abe88970..4f767928 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -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') diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 1872f08f..09385e25 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -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; diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 72095c83..98ab45fc 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -28,7 +28,7 @@ type SetModelSelectionOfFeatureFn = ( options?: { doNotApplyEffects?: true } ) => Promise; -type SetGlobalSettingFn = (settingName: T, newVal: GlobalSettings[T]) => void; +type SetGlobalSettingFn = (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; + readAndInitializeState: (providedState?: VoidSettingsState) => Promise; + onDidChangeState: Event; setSettingOfProvider: SetSettingOfProviderFn; @@ -168,6 +170,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { readonly onDidChangeState: Event = 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 // 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(); } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index 5c4b3434..c618a0da 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -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) => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 90deffe2..11ae2b76 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -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 = () => {