From 25a2be3d44e7e7f1dd9f1f8254b4372edaf3524c Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Mon, 22 Sep 2025 20:03:34 -0500 Subject: [PATCH 01/10] Added base RAG changes to cline --- proto/cline/models.proto | 2 + proto/cline/vectors.proto | 23 ++ src/core/api/index.ts | 1 + src/core/api/providers/oca.ts | 26 +- .../controller/vectors/refreshOcaVectors.ts | 96 ++++++ src/core/storage/StateManager.ts | 6 + src/core/storage/state-keys.ts | 2 + src/core/storage/utils/state-helpers.ts | 4 + src/shared/api.ts | 2 + .../models/api-configuration-conversion.ts | 11 + .../settings/providers/OcaProvider.tsx | 191 +++++++++--- .../settings/providers/OcaVectorPicker.tsx | 288 ++++++++++++++++++ .../settings/utils/providerUtils.ts | 5 + 13 files changed, 619 insertions(+), 38 deletions(-) create mode 100644 proto/cline/vectors.proto create mode 100644 src/core/controller/vectors/refreshOcaVectors.ts create mode 100644 webview-ui/src/components/settings/providers/OcaVectorPicker.tsx diff --git a/proto/cline/models.proto b/proto/cline/models.proto index 44c0ccdae78..52b78363492 100644 --- a/proto/cline/models.proto +++ b/proto/cline/models.proto @@ -359,6 +359,7 @@ message ModelsApiConfiguration { optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130; optional string plan_mode_oca_model_id = 131; optional OcaModelInfo plan_mode_oca_model_info = 132; + repeated string plan_mode_oca_vector_ids = 133; // Act mode configurations @@ -395,4 +396,5 @@ message ModelsApiConfiguration { optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230; optional string act_mode_oca_model_id = 231; optional OcaModelInfo act_mode_oca_model_info = 232; + repeated string act_mode_oca_vector_ids = 233; } diff --git a/proto/cline/vectors.proto b/proto/cline/vectors.proto new file mode 100644 index 00000000000..f17b3d376e9 --- /dev/null +++ b/proto/cline/vectors.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package cline; +import "cline/common.proto"; +option java_package = "bot.cline.proto"; +option java_multiple_files = true; + +// Service for vector-related operations +service VectorsService { + // Fetches available vectors from OCA + rpc refreshOcaVectors(StringRequest) returns (VectorStores); +} + +message VectorStoreInfo { + string id = 1; + string name = 2; + string description = 3; +} + +message VectorStores { + map vectors = 1; + optional string error = 2; +} diff --git a/src/core/api/index.ts b/src/core/api/index.ts index e84ca0d50c5..291fe447968 100644 --- a/src/core/api/index.ts +++ b/src/core/api/index.ts @@ -385,6 +385,7 @@ function createHandlerForProvider( ? options.planModeOcaModelInfo?.supportsPromptCache : options.actModeOcaModelInfo?.supportsPromptCache, taskId: options.ulid, + vectorIds: mode === "plan" ? options.planModeOcaVectorIds : options.actModeOcaVectorIds, }) default: return new AnthropicHandler({ diff --git a/src/core/api/providers/oca.ts b/src/core/api/providers/oca.ts index bbc36c0933d..53037613c92 100644 --- a/src/core/api/providers/oca.ts +++ b/src/core/api/providers/oca.ts @@ -18,6 +18,7 @@ export interface OcaHandlerOptions extends CommonApiHandlerOptions { thinkingBudgetTokens?: number ocaUsePromptCache?: boolean taskId?: string + vectorIds?: string[] } export class OcaHandler implements ApiHandler { @@ -180,7 +181,15 @@ export class OcaHandler implements ApiHandler { return message }) - const stream = await client.chat.completions.create({ + const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [] + if (this.getVectorStores().length > 0) { + tools.push({ + type: "file_search", + vector_store_ids: this.getVectorStores(), + } as any) + } + + const requestObject: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: this.options.ocaModelId || liteLlmDefaultModelId, messages: [enhancedSystemMessage, ...enhancedMessages], temperature, @@ -192,7 +201,12 @@ export class OcaHandler implements ApiHandler { ...(this.options.taskId && { litellm_session_id: `cline-${this.options.taskId}`, }), // Add session ID for LiteLLM tracking - }) + tools, + } + + console.log("Input to OCA chat completions: ", requestObject) + + const stream = await client.chat.completions.create(requestObject) const inputCost = (await this.calculateCost(1e6, 0)) || 0 const outputCost = (await this.calculateCost(0, 1e6)) || 0 @@ -258,4 +272,12 @@ export class OcaHandler implements ApiHandler { info: this.options.ocaModelInfo || liteLlmModelInfoSaneDefaults, } } + + getVectorStores() { + if (this.options.vectorIds) { + return this.options.vectorIds + } else { + return [] + } + } } diff --git a/src/core/controller/vectors/refreshOcaVectors.ts b/src/core/controller/vectors/refreshOcaVectors.ts new file mode 100644 index 00000000000..c988f29a19a --- /dev/null +++ b/src/core/controller/vectors/refreshOcaVectors.ts @@ -0,0 +1,96 @@ +import { StringRequest } from "@shared/proto/cline/common" +import { VectorStoreInfo, VectorStores } from "@shared/proto/cline/vectors" +import axios from "axios" +import { HostProvider } from "@/hosts/host-provider" +import { OcaAuthService } from "@/services/auth/oca/OcaAuthService" +import { DEFAULT_OCA_BASE_URL } from "@/services/auth/oca/utils/constants" +import { createOcaHeaders, getProxyAgents } from "@/services/auth/oca/utils/utils" +import { Logger } from "@/services/logging/Logger" +import { ShowMessageType } from "@/shared/proto/index.host" +import { Controller } from ".." + +/** + * Refreshes the Oca models and returns the updated model list + * @param controller The controller instance + * @param request Empty request object + * @returns Response containing the Oca models + */ +export async function refreshOcaVectors(controller: Controller, request: StringRequest): Promise { + const vectors: Record = {} + const ocaAccessToken = await OcaAuthService.getInstance().getAuthToken() + const baseUrl = request.value || DEFAULT_OCA_BASE_URL + const vectorsUrl = `${baseUrl}/vector_store/list` + const headers = await createOcaHeaders(ocaAccessToken!, "models-refresh") + try { + Logger.log(`Making refresh oca model request with customer opc-request-id: ${headers["opc-request-id"]}`) + const response = await axios.get(vectorsUrl, { headers, ...getProxyAgents() }) + if (response.data && response.data.data) { + const vectorIds: string[] = [] + for (const vectorStore of response.data.data) { + const vectorStoreId = vectorStore.vector_store_id + if (typeof vectorStoreId !== "string" || !vectorStoreId) { + continue + } + vectors[vectorStoreId] = VectorStoreInfo.create({ + id: vectorStoreId, + name: vectorStore.vector_store_name, + description: vectorStore.vector_store_description, + }) + vectorIds.push(vectorStoreId) + } + console.log("Oca vectors fetched", vectors) + + // Fetch current config + const apiConfiguration = controller.stateManager.getApiConfiguration() + const updatedConfig = { ...apiConfiguration } + + // Which mode(s) to update? + const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting") + const currentMode = (await controller.getCurrentMode?.()) ?? "plan" + const planModeSelectedVectorId: string[] = apiConfiguration?.planModeOcaVectorIds + ? apiConfiguration?.planModeOcaVectorIds.filter( + (vectorId) => vectorIds.filter((secondVectorId) => vectorId === secondVectorId).length >= 1, + ) + : [] + const actModeSelectedVectorId: string[] = apiConfiguration?.actModeOcaVectorIds + ? apiConfiguration?.actModeOcaVectorIds.filter( + (vectorId) => vectorIds.filter((secondVectorId) => vectorId === secondVectorId).length >= 1, + ) + : [] + + // Save new model selection(s) to configuration object, per plan/act mode setting + if (planActSeparateModelsSetting) { + if (currentMode === "plan") { + updatedConfig.planModeOcaVectorIds = planModeSelectedVectorId + } else { + updatedConfig.actModeOcaVectorIds = actModeSelectedVectorId + } + } else { + updatedConfig.planModeOcaVectorIds = planModeSelectedVectorId + updatedConfig.actModeOcaVectorIds = actModeSelectedVectorId + } + + controller.stateManager.setApiConfiguration(updatedConfig) + + HostProvider.window.showMessage({ + type: ShowMessageType.INFORMATION, + message: `Refreshed Oca knowledge bases from ${baseUrl}`, + }) + } else { + console.error("Invalid response from oca API") + HostProvider.window.showMessage({ + type: ShowMessageType.INFORMATION, + message: `Failed to fetch Oca vectors. Please check your configuration from ${baseUrl}`, + }) + } + } catch (error) { + console.error("Error fetching oca vectors:", error) + const errorMsg = error.message || "Error refreshing Oca knowledge bases" + HostProvider.window.showMessage({ + type: ShowMessageType.ERROR, + message: errorMsg, + }) + return VectorStores.create({ error: errorMsg }) + } + return VectorStores.create({ vectors }) +} diff --git a/src/core/storage/StateManager.ts b/src/core/storage/StateManager.ts index 2288b0ae0d5..06924f71673 100644 --- a/src/core/storage/StateManager.ts +++ b/src/core/storage/StateManager.ts @@ -447,6 +447,7 @@ export class StateManager { planModeVercelAiGatewayModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaVectorIds, // Act mode configurations actModeApiProvider, actModeApiModelId, @@ -481,6 +482,7 @@ export class StateManager { actModeVercelAiGatewayModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaVectorIds, } = apiConfiguration // Batch update global state keys @@ -519,6 +521,7 @@ export class StateManager { planModeVercelAiGatewayModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaVectorIds, // Act mode configuration updates actModeApiProvider, @@ -554,6 +557,7 @@ export class StateManager { actModeVercelAiGatewayModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaVectorIds, // Global state updates awsRegion, @@ -994,6 +998,7 @@ export class StateManager { this.globalStateCache["planModeVercelAiGatewayModelInfo"], planModeOcaModelId: this.globalStateCache["planModeOcaModelId"], planModeOcaModelInfo: this.globalStateCache["planModeOcaModelInfo"], + planModeOcaVectorIds: this.globalStateCache["planModeOcaVectorIds"], // Act mode configurations actModeApiProvider: this.taskStateCache["actModeApiProvider"] || this.globalStateCache["actModeApiProvider"], @@ -1056,6 +1061,7 @@ export class StateManager { this.globalStateCache["actModeVercelAiGatewayModelInfo"], actModeOcaModelId: this.globalStateCache["actModeOcaModelId"], actModeOcaModelInfo: this.globalStateCache["actModeOcaModelInfo"], + actModeOcaVectorIds: this.globalStateCache["actModeOcaVectorIds"], } } } diff --git a/src/core/storage/state-keys.ts b/src/core/storage/state-keys.ts index 41b85117577..7b0ed4cc97d 100644 --- a/src/core/storage/state-keys.ts +++ b/src/core/storage/state-keys.ts @@ -131,6 +131,7 @@ export interface Settings { planModeHuaweiCloudMaasModelInfo: ModelInfo | undefined planModeOcaModelId: string | undefined planModeOcaModelInfo: OcaModelInfo | undefined + planModeOcaVectorIds: string[] // Act mode configurations actModeApiProvider: ApiProvider actModeApiModelId: string | undefined @@ -167,6 +168,7 @@ export interface Settings { actModeVercelAiGatewayModelInfo: ModelInfo | undefined actModeOcaModelId: string | undefined actModeOcaModelInfo: OcaModelInfo | undefined + actModeOcaVectorIds: string[] } export interface Secrets { diff --git a/src/core/storage/utils/state-helpers.ts b/src/core/storage/utils/state-helpers.ts index cd7fdae8115..82db0baf6a3 100644 --- a/src/core/storage/utils/state-helpers.ts +++ b/src/core/storage/utils/state-helpers.ts @@ -305,6 +305,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis >("planModeVercelAiGatewayModelInfo") const planModeOcaModelId = context.globalState.get("planModeOcaModelId") as string | undefined const planModeOcaModelInfo = context.globalState.get("planModeOcaModelInfo") as OcaModelInfo | undefined + const planModeOcaVectorIds = context.globalState.get("planModeOcaVectorIds") as string[] | [] // Act mode configurations const actModeApiProvider = context.globalState.get("actModeApiProvider") const actModeApiModelId = context.globalState.get("actModeApiModelId") @@ -371,6 +372,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis >("actModeVercelAiGatewayModelInfo") const actModeOcaModelId = context.globalState.get("actModeOcaModelId") as string | undefined const actModeOcaModelInfo = context.globalState.get("actModeOcaModelInfo") as OcaModelInfo | undefined + const actModeOcaVectorIds = context.globalState.get("actModeOcaVectorIds") as string[] | [] const sapAiCoreUseOrchestrationMode = context.globalState.get("sapAiCoreUseOrchestrationMode") @@ -486,6 +488,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis planModeVercelAiGatewayModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaVectorIds, // Act mode configurations actModeApiProvider: actModeApiProvider || apiProvider, actModeApiModelId, @@ -520,6 +523,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis actModeVercelAiGatewayModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaVectorIds, // Other global fields focusChainSettings: focusChainSettings || DEFAULT_FOCUS_CHAIN_SETTINGS, diff --git a/src/shared/api.ts b/src/shared/api.ts index 17ab0693471..756b51d16df 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -153,6 +153,7 @@ export interface ApiHandlerOptions { planModeVercelAiGatewayModelInfo?: ModelInfo planModeOcaModelId?: string planModeOcaModelInfo?: OcaModelInfo + planModeOcaVectorIds?: string[] // Act mode configurations // Act mode configurations @@ -188,6 +189,7 @@ export interface ApiHandlerOptions { actModeVercelAiGatewayModelInfo?: ModelInfo actModeOcaModelId?: string actModeOcaModelInfo?: OcaModelInfo + actModeOcaVectorIds?: string[] } export type ApiConfiguration = ApiHandlerOptions & diff --git a/src/shared/proto-conversions/models/api-configuration-conversion.ts b/src/shared/proto-conversions/models/api-configuration-conversion.ts index 92ea00032cd..e29c71f5acb 100644 --- a/src/shared/proto-conversions/models/api-configuration-conversion.ts +++ b/src/shared/proto-conversions/models/api-configuration-conversion.ts @@ -134,6 +134,13 @@ function convertProtoOcaModelInfoToOcaModelInfo(info: ProtoOcaModelInfo | undefi } } +function convertOcaVectorIdsToProtoOcaVectorIds(vectorIds: string[] | undefined): string[] { + if (!vectorIds) { + return [] + } + return vectorIds +} + // Convert application LiteLLMModelInfo to proto LiteLLMModelInfo function convertLiteLLMModelInfoToProto(info: AppLiteLLMModelInfo | undefined): LiteLLMModelInfo | undefined { if (!info) { @@ -503,6 +510,7 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA planModeVercelAiGatewayModelInfo: convertModelInfoToProtoOpenRouter(config.planModeVercelAiGatewayModelInfo), planModeOcaModelId: config.planModeOcaModelId, planModeOcaModelInfo: convertOcaModelInfoToProtoOcaModelInfo(config.planModeOcaModelInfo), + planModeOcaVectorIds: convertOcaVectorIdsToProtoOcaVectorIds(config.planModeOcaVectorIds), // Act mode configurations actModeApiProvider: config.actModeApiProvider ? convertApiProviderToProto(config.actModeApiProvider) : undefined, @@ -538,6 +546,7 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA actModeVercelAiGatewayModelInfo: convertModelInfoToProtoOpenRouter(config.actModeVercelAiGatewayModelInfo), actModeOcaModelId: config.actModeOcaModelId, actModeOcaModelInfo: convertOcaModelInfoToProtoOcaModelInfo(config.actModeOcaModelInfo), + actModeOcaVectorIds: convertOcaVectorIdsToProtoOcaVectorIds(config.actModeOcaVectorIds), } } @@ -655,6 +664,7 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio planModeVercelAiGatewayModelInfo: convertProtoToModelInfo(protoConfig.planModeVercelAiGatewayModelInfo), planModeOcaModelId: protoConfig.planModeOcaModelId, planModeOcaModelInfo: convertProtoOcaModelInfoToOcaModelInfo(protoConfig.planModeOcaModelInfo), + planModeOcaVectorIds: protoConfig.planModeOcaVectorIds, // Act mode configurations actModeApiProvider: @@ -691,5 +701,6 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio actModeVercelAiGatewayModelInfo: convertProtoToModelInfo(protoConfig.actModeVercelAiGatewayModelInfo), actModeOcaModelId: protoConfig.actModeOcaModelId, actModeOcaModelInfo: convertProtoOcaModelInfoToOcaModelInfo(protoConfig.actModeOcaModelInfo), + actModeOcaVectorIds: protoConfig.actModeOcaVectorIds, } } diff --git a/webview-ui/src/components/settings/providers/OcaProvider.tsx b/webview-ui/src/components/settings/providers/OcaProvider.tsx index ebcfa701b89..22610650f89 100644 --- a/webview-ui/src/components/settings/providers/OcaProvider.tsx +++ b/webview-ui/src/components/settings/providers/OcaProvider.tsx @@ -1,11 +1,11 @@ import type { OcaModelInfo } from "@shared/api" -import type { OcaAuthState, OcaUserInfo } from "@shared/proto/index.cline" +import type { OcaAuthState, OcaUserInfo, VectorStoreInfo } from "@shared/proto/index.cline" import { EmptyRequest, StringRequest } from "@shared/proto/index.cline" import { Mode } from "@shared/storage/types" import { VSCodeButton, VSCodeLink, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import React, { useCallback, useEffect, useRef, useState } from "react" import { useExtensionState } from "@/context/ExtensionStateContext" -import { ModelsServiceClient, OcaAccountServiceClient } from "@/services/grpc-client" +import { ModelsServiceClient, OcaAccountServiceClient, VectorsServiceClient } from "@/services/grpc-client" import { VSC_BUTTON_BACKGROUND, VSC_BUTTON_FOREGROUND, @@ -16,6 +16,7 @@ import { import { BaseUrlField } from "../common/BaseUrlField" import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers" import OcaModelPicker from "./OcaModelPicker" +import OcaVectorPicker from "./OcaVectorPicker" /** * Props for the OcaProvider component @@ -112,7 +113,7 @@ function useOcaAuth() { * - Debounces base URL changes to avoid unnecessary calls. * - Guards against race conditions with a requestId and unmount checks. */ -function useOcaModels({ +function useOcaModelsAndKbs({ isAuthenticated, baseUrl, login, @@ -122,38 +123,70 @@ function useOcaModels({ login: () => Promise }) { const [models, setModels] = useState>({}) - const [loading, setLoading] = useState(false) - const [hasError, setHasError] = useState(false) - const [lastRefreshedAt, setLastRefreshedAt] = useState(null) + const [kbs, setKbs] = useState>({}) + const [modelsLoading, setModelsLoading] = useState(false) + const [modelsHasError, setModelsHasError] = useState(false) + const [modelsLastRefreshedAt, setModelsLastRefreshedAt] = useState(null) + const [kbsLoading, setKbsLoading] = useState(false) + const [kbsHasError, setKbsHasError] = useState(false) + const [kbsLastRefreshedAt, setKbsLastRefreshedAt] = useState(null) const reqIdRef = useRef(0) const unmountedRef = useRef(false) const debounceTimerRef = useRef(null) - const doRefresh = useCallback(async (url: string) => { + const doRefreshModels = useCallback(async (url: string) => { const myReqId = ++reqIdRef.current - setLoading(true) - setHasError(false) + setModelsLoading(true) + setModelsHasError(false) try { const resp = await ModelsServiceClient.refreshOcaModels(StringRequest.create({ value: url || "" })) // Only apply if still latest and still mounted if (!unmountedRef.current && myReqId === reqIdRef.current) { if (resp.error) { - setHasError(true) + setModelsHasError(true) } else { setModels(resp.models || {}) - setHasError(false) - setLastRefreshedAt(Date.now()) + setModelsHasError(false) + setModelsLastRefreshedAt(Date.now()) } } } catch (err) { if (!unmountedRef.current && myReqId === reqIdRef.current) { console.error("Failed to refresh Oca models:", err) - setHasError(true) + setModelsHasError(true) } } finally { if (!unmountedRef.current && myReqId === reqIdRef.current) { - setLoading(false) + setModelsLoading(false) + } + } + }, []) + + const doRefreshKbs = useCallback(async (url: string) => { + const myReqId = ++reqIdRef.current + setKbsLoading(true) + setKbsHasError(false) + try { + const resp = await VectorsServiceClient.refreshOcaVectors(StringRequest.create({ value: url || "" })) + // Only apply if still latest and still mounted + if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (resp.error) { + setKbsHasError(true) + } else { + setKbs(resp.vectors || {}) + setKbsHasError(false) + setKbsLastRefreshedAt(Date.now()) + } + } + } catch (err) { + if (!unmountedRef.current && myReqId === reqIdRef.current) { + console.error("Failed to refresh Oca knowledge bases:", err) + setKbsHasError(true) + } + } finally { + if (!unmountedRef.current && myReqId === reqIdRef.current) { + setKbsLoading(false) } } }, []) @@ -169,13 +202,18 @@ function useOcaModels({ if (!isAuthenticated) { // Clear models if logged out; prevent stale data setModels({}) - setLoading(false) - setHasError(false) + setModelsLoading(false) + setModelsHasError(false) + + setKbs({}) + setKbsLoading(false) + setKbsHasError(false) return } debounceTimerRef.current = window.setTimeout(() => { - void doRefresh(baseUrl || "") + void doRefreshModels(baseUrl || "") + void doRefreshKbs(baseUrl || "") }, 250) return () => { @@ -187,12 +225,12 @@ function useOcaModels({ // bump reqId so any in-flight result is ignored reqIdRef.current++ } - }, [isAuthenticated, baseUrl, doRefresh]) + }, [isAuthenticated, baseUrl, doRefreshModels, doRefreshKbs]) // User-initiated refresh with auto login + single retry on failure const refreshModels = useCallback(async () => { - setLoading(true) - setHasError(false) + setModelsLoading(true) + setModelsHasError(false) async function tryRefresh(retry = false): Promise { try { @@ -201,26 +239,67 @@ function useOcaModels({ throw new Error(resp.error) } setModels(resp.models || {}) - setHasError(false) - setLastRefreshedAt(Date.now()) + setModelsHasError(false) + setModelsLastRefreshedAt(Date.now()) + return true + } catch (_err) { + if (!retry) { + await login() // prompt login + return tryRefresh(true) // retry once + } else { + setModelsHasError(true) + } + return false + } finally { + setModelsLoading(false) + } + } + + await tryRefresh() + }, [baseUrl, login]) + + const refreshKbs = useCallback(async () => { + setKbsLoading(true) + setKbsHasError(false) + + async function tryRefresh(retry = false): Promise { + try { + const resp = await VectorsServiceClient.refreshOcaVectors(StringRequest.create({ value: baseUrl || "" })) + if (resp.error) { + throw new Error(resp.error) + } + setKbs(resp.vectors || {}) + setKbsHasError(false) + setKbsLastRefreshedAt(Date.now()) return true } catch (_err) { if (!retry) { await login() // prompt login return tryRefresh(true) // retry once } else { - setHasError(true) + setKbsHasError(true) } return false } finally { - setLoading(false) + setKbsLoading(false) } } await tryRefresh() }, [baseUrl, login]) - return { models, loading, hasError, refreshModels, lastRefreshedAt } + return { + models, + modelsLoading, + modelsHasError, + modelsLastRefreshedAt, + kbs, + kbsLoading, + kbsHasError, + kbsLastRefreshedAt, + refreshModels, + refreshKbs, + } } /** @@ -238,19 +317,28 @@ export const OcaProvider = ({ isPopup, currentMode }: OcaProviderProps) => { const { models: ocaModels, refreshModels, - hasError: ocaHasError, - loading: ocaLoading, - lastRefreshedAt, - } = useOcaModels({ + modelsHasError: ocaModelsHasError, + modelsLoading: ocaModelsLoading, + modelsLastRefreshedAt, + kbs: ocaKbs, + refreshKbs, + kbsHasError: ocaKbsHasError, + kbsLoading: ocaKbsLoading, + kbsLastRefreshedAt, + } = useOcaModelsAndKbs({ isAuthenticated, baseUrl: ocaBaseUrl, login, }) - const handleRefresh = useCallback(async () => { + const handleModelRefresh = useCallback(async () => { await refreshModels() }, [refreshModels]) + const handleKbRefresh = useCallback(async () => { + await refreshKbs() + }, [refreshModels]) + // On first subscription result: if user exists, refresh models once. const didInitialAuthCheckRef = useRef(false) useEffect(() => { @@ -260,9 +348,10 @@ export const OcaProvider = ({ isPopup, currentMode }: OcaProviderProps) => { didInitialAuthCheckRef.current = true if (isAuthenticated) { void refreshModels() + void refreshKbs() } // If user empty, do nothing (no auto login, no refresh) - }, [ready, isAuthenticated, refreshModels]) + }, [ready, isAuthenticated, refreshModels, refreshKbs]) return (
@@ -345,20 +434,50 @@ export const OcaProvider = ({ isPopup, currentMode }: OcaProviderProps) => { apiConfiguration={apiConfiguration} currentMode={currentMode} isPopup={isPopup} - lastRefreshedAt={lastRefreshedAt} - loading={ocaLoading} + lastRefreshedAt={modelsLastRefreshedAt} + loading={ocaModelsLoading} ocaModels={ocaModels} - onRefresh={handleRefresh} + onRefresh={handleModelRefresh} /> - {isAuthenticated && ocaHasError && ( + {isAuthenticated && ocaModelsHasError && (
Failed to refresh models. Check your session or network.
- + + Retry + + { + await login() + }}> + Sign in again + +
+
+ )} + + + + {isAuthenticated && ocaKbsHasError && ( +
+
Failed to refresh knowledge bases. Check your session or network.
+
+ Retry + onRefresh: () => void | Promise + loading?: boolean + lastRefreshedAt?: number | null +} + +const OcaVectorPicker: React.FC = ({ + apiConfiguration, + currentMode, + ocaKbs, + onRefresh, + loading, + lastRefreshedAt, +}: OcaVectorPickerProps) => { + const { handleModeFieldChange } = useApiConfigurationHandlers() + + const handleKbChange = async (kbs: string[]) => { + await handleModeFieldChange({ plan: "planModeOcaVectorIds", act: "actModeOcaVectorIds" }, kbs, currentMode) + } + + const handleRefreshToken = async () => { + await onRefresh?.() + } + + const { selectedVectorIds } = useMemo(() => { + return normalizeApiConfiguration(apiConfiguration, currentMode) + }, [apiConfiguration, currentMode]) + + const kbIds = useMemo(() => { + return Object.keys(ocaKbs || []).sort((a, b) => ocaKbs[a].name.localeCompare(ocaKbs[b].name)) + }, [ocaKbs]) + + const lastRefreshedText = useMemo(() => { + return typeof lastRefreshedAt === "number" ? new Date(lastRefreshedAt).toLocaleTimeString() : null + }, [lastRefreshedAt]) + + const toggleOption = async (option: { id: string; name: string }) => { + const prevVectorIds = selectedVectorIds || [] + const newVectorIds = prevVectorIds.includes(option.id) + ? prevVectorIds.filter((o) => o !== option.id) + : [...prevVectorIds, option.id] + await handleKbChange(newVectorIds) + } + + return ( +
+ + +
+ { + return { + id: kbId, + name: ocaKbs[kbId].name, + } + })} + selectedIds={selectedVectorIds || []} + toggleOption={toggleOption} + /> + + {loading ? "Refreshing…" : "Refresh"} + +
+ {lastRefreshedText ? ( +
+ Last refreshed at {lastRefreshedText} +
+ ) : null} +
+ ) +} + +export default OcaVectorPicker + +interface MultiSelectDropdownProps { + className?: string + id?: string + options: { + id: string + name: string + }[] + selectedIds: string[] + toggleOption: (option: { id: string; name: string }) => Promise +} + +const MultiSelectDropdown: React.FC = ({ options, selectedIds, toggleOption, className, id }) => { + const [open, setOpen] = useState(false) + const wrapperRef = useRef(null) + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + setOpen(false) + } + } + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, []) + + const selectedLabel = + selectedIds.length === 0 || options.length === 0 + ? "Select options..." + : selectedIds.map((selectedId) => options.filter((option) => selectedId === option.id)[0].name).join(", ") + + return ( +
+ {/* VSCode style dropdown button */} +
setOpen((o) => !o)} + onKeyDown={(e) => { + if (e.key === " " || e.key === "Enter") { + setOpen((o) => !o) + } + if (e.key === "Escape") { + setOpen(false) + } + }} + style={{ + display: "flex", + alignItems: "center", + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + minHeight: "100%", + border: "1px solid var(--vscode-dropdown-border, #3c3c3c)", + borderRadius: "calc(var(--corner-radius-round) * 1px)", + background: "var(--vscode-dropdown-background, #1e1e1e)", + color: "var(--vscode-dropdown-foreground, #cccccc)", + minWidth: 0, + padding: "2px 6px 2px 8px", + cursor: "pointer", + fontSize: 12, + fontFamily: "var(--vscode-font-family, inherit)", + boxSizing: "border-box", + outline: open ? "2px solid var(--vscode-focusBorder, #0078d4)" : "none", + margin: 0, + }} + tabIndex={0}> + + {selectedLabel} + + + + +
+ + {open && ( +
+ {options.map((option) => { + const checked = selectedIds.includes(option.id) + return ( +
{ + e.stopPropagation() + await toggleOption(option) + }} + onKeyDown={async (e) => { + if (e.key === " " || e.key === "Enter") { + await toggleOption(option) + } + }} + role="option" + style={{ + display: "flex", + alignItems: "center", + padding: "4px 8px", + margin: 0, + borderRadius: 3, + cursor: "pointer", + background: checked ? "var(--vscode-list-activeSelectionBackground, #094771)" : "transparent", + color: checked + ? "var(--vscode-list-activeSelectionForeground, #fff)" + : "var(--vscode-dropdown-foreground, #cccccc)", + }} + tabIndex={0}> + + + {option.name} + +
+ ) + })} +
+ )} +
+ ) +} diff --git a/webview-ui/src/components/settings/utils/providerUtils.ts b/webview-ui/src/components/settings/utils/providerUtils.ts index 6f919694251..f6e89dbf7a0 100644 --- a/webview-ui/src/components/settings/utils/providerUtils.ts +++ b/webview-ui/src/components/settings/utils/providerUtils.ts @@ -72,6 +72,7 @@ export interface NormalizedApiConfig { selectedProvider: ApiProvider selectedModelId: string selectedModelInfo: ModelInfo + selectedVectorIds?: string[] } /** @@ -352,10 +353,14 @@ export function normalizeApiConfiguration( const ocaModelId = currentMode === "plan" ? apiConfiguration?.planModeOcaModelId : apiConfiguration?.actModeOcaModelId const ocaModelInfo = currentMode === "plan" ? apiConfiguration?.planModeOcaModelInfo : apiConfiguration?.actModeOcaModelInfo + const ocaVectorIds = + currentMode === "plan" ? apiConfiguration?.planModeOcaVectorIds : apiConfiguration?.actModeOcaVectorIds + return { selectedProvider: provider, selectedModelId: ocaModelId || "", selectedModelInfo: ocaModelInfo || liteLlmModelInfoSaneDefaults, + selectedVectorIds: ocaVectorIds || [], } default: return getProviderData(anthropicModels, anthropicDefaultModelId) From e02a9f58e332b08e94558be2ca0ac17134bb337a Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Tue, 23 Sep 2025 10:12:45 -0500 Subject: [PATCH 02/10] Separated KB and Model --- .../settings/providers/OcaProvider.tsx | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/webview-ui/src/components/settings/providers/OcaProvider.tsx b/webview-ui/src/components/settings/providers/OcaProvider.tsx index 22610650f89..b8c83a49d65 100644 --- a/webview-ui/src/components/settings/providers/OcaProvider.tsx +++ b/webview-ui/src/components/settings/providers/OcaProvider.tsx @@ -131,18 +131,23 @@ function useOcaModelsAndKbs({ const [kbsHasError, setKbsHasError] = useState(false) const [kbsLastRefreshedAt, setKbsLastRefreshedAt] = useState(null) - const reqIdRef = useRef(0) - const unmountedRef = useRef(false) - const debounceTimerRef = useRef(null) + const modelReqIdRef = useRef(0) + const modelUnmountedRef = useRef(false) + const modelDebounceTimerRef = useRef(null) + + const kbReqIdRef = useRef(0) + const kbUnmountedRef = useRef(false) + const kbDebounceTimerRef = useRef(null) const doRefreshModels = useCallback(async (url: string) => { - const myReqId = ++reqIdRef.current + const myReqId = ++modelReqIdRef.current setModelsLoading(true) setModelsHasError(false) try { const resp = await ModelsServiceClient.refreshOcaModels(StringRequest.create({ value: url || "" })) + console.log(resp) // Only apply if still latest and still mounted - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!modelUnmountedRef.current && myReqId === modelReqIdRef.current) { if (resp.error) { setModelsHasError(true) } else { @@ -152,25 +157,25 @@ function useOcaModelsAndKbs({ } } } catch (err) { - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!modelUnmountedRef.current && myReqId === modelReqIdRef.current) { console.error("Failed to refresh Oca models:", err) setModelsHasError(true) } } finally { - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!modelUnmountedRef.current && myReqId === modelReqIdRef.current) { setModelsLoading(false) } } }, []) const doRefreshKbs = useCallback(async (url: string) => { - const myReqId = ++reqIdRef.current + const myReqId = ++kbReqIdRef.current setKbsLoading(true) setKbsHasError(false) try { const resp = await VectorsServiceClient.refreshOcaVectors(StringRequest.create({ value: url || "" })) // Only apply if still latest and still mounted - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!kbUnmountedRef.current && myReqId === kbReqIdRef.current) { if (resp.error) { setKbsHasError(true) } else { @@ -180,12 +185,12 @@ function useOcaModelsAndKbs({ } } } catch (err) { - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!kbUnmountedRef.current && myReqId === kbReqIdRef.current) { console.error("Failed to refresh Oca knowledge bases:", err) setKbsHasError(true) } } finally { - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!kbUnmountedRef.current && myReqId === kbReqIdRef.current) { setKbsLoading(false) } } @@ -193,10 +198,15 @@ function useOcaModelsAndKbs({ // Debounce changes to baseUrl or auth useEffect(() => { - unmountedRef.current = false - if (debounceTimerRef.current) { - window.clearTimeout(debounceTimerRef.current) - debounceTimerRef.current = null + modelUnmountedRef.current = false + kbUnmountedRef.current = false + if (modelDebounceTimerRef.current) { + window.clearTimeout(modelDebounceTimerRef.current) + modelDebounceTimerRef.current = null + } + if (kbDebounceTimerRef.current) { + window.clearTimeout(kbDebounceTimerRef.current) + kbDebounceTimerRef.current = null } if (!isAuthenticated) { @@ -211,19 +221,30 @@ function useOcaModelsAndKbs({ return } - debounceTimerRef.current = window.setTimeout(() => { + modelDebounceTimerRef.current = window.setTimeout(() => { void doRefreshModels(baseUrl || "") + }, 250) + + kbDebounceTimerRef.current = window.setTimeout(() => { void doRefreshKbs(baseUrl || "") }, 250) return () => { - unmountedRef.current = true - if (debounceTimerRef.current) { - window.clearTimeout(debounceTimerRef.current) - debounceTimerRef.current = null + modelUnmountedRef.current = true + if (modelDebounceTimerRef.current) { + window.clearTimeout(modelDebounceTimerRef.current) + modelDebounceTimerRef.current = null + } + // bump reqId so any in-flight result is ignored + modelReqIdRef.current++ + + kbUnmountedRef.current = true + if (kbDebounceTimerRef.current) { + window.clearTimeout(kbDebounceTimerRef.current) + kbDebounceTimerRef.current = null } // bump reqId so any in-flight result is ignored - reqIdRef.current++ + kbReqIdRef.current++ } }, [isAuthenticated, baseUrl, doRefreshModels, doRefreshKbs]) From 83139b57350c79746339ee3cb15d005ded520955 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Tue, 23 Sep 2025 11:12:34 -0500 Subject: [PATCH 03/10] Made dropdown unselectable when there are no knowledge bases to choose from --- .../settings/providers/OcaVectorPicker.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/settings/providers/OcaVectorPicker.tsx b/webview-ui/src/components/settings/providers/OcaVectorPicker.tsx index f8ee6bd4df5..a208898cf1f 100644 --- a/webview-ui/src/components/settings/providers/OcaVectorPicker.tsx +++ b/webview-ui/src/components/settings/providers/OcaVectorPicker.tsx @@ -148,16 +148,17 @@ const MultiSelectDropdown: React.FC = ({ options, sele }}> {/* VSCode style dropdown button */}
setOpen((o) => !o)} + onClick={() => options.length > 0 && setOpen((o) => !o)} onKeyDown={(e) => { - if (e.key === " " || e.key === "Enter") { - setOpen((o) => !o) - } if (e.key === "Escape") { setOpen(false) } + if ((e.key === " " || e.key === "Enter") && options.length > 0) { + setOpen((o) => !o) + } }} style={{ display: "flex", @@ -174,12 +175,13 @@ const MultiSelectDropdown: React.FC = ({ options, sele color: "var(--vscode-dropdown-foreground, #cccccc)", minWidth: 0, padding: "2px 6px 2px 8px", - cursor: "pointer", + cursor: options.length == 0 ? "not-allowed" : "pointer", fontSize: 12, fontFamily: "var(--vscode-font-family, inherit)", boxSizing: "border-box", outline: open ? "2px solid var(--vscode-focusBorder, #0078d4)" : "none", margin: 0, + opacity: options.length == 0 ? 0.6 : 1, }} tabIndex={0}> Date: Wed, 24 Sep 2025 10:47:17 -0500 Subject: [PATCH 04/10] Made changes to fix axios settings --- .../controller/models/refreshOcaModels.ts | 11 +++++-- .../controller/vectors/refreshOcaVectors.ts | 11 +++++-- .../auth/oca/providers/OcaAuthProvider.ts | 14 ++++---- src/services/auth/oca/utils/utils.ts | 32 +++++++------------ 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/core/controller/models/refreshOcaModels.ts b/src/core/controller/models/refreshOcaModels.ts index b4edb371aa0..2e6270fb39a 100644 --- a/src/core/controller/models/refreshOcaModels.ts +++ b/src/core/controller/models/refreshOcaModels.ts @@ -4,7 +4,7 @@ import axios from "axios" import { HostProvider } from "@/hosts/host-provider" import { OcaAuthService } from "@/services/auth/oca/OcaAuthService" import { DEFAULT_OCA_BASE_URL } from "@/services/auth/oca/utils/constants" -import { createOcaHeaders, getProxyAgents } from "@/services/auth/oca/utils/utils" +import { createOcaHeaders, getAxiosSettings } from "@/services/auth/oca/utils/utils" import { Logger } from "@/services/logging/Logger" import { ShowMessageType } from "@/shared/proto/index.host" import { Controller } from ".." @@ -25,12 +25,19 @@ export async function refreshOcaModels(controller: Controller, request: StringRe const models: Record = {} let defaultModelId: string | undefined const ocaAccessToken = await OcaAuthService.getInstance().getAuthToken() + if (!ocaAccessToken) { + HostProvider.window.showMessage({ + type: ShowMessageType.ERROR, + message: "Not authenticated with OCA. Please sign in first.", + }) + return OcaCompatibleModelInfo.create({ error: "Not authenticated with OCA" }) + } const baseUrl = request.value || DEFAULT_OCA_BASE_URL const modelsUrl = `${baseUrl}/v1/model/info` const headers = await createOcaHeaders(ocaAccessToken!, "models-refresh") try { Logger.log(`Making refresh oca model request with customer opc-request-id: ${headers["opc-request-id"]}`) - const response = await axios.get(modelsUrl, { headers, ...getProxyAgents() }) + const response = await axios.get(modelsUrl, { headers, ...getAxiosSettings() }) if (response.data?.data) { if (response.data.data.length === 0) { HostProvider.window.showMessage({ diff --git a/src/core/controller/vectors/refreshOcaVectors.ts b/src/core/controller/vectors/refreshOcaVectors.ts index c988f29a19a..99e8ba9c8d3 100644 --- a/src/core/controller/vectors/refreshOcaVectors.ts +++ b/src/core/controller/vectors/refreshOcaVectors.ts @@ -4,7 +4,7 @@ import axios from "axios" import { HostProvider } from "@/hosts/host-provider" import { OcaAuthService } from "@/services/auth/oca/OcaAuthService" import { DEFAULT_OCA_BASE_URL } from "@/services/auth/oca/utils/constants" -import { createOcaHeaders, getProxyAgents } from "@/services/auth/oca/utils/utils" +import { createOcaHeaders, getAxiosSettings } from "@/services/auth/oca/utils/utils" import { Logger } from "@/services/logging/Logger" import { ShowMessageType } from "@/shared/proto/index.host" import { Controller } from ".." @@ -18,12 +18,19 @@ import { Controller } from ".." export async function refreshOcaVectors(controller: Controller, request: StringRequest): Promise { const vectors: Record = {} const ocaAccessToken = await OcaAuthService.getInstance().getAuthToken() + if (!ocaAccessToken) { + HostProvider.window.showMessage({ + type: ShowMessageType.ERROR, + message: "Not authenticated with OCA. Please sign in first.", + }) + return VectorStores.create({ error: "Not authenticated with OCA" }) + } const baseUrl = request.value || DEFAULT_OCA_BASE_URL const vectorsUrl = `${baseUrl}/vector_store/list` const headers = await createOcaHeaders(ocaAccessToken!, "models-refresh") try { Logger.log(`Making refresh oca model request with customer opc-request-id: ${headers["opc-request-id"]}`) - const response = await axios.get(vectorsUrl, { headers, ...getProxyAgents() }) + const response = await axios.get(vectorsUrl, { headers, ...getAxiosSettings() }) if (response.data && response.data.data) { const vectorIds: string[] = [] for (const vectorStore of response.data.data) { diff --git a/src/services/auth/oca/providers/OcaAuthProvider.ts b/src/services/auth/oca/providers/OcaAuthProvider.ts index 8dad31109ad..ce73fb440c8 100644 --- a/src/services/auth/oca/providers/OcaAuthProvider.ts +++ b/src/services/auth/oca/providers/OcaAuthProvider.ts @@ -2,7 +2,7 @@ import { OcaAuthState, OcaUserInfo } from "@shared/proto/cline/oca_account" import axios from "axios" import { jwtDecode } from "jwt-decode" import { Controller } from "@/core/controller" -import { getProxyAgents } from "@/services/auth/oca/utils/utils" +import { getAxiosSettings } from "@/services/auth/oca/utils/utils" import { generateCodeVerifier, generateRandomString, pkceChallengeFromVerifier } from "../utils/utils" @@ -93,7 +93,7 @@ export class OcaAuthProvider { } try { const { idcs_url, client_id } = this._config - const discovery = await axios.get(`${idcs_url}/.well-known/openid-configuration`, { ...getProxyAgents() }) + const discovery = await axios.get(`${idcs_url}/.well-known/openid-configuration`, { ...getAxiosSettings() }) const tokenEndpoint = discovery.data.token_endpoint const params: any = { grant_type: "refresh_token", @@ -102,7 +102,7 @@ export class OcaAuthProvider { } const tokenResponse = await axios.post(tokenEndpoint, new URLSearchParams(params), { headers: { "Content-Type": "application/x-www-form-urlencoded" }, - ...getProxyAgents(), + ...getAxiosSettings(), }) const accessToken = tokenResponse.data.access_token const userInfo: OcaUserInfo = await this.getUserAccountInfo(accessToken) @@ -159,7 +159,7 @@ export class OcaAuthProvider { } const { code_verifier, nonce, redirect_uri } = entry OcaAuthProvider.pkceStateMap.delete(state) - const discovery = await axios.get(`${idcs_url}/.well-known/openid-configuration`, { ...getProxyAgents() }) + const discovery = await axios.get(`${idcs_url}/.well-known/openid-configuration`, { ...getAxiosSettings() }) const tokenEndpoint = discovery.data.token_endpoint const params: any = { grant_type: "authorization_code", @@ -170,7 +170,7 @@ export class OcaAuthProvider { } const tokenResponse = await axios.post(tokenEndpoint, new URLSearchParams(params), { headers: { "Content-Type": "application/x-www-form-urlencoded" }, - ...getProxyAgents(), + ...getAxiosSettings(), }) // Step 1: Nonce validation const idToken = tokenResponse.data.id_token @@ -179,9 +179,11 @@ export class OcaAuthProvider { if (decoded.nonce !== nonce) { throw new Error("OIDC nonce verification failed") } + } else { + throw new Error("No ID token received from OCA") } - // Step 2: Get access_token (this is what you'll use for APIs) + //Step 2: Get access_token (this is what you'll use for APIs) const accessToken = tokenResponse.data.access_token const refreshToken = tokenResponse.data.refresh_token if (refreshToken) { diff --git a/src/services/auth/oca/utils/utils.ts b/src/services/auth/oca/utils/utils.ts index 3e262448e4d..ad0004c7c7e 100644 --- a/src/services/auth/oca/utils/utils.ts +++ b/src/services/auth/oca/utils/utils.ts @@ -1,5 +1,8 @@ import crypto from "crypto" import fs from "fs" +import { type JwtPayload, jwtDecode } from "jwt-decode" +import { HostProvider } from "@/hosts/host-provider" +import { ExtensionRegistryInfo } from "@/registry" import { DEFAULT_IDCS_CLIENT_ID, DEFAULT_IDCS_PORT_CANDIDATES, @@ -70,11 +73,6 @@ export function pkceChallengeFromVerifier(verifier: string): string { .replace(/=+$/, "") } -import { HttpsProxyAgent } from "https-proxy-agent" -import { type JwtPayload, jwtDecode } from "jwt-decode" -import * as vscode from "vscode" -import { name, version } from "../../../../../package.json" - /** * Generates a compliant customer opc-request-id segment. * @@ -117,32 +115,26 @@ export async function generateOpcRequestId(taskId: string, token: string): Promi export async function createOcaHeaders(accessToken: string, taskId: string): Promise> { const opcRequestId = await generateOpcRequestId(taskId, accessToken) + const host = await HostProvider.env.getHostVersion({}) + const clineVersion = ExtensionRegistryInfo.version return { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", client: "Cline", - "client-version": `${name}-${version}`, - "client-ide": vscode.env.appName, - "client-ide-version": vscode.version, + "client-version": `${clineVersion}`, + "client-ide": host.platform || "unknown", + "client-ide-version": host.version || "unknown", "opc-request-id": opcRequestId, } } /** - * Proxy helpers for HTTPS/HTTP proxies via environment variables. - * - Prioritizes HTTPS_PROXY over HTTP_PROXY - * - Returns axios-compatible agent options when a proxy is configured + * + * @returns Axios settings including fetch adapter for compatibility */ -export function getProxyUrl(): string | undefined { - return process.env.HTTPS_PROXY || process.env.HTTP_PROXY -} - -export function getProxyAgents(): { httpAgent?: any; httpsAgent?: any } { - const proxyUrl = getProxyUrl() - if (!proxyUrl) return {} - const agent = new HttpsProxyAgent(proxyUrl) - return { httpAgent: agent as any, httpsAgent: agent as any } +export function getAxiosSettings(): { adapter?: any } { + return { adapter: "fetch" as any } } /** From 9aeb92a505d25cca51032390fb631cf747ebea66 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Mon, 22 Sep 2025 20:03:34 -0500 Subject: [PATCH 05/10] Added base RAG changes to cline --- proto/cline/models.proto | 2 + proto/cline/vectors.proto | 23 ++ src/core/api/index.ts | 1 + src/core/api/providers/oca.ts | 26 +- .../controller/vectors/refreshOcaVectors.ts | 96 ++++++ src/core/storage/StateManager.ts | 6 + src/core/storage/state-keys.ts | 2 + src/core/storage/utils/state-helpers.ts | 4 + src/shared/api.ts | 2 + .../models/api-configuration-conversion.ts | 11 + .../settings/providers/OcaProvider.tsx | 191 +++++++++--- .../settings/providers/OcaVectorPicker.tsx | 288 ++++++++++++++++++ .../settings/utils/providerUtils.ts | 5 + 13 files changed, 619 insertions(+), 38 deletions(-) create mode 100644 proto/cline/vectors.proto create mode 100644 src/core/controller/vectors/refreshOcaVectors.ts create mode 100644 webview-ui/src/components/settings/providers/OcaVectorPicker.tsx diff --git a/proto/cline/models.proto b/proto/cline/models.proto index 44c0ccdae78..52b78363492 100644 --- a/proto/cline/models.proto +++ b/proto/cline/models.proto @@ -359,6 +359,7 @@ message ModelsApiConfiguration { optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 130; optional string plan_mode_oca_model_id = 131; optional OcaModelInfo plan_mode_oca_model_info = 132; + repeated string plan_mode_oca_vector_ids = 133; // Act mode configurations @@ -395,4 +396,5 @@ message ModelsApiConfiguration { optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 230; optional string act_mode_oca_model_id = 231; optional OcaModelInfo act_mode_oca_model_info = 232; + repeated string act_mode_oca_vector_ids = 233; } diff --git a/proto/cline/vectors.proto b/proto/cline/vectors.proto new file mode 100644 index 00000000000..f17b3d376e9 --- /dev/null +++ b/proto/cline/vectors.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package cline; +import "cline/common.proto"; +option java_package = "bot.cline.proto"; +option java_multiple_files = true; + +// Service for vector-related operations +service VectorsService { + // Fetches available vectors from OCA + rpc refreshOcaVectors(StringRequest) returns (VectorStores); +} + +message VectorStoreInfo { + string id = 1; + string name = 2; + string description = 3; +} + +message VectorStores { + map vectors = 1; + optional string error = 2; +} diff --git a/src/core/api/index.ts b/src/core/api/index.ts index a02508e554f..b7b494e57b9 100644 --- a/src/core/api/index.ts +++ b/src/core/api/index.ts @@ -386,6 +386,7 @@ function createHandlerForProvider( ? options.planModeOcaModelInfo?.supportsPromptCache : options.actModeOcaModelInfo?.supportsPromptCache, taskId: options.ulid, + vectorIds: mode === "plan" ? options.planModeOcaVectorIds : options.actModeOcaVectorIds, }) default: return new AnthropicHandler({ diff --git a/src/core/api/providers/oca.ts b/src/core/api/providers/oca.ts index bbc36c0933d..53037613c92 100644 --- a/src/core/api/providers/oca.ts +++ b/src/core/api/providers/oca.ts @@ -18,6 +18,7 @@ export interface OcaHandlerOptions extends CommonApiHandlerOptions { thinkingBudgetTokens?: number ocaUsePromptCache?: boolean taskId?: string + vectorIds?: string[] } export class OcaHandler implements ApiHandler { @@ -180,7 +181,15 @@ export class OcaHandler implements ApiHandler { return message }) - const stream = await client.chat.completions.create({ + const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [] + if (this.getVectorStores().length > 0) { + tools.push({ + type: "file_search", + vector_store_ids: this.getVectorStores(), + } as any) + } + + const requestObject: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: this.options.ocaModelId || liteLlmDefaultModelId, messages: [enhancedSystemMessage, ...enhancedMessages], temperature, @@ -192,7 +201,12 @@ export class OcaHandler implements ApiHandler { ...(this.options.taskId && { litellm_session_id: `cline-${this.options.taskId}`, }), // Add session ID for LiteLLM tracking - }) + tools, + } + + console.log("Input to OCA chat completions: ", requestObject) + + const stream = await client.chat.completions.create(requestObject) const inputCost = (await this.calculateCost(1e6, 0)) || 0 const outputCost = (await this.calculateCost(0, 1e6)) || 0 @@ -258,4 +272,12 @@ export class OcaHandler implements ApiHandler { info: this.options.ocaModelInfo || liteLlmModelInfoSaneDefaults, } } + + getVectorStores() { + if (this.options.vectorIds) { + return this.options.vectorIds + } else { + return [] + } + } } diff --git a/src/core/controller/vectors/refreshOcaVectors.ts b/src/core/controller/vectors/refreshOcaVectors.ts new file mode 100644 index 00000000000..c988f29a19a --- /dev/null +++ b/src/core/controller/vectors/refreshOcaVectors.ts @@ -0,0 +1,96 @@ +import { StringRequest } from "@shared/proto/cline/common" +import { VectorStoreInfo, VectorStores } from "@shared/proto/cline/vectors" +import axios from "axios" +import { HostProvider } from "@/hosts/host-provider" +import { OcaAuthService } from "@/services/auth/oca/OcaAuthService" +import { DEFAULT_OCA_BASE_URL } from "@/services/auth/oca/utils/constants" +import { createOcaHeaders, getProxyAgents } from "@/services/auth/oca/utils/utils" +import { Logger } from "@/services/logging/Logger" +import { ShowMessageType } from "@/shared/proto/index.host" +import { Controller } from ".." + +/** + * Refreshes the Oca models and returns the updated model list + * @param controller The controller instance + * @param request Empty request object + * @returns Response containing the Oca models + */ +export async function refreshOcaVectors(controller: Controller, request: StringRequest): Promise { + const vectors: Record = {} + const ocaAccessToken = await OcaAuthService.getInstance().getAuthToken() + const baseUrl = request.value || DEFAULT_OCA_BASE_URL + const vectorsUrl = `${baseUrl}/vector_store/list` + const headers = await createOcaHeaders(ocaAccessToken!, "models-refresh") + try { + Logger.log(`Making refresh oca model request with customer opc-request-id: ${headers["opc-request-id"]}`) + const response = await axios.get(vectorsUrl, { headers, ...getProxyAgents() }) + if (response.data && response.data.data) { + const vectorIds: string[] = [] + for (const vectorStore of response.data.data) { + const vectorStoreId = vectorStore.vector_store_id + if (typeof vectorStoreId !== "string" || !vectorStoreId) { + continue + } + vectors[vectorStoreId] = VectorStoreInfo.create({ + id: vectorStoreId, + name: vectorStore.vector_store_name, + description: vectorStore.vector_store_description, + }) + vectorIds.push(vectorStoreId) + } + console.log("Oca vectors fetched", vectors) + + // Fetch current config + const apiConfiguration = controller.stateManager.getApiConfiguration() + const updatedConfig = { ...apiConfiguration } + + // Which mode(s) to update? + const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting") + const currentMode = (await controller.getCurrentMode?.()) ?? "plan" + const planModeSelectedVectorId: string[] = apiConfiguration?.planModeOcaVectorIds + ? apiConfiguration?.planModeOcaVectorIds.filter( + (vectorId) => vectorIds.filter((secondVectorId) => vectorId === secondVectorId).length >= 1, + ) + : [] + const actModeSelectedVectorId: string[] = apiConfiguration?.actModeOcaVectorIds + ? apiConfiguration?.actModeOcaVectorIds.filter( + (vectorId) => vectorIds.filter((secondVectorId) => vectorId === secondVectorId).length >= 1, + ) + : [] + + // Save new model selection(s) to configuration object, per plan/act mode setting + if (planActSeparateModelsSetting) { + if (currentMode === "plan") { + updatedConfig.planModeOcaVectorIds = planModeSelectedVectorId + } else { + updatedConfig.actModeOcaVectorIds = actModeSelectedVectorId + } + } else { + updatedConfig.planModeOcaVectorIds = planModeSelectedVectorId + updatedConfig.actModeOcaVectorIds = actModeSelectedVectorId + } + + controller.stateManager.setApiConfiguration(updatedConfig) + + HostProvider.window.showMessage({ + type: ShowMessageType.INFORMATION, + message: `Refreshed Oca knowledge bases from ${baseUrl}`, + }) + } else { + console.error("Invalid response from oca API") + HostProvider.window.showMessage({ + type: ShowMessageType.INFORMATION, + message: `Failed to fetch Oca vectors. Please check your configuration from ${baseUrl}`, + }) + } + } catch (error) { + console.error("Error fetching oca vectors:", error) + const errorMsg = error.message || "Error refreshing Oca knowledge bases" + HostProvider.window.showMessage({ + type: ShowMessageType.ERROR, + message: errorMsg, + }) + return VectorStores.create({ error: errorMsg }) + } + return VectorStores.create({ vectors }) +} diff --git a/src/core/storage/StateManager.ts b/src/core/storage/StateManager.ts index 87b8d095b35..d84114239a0 100644 --- a/src/core/storage/StateManager.ts +++ b/src/core/storage/StateManager.ts @@ -446,6 +446,7 @@ export class StateManager { planModeVercelAiGatewayModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaVectorIds, // Act mode configurations actModeApiProvider, actModeApiModelId, @@ -480,6 +481,7 @@ export class StateManager { actModeVercelAiGatewayModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaVectorIds, } = apiConfiguration // Batch update global state keys @@ -518,6 +520,7 @@ export class StateManager { planModeVercelAiGatewayModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaVectorIds, // Act mode configuration updates actModeApiProvider, @@ -553,6 +556,7 @@ export class StateManager { actModeVercelAiGatewayModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaVectorIds, // Global state updates awsRegion, @@ -993,6 +997,7 @@ export class StateManager { this.globalStateCache["planModeVercelAiGatewayModelInfo"], planModeOcaModelId: this.globalStateCache["planModeOcaModelId"], planModeOcaModelInfo: this.globalStateCache["planModeOcaModelInfo"], + planModeOcaVectorIds: this.globalStateCache["planModeOcaVectorIds"], // Act mode configurations actModeApiProvider: this.taskStateCache["actModeApiProvider"] || this.globalStateCache["actModeApiProvider"], @@ -1055,6 +1060,7 @@ export class StateManager { this.globalStateCache["actModeVercelAiGatewayModelInfo"], actModeOcaModelId: this.globalStateCache["actModeOcaModelId"], actModeOcaModelInfo: this.globalStateCache["actModeOcaModelInfo"], + actModeOcaVectorIds: this.globalStateCache["actModeOcaVectorIds"], } } } diff --git a/src/core/storage/state-keys.ts b/src/core/storage/state-keys.ts index 992100302eb..19b6750319f 100644 --- a/src/core/storage/state-keys.ts +++ b/src/core/storage/state-keys.ts @@ -135,6 +135,7 @@ export interface Settings { planModeHuaweiCloudMaasModelInfo: ModelInfo | undefined planModeOcaModelId: string | undefined planModeOcaModelInfo: OcaModelInfo | undefined + planModeOcaVectorIds: string[] // Act mode configurations actModeApiProvider: ApiProvider actModeApiModelId: string | undefined @@ -171,6 +172,7 @@ export interface Settings { actModeVercelAiGatewayModelInfo: ModelInfo | undefined actModeOcaModelId: string | undefined actModeOcaModelInfo: OcaModelInfo | undefined + actModeOcaVectorIds: string[] } export interface Secrets { diff --git a/src/core/storage/utils/state-helpers.ts b/src/core/storage/utils/state-helpers.ts index e5d5f228eb0..f13dfb71189 100644 --- a/src/core/storage/utils/state-helpers.ts +++ b/src/core/storage/utils/state-helpers.ts @@ -314,6 +314,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis >("planModeVercelAiGatewayModelInfo") const planModeOcaModelId = context.globalState.get("planModeOcaModelId") as string | undefined const planModeOcaModelInfo = context.globalState.get("planModeOcaModelInfo") as OcaModelInfo | undefined + const planModeOcaVectorIds = context.globalState.get("planModeOcaVectorIds") as string[] | [] // Act mode configurations const actModeApiProvider = context.globalState.get("actModeApiProvider") const actModeApiModelId = context.globalState.get("actModeApiModelId") @@ -380,6 +381,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis >("actModeVercelAiGatewayModelInfo") const actModeOcaModelId = context.globalState.get("actModeOcaModelId") as string | undefined const actModeOcaModelInfo = context.globalState.get("actModeOcaModelInfo") as OcaModelInfo | undefined + const actModeOcaVectorIds = context.globalState.get("actModeOcaVectorIds") as string[] | [] const sapAiCoreUseOrchestrationMode = context.globalState.get("sapAiCoreUseOrchestrationMode") @@ -492,6 +494,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis planModeVercelAiGatewayModelInfo, planModeOcaModelId, planModeOcaModelInfo, + planModeOcaVectorIds, // Act mode configurations actModeApiProvider: actModeApiProvider || apiProvider, actModeApiModelId, @@ -526,6 +529,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis actModeVercelAiGatewayModelInfo, actModeOcaModelId, actModeOcaModelInfo, + actModeOcaVectorIds, // Other global fields focusChainSettings: focusChainSettings || DEFAULT_FOCUS_CHAIN_SETTINGS, diff --git a/src/shared/api.ts b/src/shared/api.ts index cdaeced85e0..348f33eab05 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -153,6 +153,7 @@ export interface ApiHandlerOptions { planModeVercelAiGatewayModelInfo?: ModelInfo planModeOcaModelId?: string planModeOcaModelInfo?: OcaModelInfo + planModeOcaVectorIds?: string[] // Act mode configurations // Act mode configurations @@ -188,6 +189,7 @@ export interface ApiHandlerOptions { actModeVercelAiGatewayModelInfo?: ModelInfo actModeOcaModelId?: string actModeOcaModelInfo?: OcaModelInfo + actModeOcaVectorIds?: string[] } export type ApiConfiguration = ApiHandlerOptions & diff --git a/src/shared/proto-conversions/models/api-configuration-conversion.ts b/src/shared/proto-conversions/models/api-configuration-conversion.ts index 92ea00032cd..e29c71f5acb 100644 --- a/src/shared/proto-conversions/models/api-configuration-conversion.ts +++ b/src/shared/proto-conversions/models/api-configuration-conversion.ts @@ -134,6 +134,13 @@ function convertProtoOcaModelInfoToOcaModelInfo(info: ProtoOcaModelInfo | undefi } } +function convertOcaVectorIdsToProtoOcaVectorIds(vectorIds: string[] | undefined): string[] { + if (!vectorIds) { + return [] + } + return vectorIds +} + // Convert application LiteLLMModelInfo to proto LiteLLMModelInfo function convertLiteLLMModelInfoToProto(info: AppLiteLLMModelInfo | undefined): LiteLLMModelInfo | undefined { if (!info) { @@ -503,6 +510,7 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA planModeVercelAiGatewayModelInfo: convertModelInfoToProtoOpenRouter(config.planModeVercelAiGatewayModelInfo), planModeOcaModelId: config.planModeOcaModelId, planModeOcaModelInfo: convertOcaModelInfoToProtoOcaModelInfo(config.planModeOcaModelInfo), + planModeOcaVectorIds: convertOcaVectorIdsToProtoOcaVectorIds(config.planModeOcaVectorIds), // Act mode configurations actModeApiProvider: config.actModeApiProvider ? convertApiProviderToProto(config.actModeApiProvider) : undefined, @@ -538,6 +546,7 @@ export function convertApiConfigurationToProto(config: ApiConfiguration): ProtoA actModeVercelAiGatewayModelInfo: convertModelInfoToProtoOpenRouter(config.actModeVercelAiGatewayModelInfo), actModeOcaModelId: config.actModeOcaModelId, actModeOcaModelInfo: convertOcaModelInfoToProtoOcaModelInfo(config.actModeOcaModelInfo), + actModeOcaVectorIds: convertOcaVectorIdsToProtoOcaVectorIds(config.actModeOcaVectorIds), } } @@ -655,6 +664,7 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio planModeVercelAiGatewayModelInfo: convertProtoToModelInfo(protoConfig.planModeVercelAiGatewayModelInfo), planModeOcaModelId: protoConfig.planModeOcaModelId, planModeOcaModelInfo: convertProtoOcaModelInfoToOcaModelInfo(protoConfig.planModeOcaModelInfo), + planModeOcaVectorIds: protoConfig.planModeOcaVectorIds, // Act mode configurations actModeApiProvider: @@ -691,5 +701,6 @@ export function convertProtoToApiConfiguration(protoConfig: ProtoApiConfiguratio actModeVercelAiGatewayModelInfo: convertProtoToModelInfo(protoConfig.actModeVercelAiGatewayModelInfo), actModeOcaModelId: protoConfig.actModeOcaModelId, actModeOcaModelInfo: convertProtoOcaModelInfoToOcaModelInfo(protoConfig.actModeOcaModelInfo), + actModeOcaVectorIds: protoConfig.actModeOcaVectorIds, } } diff --git a/webview-ui/src/components/settings/providers/OcaProvider.tsx b/webview-ui/src/components/settings/providers/OcaProvider.tsx index ebcfa701b89..22610650f89 100644 --- a/webview-ui/src/components/settings/providers/OcaProvider.tsx +++ b/webview-ui/src/components/settings/providers/OcaProvider.tsx @@ -1,11 +1,11 @@ import type { OcaModelInfo } from "@shared/api" -import type { OcaAuthState, OcaUserInfo } from "@shared/proto/index.cline" +import type { OcaAuthState, OcaUserInfo, VectorStoreInfo } from "@shared/proto/index.cline" import { EmptyRequest, StringRequest } from "@shared/proto/index.cline" import { Mode } from "@shared/storage/types" import { VSCodeButton, VSCodeLink, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import React, { useCallback, useEffect, useRef, useState } from "react" import { useExtensionState } from "@/context/ExtensionStateContext" -import { ModelsServiceClient, OcaAccountServiceClient } from "@/services/grpc-client" +import { ModelsServiceClient, OcaAccountServiceClient, VectorsServiceClient } from "@/services/grpc-client" import { VSC_BUTTON_BACKGROUND, VSC_BUTTON_FOREGROUND, @@ -16,6 +16,7 @@ import { import { BaseUrlField } from "../common/BaseUrlField" import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers" import OcaModelPicker from "./OcaModelPicker" +import OcaVectorPicker from "./OcaVectorPicker" /** * Props for the OcaProvider component @@ -112,7 +113,7 @@ function useOcaAuth() { * - Debounces base URL changes to avoid unnecessary calls. * - Guards against race conditions with a requestId and unmount checks. */ -function useOcaModels({ +function useOcaModelsAndKbs({ isAuthenticated, baseUrl, login, @@ -122,38 +123,70 @@ function useOcaModels({ login: () => Promise }) { const [models, setModels] = useState>({}) - const [loading, setLoading] = useState(false) - const [hasError, setHasError] = useState(false) - const [lastRefreshedAt, setLastRefreshedAt] = useState(null) + const [kbs, setKbs] = useState>({}) + const [modelsLoading, setModelsLoading] = useState(false) + const [modelsHasError, setModelsHasError] = useState(false) + const [modelsLastRefreshedAt, setModelsLastRefreshedAt] = useState(null) + const [kbsLoading, setKbsLoading] = useState(false) + const [kbsHasError, setKbsHasError] = useState(false) + const [kbsLastRefreshedAt, setKbsLastRefreshedAt] = useState(null) const reqIdRef = useRef(0) const unmountedRef = useRef(false) const debounceTimerRef = useRef(null) - const doRefresh = useCallback(async (url: string) => { + const doRefreshModels = useCallback(async (url: string) => { const myReqId = ++reqIdRef.current - setLoading(true) - setHasError(false) + setModelsLoading(true) + setModelsHasError(false) try { const resp = await ModelsServiceClient.refreshOcaModels(StringRequest.create({ value: url || "" })) // Only apply if still latest and still mounted if (!unmountedRef.current && myReqId === reqIdRef.current) { if (resp.error) { - setHasError(true) + setModelsHasError(true) } else { setModels(resp.models || {}) - setHasError(false) - setLastRefreshedAt(Date.now()) + setModelsHasError(false) + setModelsLastRefreshedAt(Date.now()) } } } catch (err) { if (!unmountedRef.current && myReqId === reqIdRef.current) { console.error("Failed to refresh Oca models:", err) - setHasError(true) + setModelsHasError(true) } } finally { if (!unmountedRef.current && myReqId === reqIdRef.current) { - setLoading(false) + setModelsLoading(false) + } + } + }, []) + + const doRefreshKbs = useCallback(async (url: string) => { + const myReqId = ++reqIdRef.current + setKbsLoading(true) + setKbsHasError(false) + try { + const resp = await VectorsServiceClient.refreshOcaVectors(StringRequest.create({ value: url || "" })) + // Only apply if still latest and still mounted + if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (resp.error) { + setKbsHasError(true) + } else { + setKbs(resp.vectors || {}) + setKbsHasError(false) + setKbsLastRefreshedAt(Date.now()) + } + } + } catch (err) { + if (!unmountedRef.current && myReqId === reqIdRef.current) { + console.error("Failed to refresh Oca knowledge bases:", err) + setKbsHasError(true) + } + } finally { + if (!unmountedRef.current && myReqId === reqIdRef.current) { + setKbsLoading(false) } } }, []) @@ -169,13 +202,18 @@ function useOcaModels({ if (!isAuthenticated) { // Clear models if logged out; prevent stale data setModels({}) - setLoading(false) - setHasError(false) + setModelsLoading(false) + setModelsHasError(false) + + setKbs({}) + setKbsLoading(false) + setKbsHasError(false) return } debounceTimerRef.current = window.setTimeout(() => { - void doRefresh(baseUrl || "") + void doRefreshModels(baseUrl || "") + void doRefreshKbs(baseUrl || "") }, 250) return () => { @@ -187,12 +225,12 @@ function useOcaModels({ // bump reqId so any in-flight result is ignored reqIdRef.current++ } - }, [isAuthenticated, baseUrl, doRefresh]) + }, [isAuthenticated, baseUrl, doRefreshModels, doRefreshKbs]) // User-initiated refresh with auto login + single retry on failure const refreshModels = useCallback(async () => { - setLoading(true) - setHasError(false) + setModelsLoading(true) + setModelsHasError(false) async function tryRefresh(retry = false): Promise { try { @@ -201,26 +239,67 @@ function useOcaModels({ throw new Error(resp.error) } setModels(resp.models || {}) - setHasError(false) - setLastRefreshedAt(Date.now()) + setModelsHasError(false) + setModelsLastRefreshedAt(Date.now()) + return true + } catch (_err) { + if (!retry) { + await login() // prompt login + return tryRefresh(true) // retry once + } else { + setModelsHasError(true) + } + return false + } finally { + setModelsLoading(false) + } + } + + await tryRefresh() + }, [baseUrl, login]) + + const refreshKbs = useCallback(async () => { + setKbsLoading(true) + setKbsHasError(false) + + async function tryRefresh(retry = false): Promise { + try { + const resp = await VectorsServiceClient.refreshOcaVectors(StringRequest.create({ value: baseUrl || "" })) + if (resp.error) { + throw new Error(resp.error) + } + setKbs(resp.vectors || {}) + setKbsHasError(false) + setKbsLastRefreshedAt(Date.now()) return true } catch (_err) { if (!retry) { await login() // prompt login return tryRefresh(true) // retry once } else { - setHasError(true) + setKbsHasError(true) } return false } finally { - setLoading(false) + setKbsLoading(false) } } await tryRefresh() }, [baseUrl, login]) - return { models, loading, hasError, refreshModels, lastRefreshedAt } + return { + models, + modelsLoading, + modelsHasError, + modelsLastRefreshedAt, + kbs, + kbsLoading, + kbsHasError, + kbsLastRefreshedAt, + refreshModels, + refreshKbs, + } } /** @@ -238,19 +317,28 @@ export const OcaProvider = ({ isPopup, currentMode }: OcaProviderProps) => { const { models: ocaModels, refreshModels, - hasError: ocaHasError, - loading: ocaLoading, - lastRefreshedAt, - } = useOcaModels({ + modelsHasError: ocaModelsHasError, + modelsLoading: ocaModelsLoading, + modelsLastRefreshedAt, + kbs: ocaKbs, + refreshKbs, + kbsHasError: ocaKbsHasError, + kbsLoading: ocaKbsLoading, + kbsLastRefreshedAt, + } = useOcaModelsAndKbs({ isAuthenticated, baseUrl: ocaBaseUrl, login, }) - const handleRefresh = useCallback(async () => { + const handleModelRefresh = useCallback(async () => { await refreshModels() }, [refreshModels]) + const handleKbRefresh = useCallback(async () => { + await refreshKbs() + }, [refreshModels]) + // On first subscription result: if user exists, refresh models once. const didInitialAuthCheckRef = useRef(false) useEffect(() => { @@ -260,9 +348,10 @@ export const OcaProvider = ({ isPopup, currentMode }: OcaProviderProps) => { didInitialAuthCheckRef.current = true if (isAuthenticated) { void refreshModels() + void refreshKbs() } // If user empty, do nothing (no auto login, no refresh) - }, [ready, isAuthenticated, refreshModels]) + }, [ready, isAuthenticated, refreshModels, refreshKbs]) return (
@@ -345,20 +434,50 @@ export const OcaProvider = ({ isPopup, currentMode }: OcaProviderProps) => { apiConfiguration={apiConfiguration} currentMode={currentMode} isPopup={isPopup} - lastRefreshedAt={lastRefreshedAt} - loading={ocaLoading} + lastRefreshedAt={modelsLastRefreshedAt} + loading={ocaModelsLoading} ocaModels={ocaModels} - onRefresh={handleRefresh} + onRefresh={handleModelRefresh} /> - {isAuthenticated && ocaHasError && ( + {isAuthenticated && ocaModelsHasError && (
Failed to refresh models. Check your session or network.
- + + Retry + + { + await login() + }}> + Sign in again + +
+
+ )} + + + + {isAuthenticated && ocaKbsHasError && ( +
+
Failed to refresh knowledge bases. Check your session or network.
+
+ Retry + onRefresh: () => void | Promise + loading?: boolean + lastRefreshedAt?: number | null +} + +const OcaVectorPicker: React.FC = ({ + apiConfiguration, + currentMode, + ocaKbs, + onRefresh, + loading, + lastRefreshedAt, +}: OcaVectorPickerProps) => { + const { handleModeFieldChange } = useApiConfigurationHandlers() + + const handleKbChange = async (kbs: string[]) => { + await handleModeFieldChange({ plan: "planModeOcaVectorIds", act: "actModeOcaVectorIds" }, kbs, currentMode) + } + + const handleRefreshToken = async () => { + await onRefresh?.() + } + + const { selectedVectorIds } = useMemo(() => { + return normalizeApiConfiguration(apiConfiguration, currentMode) + }, [apiConfiguration, currentMode]) + + const kbIds = useMemo(() => { + return Object.keys(ocaKbs || []).sort((a, b) => ocaKbs[a].name.localeCompare(ocaKbs[b].name)) + }, [ocaKbs]) + + const lastRefreshedText = useMemo(() => { + return typeof lastRefreshedAt === "number" ? new Date(lastRefreshedAt).toLocaleTimeString() : null + }, [lastRefreshedAt]) + + const toggleOption = async (option: { id: string; name: string }) => { + const prevVectorIds = selectedVectorIds || [] + const newVectorIds = prevVectorIds.includes(option.id) + ? prevVectorIds.filter((o) => o !== option.id) + : [...prevVectorIds, option.id] + await handleKbChange(newVectorIds) + } + + return ( +
+ + +
+ { + return { + id: kbId, + name: ocaKbs[kbId].name, + } + })} + selectedIds={selectedVectorIds || []} + toggleOption={toggleOption} + /> + + {loading ? "Refreshing…" : "Refresh"} + +
+ {lastRefreshedText ? ( +
+ Last refreshed at {lastRefreshedText} +
+ ) : null} +
+ ) +} + +export default OcaVectorPicker + +interface MultiSelectDropdownProps { + className?: string + id?: string + options: { + id: string + name: string + }[] + selectedIds: string[] + toggleOption: (option: { id: string; name: string }) => Promise +} + +const MultiSelectDropdown: React.FC = ({ options, selectedIds, toggleOption, className, id }) => { + const [open, setOpen] = useState(false) + const wrapperRef = useRef(null) + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + setOpen(false) + } + } + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, []) + + const selectedLabel = + selectedIds.length === 0 || options.length === 0 + ? "Select options..." + : selectedIds.map((selectedId) => options.filter((option) => selectedId === option.id)[0].name).join(", ") + + return ( +
+ {/* VSCode style dropdown button */} +
setOpen((o) => !o)} + onKeyDown={(e) => { + if (e.key === " " || e.key === "Enter") { + setOpen((o) => !o) + } + if (e.key === "Escape") { + setOpen(false) + } + }} + style={{ + display: "flex", + alignItems: "center", + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + minHeight: "100%", + border: "1px solid var(--vscode-dropdown-border, #3c3c3c)", + borderRadius: "calc(var(--corner-radius-round) * 1px)", + background: "var(--vscode-dropdown-background, #1e1e1e)", + color: "var(--vscode-dropdown-foreground, #cccccc)", + minWidth: 0, + padding: "2px 6px 2px 8px", + cursor: "pointer", + fontSize: 12, + fontFamily: "var(--vscode-font-family, inherit)", + boxSizing: "border-box", + outline: open ? "2px solid var(--vscode-focusBorder, #0078d4)" : "none", + margin: 0, + }} + tabIndex={0}> + + {selectedLabel} + + + + +
+ + {open && ( +
+ {options.map((option) => { + const checked = selectedIds.includes(option.id) + return ( +
{ + e.stopPropagation() + await toggleOption(option) + }} + onKeyDown={async (e) => { + if (e.key === " " || e.key === "Enter") { + await toggleOption(option) + } + }} + role="option" + style={{ + display: "flex", + alignItems: "center", + padding: "4px 8px", + margin: 0, + borderRadius: 3, + cursor: "pointer", + background: checked ? "var(--vscode-list-activeSelectionBackground, #094771)" : "transparent", + color: checked + ? "var(--vscode-list-activeSelectionForeground, #fff)" + : "var(--vscode-dropdown-foreground, #cccccc)", + }} + tabIndex={0}> + + + {option.name} + +
+ ) + })} +
+ )} +
+ ) +} diff --git a/webview-ui/src/components/settings/utils/providerUtils.ts b/webview-ui/src/components/settings/utils/providerUtils.ts index 6f919694251..f6e89dbf7a0 100644 --- a/webview-ui/src/components/settings/utils/providerUtils.ts +++ b/webview-ui/src/components/settings/utils/providerUtils.ts @@ -72,6 +72,7 @@ export interface NormalizedApiConfig { selectedProvider: ApiProvider selectedModelId: string selectedModelInfo: ModelInfo + selectedVectorIds?: string[] } /** @@ -352,10 +353,14 @@ export function normalizeApiConfiguration( const ocaModelId = currentMode === "plan" ? apiConfiguration?.planModeOcaModelId : apiConfiguration?.actModeOcaModelId const ocaModelInfo = currentMode === "plan" ? apiConfiguration?.planModeOcaModelInfo : apiConfiguration?.actModeOcaModelInfo + const ocaVectorIds = + currentMode === "plan" ? apiConfiguration?.planModeOcaVectorIds : apiConfiguration?.actModeOcaVectorIds + return { selectedProvider: provider, selectedModelId: ocaModelId || "", selectedModelInfo: ocaModelInfo || liteLlmModelInfoSaneDefaults, + selectedVectorIds: ocaVectorIds || [], } default: return getProviderData(anthropicModels, anthropicDefaultModelId) From 4a04a821448a0fe7d5b4056ca28808da75920af1 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Tue, 23 Sep 2025 10:12:45 -0500 Subject: [PATCH 06/10] Separated KB and Model --- .../settings/providers/OcaProvider.tsx | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/webview-ui/src/components/settings/providers/OcaProvider.tsx b/webview-ui/src/components/settings/providers/OcaProvider.tsx index 22610650f89..b8c83a49d65 100644 --- a/webview-ui/src/components/settings/providers/OcaProvider.tsx +++ b/webview-ui/src/components/settings/providers/OcaProvider.tsx @@ -131,18 +131,23 @@ function useOcaModelsAndKbs({ const [kbsHasError, setKbsHasError] = useState(false) const [kbsLastRefreshedAt, setKbsLastRefreshedAt] = useState(null) - const reqIdRef = useRef(0) - const unmountedRef = useRef(false) - const debounceTimerRef = useRef(null) + const modelReqIdRef = useRef(0) + const modelUnmountedRef = useRef(false) + const modelDebounceTimerRef = useRef(null) + + const kbReqIdRef = useRef(0) + const kbUnmountedRef = useRef(false) + const kbDebounceTimerRef = useRef(null) const doRefreshModels = useCallback(async (url: string) => { - const myReqId = ++reqIdRef.current + const myReqId = ++modelReqIdRef.current setModelsLoading(true) setModelsHasError(false) try { const resp = await ModelsServiceClient.refreshOcaModels(StringRequest.create({ value: url || "" })) + console.log(resp) // Only apply if still latest and still mounted - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!modelUnmountedRef.current && myReqId === modelReqIdRef.current) { if (resp.error) { setModelsHasError(true) } else { @@ -152,25 +157,25 @@ function useOcaModelsAndKbs({ } } } catch (err) { - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!modelUnmountedRef.current && myReqId === modelReqIdRef.current) { console.error("Failed to refresh Oca models:", err) setModelsHasError(true) } } finally { - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!modelUnmountedRef.current && myReqId === modelReqIdRef.current) { setModelsLoading(false) } } }, []) const doRefreshKbs = useCallback(async (url: string) => { - const myReqId = ++reqIdRef.current + const myReqId = ++kbReqIdRef.current setKbsLoading(true) setKbsHasError(false) try { const resp = await VectorsServiceClient.refreshOcaVectors(StringRequest.create({ value: url || "" })) // Only apply if still latest and still mounted - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!kbUnmountedRef.current && myReqId === kbReqIdRef.current) { if (resp.error) { setKbsHasError(true) } else { @@ -180,12 +185,12 @@ function useOcaModelsAndKbs({ } } } catch (err) { - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!kbUnmountedRef.current && myReqId === kbReqIdRef.current) { console.error("Failed to refresh Oca knowledge bases:", err) setKbsHasError(true) } } finally { - if (!unmountedRef.current && myReqId === reqIdRef.current) { + if (!kbUnmountedRef.current && myReqId === kbReqIdRef.current) { setKbsLoading(false) } } @@ -193,10 +198,15 @@ function useOcaModelsAndKbs({ // Debounce changes to baseUrl or auth useEffect(() => { - unmountedRef.current = false - if (debounceTimerRef.current) { - window.clearTimeout(debounceTimerRef.current) - debounceTimerRef.current = null + modelUnmountedRef.current = false + kbUnmountedRef.current = false + if (modelDebounceTimerRef.current) { + window.clearTimeout(modelDebounceTimerRef.current) + modelDebounceTimerRef.current = null + } + if (kbDebounceTimerRef.current) { + window.clearTimeout(kbDebounceTimerRef.current) + kbDebounceTimerRef.current = null } if (!isAuthenticated) { @@ -211,19 +221,30 @@ function useOcaModelsAndKbs({ return } - debounceTimerRef.current = window.setTimeout(() => { + modelDebounceTimerRef.current = window.setTimeout(() => { void doRefreshModels(baseUrl || "") + }, 250) + + kbDebounceTimerRef.current = window.setTimeout(() => { void doRefreshKbs(baseUrl || "") }, 250) return () => { - unmountedRef.current = true - if (debounceTimerRef.current) { - window.clearTimeout(debounceTimerRef.current) - debounceTimerRef.current = null + modelUnmountedRef.current = true + if (modelDebounceTimerRef.current) { + window.clearTimeout(modelDebounceTimerRef.current) + modelDebounceTimerRef.current = null + } + // bump reqId so any in-flight result is ignored + modelReqIdRef.current++ + + kbUnmountedRef.current = true + if (kbDebounceTimerRef.current) { + window.clearTimeout(kbDebounceTimerRef.current) + kbDebounceTimerRef.current = null } // bump reqId so any in-flight result is ignored - reqIdRef.current++ + kbReqIdRef.current++ } }, [isAuthenticated, baseUrl, doRefreshModels, doRefreshKbs]) From 268b64a6c2c6c809714b68b5878e3cc3531bf981 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Tue, 23 Sep 2025 11:12:34 -0500 Subject: [PATCH 07/10] Made dropdown unselectable when there are no knowledge bases to choose from --- .../settings/providers/OcaVectorPicker.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/settings/providers/OcaVectorPicker.tsx b/webview-ui/src/components/settings/providers/OcaVectorPicker.tsx index f8ee6bd4df5..a208898cf1f 100644 --- a/webview-ui/src/components/settings/providers/OcaVectorPicker.tsx +++ b/webview-ui/src/components/settings/providers/OcaVectorPicker.tsx @@ -148,16 +148,17 @@ const MultiSelectDropdown: React.FC = ({ options, sele }}> {/* VSCode style dropdown button */}
setOpen((o) => !o)} + onClick={() => options.length > 0 && setOpen((o) => !o)} onKeyDown={(e) => { - if (e.key === " " || e.key === "Enter") { - setOpen((o) => !o) - } if (e.key === "Escape") { setOpen(false) } + if ((e.key === " " || e.key === "Enter") && options.length > 0) { + setOpen((o) => !o) + } }} style={{ display: "flex", @@ -174,12 +175,13 @@ const MultiSelectDropdown: React.FC = ({ options, sele color: "var(--vscode-dropdown-foreground, #cccccc)", minWidth: 0, padding: "2px 6px 2px 8px", - cursor: "pointer", + cursor: options.length == 0 ? "not-allowed" : "pointer", fontSize: 12, fontFamily: "var(--vscode-font-family, inherit)", boxSizing: "border-box", outline: open ? "2px solid var(--vscode-focusBorder, #0078d4)" : "none", margin: 0, + opacity: options.length == 0 ? 0.6 : 1, }} tabIndex={0}> Date: Wed, 24 Sep 2025 10:47:17 -0500 Subject: [PATCH 08/10] Made changes to fix axios settings --- src/core/controller/vectors/refreshOcaVectors.ts | 11 +++++++++-- src/services/auth/oca/providers/OcaAuthProvider.ts | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/controller/vectors/refreshOcaVectors.ts b/src/core/controller/vectors/refreshOcaVectors.ts index c988f29a19a..99e8ba9c8d3 100644 --- a/src/core/controller/vectors/refreshOcaVectors.ts +++ b/src/core/controller/vectors/refreshOcaVectors.ts @@ -4,7 +4,7 @@ import axios from "axios" import { HostProvider } from "@/hosts/host-provider" import { OcaAuthService } from "@/services/auth/oca/OcaAuthService" import { DEFAULT_OCA_BASE_URL } from "@/services/auth/oca/utils/constants" -import { createOcaHeaders, getProxyAgents } from "@/services/auth/oca/utils/utils" +import { createOcaHeaders, getAxiosSettings } from "@/services/auth/oca/utils/utils" import { Logger } from "@/services/logging/Logger" import { ShowMessageType } from "@/shared/proto/index.host" import { Controller } from ".." @@ -18,12 +18,19 @@ import { Controller } from ".." export async function refreshOcaVectors(controller: Controller, request: StringRequest): Promise { const vectors: Record = {} const ocaAccessToken = await OcaAuthService.getInstance().getAuthToken() + if (!ocaAccessToken) { + HostProvider.window.showMessage({ + type: ShowMessageType.ERROR, + message: "Not authenticated with OCA. Please sign in first.", + }) + return VectorStores.create({ error: "Not authenticated with OCA" }) + } const baseUrl = request.value || DEFAULT_OCA_BASE_URL const vectorsUrl = `${baseUrl}/vector_store/list` const headers = await createOcaHeaders(ocaAccessToken!, "models-refresh") try { Logger.log(`Making refresh oca model request with customer opc-request-id: ${headers["opc-request-id"]}`) - const response = await axios.get(vectorsUrl, { headers, ...getProxyAgents() }) + const response = await axios.get(vectorsUrl, { headers, ...getAxiosSettings() }) if (response.data && response.data.data) { const vectorIds: string[] = [] for (const vectorStore of response.data.data) { diff --git a/src/services/auth/oca/providers/OcaAuthProvider.ts b/src/services/auth/oca/providers/OcaAuthProvider.ts index 7e2087b5d8a..ce73fb440c8 100644 --- a/src/services/auth/oca/providers/OcaAuthProvider.ts +++ b/src/services/auth/oca/providers/OcaAuthProvider.ts @@ -183,7 +183,7 @@ export class OcaAuthProvider { throw new Error("No ID token received from OCA") } - // Step 2: Get access_token (this is what you'll use for APIs) + //Step 2: Get access_token (this is what you'll use for APIs) const accessToken = tokenResponse.data.access_token const refreshToken = tokenResponse.data.refresh_token if (refreshToken) { From a256953eda8cbf6d54b9e14e271a22aa13f317a0 Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Wed, 1 Oct 2025 10:11:09 -0500 Subject: [PATCH 09/10] Fixed updates in refreshOcaVectors after rebase --- src/core/controller/vectors/refreshOcaVectors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/controller/vectors/refreshOcaVectors.ts b/src/core/controller/vectors/refreshOcaVectors.ts index 99e8ba9c8d3..03a5727b20d 100644 --- a/src/core/controller/vectors/refreshOcaVectors.ts +++ b/src/core/controller/vectors/refreshOcaVectors.ts @@ -53,7 +53,7 @@ export async function refreshOcaVectors(controller: Controller, request: StringR // Which mode(s) to update? const planActSeparateModelsSetting = controller.stateManager.getGlobalSettingsKey("planActSeparateModelsSetting") - const currentMode = (await controller.getCurrentMode?.()) ?? "plan" + const currentMode = controller.stateManager.getGlobalSettingsKey("mode") const planModeSelectedVectorId: string[] = apiConfiguration?.planModeOcaVectorIds ? apiConfiguration?.planModeOcaVectorIds.filter( (vectorId) => vectorIds.filter((secondVectorId) => vectorId === secondVectorId).length >= 1, From d0f4fb6ec200a2a65e5516b16ed1a9a30d68ad9d Mon Sep 17 00:00:00 2001 From: Chaitanya Eranki Date: Tue, 3 Mar 2026 13:04:58 -0600 Subject: [PATCH 10/10] Adding to oca-rag --- src/core/controller/models/refreshOcaModels.ts | 1 + src/core/controller/vectors/refreshOcaVectors.ts | 4 ++-- src/services/auth/oca/providers/OcaAuthProvider.ts | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/controller/models/refreshOcaModels.ts b/src/core/controller/models/refreshOcaModels.ts index d07587d2320..26fc9d3889c 100644 --- a/src/core/controller/models/refreshOcaModels.ts +++ b/src/core/controller/models/refreshOcaModels.ts @@ -39,6 +39,7 @@ export async function refreshOcaModels(controller: Controller, request: StringRe Logger.log(`Making refresh oca model request with customer opc-request-id: ${headers["opc-request-id"]}`) const response = await axios.get(modelsUrl, { headers, ...getAxiosSettings() }) if (response.data?.data) { + console.log("Model response: ", response.data) if (response.data.data.length === 0) { HostProvider.window.showMessage({ type: ShowMessageType.ERROR, diff --git a/src/core/controller/vectors/refreshOcaVectors.ts b/src/core/controller/vectors/refreshOcaVectors.ts index 03a5727b20d..b412e6864cf 100644 --- a/src/core/controller/vectors/refreshOcaVectors.ts +++ b/src/core/controller/vectors/refreshOcaVectors.ts @@ -27,9 +27,9 @@ export async function refreshOcaVectors(controller: Controller, request: StringR } const baseUrl = request.value || DEFAULT_OCA_BASE_URL const vectorsUrl = `${baseUrl}/vector_store/list` - const headers = await createOcaHeaders(ocaAccessToken!, "models-refresh") + const headers = await createOcaHeaders(ocaAccessToken!, "vectors-refresh") try { - Logger.log(`Making refresh oca model request with customer opc-request-id: ${headers["opc-request-id"]}`) + Logger.log(`Making refresh oca vector request with customer opc-request-id: ${headers["opc-request-id"]}`) const response = await axios.get(vectorsUrl, { headers, ...getAxiosSettings() }) if (response.data && response.data.data) { const vectorIds: string[] = [] diff --git a/src/services/auth/oca/providers/OcaAuthProvider.ts b/src/services/auth/oca/providers/OcaAuthProvider.ts index ce73fb440c8..f81901e9127 100644 --- a/src/services/auth/oca/providers/OcaAuthProvider.ts +++ b/src/services/auth/oca/providers/OcaAuthProvider.ts @@ -104,10 +104,12 @@ export class OcaAuthProvider { headers: { "Content-Type": "application/x-www-form-urlencoded" }, ...getAxiosSettings(), }) + console.log("Successful response: ", tokenResponse) const accessToken = tokenResponse.data.access_token const userInfo: OcaUserInfo = await this.getUserAccountInfo(accessToken) return { user: userInfo, apiKey: accessToken } } catch (err: unknown) { + console.log(err) const isAxios = (axios as any)?.isAxiosError?.(err) const status = isAxios ? (err as any).response?.status : undefined const data: any = isAxios ? (err as any).response?.data : undefined @@ -161,6 +163,7 @@ export class OcaAuthProvider { OcaAuthProvider.pkceStateMap.delete(state) const discovery = await axios.get(`${idcs_url}/.well-known/openid-configuration`, { ...getAxiosSettings() }) const tokenEndpoint = discovery.data.token_endpoint + console.log("Sign In Token Endpoint: ", tokenEndpoint) const params: any = { grant_type: "authorization_code", code,