diff --git a/src/vs/workbench/contrib/cortexide/browser/actionIDs.ts b/src/vs/workbench/contrib/cortexide/browser/actionIDs.ts index eb8f414fa79e..89f009230785 100644 --- a/src/vs/workbench/contrib/cortexide/browser/actionIDs.ts +++ b/src/vs/workbench/contrib/cortexide/browser/actionIDs.ts @@ -5,6 +5,8 @@ // Normally you'd want to put these exports in the files that register them, but if you do that you'll get an import order error if you import them in certain cases. // (importing them runs the whole file to get the ID, causing an import error). I guess it's best practice to separate out IDs, pretty annoying... +export const CORTEXIDE_ATTACH_FILE_TO_CHAT_ACTION_ID = 'cortexide.attachFileToChat'; + export const CORTEXIDE_CTRL_L_ACTION_ID = 'cortexide.ctrlLAction'; export const CORTEXIDE_CTRL_K_ACTION_ID = 'cortexide.ctrlKAction'; diff --git a/src/vs/workbench/contrib/cortexide/browser/autocompleteService.ts b/src/vs/workbench/contrib/cortexide/browser/autocompleteService.ts index bf00e42725be..d2af05cc2595 100644 --- a/src/vs/workbench/contrib/cortexide/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/cortexide/browser/autocompleteService.ts @@ -269,7 +269,9 @@ const filterNonCodeContent = (text: string, languageId?: string): string => { filteredLines.push(line); } - return filteredLines.join('\n'); + const result = filteredLines.join('\n'); + // If filtering removed everything, keep the raw model output (issue #27). + return result || text; }; // postprocesses the result diff --git a/src/vs/workbench/contrib/cortexide/browser/chatThreadService.ts b/src/vs/workbench/contrib/cortexide/browser/chatThreadService.ts index 0fd51023867c..3d6894faebe7 100644 --- a/src/vs/workbench/contrib/cortexide/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/cortexide/browser/chatThreadService.ts @@ -7,8 +7,9 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; - import { URI } from '../../../../base/common/uri.js'; + +import { parseChatThreadsFromStorage } from '../common/chatThreadStorageReviver.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { chat_userMessageContent, isABuiltinToolName, builtinToolNames, localToolsetFor, READ_ONLY_SUBAGENT_TOOLS } from '../common/prompt/prompts.js'; @@ -24,6 +25,7 @@ import { IBackgroundAgentsService } from './backgroundAgentsService.js'; import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolResultType, ToolCallParams, ToolName, ToolResult } from '../common/toolsServiceTypes.js'; import { checkToolAllowedInMode } from '../common/toolPermissions.js'; import { classifyCommandRisk, cwdEscapesWorkspace } from '../common/commandRisk.js'; +import { formatTodoReminder } from '../common/todoReminder.js'; import { decideAutoApprove } from '../common/autoApprovePolicy.js'; import { AgentFileOpRecord, AgentFileOpType, FileOpIO, undoFileOpsAfterCheckpoint } from '../common/agentFileOps.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; @@ -558,40 +560,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // !!! this is important for properly restoring URIs and images from storage // should probably re-use code from void/src/vs/base/common/marshalling.ts instead. but this is simple enough private _convertThreadDataFromStorage(threadsStr: string): ChatThreads { - return JSON.parse(threadsStr, (key, value) => { - if (value && typeof value === 'object' && value.$mid === 1) { // $mid is the MarshalledId. $mid === 1 means it is a URI - return URI.from(value); // TODO URI.revive instead of this? - } - // Restore Uint8Array from base64 string for image data - // Only process 'data' keys that are directly under image attachment objects - // Check key === 'data' to match image attachment structure - if (key === 'data') { - if (typeof value === 'string' && value.startsWith('__base64__:')) { - // Handle base64 string format (the normal case) - try { - const base64 = value.substring(11); // Remove '__base64__:' prefix - const binaryString = atob(base64); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes; - } catch (e) { - console.error('Failed to decode base64 image data in storage reviver', e); - return value; // Return original value on error - } - } else if (Array.isArray(value)) { - // Handle case where it's already an array but not Uint8Array - // Only convert if it looks like byte data (all numbers 0-255) - if (value.length > 0 && value.every((v: any) => typeof v === 'number' && v >= 0 && v <= 255)) { - return new Uint8Array(value as number[]); - } - } - // For objects, don't try to convert here - let it be handled later if needed - // This prevents infinite recursion and unexpected conversions - } - return value; - }); + return parseChatThreadsFromStorage(threadsStr); } private _readAllThreads(): ChatThreads | null { @@ -3726,7 +3695,8 @@ Output ONLY the JSON, no other text. Start with { and end with }.` chatMode, repoIndexerPromise: repoIndexerResults ? Promise.resolve(repoIndexerResults) : repoIndexerPromise, subagentSystemPrompt: runCtx?.systemPromptOverride, - allowedToolNames: runCtx?.allowedToolNames + allowedToolNames: runCtx?.allowedToolNames, + todoReminder: formatTodoReminder(this._toolsService.getLatestTodos()) }); } catch (prepErr) { // The first prompt assembly can throw (and has no prior messages to fall back to); @@ -3818,7 +3788,8 @@ Output ONLY the JSON, no other text. Start with { and end with }.` chatMode, repoIndexerPromise: repoIndexerResults ? Promise.resolve(repoIndexerResults) : repoIndexerPromise, subagentSystemPrompt: runCtx?.systemPromptOverride, - allowedToolNames: runCtx?.allowedToolNames + allowedToolNames: runCtx?.allowedToolNames, + todoReminder: formatTodoReminder(this._toolsService.getLatestTodos()) }) if (prep2.messages && prep2.messages.length > 0) { messages = prep2.messages @@ -3950,7 +3921,8 @@ Output ONLY the JSON, no other text. Start with { and end with }.` chatMode, repoIndexerPromise: repoIndexerResults ? Promise.resolve(repoIndexerResults) : repoIndexerPromise, subagentSystemPrompt: runCtx?.systemPromptOverride, - allowedToolNames: runCtx?.allowedToolNames + allowedToolNames: runCtx?.allowedToolNames, + todoReminder: formatTodoReminder(this._toolsService.getLatestTodos()) }); messages = prepResult.messages; separateSystemMessage = prepResult.separateSystemMessage; diff --git a/src/vs/workbench/contrib/cortexide/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/cortexide/browser/convertToLLMMessageService.ts index c2fbd912b186..640ac7036af8 100644 --- a/src/vs/workbench/contrib/cortexide/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/cortexide/browser/convertToLLMMessageService.ts @@ -54,6 +54,7 @@ function uint8ArrayToBase64(data: Uint8Array): string { } } import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js'; +import { effectiveSpecialToolFormat } from '../common/providerToolFormat.js'; import { reParsedToolXMLString, chat_systemMessage, chat_systemMessage_local } from '../common/prompt/prompts.js'; import { isCapableLocalModel } from '../common/routing/codingModelScore.js'; import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; @@ -1240,7 +1241,7 @@ const prepareMessages = (params: { export interface IConvertToLLMMessageService { readonly _serviceBrand: undefined; prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined } - prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null, repoIndexerPromise?: Promise<{ results: string[], metrics: any } | null>, subagentSystemPrompt?: string, allowedToolNames?: string[] }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }> + prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null, repoIndexerPromise?: Promise<{ results: string[], metrics: any } | null>, subagentSystemPrompt?: string, allowedToolNames?: string[], todoReminder?: string }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }> prepareFIMMessage(opts: { messages: LLMFIMMessage, modelSelection: ModelSelection | null, featureName: FeatureName, languageId?: string }): { prefix: string, suffix: string, stopTokens: string[] } startRepoIndexerQuery: (chatMessages: ChatMessage[], chatMode: ChatMode) => Promise<{ results: string[], metrics: any } | null> } @@ -1488,7 +1489,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess systemMessage: enrichedSystemMessage, aiInstructions, supportsSystemMessage, - specialToolFormat, + specialToolFormat: effectiveSpecialToolFormat(specialToolFormat, isLocal), supportsAnthropicReasoning: providerName === 'anthropic', contextWindow: effectiveContextWindow, reservedOutputTokenSpace: effectiveReservedOutput, @@ -1519,7 +1520,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess } } - prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection, repoIndexerPromise, subagentSystemPrompt, allowedToolNames }) => { + prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection, repoIndexerPromise, subagentSystemPrompt, allowedToolNames, todoReminder }) => { if (modelSelection === null) return { messages: [], separateSystemMessage: undefined } const { overridesOfModel } = this.cortexideSettingsService.state @@ -1683,7 +1684,13 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess const modelSelectionOptions = this.cortexideSettingsService.state.optionsOfModelSelection['Chat'][validProviderName]?.[modelName] // Get combined AI instructions - const aiInstructions = this._getCombinedAIInstructions(); + let aiInstructions = this._getCombinedAIInstructions(); + // Re-inject the agent's current todo list as fresh working memory each turn. Folded into the + // per-turn instructions (like rules) rather than the CACHED system message, so it stays current + // as steps complete. Empty -> caller passes undefined -> zero impact (e.g. normal/plan turns). + if (todoReminder) { + aiInstructions = aiInstructions ? `${aiInstructions}\n\n${todoReminder}` : todoReminder; + } const isReasoningEnabled = getIsReasoningEnabledState('Chat', validProviderName, modelName, modelSelectionOptions, overridesOfModel) const reservedOutputTokenSpace = getReservedOutputTokenSpace(validProviderName, modelName, { isReasoningEnabled, overridesOfModel }) let llmMessages = this._chatMessagesToSimpleMessages(chatMessages) @@ -1789,7 +1796,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess // Local providers don't actually return native tool_calls (the calls arrive as XML/JSON text), // so encode prior tool turns with the XML/text format to stay consistent with the system prompt // + parser — otherwise turn 2+ of the agent loop loses all prior tool context (finding #8). - specialToolFormat: isLocalProviderForContext ? undefined : specialToolFormat, + specialToolFormat: effectiveSpecialToolFormat(specialToolFormat, isLocalProviderForContext), supportsAnthropicReasoning: validProviderName === 'anthropic', contextWindow, reservedOutputTokenSpace, diff --git a/src/vs/workbench/contrib/cortexide/browser/extensionTransferService.ts b/src/vs/workbench/contrib/cortexide/browser/extensionTransferService.ts index b8843e98b96c..4e9042a6a64f 100644 --- a/src/vs/workbench/contrib/cortexide/browser/extensionTransferService.ts +++ b/src/vs/workbench/contrib/cortexide/browser/extensionTransferService.ts @@ -11,6 +11,7 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { TransferEditorType, TransferFilesInfo } from './extensionTransferTypes.js'; +import { CORTEXIDE_APP_DATA_DIR, CORTEXIDE_DATA_FOLDER } from '../common/extensionTransferPaths.js'; export interface IExtensionTransferService { @@ -37,11 +38,12 @@ const extensionBlacklist = [ 'codeium.codeium', 'saoudrizwan.claude-dev', // cline 'rooveterinaryinc.roo-cline', // roo - 'supermaven.supermaven' // supermaven + 'supermaven.supermaven', // supermaven + 'void.void', + 'void.voidai', // 'github.copilot', ]; - const isBlacklisted = (fsPath: string | undefined) => { return extensionBlacklist.find(bItem => fsPath?.includes(bItem)) } @@ -195,37 +197,37 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit if (fromEditor === 'VS Code') { return [{ from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', CORTEXIDE_APP_DATA_DIR, 'User', 'settings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', CORTEXIDE_APP_DATA_DIR, 'User', 'keybindings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, CORTEXIDE_DATA_FOLDER, 'extensions'), isExtensions: true, }] } else if (fromEditor === 'Cursor') { return [{ from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', CORTEXIDE_APP_DATA_DIR, 'User', 'settings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', CORTEXIDE_APP_DATA_DIR, 'User', 'keybindings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, CORTEXIDE_DATA_FOLDER, 'extensions'), isExtensions: true, }] } else if (fromEditor === 'Windsurf') { return [{ from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', CORTEXIDE_APP_DATA_DIR, 'User', 'settings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', CORTEXIDE_APP_DATA_DIR, 'User', 'keybindings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, CORTEXIDE_DATA_FOLDER, 'extensions'), isExtensions: true, }] } @@ -238,37 +240,37 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit if (fromEditor === 'VS Code') { return [{ from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', CORTEXIDE_APP_DATA_DIR, 'User', 'settings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', CORTEXIDE_APP_DATA_DIR, 'User', 'keybindings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, CORTEXIDE_DATA_FOLDER, 'extensions'), isExtensions: true, }] } else if (fromEditor === 'Cursor') { return [{ from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', CORTEXIDE_APP_DATA_DIR, 'User', 'settings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', CORTEXIDE_APP_DATA_DIR, 'User', 'keybindings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, CORTEXIDE_DATA_FOLDER, 'extensions'), isExtensions: true, }] } else if (fromEditor === 'Windsurf') { return [{ from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', CORTEXIDE_APP_DATA_DIR, 'User', 'settings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', CORTEXIDE_APP_DATA_DIR, 'User', 'keybindings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, CORTEXIDE_DATA_FOLDER, 'extensions'), isExtensions: true, }] } @@ -283,37 +285,37 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit if (fromEditor === 'VS Code') { return [{ from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, CORTEXIDE_APP_DATA_DIR, 'User', 'settings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, CORTEXIDE_APP_DATA_DIR, 'User', 'keybindings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, CORTEXIDE_DATA_FOLDER, 'extensions'), isExtensions: true, }] } else if (fromEditor === 'Cursor') { return [{ from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, CORTEXIDE_APP_DATA_DIR, 'User', 'settings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, CORTEXIDE_APP_DATA_DIR, 'User', 'keybindings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.cursor', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, CORTEXIDE_DATA_FOLDER, 'extensions'), isExtensions: true, }] } else if (fromEditor === 'Windsurf') { return [{ from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'settings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, CORTEXIDE_APP_DATA_DIR, 'User', 'settings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'keybindings.json'), - to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, CORTEXIDE_APP_DATA_DIR, 'User', 'keybindings.json'), }, { from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.windsurf', 'extensions'), - to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, CORTEXIDE_DATA_FOLDER, 'extensions'), isExtensions: true, }] } diff --git a/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css b/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css index 5e0998bf8591..666b1294b586 100644 --- a/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css +++ b/src/vs/workbench/contrib/cortexide/browser/media/cortexide.css @@ -15,7 +15,9 @@ * or via a theme extension. All values below are defaults only. * =========================================================================== */ -:root { +.void-scope, +.cortex-onboarding-root, +.monaco-workbench .part.auxiliarybar > .content { /* -- Core surfaces (dark theme - light overrides below) ----------------- */ /* Stepped from near-black to visible dark grey so depth reads clearly. */ --cortex-surface-0: #0a0a0d; /* deepest background (onboarding overlay) */ @@ -179,13 +181,19 @@ body.monaco-workbench, box-shadow: var(--cortex-shadow-hairline); } -/* Disable backdrop-filter on Windows title bar - causes stacking context bugs - * that prevent menubar dropdowns from rendering above the title bar. */ -.monaco-workbench.windows .part.titlebar { +/* backdrop-filter on the title bar creates a stacking context that clips menubar + * dropdowns on every platform (issues #8, #68). Keep blur on the status bar only. */ +.monaco-workbench .part.titlebar { backdrop-filter: none; -webkit-backdrop-filter: none; } +/* Ensure menubar / context menus render above the custom title bar chrome. */ +.monaco-workbench .monaco-menu-container, +.monaco-workbench .context-view.monaco-menu-container { + z-index: 2500; +} + .monaco-workbench .part.sidebar > .content .pane-header, .monaco-workbench .part.auxiliarybar > .content .pane-header { padding: var(--cortex-space-sm) var(--cortex-space-md) !important; @@ -424,11 +432,11 @@ body.monaco-workbench, * Rounded, blurred, elevated above the editor surface. * =========================================================================== */ .monaco-workbench .quick-input-widget { - background: color-mix(in srgb, var(--cortex-surface-2) 94%, #000008 6%) !important; - color: var(--cortex-text-base) !important; + background: var(--vscode-quickInput-background, var(--vscode-editorWidget-background)) !important; + color: var(--vscode-quickInput-foreground, var(--vscode-foreground)) !important; border-radius: 20px !important; - border: 1px solid var(--cortex-border-base) !important; - box-shadow: var(--cortex-shadow-soft), inset 0 0 0 1px rgba(255, 255, 255, 0.03) !important; + border: 1px solid var(--vscode-widget-border, var(--vscode-input-border)) !important; + box-shadow: var(--vscode-widget-shadow, 0 8px 32px rgba(0, 0, 0, 0.35)) !important; backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); transform: none !important; @@ -443,20 +451,20 @@ body.monaco-workbench, .monaco-workbench .quick-input-widget .quick-input-header .monaco-inputbox, .monaco-workbench .quick-input-widget .monaco-inputbox { - background: color-mix(in srgb, var(--cortex-surface-1) 92%, #000005 8%) !important; - border: 1px solid var(--cortex-border-weak) !important; + background: var(--vscode-input-background) !important; + border: 1px solid var(--vscode-input-border) !important; border-radius: 12px !important; transition: border-color var(--cortex-transition-fast) !important; } .monaco-workbench .quick-input-widget .quick-input-header .monaco-inputbox:focus-within, .monaco-workbench .quick-input-widget .monaco-inputbox:focus-within { - border-color: var(--cortex-brand) !important; - box-shadow: 0 0 0 1px var(--cortex-brand-soft) !important; + border-color: var(--vscode-focusBorder) !important; + box-shadow: 0 0 0 1px var(--vscode-focusBorder) !important; } .monaco-workbench .quick-input-widget .monaco-inputbox input { background: transparent !important; - color: var(--cortex-text-base) !important; + color: var(--vscode-input-foreground) !important; } .monaco-workbench .quick-input-widget .quick-input-list { @@ -584,9 +592,13 @@ body.monaco-workbench, * correctly on a light background. All other CSS rules stay the same. * =========================================================================== */ -body.vscode-light :root, +body.vscode-light .void-scope, +body.vscode-light .cortex-onboarding-root, +body.vscode-light .monaco-workbench .part.auxiliarybar > .content, body.vscode-light, -.vscode-light :root, +.vscode-light .void-scope, +.vscode-light .cortex-onboarding-root, +.vscode-light .monaco-workbench .part.auxiliarybar > .content, .vscode-light { --cortex-surface-0: #ffffff; --cortex-surface-1: #f7f7fa; @@ -636,9 +648,13 @@ body.vscode-light, } /* High-contrast light theme */ -body.vscode-high-contrast-light :root, +body.vscode-high-contrast-light .void-scope, +body.vscode-high-contrast-light .cortex-onboarding-root, +body.vscode-high-contrast-light .monaco-workbench .part.auxiliarybar > .content, body.vscode-high-contrast-light, -.vscode-high-contrast-light :root, +.vscode-high-contrast-light .void-scope, +.vscode-high-contrast-light .cortex-onboarding-root, +.vscode-high-contrast-light .monaco-workbench .part.auxiliarybar > .content, .vscode-high-contrast-light { --cortex-surface-0: #ffffff; --cortex-surface-1: #f0f0f0; @@ -667,9 +683,13 @@ body.vscode-high-contrast-light, } /* High-contrast dark theme */ -body.vscode-high-contrast :root, +body.vscode-high-contrast .void-scope, +body.vscode-high-contrast .cortex-onboarding-root, +body.vscode-high-contrast .monaco-workbench .part.auxiliarybar > .content, body.vscode-high-contrast, -.vscode-high-contrast :root, +.vscode-high-contrast .void-scope, +.vscode-high-contrast .cortex-onboarding-root, +.vscode-high-contrast .monaco-workbench .part.auxiliarybar > .content, .vscode-high-contrast { --cortex-border-weak: #444444; --cortex-border-base: #666666; diff --git a/src/vs/workbench/contrib/cortexide/browser/ollamaEmbeddingProviderService.ts b/src/vs/workbench/contrib/cortexide/browser/ollamaEmbeddingProviderService.ts index fcb682ec98df..ff22174a905f 100644 --- a/src/vs/workbench/contrib/cortexide/browser/ollamaEmbeddingProviderService.ts +++ b/src/vs/workbench/contrib/cortexide/browser/ollamaEmbeddingProviderService.ts @@ -38,7 +38,10 @@ export class OllamaEmbeddingProviderContribution extends Disposable implements I @ICortexideSettingsService private readonly _settingsService: ICortexideSettingsService, ) { super(); - void this._sync(); + // Defer embedding probe so Ollama IPC does not block the UI during startup (issue #12). + const deferMs = 8_000; + const deferHandle = setTimeout(() => { void this._sync(); }, deferMs); + this._register({ dispose: () => clearTimeout(deferHandle) }); // Re-evaluate when the embedding model setting changes... this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('cortexide.rag.embeddingModel')) { void this._sync(); } diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/ExpressOnboardingFlow.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/ExpressOnboardingFlow.tsx index 1c5d8aee8b1b..66fcde65fc12 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/ExpressOnboardingFlow.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/ExpressOnboardingFlow.tsx @@ -149,6 +149,9 @@ export const ExpressOnboardingFlow = ({ onCustomize, onDismiss }: ExpressOnboard // non-empty default.) settingsService.addModel('ollama', pack.tag); await settingsService.setModelSelectionOfFeature('Chat', { providerName: 'ollama', modelName: pack.tag }); + const fimTag = pack.tag.includes('coder') ? pack.tag : 'qwen2.5-coder:7b'; + settingsService.addModel('ollama', fimTag); + await settingsService.setModelSelectionOfFeature('Autocomplete', { providerName: 'ollama', modelName: fimTag }); settingsService.setGlobalSetting('isOnboardingComplete', true); setPhase('ready'); } catch (e) { diff --git a/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/VoidOnboarding.tsx index 4a466f11cce5..ab8ce1b1f492 100644 --- a/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/cortexide/browser/react/src/onboarding/VoidOnboarding.tsx @@ -18,10 +18,11 @@ import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'; import { FileAccess } from '../../../../../../../base/common/network.js'; import { LocalSetupWizard } from './LocalSetupWizard.js'; import { ExpressOnboardingFlow } from './ExpressOnboardingFlow.js'; +import { applyLlamaServerPreset, tryAutoAssignAutocompleteModel, tryAutoAssignChatModel } from '../../../../common/onboardingHelpers.js'; const OVERRIDE_VALUE = false -const getHeroLogoUri = () => FileAccess.asBrowserUri('vs/workbench/browser/media/cortexide-main.png').toString(true) +const getHeroLogoUri = () => FileAccess.asBrowserUri('vs/workbench/browser/media/code-icon.svg').toString(true) const welcomeHighlights = [ 'Chat + Quick Edit', @@ -72,14 +73,14 @@ export const VoidOnboarding = () => {
@@ -167,10 +168,12 @@ const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzu const freeProviders: ProviderName[] = ['gemini', 'openRouter', 'pollinations', 'moonshot']; +const localTabProviders: ProviderName[] = [...localProviderNames, 'openAICompatible']; + // Data structures for provider tabs const providerNamesOfTab: Record = { Free: freeProviders, - Local: localProviderNames, + Local: localTabProviders, Paid: providerNames.filter(pn => !([...freeProviders, ...localProviderNames, ...cloudProviders] as string[]).includes(pn)) as ProviderName[], 'Cloud/Other': cloudProviders, }; @@ -194,6 +197,8 @@ const featureNameMap: { display: string, featureName: FeatureName }[] = [ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => { const [currentTab, setCurrentTab] = useState('Free'); const settingsState = useSettingsState(); + const accessor = useAccessor(); + const settingsService = accessor.get('ICortexideSettingsService'); const [errorMessage, setErrorMessage] = useState(null); const [showLocalWizard, setShowLocalWizard] = useState(false); @@ -290,6 +295,19 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP )} + {currentTab === 'Local' && !showLocalWizard && ( + + )} {currentTab === 'Local' && showLocalWizard && ( Local models auto-detect when possible. Add custom entries to fine tune routing.
)} - {currentTab === 'Local' && } + {currentTab === 'Local' && } {currentTab === 'Cloud/Other' && }
)} @@ -358,8 +376,11 @@ const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setP
setPageIndex(pageIndex - 1)} /> { - const isDisabled = isFeatureNameDisabled('Chat', settingsState) + onClick={async () => { + let state = settingsState; + state = await tryAutoAssignChatModel(settingsService, state); + await tryAutoAssignAutocompleteModel(settingsService, state); + const isDisabled = isFeatureNameDisabled('Chat', settingsService.state); if (!isDisabled) { setPageIndex(pageIndex + 1); setErrorMessage(null); @@ -596,8 +617,8 @@ const PrimaryActionButton = ({ children, className = '', ringSize, ...props }: {