diff --git a/.voidrules b/.voidrules new file mode 100644 index 00000000..c06e70b4 --- /dev/null +++ b/.voidrules @@ -0,0 +1,5 @@ +This is a fork of the VSCode repo called Void. + +Most code we care about lives in src/vs/workbench/contrib/void. + +You may sometimes need to explore the full repo to find relevant parts of code. diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 1dd0d4af..ae443a56 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -180,7 +180,7 @@ export type ThreadStreamState = { error?: undefined; llmInfo?: undefined; toolInfo?: undefined; - interrupt?: undefined; + interrupt: 'not_needed' | Promise<() => void>; // calling this should have no effect on state - would be too confusing. it just cancels the tool } } @@ -491,7 +491,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) - this._addUserCheckpoint({ threadId }) } // add tool that's running else if (this.streamState[threadId]?.isRunning === 'tool') { @@ -502,10 +501,16 @@ class ChatThreadService extends Disposable implements IChatThreadService { else if (this.streamState[threadId]?.isRunning === 'awaiting_user') { this.rejectLatestToolRequest(threadId) } + else if (this.streamState[threadId]?.isRunning === 'idle') { + // do nothing + } + + this._addUserCheckpoint({ threadId }) // interrupt any effects const interrupt = await this.streamState[threadId]?.interrupt - interrupt?.() + if (typeof interrupt === 'function') + interrupt() this._setStreamState(threadId, undefined) @@ -598,9 +603,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams }) return {} } - finally { - this._setStreamState(threadId, undefined) - } // 4. stringify the result to give to the LLM try { @@ -633,12 +635,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { }) { + let interruptedWhenIdle = false + const idleInterruptor = Promise.resolve(() => { interruptedWhenIdle = true }) + // _runToolCall does not need setStreamState({idle}) before it, but it needs it after it. (handles its own setStreamState) + // above just defines helpers, below starts the actual function const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here - // not running at start, clear state - this._setStreamState(threadId, { isRunning: 'idle' }) - let nMessagesSent = 0 let shouldSendAnotherMessage = true let isRunningWhenEnd: IsRunningType = undefined @@ -646,9 +649,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { // before enter loop, call tool if (callThisToolFirst) { const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params }) - this._setStreamState(threadId, undefined) - if (interrupted) { return } + if (interrupted) { + this._setStreamState(threadId, undefined) + this._addUserCheckpoint({ threadId }) + + } } + this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity + // tool use loop while (shouldSendAnotherMessage) { @@ -657,6 +665,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { isRunningWhenEnd = undefined nMessagesSent += 1 + this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor }) + const chatMessages = this.state.allThreads[threadId]?.messages ?? [] const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({ chatMessages, @@ -664,20 +674,20 @@ class ChatThreadService extends Disposable implements IChatThreadService { chatMode }) + if (interruptedWhenIdle) { + this._setStreamState(threadId, undefined) + return + } + let shouldRetryLLM = true let nAttempts = 0 while (shouldRetryLLM) { - // if (this.streamState[threadId]?.isRunning === 'LLM' || ) { - // // if already streaming, stop - // console.log('returning...', this.streamState[threadId]) - // return - // } + shouldRetryLLM = false - let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval) - const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) + let resMessageIsDonePromise: (res: { type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }) => void // resolves when user approves this tool use (or if tool doesn't require approval) + const messageIsDonePromise = new Promise<{ type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }>((res, rej) => { resMessageIsDonePromise = res }) - let aborted = false const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', chatMode, @@ -691,36 +701,15 @@ class ChatThreadService extends Disposable implements IChatThreadService { }, onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => { this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning }) - this._setStreamState(threadId, undefined) - resMessageIsDonePromise(toolCall) // resolve with tool calls + resMessageIsDonePromise({ type: 'llmDone', toolCall }) // resolve with tool calls }, onError: async (error) => { - if (this.streamState[threadId]?.isRunning !== 'LLM') { - console.log('Unexpected onError when', this.streamState[threadId]?.isRunning) - return - } - - if (nAttempts < CHAT_RETRIES) { - nAttempts += 1 - shouldRetryLLM = true - this._setStreamState(threadId, undefined) // clear later so can be interrupted - resMessageIsDonePromise() - } - else { - // add assistant's message to chat history, and clear selection - const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo - this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) - this._setStreamState(threadId, { isRunning: undefined, error }) - resMessageIsDonePromise() - } + resMessageIsDonePromise({ type: 'llmError', error: error }) }, onAbort: () => { // stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it) - aborted = true - this._setStreamState(threadId, { isRunning: 'idle' }) - resMessageIsDonePromise() + resMessageIsDonePromise({ type: 'llmAborted' }) this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode }) }, }) @@ -732,31 +721,63 @@ class ChatThreadService extends Disposable implements IChatThreadService { } this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: '', reasoningSoFar: '', toolCallSoFar: null }, interrupt: Promise.resolve(() => this._llmMessageService.abort(llmCancelToken)) }) - const toolCall = await messageIsDonePromise // wait for message to complete - - if (aborted) { + const llmRes = await messageIsDonePromise // wait for message to complete + if (this.streamState[threadId]?.isRunning !== 'LLM') { + console.log('Unexpected chat agent state when', this.streamState[threadId]?.isRunning) this._setStreamState(threadId, undefined) return } - if (shouldRetryLLM) { - this._setStreamState(threadId, { isRunning: 'idle' }) - await timeout(RETRY_DELAY) - continue + + // llm res aborted + if (llmRes.type === 'llmAborted') { + this._setStreamState(threadId, undefined) + return } - this._setStreamState(threadId, { isRunning: 'idle' }) + // llm res error + else if (llmRes.type === 'llmError') { + // error, should retry + if (nAttempts < CHAT_RETRIES) { + nAttempts += 1 + shouldRetryLLM = true + this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor }) + await timeout(RETRY_DELAY) + if (interruptedWhenIdle) { + this._setStreamState(threadId, undefined) + return + } + else + continue // retry + } + // error, but too many attempts + else { + const { error } = llmRes + const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo + this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name }) + + this._setStreamState(threadId, { isRunning: undefined, error }) + this._addUserCheckpoint({ threadId }) + return + } + } + + + + // llm res success + const { toolCall } = llmRes + this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity // call tool if there is one if (toolCall) { const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, { preapproved: false, unvalidatedToolParams: toolCall.rawParams }) - if (interrupted) { this._setStreamState(threadId, undefined) return } - this._setStreamState(threadId, { isRunning: 'idle' }) - if (awaitingUserApproval) { isRunningWhenEnd = 'awaiting_user' } else { shouldSendAnotherMessage = true } + + this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity } } // end while (attempts) @@ -766,8 +787,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { isRunning: isRunningWhenEnd }) // add checkpoint before the next user message - if (!isRunningWhenEnd) - this._addUserCheckpoint({ threadId }) + if (!isRunningWhenEnd) this._addUserCheckpoint({ threadId }) // capture number of messages sent this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode }) diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 56b66e02..43b91c84 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -363,7 +363,7 @@ const prepareOpenAIOrAnthropicMessages = ({ // find system messages and concatenate them const newSystemMessage = aiInstructions ? - `${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES\n${aiInstructions}` + `${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES (from the user's .voidrules file):\n${aiInstructions}` : systemMessage let separateSystemMessageStr: string | undefined = undefined 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 88590141..8733e140 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 @@ -591,7 +591,7 @@ export const SelectedFiles = ( select-none text-xs text-nowrap border rounded-sm - ${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'} + ${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-1 hover:brightness-95 text-void-fg-1'} ${isThisSelectionProspective ? 'border-void-border-2' : 'border-void-border-1' @@ -2881,7 +2881,7 @@ export const SidebarChat = () => { {generatingTool} {/* loading indicator */} - {isRunning === 'LLM' && !toolIsGenerating ? + {isRunning === 'LLM' || isRunning === 'idle' && !toolIsGenerating ? {} : null} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 78a2fcb9..6381867b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -818,19 +818,22 @@ export const VoidInputBox2 = forwardRef(fun key={o.fullName} className={` flex items-center gap-2 - px-3 py-1 cursor-pointer bg-void-bg-2-alt - ${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''} + px-3 py-1 cursor-pointer + ${oIdx === optionIdx ? 'bg-blue-500 text-white/80' : 'bg-void-bg-2-alt text-void-fg-1'} `} onClick={() => { onSelectOption(); }} onMouseMove={() => { setOptionIdx(oIdx) }} > {} - {o.abbreviatedName} - {o.fullName && o.fullName !== o.abbreviatedName && {o.fullName}} + {o.abbreviatedName} + + {o.fullName && o.fullName !== o.abbreviatedName && {o.fullName}} + {o.nextOptions || o.generateNextOptions ? ( ) : null} + ) }) @@ -1379,7 +1382,7 @@ export const VoidCustomDropdownBox = >({ key={optionName} className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap transition-all duration-100 - ${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2-alt hover:bg-void-bg-2-hover'} + ${thisOptionIsSelected ? 'bg-blue-500 text-white/80' : 'hover:bg-blue-500 hover:text-white/80'} `} onClick={() => { onChangeOption(option); @@ -1401,7 +1404,7 @@ export const VoidCustomDropdownBox = >({ {optionName} - {optionDetail} + {optionDetail} ); 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 de913465..df46b296 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 @@ -827,12 +827,16 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: 'rooveterinaryinc.roo-cline', // roo ]; for (const { from, to } of transferTheseFiles) { + console.log('Transferring...', from) try { // find a blacklisted item const isBlacklisted = extensionBlacklist.find(blacklistItem => { return from.fsPath?.includes(blacklistItem) }) - if (isBlacklisted) continue + if (isBlacklisted) { + console.log(`Skipping conflicting item (${isBlacklisted})`) + continue + } } catch { } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index faae202f..2971b994 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -83,7 +83,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn return { title: 'LM Studio', } } else if (providerName === 'openAICompatible') { - return { title: 'OpenAI-Compatible', } + return { title: 'Custom', } } else if (providerName === 'gemini') { return { title: 'Gemini', } @@ -117,7 +117,7 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => { if (providerName === 'groq') return 'Get your [API Key here](https://console.groq.com/keys).' if (providerName === 'xAI') return 'Get your [API Key here](https://console.x.ai).' if (providerName === 'mistral') return 'Get your [API Key here](https://console.mistral.ai/api-keys).' - if (providerName === 'openAICompatible') return `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).` + if (providerName === 'openAICompatible') return `Use any provider that's OpenAI-compatible (most popular ones are).` // if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).' if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).' if (providerName === 'ollama') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'