diff --git a/.voidrules b/.voidrules index c06e70b4..7bac91a3 100644 --- a/.voidrules +++ b/.voidrules @@ -2,4 +2,7 @@ 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. +You may often need to explore the full repo to find relevant parts of code. +Look for services and built-in functions that you might need to use to solve the problem. + +NEVER lazily cast to 'any' in typescript. Find the correct type to apply and use it. diff --git a/package-lock.json b/package-lock.json index a365be59..7057acfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "cross-spawn": "^7.0.6", "diff": "^7.0.0", "eslint-plugin-react": "^7.37.5", + "google-auth-library": "^9.15.1", "groq-sdk": "^0.20.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -6023,6 +6024,15 @@ "node": "*" } }, + "node_modules/bignumber.js": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -6253,6 +6263,12 @@ "node": ">=0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -8100,6 +8116,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/editorconfig": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.2.tgz", @@ -9340,8 +9365,7 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/extend-shallow": { "version": "3.0.2", @@ -10308,6 +10332,68 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -10944,6 +11030,32 @@ "node": ">= 0.10" } }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -11016,6 +11128,19 @@ "undici-types": "~5.26.4" } }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/gulp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", @@ -13975,6 +14100,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -14095,6 +14229,27 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kerberos": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-2.1.1.tgz", diff --git a/package.json b/package.json index 462cf549..767d6b49 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "cross-spawn": "^7.0.6", "diff": "^7.0.0", "eslint-plugin-react": "^7.37.5", + "google-auth-library": "^9.15.1", "groq-sdk": "^0.20.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/product.json b/product.json index f8b447dc..a2f96552 100644 --- a/product.json +++ b/product.json @@ -1,7 +1,7 @@ { "nameShort": "Void", "nameLong": "Void", - "voidVersion": "1.2.8", + "voidVersion": "1.3.0", "applicationName": "void", "dataFolderName": ".void-editor", "win32MutexName": "voideditor", @@ -38,6 +38,7 @@ "builtInExtensions": [], "linkProtectionTrustedDomains": [ "https://voideditor.com", - "https://voideditor.dev" + "https://voideditor.dev", + "https://github.com/voideditor/void" ] } diff --git a/src/vs/workbench/contrib/void/browser/actionIDs.ts b/src/vs/workbench/contrib/void/browser/actionIDs.ts index b237ecf8..5efe6b25 100644 --- a/src/vs/workbench/contrib/void/browser/actionIDs.ts +++ b/src/vs/workbench/contrib/void/browser/actionIDs.ts @@ -4,3 +4,7 @@ export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction' export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction' + +export const VOID_ACCEPT_DIFF_ACTION_ID = 'void.acceptDiff' + +export const VOID_REJECT_DIFF_ACTION_ID = 'void.rejectDiff' diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index bfd62321..22c86eb6 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -790,6 +790,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ console.log('starting autocomplete...', predictionType) const featureName: FeatureName = 'Autocomplete' + const overridesOfModel = this._settingsService.state.overridesOfModel const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined @@ -807,6 +808,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ }), modelSelection, modelSelectionOptions, + overridesOfModel, logging: { loggingName: 'Autocomplete' }, onText: () => { }, // unused in FIMMessage // onText: async ({ fullText, newText }) => { diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index ae443a56..7f8cc7af 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -641,6 +641,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 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 + const { overridesOfModel } = this._settingsService.state let nMessagesSent = 0 let shouldSendAnotherMessage = true @@ -682,8 +683,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { let shouldRetryLLM = true let nAttempts = 0 while (shouldRetryLLM) { - 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 }) @@ -694,6 +695,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { messages: messages, modelSelection, modelSelectionOptions, + overridesOfModel, logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, separateSystemMessage: separateSystemMessage, onText: ({ fullText, fullReasoning, toolCall }) => { @@ -724,7 +726,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { 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 } @@ -737,7 +738,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { 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) diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 43b91c84..27c054ce 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -6,7 +6,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ChatMessage } from '../common/chatThreadServiceTypes.js'; -import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js'; +import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js'; import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js'; import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; @@ -39,7 +39,7 @@ type SimpleLLMMessage = { const EMPTY_MESSAGE = '(empty message)' const CHARS_PER_TOKEN = 4 -const TRIM_TO_LEN = 60 +const TRIM_TO_LEN = 120 @@ -252,14 +252,14 @@ export type GeminiMessage = { // --- CHAT --- const prepareOpenAIOrAnthropicMessages = ({ - messages, + messages: messages_, systemMessage, aiInstructions, supportsSystemMessage, specialToolFormat, supportsAnthropicReasoning, contextWindow, - maxOutputTokens, + reservedOutputTokenSpace, }: { messages: SimpleLLMMessage[], systemMessage: string, @@ -268,20 +268,32 @@ const prepareOpenAIOrAnthropicMessages = ({ specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean, contextWindow: number, - maxOutputTokens: number | null | undefined, + reservedOutputTokenSpace: number | null | undefined, }): { messages: AnthropicOrOpenAILLMMessage[], separateSystemMessage: string | undefined } => { - maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096 + + reservedOutputTokenSpace = reservedOutputTokenSpace ?? 4_096 // default to 4096 + let messages: (SimpleLLMMessage | { role: 'system', content: string })[] = deepClone(messages_) + + // ================ system message ================ + // A COMPLETE HACK: last message is system message for context purposes + + const sysMsgParts: string[] = [] + if (aiInstructions) sysMsgParts.push(`GUIDELINES (from the user's .voidrules file):\n${aiInstructions}`) + if (systemMessage) sysMsgParts.push(systemMessage) + const combinedSystemMessage = sysMsgParts.join('\n\n') + + messages.unshift({ role: 'system', content: combinedSystemMessage }) // ================ trim ================ - - messages = deepClone(messages) messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content })) + type MesType = (typeof messages)[0] + // ================ fit into context ================ // the higher the weight, the higher the desire to truncate - TRIM HIGHEST WEIGHT MESSAGES const alreadyTrimmedIdxes = new Set() - const weight = (message: SimpleLLMMessage, messages: SimpleLLMMessage[], idx: number) => { + const weight = (message: MesType, messages: MesType[], idx: number) => { const base = message.content.length let multiplier: number @@ -289,22 +301,30 @@ const prepareOpenAIOrAnthropicMessages = ({ if (message.role === 'user') { multiplier *= 1 } + else if (message.role === 'system') { + multiplier *= .01 // very low weight + } else { multiplier *= 10 // llm tokens are far less valuable than user tokens } - // 1st message, last 3 msgs, any already modified message should be low in weight - if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) { + + // any already modified message should not be trimmed again + if (alreadyTrimmedIdxes.has(idx)) { + multiplier = 0 + } + // 1st and last messages should be very low weight + if (idx <= 1 || idx >= messages.length - 1 - 3) { multiplier *= .05 } return base * multiplier } - const _findLargestByWeight = (messages: SimpleLLMMessage[]) => { + const _findLargestByWeight = (messages_: MesType[]) => { let largestIndex = -1 let largestWeight = -Infinity for (let i = 0; i < messages.length; i += 1) { const m = messages[i] - const w = weight(m, messages, i) + const w = weight(m, messages_, i) if (w > largestWeight) { largestWeight = w largestIndex = i @@ -315,7 +335,11 @@ const prepareOpenAIOrAnthropicMessages = ({ let totalLen = 0 for (const m of messages) { totalLen += m.content.length } - const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN + const charsNeedToTrim = totalLen - Math.max( + (contextWindow - reservedOutputTokenSpace) * CHARS_PER_TOKEN, // can be 0, in which case charsNeedToTrim=everything, bad + 4_096 // ensure we don't trim at least 4096 chars (just a random small value) + ) + // <-----------------------------------------> // 0 | | | @@ -335,53 +359,53 @@ const prepareOpenAIOrAnthropicMessages = ({ // if can finish here, do const numCharsWillTrim = m.content.length - TRIM_TO_LEN if (numCharsWillTrim > remainingCharsToTrim) { - m.content = m.content.slice(0, m.content.length - remainingCharsToTrim).trim() + m.content = m.content.slice(0, m.content.length - remainingCharsToTrim - '...'.length).trim() + '...' break } remainingCharsToTrim -= numCharsWillTrim - m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...' + m.content = m.content.substring(0, TRIM_TO_LEN - '...'.length) + '...' alreadyTrimmedIdxes.add(trimIdx) } + // ================ system message hack ================ + const newSysMsg = messages.shift()!.content + + // ================ tools and anthropicReasoning ================ + // SYSTEM MESSAGE HACK: we shifted (removed) the system message role, so now SimpleLLMMessage[] is valid let llmChatMessages: AnthropicOrOpenAILLMMessage[] = [] if (!specialToolFormat) { // XML tool behavior - llmChatMessages = prepareMessages_XML_tools(messages, supportsAnthropicReasoning) + llmChatMessages = prepareMessages_XML_tools(messages as SimpleLLMMessage[], supportsAnthropicReasoning) } else if (specialToolFormat === 'anthropic-style') { - llmChatMessages = prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning) + llmChatMessages = prepareMessages_anthropic_tools(messages as SimpleLLMMessage[], supportsAnthropicReasoning) } else if (specialToolFormat === 'openai-style') { - llmChatMessages = prepareMessages_openai_tools(messages) + llmChatMessages = prepareMessages_openai_tools(messages as SimpleLLMMessage[]) } const llmMessages = llmChatMessages - // ================ system message concat ================ - - // find system messages and concatenate them - const newSystemMessage = aiInstructions ? - `${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES (from the user's .voidrules file):\n${aiInstructions}` - : systemMessage + // ================ system message add as first llmMessage ================ let separateSystemMessageStr: string | undefined = undefined // if supports system message if (supportsSystemMessage) { if (supportsSystemMessage === 'separated') - separateSystemMessageStr = newSystemMessage + separateSystemMessageStr = newSysMsg else if (supportsSystemMessage === 'system-role') - llmMessages.unshift({ role: 'system', content: newSystemMessage }) // add new first message + llmMessages.unshift({ role: 'system', content: newSysMsg }) // add new first message else if (supportsSystemMessage === 'developer-role') - llmMessages.unshift({ role: 'developer', content: newSystemMessage }) // add new first message + llmMessages.unshift({ role: 'developer', content: newSysMsg }) // add new first message } // if does not support system message else { const newFirstMessage = { role: 'user', - content: `\n${newSystemMessage}\n\n${llmMessages[0].content}` + content: `\n${newSysMsg}\n\n${llmMessages[0].content}` } as const llmMessages.splice(0, 1) // delete first message llmMessages.unshift(newFirstMessage) // add new first message @@ -470,7 +494,7 @@ const prepareMessages = (params: { specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined, supportsAnthropicReasoning: boolean, contextWindow: number, - maxOutputTokens: number | null | undefined, + reservedOutputTokenSpace: number | null | undefined, providerName: ProviderName }): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => { @@ -607,20 +631,23 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess prepareLLMSimpleMessages: IConvertToLLMMessageService['prepareLLMSimpleMessages'] = ({ simpleMessages, systemMessage, modelSelection, featureName }) => { if (modelSelection === null) return { messages: [], separateSystemMessage: undefined } + + const { overridesOfModel } = this.voidSettingsService.state + const { providerName, modelName } = modelSelection const { specialToolFormat, contextWindow, supportsSystemMessage, - } = getModelCapabilities(providerName, modelName) + } = getModelCapabilities(providerName, modelName, overridesOfModel) const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] // Get combined AI instructions const aiInstructions = this._getCombinedAIInstructions(); - const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions) - const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled }) + const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel) + const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel }) const { messages, separateSystemMessage } = prepareMessages({ messages: simpleMessages, @@ -630,19 +657,22 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess specialToolFormat, supportsAnthropicReasoning: providerName === 'anthropic', contextWindow, - maxOutputTokens, + reservedOutputTokenSpace, providerName, }) return { messages, separateSystemMessage }; } prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => { if (modelSelection === null) return { messages: [], separateSystemMessage: undefined } + + const { overridesOfModel } = this.voidSettingsService.state + const { providerName, modelName } = modelSelection const { specialToolFormat, contextWindow, supportsSystemMessage, - } = getModelCapabilities(providerName, modelName) + } = getModelCapabilities(providerName, modelName, overridesOfModel) const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat) const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName] @@ -650,8 +680,8 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess // Get combined AI instructions const aiInstructions = this._getCombinedAIInstructions(); - const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions) - const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled }) + const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions, overridesOfModel) + const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel }) const llmMessages = this._chatMessagesToSimpleMessages(chatMessages) const { messages, separateSystemMessage } = prepareMessages({ @@ -662,7 +692,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess specialToolFormat, supportsAnthropicReasoning: providerName === 'anthropic', contextWindow, - maxOutputTokens, + reservedOutputTokenSpace, providerName, }) return { messages, separateSystemMessage }; diff --git a/src/vs/workbench/contrib/void/browser/directoryStrService.ts b/src/vs/workbench/contrib/void/browser/directoryStrService.ts index dcdbaa42..80f70373 100644 --- a/src/vs/workbench/contrib/void/browser/directoryStrService.ts +++ b/src/vs/workbench/contrib/void/browser/directoryStrService.ts @@ -16,14 +16,14 @@ import { ExplorerItem } from '../../files/common/explorerModel.js'; import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js'; -const MAX_FILES_TOTAL = 300; +const MAX_FILES_TOTAL = 1000; -const DEFAULT_MAX_DEPTH = 3; -const DEFAULT_MAX_ITEMS_PER_DIR = 3; const START_MAX_DEPTH = Infinity; const START_MAX_ITEMS_PER_DIR = Infinity; // Add start value as Infinity +const DEFAULT_MAX_DEPTH = 3; +const DEFAULT_MAX_ITEMS_PER_DIR = 3; export interface IDirectoryStrService { readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index a106e601..00c664a2 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -24,6 +24,9 @@ import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, tripleTick, } from '../common/prompt/prompts.js'; +import { IVoidCommandBarService } from './voidCommandBarService.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID } from './actionIDs.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -106,37 +109,42 @@ const removeWhitespaceExceptNewlines = (str: string): string => { // finds block.orig in fileContents and return its range in file // startingAtLine is 1-indexed and inclusive -const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, opts: { startingAtLine?: number, returnType: 'lines' | 'indices' }) => { +// returns 1-indexed lines +const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, opts: { startingAtLine?: number, returnType: 'lines' }) => { - const startLineIdx = (fileContents: string) => opts?.startingAtLine !== undefined ? + const returnAns = (fileContents: string, idx: number) => { + const startLine = numLinesOfStr(fileContents.substring(0, idx + 1)) + const numLines = numLinesOfStr(text) + const endLine = startLine + numLines - 1 + + return [startLine, endLine] as const + } + + const startingAtLineIdx = (fileContents: string) => opts?.startingAtLine !== undefined ? fileContents.split('\n').slice(0, opts.startingAtLine).join('\n').length // num characters in all lines before startingAtLine : 0 // idx = starting index in fileContents - let idx = fileContents.indexOf(text, startLineIdx(fileContents)) + let idx = fileContents.indexOf(text, startingAtLineIdx(fileContents)) + + // if idx was found + if (idx !== -1) { + return returnAns(fileContents, idx) + } + + if (!canFallbackToRemoveWhitespace) + return 'Not found' as const // try to find it ignoring all whitespace this time - if (idx === -1 && canFallbackToRemoveWhitespace) { - text = removeWhitespaceExceptNewlines(text) - fileContents = removeWhitespaceExceptNewlines(fileContents) - idx = fileContents.indexOf(text, startLineIdx(fileContents)); - } + text = removeWhitespaceExceptNewlines(text) + fileContents = removeWhitespaceExceptNewlines(fileContents) + idx = fileContents.indexOf(text, startingAtLineIdx(fileContents)); if (idx === -1) return 'Not found' as const const lastIdx = fileContents.lastIndexOf(text) if (lastIdx !== idx) return 'Not unique' as const - if (opts.returnType === 'lines') { - const startLine = fileContents.substring(0, idx).split('\n').length - const numLines = numLinesOfStr(text) - const endLine = startLine + numLines - 1 - return [startLine, endLine] as const - } - - else if (opts.returnType === 'indices') { - return [idx, idx + text.length] as const - } - else throw new Error(`findTextInCode: Invalid returnType ${opts.returnType}`) + return returnAns(fileContents, idx) } @@ -575,7 +583,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } else { throw new Error('Void 1') } - const buttonsWidget = new AcceptRejectInlineWidget({ + const buttonsWidget = this._instantiationService.createInstance(AcceptRejectInlineWidget, { editor, onAccept: () => { this.acceptDiff({ diffid }) @@ -1208,7 +1216,7 @@ class EditCodeService extends Disposable implements IEditCodeService { onFinishEdit() } - this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: false }) + this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: true }) onDone() } @@ -1331,6 +1339,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const { from, } = opts const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K' + const overridesOfModel = this._settingsService.state.overridesOfModel const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined @@ -1482,6 +1491,7 @@ class EditCodeService extends Disposable implements IEditCodeService { messages, modelSelection, modelSelectionOptions, + overridesOfModel, separateSystemMessage, chatMode: null, // not chat onText: (params) => { @@ -1556,17 +1566,30 @@ class EditCodeService extends Disposable implements IEditCodeService { } - private _errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => { - + /** + * Generates a human-readable error message for an invalid ORIGINAL search block. + */ + private _errContentOfInvalidStr = ( + str: 'Not found' | 'Not unique' | 'Has overlap', + blockOrig: string, + ): string => { const problematicCode = `${tripleTick[0]}\n${JSON.stringify(blockOrig)}\n${tripleTick[1]}` - const descStr = str === `Not found` ? - `The edit was not applied. The text in ORIGINAL must EXACTLY match lines of code in the file, but there was no match for:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code matches a code excerpt exactly.` - : str === `Not unique` ? - `The edit was not applied. The text in ORIGINAL must be unique, but the following ORIGINAL code appears multiple times in the file:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code is unique.` - : str === 'Has overlap' ? - `The edit was not applied. The text in the ORIGINAL blocks must not overlap, but the following ORIGINAL code had overlap with another ORIGINAL string:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code blocks do not overlap.` - : `` + // use a switch for better readability / exhaustiveness check + let descStr: string + switch (str) { + case 'Not found': + descStr = `The edit was not applied. The text in ORIGINAL must EXACTLY match lines of code in the file, but there was no match for:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code matches a code excerpt exactly.` + break + case 'Not unique': + descStr = `The edit was not applied. The text in ORIGINAL must be unique in the file being edited, but the following ORIGINAL code appears multiple times in the file:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code is unique.` + break + case 'Has overlap': + descStr = `The edit was not applied. The text in the ORIGINAL blocks must not overlap, but the following ORIGINAL code had overlap with another ORIGINAL string:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code blocks do not overlap.` + break + default: + descStr = '' + } return descStr } @@ -1578,22 +1601,31 @@ class EditCodeService extends Disposable implements IEditCodeService { const { model } = this._voidModelService.getModel(uri) if (!model) throw new Error(`Error applying Search/Replace blocks: File does not exist.`) const modelStr = model.getValue(EndOfLinePreference.LF) + // .split('\n').map(l => '\t' + l).join('\n') // for testing purposes only, remember to remove this + const modelStrLines = modelStr.split('\n') + + const replacements: { origStart: number; origEnd: number; block: ExtractedSearchReplaceBlock }[] = [] for (const b of blocks) { - const res = findTextInCode(b.orig, modelStr, true, { returnType: 'indices' }) + const res = findTextInCode(b.orig, modelStr, true, { returnType: 'lines' }) if (typeof res === 'string') throw new Error(this._errContentOfInvalidStr(res, b.orig)) - const [i, _] = res + let [startLine, endLine] = res + startLine -= 1 // 0-index + endLine -= 1 - replacements.push({ - origStart: i, - origEnd: i + b.orig.length - 1, // INCLUSIVE - block: b, - }) + // including newline before start + const origStart = (startLine !== 0 ? + modelStrLines.slice(0, startLine).join('\n') + '\n' + : '').length + + // including endline at end + const origEnd = modelStrLines.slice(0, endLine + 1).join('\n').length - 1 + + replacements.push({ origStart, origEnd, block: b }); } - // sort in increasing order replacements.sort((a, b) => a.origStart - b.origStart) @@ -1615,12 +1647,12 @@ class EditCodeService extends Disposable implements IEditCodeService { 'wholeFileRange', { shouldRealignDiffAreas: true } ) - } private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise] | undefined { const { from, applyStr, } = opts const featureName: FeatureName = 'Apply' + const overridesOfModel = this._settingsService.state.overridesOfModel const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined @@ -1900,6 +1932,7 @@ class EditCodeService extends Disposable implements IEditCodeService { messages, modelSelection, modelSelectionOptions, + overridesOfModel, separateSystemMessage, chatMode: null, // not chat onText: (params) => { @@ -1913,6 +1946,8 @@ class EditCodeService extends Disposable implements IEditCodeService { if (blocks.length === 0) { this._notificationService.info(`Void: We ran Fast Apply, but the LLM didn't output any changes.`) } + this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true }) + try { this._instantlyApplySRBlocks(uri, fullText) @@ -2220,31 +2255,81 @@ registerSingleton(IEditCodeService, EditCodeService, InstantiationType.Eager); +const processRawKeybindingText = (keybindingStr: string) => { + return keybindingStr + .replace(/Enter/g, '↵') // ⏎ + .replace(/Backspace/g, '⌫') - - +} class AcceptRejectInlineWidget extends Widget implements IOverlayWidget { - public getId() { return this.ID } - public getDomNode() { return this._domNode; } - public getPosition() { return null } + public getId(): string { + return this.ID || ''; // Ensure we always return a string + } + public getDomNode(): HTMLElement { + return this._domNode; + } + public getPosition() { + return null; + } - private readonly _domNode: HTMLElement; - private readonly editor - private readonly ID - private readonly startLine + private readonly _domNode: HTMLElement; // Using the definite assignment assertion + private readonly editor: ICodeEditor; + private readonly ID: string; + private readonly startLine: number; - constructor({ editor, onAccept, onReject, diffid, startLine, offsetLines }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number, offsetLines: number }) { - super() + constructor( + { editor, onAccept, onReject, diffid, startLine, offsetLines }: { + editor: ICodeEditor; + onAccept: () => void; + onReject: () => void; + diffid: string, + startLine: number, + offsetLines: number + }, + @IVoidCommandBarService private readonly _voidCommandBarService: IVoidCommandBarService, + @IKeybindingService private readonly _keybindingService: IKeybindingService + ) { + super(); - - this.ID = editor.getModel()?.uri.fsPath + diffid; + const uri = editor.getModel()?.uri; + // Initialize with default values + this.ID = '' this.editor = editor; this.startLine = startLine; + if (!uri) { + const { dummyDiv } = dom.h('div@dummyDiv'); + this._domNode = dummyDiv + return; + } + + this.ID = uri.fsPath + diffid; + const lineHeight = editor.getOption(EditorOption.lineHeight); + const getAcceptRejectText = () => { + const acceptKeybinding = this._keybindingService.lookupKeybinding(VOID_ACCEPT_DIFF_ACTION_ID); + const rejectKeybinding = this._keybindingService.lookupKeybinding(VOID_REJECT_DIFF_ACTION_ID); + + const acceptKeybindLabel = processRawKeybindingText(acceptKeybinding && acceptKeybinding.getLabel() || ''); + const rejectKeybindLabel = processRawKeybindingText(rejectKeybinding && rejectKeybinding.getLabel() || '') + + const commandBarStateAtUri = this._voidCommandBarService.stateOfURI[uri.fsPath]; + const selectedDiffIdx = commandBarStateAtUri?.diffIdx ?? 0; // 0th item is selected by default + const thisDiffIdx = commandBarStateAtUri?.sortedDiffIds.indexOf(diffid) ?? null; + + const showLabel = thisDiffIdx === selectedDiffIdx + + const acceptText = `Accept${showLabel ? ` ` + acceptKeybindLabel : ''}`; + const rejectText = `Reject${showLabel ? ` ` + rejectKeybindLabel : ''}`; + + return { acceptText, rejectText } + } + + const { acceptText, rejectText } = getAcceptRejectText() + // Create container div with buttons const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [ dom.h('button@acceptButton', []), @@ -2258,11 +2343,14 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget { buttons.style.paddingRight = '4px'; buttons.style.zIndex = '1'; buttons.style.transform = `translateY(${offsetLines * lineHeight}px)`; + buttons.style.justifyContent = 'flex-end'; + buttons.style.width = '100%'; + buttons.style.pointerEvents = 'none'; // Style accept button acceptButton.onclick = onAccept; - acceptButton.textContent = 'Accept'; + acceptButton.textContent = acceptText; acceptButton.style.backgroundColor = acceptBg; acceptButton.style.border = acceptBorder; acceptButton.style.color = buttonTextColor; @@ -2276,10 +2364,12 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget { acceptButton.style.cursor = 'pointer'; acceptButton.style.height = '100%'; acceptButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)'; + acceptButton.style.pointerEvents = 'auto'; + // Style reject button rejectButton.onclick = onReject; - rejectButton.textContent = 'Reject'; + rejectButton.textContent = rejectText; rejectButton.style.backgroundColor = rejectBg; rejectButton.style.border = rejectBorder; rejectButton.style.color = buttonTextColor; @@ -2293,6 +2383,7 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget { rejectButton.style.cursor = 'pointer'; rejectButton.style.height = '100%'; rejectButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)'; + rejectButton.style.pointerEvents = 'auto'; @@ -2313,16 +2404,28 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget { } // Mount first, then update positions - editor.addOverlayWidget(this); - - - updateTop() - updateLeft() + setTimeout(() => { + updateTop() + updateLeft() + }, 0) this._register(editor.onDidScrollChange(e => { updateTop() })) this._register(editor.onDidChangeModelContent(e => { updateTop() })) this._register(editor.onDidLayoutChange(e => { updateTop(); updateLeft() })) + + // Listen for state changes in the command bar service + this._register(this._voidCommandBarService.onDidChangeState(e => { + if (uri && e.uri.fsPath === uri.fsPath) { + + const { acceptText, rejectText } = getAcceptRejectText() + + acceptButton.textContent = acceptText; + rejectButton.textContent = rejectText; + + } + })); + // mount this widget editor.addOverlayWidget(this); @@ -2330,8 +2433,8 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget { } public override dispose(): void { - this.editor.removeOverlayWidget(this) - super.dispose() + this.editor.removeOverlayWidget(this); + super.dispose(); } } diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 26ff9d2b..4d7fafd6 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -54,6 +54,8 @@ export interface IEditCodeService { diffOfId: Record; acceptOrRejectAllDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept', _addToHistory?: boolean }): void; + acceptDiff({ diffid }: { diffid: number }): void; + rejectDiff({ diffid }: { diffid: number }): void; // events onDidAddOrDeleteDiffZones: Event<{ uri: URI }>; 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 a668dbe7..2d397ecf 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 @@ -325,13 +325,13 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string export const BlockCodeApplyWrapper = ({ children, - initValue, + codeStr, applyBoxId, language, canApply, uri, }: { - initValue: string; + codeStr: string; children: React.ReactNode; applyBoxId: string; canApply: boolean; @@ -364,8 +364,8 @@ export const BlockCodeApplyWrapper = ({
- {currStreamState === 'idle-no-changes' && } - + {currStreamState === 'idle-no-changes' && } +
diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index c7e972b8..96067725 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -319,12 +319,12 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. 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 8733e140..dbb539f0 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 @@ -153,27 +153,32 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => const voidSettingsState = useSettingsState() const modelSelection = voidSettingsState.modelSelectionOfFeature[featureName] + const overridesOfModel = voidSettingsState.overridesOfModel + if (!modelSelection) return null const { modelName, providerName } = modelSelection - const { reasoningCapabilities } = getModelCapabilities(providerName, modelName) - const { canTurnOffReasoning, reasoningBudgetSlider } = reasoningCapabilities || {} + const { reasoningCapabilities } = getModelCapabilities(providerName, modelName, overridesOfModel) + const { canTurnOffReasoning, reasoningSlider: reasoningBudgetSlider } = reasoningCapabilities || {} const modelSelectionOptions = voidSettingsState.optionsOfModelSelection[featureName][providerName]?.[modelName] - const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions) - if (canTurnOffReasoning && !reasoningBudgetSlider) { // if it's just a on/off toggle without a power slider (no models right now) - return null // unused right now - // return
- // {isReasoningEnabled ? 'Thinking' : 'Thinking'} - // { } } - // /> - //
+ const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel) + + if (canTurnOffReasoning && !reasoningBudgetSlider) { // if it's just a on/off toggle without a power slider + return
+ Thinking + { + const isOff = canTurnOffReasoning && !newVal + voidSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff }) + }} + /> +
} - if (reasoningBudgetSlider?.type === 'slider') { // if it's a slider + if (reasoningBudgetSlider?.type === 'budget_slider') { // if it's a slider const { min: min_, max, default: defaultVal } = reasoningBudgetSlider const nSteps = 8 // only used in calculating stepSize, stepSize is what actually matters @@ -184,7 +189,6 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => const value = isReasoningEnabled ? voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal : valueIfOff - return
Thinking step={stepSize} value={value} onChange={(newVal) => { - const disabled = newVal === min && canTurnOffReasoning - voidSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !disabled, reasoningBudget: newVal }) + const isOff = canTurnOffReasoning && newVal === valueIfOff + voidSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff, reasoningBudget: newVal }) }} /> {isReasoningEnabled ? `${value} tokens` : 'Thinking disabled'}
} + if (reasoningBudgetSlider?.type === 'effort_slider') { + + const { values, default: defaultVal } = reasoningBudgetSlider + + const min = canTurnOffReasoning ? -1 : 0 + const max = values.length - 1 + + const currentEffort = voidSettingsState.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]?.reasoningEffort ?? defaultVal + const valueIfOff = -1 + const value = isReasoningEnabled && currentEffort ? values.indexOf(currentEffort) : valueIfOff + + const currentEffortCapitalized = currentEffort.charAt(0).toUpperCase() + currentEffort.slice(1, Infinity) + + return
+ Thinking + { + const isOff = canTurnOffReasoning && newVal === valueIfOff + voidSettingsService.setOptionsOfModelSelection(featureName, modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !isOff, reasoningEffort: values[newVal] ?? undefined }) + }} + /> + {isReasoningEnabled ? `${currentEffortCapitalized}` : 'Thinking disabled'} +
+ } + return null } @@ -216,8 +251,8 @@ const nameOfChatMode = { const detailOfChatMode = { 'normal': 'Normal chat', - 'gather': 'Discover relevant files', - 'agent': 'Edit files and use tools', + 'gather': 'Reads files, but can\'t edit', + 'agent': 'Edits files and uses tools', } @@ -726,6 +761,7 @@ const ToolHeaderWrapper = ({
{/* left */}
{ const { displayContentSoFar, toolCallSoFar, reasoningSoFar } = currThreadStreamState?.llmInfo ?? {} // this is just if it's currently being generated, NOT if it's currently running - const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone && toolCallSoFar.name === 'edit_file' // show loading for slow tools (right now just edit) + const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone // show loading for slow tools (right now just edit) // ----- SIDEBAR CHAT state (local) ----- @@ -2913,11 +2949,9 @@ export const SidebarChat = () => { } }, [onSubmit, onAbort, isRunning]) - - const inputChatArea = onSubmit()} onAbort={onAbort} isStreaming={!!isRunning} isDisabled={isDisabled} @@ -2986,14 +3020,14 @@ export const SidebarChat = () => { {landingPageInput} - {Object.keys(chatThreadsState.allThreads).length > 1 ? // show if there are threads + {Object.keys(chatThreadsState.allThreads).length > 1 ? // show if there are threads -
Previous Threads
+
Previous Threads
: -
Suggestions
+
Suggestions
{initiallySuggestedPromptsHTML}
} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index 5e31c59b..e69c67ab 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -377,7 +377,7 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
{/* spinner */} - {isRunning === 'LLM' || isRunning === 'tool' ? + {isRunning === 'LLM' || isRunning === 'tool' || isRunning === 'idle' ? : isRunning === 'awaiting_user' ? : 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 6381867b..4530e78c 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 @@ -160,7 +160,7 @@ export function getRelativeWorkspacePath(accessor: ReturnType(fun // Focus the textarea first textarea.focus(); - // The most reliable way to simulate typing is to use execCommand - // which will trigger all the appropriate native events - document.execCommand('insertText', false, text + ' '); // add space after too + // Insert the @ to mention text in the editor (we decided not to do this for now) + // document.execCommand('insertText', false, text + ' '); // add space after too // React's onChange relies on a SyntheticEvent system // The best way to ensure it runs is to call callbacks directly @@ -754,7 +753,7 @@ export const VoidInputBox2 = forwardRef(fun } if (e.key === 'Backspace') { // TODO allow user to undo this. - if (!e.currentTarget.value) { // if there is no text, remove a selection + if (!e.currentTarget.value || (e.currentTarget.selectionStart === 0 && e.currentTarget.selectionEnd === 0)) { // if there is no text or cursor is at position 0, remove a selection if (e.metaKey || e.ctrlKey) { // Ctrl+Backspace = remove all chatThreadService.popStagingSelections(Number.MAX_SAFE_INTEGER) } else { // Backspace = pop 1 selection diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx index 40b34d14..dcf97f8e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-editor-widgets-tsx/VoidCommandBar.tsx @@ -111,7 +111,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const { model } = await voidModelService.getModelSafe(nextURI) if (model) { // switch to the URI - editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor) + editorService.openCodeEditor({ resource: nextURI, options: { revealIfVisible: true } }, editor) } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index 81a76547..8c968c62 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -355,7 +355,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {} - const capabilities = getModelCapabilities(providerName, modelName) + const capabilities = getModelCapabilities(providerName, modelName, undefined) const { downloadable, cost, @@ -364,7 +364,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName contextWindow, isUnrecognizedModel, - maxOutputTokens, + reservedOutputTokenSpace, supportsSystemMessage, } = capabilities diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx index 87ddf7d6..3facd801 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx @@ -56,7 +56,7 @@ const MemoizedModelDropdown = ({ featureName, className }: { featureName: Featur useEffect(() => { const oldOptions = oldOptionsRef.current - const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode })) + const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode, overridesOfModel: settingsState.overridesOfModel })) if (!optionsEqual(oldOptions, newOptions)) { setMemoizedOptions(newOptions) 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 df46b296..59a0cc87 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 @@ -3,12 +3,12 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react' +import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; // Added useRef import just in case it was missed, though likely already present import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js' import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js' -import { X, RefreshCw, Loader2, Check, Asterisk } from 'lucide-react' +import { X, RefreshCw, Loader2, Check, Asterisk, Plus } from 'lucide-react' import { URI } from '../../../../../../../base/common/uri.js' import { env } from '../../../../../../../base/common/process.js' import { ModelDropdown } from './ModelDropdown.js' @@ -18,6 +18,7 @@ import { os } from '../../../../common/helpers/systemInfo.js' import { IconLoading } from '../sidebar-tsx/SidebarChat.js' import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js' import Severity from '../../../../../../../base/common/severity.js' +import { getModelCapabilities, ModelOverrides } from '../../../../common/modelCapabilities.js'; const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => { @@ -183,6 +184,180 @@ const ConfirmButton = ({ children, onConfirm, className }: { children: React.Rea ); }; +// ---------------- Simplified Model Settings Dialog ------------------ +// This new dialog replaces the verbose UI with a single JSON override box. +const SimpleModelSettingsDialog = ({ + isOpen, + onClose, + modelInfo, +}: { + isOpen: boolean; + onClose: () => void; + modelInfo: { modelName: string; providerName: ProviderName; type: 'autodetected' | 'custom' | 'default' } | null; +}) => { + if (!isOpen || !modelInfo) return null; + + const { modelName, providerName, type } = modelInfo; + const accessor = useAccessor(); + const settingsState = useSettingsState(); + const mouseDownInsideModal = useRef(false); // Ref to track mousedown origin + const settingsStateService = accessor.get('IVoidSettingsService'); + + // current overrides and defaults + const defaultModelCapabilities = getModelCapabilities(providerName, modelName, undefined); + const currentOverrides = settingsState.overridesOfModel?.[providerName]?.[modelName] ?? undefined; + const { recognizedModelName, isUnrecognizedModel } = defaultModelCapabilities + + // keys of ModelOverrides we allow the user to override + const allowedKeys: (string & (keyof ModelOverrides))[] = [ + 'contextWindow', + 'reservedOutputTokenSpace', + 'supportsSystemMessage', + 'specialToolFormat', + 'supportsFIM', + 'reasoningCapabilities', + ]; + + // Create the placeholder with the default values for allowed keys + const partialDefaults: Partial = {}; + for (const k of allowedKeys) { if (defaultModelCapabilities[k]) partialDefaults[k] = defaultModelCapabilities[k] as any; } + const placeholder = JSON.stringify(partialDefaults, null, 2); + + const [overrideEnabled, setOverrideEnabled] = useState(() => !!currentOverrides); + const [jsonText, setJsonText] = useState(() => currentOverrides ? JSON.stringify(currentOverrides, null, 2) : placeholder); + + const [readOnlyHeight, setReadOnlyHeight] = useState(undefined); + const [errorMsg, setErrorMsg] = useState(null); + + // reset when dialog toggles + useEffect(() => { + if (!isOpen) return; + const cur = settingsState.overridesOfModel?.[providerName]?.[modelName]; + setOverrideEnabled(!!cur); + // If there are overrides, show them; otherwise use default values + setJsonText(cur ? JSON.stringify(cur, null, 2) : placeholder); + setErrorMsg(null); + }, [isOpen, providerName, modelName, settingsState.overridesOfModel, placeholder]); + + const onSave = async () => { + + // if disabled override, reset overrides + if (!overrideEnabled) { + await settingsStateService.setOverridesOfModel(providerName, modelName, undefined); + onClose(); + return; + } + + // enabled overrides + // parse json + let parsedInput: Record + if (jsonText.trim()) { + try { + parsedInput = JSON.parse(jsonText); + } catch (e) { + setErrorMsg('Invalid JSON'); + return; + } + } else { + setErrorMsg('Invalid JSON'); + return; + } + + // only keep allowed keys + const cleaned: Partial = {}; + for (const k of allowedKeys) { + if (!(k in parsedInput)) continue + const isEmpty = parsedInput[k] === '' || parsedInput[k] === null || parsedInput[k] === undefined; + if (!isEmpty && (k in partialDefaults)) { + cleaned[k] = parsedInput[k] as any; + } + } + await settingsStateService.setOverridesOfModel(providerName, modelName, cleaned); + onClose(); + }; + + return ( +
{ + mouseDownInsideModal.current = false; + }} + onMouseUp={() => { + if (!mouseDownInsideModal.current) { + onClose(); + } + mouseDownInsideModal.current = false; + }} + > + {/* MODAL */} +
e.stopPropagation()} // Keep stopping propagation for normal clicks inside + onMouseDown={(e) => { + mouseDownInsideModal.current = true; + e.stopPropagation(); + }} + > +
+

+ Change Defaults for {modelName} ({displayInfoOfProviderName(providerName).title}) +

+ +
+ + {/* Display model recognition status */} +
+ {type === 'default' ? `${modelName} comes packaged with Void, so you shouldn't need to change these settings.` + : isUnrecognizedModel + ? `Model not recognized by Void.` + : `Void recognizes ${modelName} ("${recognizedModelName}").`} +
+ + + {/* override toggle */} +
+ +Override model defaults +
+ + {/* Informational link */} + {overrideEnabled &&
+ +
} + +