mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-09 03:17:58 +00:00
Organize provider settings into separate components (#3196)
This commit is contained in:
parent
883be32b1a
commit
da6c50bf10
16 changed files with 1550 additions and 1183 deletions
5
.changeset/five-pigs-talk.md
Normal file
5
.changeset/five-pigs-talk.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Organize provider settings into separate components
|
||||
|
|
@ -19,11 +19,11 @@ describe("ChutesHandler", () => {
|
|||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockCreate = (OpenAI as unknown as jest.Mock)().chat.completions.create
|
||||
handler = new ChutesHandler({})
|
||||
handler = new ChutesHandler({ chutesApiKey: "test-chutes-api-key" })
|
||||
})
|
||||
|
||||
test("should use the correct Chutes base URL", () => {
|
||||
new ChutesHandler({})
|
||||
new ChutesHandler({ chutesApiKey: "test-chutes-api-key" })
|
||||
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://llm.chutes.ai/v1" }))
|
||||
})
|
||||
|
||||
|
|
@ -41,9 +41,8 @@ describe("ChutesHandler", () => {
|
|||
|
||||
test("should return specified model when valid model is provided", () => {
|
||||
const testModelId: ChutesModelId = "deepseek-ai/DeepSeek-R1"
|
||||
const handlerWithModel = new ChutesHandler({ apiModelId: testModelId })
|
||||
const handlerWithModel = new ChutesHandler({ apiModelId: testModelId, chutesApiKey: "test-chutes-api-key" })
|
||||
const model = handlerWithModel.getModel()
|
||||
|
||||
expect(model.id).toBe(testModelId)
|
||||
expect(model.info).toEqual(chutesModels[testModelId])
|
||||
})
|
||||
|
|
@ -110,7 +109,7 @@ describe("ChutesHandler", () => {
|
|||
test("createMessage should pass correct parameters to Chutes client", async () => {
|
||||
const modelId: ChutesModelId = "deepseek-ai/DeepSeek-R1"
|
||||
const modelInfo = chutesModels[modelId]
|
||||
const handlerWithModel = new ChutesHandler({ apiModelId: modelId })
|
||||
const handlerWithModel = new ChutesHandler({ apiModelId: modelId, chutesApiKey: "test-chutes-api-key" })
|
||||
|
||||
mockCreate.mockImplementationOnce(() => {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ describe("GroqHandler", () => {
|
|||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockCreate = (OpenAI as unknown as jest.Mock)().chat.completions.create
|
||||
handler = new GroqHandler({})
|
||||
handler = new GroqHandler({ groqApiKey: "test-groq-api-key" })
|
||||
})
|
||||
|
||||
test("should use the correct Groq base URL", () => {
|
||||
new GroqHandler({})
|
||||
new GroqHandler({ groqApiKey: "test-groq-api-key" })
|
||||
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.groq.com/openai/v1" }))
|
||||
})
|
||||
|
||||
|
|
@ -35,17 +35,16 @@ describe("GroqHandler", () => {
|
|||
|
||||
test("should return default model when no model is specified", () => {
|
||||
const model = handler.getModel()
|
||||
expect(model.id).toBe(groqDefaultModelId) // Use groqDefaultModelId
|
||||
expect(model.info).toEqual(groqModels[groqDefaultModelId]) // Use groqModels
|
||||
expect(model.id).toBe(groqDefaultModelId)
|
||||
expect(model.info).toEqual(groqModels[groqDefaultModelId])
|
||||
})
|
||||
|
||||
test("should return specified model when valid model is provided", () => {
|
||||
const testModelId: GroqModelId = "llama-3.3-70b-versatile" // Use a valid Groq model ID and type
|
||||
const handlerWithModel = new GroqHandler({ apiModelId: testModelId }) // Instantiate GroqHandler
|
||||
const testModelId: GroqModelId = "llama-3.3-70b-versatile"
|
||||
const handlerWithModel = new GroqHandler({ apiModelId: testModelId, groqApiKey: "test-groq-api-key" })
|
||||
const model = handlerWithModel.getModel()
|
||||
|
||||
expect(model.id).toBe(testModelId)
|
||||
expect(model.info).toEqual(groqModels[testModelId]) // Use groqModels
|
||||
expect(model.info).toEqual(groqModels[testModelId])
|
||||
})
|
||||
|
||||
test("completePrompt method should return text from Groq API", async () => {
|
||||
|
|
@ -110,7 +109,7 @@ describe("GroqHandler", () => {
|
|||
test("createMessage should pass correct parameters to Groq client", async () => {
|
||||
const modelId: GroqModelId = "llama-3.1-8b-instant"
|
||||
const modelInfo = groqModels[modelId]
|
||||
const handlerWithModel = new GroqHandler({ apiModelId: modelId })
|
||||
const handlerWithModel = new GroqHandler({ apiModelId: modelId, groqApiKey: "test-groq-api-key" })
|
||||
|
||||
mockCreate.mockImplementationOnce(() => {
|
||||
return {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
84
webview-ui/src/components/settings/providers/Anthropic.tsx
Normal file
84
webview-ui/src/components/settings/providers/Anthropic.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { useCallback, useState } from "react"
|
||||
import { Checkbox } from "vscrui"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { ApiConfiguration } from "@roo/shared/api"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
|
||||
|
||||
import { inputEventTransform, noTransform } from "../transforms"
|
||||
|
||||
type AnthropicProps = {
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
|
||||
}
|
||||
|
||||
export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: AnthropicProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ApiConfiguration, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.apiKey || ""}
|
||||
type="password"
|
||||
onInput={handleInputChange("apiKey")}
|
||||
placeholder={t("settings:placeholders.apiKey")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.anthropicApiKey")}</label>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground -mt-2">
|
||||
{t("settings:providers.apiKeyStorageNotice")}
|
||||
</div>
|
||||
{!apiConfiguration?.apiKey && (
|
||||
<VSCodeButtonLink href="https://console.anthropic.com/settings/keys" appearance="secondary">
|
||||
{t("settings:providers.getAnthropicApiKey")}
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={anthropicBaseUrlSelected}
|
||||
onChange={(checked: boolean) => {
|
||||
setAnthropicBaseUrlSelected(checked)
|
||||
|
||||
if (!checked) {
|
||||
setApiConfigurationField("anthropicBaseUrl", "")
|
||||
setApiConfigurationField("anthropicUseAuthToken", false)
|
||||
}
|
||||
}}>
|
||||
{t("settings:providers.useCustomBaseUrl")}
|
||||
</Checkbox>
|
||||
{anthropicBaseUrlSelected && (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.anthropicBaseUrl || ""}
|
||||
type="url"
|
||||
onInput={handleInputChange("anthropicBaseUrl")}
|
||||
placeholder="https://api.anthropic.com"
|
||||
className="w-full mt-1"
|
||||
/>
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.anthropicUseAuthToken ?? false}
|
||||
onChange={handleInputChange("anthropicUseAuthToken", noTransform)}
|
||||
className="w-full mt-1">
|
||||
{t("settings:providers.anthropicUseAuthToken")}
|
||||
</Checkbox>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
126
webview-ui/src/components/settings/providers/Bedrock.tsx
Normal file
126
webview-ui/src/components/settings/providers/Bedrock.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { useCallback } from "react"
|
||||
import { Checkbox } from "vscrui"
|
||||
import { VSCodeTextField, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { ApiConfiguration, ModelInfo } from "@roo/shared/api"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
|
||||
|
||||
import { AWS_REGIONS } from "../constants"
|
||||
import { inputEventTransform, noTransform } from "../transforms"
|
||||
|
||||
type BedrockProps = {
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
|
||||
selectedModelInfo?: ModelInfo
|
||||
}
|
||||
|
||||
export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedModelInfo }: BedrockProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ApiConfiguration, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeRadioGroup
|
||||
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
|
||||
onChange={handleInputChange(
|
||||
"awsUseProfile",
|
||||
(e) => (e.target as HTMLInputElement).value === "profile",
|
||||
)}>
|
||||
<VSCodeRadio value="credentials">{t("settings:providers.awsCredentials")}</VSCodeRadio>
|
||||
<VSCodeRadio value="profile">{t("settings:providers.awsProfile")}</VSCodeRadio>
|
||||
</VSCodeRadioGroup>
|
||||
<div className="text-sm text-vscode-descriptionForeground -mt-3">
|
||||
{t("settings:providers.apiKeyStorageNotice")}
|
||||
</div>
|
||||
{apiConfiguration?.awsUseProfile ? (
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsProfile || ""}
|
||||
onInput={handleInputChange("awsProfile")}
|
||||
placeholder={t("settings:placeholders.profileName")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.awsProfileName")}</label>
|
||||
</VSCodeTextField>
|
||||
) : (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsAccessKey || ""}
|
||||
type="password"
|
||||
onInput={handleInputChange("awsAccessKey")}
|
||||
placeholder={t("settings:placeholders.accessKey")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.awsAccessKey")}</label>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsSecretKey || ""}
|
||||
type="password"
|
||||
onInput={handleInputChange("awsSecretKey")}
|
||||
placeholder={t("settings:placeholders.secretKey")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.awsSecretKey")}</label>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsSessionToken || ""}
|
||||
type="password"
|
||||
onInput={handleInputChange("awsSessionToken")}
|
||||
placeholder={t("settings:placeholders.sessionToken")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.awsSessionToken")}</label>
|
||||
</VSCodeTextField>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<label className="block font-medium mb-1">{t("settings:providers.awsRegion")}</label>
|
||||
<Select
|
||||
value={apiConfiguration?.awsRegion || ""}
|
||||
onValueChange={(value) => setApiConfigurationField("awsRegion", value)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t("settings:common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{AWS_REGIONS.map(({ value, label }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.awsUseCrossRegionInference || false}
|
||||
onChange={handleInputChange("awsUseCrossRegionInference", noTransform)}>
|
||||
{t("settings:providers.awsCrossRegion")}
|
||||
</Checkbox>
|
||||
{selectedModelInfo?.supportsPromptCache && (
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.awsUsePromptCache || false}
|
||||
onChange={handleInputChange("awsUsePromptCache", noTransform)}>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t("settings:providers.enablePromptCaching")}</span>
|
||||
<i
|
||||
className="codicon codicon-info text-vscode-descriptionForeground"
|
||||
title={t("settings:providers.enablePromptCachingTitle")}
|
||||
style={{ fontSize: "12px" }}
|
||||
/>
|
||||
</div>
|
||||
</Checkbox>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm text-vscode-descriptionForeground ml-6 mt-1">
|
||||
{t("settings:providers.cacheUsageNote")}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
77
webview-ui/src/components/settings/providers/Gemini.tsx
Normal file
77
webview-ui/src/components/settings/providers/Gemini.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { useCallback, useState } from "react"
|
||||
import { Checkbox } from "vscrui"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { ApiConfiguration } from "@roo/shared/api"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
|
||||
type GeminiProps = {
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
|
||||
}
|
||||
|
||||
export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const [googleGeminiBaseUrlSelected, setGoogleGeminiBaseUrlSelected] = useState(
|
||||
!!apiConfiguration?.googleGeminiBaseUrl,
|
||||
)
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ApiConfiguration, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.geminiApiKey || ""}
|
||||
type="password"
|
||||
onInput={handleInputChange("geminiApiKey")}
|
||||
placeholder={t("settings:placeholders.apiKey")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.geminiApiKey")}</label>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground -mt-2">
|
||||
{t("settings:providers.apiKeyStorageNotice")}
|
||||
</div>
|
||||
{!apiConfiguration?.geminiApiKey && (
|
||||
<VSCodeButtonLink href="https://ai.google.dev/" appearance="secondary">
|
||||
{t("settings:providers.getGeminiApiKey")}
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={googleGeminiBaseUrlSelected}
|
||||
onChange={(checked: boolean) => {
|
||||
setGoogleGeminiBaseUrlSelected(checked)
|
||||
|
||||
if (!checked) {
|
||||
setApiConfigurationField("googleGeminiBaseUrl", "")
|
||||
}
|
||||
}}>
|
||||
{t("settings:providers.useCustomBaseUrl")}
|
||||
</Checkbox>
|
||||
{googleGeminiBaseUrlSelected && (
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.googleGeminiBaseUrl || ""}
|
||||
type="url"
|
||||
onInput={handleInputChange("googleGeminiBaseUrl")}
|
||||
placeholder={t("settings:defaults.geminiUrl")}
|
||||
className="w-full mt-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
152
webview-ui/src/components/settings/providers/LMStudio.tsx
Normal file
152
webview-ui/src/components/settings/providers/LMStudio.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { useCallback, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { Trans } from "react-i18next"
|
||||
import { Checkbox } from "vscrui"
|
||||
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { ApiConfiguration } from "@roo/shared/api"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
|
||||
type LMStudioProps = {
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
|
||||
}
|
||||
|
||||
export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudioProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const [lmStudioModels, setLmStudioModels] = useState<string[]>([])
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ApiConfiguration, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
const onMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
|
||||
switch (message.type) {
|
||||
case "lmStudioModels":
|
||||
{
|
||||
const newModels = message.lmStudioModels ?? []
|
||||
setLmStudioModels(newModels)
|
||||
}
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("message", onMessage)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.lmStudioBaseUrl || ""}
|
||||
type="url"
|
||||
onInput={handleInputChange("lmStudioBaseUrl")}
|
||||
placeholder={t("settings:defaults.lmStudioUrl")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.lmStudio.baseUrl")}</label>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.lmStudioModelId || ""}
|
||||
onInput={handleInputChange("lmStudioModelId")}
|
||||
placeholder={t("settings:placeholders.modelId.lmStudio")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.lmStudio.modelId")}</label>
|
||||
</VSCodeTextField>
|
||||
{lmStudioModels.length > 0 && (
|
||||
<VSCodeRadioGroup
|
||||
value={
|
||||
lmStudioModels.includes(apiConfiguration?.lmStudioModelId || "")
|
||||
? apiConfiguration?.lmStudioModelId
|
||||
: ""
|
||||
}
|
||||
onChange={handleInputChange("lmStudioModelId")}>
|
||||
{lmStudioModels.map((model) => (
|
||||
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.lmStudioModelId === model}>
|
||||
{model}
|
||||
</VSCodeRadio>
|
||||
))}
|
||||
</VSCodeRadioGroup>
|
||||
)}
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.lmStudioSpeculativeDecodingEnabled === true}
|
||||
onChange={(checked) => {
|
||||
setApiConfigurationField("lmStudioSpeculativeDecodingEnabled", checked)
|
||||
}}>
|
||||
{t("settings:providers.lmStudio.speculativeDecoding")}
|
||||
</Checkbox>
|
||||
{apiConfiguration?.lmStudioSpeculativeDecodingEnabled && (
|
||||
<>
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.lmStudioDraftModelId || ""}
|
||||
onInput={handleInputChange("lmStudioDraftModelId")}
|
||||
placeholder={t("settings:placeholders.modelId.lmStudioDraft")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">
|
||||
{t("settings:providers.lmStudio.draftModelId")}
|
||||
</label>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:providers.lmStudio.draftModelDesc")}
|
||||
</div>
|
||||
</div>
|
||||
{lmStudioModels.length > 0 && (
|
||||
<>
|
||||
<div className="font-medium">{t("settings:providers.lmStudio.selectDraftModel")}</div>
|
||||
<VSCodeRadioGroup
|
||||
value={
|
||||
lmStudioModels.includes(apiConfiguration?.lmStudioDraftModelId || "")
|
||||
? apiConfiguration?.lmStudioDraftModelId
|
||||
: ""
|
||||
}
|
||||
onChange={handleInputChange("lmStudioDraftModelId")}>
|
||||
{lmStudioModels.map((model) => (
|
||||
<VSCodeRadio key={`draft-${model}`} value={model}>
|
||||
{model}
|
||||
</VSCodeRadio>
|
||||
))}
|
||||
</VSCodeRadioGroup>
|
||||
{lmStudioModels.length === 0 && (
|
||||
<div
|
||||
className="text-sm rounded-xs p-2"
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-inputValidation-infoBackground)",
|
||||
border: "1px solid var(--vscode-inputValidation-infoBorder)",
|
||||
color: "var(--vscode-inputValidation-infoForeground)",
|
||||
}}>
|
||||
{t("settings:providers.lmStudio.noModelsFound")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
<Trans
|
||||
i18nKey="settings:providers.lmStudio.description"
|
||||
components={{
|
||||
a: <VSCodeLink href="https://lmstudio.ai/docs" />,
|
||||
b: <VSCodeLink href="https://lmstudio.ai/docs/basics/server" />,
|
||||
span: (
|
||||
<span className="text-vscode-errorForeground ml-1">
|
||||
<span className="font-medium">Note:</span>
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
86
webview-ui/src/components/settings/providers/Ollama.tsx
Normal file
86
webview-ui/src/components/settings/providers/Ollama.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { useState, useCallback } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { ApiConfiguration } from "@roo/shared/api"
|
||||
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
|
||||
type OllamaProps = {
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
|
||||
}
|
||||
|
||||
export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const [ollamaModels, setOllamaModels] = useState<string[]>([])
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ApiConfiguration, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
const onMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
|
||||
switch (message.type) {
|
||||
case "ollamaModels":
|
||||
{
|
||||
const newModels = message.ollamaModels ?? []
|
||||
setOllamaModels(newModels)
|
||||
}
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("message", onMessage)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.ollamaBaseUrl || ""}
|
||||
type="url"
|
||||
onInput={handleInputChange("ollamaBaseUrl")}
|
||||
placeholder={t("settings:defaults.ollamaUrl")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.ollama.baseUrl")}</label>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.ollamaModelId || ""}
|
||||
onInput={handleInputChange("ollamaModelId")}
|
||||
placeholder={t("settings:placeholders.modelId.ollama")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.ollama.modelId")}</label>
|
||||
</VSCodeTextField>
|
||||
{ollamaModels.length > 0 && (
|
||||
<VSCodeRadioGroup
|
||||
value={
|
||||
ollamaModels.includes(apiConfiguration?.ollamaModelId || "")
|
||||
? apiConfiguration?.ollamaModelId
|
||||
: ""
|
||||
}
|
||||
onChange={handleInputChange("ollamaModelId")}>
|
||||
{ollamaModels.map((model) => (
|
||||
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.ollamaModelId === model}>
|
||||
{model}
|
||||
</VSCodeRadio>
|
||||
))}
|
||||
</VSCodeRadioGroup>
|
||||
)}
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:providers.ollama.description")}
|
||||
<span className="text-vscode-errorForeground ml-1">{t("settings:providers.ollama.warning")}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
77
webview-ui/src/components/settings/providers/OpenAI.tsx
Normal file
77
webview-ui/src/components/settings/providers/OpenAI.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { useCallback, useState } from "react"
|
||||
import { Checkbox } from "vscrui"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { ApiConfiguration } from "@roo/shared/api"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
|
||||
type OpenAIProps = {
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
|
||||
}
|
||||
|
||||
export const OpenAI = ({ apiConfiguration, setApiConfigurationField }: OpenAIProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const [openAiNativeBaseUrlSelected, setOpenAiNativeBaseUrlSelected] = useState(
|
||||
!!apiConfiguration?.openAiNativeBaseUrl,
|
||||
)
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ApiConfiguration, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Checkbox
|
||||
checked={openAiNativeBaseUrlSelected}
|
||||
onChange={(checked: boolean) => {
|
||||
setOpenAiNativeBaseUrlSelected(checked)
|
||||
|
||||
if (!checked) {
|
||||
setApiConfigurationField("openAiNativeBaseUrl", "")
|
||||
}
|
||||
}}>
|
||||
{t("settings:providers.useCustomBaseUrl")}
|
||||
</Checkbox>
|
||||
{openAiNativeBaseUrlSelected && (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiNativeBaseUrl || ""}
|
||||
type="url"
|
||||
onInput={handleInputChange("openAiNativeBaseUrl")}
|
||||
placeholder="https://api.openai.com/v1"
|
||||
className="w-full mt-1"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiNativeApiKey || ""}
|
||||
type="password"
|
||||
onInput={handleInputChange("openAiNativeApiKey")}
|
||||
placeholder={t("settings:placeholders.apiKey")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.openAiApiKey")}</label>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground -mt-2">
|
||||
{t("settings:providers.apiKeyStorageNotice")}
|
||||
</div>
|
||||
{!apiConfiguration?.openAiNativeApiKey && (
|
||||
<VSCodeButtonLink href="https://platform.openai.com/api-keys" appearance="secondary">
|
||||
{t("settings:providers.getOpenAiApiKey")}
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,578 @@
|
|||
import { useState, useCallback } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { Checkbox } from "vscrui"
|
||||
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { ModelInfo, ReasoningEffort as ReasoningEffortType } from "@roo/schemas"
|
||||
import { ApiConfiguration, azureOpenAiDefaultApiVersion, openAiModelInfoSaneDefaults } from "@roo/shared/api"
|
||||
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { Button } from "@src/components/ui"
|
||||
|
||||
import { inputEventTransform, noTransform } from "../transforms"
|
||||
import { ModelPicker } from "../ModelPicker"
|
||||
import { R1FormatSetting } from "../R1FormatSetting"
|
||||
import { ReasoningEffort } from "../ReasoningEffort"
|
||||
|
||||
type OpenAICompatibleProps = {
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
|
||||
}
|
||||
|
||||
export const OpenAICompatible = ({ apiConfiguration, setApiConfigurationField }: OpenAICompatibleProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
|
||||
const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState(!!apiConfiguration?.openAiLegacyFormat)
|
||||
|
||||
const [openAiModels, setOpenAiModels] = useState<Record<string, ModelInfo> | null>(null)
|
||||
|
||||
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
|
||||
const headers = apiConfiguration?.openAiHeaders || {}
|
||||
return Object.entries(headers)
|
||||
})
|
||||
|
||||
const handleAddCustomHeader = useCallback(() => {
|
||||
// Only update the local state to show the new row in the UI.
|
||||
setCustomHeaders((prev) => [...prev, ["", ""]])
|
||||
// Do not update the main configuration yet, wait for user input.
|
||||
}, [])
|
||||
|
||||
const handleUpdateHeaderKey = useCallback((index: number, newKey: string) => {
|
||||
setCustomHeaders((prev) => {
|
||||
const updated = [...prev]
|
||||
|
||||
if (updated[index]) {
|
||||
updated[index] = [newKey, updated[index][1]]
|
||||
}
|
||||
|
||||
return updated
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleUpdateHeaderValue = useCallback((index: number, newValue: string) => {
|
||||
setCustomHeaders((prev) => {
|
||||
const updated = [...prev]
|
||||
|
||||
if (updated[index]) {
|
||||
updated[index] = [updated[index][0], newValue]
|
||||
}
|
||||
|
||||
return updated
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleRemoveCustomHeader = useCallback((index: number) => {
|
||||
setCustomHeaders((prev) => prev.filter((_, i) => i !== index))
|
||||
}, [])
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ApiConfiguration, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
const onMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
|
||||
switch (message.type) {
|
||||
case "openAiModels": {
|
||||
const updatedModels = message.openAiModels ?? []
|
||||
setOpenAiModels(Object.fromEntries(updatedModels.map((item) => [item, openAiModelInfoSaneDefaults])))
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("message", onMessage)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiBaseUrl || ""}
|
||||
type="url"
|
||||
onInput={handleInputChange("openAiBaseUrl")}
|
||||
placeholder={t("settings:placeholders.baseUrl")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.openAiBaseUrl")}</label>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiApiKey || ""}
|
||||
type="password"
|
||||
onInput={handleInputChange("openAiApiKey")}
|
||||
placeholder={t("settings:placeholders.apiKey")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.openAiApiKey")}</label>
|
||||
</VSCodeTextField>
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId="gpt-4o"
|
||||
models={openAiModels}
|
||||
modelIdKey="openAiModelId"
|
||||
serviceName="OpenAI"
|
||||
serviceUrl="https://platform.openai.com"
|
||||
/>
|
||||
<R1FormatSetting
|
||||
onChange={handleInputChange("openAiR1FormatEnabled", noTransform)}
|
||||
openAiR1FormatEnabled={apiConfiguration?.openAiR1FormatEnabled ?? false}
|
||||
/>
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={openAiLegacyFormatSelected}
|
||||
onChange={(checked: boolean) => {
|
||||
setOpenAiLegacyFormatSelected(checked)
|
||||
setApiConfigurationField("openAiLegacyFormat", checked)
|
||||
}}>
|
||||
{t("settings:providers.useLegacyFormat")}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.openAiStreamingEnabled ?? true}
|
||||
onChange={handleInputChange("openAiStreamingEnabled", noTransform)}>
|
||||
{t("settings:modelInfo.enableStreaming")}
|
||||
</Checkbox>
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.openAiUseAzure ?? false}
|
||||
onChange={handleInputChange("openAiUseAzure", noTransform)}>
|
||||
{t("settings:modelInfo.useAzure")}
|
||||
</Checkbox>
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={azureApiVersionSelected}
|
||||
onChange={(checked: boolean) => {
|
||||
setAzureApiVersionSelected(checked)
|
||||
|
||||
if (!checked) {
|
||||
setApiConfigurationField("azureApiVersion", "")
|
||||
}
|
||||
}}>
|
||||
{t("settings:modelInfo.azureApiVersion")}
|
||||
</Checkbox>
|
||||
{azureApiVersionSelected && (
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.azureApiVersion || ""}
|
||||
onInput={handleInputChange("azureApiVersion")}
|
||||
placeholder={`Default: ${azureOpenAiDefaultApiVersion}`}
|
||||
className="w-full mt-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Custom Headers UI */}
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="block font-medium">{t("settings:providers.customHeaders")}</label>
|
||||
<VSCodeButton appearance="icon" title={t("settings:common.add")} onClick={handleAddCustomHeader}>
|
||||
<span className="codicon codicon-add"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
{!customHeaders.length ? (
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:providers.noCustomHeaders")}
|
||||
</div>
|
||||
) : (
|
||||
customHeaders.map(([key, value], index) => (
|
||||
<div key={index} className="flex items-center mb-2">
|
||||
<VSCodeTextField
|
||||
value={key}
|
||||
className="flex-1 mr-2"
|
||||
placeholder={t("settings:providers.headerName")}
|
||||
onInput={(e: any) => handleUpdateHeaderKey(index, e.target.value)}
|
||||
/>
|
||||
<VSCodeTextField
|
||||
value={value}
|
||||
className="flex-1 mr-2"
|
||||
placeholder={t("settings:providers.headerValue")}
|
||||
onInput={(e: any) => handleUpdateHeaderValue(index, e.target.value)}
|
||||
/>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
title={t("settings:common.remove")}
|
||||
onClick={() => handleRemoveCustomHeader(index)}>
|
||||
<span className="codicon codicon-trash"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Checkbox
|
||||
checked={apiConfiguration.enableReasoningEffort ?? false}
|
||||
onChange={(checked: boolean) => {
|
||||
setApiConfigurationField("enableReasoningEffort", checked)
|
||||
|
||||
if (!checked) {
|
||||
const { reasoningEffort: _, ...openAiCustomModelInfo } =
|
||||
apiConfiguration.openAiCustomModelInfo || openAiModelInfoSaneDefaults
|
||||
|
||||
setApiConfigurationField("openAiCustomModelInfo", openAiCustomModelInfo)
|
||||
}
|
||||
}}>
|
||||
{t("settings:providers.setReasoningLevel")}
|
||||
</Checkbox>
|
||||
{!!apiConfiguration.enableReasoningEffort && (
|
||||
<ReasoningEffort
|
||||
apiConfiguration={{
|
||||
...apiConfiguration,
|
||||
reasoningEffort: apiConfiguration.openAiCustomModelInfo?.reasoningEffort,
|
||||
}}
|
||||
setApiConfigurationField={(field, value) => {
|
||||
if (field === "reasoningEffort") {
|
||||
const openAiCustomModelInfo =
|
||||
apiConfiguration.openAiCustomModelInfo || openAiModelInfoSaneDefaults
|
||||
|
||||
setApiConfigurationField("openAiCustomModelInfo", {
|
||||
...openAiCustomModelInfo,
|
||||
reasoningEffort: value as ReasoningEffortType,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="text-sm text-vscode-descriptionForeground whitespace-pre-line">
|
||||
{t("settings:providers.customModel.capabilities")}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiCustomModelInfo?.maxTokens?.toString() ||
|
||||
openAiModelInfoSaneDefaults.maxTokens?.toString() ||
|
||||
""
|
||||
}
|
||||
type="text"
|
||||
style={{
|
||||
borderColor: (() => {
|
||||
const value = apiConfiguration?.openAiCustomModelInfo?.maxTokens
|
||||
|
||||
if (!value) {
|
||||
return "var(--vscode-input-border)"
|
||||
}
|
||||
|
||||
return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
|
||||
})(),
|
||||
}}
|
||||
title={t("settings:providers.customModel.maxTokens.description")}
|
||||
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = parseInt((e.target as HTMLInputElement).value)
|
||||
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
|
||||
maxTokens: isNaN(value) ? undefined : value,
|
||||
}
|
||||
})}
|
||||
placeholder={t("settings:placeholders.numbers.maxTokens")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">
|
||||
{t("settings:providers.customModel.maxTokens.label")}
|
||||
</label>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:providers.customModel.maxTokens.description")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiCustomModelInfo?.contextWindow?.toString() ||
|
||||
openAiModelInfoSaneDefaults.contextWindow?.toString() ||
|
||||
""
|
||||
}
|
||||
type="text"
|
||||
style={{
|
||||
borderColor: (() => {
|
||||
const value = apiConfiguration?.openAiCustomModelInfo?.contextWindow
|
||||
|
||||
if (!value) {
|
||||
return "var(--vscode-input-border)"
|
||||
}
|
||||
|
||||
return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
|
||||
})(),
|
||||
}}
|
||||
title={t("settings:providers.customModel.contextWindow.description")}
|
||||
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
const parsed = parseInt(value)
|
||||
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
|
||||
contextWindow: isNaN(parsed) ? openAiModelInfoSaneDefaults.contextWindow : parsed,
|
||||
}
|
||||
})}
|
||||
placeholder={t("settings:placeholders.numbers.contextWindow")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">
|
||||
{t("settings:providers.customModel.contextWindow.label")}
|
||||
</label>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:providers.customModel.contextWindow.description")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Checkbox
|
||||
checked={
|
||||
apiConfiguration?.openAiCustomModelInfo?.supportsImages ??
|
||||
openAiModelInfoSaneDefaults.supportsImages
|
||||
}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (checked) => {
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
|
||||
supportsImages: checked,
|
||||
}
|
||||
})}>
|
||||
<span className="font-medium">
|
||||
{t("settings:providers.customModel.imageSupport.label")}
|
||||
</span>
|
||||
</Checkbox>
|
||||
<i
|
||||
className="codicon codicon-info text-vscode-descriptionForeground"
|
||||
title={t("settings:providers.customModel.imageSupport.description")}
|
||||
style={{ fontSize: "12px" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-vscode-descriptionForeground pt-1">
|
||||
{t("settings:providers.customModel.imageSupport.description")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.openAiCustomModelInfo?.supportsComputerUse ?? false}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (checked) => {
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
|
||||
supportsComputerUse: checked,
|
||||
}
|
||||
})}>
|
||||
<span className="font-medium">{t("settings:providers.customModel.computerUse.label")}</span>
|
||||
</Checkbox>
|
||||
<i
|
||||
className="codicon codicon-info text-vscode-descriptionForeground"
|
||||
title={t("settings:providers.customModel.computerUse.description")}
|
||||
style={{ fontSize: "12px" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-vscode-descriptionForeground pt-1">
|
||||
{t("settings:providers.customModel.computerUse.description")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.openAiCustomModelInfo?.supportsPromptCache ?? false}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (checked) => {
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
|
||||
supportsPromptCache: checked,
|
||||
}
|
||||
})}>
|
||||
<span className="font-medium">{t("settings:providers.customModel.promptCache.label")}</span>
|
||||
</Checkbox>
|
||||
<i
|
||||
className="codicon codicon-info text-vscode-descriptionForeground"
|
||||
title={t("settings:providers.customModel.promptCache.description")}
|
||||
style={{ fontSize: "12px" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-vscode-descriptionForeground pt-1">
|
||||
{t("settings:providers.customModel.promptCache.description")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiCustomModelInfo?.inputPrice?.toString() ??
|
||||
openAiModelInfoSaneDefaults.inputPrice?.toString() ??
|
||||
""
|
||||
}
|
||||
type="text"
|
||||
style={{
|
||||
borderColor: (() => {
|
||||
const value = apiConfiguration?.openAiCustomModelInfo?.inputPrice
|
||||
|
||||
if (!value && value !== 0) {
|
||||
return "var(--vscode-input-border)"
|
||||
}
|
||||
|
||||
return value >= 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
|
||||
})(),
|
||||
}}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
const parsed = parseFloat(value)
|
||||
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults),
|
||||
inputPrice: isNaN(parsed) ? openAiModelInfoSaneDefaults.inputPrice : parsed,
|
||||
}
|
||||
})}
|
||||
placeholder={t("settings:placeholders.numbers.inputPrice")}
|
||||
className="w-full">
|
||||
<div className="flex items-center gap-1">
|
||||
<label className="block font-medium mb-1">
|
||||
{t("settings:providers.customModel.pricing.input.label")}
|
||||
</label>
|
||||
<i
|
||||
className="codicon codicon-info text-vscode-descriptionForeground"
|
||||
title={t("settings:providers.customModel.pricing.input.description")}
|
||||
style={{ fontSize: "12px" }}
|
||||
/>
|
||||
</div>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={
|
||||
apiConfiguration?.openAiCustomModelInfo?.outputPrice?.toString() ||
|
||||
openAiModelInfoSaneDefaults.outputPrice?.toString() ||
|
||||
""
|
||||
}
|
||||
type="text"
|
||||
style={{
|
||||
borderColor: (() => {
|
||||
const value = apiConfiguration?.openAiCustomModelInfo?.outputPrice
|
||||
|
||||
if (!value && value !== 0) {
|
||||
return "var(--vscode-input-border)"
|
||||
}
|
||||
|
||||
return value >= 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
|
||||
})(),
|
||||
}}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
const parsed = parseFloat(value)
|
||||
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
|
||||
outputPrice: isNaN(parsed) ? openAiModelInfoSaneDefaults.outputPrice : parsed,
|
||||
}
|
||||
})}
|
||||
placeholder={t("settings:placeholders.numbers.outputPrice")}
|
||||
className="w-full">
|
||||
<div className="flex items-center gap-1">
|
||||
<label className="block font-medium mb-1">
|
||||
{t("settings:providers.customModel.pricing.output.label")}
|
||||
</label>
|
||||
<i
|
||||
className="codicon codicon-info text-vscode-descriptionForeground"
|
||||
title={t("settings:providers.customModel.pricing.output.description")}
|
||||
style={{ fontSize: "12px" }}
|
||||
/>
|
||||
</div>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
|
||||
{apiConfiguration?.openAiCustomModelInfo?.supportsPromptCache && (
|
||||
<>
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiCustomModelInfo?.cacheReadsPrice?.toString() ?? "0"}
|
||||
type="text"
|
||||
style={{
|
||||
borderColor: (() => {
|
||||
const value = apiConfiguration?.openAiCustomModelInfo?.cacheReadsPrice
|
||||
|
||||
if (!value && value !== 0) {
|
||||
return "var(--vscode-input-border)"
|
||||
}
|
||||
|
||||
return value >= 0
|
||||
? "var(--vscode-charts-green)"
|
||||
: "var(--vscode-errorForeground)"
|
||||
})(),
|
||||
}}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
const parsed = parseFloat(value)
|
||||
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults),
|
||||
cacheReadsPrice: isNaN(parsed) ? 0 : parsed,
|
||||
}
|
||||
})}
|
||||
placeholder={t("settings:placeholders.numbers.inputPrice")}
|
||||
className="w-full">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium">
|
||||
{t("settings:providers.customModel.pricing.cacheReads.label")}
|
||||
</span>
|
||||
<i
|
||||
className="codicon codicon-info text-vscode-descriptionForeground"
|
||||
title={t("settings:providers.customModel.pricing.cacheReads.description")}
|
||||
style={{ fontSize: "12px" }}
|
||||
/>
|
||||
</div>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openAiCustomModelInfo?.cacheWritesPrice?.toString() ?? "0"}
|
||||
type="text"
|
||||
style={{
|
||||
borderColor: (() => {
|
||||
const value = apiConfiguration?.openAiCustomModelInfo?.cacheWritesPrice
|
||||
|
||||
if (!value && value !== 0) {
|
||||
return "var(--vscode-input-border)"
|
||||
}
|
||||
|
||||
return value >= 0
|
||||
? "var(--vscode-charts-green)"
|
||||
: "var(--vscode-errorForeground)"
|
||||
})(),
|
||||
}}
|
||||
onChange={handleInputChange("openAiCustomModelInfo", (e) => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
const parsed = parseFloat(value)
|
||||
|
||||
return {
|
||||
...(apiConfiguration?.openAiCustomModelInfo ?? openAiModelInfoSaneDefaults),
|
||||
cacheWritesPrice: isNaN(parsed) ? 0 : parsed,
|
||||
}
|
||||
})}
|
||||
placeholder={t("settings:placeholders.numbers.cacheWritePrice")}
|
||||
className="w-full">
|
||||
<div className="flex items-center gap-1">
|
||||
<label className="block font-medium mb-1">
|
||||
{t("settings:providers.customModel.pricing.cacheWrites.label")}
|
||||
</label>
|
||||
<i
|
||||
className="codicon codicon-info text-vscode-descriptionForeground"
|
||||
title={t("settings:providers.customModel.pricing.cacheWrites.description")}
|
||||
style={{ fontSize: "12px" }}
|
||||
/>
|
||||
</div>
|
||||
</VSCodeTextField>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setApiConfigurationField("openAiCustomModelInfo", openAiModelInfoSaneDefaults)}>
|
||||
{t("settings:providers.customModel.resetDefaults")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
109
webview-ui/src/components/settings/providers/OpenRouter.tsx
Normal file
109
webview-ui/src/components/settings/providers/OpenRouter.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { useCallback, useState } from "react"
|
||||
import { Trans } from "react-i18next"
|
||||
import { Checkbox } from "vscrui"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { ApiConfiguration } from "@roo/shared/api"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { getOpenRouterAuthUrl } from "@src/oauth/urls"
|
||||
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
|
||||
|
||||
import { inputEventTransform, noTransform } from "../transforms"
|
||||
|
||||
import { OpenRouterBalanceDisplay } from "./OpenRouterBalanceDisplay"
|
||||
|
||||
type OpenRouterProps = {
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
|
||||
uriScheme: string | undefined
|
||||
fromWelcomeView?: boolean
|
||||
}
|
||||
|
||||
export const OpenRouter = ({
|
||||
apiConfiguration,
|
||||
setApiConfigurationField,
|
||||
uriScheme,
|
||||
fromWelcomeView,
|
||||
}: OpenRouterProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl)
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ApiConfiguration, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openRouterApiKey || ""}
|
||||
type="password"
|
||||
onInput={handleInputChange("openRouterApiKey")}
|
||||
placeholder={t("settings:placeholders.apiKey")}
|
||||
className="w-full">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="block font-medium">{t("settings:providers.openRouterApiKey")}</label>
|
||||
{apiConfiguration?.openRouterApiKey && (
|
||||
<OpenRouterBalanceDisplay
|
||||
apiKey={apiConfiguration.openRouterApiKey}
|
||||
baseUrl={apiConfiguration.openRouterBaseUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground -mt-2">
|
||||
{t("settings:providers.apiKeyStorageNotice")}
|
||||
</div>
|
||||
{!apiConfiguration?.openRouterApiKey && (
|
||||
<VSCodeButtonLink href={getOpenRouterAuthUrl(uriScheme)} style={{ width: "100%" }} appearance="primary">
|
||||
{t("settings:providers.getOpenRouterApiKey")}
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
{!fromWelcomeView && (
|
||||
<>
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={openRouterBaseUrlSelected}
|
||||
onChange={(checked: boolean) => {
|
||||
setOpenRouterBaseUrlSelected(checked)
|
||||
|
||||
if (!checked) {
|
||||
setApiConfigurationField("openRouterBaseUrl", "")
|
||||
}
|
||||
}}>
|
||||
{t("settings:providers.useCustomBaseUrl")}
|
||||
</Checkbox>
|
||||
{openRouterBaseUrlSelected && (
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.openRouterBaseUrl || ""}
|
||||
type="url"
|
||||
onInput={handleInputChange("openRouterBaseUrl")}
|
||||
placeholder="Default: https://openrouter.ai/api/v1"
|
||||
className="w-full mt-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.openRouterUseMiddleOutTransform ?? true}
|
||||
onChange={handleInputChange("openRouterUseMiddleOutTransform", noTransform)}>
|
||||
<Trans
|
||||
i18nKey="settings:providers.openRouterTransformsText"
|
||||
components={{
|
||||
// eslint-disable-next-line jsx-a11y/anchor-has-content
|
||||
a: <a href="https://openrouter.ai/docs/transforms" />,
|
||||
}}
|
||||
/>
|
||||
</Checkbox>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
86
webview-ui/src/components/settings/providers/VSCodeLM.tsx
Normal file
86
webview-ui/src/components/settings/providers/VSCodeLM.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { useState, useCallback } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { LanguageModelChatSelector } from "vscode"
|
||||
|
||||
import { ApiConfiguration } from "@roo/shared/api"
|
||||
import { ExtensionMessage } from "@roo/shared/ExtensionMessage"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
|
||||
type VSCodeLMProps = {
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
|
||||
}
|
||||
|
||||
export const VSCodeLM = ({ apiConfiguration, setApiConfigurationField }: VSCodeLMProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const [vsCodeLmModels, setVsCodeLmModels] = useState<LanguageModelChatSelector[]>([])
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ApiConfiguration, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
const onMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
|
||||
switch (message.type) {
|
||||
case "vsCodeLmModels":
|
||||
{
|
||||
const newModels = message.vsCodeLmModels ?? []
|
||||
setVsCodeLmModels(newModels)
|
||||
}
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEvent("message", onMessage)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label className="block font-medium mb-1">{t("settings:providers.vscodeLmModel")}</label>
|
||||
{vsCodeLmModels.length > 0 ? (
|
||||
<Select
|
||||
value={
|
||||
apiConfiguration?.vsCodeLmModelSelector
|
||||
? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
|
||||
: ""
|
||||
}
|
||||
onValueChange={handleInputChange("vsCodeLmModelSelector", (value) => {
|
||||
const [vendor, family] = value.split("/")
|
||||
return { vendor, family }
|
||||
})}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t("settings:common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{vsCodeLmModels.map((model) => (
|
||||
<SelectItem
|
||||
key={`${model.vendor}/${model.family}`}
|
||||
value={`${model.vendor}/${model.family}`}>
|
||||
{`${model.vendor} - ${model.family}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:providers.vscodeLmDescription")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-vscode-errorForeground">{t("settings:providers.vscodeLmWarning")}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
97
webview-ui/src/components/settings/providers/Vertex.tsx
Normal file
97
webview-ui/src/components/settings/providers/Vertex.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { useCallback } from "react"
|
||||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { ApiConfiguration } from "@roo/shared/api"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
import { VERTEX_REGIONS } from "../constants"
|
||||
|
||||
type VertexProps = {
|
||||
apiConfiguration: ApiConfiguration
|
||||
setApiConfigurationField: (field: keyof ApiConfiguration, value: ApiConfiguration[keyof ApiConfiguration]) => void
|
||||
}
|
||||
|
||||
export const Vertex = ({ apiConfiguration, setApiConfigurationField }: VertexProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ApiConfiguration, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ApiConfiguration[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
<div>{t("settings:providers.googleCloudSetup.title")}</div>
|
||||
<div>
|
||||
<VSCodeLink
|
||||
href="https://cloud.google.com/vertex-ai/generative-ai/docs/partner-models/use-claude#before_you_begin"
|
||||
className="text-sm">
|
||||
{t("settings:providers.googleCloudSetup.step1")}
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
<div>
|
||||
<VSCodeLink
|
||||
href="https://cloud.google.com/docs/authentication/provide-credentials-adc#google-idp"
|
||||
className="text-sm">
|
||||
{t("settings:providers.googleCloudSetup.step2")}
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
<div>
|
||||
<VSCodeLink
|
||||
href="https://developers.google.com/workspace/guides/create-credentials?hl=en#service-account"
|
||||
className="text-sm">
|
||||
{t("settings:providers.googleCloudSetup.step3")}
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
</div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.vertexJsonCredentials || ""}
|
||||
onInput={handleInputChange("vertexJsonCredentials")}
|
||||
placeholder={t("settings:placeholders.credentialsJson")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.googleCloudCredentials")}</label>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.vertexKeyFile || ""}
|
||||
onInput={handleInputChange("vertexKeyFile")}
|
||||
placeholder={t("settings:placeholders.keyFilePath")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.googleCloudKeyFile")}</label>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.vertexProjectId || ""}
|
||||
onInput={handleInputChange("vertexProjectId")}
|
||||
placeholder={t("settings:placeholders.projectId")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.googleCloudProjectId")}</label>
|
||||
</VSCodeTextField>
|
||||
<div>
|
||||
<label className="block font-medium mb-1">{t("settings:providers.googleCloudRegion")}</label>
|
||||
<Select
|
||||
value={apiConfiguration?.vertexRegion || ""}
|
||||
onValueChange={(value) => setApiConfigurationField("vertexRegion", value)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t("settings:common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{VERTEX_REGIONS.map(({ value, label }) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
3
webview-ui/src/components/settings/transforms.ts
Normal file
3
webview-ui/src/components/settings/transforms.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const noTransform = <T>(value: T) => value
|
||||
|
||||
export const inputEventTransform = <E>(event: E) => (event as { target: HTMLInputElement })?.target?.value as any
|
||||
Loading…
Add table
Reference in a new issue