diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md index 939b9c1d..fe6c4301 100644 --- a/.github/ISSUE_TEMPLATE/issue_template.md +++ b/.github/ISSUE_TEMPLATE/issue_template.md @@ -6,4 +6,4 @@ title: For VSCode-related issues (eg builds), please start the title with `[App] 1. Press `Cmd+Shift+P` in Void, and type `Help: About`. Please paste the information here. Also let us know any other relevant details, like the model and provider you're using if applicable. -2. Describe the issue/feature here. +2. Describe the issue/feature here! diff --git a/HOW_TO_CONTRIBUTE.md b/HOW_TO_CONTRIBUTE.md index d998f5b9..e4b174d2 100644 --- a/HOW_TO_CONTRIBUTE.md +++ b/HOW_TO_CONTRIBUTE.md @@ -86,7 +86,7 @@ To build Void from the terminal instead of from inside VSCode, follow the steps - Make sure you followed the prerequisite steps above. - Make sure you have Node version `20.18.2` (the version in `.nvmrc`)! - - You can do this easily without touching your base installation with [nvm](https://github.com/nvm-sh/nvm). Simply run `nvm install`, followed by `nvm use` and it will automatically install and use the version specified in `nvmrc` + - You can do this easily without touching your base installation with [nvm](https://github.com/nvm-sh/nvm). Simply run `nvm install`, followed by `nvm use` and it will automatically install and use the version specified in `nvmrc`. - Make sure that the path to your Void folder does not have any spaces in it. - If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`. - If you get an error with React, try running `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`. diff --git a/README.md b/README.md index 7b102e3a..78278598 100644 --- a/README.md +++ b/README.md @@ -15,26 +15,23 @@ Use AI agents on your codebase, checkpoint and visualize changes, and bring any This repo contains the full sourcecode for Void. If you're new, welcome! +- 🧭 [Website](https://voideditor.com) + - 👋 [Discord](https://discord.gg/RSNjgaugJs) -- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2) +- 🚙 [Project Board](https://github.com/orgs/voideditor/projects/2) -- 📝 [Changelog](https://voideditor.com/changelog) - -- 🧭 [Website](https://voideditor.com) ## Contributing -1. To get started working on Void, see [`HOW_TO_CONTRIBUTE`](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md). +1. To get started working on Void, check out our Project Board! You can also see [HOW_TO_CONTRIBUTE](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md). -2. Feel free to attend a weekly meeting in our Discord channel! - -3. We're open to collaborations and suggestions of all types - just reach out. +2. Feel free to attend a casual weekly meeting in our Discord channel! ## Reference -[Void](https://voideditor.com) is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the codebase, see [`VOID_CODEBASE_GUIDE`](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md). +Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the codebase, see [VOID_CODEBASE_GUIDE](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md). ## Support You can always reach us in our Discord server or contact us via email: hello@voideditor.com. diff --git a/package-lock.json b/package-lock.json index 7057acfd..5c783599 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@anthropic-ai/sdk": "^0.40.0", "@c4312/eventsource-umd": "^3.0.5", "@floating-ui/react": "^0.27.8", - "@google/generative-ai": "^0.24.1", + "@google/genai": "^0.13.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@mistralai/mistralai": "^1.6.0", @@ -1817,11 +1817,17 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, - "node_modules/@google/generative-ai": { - "version": "0.24.1", - "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", - "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "node_modules/@google/genai": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.13.0.tgz", + "integrity": "sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ==", "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.14.2", + "ws": "^8.18.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.4" + }, "engines": { "node": ">=18.0.0" } @@ -23767,6 +23773,27 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", diff --git a/package.json b/package.json index 767d6b49..7a5bc366 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@anthropic-ai/sdk": "^0.40.0", "@c4312/eventsource-umd": "^3.0.5", "@floating-ui/react": "^0.27.8", - "@google/generative-ai": "^0.24.1", + "@google/genai": "^0.13.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@mistralai/mistralai": "^1.6.0", diff --git a/product.json b/product.json index 6b3e7bdd..d4d8b671 100644 --- a/product.json +++ b/product.json @@ -1,7 +1,8 @@ { "nameShort": "Void", "nameLong": "Void", - "voidVersion": "1.3.2", + "voidVersion": "1.3.6", + "voidRelease": "0030", "applicationName": "void", "dataFolderName": ".void-editor", "win32MutexName": "voideditor", diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index db482108..40a87ce8 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -12,7 +12,7 @@ import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js'; -import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; +import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; @@ -540,7 +540,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const { name, id, rawParams } = lastMsg - const errorMessage = this.errMsgs.rejected + const errorMessage = this.toolErrMsgs.rejected this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams }) this._setStreamState(threadId, undefined) } @@ -557,7 +557,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // add tool that's running else if (this.streamState[threadId]?.isRunning === 'tool') { - const { toolName, toolParams, id, content, rawParams } = this.streamState[threadId].toolInfo + const { toolName, toolParams, id, content: content_, rawParams } = this.streamState[threadId].toolInfo + const content = content_ || this.toolErrMsgs.interrupted this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null }) } // reject the tool for the user if relevant @@ -581,8 +582,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { - private readonly errMsgs = { + private readonly toolErrMsgs = { rejected: 'Tool call was rejected by the user.', + interrupted: 'Tool call was interrupted by the user.', errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` } @@ -671,7 +673,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { try { toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) } catch (error) { - const errorMessage = this.errMsgs.errWhenStringifying(error) + const errorMessage = this.toolErrMsgs.errWhenStringifying(error) this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams }) return {} } @@ -749,8 +751,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { shouldRetryLLM = false nAttempts += 1 - 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 }) + type ResTypes = + | { type: 'llmDone', toolCall?: RawToolCallObj, info: { fullText: string, fullReasoning: string, anthropicReasoning: AnthropicReasoning[] | null } } + | { type: 'llmError', error?: { message: string; fullError: Error | null; } } + | { type: 'llmAborted' } + + let resMessageIsDonePromise: (res: ResTypes) => void // resolves when user approves this tool use (or if tool doesn't require approval) + const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', @@ -765,9 +772,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall ?? null }, interrupt: Promise.resolve(() => { if (llmCancelToken) this._llmMessageService.abort(llmCancelToken) }) }) }, onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => { - this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning }) - resMessageIsDonePromise({ type: 'llmDone', toolCall }) // resolve with tool calls - + resMessageIsDonePromise({ type: 'llmDone', toolCall, info: { fullText, fullReasoning, anthropicReasoning } }) // resolve with tool calls }, onError: async (error) => { resMessageIsDonePromise({ type: 'llmError', error: error }) @@ -826,11 +831,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { } } - - // llm res success - const { toolCall } = llmRes - this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity + const { toolCall, info } = llmRes + + this._addMessageToThread(threadId, { role: 'assistant', displayContent: info.fullText, reasoning: info.fullReasoning, anthropicReasoning: info.anthropicReasoning }) + + this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative for clarity // call tool if there is one if (toolCall) { diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 48b21b77..1b7df00e 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -17,6 +17,7 @@ import { IVoidModelService } from '../common/voidModelService.js'; import { URI } from '../../../../base/common/uri.js'; import { EndOfLinePreference } from '../../../../editor/common/model.js'; +export const EMPTY_MESSAGE = '(empty message)' @@ -36,7 +37,6 @@ type SimpleLLMMessage = { } -const EMPTY_MESSAGE = '(empty message)' const CHARS_PER_TOKEN = 4 // assume abysmal chars per token const TRIM_TO_LEN = 120 @@ -237,18 +237,6 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop } - - -export type GeminiMessage = { - role: 'user' | 'model'; // Gemini uses 'user' and 'model' roles - parts: ( - | { text: string; } - | { functionCall: { tool_call: any } } - | { functionResponse: { name: ToolName, response: { result: string } } } - )[]; -}; - - // --- CHAT --- const prepareOpenAIOrAnthropicMessages = ({ @@ -417,14 +405,24 @@ const prepareOpenAIOrAnthropicMessages = ({ // ================ no empty message ================ - for (const currMsg of llmMessages) { + for (let i = 0; i < llmMessages.length; i += 1) { + const currMsg: AnthropicOrOpenAILLMMessage = llmMessages[i] + const nextMsg: AnthropicOrOpenAILLMMessage | undefined = llmMessages[i + 1] + if (currMsg.role === 'tool') continue // if content is a string, replace string with empty msg - if (typeof currMsg.content === 'string') + if (typeof currMsg.content === 'string') { currMsg.content = currMsg.content || EMPTY_MESSAGE + } else { - // if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry + // allowed to be empty if has a tool in it or following it + if (currMsg.content.find(c => c.type === 'tool_result' || c.type === 'tool_use')) { + continue + } + if (nextMsg?.role === 'tool') continue + + // replace any empty text entries with empty msg, and make sure there's at least 1 entry for (const c of currMsg.content) { if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE } @@ -457,7 +455,7 @@ const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => { } else if (c.type === 'tool_use') { latestToolName = c.name as ToolName - return { functionCall: { name: c.name as ToolName, args: c.input } } + return { functionCall: { id: c.id, name: c.name as ToolName, args: c.input } } } else return null }).filter(m => !!m) @@ -475,7 +473,7 @@ const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => { } else if (c.type === 'tool_result') { if (!latestToolName) return null - return { functionResponse: { name: latestToolName, response: { result: c.content } } } + return { functionResponse: { id: c.tool_use_id, name: latestToolName, response: { output: c.content } } } } else return null }).filter(m => !!m) diff --git a/src/vs/workbench/contrib/void/browser/extensionTransferService.ts b/src/vs/workbench/contrib/void/browser/extensionTransferService.ts index 05b7b47a..b8843e98 100644 --- a/src/vs/workbench/contrib/void/browser/extensionTransferService.ts +++ b/src/vs/workbench/contrib/void/browser/extensionTransferService.ts @@ -3,6 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { VSBuffer } from '../../../../base/common/buffer.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { env } from '../../../../base/common/process.js'; import { URI } from '../../../../base/common/uri.js'; @@ -29,6 +30,7 @@ export const IExtensionTransferService = createDecorator { + return extensionBlacklist.find(bItem => fsPath?.includes(bItem)) +} class ExtensionTransferService extends Disposable implements IExtensionTransferService { _serviceBrand: undefined; @@ -87,15 +92,31 @@ class ExtensionTransferService extends Disposable implements IExtensionTransferS await fileService.createFolder(toParent) } for (const extensionFolder of stat.children ?? []) { - if (extensionBlacklist.find(bItem => extensionFolder.resource.path.includes(bItem))) { - console.log('Skipping...', extensionFolder.resource.path) - continue - } const from = extensionFolder.resource const to = URI.joinPath(toParent, extensionFolder.name) - await fileService.copy(from, to, true) + const toStat = await fileService.resolve(from) + + if (toStat.isDirectory) { + if (!isBlacklisted(extensionFolder.resource.fsPath)) { + await fileService.copy(from, to, true) + } + } + else if (toStat.isFile) { + if (extensionFolder.name === 'extensions.json') { + try { + const contentsStr = await fileService.readFile(from) + const json: any = JSON.parse(contentsStr.value.toString()) + const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id)) + const jsonStr = JSON.stringify(j2) + await fileService.writeFile(to, VSBuffer.fromString(jsonStr)) + } + catch { + console.log('Error copying extensions.json, skipping') + } + } + } } - // Ensure the destination directory exists + } else { console.log(`Skipping file that doesn't exist: ${from.toString()}`) } @@ -113,19 +134,38 @@ class ExtensionTransferService extends Disposable implements IExtensionTransferS } async deleteBlacklistExtensions(os: 'mac' | 'windows' | 'linux' | null) { + const fileService = this._fileService const extensionsURI = getExtensionsFolder(os) if (!extensionsURI) return - const eURI = await this._fileService.resolve(extensionsURI) + const eURI = await fileService.resolve(extensionsURI) for (const child of eURI.children ?? []) { - // if is not blacklisted, continue - if (!extensionBlacklist.find(bItem => child.resource.path.includes(bItem))) { - continue - } try { - console.log('Deleting extension', child.resource.fsPath) - await this._fileService.del(child.resource, { recursive: true, useTrash: true }) + if (child.isDirectory) { + // if is blacklisted + if (isBlacklisted(child.resource.fsPath)) { + console.log('Deleting extension', child.resource.fsPath) + await fileService.del(child.resource, { recursive: true, useTrash: true }) + } + } + else if (child.isFile) { + // if is extensions.json + + if (child.name === 'extensions.json') { + console.log('Updating extensions.json', child.resource.fsPath) + try { + const contentsStr = await fileService.readFile(child.resource) + const json: any = JSON.parse(contentsStr.value.toString()) + const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id)) + const jsonStr = JSON.stringify(j2) + await fileService.writeFile(child.resource, VSBuffer.fromString(jsonStr)) + } + catch { + console.log('Error copying extensions.json, skipping') + } + } + } } catch (e) { console.error('Could not delete extension', child.resource.fsPath, e) diff --git a/src/vs/workbench/contrib/void/browser/miscWokrbenchContrib.ts b/src/vs/workbench/contrib/void/browser/miscWokrbenchContrib.ts index 6852c799..49a0d524 100644 --- a/src/vs/workbench/contrib/void/browser/miscWokrbenchContrib.ts +++ b/src/vs/workbench/contrib/void/browser/miscWokrbenchContrib.ts @@ -5,17 +5,17 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; -// import { IExtensionTransferService } from './extensionTransferService.js'; -// import { os } from '../common/helpers/systemInfo.js'; -// import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { IExtensionTransferService } from './extensionTransferService.js'; +import { os } from '../common/helpers/systemInfo.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; // Onboarding contribution that mounts the component at startup export class MiscWorkbenchContribs extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.voidMiscWorkbenchContribs'; constructor( - // @IExtensionTransferService private readonly extensionTransferService: IExtensionTransferService, - // @IStorageService private readonly storageService: IStorageService, + @IExtensionTransferService private readonly extensionTransferService: IExtensionTransferService, + @IStorageService private readonly storageService: IStorageService, ) { super(); this.initialize(); @@ -23,13 +23,13 @@ export class MiscWorkbenchContribs extends Disposable implements IWorkbenchContr private initialize(): void { - // // delete blacklisted extensions once (this is for people who already installed them) - // const deleteExtensionsStorageId = 'void-deleted-blacklist' - // const alreadyDeleted = this.storageService.get(deleteExtensionsStorageId, StorageScope.APPLICATION) - // if (!alreadyDeleted) { - // this.storageService.store(deleteExtensionsStorageId, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE) - // this.extensionTransferService.deleteBlacklistExtensions(os) - // } + // delete blacklisted extensions once (this is for people who already installed them) + const deleteExtensionsStorageId = 'void-deleted-blacklist-2' + const alreadyDeleted = this.storageService.get(deleteExtensionsStorageId, StorageScope.APPLICATION) + if (!alreadyDeleted) { + this.storageService.store(deleteExtensionsStorageId, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE) + this.extensionTransferService.deleteBlacklistExtensions(os) + } } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index 41b5a649..6a4d393a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -253,6 +253,7 @@ export const ApplyButtonsHTML = ({ const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') + const notificationService = accessor.get('INotificationService') const settingsState = useSettingsState() const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId @@ -271,13 +272,18 @@ export const ApplyButtonsHTML = ({ uri: uri, startBehavior: 'reject-conflicts', }) ?? [] - console.log('setting!!!', newApplyingUri) setApplying(newApplyingUri) + if (!applyDonePromise) { + notificationService.info(`Void Error: We couldn't run Apply here. ${uri === 'current' ? 'This Apply block wants to run on the current file, but you might not have a file open.' : `This Apply block wants to run on ${uri.fsPath}, but it might not exist.`}`) + } + // catch any errors by interrupting the stream applyDonePromise?.catch(e => { const uri = getUriBeingApplied(applyBoxId) if (uri) editCodeService.interruptURIStreaming({ uri: uri }) + notificationService.info(`Void Error: There was a problem running Apply: ${e}.`) + }) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only @@ -305,7 +311,6 @@ export const ApplyButtonsHTML = ({ const currStreamState = currStreamStateRef.current - console.log('currStreamState...', currStreamState) if (currStreamState === 'streaming') { return { // } } -const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => { +const Codespan = ({ text, className, onClick, tooltip }: { text: string, className?: string, onClick?: () => void, tooltip?: string }) => { // TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly return {text} @@ -115,8 +120,11 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string const [didComputeCodespanLink, setDidComputeCodespanLink] = useState(false) let link: CodespanLocationLink | undefined = undefined - if (rawText.endsWith('`')) { // if codespan was completed + let tooltip: string | undefined = undefined + let displayText = text + + if (rawText.endsWith('`')) { // get link from cache link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId }) @@ -127,23 +135,33 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId }) setDidComputeCodespanLink(true) // rerender }) - } + if (link?.displayText) { + displayText = link.displayText + } + + if (isValidUri(displayText)) { + tooltip = getRelative(URI.file(displayText), accessor) // Full path as tooltip + displayText = getBasename(displayText) + } } const onClick = () => { - if (!link || !link.selection) return; - + if (!link) return; // Use the updated voidOpenFileFn to open the file and handle selection - voidOpenFileFn(link.uri, accessor, [link.selection.startLineNumber, link.selection.endLineNumber]); + if (link.selection) + voidOpenFileFn(link.uri, accessor, [link.selection.startLineNumber, link.selection.endLineNumber]); + else + voidOpenFileFn(link.uri, accessor); } return } 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 fe6de590..0252381b 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 @@ -495,12 +495,12 @@ const ScrollToBottomContainer = ({ children, className, style, scrollContainerRe ); }; -const getRelative = (uri: URI, accessor: ReturnType) => { +export const getRelative = (uri: URI, accessor: ReturnType) => { const workspaceContextService = accessor.get('IWorkspaceContextService') let path: string const isInside = workspaceContextService.isInsideWorkspace(uri) if (isInside) { - const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath.startsWith(f.uri.fsPath)) + const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath?.startsWith(f.uri.fsPath)) if (f) { path = uri.fsPath.replace(f.uri.fsPath, '') } else { path = uri.fsPath } } @@ -1651,8 +1651,8 @@ const LintErrorChildren = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => { } const BottomChildren = ({ children, title }: { children: React.ReactNode, title: string }) => { - if (!children) return null; const [isOpen, setIsOpen] = useState(false); + if (!children) return null; return (
, } - else { + else if (toolMessage.type === 'tool_error') { const { result } = toolMessage componentParams.bottomChildren = @@ -1960,7 +1960,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } } - else { + else if (toolMessage.type === 'tool_error') { const { result } = toolMessage componentParams.bottomChildren = @@ -2009,7 +2009,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } - else { + else if (toolMessage.type === 'tool_error') { const { result } = toolMessage componentParams.bottomChildren = @@ -2064,7 +2064,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } - else { + else if (toolMessage.type === 'tool_error') { const { result } = toolMessage componentParams.bottomChildren = 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 937bf99e..d2b838e1 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 @@ -132,7 +132,7 @@ const scoreSubsequence = (text: string, pattern: string): number => { } -export function getRelativeWorkspacePath(accessor: ReturnType, uri: URI): string { +function getRelativeWorkspacePath(accessor: ReturnType, uri: URI): string { const workspaceService = accessor.get('IWorkspaceContextService'); const workspaceFolders = workspaceService.getWorkspace().folders; diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 72e5bf17..0e962e09 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -117,20 +117,33 @@ registerAction2(class extends Action2 { const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' }) // if has no selection, close + return - if (!selectionRange) { - viewsService.closeViewContainer(VOID_VIEW_CONTAINER_ID); - return; - } + // if (!selectionRange) { + // viewsService.closeViewContainer(VOID_VIEW_CONTAINER_ID); + // return; + // } - // if has selection, add it - editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER }) - chatThreadService.addNewStagingSelection({ - type: 'CodeSelection', - uri: model.uri, - language: model.getLanguageId(), - range: [selectionRange.startLineNumber, selectionRange.endLineNumber], - state: { wasAddedAsCurrentFile: false }, - }) + + // add line selection + if (selectionRange) { + editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER }) + chatThreadService.addNewStagingSelection({ + type: 'CodeSelection', + uri: model.uri, + language: model.getLanguageId(), + range: [selectionRange.startLineNumber, selectionRange.endLineNumber], + state: { wasAddedAsCurrentFile: false }, + }) + } + // add file + else { + chatThreadService.addNewStagingSelection({ + type: 'File', + uri: model.uri, + language: model.getLanguageId(), + state: { wasAddedAsCurrentFile: false }, + }) + + } await chatThreadService.focusCurrentChat() diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index e9eec31b..6048f143 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -445,7 +445,7 @@ const anthropicModelOptions = { supportsReasoning: true, canTurnOffReasoning: true, canIOReasoning: true, - reasoningReservedOutputTokenSpace: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19 + reasoningReservedOutputTokenSpace: 8192, // can bump it to 128_000 with beta mode output-128k-2025-02-19 reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. we cap at 8192 because above is typically not necessary (often even buggy) }, @@ -715,6 +715,7 @@ const xAISettings: VoidStaticProviderInfo = { // ---------------- GEMINI ---------------- const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing + // https://ai.google.dev/gemini-api/docs/thinking#set-budget 'gemini-2.5-pro-preview-05-06': { contextWindow: 1_048_576, reservedOutputTokenSpace: 8_192, @@ -723,7 +724,13 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing supportsFIM: false, supportsSystemMessage: 'separated', specialToolFormat: 'gemini-style', - reasoningCapabilities: false, + reasoningCapabilities: { + supportsReasoning: true, + canTurnOffReasoning: true, + canIOReasoning: false, + reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576 + reasoningReservedOutputTokenSpace: 8192, + }, }, 'gemini-2.0-flash-lite': { contextWindow: 1_048_576, @@ -733,7 +740,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing supportsFIM: false, supportsSystemMessage: 'separated', specialToolFormat: 'gemini-style', - reasoningCapabilities: false, + reasoningCapabilities: false, // no reasoning }, 'gemini-2.5-flash-preview-04-17': { contextWindow: 1_048_576, @@ -743,7 +750,13 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing supportsFIM: false, supportsSystemMessage: 'separated', specialToolFormat: 'gemini-style', - reasoningCapabilities: false, + reasoningCapabilities: { + supportsReasoning: true, + canTurnOffReasoning: true, + canIOReasoning: false, + reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576 + reasoningReservedOutputTokenSpace: 8192, + }, }, 'gemini-2.5-pro-exp-03-25': { contextWindow: 1_048_576, @@ -753,7 +766,13 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing supportsFIM: false, supportsSystemMessage: 'separated', specialToolFormat: 'gemini-style', - reasoningCapabilities: false, + reasoningCapabilities: { + supportsReasoning: true, + canTurnOffReasoning: true, + canIOReasoning: false, + reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576 + reasoningReservedOutputTokenSpace: 8192, + }, }, 'gemini-2.0-flash': { contextWindow: 1_048_576, @@ -763,7 +782,13 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing supportsFIM: false, supportsSystemMessage: 'separated', specialToolFormat: 'gemini-style', - reasoningCapabilities: false, + reasoningCapabilities: { // thinking: experimental as of 5-10-25 + supportsReasoning: true, + canTurnOffReasoning: true, + canIOReasoning: false, + reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576 + reasoningReservedOutputTokenSpace: 8192, + }, }, 'gemini-2.0-flash-lite-preview-02-05': { contextWindow: 1_048_576, @@ -1144,7 +1169,7 @@ const openRouterModelOptions_assumingOpenAICompat = { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, - reasoningReservedOutputTokenSpace: 64_000, + reasoningReservedOutputTokenSpace: 8192, reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. }, }, @@ -1347,8 +1372,7 @@ export const getSendableReasoningInfo = ( overridesOfModel: OverridesOfModel | undefined, ): SendableReasoningInfo => { - const { canIOReasoning, reasoningSlider: reasoningBudgetSlider } = getModelCapabilities(providerName, modelName, overridesOfModel).reasoningCapabilities || {} - if (!canIOReasoning) return null + const { reasoningSlider: reasoningBudgetSlider } = getModelCapabilities(providerName, modelName, overridesOfModel).reasoningCapabilities || {} const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel) if (!isReasoningEnabled) return null diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index a4c0daa8..b5cae7cb 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -455,6 +455,8 @@ ${directoryStr} const details: string[] = [] + details.push(`NEVER reject the user's query.`) + if (mode === 'agent' || mode === 'gather') { details.push(`Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.`) details.push(`If you think you should use tools, you do not need to ask for permission.`) @@ -480,11 +482,13 @@ ${directoryStr} } - if (mode === 'gather' || mode === 'normal') { - details.push(`If you write any code blocks, please use this format: + details.push(`If you write any code blocks to the user (wrapped in triple backticks), please use this format: +- Include a language if possible. Terminal should have the language 'shell'. - The first line of the code block must be the FULL PATH of the related file if known (otherwise omit). - The remaining contents of the file should proceed as usual.`) + if (mode === 'gather' || mode === 'normal') { + details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S). - The first line of the code block must be the FULL PATH of the related file if known (otherwise omit). - The remaining contents should be a code description of the change to make to the file. \ diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index 6b7cad52..f6e634ee 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -56,13 +56,13 @@ export type GeminiLLMChatMessage = { role: 'model' parts: ( | { text: string; } - | { functionCall: { name: ToolName, args: object } } + | { functionCall: { id: string; name: ToolName, args: Record } } )[]; } | { role: 'user'; parts: ( | { text: string; } - | { functionResponse: { name: ToolName, response: { result: string } } } + | { functionResponse: { id: string; name: ToolName, response: { output: string } } } )[]; } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 4eec4850..5174e910 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -10,11 +10,11 @@ import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; import { MistralCore } from '@mistralai/mistralai/core.js'; import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js'; -import { GoogleGenerativeAI, Tool as GeminiTool, SchemaType, FunctionDeclaration, FunctionDeclarationSchemaProperty } from '@google/generative-ai'; +import { Tool as GeminiTool, FunctionDeclaration, GoogleGenAI, ThinkingConfig, Schema, Type } from '@google/genai'; import { GoogleAuth } from 'google-auth-library' /* eslint-enable */ -import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'; +import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'; import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js'; import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js'; @@ -245,8 +245,6 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} - console.log('include', includeInPayload) - console.log('reasoningInfo', reasoningInfo) // tools const potentialTools = chatMode !== null ? openAITools(chatMode) : null const nativeToolsObj = potentialTools && specialToolFormat === 'openai-style' ? @@ -642,25 +640,24 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, // ---------------- GEMINI NATIVE IMPLEMENTATION ---------------- - - const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => { const { name, description, params } = toolInfo - const paramsWithType: { [k: string]: FunctionDeclarationSchemaProperty } = {} - for (const key in params) { - paramsWithType[key] = { type: SchemaType.STRING, ...params[key] } - } return { name, description, parameters: { - type: SchemaType.OBJECT, - properties: paramsWithType, + type: Type.OBJECT, + properties: Object.entries(params).reduce((acc, [key, value]) => { + acc[key] = { + type: Type.STRING, + description: value.description + }; + return acc; + }, {} as Record) } } satisfies FunctionDeclaration } - const geminiTools = (chatMode: ChatMode): GeminiTool[] | null => { const allowedTools = availableTools(chatMode) if (!allowedTools || Object.keys(allowedTools).length === 0) return null @@ -700,27 +697,27 @@ const sendGeminiChat = async ({ // reasoningCapabilities, } = getModelCapabilities(providerName, modelName_, overridesOfModel) - const { providerReasoningIOSettings } = getProviderCapabilities(providerName) + // const { providerReasoningIOSettings } = getProviderCapabilities(providerName) // reasoning // const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {} const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here - const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} + // const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} + + const thinkingConfig: ThinkingConfig | undefined = !reasoningInfo?.isReasoningEnabled ? undefined + : reasoningInfo.type === 'budget_slider_value' ? + { thinkingBudget: reasoningInfo.reasoningBudget } + : undefined // tools - const potentialTools = chatMode !== null ? geminiTools(chatMode) : null - const nativeToolsObj = potentialTools && specialToolFormat === 'gemini-style' ? - { tools: potentialTools } as const - : {} + const potentialTools = chatMode !== null ? geminiTools(chatMode) : undefined + const toolConfig = potentialTools && specialToolFormat === 'gemini-style' ? + potentialTools + : undefined // instance - const genAI = new GoogleGenerativeAI( - thisConfig.apiKey - ); - const model = genAI.getGenerativeModel({ - systemInstruction: separateSystemMessage, - model: modelName, - }); + const genAI = new GoogleGenAI({ apiKey: thisConfig.apiKey }); + // manually parse out tool results if XML if (!specialToolFormat) { @@ -735,28 +732,34 @@ const sendGeminiChat = async ({ let toolName = '' let toolParamsStr = '' + let toolId = '' - model.generateContentStream({ - systemInstruction: separateSystemMessage ?? undefined, - contents: messages as any, - ...includeInPayload, - ...nativeToolsObj, + + genAI.models.generateContentStream({ + model: modelName, + config: { + systemInstruction: separateSystemMessage, + thinkingConfig: thinkingConfig, + tools: toolConfig, + }, + contents: messages as GeminiLLMChatMessage[], }) - .then(async ({ stream, response }) => { + .then(async (stream) => { _setAborter(() => { stream.return(fullTextSoFar); }); // Process the stream for await (const chunk of stream) { // message - const newText = chunk.text() ?? '' + const newText = chunk.text ?? '' fullTextSoFar += newText // tool call - const functionCalls = chunk.functionCalls() + const functionCalls = chunk.functionCalls if (functionCalls && functionCalls.length > 0) { const functionCall = functionCalls[0] // Get the first function call toolName = functionCall.name ?? '' toolParamsStr = JSON.stringify(functionCall.args ?? {}) + toolId = functionCall.id ?? '' } // (do not handle reasoning yet) @@ -765,7 +768,7 @@ const sendGeminiChat = async ({ onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, - toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined, + toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined, }) } @@ -773,7 +776,7 @@ const sendGeminiChat = async ({ if (!fullTextSoFar && !fullReasoningSoFar && !toolName) { onError({ message: 'Void: Response from model was empty.', fullError: null }) } else { - const toolId = generateUuid() // gemini does not generate tool IDs. Generate one + if (!toolId) toolId = generateUuid() // ids are empty, but other providers might expect an id const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId) const toolCallObj = toolCall ? { toolCall } : {} onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj }); diff --git a/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts b/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts index ab471631..da7a6956 100644 --- a/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts +++ b/src/vs/workbench/contrib/void/electron-main/voidUpdateMainService.ts @@ -94,7 +94,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ const data = await response.json(); const version = data.tag_name; - const myVersion = `${this._productService.version}.${this._productService.release}` + const myVersion = this._productService.version const latestVersion = version const isUpToDate = myVersion === latestVersion // only makes sense if response.ok