mirror of
https://github.com/cline/cline.git
synced 2026-09-14 19:39:22 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e81d283e15 | ||
|
|
b65be180f7 | ||
|
|
128ee4b490 | ||
|
|
0a096ad024 | ||
|
|
984d958a81 | ||
|
|
daffe3694b | ||
|
|
43e303763e | ||
|
|
7a65b1df5a | ||
|
|
c8936fee39 | ||
|
|
e178bd3a65 | ||
|
|
5f821aefe8 | ||
|
|
8683e8b296 | ||
|
|
496722f370 | ||
|
|
2b20d5137b | ||
|
|
2e028a49bc | ||
|
|
97a36d5306 | ||
|
|
e67bb6c636 | ||
|
|
fa7794e9a6 | ||
|
|
88029834be | ||
|
|
1f267d058a | ||
|
|
853b8a6470 | ||
|
|
a5258e46e1 | ||
|
|
36dfc7de11 |
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ export type GlobalStateKey =
|
||||
| "terminalReuseEnabled"
|
||||
| "defaultTerminalProfile"
|
||||
| "isNewUser"
|
||||
| "welcomeViewCompleted"
|
||||
| "terminalOutputLineLimit"
|
||||
| "mcpRichDisplayEnabled"
|
||||
| "sapAiCoreTokenUrl"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user