Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ export interface WebviewMessage {
| "checkRulesDirectory"
| "checkRulesDirectoryResult"
| "saveCodeIndexSettingsAtomic"
| "setUseWorkspaceConfig"
| "requestCodeIndexSecretStatus"
| "requestCommands"
| "openCommandFile"
Expand Down Expand Up @@ -648,6 +649,8 @@ export interface WebviewMessage {
organizationId?: string | null // For organization switching
useProviderSignup?: boolean // For rooCloudSignIn to use provider signup flow
codeIndexSettings?: {
// Whether to save as workspace-specific config
useWorkspaceConfig?: boolean
// Global state settings
codebaseIndexEnabled: boolean
codebaseIndexQdrantUrl: string
Expand Down Expand Up @@ -750,6 +753,7 @@ export interface IndexingStatus {
workspacePath?: string
workspaceEnabled?: boolean
autoEnableDefault?: boolean
useWorkspaceConfig?: boolean
}

export interface IndexingStatusUpdateMessage {
Expand Down
71 changes: 38 additions & 33 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2321,20 +2321,25 @@ export class ClineProvider
organizationSettingsVersion,
customCondensingPrompt,
codebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
codebaseIndexConfig: {
codebaseIndexEnabled: codebaseIndexConfig?.codebaseIndexEnabled ?? false,
codebaseIndexQdrantUrl: codebaseIndexConfig?.codebaseIndexQdrantUrl ?? "http://localhost:6333",
codebaseIndexEmbedderProvider: codebaseIndexConfig?.codebaseIndexEmbedderProvider ?? "openai",
codebaseIndexEmbedderBaseUrl: codebaseIndexConfig?.codebaseIndexEmbedderBaseUrl ?? "",
codebaseIndexEmbedderModelId: codebaseIndexConfig?.codebaseIndexEmbedderModelId ?? "",
codebaseIndexEmbedderModelDimension: codebaseIndexConfig?.codebaseIndexEmbedderModelDimension ?? 1536,
codebaseIndexOpenAiCompatibleBaseUrl: codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl,
codebaseIndexSearchMaxResults: codebaseIndexConfig?.codebaseIndexSearchMaxResults,
codebaseIndexSearchMinScore: codebaseIndexConfig?.codebaseIndexSearchMinScore,
codebaseIndexBedrockRegion: codebaseIndexConfig?.codebaseIndexBedrockRegion,
codebaseIndexBedrockProfile: codebaseIndexConfig?.codebaseIndexBedrockProfile,
codebaseIndexOpenRouterSpecificProvider: codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider,
},
codebaseIndexConfig: (() => {
// Use workspace-specific config if the current workspace has it enabled
const currentManager = this.getCurrentWorkspaceCodeIndexManager()
const effectiveConfig = currentManager?.getEffectiveConfig() ?? codebaseIndexConfig
return {
codebaseIndexEnabled: effectiveConfig?.codebaseIndexEnabled ?? false,
codebaseIndexQdrantUrl: effectiveConfig?.codebaseIndexQdrantUrl ?? "http://localhost:6333",
codebaseIndexEmbedderProvider: effectiveConfig?.codebaseIndexEmbedderProvider ?? "openai",
codebaseIndexEmbedderBaseUrl: effectiveConfig?.codebaseIndexEmbedderBaseUrl ?? "",
codebaseIndexEmbedderModelId: effectiveConfig?.codebaseIndexEmbedderModelId ?? "",
codebaseIndexEmbedderModelDimension: effectiveConfig?.codebaseIndexEmbedderModelDimension ?? 1536,
codebaseIndexOpenAiCompatibleBaseUrl: effectiveConfig?.codebaseIndexOpenAiCompatibleBaseUrl,
codebaseIndexSearchMaxResults: effectiveConfig?.codebaseIndexSearchMaxResults,
codebaseIndexSearchMinScore: effectiveConfig?.codebaseIndexSearchMinScore,
codebaseIndexBedrockRegion: effectiveConfig?.codebaseIndexBedrockRegion,
codebaseIndexBedrockProfile: effectiveConfig?.codebaseIndexBedrockProfile,
codebaseIndexOpenRouterSpecificProvider: effectiveConfig?.codebaseIndexOpenRouterSpecificProvider,
}
})(),
// Only set mdmCompliant if there's an actual MDM policy
// undefined means no MDM policy, true means compliant, false means non-compliant
mdmCompliant: this.mdmService?.requiresCloudAuth() ? this.checkMdmCompliance() : undefined,
Expand Down Expand Up @@ -2542,25 +2547,25 @@ export class ClineProvider
organizationSettingsVersion,
customCondensingPrompt: stateValues.customCondensingPrompt,
codebaseIndexModels: stateValues.codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
codebaseIndexConfig: {
codebaseIndexEnabled: stateValues.codebaseIndexConfig?.codebaseIndexEnabled ?? false,
codebaseIndexQdrantUrl:
stateValues.codebaseIndexConfig?.codebaseIndexQdrantUrl ?? "http://localhost:6333",
codebaseIndexEmbedderProvider:
stateValues.codebaseIndexConfig?.codebaseIndexEmbedderProvider ?? "openai",
codebaseIndexEmbedderBaseUrl: stateValues.codebaseIndexConfig?.codebaseIndexEmbedderBaseUrl ?? "",
codebaseIndexEmbedderModelId: stateValues.codebaseIndexConfig?.codebaseIndexEmbedderModelId ?? "",
codebaseIndexEmbedderModelDimension:
stateValues.codebaseIndexConfig?.codebaseIndexEmbedderModelDimension,
codebaseIndexOpenAiCompatibleBaseUrl:
stateValues.codebaseIndexConfig?.codebaseIndexOpenAiCompatibleBaseUrl,
codebaseIndexSearchMaxResults: stateValues.codebaseIndexConfig?.codebaseIndexSearchMaxResults,
codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore,
codebaseIndexBedrockRegion: stateValues.codebaseIndexConfig?.codebaseIndexBedrockRegion,
codebaseIndexBedrockProfile: stateValues.codebaseIndexConfig?.codebaseIndexBedrockProfile,
codebaseIndexOpenRouterSpecificProvider:
stateValues.codebaseIndexConfig?.codebaseIndexOpenRouterSpecificProvider,
},
codebaseIndexConfig: (() => {
// Use workspace-specific config if the current workspace has it enabled
const wsManager = this.getCurrentWorkspaceCodeIndexManager()
const effectiveCfg = wsManager?.getEffectiveConfig() ?? stateValues.codebaseIndexConfig
return {
codebaseIndexEnabled: effectiveCfg?.codebaseIndexEnabled ?? false,
codebaseIndexQdrantUrl: effectiveCfg?.codebaseIndexQdrantUrl ?? "http://localhost:6333",
codebaseIndexEmbedderProvider: effectiveCfg?.codebaseIndexEmbedderProvider ?? "openai",
codebaseIndexEmbedderBaseUrl: effectiveCfg?.codebaseIndexEmbedderBaseUrl ?? "",
codebaseIndexEmbedderModelId: effectiveCfg?.codebaseIndexEmbedderModelId ?? "",
codebaseIndexEmbedderModelDimension: effectiveCfg?.codebaseIndexEmbedderModelDimension,
codebaseIndexOpenAiCompatibleBaseUrl: effectiveCfg?.codebaseIndexOpenAiCompatibleBaseUrl,
codebaseIndexSearchMaxResults: effectiveCfg?.codebaseIndexSearchMaxResults,
codebaseIndexSearchMinScore: effectiveCfg?.codebaseIndexSearchMinScore,
codebaseIndexBedrockRegion: effectiveCfg?.codebaseIndexBedrockRegion,
codebaseIndexBedrockProfile: effectiveCfg?.codebaseIndexBedrockProfile,
codebaseIndexOpenRouterSpecificProvider: effectiveCfg?.codebaseIndexOpenRouterSpecificProvider,
}
})(),
profileThresholds: stateValues.profileThresholds ?? {},
lockApiConfigAcrossModes: this.context.workspaceState.get("lockApiConfigAcrossModes", false),
includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true,
Expand Down
113 changes: 80 additions & 33 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2495,16 +2495,15 @@ export const webviewMessageHandler = async (
}

const settings = message.codeIndexSettings
const saveToWorkspace = settings.useWorkspaceConfig === true

try {
// Check if embedder provider has changed
const currentConfig = getGlobalState("codebaseIndexConfig") || {}
const embedderProviderChanged =
currentConfig.codebaseIndexEmbedderProvider !== settings.codebaseIndexEmbedderProvider
const currentCodeIndexManager = provider.getCurrentWorkspaceCodeIndexManager()

// Save global state settings atomically
const globalStateConfig = {
...currentConfig,
// Build the config object (used for both global and workspace saves)
const configObject = {
codebaseIndexEnabled: settings.codebaseIndexEnabled,
codebaseIndexQdrantUrl: settings.codebaseIndexQdrantUrl,
codebaseIndexEmbedderProvider: settings.codebaseIndexEmbedderProvider,
Expand All @@ -2519,59 +2518,71 @@ export const webviewMessageHandler = async (
codebaseIndexOpenRouterSpecificProvider: settings.codebaseIndexOpenRouterSpecificProvider,
}

// Save global state first
await updateGlobalState("codebaseIndexConfig", globalStateConfig)

// Save secrets directly using context proxy
// Build secrets map
const secretEntries: Record<string, string> = {}
if (settings.codeIndexOpenAiKey !== undefined) {
await provider.contextProxy.storeSecret("codeIndexOpenAiKey", settings.codeIndexOpenAiKey)
secretEntries.codeIndexOpenAiKey = settings.codeIndexOpenAiKey
}
if (settings.codeIndexQdrantApiKey !== undefined) {
await provider.contextProxy.storeSecret("codeIndexQdrantApiKey", settings.codeIndexQdrantApiKey)
secretEntries.codeIndexQdrantApiKey = settings.codeIndexQdrantApiKey
}
if (settings.codebaseIndexOpenAiCompatibleApiKey !== undefined) {
await provider.contextProxy.storeSecret(
"codebaseIndexOpenAiCompatibleApiKey",
settings.codebaseIndexOpenAiCompatibleApiKey,
)
secretEntries.codebaseIndexOpenAiCompatibleApiKey = settings.codebaseIndexOpenAiCompatibleApiKey
}
if (settings.codebaseIndexGeminiApiKey !== undefined) {
await provider.contextProxy.storeSecret(
"codebaseIndexGeminiApiKey",
settings.codebaseIndexGeminiApiKey,
)
secretEntries.codebaseIndexGeminiApiKey = settings.codebaseIndexGeminiApiKey
}
if (settings.codebaseIndexMistralApiKey !== undefined) {
await provider.contextProxy.storeSecret(
"codebaseIndexMistralApiKey",
settings.codebaseIndexMistralApiKey,
)
secretEntries.codebaseIndexMistralApiKey = settings.codebaseIndexMistralApiKey
}
if (settings.codebaseIndexVercelAiGatewayApiKey !== undefined) {
await provider.contextProxy.storeSecret(
"codebaseIndexVercelAiGatewayApiKey",
settings.codebaseIndexVercelAiGatewayApiKey,
)
secretEntries.codebaseIndexVercelAiGatewayApiKey = settings.codebaseIndexVercelAiGatewayApiKey
}
if (settings.codebaseIndexOpenRouterApiKey !== undefined) {
await provider.contextProxy.storeSecret(
"codebaseIndexOpenRouterApiKey",
settings.codebaseIndexOpenRouterApiKey,
)
secretEntries.codebaseIndexOpenRouterApiKey = settings.codebaseIndexOpenRouterApiKey
}

// Determine effective previous config for change detection
const effectivePreviousProvider = saveToWorkspace
? currentCodeIndexManager?.getWorkspaceConfig()?.codebaseIndexEmbedderProvider
: currentConfig.codebaseIndexEmbedderProvider
const embedderProviderChanged = effectivePreviousProvider !== settings.codebaseIndexEmbedderProvider

if (saveToWorkspace && currentCodeIndexManager) {
// Save to workspace-specific storage
await currentCodeIndexManager.saveWorkspaceConfig(configObject)

// Merge new secrets with existing workspace secrets
const existingSecrets = currentCodeIndexManager.getWorkspaceSecrets()
await currentCodeIndexManager.saveWorkspaceSecrets({
...existingSecrets,
...secretEntries,
})
} else {
// Save to global state (existing behavior)
const globalStateConfig = {
...currentConfig,
...configObject,
}
await updateGlobalState("codebaseIndexConfig", globalStateConfig)

// Save secrets to global secret store
for (const [key, value] of Object.entries(secretEntries)) {
await provider.contextProxy.storeSecret(key as any, value)
}
}

// Send success response first - settings are saved regardless of validation
await provider.postMessageToWebview({
type: "codeIndexSettingsSaved",
success: true,
settings: globalStateConfig,
settings: configObject,
})

// Update webview state
await provider.postStateToWebview()

// Then handle validation and initialization for the current workspace
const currentCodeIndexManager = provider.getCurrentWorkspaceCodeIndexManager()
if (currentCodeIndexManager) {
// If embedder provider changed, perform proactive validation
if (embedderProviderChanged) {
Expand Down Expand Up @@ -2796,6 +2807,42 @@ export const webviewMessageHandler = async (
}
break
}
case "setUseWorkspaceConfig": {
try {
const manager = provider.getCurrentWorkspaceCodeIndexManager()
if (!manager) {
provider.log("Cannot toggle workspace config: No workspace folder open")
return
}
const enabled = message.bool ?? false
await manager.setUseWorkspaceConfig(enabled)

if (enabled && !manager.getWorkspaceConfig()) {
// When enabling workspace config for the first time, copy global config as starting point
const globalConfig = getGlobalState("codebaseIndexConfig") || {}
await manager.saveWorkspaceConfig(globalConfig)
}

// Force config manager re-creation to pick up the new config source
await manager.recoverFromError()
if (manager.isFeatureEnabled && manager.isFeatureConfigured && manager.isWorkspaceEnabled) {
await manager.initialize(provider.contextProxy)
}

provider.postMessageToWebview({
type: "indexingStatusUpdate",
values: manager.getCurrentStatus(),
})

// Refresh webview state so it picks up the effective config
await provider.postStateToWebview()
} catch (error) {
provider.log(
`Error toggling workspace config: ${error instanceof Error ? error.message : String(error)}`,
)
}
break
}
case "setAutoEnableDefault": {
try {
const manager = provider.getCurrentWorkspaceCodeIndexManager()
Expand Down
50 changes: 40 additions & 10 deletions src/services/code-index/config-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@ import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config"
import { DEFAULT_SEARCH_MIN_SCORE, DEFAULT_MAX_SEARCH_RESULTS } from "./constants"
import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "../../shared/embeddingModels"

/**
* A function that resolves workspace-specific config and secrets.
* When it returns undefined, the global config is used instead.
*/
export type WorkspaceConfigResolver = () => { config: Record<string, any>; secrets: Record<string, string> } | undefined

/**
* Manages configuration state and validation for the code indexing feature.
* Handles loading, validating, and providing access to configuration values.
*
* Supports both global configuration (via ContextProxy) and per-workspace
* configuration overrides (via WorkspaceConfigResolver). When a workspace
* config resolver is provided and returns a value, it takes priority over
* the global configuration.
*/
export class CodeIndexConfigManager {
private codebaseIndexEnabled: boolean = false
Expand All @@ -27,7 +38,10 @@ export class CodeIndexConfigManager {
private searchMinScore?: number
private searchMaxResults?: number

constructor(private readonly contextProxy: ContextProxy) {
constructor(
private readonly contextProxy: ContextProxy,
private readonly workspaceConfigResolver?: WorkspaceConfigResolver,
) {
// Initialize with current configuration to avoid false restart triggers
this._loadAndSetConfiguration()
}
Expand All @@ -44,8 +58,10 @@ export class CodeIndexConfigManager {
* This eliminates code duplication between initializeWithCurrentConfig() and loadConfiguration().
*/
private _loadAndSetConfiguration(): void {
// Load configuration from storage
const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
// Check for workspace-specific config first, fall back to global config
const workspaceSource = this.workspaceConfigResolver?.()

const defaultConfig: Record<string, any> = {
codebaseIndexEnabled: false,
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexEmbedderProvider: "openai",
Expand All @@ -57,6 +73,12 @@ export class CodeIndexConfigManager {
codebaseIndexBedrockProfile: "",
}

// Load configuration from workspace source or global state.
// Use Record<string, any> to allow flexible property access for both
// workspace config (which is a plain object) and global config.
const codebaseIndexConfig: Record<string, any> =
workspaceSource?.config ?? this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? defaultConfig

const {
codebaseIndexEnabled,
codebaseIndexQdrantUrl,
Expand All @@ -67,17 +89,25 @@ export class CodeIndexConfigManager {
codebaseIndexSearchMaxResults,
} = codebaseIndexConfig

const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? ""
const qdrantApiKey = this.contextProxy?.getSecret("codeIndexQdrantApiKey") ?? ""
// Helper to resolve secrets: workspace source takes priority over global secrets
const getSecret = (key: string): string => {
if (workspaceSource?.secrets[key] !== undefined) {
return workspaceSource.secrets[key]
}
return this.contextProxy?.getSecret(key as any) ?? ""
}

const openAiKey = getSecret("codeIndexOpenAiKey")
const qdrantApiKey = getSecret("codeIndexQdrantApiKey")
// Fix: Read OpenAI Compatible settings from the correct location within codebaseIndexConfig
const openAiCompatibleBaseUrl = codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl ?? ""
const openAiCompatibleApiKey = this.contextProxy?.getSecret("codebaseIndexOpenAiCompatibleApiKey") ?? ""
const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? ""
const mistralApiKey = this.contextProxy?.getSecret("codebaseIndexMistralApiKey") ?? ""
const vercelAiGatewayApiKey = this.contextProxy?.getSecret("codebaseIndexVercelAiGatewayApiKey") ?? ""
const openAiCompatibleApiKey = getSecret("codebaseIndexOpenAiCompatibleApiKey")
const geminiApiKey = getSecret("codebaseIndexGeminiApiKey")
const mistralApiKey = getSecret("codebaseIndexMistralApiKey")
const vercelAiGatewayApiKey = getSecret("codebaseIndexVercelAiGatewayApiKey")
const bedrockRegion = codebaseIndexConfig.codebaseIndexBedrockRegion ?? "us-east-1"
const bedrockProfile = codebaseIndexConfig.codebaseIndexBedrockProfile ?? ""
const openRouterApiKey = this.contextProxy?.getSecret("codebaseIndexOpenRouterApiKey") ?? ""
const openRouterApiKey = getSecret("codebaseIndexOpenRouterApiKey")
const openRouterSpecificProvider = codebaseIndexConfig.codebaseIndexOpenRouterSpecificProvider ?? ""

// Update instance variables with configuration
Expand Down
Loading
Loading