diff --git a/package-lock.json b/package-lock.json index 196334c5..3c5fa7a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "cross-spawn": "^7.0.6", "diff": "^7.0.0", "eslint-plugin-react": "^7.37.4", + "fast-json-stable-stringify": "^2.1.0", "google-auth-library": "^9.15.1", "groq-sdk": "^0.15.0", "http-proxy-agent": "^7.0.0", @@ -8067,7 +8068,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", diff --git a/package.json b/package.json index 689f7410..d4e0fc21 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "cross-spawn": "^7.0.6", "diff": "^7.0.0", "eslint-plugin-react": "^7.37.4", + "fast-json-stable-stringify": "^2.1.0", "google-auth-library": "^9.15.1", "groq-sdk": "^0.15.0", "http-proxy-agent": "^7.0.0", diff --git a/src/vs/workbench/contrib/void/browser/_dummyContrib.ts b/src/vs/workbench/contrib/void/browser/_dummyContrib.ts index 6898845e..be6b00f9 100644 --- a/src/vs/workbench/contrib/void/browser/_dummyContrib.ts +++ b/src/vs/workbench/contrib/void/browser/_dummyContrib.ts @@ -59,6 +59,6 @@ class DummyService extends Disposable implements IWorkbenchContribution, IDummyS // pick one and delete the other: -registerSingleton(IDummyService, DummyService, InstantiationType.Eager); +registerSingleton(IDummyService, DummyService, InstantiationType.Eager); // lazily loaded, even if Eager -registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore); // mounts on start diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index a72070c7..96b47e50 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -32,6 +32,9 @@ import { INotificationService, Severity } from '../../../../platform/notificatio import { truncate } from '../../../../base/common/strings.js'; import { THREAD_STORAGE_KEY } from '../common/storageKeys.js'; import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'; +import { timeout } from '../../../../base/common/async.js'; + +const CHAT_RETRIES = 3 export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => { if (!currentSelections) return null @@ -565,7 +568,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { let nMessagesSent = 0 let shouldSendAnotherMessage = true let isRunningWhenEnd: IsRunningType = undefined - let aborted = false // before enter loop, call tool if (callThisToolFirst) { @@ -593,69 +595,94 @@ class ChatThreadService extends Disposable implements IChatThreadService { chatMode }) - const llmCancelToken = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - chatMode, - messages: messages, - modelSelection, - modelSelectionOptions, - logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, - separateSystemMessage: separateSystemMessage, - onText: ({ fullText, fullReasoning, toolCall }) => { - this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge') - }, - onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => { - this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning }) - this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge') - resMessageIsDonePromise(toolCall) // resolve with tool calls - }, - onError: (error) => { - const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? '' - const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - // const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar - // add assistant's message to chat history, and clear selection - this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - this._setStreamState(threadId, { error }, 'set') - resMessageIsDonePromise() - }, - onAbort: () => { - // stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it) - resMessageIsDonePromise() - this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode }) - aborted = true - }, - }) - // should never happen, just for safety - if (llmCancelToken === null) { - this._setStreamState(threadId, { - error: { message: 'There was an unexpected error when sending your chat message.', fullError: null } - }, 'set') - break - } - this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message - const toolCall = await messageIsDonePromise // wait for message to complete - if (aborted) { return } - this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done + let aborted = false - // call tool if there is one - const tool: RawToolCallObj | undefined = toolCall - if (tool) { - const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams }) + let shouldRetry = true + let nAttempts = 0 - // stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools. - // just detect tool interruption which is the same as chat interruption right now - if (interrupted) { return } + while (shouldRetry) { + shouldRetry = false - if (awaitingUserApproval) { - isRunningWhenEnd = 'awaiting_user' + const llmCancelToken = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + chatMode, + messages: messages, + modelSelection, + modelSelectionOptions, + logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, + separateSystemMessage: separateSystemMessage, + onText: ({ fullText, fullReasoning, toolCall }) => { + this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge') + }, + onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => { + this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning }) + this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge') + resMessageIsDonePromise(toolCall) // resolve with tool calls + + }, + onError: (error) => { + const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? '' + const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + + if (nAttempts < CHAT_RETRIES) { + nAttempts += 1 + shouldRetry = true + this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge') + timeout(2500).then(() => { resMessageIsDonePromise() }) + } + else { + // const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar + // add assistant's message to chat history, and clear selection + this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + this._setStreamState(threadId, { error }, 'set') + resMessageIsDonePromise() + } + }, + onAbort: () => { + // stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it) + resMessageIsDonePromise() + this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode }) + aborted = true + }, + }) + + // should never happen, just for safety + if (llmCancelToken === null) { + this._setStreamState(threadId, { + error: { message: 'There was an unexpected error when sending your chat message.', fullError: null } + }, 'set') + break } - else { - shouldSendAnotherMessage = true + this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message + const toolCall = await messageIsDonePromise // wait for message to complete + if (shouldRetry) { + continue } - } + if (aborted) { + return + } + this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done - } // end while + // call tool if there is one + const tool: RawToolCallObj | undefined = toolCall + if (tool) { + const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams }) + + // stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools. + // just detect tool interruption which is the same as chat interruption right now + if (interrupted) { return } + + if (awaitingUserApproval) { + isRunningWhenEnd = 'awaiting_user' + } + else { + shouldSendAnotherMessage = true + } + } + + } // end while (attempts) + } // end while (send message) // if awaiting user approval, keep isRunning true, else end isRunning 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 a0942377..9464bf91 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 @@ -29,6 +29,7 @@ import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg import { ToolName, toolNames } from '../../../../common/prompt/prompts.js'; import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js'; +import jsonStringify from 'fast-json-stable-stringify' import ErrorBoundary from './ErrorBoundary.js'; @@ -143,9 +144,6 @@ export const IconLoading = ({ className = '' }: { className?: string }) => { } -const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-${messageIdx}`; - - // SLIDER ONLY: const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => { @@ -554,8 +552,7 @@ export const SelectedFiles = ( {allSelections.map((selection, i) => { const isThisSelectionProspective = i > selections.length - 1 - - const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}` + const thisKey = jsonStringify(selection) return
void) | null, } -const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => { +const ChatBubble = (props: ChatBubbleProps) => { + return + <_ChatBubble {...props} /> + +} + +const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => { const role = chatMessage.role const isCheckpointGhost = messageIdx > (currCheckpointIdx ?? Infinity) && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts) @@ -2001,33 +2004,6 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes isCommitted={isCommitted} /> } - // else if (role === 'tool_request') { - // const ToolRequestWrapper = toolNameToComponent[chatMessage.name]?.requestWrapper as RequestWrapper - // const toolRequestState = ( - // chatIsRunning === 'awaiting_user' ? 'awaiting_user' - // : chatIsRunning === 'tool' ? 'running' - // : chatIsRunning === 'message' ? null - // : null - // ) - // if (ToolRequestWrapper && canAcceptReject) { // if it's the last message - // return <> - // {toolRequestState !== null && - //
- // - //
} - // {chatIsRunning === 'awaiting_user' && - //
- // - //
} - // - // } - // return null - // } else if (role === 'tool') { if (chatMessage.type === 'invalid_params') { @@ -2537,8 +2513,8 @@ export const SidebarChat = () => { // const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint') // tool request shows up as Editing... if in progress return previousMessages.map((message, i) => { - return { chatIsRunning={isRunning} threadId={threadId} _scrollToBottom={() => scrollToBottom(scrollContainerRef)} - /> + /> }) }, [previousMessages, threadId, currCheckpointIdx, isRunning]) const streamingChatIdx = previousMessagesHTML.length const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ? - { threadId={threadId} _scrollToBottom={null} - /> : null + /> : null // the tool currently being generated const generatingTool = toolIsGenerating ? toolCallSoFar.name === 'edit_file' ? : null @@ -2722,9 +2698,13 @@ export const SidebarChat = () => {
- return (isLandingPage ? - landingPageContent - : threadPageContent + return ( + + {isLandingPage ? + landingPageContent + : threadPageContent} + ) } diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 47fd95f8..8966ccf7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -332,7 +332,7 @@ export const ModelDump = () => { />
- {type === 'default' ? null : } + {type === 'default' || type === 'autodetected' ? null : }