Compare commits

...
Author SHA1 Message Date
celestial-vault e81d283e15 make welcomeViewCompleted context value initial value false 2025-07-02 12:32:48 -05:00
celestial-vault b65be180f7 merge conflicts 2025-07-01 17:50:29 -05:00
celestial-vault 128ee4b490 remove memo 2025-07-01 10:09:21 -07:00
celestial-vault 0a096ad024 cleanup 2025-06-30 23:47:14 -07:00
celestial-vault 984d958a81 refactor other sections to save-on-change; remove form diff calculation logic 2025-06-30 23:37:48 -07:00
celestial-vault daffe3694b add welcomeViewCompleted state boolean to control welcome view showing 2025-06-30 21:11:08 -07:00
celestial-vault 43e303763e use context value directly for apiProvider dropdown value; remove unecessary memo; remove keys from ApiOptions 2025-06-30 15:38:39 -07:00
celestial-vault 7a65b1df5a Merge branch 'main' into settingsview-apiconfig-save-on-change 2025-06-30 10:48:19 -07:00
celestial-vault c8936fee39 merge conflicts 2025-06-30 10:45:15 -07:00
celestial-vault e178bd3a65 add debounced text field for smooth typing 2025-06-29 15:35:00 -07:00
celestial-vault 5f821aefe8 add doc strings to new hook functions 2025-06-28 16:31:10 -07:00
celestial-vault 8683e8b296 implement save on change and remove confirmation modals for api config section 2025-06-28 16:26:29 -07:00
celestial-vault 496722f370 merge conflicts 2025-06-27 16:06:02 -07:00
celestial-vault 2b20d5137b pull out about section 2025-06-27 15:55:02 -07:00
celestial-vault 2e028a49bc merge conflicts 2025-06-27 15:48:20 -07:00
celestial-vault 97a36d5306 refactor out debug section 2025-06-27 15:44:10 -07:00
celestial-vault e67bb6c636 merge conflicts 2025-06-27 15:36:36 -07:00
celestial-vault fa7794e9a6 move files to sections folder 2025-06-27 15:00:06 -07:00
celestial-vault 88029834be move terminal, browser, and feature settings 2025-06-27 14:55:06 -07:00
celestial-vault 1f267d058a duplicate import 2025-06-27 14:26:35 -07:00
celestial-vault 853b8a6470 merge conflicts 2025-06-27 14:23:04 -07:00
celestial-vault a5258e46e1 add general settings section 2025-06-27 10:24:15 -07:00
celestial-vault 36dfc7de11 refactor out apiconfig section 2025-06-27 10:04:57 -07:00
55 changed files with 987 additions and 1690 deletions
+1
View File
@@ -18,6 +18,7 @@ service StateService {
rpc updateAutoApprovalSettings(AutoApprovalSettingsRequest) returns (Empty);
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
}
message State {
+2
View File
@@ -848,6 +848,7 @@ export class Controller {
terminalReuseEnabled,
defaultTerminalProfile,
isNewUser,
welcomeViewCompleted,
mcpResponsesCollapsed,
terminalOutputLineLimit,
} = await getAllExtensionState(this.context)
@@ -902,6 +903,7 @@ export class Controller {
terminalReuseEnabled,
defaultTerminalProfile,
isNewUser,
welcomeViewCompleted: welcomeViewCompleted as boolean, // Can be undefined but is set to either true or false by the migration that runs on extension launch in extension.ts
mcpResponsesCollapsed,
terminalOutputLineLimit,
}
@@ -0,0 +1,25 @@
import type { BooleanRequest } from "../../../shared/proto/common"
import { Empty } from "../../../shared/proto/common"
import type { Controller } from "../index"
import { updateGlobalState } from "../../storage/state"
/**
* Sets the welcomeViewCompleted flag to the specified boolean value
* @param controller The controller instance
* @param request The boolean request containing the value to set
* @returns Empty response
*/
export async function setWelcomeViewCompleted(controller: Controller, request: BooleanRequest): Promise<Empty> {
try {
// Update the global state to set welcomeViewCompleted to the requested value
await updateGlobalState(controller.context, "welcomeViewCompleted", request.value)
await controller.postStateToWebview()
console.log(`Welcome view completed set to: ${request.value}`)
return Empty.create({})
} catch (error) {
console.error("Failed to set welcome view completed:", error)
throw error
}
}
+1
View File
@@ -71,6 +71,7 @@ export type GlobalStateKey =
| "terminalReuseEnabled"
| "defaultTerminalProfile"
| "isNewUser"
| "welcomeViewCompleted"
| "terminalOutputLineLimit"
| "mcpRichDisplayEnabled"
| "sapAiCoreTokenUrl"
+53 -1
View File
@@ -2,7 +2,7 @@ import * as vscode from "vscode"
import { ensureRulesDirectoryExists } from "./disk"
import fs from "fs/promises"
import path from "path"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState } from "./state"
import { getGlobalState, getWorkspaceState, updateGlobalState, updateWorkspaceState, getAllExtensionState } from "./state"
import { GlobalStateKey } from "./state-keys"
export async function migratePlanActGlobalToWorkspaceStorage(context: vscode.ExtensionContext) {
@@ -145,3 +145,55 @@ export async function migrateModeFromWorkspaceStorageToControllerState(context:
// Continue execution - migration failure shouldn't break extension startup
}
}
export async function migrateWelcomeViewCompleted(context: vscode.ExtensionContext) {
try {
// Check if welcomeViewCompleted is already set
const welcomeViewCompleted = await getGlobalState(context, "welcomeViewCompleted")
if (welcomeViewCompleted === undefined) {
console.log("Migrating welcomeViewCompleted setting...")
// Get all extension state to check for existing API keys
const extensionState = await getAllExtensionState(context)
const config = extensionState.apiConfiguration
// This is the original logic used for checking is the welcome view should be shown
// It was located in the ExtensionStateContextProvider
const hasKey = config
? [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
config.sapAiCoreClientId,
].some((key) => key !== undefined)
: false
// Set welcomeViewCompleted based on whether user has keys
await updateGlobalState(context, "welcomeViewCompleted", hasKey)
console.log(`Migration: Set welcomeViewCompleted to ${hasKey} based on existing API keys`)
}
} catch (error) {
console.error("Failed to migrate welcomeViewCompleted:", error)
// Continue execution - migration failure shouldn't break extension startup
}
}
+3
View File
@@ -55,6 +55,7 @@ export async function getWorkspaceState(context: vscode.ExtensionContext, key: s
export async function getAllExtensionState(context: vscode.ExtensionContext) {
const [
isNewUser,
welcomeViewCompleted,
apiKey,
openRouterApiKey,
clineApiKey,
@@ -128,6 +129,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
claudeCodePath,
] = await Promise.all([
getGlobalState(context, "isNewUser") as Promise<boolean | undefined>,
getGlobalState(context, "welcomeViewCompleted") as Promise<boolean | undefined>,
getSecret(context, "apiKey") as Promise<string | undefined>,
getSecret(context, "openRouterApiKey") as Promise<string | undefined>,
getSecret(context, "clineApiKey") as Promise<string | undefined>,
@@ -388,6 +390,7 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
sapAiCoreModelId,
},
isNewUser: isNewUser ?? true,
welcomeViewCompleted,
lastShownAnnouncementId,
taskHistory,
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
+4
View File
@@ -26,6 +26,7 @@ import {
migratePlanActGlobalToWorkspaceStorage,
migrateCustomInstructionsToGlobalRules,
migrateModeFromWorkspaceStorageToControllerState,
migrateWelcomeViewCompleted,
} from "./core/storage/state-migrations"
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
@@ -68,6 +69,9 @@ export async function activate(context: vscode.ExtensionContext) {
// Migrate mode from workspace storage to controller state (one-time cleanup)
await migrateModeFromWorkspaceStorageToControllerState(context)
// Migrate welcomeViewCompleted setting based on existing API keys (one-time cleanup)
await migrateWelcomeViewCompleted(context)
// Clean up orphaned file context warnings (startup cleanup)
await FileContextTracker.cleanupOrphanedWarnings(context)
+1
View File
@@ -60,6 +60,7 @@ export const DEFAULT_PLATFORM = "unknown"
export interface ExtensionState {
isNewUser: boolean
welcomeViewCompleted: boolean
apiConfiguration?: ApiConfiguration
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
@@ -1725,7 +1725,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
apiErrorMessage={undefined}
modelIdErrorMessage={undefined}
isPopup={true}
saveImmediately={true} // Ensure popup saves immediately
/>
</ModelSelectorTooltip>
)}
+37 -213
View File
@@ -1,11 +1,8 @@
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { ApiConfiguration } from "@shared/api"
import { StringRequest } from "@shared/proto/common"
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { memo, useCallback, useEffect, useMemo, useState } from "react"
import { useCallback, useEffect, useState } from "react"
import { useInterval } from "react-use"
import styled from "styled-components"
import { OPENROUTER_MODEL_PICKER_Z_INDEX } from "./OpenRouterModelPicker"
@@ -38,13 +35,13 @@ import { NebiusProvider } from "./providers/NebiusProvider"
import { LiteLlmProvider } from "./providers/LiteLlmProvider"
import { VSCodeLmProvider } from "./providers/VSCodeLmProvider"
import { LMStudioProvider } from "./providers/LMStudioProvider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
interface ApiOptionsProps {
showModelOptions: boolean
apiErrorMessage?: string
modelIdErrorMessage?: string
isPopup?: boolean
saveImmediately?: boolean // Add prop to control immediate saving
}
// This is necessary to ensure dropdown opens downward, important for when this is used in popup
@@ -71,52 +68,15 @@ declare module "vscode" {
}
}
const ApiOptions = ({
showModelOptions,
apiErrorMessage,
modelIdErrorMessage,
isPopup,
saveImmediately = false, // Default to false
}: ApiOptionsProps) => {
const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, isPopup }: ApiOptionsProps) => {
// Use full context state for immediate save payload
const extensionState = useExtensionState()
const { apiConfiguration, setApiConfiguration, uriScheme } = extensionState
const { apiConfiguration, uriScheme } = useExtensionState()
const selectedProvider = apiConfiguration?.apiProvider
const { handleFieldChange } = useApiConfigurationHandlers()
const [ollamaModels, setOllamaModels] = useState<string[]>([])
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
const newValue = event.target.value
// Update local state
setApiConfiguration({
...apiConfiguration,
[field]: newValue,
})
// If the field is the provider AND saveImmediately is true, save it immediately using the full context state
if (saveImmediately && field === "apiProvider") {
// Use apiConfiguration from the full extensionState context to send the most complete data
const currentFullApiConfig = extensionState.apiConfiguration
// Convert to proto format and send via gRPC
const updatedConfig = {
...currentFullApiConfig,
apiProvider: newValue,
}
const protoConfig = convertApiConfigurationToProto(updatedConfig)
ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: protoConfig,
}),
).catch((error) => {
console.error("Failed to update API configuration:", error)
})
}
}
const { selectedProvider, selectedModelId, selectedModelInfo } = useMemo(() => {
return normalizeApiConfiguration(apiConfiguration)
}, [apiConfiguration])
// Poll ollama/vscode-lm models
const requestLocalModels = useCallback(async () => {
@@ -161,7 +121,7 @@ const ApiOptions = ({
<VSCodeDropdown
id="api-provider"
value={selectedProvider}
onChange={handleInputChange("apiProvider")}
onChange={(e: any) => handleFieldChange("apiProvider", e.target.value)}
style={{
minWidth: 130,
position: "relative",
@@ -196,241 +156,105 @@ const ApiOptions = ({
</DropdownContainer>
{apiConfiguration && selectedProvider === "cline" && (
<ClineProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<ClineProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "asksage" && (
<AskSageProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<AskSageProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "anthropic" && (
<AnthropicProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<AnthropicProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "claude-code" && (
<ClaudeCodeProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<ClaudeCodeProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "openai-native" && (
<OpenAINativeProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<OpenAINativeProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "qwen" && (
<QwenProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<QwenProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "doubao" && (
<DoubaoProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<DoubaoProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "mistral" && (
<MistralProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<MistralProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "openrouter" && (
<OpenRouterProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
uriScheme={uriScheme}
/>
<OpenRouterProvider showModelOptions={showModelOptions} isPopup={isPopup} uriScheme={uriScheme} />
)}
{apiConfiguration && selectedProvider === "deepseek" && (
<DeepSeekProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<DeepSeekProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "together" && (
<TogetherProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<TogetherProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "openai" && (
<OpenAICompatibleProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<OpenAICompatibleProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "sambanova" && (
<SambanovaProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<SambanovaProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "bedrock" && (
<BedrockProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<BedrockProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "vertex" && (
<VertexProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<VertexProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "gemini" && (
<GeminiProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<GeminiProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "requesty" && (
<RequestyProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<RequestyProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "fireworks" && (
<FireworksProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<FireworksProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "vscode-lm" && (
<VSCodeLmProvider apiConfiguration={apiConfiguration} handleInputChange={handleInputChange} />
)}
{apiConfiguration && selectedProvider === "vscode-lm" && <VSCodeLmProvider />}
{apiConfiguration && selectedProvider === "litellm" && (
<LiteLlmProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<LiteLlmProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "lmstudio" && (
<LMStudioProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<LMStudioProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "ollama" && (
<OllamaProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<OllamaProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "nebius" && (
<NebiusProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<NebiusProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "xai" && (
<XaiProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
setApiConfiguration={setApiConfiguration}
/>
<XaiProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "cerebras" && (
<CerebrasProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<CerebrasProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiConfiguration && selectedProvider === "sapaicore" && (
<SapAiCoreProvider
apiConfiguration={apiConfiguration}
handleInputChange={handleInputChange}
showModelOptions={showModelOptions}
isPopup={isPopup}
/>
<SapAiCoreProvider showModelOptions={showModelOptions} isPopup={isPopup} />
)}
{apiErrorMessage && (
@@ -457,4 +281,4 @@ const ApiOptions = ({
)
}
export default memo(ApiOptions)
export default ApiOptions
@@ -1,7 +1,7 @@
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { StateServiceClient } from "@/services/grpc-client"
import { openRouterDefaultModelId } from "@shared/api"
import { ApiConfiguration, openRouterDefaultModelId } from "@shared/api"
import { StringRequest } from "@shared/proto/common"
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import Fuse from "fuse.js"
@@ -14,6 +14,7 @@ import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import FeaturedModelCard from "./FeaturedModelCard"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
// Star icon for favorites
const StarIcon = ({ isFavorite, onClick }: { isFavorite: boolean; onClick: (e: React.MouseEvent) => void }) => {
@@ -60,7 +61,8 @@ const featuredModels = [
]
const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }) => {
const { apiConfiguration, setApiConfiguration, openRouterModels, refreshOpenRouterModels } = useExtensionState()
const { handleFieldsChange } = useApiConfigurationHandlers()
const { apiConfiguration, openRouterModels, refreshOpenRouterModels } = useExtensionState()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.openRouterModelId || openRouterDefaultModelId)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -70,14 +72,13 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
const handleModelChange = (newModelId: string) => {
// could be setting invalid model id/undefined info but validation will catch it
setApiConfiguration({
...apiConfiguration,
...{
openRouterModelId: newModelId,
openRouterModelInfo: openRouterModels[newModelId],
},
})
setSearchTerm(newModelId)
handleFieldsChange({
openRouterModelId: newModelId,
openRouterModelInfo: openRouterModels[newModelId],
})
}
const { selectedModelId, selectedModelInfo } = useMemo(() => {
@@ -306,9 +307,7 @@ const OpenRouterModelPicker: React.FC<OpenRouterModelPickerProps> = ({ isPopup }
{hasInfo ? (
<>
{showBudgetSlider && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
{showBudgetSlider && <ThinkingBudgetSlider />}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
@@ -1,13 +1,24 @@
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import React from "react"
import { ChatSettings } from "@shared/ChatSettings"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { updateSetting } from "./utils/settingsHandlers"
import { convertChatSettingsToProtoChatSettings } from "@shared/proto-conversions/state/chat-settings-conversion"
interface PreferredLanguageSettingProps {
chatSettings: ChatSettings
setChatSettings: (settings: ChatSettings) => void
}
const PreferredLanguageSetting: React.FC = () => {
const { chatSettings } = useExtensionState()
const handleLanguageChange = (newLanguage: string) => {
if (!chatSettings) return
const updatedChatSettings = {
...chatSettings,
preferredLanguage: newLanguage,
}
const protoChatSettings = convertChatSettingsToProtoChatSettings(updatedChatSettings)
updateSetting("chatSettings", protoChatSettings)
}
const PreferredLanguageSetting: React.FC<PreferredLanguageSettingProps> = ({ chatSettings, setChatSettings }) => {
return (
<div style={{}}>
<label htmlFor="preferred-language-dropdown" className="block mb-1 text-sm font-medium">
@@ -17,11 +28,7 @@ const PreferredLanguageSetting: React.FC<PreferredLanguageSettingProps> = ({ cha
id="preferred-language-dropdown"
currentValue={chatSettings.preferredLanguage || "English"}
onChange={(e: any) => {
const newLanguage = e.target.value
setChatSettings({
...chatSettings,
preferredLanguage: newLanguage,
}) // This constructs a full ChatSettings object
handleLanguageChange(e.target.value)
}}
style={{ width: "100%" }}>
<VSCodeOption value="English">English</VSCodeOption>
@@ -5,7 +5,7 @@ import React, { KeyboardEvent, memo, useEffect, useMemo, useRef, useState } from
import { useRemark } from "react-remark"
import { useMount } from "react-use"
import styled from "styled-components"
import { requestyDefaultModelId, requestyDefaultModelInfo } from "../../../../src/shared/api"
import { requestyDefaultModelId, requestyDefaultModelInfo } from "@shared/api"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { ModelsServiceClient } from "../../services/grpc-client"
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
@@ -13,13 +13,15 @@ import { highlight } from "../history/HistoryView"
import { ModelInfoView } from "./common/ModelInfoView"
import { normalizeApiConfiguration } from "./utils/providerUtils"
import ThinkingBudgetSlider from "./ThinkingBudgetSlider"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
export interface RequestyModelPickerProps {
isPopup?: boolean
}
const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) => {
const { apiConfiguration, setApiConfiguration, requestyModels, setRequestyModels } = useExtensionState()
const { apiConfiguration, requestyModels, setRequestyModels } = useExtensionState()
const { handleFieldsChange } = useApiConfigurationHandlers()
const [searchTerm, setSearchTerm] = useState(apiConfiguration?.requestyModelId || requestyDefaultModelId)
const [isDropdownVisible, setIsDropdownVisible] = useState(false)
const [selectedIndex, setSelectedIndex] = useState(-1)
@@ -29,12 +31,10 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
const handleModelChange = (newModelId: string) => {
// could be setting invalid model id/undefined info but validation will catch it
setApiConfiguration({
...apiConfiguration,
...{
requestyModelId: newModelId,
requestyModelInfo: requestyModels[newModelId],
},
handleFieldsChange({
requestyModelId: newModelId,
requestyModelInfo: requestyModels[newModelId],
})
setSearchTerm(newModelId)
}
@@ -226,9 +226,7 @@ const RequestyModelPicker: React.FC<RequestyModelPickerProps> = ({ isPopup }) =>
{hasInfo ? (
<>
{showBudgetSlider && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
)}
{showBudgetSlider && <ThinkingBudgetSlider />}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
</>
) : (
@@ -1,19 +1,14 @@
import { UnsavedChangesDialog } from "@/components/common/AlertDialog"
import HeroTooltip from "@/components/common/HeroTooltip"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { StateServiceClient } from "@/services/grpc-client"
import { validateApiConfiguration, validateModelId } from "@/utils/validate"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { StringRequest } from "@shared/proto/common"
import { PlanActMode, ResetStateRequest, TogglePlanActModeRequest, UpdateSettingsRequest } from "@shared/proto/state"
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { PlanActMode, ResetStateRequest, TogglePlanActModeRequest } from "@shared/proto/state"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { CheckCheck, FlaskConical, Info, LucideIcon, Settings, SquareMousePointer, SquareTerminal, Webhook } from "lucide-react"
import { memo, useCallback, useEffect, useRef, useState } from "react"
import { useCallback, useEffect, useRef, useState } from "react"
import { useEvent } from "react-use"
import { Tab, TabContent, TabHeader, TabList, TabTrigger } from "../common/Tab"
import { BrowserSettings } from "@shared/BrowserSettings"
import FeatureSettingsSection from "./sections/FeatureSettingsSection"
import Section from "./Section"
import SectionHeader from "./SectionHeader"
import TerminalSettingsSection from "./sections/TerminalSettingsSection"
import ApiConfigurationSection from "./sections/ApiConfigurationSection"
@@ -21,8 +16,6 @@ import GeneralSettingsSection from "./sections/GeneralSettingsSection"
import BrowserSettingsSection from "./sections/BrowserSettingsSection"
import DebugSection from "./sections/DebugSection"
import AboutSection from "./sections/AboutSection"
import { convertApiConfigurationToProtoApiConfiguration } from "@shared/proto-conversions/state/settings-conversion"
import { convertChatSettingsToProtoChatSettings } from "@shared/proto-conversions/state/chat-settings-conversion"
const IS_DEV = process.env.IS_DEV
@@ -107,437 +100,12 @@ type SettingsViewProps = {
}
const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
// Track if there are unsaved changes
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
// State for the unsaved changes dialog
const [isUnsavedChangesDialogOpen, setIsUnsavedChangesDialogOpen] = useState(false)
// Store the action to perform after confirmation
const pendingAction = useRef<() => void>()
// Track active tab
const [activeTab, setActiveTab] = useState<string>(targetSection || SETTINGS_TABS[0].id)
// Track if we're currently switching modes
const [isSwitchingMode, setIsSwitchingMode] = useState(false)
// Track pending mode switch when there are unsaved changes
const [pendingModeSwitch, setPendingModeSwitch] = useState<"plan" | "act" | null>(null)
const {
apiConfiguration,
version,
openRouterModels,
telemetrySetting,
setTelemetrySetting,
chatSettings,
setChatSettings,
planActSeparateModelsSetting,
setPlanActSeparateModelsSetting,
enableCheckpointsSetting,
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
mcpRichDisplayEnabled,
setMcpRichDisplayEnabled,
shellIntegrationTimeout,
setShellIntegrationTimeout,
terminalOutputLineLimit,
setTerminalOutputLineLimit,
terminalReuseEnabled,
setTerminalReuseEnabled,
defaultTerminalProfile,
setDefaultTerminalProfile,
mcpResponsesCollapsed,
setMcpResponsesCollapsed,
setApiConfiguration,
browserSettings,
} = useExtensionState()
// Local state for browser settings
const [localBrowserSettings, setLocalBrowserSettings] = useState<BrowserSettings>(browserSettings)
// Store the original state to detect changes
const originalState = useRef({
apiConfiguration,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpResponsesCollapsed,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
terminalOutputLineLimit,
defaultTerminalProfile,
browserSettings,
})
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
const handleSubmit = async (withoutDone: boolean = false) => {
const apiValidationResult = validateApiConfiguration(apiConfiguration)
const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
// setApiErrorMessage(apiValidationResult)
// setModelIdErrorMessage(modelIdValidationResult)
let apiConfigurationToSubmit = apiConfiguration
if (!apiValidationResult && !modelIdValidationResult) {
// vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
// vscode.postMessage({
// type: "telemetrySetting",
// text: telemetrySetting,
// })
// console.log("handleSubmit", withoutDone)
// vscode.postMessage({
// type: "separateModeSetting",
// text: separateModeSetting,
// })
} else {
// if the api configuration is invalid, we don't save it
apiConfigurationToSubmit = undefined
}
try {
await StateServiceClient.updateSettings(
UpdateSettingsRequest.create({
planActSeparateModelsSetting,
telemetrySetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
shellIntegrationTimeout,
terminalReuseEnabled,
mcpResponsesCollapsed,
apiConfiguration: apiConfigurationToSubmit
? convertApiConfigurationToProtoApiConfiguration(apiConfigurationToSubmit)
: undefined,
chatSettings: chatSettings ? convertChatSettingsToProtoChatSettings(chatSettings) : undefined,
terminalOutputLineLimit,
}),
)
// Update default terminal profile if it has changed
if (defaultTerminalProfile !== originalState.current.defaultTerminalProfile) {
await StateServiceClient.updateDefaultTerminalProfile({
value: defaultTerminalProfile || "default",
} as StringRequest)
}
// Update browser settings if they have changed
if (JSON.stringify(localBrowserSettings) !== JSON.stringify(originalState.current.browserSettings)) {
const { BrowserServiceClient } = await import("@/services/grpc-client")
const { UpdateBrowserSettingsRequest } = await import("@shared/proto/browser")
await BrowserServiceClient.updateBrowserSettings(
UpdateBrowserSettingsRequest.create({
metadata: {},
viewport: localBrowserSettings.viewport,
remoteBrowserEnabled: localBrowserSettings.remoteBrowserEnabled,
remoteBrowserHost: localBrowserSettings.remoteBrowserHost,
chromeExecutablePath: localBrowserSettings.chromeExecutablePath,
disableToolUse: localBrowserSettings.disableToolUse,
}),
)
}
// Update the original state to reflect the saved changes
originalState.current = {
apiConfiguration,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpResponsesCollapsed,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
terminalOutputLineLimit,
defaultTerminalProfile,
browserSettings: localBrowserSettings,
}
} catch (error) {
console.error("Failed to update settings:", error)
}
if (!withoutDone) {
onDone()
}
}
useEffect(() => {
setApiErrorMessage(undefined)
setModelIdErrorMessage(undefined)
}, [apiConfiguration])
// Track the previous mode to detect mode switches
const previousMode = useRef(chatSettings.mode)
// Update original state when mode changes
useEffect(() => {
// Detect if the mode has changed
if (previousMode.current !== chatSettings.mode) {
// Mode has changed, update the original state immediately to reflect the new apiConfiguration and chatSettings
originalState.current = {
...originalState.current,
apiConfiguration: apiConfiguration,
chatSettings: chatSettings,
}
// Update the previous mode reference
previousMode.current = chatSettings.mode
}
}, [chatSettings.mode, apiConfiguration, chatSettings])
// Check for unsaved changes by comparing current state with original state
useEffect(() => {
// Don't check for changes while switching modes
if (isSwitchingMode) {
return
}
const hasChanges =
JSON.stringify(apiConfiguration) !== JSON.stringify(originalState.current.apiConfiguration) ||
telemetrySetting !== originalState.current.telemetrySetting ||
planActSeparateModelsSetting !== originalState.current.planActSeparateModelsSetting ||
enableCheckpointsSetting !== originalState.current.enableCheckpointsSetting ||
mcpMarketplaceEnabled !== originalState.current.mcpMarketplaceEnabled ||
mcpRichDisplayEnabled !== originalState.current.mcpRichDisplayEnabled ||
JSON.stringify(chatSettings) !== JSON.stringify(originalState.current.chatSettings) ||
mcpResponsesCollapsed !== originalState.current.mcpResponsesCollapsed ||
shellIntegrationTimeout !== originalState.current.shellIntegrationTimeout ||
terminalOutputLineLimit !== originalState.current.terminalOutputLineLimit ||
terminalReuseEnabled !== originalState.current.terminalReuseEnabled ||
defaultTerminalProfile !== originalState.current.defaultTerminalProfile ||
JSON.stringify(localBrowserSettings) !== JSON.stringify(originalState.current.browserSettings)
setHasUnsavedChanges(hasChanges)
}, [
apiConfiguration,
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting,
mcpMarketplaceEnabled,
mcpRichDisplayEnabled,
mcpResponsesCollapsed,
chatSettings,
shellIntegrationTimeout,
terminalReuseEnabled,
terminalOutputLineLimit,
defaultTerminalProfile,
isSwitchingMode,
])
// Handle cancel button click
const handleCancel = useCallback(() => {
if (hasUnsavedChanges) {
// Show confirmation dialog
setIsUnsavedChangesDialogOpen(true)
pendingAction.current = () => {
// Reset all tracked state to original values
setTelemetrySetting(originalState.current.telemetrySetting)
setPlanActSeparateModelsSetting(originalState.current.planActSeparateModelsSetting)
setChatSettings(originalState.current.chatSettings)
if (typeof setApiConfiguration === "function") {
setApiConfiguration(originalState.current.apiConfiguration ?? {})
}
if (typeof setEnableCheckpointsSetting === "function") {
setEnableCheckpointsSetting(
typeof originalState.current.enableCheckpointsSetting === "boolean"
? originalState.current.enableCheckpointsSetting
: false,
)
}
if (typeof setMcpMarketplaceEnabled === "function") {
setMcpMarketplaceEnabled(
typeof originalState.current.mcpMarketplaceEnabled === "boolean"
? originalState.current.mcpMarketplaceEnabled
: false,
)
}
if (typeof setMcpRichDisplayEnabled === "function") {
setMcpRichDisplayEnabled(
typeof originalState.current.mcpRichDisplayEnabled === "boolean"
? originalState.current.mcpRichDisplayEnabled
: true,
)
}
// Reset terminal settings
if (typeof setShellIntegrationTimeout === "function") {
setShellIntegrationTimeout(originalState.current.shellIntegrationTimeout)
}
if (typeof setTerminalOutputLineLimit === "function") {
setTerminalOutputLineLimit(originalState.current.terminalOutputLineLimit)
}
if (typeof setTerminalReuseEnabled === "function") {
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
}
if (typeof setDefaultTerminalProfile === "function") {
setDefaultTerminalProfile(originalState.current.defaultTerminalProfile ?? "default")
}
if (typeof setMcpResponsesCollapsed === "function") {
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
}
// Reset browser settings
setLocalBrowserSettings(originalState.current.browserSettings)
// Close settings view
onDone()
}
} else {
// No changes, just close
onDone()
}
}, [
hasUnsavedChanges,
onDone,
setTelemetrySetting,
setPlanActSeparateModelsSetting,
setChatSettings,
setApiConfiguration,
setEnableCheckpointsSetting,
setMcpMarketplaceEnabled,
setMcpRichDisplayEnabled,
setMcpResponsesCollapsed,
])
// Handle confirmation dialog actions
const handleConfirmDiscard = useCallback(async () => {
setIsUnsavedChangesDialogOpen(false)
// Check if this is for a mode switch
if (pendingModeSwitch) {
// Reset all state to original values (discard changes)
setTelemetrySetting(originalState.current.telemetrySetting)
setPlanActSeparateModelsSetting(originalState.current.planActSeparateModelsSetting)
setChatSettings(originalState.current.chatSettings)
if (typeof setApiConfiguration === "function") {
setApiConfiguration(originalState.current.apiConfiguration ?? {})
}
if (typeof setEnableCheckpointsSetting === "function") {
setEnableCheckpointsSetting(
typeof originalState.current.enableCheckpointsSetting === "boolean"
? originalState.current.enableCheckpointsSetting
: false,
)
}
if (typeof setMcpMarketplaceEnabled === "function") {
setMcpMarketplaceEnabled(
typeof originalState.current.mcpMarketplaceEnabled === "boolean"
? originalState.current.mcpMarketplaceEnabled
: false,
)
}
if (typeof setMcpRichDisplayEnabled === "function") {
setMcpRichDisplayEnabled(
typeof originalState.current.mcpRichDisplayEnabled === "boolean"
? originalState.current.mcpRichDisplayEnabled
: true,
)
}
// Reset terminal settings
if (typeof setShellIntegrationTimeout === "function") {
setShellIntegrationTimeout(originalState.current.shellIntegrationTimeout)
}
if (typeof setTerminalOutputLineLimit === "function") {
setTerminalOutputLineLimit(originalState.current.terminalOutputLineLimit)
}
if (typeof setTerminalReuseEnabled === "function") {
setTerminalReuseEnabled(originalState.current.terminalReuseEnabled ?? true)
}
if (typeof setDefaultTerminalProfile === "function") {
setDefaultTerminalProfile(originalState.current.defaultTerminalProfile ?? "default")
}
if (typeof setMcpResponsesCollapsed === "function") {
setMcpResponsesCollapsed(originalState.current.mcpResponsesCollapsed ?? false)
}
// Now perform the mode switch
const targetMode = pendingModeSwitch
setPendingModeSwitch(null)
setIsSwitchingMode(true)
try {
await StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: targetMode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
preferredLanguage: chatSettings.preferredLanguage,
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
},
}),
)
} catch (error) {
console.error("Failed to toggle Plan/Act mode:", error)
} finally {
setIsSwitchingMode(false)
}
} else if (pendingAction.current) {
// Regular cancel button flow
pendingAction.current()
pendingAction.current = undefined
}
}, [
pendingModeSwitch,
setTelemetrySetting,
setPlanActSeparateModelsSetting,
setChatSettings,
setApiConfiguration,
setEnableCheckpointsSetting,
setMcpMarketplaceEnabled,
setMcpRichDisplayEnabled,
setShellIntegrationTimeout,
setTerminalOutputLineLimit,
setTerminalReuseEnabled,
setDefaultTerminalProfile,
setMcpResponsesCollapsed,
chatSettings.preferredLanguage,
chatSettings.openAIReasoningEffort,
])
// Handle save and switch for mode changes
const handleSaveAndSwitch = useCallback(async () => {
setIsUnsavedChangesDialogOpen(false)
if (pendingModeSwitch) {
// Save the current settings first
await handleSubmit(true)
// Now perform the mode switch
const targetMode = pendingModeSwitch
setPendingModeSwitch(null)
setIsSwitchingMode(true)
try {
await StateServiceClient.togglePlanActMode(
TogglePlanActModeRequest.create({
chatSettings: {
mode: targetMode === "plan" ? PlanActMode.PLAN : PlanActMode.ACT,
preferredLanguage: chatSettings.preferredLanguage,
openAiReasoningEffort: chatSettings.openAIReasoningEffort,
},
}),
)
} catch (error) {
console.error("Failed to toggle Plan/Act mode:", error)
} finally {
setIsSwitchingMode(false)
}
}
}, [pendingModeSwitch, handleSubmit, chatSettings.preferredLanguage, chatSettings.openAIReasoningEffort])
const handleCancelDiscard = useCallback(() => {
setIsUnsavedChangesDialogOpen(false)
pendingAction.current = undefined
setPendingModeSwitch(null)
}, [])
// validate as soon as the component is mounted
/*
useEffect will use stale values of variables if they are not included in the dependency array.
so trying to use useEffect with a dependency array of only one value for example will use any
other variables' old values. In most cases you don't want this, and should opt to use react-use
hooks.
// uses someVar and anotherVar
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [someVar])
If we only want to run code once on mount we can use react-use's useEffectOnce or useMount
*/
const { version, chatSettings } = useExtensionState()
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
@@ -596,16 +164,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
return
}
// Check if there are unsaved changes
if (hasUnsavedChanges) {
// Store the pending mode switch
setPendingModeSwitch(tab)
// Show the unsaved changes dialog
setIsUnsavedChangesDialogOpen(true)
return
}
// No unsaved changes, proceed with the switch
// All settings save immediately, so we can switch modes directly
setIsSwitchingMode(true)
try {
@@ -627,9 +186,6 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
}
}
// Track active tab
const [activeTab, setActiveTab] = useState<string>(targetSection || SETTINGS_TABS[0].id)
// Update active tab when targetSection changes
useEffect(() => {
if (targetSection) {
@@ -680,12 +236,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
<h3 className="text-[var(--vscode-foreground)] m-0">Settings</h3>
</div>
<div className="flex gap-2">
<VSCodeButton appearance="secondary" onClick={handleCancel}>
Cancel
</VSCodeButton>
<VSCodeButton onClick={() => handleSubmit(false)} disabled={!hasUnsavedChanges}>
Save
</VSCodeButton>
{/* All settings now save immediately, so only show Done button */}
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
</div>
</TabHeader>
@@ -763,39 +315,20 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
{/* API Configuration Tab */}
{activeTab === "api-config" && (
<ApiConfigurationSection
planActSeparateModelsSetting={planActSeparateModelsSetting}
chatSettings={chatSettings}
isSwitchingMode={isSwitchingMode}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
handlePlanActModeChange={handlePlanActModeChange}
setPlanActSeparateModelsSetting={setPlanActSeparateModelsSetting}
renderSectionHeader={renderSectionHeader}
/>
)}
{/* General Settings Tab */}
{activeTab === "general" && (
<GeneralSettingsSection
chatSettings={chatSettings}
setChatSettings={setChatSettings}
telemetrySetting={telemetrySetting}
setTelemetrySetting={setTelemetrySetting}
renderSectionHeader={renderSectionHeader}
/>
)}
{activeTab === "general" && <GeneralSettingsSection renderSectionHeader={renderSectionHeader} />}
{/* Feature Settings Tab */}
{activeTab === "features" && <FeatureSettingsSection renderSectionHeader={renderSectionHeader} />}
{/* Browser Settings Tab */}
{activeTab === "browser" && (
<BrowserSettingsSection
localBrowserSettings={localBrowserSettings}
onBrowserSettingsChange={setLocalBrowserSettings}
renderSectionHeader={renderSectionHeader}
/>
)}
{activeTab === "browser" && <BrowserSettingsSection renderSectionHeader={renderSectionHeader} />}
{/* Terminal Settings Tab */}
{activeTab === "terminal" && <TerminalSettingsSection renderSectionHeader={renderSectionHeader} />}
@@ -813,26 +346,8 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
)
})()}
</div>
{/* Unsaved Changes Dialog */}
<UnsavedChangesDialog
open={isUnsavedChangesDialogOpen}
onOpenChange={setIsUnsavedChangesDialogOpen}
onConfirm={handleConfirmDiscard}
onCancel={handleCancelDiscard}
onSave={pendingModeSwitch ? handleSaveAndSwitch : undefined}
title={pendingModeSwitch ? "Save Changes?" : "Unsaved Changes"}
description={
pendingModeSwitch
? `Do you want to save your changes to ${chatSettings.mode === "plan" ? "Plan" : "Act"} mode before switching to ${pendingModeSwitch === "plan" ? "Plan" : "Act"} mode?`
: "You have unsaved changes. Are you sure you want to discard them?"
}
confirmText={pendingModeSwitch ? "Switch Without Saving" : "Discard Changes"}
saveText="Save & Switch"
showSaveOption={!!pendingModeSwitch}
/>
</Tab>
)
}
export default memo(SettingsView)
export default SettingsView
@@ -1,12 +1,13 @@
import React from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { updateSetting } from "./utils/settingsHandlers"
const TerminalOutputLineLimitSlider: React.FC = () => {
const { terminalOutputLineLimit, setTerminalOutputLineLimit } = useExtensionState()
const { terminalOutputLineLimit } = useExtensionState()
const handleSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value, 10)
setTerminalOutputLineLimit(value)
updateSetting("terminalOutputLineLimit", value)
}
return (
@@ -1,7 +1,9 @@
import { memo, useCallback, useState } from "react"
import { memo, useCallback, useEffect, useMemo, useState } from "react"
import { anthropicModels, ApiConfiguration, geminiDefaultModelId, geminiModels, ModelInfo } from "@shared/api"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "./utils/useApiConfigurationHandlers"
// Constants
const DEFAULT_MIN_VALID_TOKENS = 1024
@@ -80,26 +82,30 @@ const RangeInput = styled.input<{ $value: number; $min: number; $max: number }>`
`
interface ThinkingBudgetSliderProps {
apiConfiguration: ApiConfiguration | undefined
setApiConfiguration: (apiConfiguration: ApiConfiguration) => void
maxBudget?: number
}
const ThinkingBudgetSlider = ({ apiConfiguration, setApiConfiguration, maxBudget }: ThinkingBudgetSliderProps) => {
const maxTokens =
apiConfiguration?.apiProvider === "gemini"
? geminiModels[geminiDefaultModelId].maxTokens
: anthropicModels["claude-3-7-sonnet-20250219"].maxTokens
const ThinkingBudgetSlider = ({ maxBudget }: ThinkingBudgetSliderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const [isEnabled, setIsEnabled] = useState<boolean>((apiConfiguration?.thinkingBudgetTokens || 0) > 0)
const maxTokens = useMemo(
() =>
apiConfiguration?.apiProvider === "gemini"
? geminiModels[geminiDefaultModelId].maxTokens
: anthropicModels["claude-3-7-sonnet-20250219"].maxTokens,
[apiConfiguration?.apiProvider],
)
// use maxBudget prop if provided, otherwise apply the percentage cap to maxTokens
const maxSliderValue = (() => {
const maxSliderValue = useMemo(() => {
if (maxBudget !== undefined) {
return maxBudget
}
return Math.floor(maxTokens * MAX_PERCENTAGE)
})()
const isEnabled = (apiConfiguration?.thinkingBudgetTokens || 0) > 0
}, [maxBudget, maxTokens])
// Add local state for the slider value
const [localValue, setLocalValue] = useState(apiConfiguration?.thinkingBudgetTokens || 0)
@@ -110,20 +116,16 @@ const ThinkingBudgetSlider = ({ apiConfiguration, setApiConfiguration, maxBudget
}, [])
const handleSliderComplete = () => {
setApiConfiguration({
...apiConfiguration,
thinkingBudgetTokens: localValue,
})
handleFieldChange("thinkingBudgetTokens", localValue)
}
const handleToggleChange = (event: any) => {
const isChecked = (event.target as HTMLInputElement).checked
const newValue = isChecked ? DEFAULT_MIN_VALID_TOKENS : 0
setIsEnabled(isChecked)
setLocalValue(newValue)
setApiConfiguration({
...apiConfiguration,
thinkingBudgetTokens: newValue,
})
handleFieldChange("thinkingBudgetTokens", newValue)
}
return (
@@ -1,11 +1,12 @@
import { useDebouncedInput } from "../utils/useDebouncedInput"
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
/**
* Props for the ApiKeyField component
*/
interface ApiKeyFieldProps {
value: string
onChange: (e: any) => void
initialValue: string
onChange: (value: string) => void
providerName: string
signupUrl?: string
placeholder?: string
@@ -16,34 +17,43 @@ interface ApiKeyFieldProps {
* A reusable component for API key input fields with standard styling and help text for signing up for key
*/
export const ApiKeyField = ({
value,
initialValue,
onChange,
providerName,
signupUrl,
placeholder = "Enter API Key...",
helpText,
}: ApiKeyFieldProps) => (
<div>
<VSCodeTextField value={value} style={{ width: "100%" }} type="password" onInput={onChange} placeholder={placeholder}>
<span style={{ fontWeight: 500 }}>{providerName} API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
{helpText || "This key is stored locally and only used to make API requests from this extension."}
{!value && signupUrl && (
<VSCodeLink
href={signupUrl}
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a{/^[aeiou]/i.test(providerName) ? "n" : ""} {providerName} API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}: ApiKeyFieldProps) => {
const [localValue, setLocalValue] = useDebouncedInput(initialValue, onChange)
return (
<div>
<VSCodeTextField
value={localValue}
style={{ width: "100%" }}
type="password"
onInput={(e: any) => setLocalValue(e.target.value)}
placeholder={placeholder}>
<span style={{ fontWeight: 500 }}>{providerName} API Key</span>
</VSCodeTextField>
<p
style={{
fontSize: "12px",
marginTop: 3,
color: "var(--vscode-descriptionForeground)",
}}>
{helpText || "This key is stored locally and only used to make API requests from this extension."}
{!localValue && signupUrl && (
<VSCodeLink
href={signupUrl}
style={{
display: "inline",
fontSize: "inherit",
}}>
You can get a{/^[aeiou]/i.test(providerName) ? "n" : ""} {providerName} API key by signing up here.
</VSCodeLink>
)}
</p>
</div>
)
}
@@ -1,11 +1,12 @@
import { useState, useEffect } from "react"
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useDebouncedInput } from "../utils/useDebouncedInput"
/**
* Props for the BaseUrlField component
*/
interface BaseUrlFieldProps {
value: string | undefined
initialValue: string | undefined
onChange: (value: string) => void
defaultValue?: string
label?: string
@@ -16,24 +17,19 @@ interface BaseUrlFieldProps {
* A reusable component for toggling and entering custom base URLs
*/
export const BaseUrlField = ({
value,
initialValue,
onChange,
defaultValue = "",
label = "Use custom base URL",
placeholder = "Default: https://api.example.com",
}: BaseUrlFieldProps) => {
const [isEnabled, setIsEnabled] = useState(!!value)
// When value changes externally, update isEnabled state
useEffect(() => {
setIsEnabled(!!value)
}, [value])
const [isEnabled, setIsEnabled] = useState(!!initialValue)
const [localValue, setLocalValue] = useDebouncedInput(initialValue || "", onChange)
const handleToggle = (e: any) => {
const checked = e.target.checked === true
setIsEnabled(checked)
if (!checked) {
onChange("")
setLocalValue("")
}
}
@@ -45,10 +41,10 @@ export const BaseUrlField = ({
{isEnabled && (
<VSCodeTextField
value={value || ""}
value={localValue}
style={{ width: "100%", marginTop: 3 }}
type="url"
onInput={(e: any) => onChange(e.target.value)}
onInput={(e: any) => setLocalValue(e.target.value)}
placeholder={placeholder}
/>
)}
@@ -0,0 +1,33 @@
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { useDebouncedInput } from "../utils/useDebouncedInput"
/**
* Props for the DebouncedTextField component
*/
interface DebouncedTextFieldProps {
// Custom props for debouncing functionality
initialValue: string
onChange: (value: string) => void
// Common VSCodeTextField props
style?: React.CSSProperties
type?: "text" | "password" | "url"
placeholder?: string
id?: string
children?: React.ReactNode
disabled?: boolean
}
/**
* A wrapper around VSCodeTextField that automatically handles debounced input
* to prevent excessive API calls while typing
*/
export const DebouncedTextField = ({ initialValue, onChange, children, ...otherProps }: DebouncedTextFieldProps) => {
const [localValue, setLocalValue] = useDebouncedInput(initialValue, onChange)
return (
<VSCodeTextField {...otherProps} value={localValue} onInput={(e: any) => setLocalValue(e.target.value)}>
{children}
</VSCodeTextField>
)
}
@@ -1,10 +1,12 @@
import { ApiConfiguration, anthropicModels } from "@shared/api"
import { anthropicModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { BaseUrlField } from "../common/BaseUrlField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
// Anthropic models that support thinking/reasoning mode
const SUPPORTED_THINKING_MODELS = ["claude-3-7-sonnet-20250219", "claude-sonnet-4-20250514", "claude-opus-4-20250514"]
@@ -13,43 +15,32 @@ const SUPPORTED_THINKING_MODELS = ["claude-3-7-sonnet-20250219", "claude-sonnet-
* Props for the AnthropicProvider component
*/
interface AnthropicProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration?: (config: ApiConfiguration) => void
}
/**
* The Anthropic provider configuration component
*/
export const AnthropicProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: AnthropicProviderProps) => {
export const AnthropicProvider = ({ showModelOptions, isPopup }: AnthropicProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
// Create a wrapper for handling field changes more directly
const handleFieldChange = (field: keyof ApiConfiguration) => (value: string) => {
handleInputChange(field)({ target: { value } })
}
return (
<div>
<ApiKeyField
value={apiConfiguration?.apiKey || ""}
onChange={handleInputChange("apiKey")}
initialValue={apiConfiguration?.apiKey || ""}
onChange={(value) => handleFieldChange("apiKey", value)}
providerName="Anthropic"
signupUrl="https://console.anthropic.com/settings/keys"
/>
<BaseUrlField
value={apiConfiguration?.anthropicBaseUrl}
onChange={handleFieldChange("anthropicBaseUrl")}
initialValue={apiConfiguration?.anthropicBaseUrl}
onChange={(value) => handleFieldChange("anthropicBaseUrl", value)}
placeholder="Default: https://api.anthropic.com"
label="Use custom base URL"
/>
@@ -59,16 +50,12 @@ export const AnthropicProvider = ({
<ModelSelector
models={anthropicModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && setApiConfiguration && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -1,16 +1,16 @@
import { ApiConfiguration, askSageModels, askSageDefaultURL } from "@shared/api"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiKeyField } from "../common/ApiKeyField"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the AskSageProvider component
*/
interface AskSageProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -18,34 +18,37 @@ interface AskSageProviderProps {
/**
* The AskSage provider configuration component
*/
export const AskSageProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: AskSageProviderProps) => {
export const AskSageProvider = ({ showModelOptions, isPopup }: AskSageProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.asksageApiKey || ""}
onChange={handleInputChange("asksageApiKey")}
initialValue={apiConfiguration?.asksageApiKey || ""}
onChange={(value) => handleFieldChange("asksageApiKey", value)}
providerName="AskSage"
helpText="This key is stored locally and only used to make API requests from this extension."
/>
<VSCodeTextField
value={apiConfiguration?.asksageApiUrl || askSageDefaultURL}
<DebouncedTextField
initialValue={apiConfiguration?.asksageApiUrl || askSageDefaultURL}
onChange={(value) => handleFieldChange("asksageApiUrl", value)}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("asksageApiUrl")}
placeholder="Enter AskSage API URL...">
<span style={{ fontWeight: 500 }}>AskSage API URL</span>
</VSCodeTextField>
</DebouncedTextField>
{showModelOptions && (
<>
<ModelSelector
models={askSageModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,36 +1,26 @@
import { ApiConfiguration, bedrockDefaultModelId, bedrockModels } from "@shared/api"
import {
VSCodeCheckbox,
VSCodeDropdown,
VSCodeOption,
VSCodeRadio,
VSCodeRadioGroup,
VSCodeTextField,
} from "@vscode/webview-ui-toolkit/react"
import { bedrockDefaultModelId, bedrockModels } from "@shared/api"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelInfoView } from "../common/ModelInfoView"
import { DropdownContainer } from "../common/ModelSelector"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
// Z-index constants for proper dropdown layering
const DROPDOWN_Z_INDEX = 1000
interface BedrockProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
export const BedrockProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: BedrockProviderProps) => {
export const BedrockProvider = ({ showModelOptions, isPopup }: BedrockProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleFieldsChange } = useApiConfigurationHandlers()
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpoint)
@@ -46,49 +36,47 @@ export const BedrockProvider = ({
onChange={(e) => {
const value = (e.target as HTMLInputElement)?.value
const useProfile = value === "profile"
setApiConfiguration({
...apiConfiguration,
awsUseProfile: useProfile,
})
handleFieldChange("awsUseProfile", useProfile)
}}>
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
</VSCodeRadioGroup>
{apiConfiguration?.awsUseProfile ? (
<VSCodeTextField
value={apiConfiguration?.awsProfile || ""}
<DebouncedTextField
initialValue={apiConfiguration?.awsProfile || ""}
onChange={(value) => handleFieldChange("awsProfile", value)}
style={{ width: "100%" }}
onInput={handleInputChange("awsProfile")}
placeholder="Enter profile name (default if empty)">
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
</VSCodeTextField>
</DebouncedTextField>
) : (
<>
<VSCodeTextField
value={apiConfiguration?.awsAccessKey || ""}
<DebouncedTextField
initialValue={apiConfiguration?.awsAccessKey || ""}
onChange={(value) => handleFieldChange("awsAccessKey", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsAccessKey")}
placeholder="Enter Access Key...">
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSecretKey || ""}
</DebouncedTextField>
<DebouncedTextField
initialValue={apiConfiguration?.awsSecretKey || ""}
onChange={(value) => handleFieldChange("awsSecretKey", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSecretKey")}
placeholder="Enter Secret Key...">
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.awsSessionToken || ""}
</DebouncedTextField>
<DebouncedTextField
initialValue={apiConfiguration?.awsSessionToken || ""}
onChange={(value) => handleFieldChange("awsSessionToken", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("awsSessionToken")}
placeholder="Enter Session Token...">
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
</VSCodeTextField>
</DebouncedTextField>
</>
)}
@@ -100,7 +88,7 @@ export const BedrockProvider = ({
id="aws-region-dropdown"
value={apiConfiguration?.awsRegion || ""}
style={{ width: "100%" }}
onChange={handleInputChange("awsRegion")}>
onChange={(e: any) => handleFieldChange("awsRegion", e.target.value)}>
<VSCodeOption value="">Select a region...</VSCodeOption>
{/* The user will have to choose a region that supports the model they use, but this shouldn't be a problem since they'd have to request access for it in that region in the first place. */}
<VSCodeOption value="us-east-1">us-east-1</VSCodeOption>
@@ -139,21 +127,18 @@ export const BedrockProvider = ({
const isChecked = e.target.checked === true
setAwsEndpointSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
awsBedrockEndpoint: "",
})
handleFieldChange("awsBedrockEndpoint", "")
}
}}>
Use custom VPC endpoint
</VSCodeCheckbox>
{awsEndpointSelected && (
<VSCodeTextField
value={apiConfiguration?.awsBedrockEndpoint || ""}
<DebouncedTextField
initialValue={apiConfiguration?.awsBedrockEndpoint || ""}
onChange={(value) => handleFieldChange("awsBedrockEndpoint", value)}
style={{ width: "100%", marginTop: 3, marginBottom: 5 }}
type="url"
onInput={handleInputChange("awsBedrockEndpoint")}
placeholder="Enter VPC Endpoint URL (optional)"
/>
)}
@@ -162,10 +147,8 @@ export const BedrockProvider = ({
checked={apiConfiguration?.awsUseCrossRegionInference || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
awsUseCrossRegionInference: isChecked,
})
handleFieldChange("awsUseCrossRegionInference", isChecked)
}}>
Use cross-region inference
</VSCodeCheckbox>
@@ -176,10 +159,7 @@ export const BedrockProvider = ({
checked={apiConfiguration?.awsBedrockUsePromptCache || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
awsBedrockUsePromptCache: isChecked,
})
handleFieldChange("awsBedrockUsePromptCache", isChecked)
}}>
Use prompt caching
</VSCodeCheckbox>
@@ -218,8 +198,8 @@ export const BedrockProvider = ({
value={apiConfiguration?.awsBedrockCustomSelected ? "custom" : selectedModelId}
onChange={(e: any) => {
const isCustom = e.target.value === "custom"
setApiConfiguration({
...apiConfiguration,
handleFieldsChange({
apiModelId: isCustom ? "" : e.target.value,
awsBedrockCustomSelected: isCustom,
awsBedrockCustomModelBaseId: bedrockDefaultModelId,
@@ -254,16 +234,14 @@ export const BedrockProvider = ({
Select "Custom" when using the Application Inference Profile in Bedrock. Enter the Application
Inference Profile ARN in the Model ID field.
</p>
<label htmlFor="bedrock-model-input">
<span style={{ fontWeight: 500 }}>Model ID</span>
</label>
<VSCodeTextField
<DebouncedTextField
id="bedrock-model-input"
value={apiConfiguration?.apiModelId || ""}
initialValue={apiConfiguration?.apiModelId || ""}
onChange={(value) => handleFieldChange("apiModelId", value)}
style={{ width: "100%", marginTop: 3 }}
onInput={handleInputChange("apiModelId")}
placeholder="Enter custom model ID..."
/>
placeholder="Enter custom model ID...">
<span style={{ fontWeight: 500 }}>Model ID</span>
</DebouncedTextField>
<label htmlFor="bedrock-base-model-dropdown">
<span style={{ fontWeight: 500 }}>Base Inference Model</span>
</label>
@@ -271,7 +249,7 @@ export const BedrockProvider = ({
<VSCodeDropdown
id="bedrock-base-model-dropdown"
value={apiConfiguration?.awsBedrockCustomModelBaseId || bedrockDefaultModelId}
onChange={handleInputChange("awsBedrockCustomModelBaseId")}
onChange={(e: any) => handleFieldChange("awsBedrockCustomModelBaseId", e.target.value)}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
{Object.keys(bedrockModels).map((modelId) => (
@@ -300,7 +278,7 @@ export const BedrockProvider = ({
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-sonnet-4-20250514-v1:0") ||
(apiConfiguration?.awsBedrockCustomSelected &&
apiConfiguration?.awsBedrockCustomModelBaseId === "anthropic.claude-opus-4-20250514-v1:0")) && (
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
<ThinkingBudgetSlider />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -1,15 +1,15 @@
import { ApiConfiguration, cerebrasModels } from "@shared/api"
import { cerebrasModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the CerebrasProvider component
*/
interface CerebrasProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface CerebrasProviderProps {
/**
* The Cerebras provider configuration component
*/
export const CerebrasProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: CerebrasProviderProps) => {
export const CerebrasProvider = ({ showModelOptions, isPopup }: CerebrasProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.cerebrasApiKey || ""}
onChange={handleInputChange("cerebrasApiKey")}
initialValue={apiConfiguration?.cerebrasApiKey || ""}
onChange={(value) => handleFieldChange("cerebrasApiKey", value)}
providerName="Cerebras"
signupUrl="https://cloud.cerebras.ai/"
/>
@@ -35,7 +38,7 @@ export const CerebrasProvider = ({ apiConfiguration, handleInputChange, showMode
<ModelSelector
models={cerebrasModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,15 +1,15 @@
import { ApiConfiguration, claudeCodeModels } from "@shared/api"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { claudeCodeModels } from "@shared/api"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the ClaudeCodeProvider component
*/
interface ClaudeCodeProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,25 +17,23 @@ interface ClaudeCodeProviderProps {
/**
* The Claude Code provider configuration component
*/
export const ClaudeCodeProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
}: ClaudeCodeProviderProps) => {
export const ClaudeCodeProvider = ({ showModelOptions, isPopup }: ClaudeCodeProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<VSCodeTextField
value={apiConfiguration?.claudeCodePath || ""}
<DebouncedTextField
initialValue={apiConfiguration?.claudeCodePath || ""}
onChange={(value) => handleFieldChange("claudeCodePath", value)}
style={{ width: "100%", marginTop: 3 }}
type="text"
onInput={handleInputChange("claudeCodePath")}
placeholder="Default: claude">
<span style={{ fontWeight: 500 }}>Claude Code CLI Path</span>
</VSCodeTextField>
</DebouncedTextField>
<p
style={{
@@ -51,7 +49,7 @@ export const ClaudeCodeProvider = ({
<ModelSelector
models={claudeCodeModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -4,13 +4,13 @@ import { useState } from "react"
import { ClineAccountInfoCard } from "../ClineAccountInfoCard"
import OpenRouterModelPicker, { OPENROUTER_MODEL_PICKER_Z_INDEX } from "../OpenRouterModelPicker"
import { DropdownContainer } from "../common/ModelSelector"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the ClineProvider component
*/
interface ClineProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -18,13 +18,11 @@ interface ClineProviderProps {
/**
* The Cline provider configuration component
*/
export const ClineProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: ClineProviderProps) => {
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
export const ClineProvider = ({ showModelOptions, isPopup }: ClineProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Create a wrapper for handling field changes more directly
const handleFieldChange = (field: keyof ApiConfiguration) => (value: any) => {
handleInputChange(field)({ target: { value } })
}
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
return (
<div>
@@ -43,7 +41,7 @@ export const ClineProvider = ({ apiConfiguration, handleInputChange, showModelOp
const isChecked = e.target.checked === true
setProviderSortingSelected(isChecked)
if (!isChecked) {
handleFieldChange("openRouterProviderSorting")("")
handleFieldChange("openRouterProviderSorting", "")
}
}}>
Sort underlying provider routing
@@ -56,7 +54,7 @@ export const ClineProvider = ({ apiConfiguration, handleInputChange, showModelOp
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.openRouterProviderSorting}
onChange={(e: any) => {
handleFieldChange("openRouterProviderSorting")(e.target.value)
handleFieldChange("openRouterProviderSorting", e.target.value)
}}>
<VSCodeOption value="">Default</VSCodeOption>
<VSCodeOption value="price">Price</VSCodeOption>
@@ -3,13 +3,13 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the DeepSeekProvider component
*/
interface DeepSeekProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface DeepSeekProviderProps {
/**
* The DeepSeek provider configuration component
*/
export const DeepSeekProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: DeepSeekProviderProps) => {
export const DeepSeekProvider = ({ showModelOptions, isPopup }: DeepSeekProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.deepSeekApiKey || ""}
onChange={handleInputChange("deepSeekApiKey")}
initialValue={apiConfiguration?.deepSeekApiKey || ""}
onChange={(value) => handleFieldChange("deepSeekApiKey", value)}
providerName="DeepSeek"
signupUrl="https://www.deepseek.com/"
/>
@@ -35,7 +38,7 @@ export const DeepSeekProvider = ({ apiConfiguration, handleInputChange, showMode
<ModelSelector
models={deepSeekModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -3,13 +3,13 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the DoubaoProvider component
*/
interface DoubaoProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface DoubaoProviderProps {
/**
* The ByteDance Doubao provider configuration component
*/
export const DoubaoProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: DoubaoProviderProps) => {
export const DoubaoProvider = ({ showModelOptions, isPopup }: DoubaoProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.doubaoApiKey || ""}
onChange={handleInputChange("doubaoApiKey")}
initialValue={apiConfiguration?.doubaoApiKey || ""}
onChange={(value) => handleFieldChange("doubaoApiKey", value)}
providerName="Doubao"
signupUrl="https://console.volcengine.com/home"
/>
@@ -35,7 +38,7 @@ export const DoubaoProvider = ({ apiConfiguration, handleInputChange, showModelO
<ModelSelector
models={doubaoModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,13 +1,13 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { ApiKeyField } from "../common/ApiKeyField"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { ApiConfiguration } from "@shared/api"
/**
* Props for the FireworksProvider component
*/
interface FireworksProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -15,10 +15,12 @@ interface FireworksProviderProps {
/**
* The Fireworks provider configuration component
*/
export const FireworksProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: FireworksProviderProps) => {
export const FireworksProvider = ({ showModelOptions, isPopup }: FireworksProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Handler for number input fields with validation
const handleNumberInputChange = (field: keyof ApiConfiguration) => (e: any) => {
const value = (e.target as HTMLInputElement).value
const handleNumberInputChange = (field: keyof ApiConfiguration, value: string) => {
if (!value) {
return
}
@@ -26,31 +28,27 @@ export const FireworksProvider = ({ apiConfiguration, handleInputChange, showMod
if (isNaN(num)) {
return
}
handleInputChange(field)({
target: {
value: num,
},
})
handleFieldChange(field, num)
}
return (
<div>
<ApiKeyField
value={apiConfiguration?.fireworksApiKey || ""}
onChange={handleInputChange("fireworksApiKey")}
initialValue={apiConfiguration?.fireworksApiKey || ""}
onChange={(value) => handleFieldChange("fireworksApiKey", value)}
providerName="Fireworks"
signupUrl="https://fireworks.ai/settings/users/api-keys"
/>
{showModelOptions && (
<>
<VSCodeTextField
value={apiConfiguration?.fireworksModelId || ""}
<DebouncedTextField
initialValue={apiConfiguration?.fireworksModelId || ""}
onChange={(value) => handleFieldChange("fireworksModelId", value)}
style={{ width: "100%" }}
onInput={handleInputChange("fireworksModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
</DebouncedTextField>
<p
style={{
fontSize: "12px",
@@ -62,20 +60,20 @@ export const FireworksProvider = ({ apiConfiguration, handleInputChange, showMod
models. Less capable models may not work as expected.)
</span>
</p>
<VSCodeTextField
value={apiConfiguration?.fireworksModelMaxCompletionTokens?.toString() || ""}
<DebouncedTextField
initialValue={apiConfiguration?.fireworksModelMaxCompletionTokens?.toString() || ""}
onChange={(value) => handleNumberInputChange("fireworksModelMaxCompletionTokens", value)}
style={{ width: "100%", marginBottom: 8 }}
onInput={handleNumberInputChange("fireworksModelMaxCompletionTokens")}
placeholder={"2000"}>
<span style={{ fontWeight: 500 }}>Max Completion Tokens</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.fireworksModelMaxTokens?.toString() || ""}
</DebouncedTextField>
<DebouncedTextField
initialValue={apiConfiguration?.fireworksModelMaxTokens?.toString() || ""}
onChange={(value) => handleNumberInputChange("fireworksModelMaxTokens", value)}
style={{ width: "100%", marginBottom: 8 }}
onInput={handleNumberInputChange("fireworksModelMaxTokens")}
placeholder={"4000"}>
<span style={{ fontWeight: 500 }}>Max Context Tokens</span>
</VSCodeTextField>
</DebouncedTextField>
</>
)}
</div>
@@ -1,10 +1,12 @@
import { ApiConfiguration, geminiModels } from "@shared/api"
import { geminiModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { BaseUrlField } from "../common/BaseUrlField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
// Gemini models that support thinking/reasoning mode
const SUPPORTED_THINKING_MODELS = ["gemini-2.5-pro", "gemini-2.5-flash"]
@@ -13,43 +15,32 @@ const SUPPORTED_THINKING_MODELS = ["gemini-2.5-pro", "gemini-2.5-flash"]
* Props for the GeminiProvider component
*/
interface GeminiProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration?: (config: ApiConfiguration) => void
}
/**
* The Gemini provider configuration component
*/
export const GeminiProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: GeminiProviderProps) => {
export const GeminiProvider = ({ showModelOptions, isPopup }: GeminiProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
// Create a wrapper for handling field changes more directly
const handleFieldChange = (field: keyof ApiConfiguration) => (value: string) => {
handleInputChange(field)({ target: { value } })
}
return (
<div>
<ApiKeyField
value={apiConfiguration?.geminiApiKey || ""}
onChange={handleInputChange("geminiApiKey")}
initialValue={apiConfiguration?.geminiApiKey || ""}
onChange={(value) => handleFieldChange("geminiApiKey", value)}
providerName="Gemini"
signupUrl="https://aistudio.google.com/apikey"
/>
<BaseUrlField
value={apiConfiguration?.geminiBaseUrl}
onChange={handleFieldChange("geminiBaseUrl")}
initialValue={apiConfiguration?.geminiBaseUrl}
onChange={(value) => handleFieldChange("geminiBaseUrl", value)}
placeholder="Default: https://generativelanguage.googleapis.com"
label="Use custom base URL"
/>
@@ -59,16 +50,12 @@ export const GeminiProvider = ({
<ModelSelector
models={geminiModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && setApiConfiguration && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -1,17 +1,17 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { VSCodeRadioGroup, VSCodeRadio, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useState, useCallback, useEffect } from "react"
import { useInterval } from "react-use"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelsServiceClient } from "@/services/grpc-client"
import { StringRequest } from "@shared/proto/common"
import { BaseUrlField } from "../common/BaseUrlField"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the LMStudioProvider component
*/
interface LMStudioProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -19,7 +19,10 @@ interface LMStudioProviderProps {
/**
* The LM Studio provider configuration component
*/
export const LMStudioProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: LMStudioProviderProps) => {
export const LMStudioProvider = ({ showModelOptions, isPopup }: LMStudioProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
// Poll LM Studio models
@@ -48,19 +51,19 @@ export const LMStudioProvider = ({ apiConfiguration, handleInputChange, showMode
return (
<div>
<BaseUrlField
value={apiConfiguration?.lmStudioBaseUrl}
onChange={(value) => handleInputChange("lmStudioBaseUrl")({ target: { value } })}
initialValue={apiConfiguration?.lmStudioBaseUrl}
onChange={(value) => handleFieldChange("lmStudioBaseUrl", value)}
placeholder="Default: http://localhost:1234"
label="Use custom base URL"
/>
<VSCodeTextField
value={apiConfiguration?.lmStudioModelId || ""}
<DebouncedTextField
initialValue={apiConfiguration?.lmStudioModelId || ""}
onChange={(value) => handleFieldChange("lmStudioModelId", value)}
style={{ width: "100%" }}
onInput={handleInputChange("lmStudioModelId")}
placeholder={"e.g. meta-llama-3.1-8b-instruct"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
</DebouncedTextField>
{lmStudioModels.length > 0 && (
<VSCodeRadioGroup
@@ -71,9 +74,7 @@ export const LMStudioProvider = ({ apiConfiguration, handleInputChange, showMode
const value = (e.target as HTMLInputElement)?.value
// need to check value first since radio group returns empty string sometimes
if (value) {
handleInputChange("lmStudioModelId")({
target: { value },
})
handleFieldChange("lmStudioModelId", value)
}
}}>
{lmStudioModels.map((model) => (
@@ -1,32 +1,29 @@
import { useState } from "react"
import { ApiConfiguration, liteLlmModelInfoSaneDefaults } from "@shared/api"
import { VSCodeTextField, VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { liteLlmModelInfoSaneDefaults } from "@shared/api"
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { ModelInfoView } from "../common/ModelInfoView"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the LiteLlmProvider component
*/
interface LiteLlmProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
/**
* The LiteLLM provider configuration component
*/
export const LiteLlmProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: LiteLlmProviderProps) => {
export const LiteLlmProvider = ({ showModelOptions, isPopup }: LiteLlmProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
@@ -35,29 +32,29 @@ export const LiteLlmProvider = ({
return (
<div>
<VSCodeTextField
value={apiConfiguration?.liteLlmBaseUrl || ""}
<DebouncedTextField
initialValue={apiConfiguration?.liteLlmBaseUrl || ""}
onChange={(value) => handleFieldChange("liteLlmBaseUrl", value)}
style={{ width: "100%" }}
type="url"
onInput={handleInputChange("liteLlmBaseUrl")}
placeholder={"Default: http://localhost:4000"}>
<span style={{ fontWeight: 500 }}>Base URL (optional)</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmApiKey || ""}
</DebouncedTextField>
<DebouncedTextField
initialValue={apiConfiguration?.liteLlmApiKey || ""}
onChange={(value) => handleFieldChange("liteLlmApiKey", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("liteLlmApiKey")}
placeholder="Default: noop">
<span style={{ fontWeight: 500 }}>API Key</span>
</VSCodeTextField>
<VSCodeTextField
value={apiConfiguration?.liteLlmModelId || ""}
</DebouncedTextField>
<DebouncedTextField
initialValue={apiConfiguration?.liteLlmModelId || ""}
onChange={(value) => handleFieldChange("liteLlmModelId", value)}
style={{ width: "100%" }}
onInput={handleInputChange("liteLlmModelId")}
placeholder={"e.g. anthropic/claude-sonnet-4-20250514"}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
</DebouncedTextField>
<div style={{ display: "flex", flexDirection: "column", marginTop: 10, marginBottom: 10 }}>
{selectedModelInfo.supportsPromptCache && (
@@ -66,10 +63,8 @@ export const LiteLlmProvider = ({
checked={apiConfiguration?.liteLlmUsePromptCache || false}
onChange={(e: any) => {
const isChecked = e.target.checked === true
setApiConfiguration({
...apiConfiguration,
liteLlmUsePromptCache: isChecked,
})
handleFieldChange("liteLlmUsePromptCache", isChecked)
}}
style={{ fontWeight: 500, color: "var(--vscode-charts-green)" }}>
Use prompt caching (GA)
@@ -82,7 +77,7 @@ export const LiteLlmProvider = ({
</div>
<>
<ThinkingBudgetSlider apiConfiguration={apiConfiguration} setApiConfiguration={setApiConfiguration} />
<ThinkingBudgetSlider />
<p
style={{
fontSize: "12px",
@@ -130,83 +125,69 @@ export const LiteLlmProvider = ({
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
handleFieldChange("liteLlmModelInfo", modelInfo)
}}>
Supports Images
</VSCodeCheckbox>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.liteLlmModelInfo?.contextWindow
? apiConfiguration.liteLlmModelInfo.contextWindow.toString()
: liteLlmModelInfoSaneDefaults.contextWindow?.toString()
: (liteLlmModelInfoSaneDefaults.contextWindow?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.contextWindow = Number(input.target.value)
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
modelInfo.contextWindow = Number(value)
handleFieldChange("liteLlmModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Context Window Size</span>
</VSCodeTextField>
<VSCodeTextField
value={
</DebouncedTextField>
<DebouncedTextField
initialValue={
apiConfiguration?.liteLlmModelInfo?.maxTokens
? apiConfiguration.liteLlmModelInfo.maxTokens.toString()
: liteLlmModelInfoSaneDefaults.maxTokens?.toString()
: (liteLlmModelInfoSaneDefaults.maxTokens?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
modelInfo.maxTokens = input.target.value
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
modelInfo.maxTokens = Number(value)
handleFieldChange("liteLlmModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
</VSCodeTextField>
</DebouncedTextField>
</div>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.liteLlmModelInfo?.temperature !== undefined
? apiConfiguration.liteLlmModelInfo.temperature.toString()
: liteLlmModelInfoSaneDefaults.temperature?.toString()
: (liteLlmModelInfoSaneDefaults.temperature?.toString() ?? "")
}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.liteLlmModelInfo
? apiConfiguration.liteLlmModelInfo
: { ...liteLlmModelInfoSaneDefaults }
// Check if the input ends with a decimal point or has trailing zeros after decimal
const value = input.target.value
const shouldPreserveFormat = value.endsWith(".") || (value.includes(".") && value.endsWith("0"))
modelInfo.temperature =
value === ""
? liteLlmModelInfoSaneDefaults.temperature
: shouldPreserveFormat
? value // Keep as string to preserve decimal format
: parseFloat(value)
value === "" ? liteLlmModelInfoSaneDefaults.temperature : parseFloat(value)
setApiConfiguration({
...apiConfiguration,
liteLlmModelInfo: modelInfo,
})
handleFieldChange("liteLlmModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Temperature</span>
</VSCodeTextField>
</DebouncedTextField>
</div>
</>
)}
@@ -3,13 +3,13 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the MistralProvider component
*/
interface MistralProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface MistralProviderProps {
/**
* The Mistral provider configuration component
*/
export const MistralProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: MistralProviderProps) => {
export const MistralProvider = ({ showModelOptions, isPopup }: MistralProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.mistralApiKey || ""}
onChange={handleInputChange("mistralApiKey")}
initialValue={apiConfiguration?.mistralApiKey || ""}
onChange={(value) => handleFieldChange("mistralApiKey", value)}
providerName="Mistral"
signupUrl="https://console.mistral.ai/codestral"
/>
@@ -35,7 +38,7 @@ export const MistralProvider = ({ apiConfiguration, handleInputChange, showModel
<ModelSelector
models={mistralModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,15 +1,15 @@
import { ApiConfiguration, nebiusModels } from "@shared/api"
import { nebiusModels } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the NebiusProvider component
*/
interface NebiusProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface NebiusProviderProps {
/**
* The Nebius AI Studio provider configuration component
*/
export const NebiusProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: NebiusProviderProps) => {
export const NebiusProvider = ({ showModelOptions, isPopup }: NebiusProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.nebiusApiKey || ""}
onChange={handleInputChange("nebiusApiKey")}
initialValue={apiConfiguration?.nebiusApiKey || ""}
onChange={(value) => handleFieldChange("nebiusApiKey", value)}
providerName="Nebius"
signupUrl="https://studio.nebius.com/settings/api-keys"
helpText="This key is stored locally and only used to make API requests from this extension. (Note: Cline uses complex prompts and works best with Claude models. Less capable models may not work as expected.)"
@@ -36,7 +39,7 @@ export const NebiusProvider = ({ apiConfiguration, handleInputChange, showModelO
<ModelSelector
models={nebiusModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,33 +1,29 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useState, useCallback, useEffect } from "react"
import { useInterval } from "react-use"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelsServiceClient } from "@/services/grpc-client"
import { StringRequest } from "@shared/proto/common"
import OllamaModelPicker from "../OllamaModelPicker"
import { BaseUrlField } from "../common/BaseUrlField"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the OllamaProvider component
*/
interface OllamaProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
/**
* The Ollama provider configuration component
*/
export const OllamaProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: OllamaProviderProps) => {
export const OllamaProvider = ({ showModelOptions, isPopup }: OllamaProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const [ollamaModels, setOllamaModels] = useState<string[]>([])
// Poll ollama models
@@ -56,8 +52,8 @@ export const OllamaProvider = ({
return (
<div>
<BaseUrlField
value={apiConfiguration?.ollamaBaseUrl}
onChange={(value) => handleInputChange("ollamaBaseUrl")({ target: { value } })}
initialValue={apiConfiguration?.ollamaBaseUrl}
onChange={(value) => handleFieldChange("ollamaBaseUrl", value)}
placeholder="Default: http://localhost:11434"
label="Use custom base URL"
/>
@@ -70,10 +66,7 @@ export const OllamaProvider = ({
ollamaModels={ollamaModels}
selectedModelId={apiConfiguration?.ollamaModelId || ""}
onModelChange={(modelId) => {
setApiConfiguration({
...apiConfiguration,
ollamaModelId: modelId,
})
handleFieldChange("ollamaModelId", modelId)
}}
placeholder={ollamaModels.length > 0 ? "Search and select a model..." : "e.g. llama3.1"}
/>
@@ -92,33 +85,29 @@ export const OllamaProvider = ({
</p>
)}
<VSCodeTextField
value={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
<DebouncedTextField
initialValue={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
onChange={(value) => handleFieldChange("ollamaApiOptionsCtxNum", value)}
style={{ width: "100%" }}
onInput={handleInputChange("ollamaApiOptionsCtxNum")}
placeholder={"e.g. 32768"}>
<span style={{ fontWeight: 500 }}>Model Context Window</span>
</VSCodeTextField>
</DebouncedTextField>
{showModelOptions && (
<>
<VSCodeTextField
value={apiConfiguration?.requestTimeoutMs ? apiConfiguration.requestTimeoutMs.toString() : "30000"}
style={{ width: "100%" }}
onInput={(e: any) => {
const value = e.target.value
<DebouncedTextField
initialValue={apiConfiguration?.requestTimeoutMs ? apiConfiguration.requestTimeoutMs.toString() : "30000"}
onChange={(value) => {
// Convert to number, with validation
const numValue = parseInt(value, 10)
if (!isNaN(numValue) && numValue > 0) {
setApiConfiguration({
...apiConfiguration,
requestTimeoutMs: numValue,
})
handleFieldChange("requestTimeoutMs", numValue)
}
}}
style={{ width: "100%" }}
placeholder="Default: 30000 (30 seconds)">
<span style={{ fontWeight: 500 }}>Request Timeout (ms)</span>
</VSCodeTextField>
</DebouncedTextField>
<p style={{ fontSize: "12px", marginTop: 3, color: "var(--vscode-descriptionForeground)" }}>
Maximum time in milliseconds to wait for API responses before timing out.
</p>
@@ -2,19 +2,20 @@ import { ApiConfiguration, azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefa
import { OpenAiModelsRequest } from "@shared/proto/models"
import { ModelsServiceClient } from "@/services/grpc-client"
import { getAsVar, VSC_DESCRIPTION_FOREGROUND } from "@/utils/vscStyles"
import { VSCodeTextField, VSCodeButton, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useEffect, useRef, useState } from "react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ApiKeyField } from "../common/ApiKeyField"
import { BaseUrlField } from "../common/BaseUrlField"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the OpenAICompatibleProvider component
*/
interface OpenAICompatibleProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -22,12 +23,10 @@ interface OpenAICompatibleProviderProps {
/**
* The OpenAI Compatible provider configuration component
*/
export const OpenAICompatibleProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
}: OpenAICompatibleProviderProps) => {
export const OpenAICompatibleProvider = ({ showModelOptions, isPopup }: OpenAICompatibleProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const [modelConfigurationSelected, setModelConfigurationSelected] = useState(false)
// Get the normalized configuration
@@ -65,38 +64,34 @@ export const OpenAICompatibleProvider = ({
return (
<div>
<VSCodeTextField
value={apiConfiguration?.openAiBaseUrl || ""}
<DebouncedTextField
initialValue={apiConfiguration?.openAiBaseUrl || ""}
onChange={(value) => {
handleFieldChange("openAiBaseUrl", value)
debouncedRefreshOpenAiModels(value, apiConfiguration?.openAiApiKey)
}}
style={{ width: "100%", marginBottom: 10 }}
type="url"
onInput={(e: any) => {
const baseUrl = e.target.value
handleInputChange("openAiBaseUrl")({ target: { value: baseUrl } })
debouncedRefreshOpenAiModels(baseUrl, apiConfiguration?.openAiApiKey)
}}
placeholder={"Enter base URL..."}>
<span style={{ fontWeight: 500 }}>Base URL</span>
</VSCodeTextField>
</DebouncedTextField>
<ApiKeyField
value={apiConfiguration?.openAiApiKey || ""}
onChange={(e: any) => {
const apiKey = e.target.value
handleInputChange("openAiApiKey")({ target: { value: apiKey } })
debouncedRefreshOpenAiModels(apiConfiguration?.openAiBaseUrl, apiKey)
initialValue={apiConfiguration?.openAiApiKey || ""}
onChange={(value) => {
handleFieldChange("openAiApiKey", value)
debouncedRefreshOpenAiModels(apiConfiguration?.openAiBaseUrl, value)
}}
providerName="OpenAI Compatible"
/>
<VSCodeTextField
value={apiConfiguration?.openAiModelId || ""}
<DebouncedTextField
initialValue={apiConfiguration?.openAiModelId || ""}
onChange={(value) => handleFieldChange("openAiModelId", value)}
style={{ width: "100%", marginBottom: 10 }}
onInput={handleInputChange("openAiModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
</DebouncedTextField>
{/* OpenAI Compatible Custom Headers */}
{(() => {
@@ -111,11 +106,7 @@ export const OpenAICompatibleProvider = ({
const headerCount = Object.keys(currentHeaders).length
const newKey = `header${headerCount + 1}`
currentHeaders[newKey] = ""
handleInputChange("openAiHeaders")({
target: {
value: currentHeaders,
},
})
handleFieldChange("openAiHeaders", currentHeaders)
}}>
Add Header
</VSCodeButton>
@@ -123,38 +114,29 @@ export const OpenAICompatibleProvider = ({
<div>
{headerEntries.map(([key, value], index) => (
<div key={index} style={{ display: "flex", gap: 5, marginTop: 5 }}>
<VSCodeTextField
value={key}
<DebouncedTextField
initialValue={key}
style={{ width: "40%" }}
placeholder="Header name"
onInput={(e: any) => {
onChange={(newValue) => {
const currentHeaders = apiConfiguration?.openAiHeaders ?? {}
const newValue = e.target.value
if (newValue && newValue !== key) {
const { [key]: _, ...rest } = currentHeaders
handleInputChange("openAiHeaders")({
target: {
value: {
...rest,
[newValue]: value,
},
},
handleFieldChange("openAiHeaders", {
...rest,
[newValue]: value,
})
}
}}
/>
<VSCodeTextField
value={value}
<DebouncedTextField
initialValue={value}
style={{ width: "40%" }}
placeholder="Header value"
onInput={(e: any) => {
handleInputChange("openAiHeaders")({
target: {
value: {
...(apiConfiguration?.openAiHeaders ?? {}),
[key]: e.target.value,
},
},
onChange={(newValue) => {
handleFieldChange("openAiHeaders", {
...(apiConfiguration?.openAiHeaders ?? {}),
[key]: newValue,
})
}}
/>
@@ -162,11 +144,7 @@ export const OpenAICompatibleProvider = ({
appearance="secondary"
onClick={() => {
const { [key]: _, ...rest } = apiConfiguration?.openAiHeaders ?? {}
handleInputChange("openAiHeaders")({
target: {
value: rest,
},
})
handleFieldChange("openAiHeaders", rest)
}}>
Remove
</VSCodeButton>
@@ -178,8 +156,8 @@ export const OpenAICompatibleProvider = ({
})()}
<BaseUrlField
value={apiConfiguration?.azureApiVersion}
onChange={(value) => handleInputChange("azureApiVersion")({ target: { value } })}
initialValue={apiConfiguration?.azureApiVersion}
onChange={(value) => handleFieldChange("azureApiVersion", value)}
label="Set Azure API version"
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
/>
@@ -217,9 +195,7 @@ export const OpenAICompatibleProvider = ({
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
handleFieldChange("openAiModelInfo", modelInfo)
}}>
Supports Images
</VSCodeCheckbox>
@@ -232,9 +208,7 @@ export const OpenAICompatibleProvider = ({
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.supportsImages = isChecked
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
handleFieldChange("openAiModelInfo", modelInfo)
}}>
Supports browser use
</VSCodeCheckbox>
@@ -248,122 +222,108 @@ export const OpenAICompatibleProvider = ({
: { ...openAiModelInfoSaneDefaults }
modelInfo = { ...modelInfo, isR1FormatRequired: isChecked }
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
handleFieldChange("openAiModelInfo", modelInfo)
}}>
Enable R1 messages format
</VSCodeCheckbox>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.contextWindow
? apiConfiguration.openAiModelInfo.contextWindow.toString()
: openAiModelInfoSaneDefaults.contextWindow?.toString()
: (openAiModelInfoSaneDefaults.contextWindow?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.contextWindow = Number(input.target.value)
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
modelInfo.contextWindow = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Context Window Size</span>
</VSCodeTextField>
</DebouncedTextField>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.maxTokens
? apiConfiguration.openAiModelInfo.maxTokens.toString()
: openAiModelInfoSaneDefaults.maxTokens?.toString()
: (openAiModelInfoSaneDefaults.maxTokens?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.maxTokens = input.target.value
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
modelInfo.maxTokens = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Max Output Tokens</span>
</VSCodeTextField>
</DebouncedTextField>
</div>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.inputPrice
? apiConfiguration.openAiModelInfo.inputPrice.toString()
: openAiModelInfoSaneDefaults.inputPrice?.toString()
: (openAiModelInfoSaneDefaults.inputPrice?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.inputPrice = input.target.value
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
modelInfo.inputPrice = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Input Price / 1M tokens</span>
</VSCodeTextField>
</DebouncedTextField>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.outputPrice
? apiConfiguration.openAiModelInfo.outputPrice.toString()
: openAiModelInfoSaneDefaults.outputPrice?.toString()
: (openAiModelInfoSaneDefaults.outputPrice?.toString() ?? "")
}
style={{ flex: 1 }}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
modelInfo.outputPrice = input.target.value
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
modelInfo.outputPrice = Number(value)
handleFieldChange("openAiModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Output Price / 1M tokens</span>
</VSCodeTextField>
</DebouncedTextField>
</div>
<div style={{ display: "flex", gap: 10, marginTop: "5px" }}>
<VSCodeTextField
value={
<DebouncedTextField
initialValue={
apiConfiguration?.openAiModelInfo?.temperature
? apiConfiguration.openAiModelInfo.temperature.toString()
: openAiModelInfoSaneDefaults.temperature?.toString()
: (openAiModelInfoSaneDefaults.temperature?.toString() ?? "")
}
onInput={(input: any) => {
onChange={(value) => {
const modelInfo = apiConfiguration?.openAiModelInfo
? apiConfiguration.openAiModelInfo
: { ...openAiModelInfoSaneDefaults }
// Check if the input ends with a decimal point or has trailing zeros after decimal
const value = input.target.value
const shouldPreserveFormat = value.endsWith(".") || (value.includes(".") && value.endsWith("0"))
modelInfo.temperature =
value === ""
? openAiModelInfoSaneDefaults.temperature
: shouldPreserveFormat
? value // Keep as string to preserve decimal format
? (value as any)
: parseFloat(value)
handleInputChange("openAiModelInfo")({
target: { value: modelInfo },
})
handleFieldChange("openAiModelInfo", modelInfo)
}}>
<span style={{ fontWeight: 500 }}>Temperature</span>
</VSCodeTextField>
</DebouncedTextField>
</div>
</>
)}
@@ -3,13 +3,13 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the OpenAINativeProvider component
*/
interface OpenAINativeProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,20 +17,18 @@ interface OpenAINativeProviderProps {
/**
* The OpenAI (native) provider configuration component
*/
export const OpenAINativeProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
}: OpenAINativeProviderProps) => {
export const OpenAINativeProvider = ({ showModelOptions, isPopup }: OpenAINativeProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.openAiNativeApiKey || ""}
onChange={handleInputChange("openAiNativeApiKey")}
initialValue={apiConfiguration?.openAiNativeApiKey || ""}
onChange={(value) => handleFieldChange("openAiNativeApiKey", value)}
providerName="OpenAI"
signupUrl="https://platform.openai.com/api-keys"
/>
@@ -40,7 +38,7 @@ export const OpenAINativeProvider = ({
<ModelSelector
models={openAiNativeModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,5 +1,6 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { DropdownContainer } from "../common/ModelSelector"
import { useState } from "react"
import { getOpenRouterAuthUrl } from "../utils/providerUtils"
@@ -7,6 +8,8 @@ import { useOpenRouterKeyInfo } from "../../ui/hooks/useOpenRouterKeyInfo"
import VSCodeButtonLink from "../../common/VSCodeButtonLink"
import OpenRouterModelPicker, { OPENROUTER_MODEL_PICKER_Z_INDEX } from "../OpenRouterModelPicker"
import { formatPrice } from "../utils/pricingUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Component to display OpenRouter balance information
@@ -48,8 +51,6 @@ const OpenRouterBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
* Props for the OpenRouterProvider component
*/
interface OpenRouterProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
uriScheme?: string
@@ -58,28 +59,20 @@ interface OpenRouterProviderProps {
/**
* The OpenRouter provider configuration component
*/
export const OpenRouterProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
uriScheme,
}: OpenRouterProviderProps) => {
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
export const OpenRouterProvider = ({ showModelOptions, isPopup, uriScheme }: OpenRouterProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Create a wrapper for handling field changes more directly
const handleFieldChange = (field: keyof ApiConfiguration) => (value: any) => {
handleInputChange(field)({ target: { value } })
}
const [providerSortingSelected, setProviderSortingSelected] = useState(!!apiConfiguration?.openRouterProviderSorting)
return (
<div>
<div>
<VSCodeTextField
value={apiConfiguration?.openRouterApiKey || ""}
<DebouncedTextField
initialValue={apiConfiguration?.openRouterApiKey || ""}
onChange={(value) => handleFieldChange("openRouterApiKey", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("openRouterApiKey")}
placeholder="Enter API Key...">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "100%" }}>
<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
@@ -87,7 +80,7 @@ export const OpenRouterProvider = ({
<OpenRouterBalanceDisplay apiKey={apiConfiguration.openRouterApiKey} />
)}
</div>
</VSCodeTextField>
</DebouncedTextField>
{!apiConfiguration?.openRouterApiKey && (
<VSCodeButtonLink
href={getOpenRouterAuthUrl(uriScheme)}
@@ -115,7 +108,7 @@ export const OpenRouterProvider = ({
const isChecked = e.target.checked === true
setProviderSortingSelected(isChecked)
if (!isChecked) {
handleFieldChange("openRouterProviderSorting")("")
handleFieldChange("openRouterProviderSorting", "")
}
}}>
Sort underlying provider routing
@@ -128,7 +121,7 @@ export const OpenRouterProvider = ({
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.openRouterProviderSorting}
onChange={(e: any) => {
handleFieldChange("openRouterProviderSorting")(e.target.value)
handleFieldChange("openRouterProviderSorting", e.target.value)
}}>
<VSCodeOption value="">Default</VSCodeOption>
<VSCodeOption value="price">Price</VSCodeOption>
@@ -6,6 +6,8 @@ import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { DROPDOWN_Z_INDEX } from "../ApiOptions"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
const SUPPORTED_THINKING_MODELS = [
"qwen3-235b-a22b",
@@ -24,23 +26,17 @@ const SUPPORTED_THINKING_MODELS = [
* Props for the QwenProvider component
*/
interface QwenProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
/**
* The Alibaba Qwen provider configuration component
*/
export const QwenProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: QwenProviderProps) => {
export const QwenProvider = ({ showModelOptions, isPopup }: QwenProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
@@ -56,7 +52,7 @@ export const QwenProvider = ({
<VSCodeDropdown
id="qwen-line-provider"
value={apiConfiguration?.qwenApiLine || "china"}
onChange={handleInputChange("qwenApiLine")}
onChange={(e: any) => handleFieldChange("qwenApiLine", e.target.value)}
style={{
minWidth: 130,
position: "relative",
@@ -76,8 +72,8 @@ export const QwenProvider = ({
</p>
<ApiKeyField
value={apiConfiguration?.qwenApiKey || ""}
onChange={handleInputChange("qwenApiKey")}
initialValue={apiConfiguration?.qwenApiKey || ""}
onChange={(value) => handleFieldChange("qwenApiKey", value)}
providerName="Qwen"
signupUrl="https://bailian.console.aliyun.com/"
/>
@@ -87,17 +83,13 @@ export const QwenProvider = ({
<ModelSelector
models={qwenModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
zIndex={DROPDOWN_Z_INDEX - 2}
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -1,13 +1,12 @@
import { ApiConfiguration } from "@shared/api"
import { ApiKeyField } from "../common/ApiKeyField"
import RequestyModelPicker from "../RequestyModelPicker"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the RequestyProvider component
*/
interface RequestyProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -15,12 +14,15 @@ interface RequestyProviderProps {
/**
* The Requesty provider configuration component
*/
export const RequestyProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: RequestyProviderProps) => {
export const RequestyProvider = ({ showModelOptions, isPopup }: RequestyProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
return (
<div>
<ApiKeyField
value={apiConfiguration?.requestyApiKey || ""}
onChange={handleInputChange("requestyApiKey")}
initialValue={apiConfiguration?.requestyApiKey || ""}
onChange={(value) => handleFieldChange("requestyApiKey", value)}
providerName="Requesty"
signupUrl="https://app.requesty.ai/manage-api"
/>
@@ -3,13 +3,13 @@ import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the SambanovaProvider component
*/
interface SambanovaProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,15 +17,18 @@ interface SambanovaProviderProps {
/**
* The SambaNova provider configuration component
*/
export const SambanovaProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: SambanovaProviderProps) => {
export const SambanovaProvider = ({ showModelOptions, isPopup }: SambanovaProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div>
<ApiKeyField
value={apiConfiguration?.sambanovaApiKey || ""}
onChange={handleInputChange("sambanovaApiKey")}
initialValue={apiConfiguration?.sambanovaApiKey || ""}
onChange={(value) => handleFieldChange("sambanovaApiKey", value)}
providerName="SambaNova"
signupUrl="https://docs.sambanova.ai/cloud/docs/get-started/overview"
/>
@@ -35,7 +38,7 @@ export const SambanovaProvider = ({ apiConfiguration, handleInputChange, showMod
<ModelSelector
models={sambanovaModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,15 +1,16 @@
import { ApiConfiguration, sapAiCoreModels } from "@shared/api"
import { VSCodeTextField, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { sapAiCoreModels } from "@shared/api"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the SapAiCoreProvider component
*/
interface SapAiCoreProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -17,62 +18,65 @@ interface SapAiCoreProviderProps {
/**
* The SAP AI Core provider configuration component
*/
export const SapAiCoreProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: SapAiCoreProviderProps) => {
export const SapAiCoreProvider = ({ showModelOptions, isPopup }: SapAiCoreProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
return (
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
<VSCodeTextField
value={apiConfiguration?.sapAiCoreClientId || ""}
<DebouncedTextField
initialValue={apiConfiguration?.sapAiCoreClientId || ""}
onChange={(value) => handleFieldChange("sapAiCoreClientId", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("sapAiCoreClientId")}
placeholder="Enter AI Core Client Id...">
<span style={{ fontWeight: 500 }}>AI Core Client Id</span>
</VSCodeTextField>
</DebouncedTextField>
{apiConfiguration?.sapAiCoreClientId && (
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>
Client Id is set. To change it, please re-enter the value.
</p>
)}
<VSCodeTextField
value={apiConfiguration?.sapAiCoreClientSecret ? "********" : ""}
<DebouncedTextField
initialValue={apiConfiguration?.sapAiCoreClientSecret ? "********" : ""}
onChange={(value) => handleFieldChange("sapAiCoreClientSecret", value)}
style={{ width: "100%" }}
type="password"
onInput={handleInputChange("sapAiCoreClientSecret")}
placeholder="Enter AI Core Client Secret...">
<span style={{ fontWeight: 500 }}>AI Core Client Secret</span>
</VSCodeTextField>
</DebouncedTextField>
{apiConfiguration?.sapAiCoreClientSecret && (
<p style={{ fontSize: "12px", color: "var(--vscode-descriptionForeground)" }}>
Client Secret is set. To change it, please re-enter the value.
</p>
)}
<VSCodeTextField
value={apiConfiguration?.sapAiCoreBaseUrl || ""}
<DebouncedTextField
initialValue={apiConfiguration?.sapAiCoreBaseUrl || ""}
onChange={(value) => handleFieldChange("sapAiCoreBaseUrl", value)}
style={{ width: "100%" }}
onInput={handleInputChange("sapAiCoreBaseUrl")}
placeholder="Enter AI Core Base URL...">
<span style={{ fontWeight: 500 }}>AI Core Base URL</span>
</VSCodeTextField>
</DebouncedTextField>
<VSCodeTextField
value={apiConfiguration?.sapAiCoreTokenUrl || ""}
<DebouncedTextField
initialValue={apiConfiguration?.sapAiCoreTokenUrl || ""}
onChange={(value) => handleFieldChange("sapAiCoreTokenUrl", value)}
style={{ width: "100%" }}
onInput={handleInputChange("sapAiCoreTokenUrl")}
placeholder="Enter AI Core Auth URL...">
<span style={{ fontWeight: 500 }}>AI Core Auth URL</span>
</VSCodeTextField>
</DebouncedTextField>
<VSCodeTextField
value={apiConfiguration?.sapAiResourceGroup || ""}
<DebouncedTextField
initialValue={apiConfiguration?.sapAiResourceGroup || ""}
onChange={(value) => handleFieldChange("sapAiResourceGroup", value)}
style={{ width: "100%" }}
onInput={handleInputChange("sapAiResourceGroup")}
placeholder="Enter AI Core Resource Group...">
<span style={{ fontWeight: 500 }}>AI Core Resource Group</span>
</VSCodeTextField>
</DebouncedTextField>
<p
style={{
@@ -93,7 +97,7 @@ export const SapAiCoreProvider = ({ apiConfiguration, handleInputChange, showMod
<ModelSelector
models={sapAiCoreModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -1,13 +1,13 @@
import { ApiConfiguration } from "@shared/api"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ApiKeyField } from "../common/ApiKeyField"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the TogetherProvider component
*/
interface TogetherProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
}
@@ -15,21 +15,24 @@ interface TogetherProviderProps {
/**
* The Together provider configuration component
*/
export const TogetherProvider = ({ apiConfiguration, handleInputChange, showModelOptions, isPopup }: TogetherProviderProps) => {
export const TogetherProvider = ({ showModelOptions, isPopup }: TogetherProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
return (
<div>
<ApiKeyField
value={apiConfiguration?.togetherApiKey || ""}
onChange={handleInputChange("togetherApiKey")}
initialValue={apiConfiguration?.togetherApiKey || ""}
onChange={(value) => handleFieldChange("togetherApiKey", value)}
providerName="Together"
/>
<VSCodeTextField
value={apiConfiguration?.togetherModelId || ""}
<DebouncedTextField
initialValue={apiConfiguration?.togetherModelId || ""}
onChange={(value) => handleFieldChange("togetherModelId", value)}
style={{ width: "100%" }}
onInput={handleInputChange("togetherModelId")}
placeholder={"Enter Model ID..."}>
<span style={{ fontWeight: 500 }}>Model ID</span>
</VSCodeTextField>
</DebouncedTextField>
<p
style={{
fontSize: "12px",
@@ -1,4 +1,3 @@
import { ApiConfiguration } from "@shared/api"
import { EmptyRequest } from "@shared/proto/common"
import { ModelsServiceClient } from "@/services/grpc-client"
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
@@ -6,14 +5,13 @@ import { useState, useCallback, useEffect } from "react"
import { useInterval } from "react-use"
import * as vscodemodels from "vscode"
import { DropdownContainer, DROPDOWN_Z_INDEX } from "../ApiOptions"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
interface VSCodeLmProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
}
export const VSCodeLmProvider = ({ apiConfiguration, handleInputChange }: VSCodeLmProviderProps) => {
export const VSCodeLmProvider = () => {
const [vsCodeLmModels, setVsCodeLmModels] = useState<vscodemodels.LanguageModelChatSelector[]>([])
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Poll VS Code LM models
const requestVsCodeLmModels = useCallback(async () => {
@@ -54,11 +52,8 @@ export const VSCodeLmProvider = ({ apiConfiguration, handleInputChange }: VSCode
return
}
const [vendor, family] = value.split("/")
handleInputChange("vsCodeLmModelSelector")({
target: {
value: { vendor, family },
},
})
handleFieldChange("vsCodeLmModelSelector", { vendor, family })
}}
style={{ width: "100%" }}>
<VSCodeOption value="">Select a model...</VSCodeOption>
@@ -1,20 +1,20 @@
import { ApiConfiguration, vertexGlobalModels, vertexModels } from "@shared/api"
import { VSCodeTextField, VSCodeDropdown, VSCodeOption, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { vertexGlobalModels, vertexModels } from "@shared/api"
import { VSCodeDropdown, VSCodeOption, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelSelector } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { DropdownContainer, DROPDOWN_Z_INDEX } from "../ApiOptions"
import ThinkingBudgetSlider from "../ThinkingBudgetSlider"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
/**
* Props for the VertexProvider component
*/
interface VertexProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
// Vertex models that support thinking
@@ -29,13 +29,10 @@ const SUPPORTED_THINKING_MODELS = [
/**
* The GCP Vertex AI provider configuration component
*/
export const VertexProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: VertexProviderProps) => {
export const VertexProvider = ({ showModelOptions, isPopup }: VertexProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
@@ -49,13 +46,13 @@ export const VertexProvider = ({
flexDirection: "column",
gap: 5,
}}>
<VSCodeTextField
value={apiConfiguration?.vertexProjectId || ""}
<DebouncedTextField
initialValue={apiConfiguration?.vertexProjectId || ""}
onChange={(value) => handleFieldChange("vertexProjectId", value)}
style={{ width: "100%" }}
onInput={handleInputChange("vertexProjectId")}
placeholder="Enter Project ID...">
<span style={{ fontWeight: 500 }}>Google Cloud Project ID</span>
</VSCodeTextField>
</DebouncedTextField>
<DropdownContainer zIndex={DROPDOWN_Z_INDEX - 1} className="dropdown-container">
<label htmlFor="vertex-region-dropdown">
@@ -65,7 +62,7 @@ export const VertexProvider = ({
id="vertex-region-dropdown"
value={apiConfiguration?.vertexRegion || ""}
style={{ width: "100%" }}
onChange={handleInputChange("vertexRegion")}>
onChange={(e: any) => handleFieldChange("vertexRegion", e.target.value)}>
<VSCodeOption value="">Select a region...</VSCodeOption>
<VSCodeOption value="us-east5">us-east5</VSCodeOption>
<VSCodeOption value="us-central1">us-central1</VSCodeOption>
@@ -100,17 +97,13 @@ export const VertexProvider = ({
<ModelSelector
models={modelsToUse}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
zIndex={DROPDOWN_Z_INDEX - 2}
/>
{SUPPORTED_THINKING_MODELS.includes(selectedModelId) && (
<ThinkingBudgetSlider
apiConfiguration={apiConfiguration}
setApiConfiguration={setApiConfiguration}
maxBudget={selectedModelInfo.thinkingConfig?.maxBudget}
/>
<ThinkingBudgetSlider maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
)}
<ModelInfoView selectedModelId={selectedModelId} modelInfo={selectedModelInfo} isPopup={isPopup} />
@@ -1,33 +1,26 @@
import { ApiConfiguration, xaiModels } from "@shared/api"
import { xaiModels } from "@shared/api"
import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { useState } from "react"
import { useState, useEffect } from "react"
import { ApiKeyField } from "../common/ApiKeyField"
import { ModelSelector, DropdownContainer } from "../common/ModelSelector"
import { ModelInfoView } from "../common/ModelInfoView"
import { normalizeApiConfiguration } from "../utils/providerUtils"
import { DROPDOWN_Z_INDEX } from "../ApiOptions"
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
import { useExtensionState } from "@/context/ExtensionStateContext"
/**
* Props for the XaiProvider component
*/
interface XaiProviderProps {
apiConfiguration: ApiConfiguration
handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
showModelOptions: boolean
isPopup?: boolean
setApiConfiguration: (config: ApiConfiguration) => void
}
/**
* The xAI provider configuration component
*/
export const XaiProvider = ({
apiConfiguration,
handleInputChange,
showModelOptions,
isPopup,
setApiConfiguration,
}: XaiProviderProps) => {
export const XaiProvider = ({ showModelOptions, isPopup }: XaiProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange } = useApiConfigurationHandlers()
// Get the normalized configuration
const { selectedModelId, selectedModelInfo } = normalizeApiConfiguration(apiConfiguration)
@@ -38,8 +31,8 @@ export const XaiProvider = ({
<div>
<div>
<ApiKeyField
value={apiConfiguration?.xaiApiKey || ""}
onChange={handleInputChange("xaiApiKey")}
initialValue={apiConfiguration?.xaiApiKey || ""}
onChange={(value) => handleFieldChange("xaiApiKey", value)}
providerName="X AI"
signupUrl="https://x.ai"
/>
@@ -61,7 +54,7 @@ export const XaiProvider = ({
<ModelSelector
models={xaiModels}
selectedModelId={selectedModelId}
onChange={handleInputChange("apiModelId")}
onChange={(e: any) => handleFieldChange("apiModelId", e.target.value)}
label="Model"
/>
@@ -74,10 +67,7 @@ export const XaiProvider = ({
const isChecked = e.target.checked === true
setReasoningEffortSelected(isChecked)
if (!isChecked) {
setApiConfiguration({
...apiConfiguration,
reasoningEffort: "",
})
handleFieldChange("reasoningEffort", "")
}
}}>
Modify reasoning effort
@@ -94,10 +84,7 @@ export const XaiProvider = ({
style={{ width: "100%", marginTop: 3 }}
value={apiConfiguration?.reasoningEffort || "high"}
onChange={(e: any) => {
setApiConfiguration({
...apiConfiguration,
reasoningEffort: e.target.value,
})
handleFieldChange("reasoningEffort", e.target.value)
}}>
<VSCodeOption value="low">low</VSCodeOption>
<VSCodeOption value="high">high</VSCodeOption>
@@ -2,30 +2,22 @@ import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { TabButton } from "../../mcp/configuration/McpConfigurationView"
import ApiOptions from "../ApiOptions"
import Section from "../Section"
import { ChatSettings } from "@shared/ChatSettings"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { StateServiceClient } from "@/services/grpc-client"
import { UpdateSettingsRequest } from "@shared/proto/state"
interface ApiConfigurationSectionProps {
planActSeparateModelsSetting: boolean
chatSettings: ChatSettings
isSwitchingMode: boolean
apiErrorMessage?: string
modelIdErrorMessage?: string
handlePlanActModeChange: (mode: "plan" | "act") => Promise<void>
setPlanActSeparateModelsSetting: (value: boolean) => void
renderSectionHeader: (tabId: string) => JSX.Element | null
}
const ApiConfigurationSection = ({
planActSeparateModelsSetting,
chatSettings,
isSwitchingMode,
apiErrorMessage,
modelIdErrorMessage,
handlePlanActModeChange,
setPlanActSeparateModelsSetting,
renderSectionHeader,
}: ApiConfigurationSectionProps) => {
const { planActSeparateModelsSetting, chatSettings } = useExtensionState()
return (
<div>
{renderSectionHeader("api-config")}
@@ -58,30 +50,28 @@ const ApiConfigurationSection = ({
{/* Content container */}
<div className="-mb-3">
<ApiOptions
key={chatSettings.mode}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
<ApiOptions showModelOptions={true} />
</div>
</div>
) : (
<ApiOptions
key={"single"}
showModelOptions={true}
apiErrorMessage={apiErrorMessage}
modelIdErrorMessage={modelIdErrorMessage}
/>
<ApiOptions showModelOptions={true} />
)}
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={planActSeparateModelsSetting}
onChange={(e: any) => {
onChange={async (e: any) => {
const checked = e.target.checked === true
setPlanActSeparateModelsSetting(checked)
try {
await StateServiceClient.updateSettings(
UpdateSettingsRequest.create({
planActSeparateModelsSetting: checked,
}),
)
} catch (error) {
console.error("Failed to update separate models setting:", error)
}
}}>
Use different models for Plan and Act modes
</VSCodeCheckbox>
@@ -1,17 +1,15 @@
import { VSCodeButton, VSCodeCheckbox, VSCodeDropdown, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import debounce from "debounce"
import React, { useCallback, useEffect, useState } from "react"
import styled from "styled-components"
import { BROWSER_VIEWPORT_PRESETS } from "../../../../../src/shared/BrowserSettings"
import { useExtensionState } from "../../../context/ExtensionStateContext"
import { BrowserServiceClient } from "../../../services/grpc-client"
import { EmptyRequest, StringRequest } from "@shared/proto/common"
import { BrowserSettings } from "@shared/BrowserSettings"
import { updateBrowserSetting } from "../utils/settingsHandlers"
import { DebouncedTextField } from "../common/DebouncedTextField"
import Section from "../Section"
interface BrowserSettingsSectionProps {
localBrowserSettings: BrowserSettings
onBrowserSettingsChange: (settings: BrowserSettings) => void
renderSectionHeader: (tabId: string) => JSX.Element | null
}
@@ -58,13 +56,8 @@ const CollapsibleContent = styled.div<{ isOpen: boolean }>`
visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")};
`
export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
localBrowserSettings,
onBrowserSettingsChange,
renderSectionHeader,
}) => {
export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({ renderSectionHeader }) => {
const { browserSettings } = useExtensionState()
const [localChromePath, setLocalChromePath] = useState(localBrowserSettings.chromeExecutablePath || "")
const [isCheckingConnection, setIsCheckingConnection] = useState(false)
const [connectionStatus, setConnectionStatus] = useState<boolean | null>(null)
const [relaunchResult, setRelaunchResult] = useState<{ success: boolean; message: string } | null>(null)
@@ -78,15 +71,12 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
const timer = setTimeout(() => {
setRelaunchResult(null)
}, 15000)
// Clear timeout if component unmounts or relaunchResult changes
return () => clearTimeout(timer)
}
}, [relaunchResult])
// Request detected Chrome path on mount
useEffect(() => {
// Use gRPC for getDetectedChromePath
BrowserServiceClient.getDetectedChromePath(EmptyRequest.create({}))
.then((result) => {
setDetectedChromePath(result.path)
@@ -97,111 +87,10 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
})
}, [])
// Sync localChromePath with prop changes
useEffect(() => {
if (localBrowserSettings.chromeExecutablePath !== localChromePath) {
setLocalChromePath(localBrowserSettings.chromeExecutablePath || "")
}
}, [localBrowserSettings.chromeExecutablePath])
// Debounced connection check function
const debouncedCheckConnection = useCallback(
debounce(() => {
if (localBrowserSettings.remoteBrowserEnabled) {
setIsCheckingConnection(true)
setConnectionStatus(null)
if (localBrowserSettings.remoteBrowserHost) {
// Use gRPC for testBrowserConnection
BrowserServiceClient.testBrowserConnection(
StringRequest.create({ value: localBrowserSettings.remoteBrowserHost }),
)
.then((result) => {
setConnectionStatus(result.success)
setIsCheckingConnection(false)
})
.catch((error) => {
console.error("Error testing browser connection:", error)
setConnectionStatus(false)
setIsCheckingConnection(false)
})
} else {
BrowserServiceClient.discoverBrowser(EmptyRequest.create({}))
.then((result) => {
setConnectionStatus(result.success)
setIsCheckingConnection(false)
})
.catch((error) => {
console.error("Error discovering browser:", error)
setConnectionStatus(false)
setIsCheckingConnection(false)
})
}
}
}, 1000),
[localBrowserSettings.remoteBrowserEnabled, localBrowserSettings.remoteBrowserHost],
)
// Check connection when component mounts or when remote settings change
useEffect(() => {
if (localBrowserSettings.remoteBrowserEnabled) {
debouncedCheckConnection()
} else {
setConnectionStatus(null)
}
}, [localBrowserSettings.remoteBrowserEnabled, localBrowserSettings.remoteBrowserHost, debouncedCheckConnection])
const handleViewportChange = (event: Event) => {
const target = event.target as HTMLSelectElement
const selectedSize = BROWSER_VIEWPORT_PRESETS[target.value as keyof typeof BROWSER_VIEWPORT_PRESETS]
if (selectedSize) {
onBrowserSettingsChange({
...localBrowserSettings,
viewport: {
width: selectedSize.width,
height: selectedSize.height,
},
})
}
}
const updateRemoteBrowserEnabled = (enabled: boolean) => {
onBrowserSettingsChange({
...localBrowserSettings,
remoteBrowserEnabled: enabled,
// If disabling, also clear the host
remoteBrowserHost: enabled ? localBrowserSettings.remoteBrowserHost : undefined,
})
}
const updateRemoteBrowserHost = (host: string | undefined) => {
onBrowserSettingsChange({
...localBrowserSettings,
remoteBrowserHost: host,
})
}
const debouncedUpdateChromePath = useCallback(
debounce((newPath: string | undefined) => {
onBrowserSettingsChange({
...localBrowserSettings,
chromeExecutablePath: newPath,
})
}, 500),
[localBrowserSettings, onBrowserSettingsChange],
)
const updateChromeExecutablePath = (path: string | undefined) => {
setLocalChromePath(path || "")
debouncedUpdateChromePath(path)
}
// Function to check connection once without changing UI state immediately
const checkConnectionOnce = useCallback(() => {
// Don't show the spinner for every check to avoid UI flicker
// We'll rely on the response to update the connectionStatus
if (localBrowserSettings.remoteBrowserHost) {
// Use gRPC for testBrowserConnection
BrowserServiceClient.testBrowserConnection(StringRequest.create({ value: localBrowserSettings.remoteBrowserHost }))
if (browserSettings.remoteBrowserHost) {
BrowserServiceClient.testBrowserConnection(StringRequest.create({ value: browserSettings.remoteBrowserHost }))
.then((result) => {
setConnectionStatus(result.success)
})
@@ -219,40 +108,37 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
setConnectionStatus(false)
})
}
}, [localBrowserSettings.remoteBrowserHost])
}, [browserSettings.remoteBrowserHost])
// Setup continuous polling for connection status when remote browser is enabled
useEffect(() => {
// Only poll if remote browser mode is enabled
if (!localBrowserSettings.remoteBrowserEnabled) {
// Make sure we're not showing checking state when disabled
if (!browserSettings.remoteBrowserEnabled) {
setIsCheckingConnection(false)
return
}
// Check immediately when enabled
checkConnectionOnce()
// Then check every second
const pollInterval = setInterval(() => {
checkConnectionOnce()
}, 1000)
// Cleanup the interval if the component unmounts or remote browser is disabled
return () => clearInterval(pollInterval)
}, [localBrowserSettings.remoteBrowserEnabled, checkConnectionOnce])
}, [browserSettings.remoteBrowserEnabled, checkConnectionOnce])
const updateDisableToolUse = (disabled: boolean) => {
onBrowserSettingsChange({
...localBrowserSettings,
disableToolUse: disabled,
})
const handleViewportChange = (event: Event) => {
const target = event.target as HTMLSelectElement
const selectedSize = BROWSER_VIEWPORT_PRESETS[target.value as keyof typeof BROWSER_VIEWPORT_PRESETS]
if (selectedSize) {
updateBrowserSetting("viewport", {
width: selectedSize.width,
height: selectedSize.height,
})
}
}
const relaunchChromeDebugMode = () => {
setDebugMode(true)
setRelaunchResult(null)
// The connection status will be automatically updated by our polling
BrowserServiceClient.relaunchChromeDebugMode(EmptyRequest.create({}))
.then((result) => {
@@ -273,9 +159,9 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
}
// Determine if we should show the relaunch button
const isRemoteEnabled = Boolean(localBrowserSettings.remoteBrowserEnabled)
const isRemoteEnabled = Boolean(browserSettings.remoteBrowserEnabled)
const shouldShowRelaunchButton = isRemoteEnabled && connectionStatus === false
const isSubSettingsOpen = !(localBrowserSettings.disableToolUse || false)
const isSubSettingsOpen = !(browserSettings.disableToolUse || false)
return (
<div>
@@ -285,8 +171,8 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
{/* Master Toggle */}
<div style={{ marginBottom: isSubSettingsOpen ? 0 : 10 }}>
<VSCodeCheckbox
checked={localBrowserSettings.disableToolUse || false}
onChange={(e) => updateDisableToolUse((e.target as HTMLInputElement).checked)}>
checked={browserSettings.disableToolUse || false}
onChange={(e) => updateBrowserSetting("disableToolUse", (e.target as HTMLInputElement).checked)}>
Disable browser tool usage
</VSCodeCheckbox>
<p
@@ -309,8 +195,8 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
Object.entries(BROWSER_VIEWPORT_PRESETS).find(([_, size]) => {
const typedSize = size as { width: number; height: number }
return (
typedSize.width === localBrowserSettings.viewport.width &&
typedSize.height === localBrowserSettings.viewport.height
typedSize.width === browserSettings.viewport.width &&
typedSize.height === browserSettings.viewport.height
)
})?.[0]
}
@@ -343,14 +229,21 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
justifyContent: "space-between",
}}>
<VSCodeCheckbox
checked={localBrowserSettings.remoteBrowserEnabled}
onChange={(e) => updateRemoteBrowserEnabled((e.target as HTMLInputElement).checked)}>
checked={browserSettings.remoteBrowserEnabled}
onChange={(e) => {
const enabled = (e.target as HTMLInputElement).checked
updateBrowserSetting("remoteBrowserEnabled", enabled)
// If disabling, also clear the host
if (!enabled) {
updateBrowserSetting("remoteBrowserHost", undefined)
}
}}>
Use remote browser connection
</VSCodeCheckbox>
<ConnectionStatusIndicator
isChecking={isCheckingConnection}
isConnected={connectionStatus}
remoteBrowserEnabled={localBrowserSettings.remoteBrowserEnabled}
remoteBrowserEnabled={browserSettings.remoteBrowserEnabled}
/>
</div>
<p
@@ -367,7 +260,7 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
: ""}
. You can specify a custom path below. Using a remote browser connection requires starting Chrome
in debug mode
{localBrowserSettings.remoteBrowserEnabled ? (
{browserSettings.remoteBrowserEnabled ? (
<>
{" "}
manually (<code>--remote-debugging-port=9222</code>) or using the button below. Enter the
@@ -378,13 +271,13 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
)}
</p>
{/* Moved remote-specific settings to appear directly after enabling remote connection */}
{localBrowserSettings.remoteBrowserEnabled && (
{browserSettings.remoteBrowserEnabled && (
<div style={{ marginLeft: 0, marginTop: 8 }}>
<VSCodeTextField
value={localBrowserSettings.remoteBrowserHost || ""}
<DebouncedTextField
initialValue={browserSettings.remoteBrowserHost || ""}
placeholder="http://localhost:9222"
style={{ width: "100%", marginBottom: 8 }}
onChange={(e: any) => updateRemoteBrowserHost(e.target.value || undefined)}
onChange={(value) => updateBrowserSetting("remoteBrowserHost", value || undefined)}
/>
{shouldShowRelaunchButton && (
@@ -433,15 +326,12 @@ export const BrowserSettingsSection: React.FC<BrowserSettingsSectionProps> = ({
style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
Chrome Executable Path (Optional)
</label>
<VSCodeTextField
<DebouncedTextField
id="chrome-executable-path"
value={localChromePath}
initialValue={browserSettings.chromeExecutablePath || ""}
placeholder="e.g., /usr/bin/google-chrome or C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
style={{ width: "100%" }}
onChange={(e: any) => {
const newValue = e.target.value || ""
updateChromeExecutablePath(newValue)
}}
onChange={(value) => updateBrowserSetting("chromeExecutablePath", value || undefined)}
/>
<p
style={{
@@ -2,6 +2,8 @@ import { VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui
import { useExtensionState } from "@/context/ExtensionStateContext"
import { memo } from "react"
import { OpenAIReasoningEffort } from "@shared/ChatSettings"
import { updateSetting } from "../utils/settingsHandlers"
import { convertChatSettingsToProtoChatSettings } from "@shared/proto-conversions/state/chat-settings-conversion"
import Section from "../Section"
interface FeatureSettingsSectionProps {
@@ -9,18 +11,20 @@ interface FeatureSettingsSectionProps {
}
const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionProps) => {
const {
enableCheckpointsSetting,
setEnableCheckpointsSetting,
mcpMarketplaceEnabled,
setMcpMarketplaceEnabled,
mcpRichDisplayEnabled,
setMcpRichDisplayEnabled,
mcpResponsesCollapsed,
setMcpResponsesCollapsed,
chatSettings,
setChatSettings,
} = useExtensionState()
const { enableCheckpointsSetting, mcpMarketplaceEnabled, mcpRichDisplayEnabled, mcpResponsesCollapsed, chatSettings } =
useExtensionState()
const handleReasoningEffortChange = (newValue: OpenAIReasoningEffort) => {
if (!chatSettings) return
const updatedChatSettings = {
...chatSettings,
openAIReasoningEffort: newValue,
}
const protoChatSettings = convertChatSettingsToProtoChatSettings(updatedChatSettings)
updateSetting("chatSettings", protoChatSettings)
}
return (
<div>
@@ -32,7 +36,7 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
checked={enableCheckpointsSetting}
onChange={(e: any) => {
const checked = e.target.checked === true
setEnableCheckpointsSetting(checked)
updateSetting("enableCheckpointsSetting", checked)
}}>
Enable Checkpoints
</VSCodeCheckbox>
@@ -46,7 +50,7 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
checked={mcpMarketplaceEnabled}
onChange={(e: any) => {
const checked = e.target.checked === true
setMcpMarketplaceEnabled(checked)
updateSetting("mcpMarketplaceEnabled", checked)
}}>
Enable MCP Marketplace
</VSCodeCheckbox>
@@ -59,7 +63,7 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
checked={mcpRichDisplayEnabled}
onChange={(e: any) => {
const checked = e.target.checked === true
setMcpRichDisplayEnabled(checked)
updateSetting("mcpRichDisplayEnabled", checked)
}}>
Enable Rich MCP Display
</VSCodeCheckbox>
@@ -72,7 +76,7 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
checked={mcpResponsesCollapsed}
onChange={(e: any) => {
const checked = e.target.checked === true
setMcpResponsesCollapsed(checked)
updateSetting("mcpResponsesCollapsed", checked)
}}>
Collapse MCP Responses
</VSCodeCheckbox>
@@ -91,10 +95,7 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
currentValue={chatSettings.openAIReasoningEffort || "medium"}
onChange={(e: any) => {
const newValue = e.target.currentValue as OpenAIReasoningEffort
setChatSettings({
...chatSettings,
openAIReasoningEffort: newValue,
})
handleReasoningEffortChange(newValue)
}}
className="w-full">
<VSCodeOption value="low">Low</VSCodeOption>
@@ -1,29 +1,21 @@
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { ChatSettings } from "@shared/ChatSettings"
import { TelemetrySetting } from "@shared/TelemetrySetting"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { updateSetting } from "../utils/settingsHandlers"
import PreferredLanguageSetting from "../PreferredLanguageSetting"
import Section from "../Section"
interface GeneralSettingsSectionProps {
chatSettings: ChatSettings
setChatSettings: (settings: ChatSettings) => void
telemetrySetting: string
setTelemetrySetting: (value: TelemetrySetting) => void
renderSectionHeader: (tabId: string) => JSX.Element | null
}
const GeneralSettingsSection = ({
chatSettings,
setChatSettings,
telemetrySetting,
setTelemetrySetting,
renderSectionHeader,
}: GeneralSettingsSectionProps) => {
const GeneralSettingsSection = ({ renderSectionHeader }: GeneralSettingsSectionProps) => {
const { telemetrySetting } = useExtensionState()
return (
<div>
{renderSectionHeader("general")}
<Section>
{chatSettings && <PreferredLanguageSetting chatSettings={chatSettings} setChatSettings={setChatSettings} />}
<PreferredLanguageSetting />
<div className="mb-[5px]">
<VSCodeCheckbox
@@ -31,7 +23,7 @@ const GeneralSettingsSection = ({
checked={telemetrySetting !== "disabled"}
onChange={(e: any) => {
const checked = e.target.checked === true
setTelemetrySetting(checked ? "enabled" : "disabled")
updateSetting("telemetrySetting", checked ? "enabled" : "disabled")
}}>
Allow anonymous error and usage reporting
</VSCodeCheckbox>
@@ -3,24 +3,17 @@ import { VSCodeTextField, VSCodeCheckbox, VSCodeDropdown, VSCodeOption } from "@
import { useExtensionState } from "@/context/ExtensionStateContext"
import TerminalOutputLineLimitSlider from "../TerminalOutputLineLimitSlider"
import { StateServiceClient } from "../../../services/grpc-client"
import { Int64, Int64Request } from "@shared/proto/common"
import { Int64, Int64Request, StringRequest } from "@shared/proto/common"
import Section from "../Section"
import { updateSetting } from "../utils/settingsHandlers"
interface TerminalSettingsSectionProps {
renderSectionHeader: (tabId: string) => JSX.Element | null
}
export const TerminalSettingsSection: React.FC<TerminalSettingsSectionProps> = ({ renderSectionHeader }) => {
const {
shellIntegrationTimeout,
setShellIntegrationTimeout,
terminalReuseEnabled,
setTerminalReuseEnabled,
defaultTerminalProfile,
setDefaultTerminalProfile,
availableTerminalProfiles,
platform,
} = useExtensionState()
const { shellIntegrationTimeout, terminalReuseEnabled, defaultTerminalProfile, availableTerminalProfiles } =
useExtensionState()
const [inputValue, setInputValue] = useState((shellIntegrationTimeout / 1000).toString())
const [inputError, setInputError] = useState<string | null>(null)
@@ -40,13 +33,12 @@ export const TerminalSettingsSection: React.FC<TerminalSettingsSectionProps> = (
setInputError(null)
const timeout = Math.round(seconds * 1000)
setShellIntegrationTimeout(timeout)
StateServiceClient.updateTerminalConnectionTimeout({
value: timeout,
} as Int64Request)
.then((response: Int64) => {
setShellIntegrationTimeout(response.value)
// Backend calls postStateToWebview(), so state will update via subscription
// Just sync the input value with the confirmed backend value
setInputValue((response.value / 1000).toString())
})
.catch((error) => {
@@ -64,18 +56,20 @@ export const TerminalSettingsSection: React.FC<TerminalSettingsSectionProps> = (
const handleTerminalReuseChange = (event: Event) => {
const target = event.target as HTMLInputElement
const checked = target.checked
setTerminalReuseEnabled(checked)
StateServiceClient.updateTerminalReuseEnabled({ value: checked } as any).catch((error) => {
console.error("Failed to update terminal reuse enabled:", error)
})
updateSetting("terminalReuseEnabled", checked)
}
// Use any to avoid type conflicts between Event and FormEvent
const handleDefaultTerminalProfileChange = (event: any) => {
const target = event.target as HTMLSelectElement
const profileId = target.value
// Only update the local state, let the Save button handle the backend update
setDefaultTerminalProfile(profileId)
// Save immediately - the backend will call postStateToWebview() to update our state
StateServiceClient.updateDefaultTerminalProfile({
value: profileId || "default",
} as StringRequest).catch((error) => {
console.error("Failed to update default terminal profile:", error)
})
}
const profilesToShow = availableTerminalProfiles
@@ -0,0 +1,35 @@
import { StateServiceClient, BrowserServiceClient } from "@/services/grpc-client"
import { UpdateSettingsRequest } from "@shared/proto/state"
import { UpdateBrowserSettingsRequest } from "@shared/proto/browser"
/**
* Updates a single field in the settings.
*
* @param field - The field key to update
* @param value - The new value for the field
*/
export const updateSetting = (field: keyof UpdateSettingsRequest, value: any) => {
const updateRequest: Partial<UpdateSettingsRequest> = {}
updateRequest[field] = value
StateServiceClient.updateSettings(UpdateSettingsRequest.create(updateRequest)).catch((error) => {
console.error(`Failed to update setting ${field}:`, error)
})
}
/**
* Updates a single browser setting field.
*
* @param field - The field key to update
* @param value - The new value for the field
*/
export const updateBrowserSetting = (field: keyof UpdateBrowserSettingsRequest, value: any) => {
const updateRequest: Partial<UpdateBrowserSettingsRequest> = {
metadata: {},
[field]: value,
}
BrowserServiceClient.updateBrowserSettings(UpdateBrowserSettingsRequest.create(updateRequest)).catch((error) => {
console.error(`Failed to update browser setting ${field}:`, error)
})
}
@@ -0,0 +1,62 @@
import { useExtensionState } from "@/context/ExtensionStateContext"
import { ModelsServiceClient } from "@/services/grpc-client"
import { ApiConfiguration } from "@shared/api"
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
export const useApiConfigurationHandlers = () => {
const { apiConfiguration } = useExtensionState()
/**
* Updates a single field in the API configuration.
*
* **Warning**: If this function is called multiple times in rapid succession,
* it can lead to race conditions where later calls may overwrite changes from
* earlier calls. For updating multiple fields, use `handleFieldsChange` instead.
*
* @param field - The field key to update
* @param value - The new value for the field
*/
const handleFieldChange = <K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => {
const updatedConfig = {
...apiConfiguration,
[field]: value,
}
const protoConfig = convertApiConfigurationToProto(updatedConfig)
ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: protoConfig,
}),
).catch((error) => {
console.error(`Failed to update API configuration field ${field}:`, error)
})
}
/**
* Updates multiple fields in the API configuration at once.
*
* This function should be used when updating multiple fields to avoid race conditions
* that can occur when calling `handleFieldChange` multiple times in succession.
* All updates are applied together as a single operation.
*
* @param updates - An object containing the fields to update and their new values
*/
const handleFieldsChange = (updates: Partial<ApiConfiguration>) => {
const updatedConfig = {
...apiConfiguration,
...updates,
}
const protoConfig = convertApiConfigurationToProto(updatedConfig)
ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: protoConfig,
}),
).catch((error) => {
console.error("Failed to update API configuration fields:", error)
})
}
return { handleFieldChange, handleFieldsChange }
}
@@ -0,0 +1,31 @@
import { useState } from "react"
import { useDebounceEffect } from "@/utils/useDebounceEffect"
/**
* A custom hook that provides debounced input handling to prevent jumpy text inputs
* when saving changes directly to backend on every keystroke.
*
* @param initialValue - The initial value for the input
* @param onChange - Callback function to save the value (e.g., to backend)
* @param debounceMs - Debounce delay in milliseconds (default: 500ms)
* @returns A tuple of [currentValue, setValue] similar to useState
*/
export function useDebouncedInput<T>(
initialValue: T,
onChange: (value: T) => void,
debounceMs: number = 100,
): [T, (value: T) => void] {
// Local state to prevent jumpy input - initialize once
const [localValue, setLocalValue] = useState(initialValue)
// Debounced backend save - saves after user stops changing value
useDebounceEffect(
() => {
onChange(localValue)
},
debounceMs,
[localValue],
)
return [localValue, setLocalValue]
}
@@ -2,13 +2,10 @@ import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { useEffect, useState, memo } from "react"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { validateApiConfiguration } from "@/utils/validate"
import { vscode } from "@/utils/vscode"
import ApiOptions from "@/components/settings/ApiOptions"
import ClineLogoWhite from "@/assets/ClineLogoWhite"
import { AccountServiceClient, ModelsServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { UpdateApiConfigurationRequest } from "@shared/proto/models"
import { convertApiConfigurationToProto } from "@shared/proto-conversions/models/api-configuration-conversion"
import { AccountServiceClient, ModelsServiceClient, StateServiceClient } from "@/services/grpc-client"
import { EmptyRequest, BooleanRequest } from "@shared/proto/common"
const WelcomeView = memo(() => {
const { apiConfiguration } = useExtensionState()
@@ -24,16 +21,10 @@ const WelcomeView = memo(() => {
}
const handleSubmit = async () => {
if (apiConfiguration) {
try {
await ModelsServiceClient.updateApiConfigurationProto(
UpdateApiConfigurationRequest.create({
apiConfiguration: convertApiConfigurationToProto(apiConfiguration),
}),
)
} catch (error) {
console.error("Failed to update API configuration:", error)
}
try {
await StateServiceClient.setWelcomeViewCompleted(BooleanRequest.create({ value: true }))
} catch (error) {
console.error("Failed to update API configuration or complete welcome view:", error)
}
}
@@ -202,6 +202,7 @@ export const ExtensionStateContextProvider: React.FC<{
terminalOutputLineLimit: 500,
defaultTerminalProfile: "default",
isNewUser: false,
welcomeViewCompleted: false,
mcpResponsesCollapsed: false, // Default value (expanded), will be overwritten by extension state
})
const [didHydrateState, setDidHydrateState] = useState(false)
@@ -278,35 +279,7 @@ export const ExtensionStateContextProvider: React.FC<{
}
// Update welcome screen state based on API configuration
const config = stateData.apiConfiguration
const hasKey = config
? [
config.apiKey,
config.openRouterApiKey,
config.awsRegion,
config.vertexProjectId,
config.openAiApiKey,
config.ollamaModelId,
config.lmStudioModelId,
config.liteLlmApiKey,
config.geminiApiKey,
config.openAiNativeApiKey,
config.deepSeekApiKey,
config.requestyApiKey,
config.togetherApiKey,
config.qwenApiKey,
config.doubaoApiKey,
config.mistralApiKey,
config.vsCodeLmModelSelector,
config.clineApiKey,
config.asksageApiKey,
config.xaiApiKey,
config.sambanovaApiKey,
config.sapAiCoreClientId,
].some((key) => key !== undefined)
: false
setShowWelcome(!hasKey)
setShowWelcome(!newState.welcomeViewCompleted)
setDidHydrateState(true)
console.log("[DEBUG] returning new state in ESC")