Merge remote-tracking branch 'origin/main' into pr/steaks/507

This commit is contained in:
Andrew Pareles 2025-05-12 18:20:55 -07:00
commit f8745d8b40
20 changed files with 292 additions and 156 deletions

View file

@ -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!

View file

@ -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`.

View file

@ -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.

37
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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<ResTypes>((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) {

View file

@ -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)

View file

@ -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<IExtensionTransferServi
const extensionBlacklist = [
// ignore extensions
'ms-vscode-remote.remote', // ms-vscode-remote.remote-ssh, ms-vscode-remote.remote-wsl
'ms-vscode.remote', // ms-vscode.remote-explorer
// ignore other AI copilots that could conflict with Void keybindings
'sourcegraph.cody-ai',
'continue.continue',
@ -40,6 +42,9 @@ const extensionBlacklist = [
];
const isBlacklisted = (fsPath: string | undefined) => {
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)

View file

@ -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)
}
}
}

View file

@ -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 <IconShell1

View file

@ -14,7 +14,7 @@ import { isAbsolute } from '../../../../../../../base/common/path.js'
import { separateOutFirstLine } from '../../../../common/helpers/util.js'
import { BlockCode } from '../util/inputs.js'
import { CodespanLocationLink } from '../../../../common/chatThreadServiceTypes.js'
import { voidOpenFileFn } from '../sidebar-tsx/SidebarChat.js'
import { getBasename, getRelative, voidOpenFileFn } from '../sidebar-tsx/SidebarChat.js'
export type ChatMessageLocation = {
@ -89,13 +89,18 @@ const LatexRender = ({ latex }: { latex: string }) => {
// }
}
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 <code
className={`font-mono font-medium rounded-sm bg-void-bg-1 px-1 ${className}`}
onClick={onClick}
{...tooltip ? {
'data-tooltip-id': 'void-tooltip',
'data-tooltip-content': tooltip,
'data-tooltip-place': 'top',
} : {}}
>
{text}
</code>
@ -115,8 +120,11 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(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 <Codespan
text={link?.displayText || text}
text={displayText}
onClick={onClick}
className={link ? 'underline hover:brightness-90 transition-all duration-200 cursor-pointer' : ''}
tooltip={tooltip || undefined}
/>
}

View file

@ -495,12 +495,12 @@ const ScrollToBottomContainer = ({ children, className, style, scrollContainerRe
);
};
const getRelative = (uri: URI, accessor: ReturnType<typeof useAccessor>) => {
export const getRelative = (uri: URI, accessor: ReturnType<typeof useAccessor>) => {
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 (
<div className="w-full px-2 mt-0.5">
<div
@ -1905,7 +1905,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
</SmallProseWrapper>
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
@ -1960,7 +1960,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
}
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
@ -2009,7 +2009,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
@ -2064,7 +2064,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>

View file

@ -132,7 +132,7 @@ const scoreSubsequence = (text: string, pattern: string): number => {
}
export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
const workspaceService = accessor.get('IWorkspaceContextService');
const workspaceFolders = workspaceService.getWorkspace().folders;

View file

@ -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()

View file

@ -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

View file

@ -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. \

View file

@ -56,13 +56,13 @@ export type GeminiLLMChatMessage = {
role: 'model'
parts: (
| { text: string; }
| { functionCall: { name: ToolName, args: object } }
| { functionCall: { id: string; name: ToolName, args: Record<string, unknown> } }
)[];
} | {
role: 'user';
parts: (
| { text: string; }
| { functionResponse: { name: ToolName, response: { result: string } } }
| { functionResponse: { id: string; name: ToolName, response: { output: string } } }
)[];
}

View file

@ -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<string, Schema>)
}
} 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 });

View file

@ -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